This commit is contained in:
renzhiyuan 2026-04-17 03:31:16 +08:00
parent 7eda42b8a7
commit 264788d3ec
19 changed files with 710 additions and 176 deletions

165
a_test.go Normal file

File diff suppressed because one or more lines are too long

View File

@ -42,7 +42,7 @@ func InitializeApp(configConfig *config.Config, allLogger log.AllLogger) (*serve
}
productBiz := biz.NewProductBiz(productImpl, productSourceImpl, configConfig, oss)
productService := service.NewProductService(configConfig, productImpl, authBiz, productBiz)
aiBiz := biz.NewAiBiz(platImpl)
aiBiz := biz.NewAiBiz(platImpl, articleTypeImpl)
productSourceService := service.NewProductSourceService(configConfig, productImpl, authBiz, aiBiz, productBiz, productSourceImpl, publishBiz, articleTypeImpl)
appModule := router.NewAppModule(configConfig, appService, loginService, publishService, productService, productSourceService)
routerServer := router.NewRouterServer(appModule)

View File

@ -1 +0,0 @@
[{"name":"webId","value":"f840cc8b10e0a01b0bdb839482af19d1","domain":".xiaohongshu.com","path":"/","expires":1807155738,"size":37,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"gid","value":"yjfKDJiJ0J7SyjfKDJi8Dqf884ufUMYf3MlIVqfYTYl3D3q8Svu0VW888yyYW4Y8JD0j8Yd2","domain":".xiaohongshu.com","path":"/","expires":1810372429.581067,"size":75,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"x-user-id-creator.xiaohongshu.com","value":"65d74a4c0000000005032a98","domain":".xiaohongshu.com","path":"/","expires":1810179755.091531,"size":57,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"galaxy_creator_session_id","value":"nWC5PzTFCCSJsLYiRFLFORIWUoUf5c4Egr3v","domain":".xiaohongshu.com","path":"/","expires":1778211755.091586,"size":61,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"acw_tc","value":"0a00075317759153837631021e2d5e31fa378daf9c77578ea082108e84e3cf","domain":"creator.xiaohongshu.com","path":"/","expires":1775916920.153024,"size":68,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"loadts","value":"1775915120761","domain":".xiaohongshu.com","path":"/","expires":1807451120,"size":19,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"websectiga","value":"cffd9dcea65962b05ab048ac76962acee933d26157113bb213105a116241fa6c","domain":".xiaohongshu.com","path":"/","expires":1776174321,"size":74,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"xsecappid","value":"ugc","domain":".xiaohongshu.com","path":"/","expires":1807451120,"size":12,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"customerClientId","value":"231145420384063","domain":".xiaohongshu.com","path":"/","expires":1810179755.091552,"size":31,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"customer-sso-sid","value":"68c5176262287866115194944uxxdffhcemiwvwt","domain":".xiaohongshu.com","path":"/","expires":1776224554.091467,"size":56,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"ets","value":"1775619738206","domain":".xiaohongshu.com","path":"/","expires":1778211738.206388,"size":16,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"a1","value":"19d6b2f0b04zdps8dison8k8oibcyzaju3ji7d03d30000118748","domain":".xiaohongshu.com","path":"/","expires":1807155738,"size":54,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"sec_poison_id","value":"d361d271-b86e-4565-8105-4f11e54cd97c","domain":".xiaohongshu.com","path":"/","expires":1775915726,"size":49,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"access-token-creator.xiaohongshu.com","value":"customer.creator.AT-68c517626228786611519495vggizcwmifuvnaaw","domain":".xiaohongshu.com","path":"/","expires":1778211754.091569,"size":96,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"galaxy.creator.beaker.session.id","value":"1775619757404082990054","domain":".xiaohongshu.com","path":"/","expires":1778211755.091605,"size":54,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443}]

View File

@ -3,22 +3,22 @@ package biz
import (
"context"
"geo/internal/data/impl"
"geo/internal/data/model"
"geo/internal/entitys"
"geo/pkg"
volmodle "github.com/volcengine/volcengine-go-sdk/service/arkruntime/model"
"github.com/volcengine/volcengine-go-sdk/volcengine"
"xorm.io/builder"
)
type AiBiz struct {
platImpl *impl.PlatImpl
platImpl *impl.PlatImpl
articleImpl *impl.ArticleTypeImpl
}
func NewAiBiz(platImpl *impl.PlatImpl) *AiBiz {
func NewAiBiz(platImpl *impl.PlatImpl, articleImpl *impl.ArticleTypeImpl) *AiBiz {
return &AiBiz{
platImpl: platImpl,
platImpl: platImpl,
articleImpl: articleImpl,
}
}
@ -31,32 +31,57 @@ func (a *AiBiz) CreateArticlePrompt(ctx context.Context, data *entitys.BotChat)
},
},
}
var plats []*model.Plat
cond := builder.NewCond().
And(builder.Eq{"plat_type": 1}).
And(builder.Eq{"status": 1})
_, err := a.platImpl.GetListToStruct(ctx, &cond, nil, &plats, "id asc")
if err != nil {
return mes
}
var platList = &entitys.PlatList{
Desc: "平台列表",
PlatItem: make([]*entitys.PlatItem, 0, len(plats)),
}
for _, plat := range plats {
platList.PlatItem = append(platList.PlatItem, &entitys.PlatItem{
Platform: plat.Name,
PlatformIndex: plat.Index,
})
}
if len(platList.PlatItem) > 0 {
mes = append(mes, &volmodle.ChatCompletionMessage{
Role: volmodle.ChatMessageRoleSystem,
Content: &volmodle.ChatCompletionMessageContent{
StringValue: volcengine.String(pkg.JsonStringIgonErr(platList)),
},
})
}
return mes
//var plats []*model.Plat
//cond := builder.NewCond().
// And(builder.Eq{"plat_type": 1}).
// And(builder.Eq{"status": 1})
//_, err := a.platImpl.GetListToStruct(ctx, &cond, nil, &plats, "id asc")
//if err != nil {
// return mes
//}
//var platList = &entitys.PlatList{
// Desc: "平台列表",
// PlatItem: make([]*entitys.PlatItem, 0, len(plats)),
//}
//for _, plat := range plats {
// platList.PlatItem = append(platList.PlatItem, &entitys.PlatItem{
// Platform: plat.Name,
// PlatformIndex: plat.Index,
// })
//}
//if len(platList.PlatItem) > 0 {
// mes = append(mes, &volmodle.ChatCompletionMessage{
// Role: volmodle.ChatMessageRoleAssistant,
// Content: &volmodle.ChatCompletionMessageContent{
// StringValue: volcengine.String(pkg.JsonStringIgonErr(platList)),
// },
// })
//}
//var articleType []*model.ArticleType
//cond := builder.NewCond().
// And(builder.Eq{"status": 1})
//_, err := a.articleImpl.GetListToStruct(ctx, &cond, nil, &articleType, "id asc")
//if err != nil {
// return mes
//}
//var articleList = &entitys.ArticleTypeList{
// Desc: "文章类型以及描述",
// ArticleItem: make([]*entitys.ArticleItem, 0, len(articleType)),
//}
//for _, article := range articleType {
// articleList.ArticleItem = append(articleList.ArticleItem, &entitys.ArticleItem{
// ArticleType: article.ArticleName,
// TypeDesc: article.Desc,
// })
//}
//if len(articleList.ArticleItem) > 0 {
// mes = append(mes, &volmodle.ChatCompletionMessage{
// Role: volmodle.ChatMessageRoleAssistant,
// Content: &volmodle.ChatCompletionMessageContent{
// StringValue: volcengine.String(pkg.JsonStringIgonErr(articleList)),
// },
// })
//}
return mes
}

View File

@ -91,14 +91,14 @@ func (p *ProductBiz) AddSource(ctx context.Context, source *model.ProductSource)
}
func (p *ProductBiz) SourceUpload(ctx context.Context, file []byte, fileName string) (string, error) {
url, err := p.oss.UploadBytes(p.cfg.Oss.FilePath+fileName, file)
url, err := p.oss.UploadBytes(p.cfg.Oss.FilePath+"source/"+fileName, file)
if err != nil {
return "", fmt.Errorf("上传文件失败: %w", err)
}
return url, nil
}
func (p *ProductBiz) UpdateSourceById(ctx context.Context, id int32, update *model.ProductSource) error {
func (p *ProductBiz) UpdateSourceById(ctx context.Context, id int32, update any) error {
return p.productSourceImpl.UpdateByKey(ctx, p.productSourceImpl.PrimaryKey(), id, update)
}

View File

@ -65,6 +65,17 @@ func (b *PublishBiz) GetTaskByRequestID(ctx context.Context, requestID string) (
return b.publishImpl.GetOneWithPlat(ctx, &cond)
}
func (b *PublishBiz) GetTaskByPublishId(ctx context.Context, id int) (*model.Publish, error) {
var data model.Publish
cond := builder.NewCond().
And(builder.Eq{"id": id})
err := b.publishImpl.GetOneBySearchStruct(ctx, &cond, &data)
if err != nil {
return nil, err
}
return &data, err
}
func (b *PublishBiz) UpdatePublishStatus(ctx context.Context, requestID string, status int, msg string) error {
return b.publishImpl.UpdateStatus(ctx, requestID, status, msg)
}

View File

@ -9,7 +9,7 @@ const TableNameArticalType = "article_type"
// ArticalType mapped from table <artical_type>
type ArticleType struct {
ID int32 `gorm:"column:id;primaryKey" json:"id"`
ArticleName string `gorm:"column:artical_name;not null" json:"artical_name"`
ArticleName string `gorm:"column:article_name;not null" json:"article_name"`
Desc string `gorm:"column:desc;not null" json:"desc"`
Status int32 `gorm:"column:status;not null;default:1" json:"status"`
}

View File

@ -24,6 +24,7 @@ type Publish struct {
URL string `gorm:"column:url;not null;comment:资源文件下载地址" json:"url"` // 资源文件下载地址
PublishTime time.Time `gorm:"column:publish_time;not null;comment:发布时间" json:"publish_time"` // 发布时间
Img string `gorm:"column:img;not null" json:"img"`
LogUrl string `gorm:"column:log_url;not null" json:"log_url"`
Status int32 `gorm:"column:status;not null;default:1;comment:1待发布2发布中3发布失败4发布成功" json:"status"` // 1待发布2发布中3发布失败4发布成功
CreateTime time.Time `gorm:"column:create_time;not null;default:CURRENT_TIMESTAMP;comment:创建时间" json:"create_time"` // 创建时间
Msg string `gorm:"column:msg" json:"msg"`

View File

@ -63,10 +63,20 @@ type BrandInfo struct {
}
type PlatList struct {
Desc string `json:"question"`
Desc string `json:"desc"`
PlatItem []*PlatItem `json:"plat_item"`
}
type ArticleTypeList struct {
Desc string `json:"desc"`
ArticleItem []*ArticleItem `json:"article_item"`
}
type ArticleItem struct {
ArticleType string `json:"article_type"`
TypeDesc string `json:"type_esc"`
}
type PlatItem struct {
Platform string `json:"platform"`
PlatformIndex string `json:"platform_index"`

View File

@ -1,5 +1,11 @@
package entitys
import (
"fmt"
"strings"
"time"
)
type (
LoginAppRequest struct {
Secret string `json:"secret" validate:"required" zh:"密钥"`
@ -185,6 +191,40 @@ type (
AccessToken string `json:"access_token" validate:"required" zh:"access_token"`
SourceId int32 `json:"source_id" validate:"required" zh:"资源id"`
Plat []string `json:"plat" validate:"required" zh:"平台"`
PublishTime string `json:"publish_time" validate:"required" zh:"发布时间"`
PublishTime MyTime `json:"publish_time" validate:"required" zh:"发布时间"`
}
)
type MyTime struct {
time.Time
}
func (t *MyTime) UnmarshalJSON(data []byte) error {
str := strings.Trim(string(data), `"`)
if str == "" || str == "null" {
return nil
}
// 解析为本地时区(东八区)
loc, _ := time.LoadLocation("Asia/Shanghai") // 或 "Local"
formats := []string{
"2006-01-02T15:04",
"2006-01-02T15:04:05",
time.RFC3339,
}
for _, format := range formats {
if parsed, err := time.ParseInLocation(format, str, loc); err == nil {
t.Time = parsed
return nil
}
}
return fmt.Errorf("无法解析时间: %s", str)
}
func (t *MyTime) MarshalJSON() ([]byte, error) {
// 输出时也使用相同格式和时区
return []byte(`"` + t.Time.Format("2006-01-02T15:04") + `"`), nil
}

View File

@ -9,6 +9,7 @@ import (
"geo/internal/publisher"
"geo/pkg"
"geo/utils"
"geo/utils/utils_oss"
"io"
"log"
"os"
@ -73,25 +74,17 @@ func GetPublishManager(config *config.Config, db *utils.Db, publicBiz *biz.Publi
}
// getTaskLogger 获取任务专属日志记录器
func (pm *PublishManager) getTaskLogger(requestID string) (*log.Logger, *os.File, error) {
if requestID == "" {
return nil, nil, fmt.Errorf("requestID不能为空")
}
func (pm *PublishManager) getTaskLogger(publishID int, requestID string) (*log.Logger, *os.File, string, error) {
logsDir := pm.Conf.Sys.LogsDir
if logsDir == "" {
logsDir = "./logs"
}
if err := os.MkdirAll(logsDir, 0755); err != nil {
return nil, nil, fmt.Errorf("创建日志目录失败: %v", err)
return nil, nil, "", fmt.Errorf("创建日志目录失败: %v", err)
}
logPath := filepath.Join(logsDir, fmt.Sprintf("%s.log", requestID))
logPath := filepath.Join(logsDir, fmt.Sprintf("%d_%s.log", publishID, requestID))
logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return nil, nil, fmt.Errorf("创建日志文件失败: %v", err)
return nil, nil, "", fmt.Errorf("创建日志文件失败: %v", err)
}
multiWriter := io.MultiWriter(logFile, os.Stdout)
@ -101,7 +94,7 @@ func (pm *PublishManager) getTaskLogger(requestID string) (*log.Logger, *os.File
taskLogger.Printf("任务开始 | RequestID: %s | 时间: %s", requestID, time.Now().Format("2006-01-02 15:04:05.000"))
taskLogger.Printf(strings.Repeat("=", 80))
return taskLogger, logFile, nil
return taskLogger, logFile, logPath, nil
}
// Start 启动自动发布支持并发worker数量
@ -317,7 +310,7 @@ func (pm *PublishManager) processTask(ctx context.Context, publishData *entitys.
default:
}
taskLogger, logFile, err := pm.getTaskLogger(publishData.RequestID)
taskLogger, logFile, logPath, err := pm.getTaskLogger(publishData.ID, publishData.RequestID)
if err != nil {
log.Printf("[任务 %s] 创建日志文件失败: %v使用全局日志", publishData.RequestID, err)
taskLogger = log.Default()
@ -330,7 +323,7 @@ func (pm *PublishManager) processTask(ctx context.Context, publishData *entitys.
if r := recover(); r != nil {
errMsg := fmt.Sprintf("任务执行发生panic: %v", r)
taskLogger.Printf("❌ CRITICAL: %s", errMsg)
pm.updatePublishStatus(publishData, StatusFailed, errMsg)
pm.updatePublishStatus(publishData, StatusFailed, errMsg, "")
}
}()
@ -338,7 +331,7 @@ func (pm *PublishManager) processTask(ctx context.Context, publishData *entitys.
params, sourceUrl := pm.extractTaskParams(publishData, taskLogger)
if params == nil {
pm.updatePublishStatus(publishData, StatusFailed, "提取任务参数失败")
pm.updatePublishStatus(publishData, StatusFailed, "提取任务参数失败", "")
return &SingleResult{Success: false, Message: "提取任务参数失败", RequestId: publishData.RequestID}
}
params.Headless = headless
@ -347,7 +340,7 @@ func (pm *PublishManager) processTask(ctx context.Context, publishData *entitys.
if publisherClass == nil {
errMsg := fmt.Sprintf("不支持的平台: %s", params.PlatIndex)
taskLogger.Printf("[任务 %s] ❌ %s", publishData.RequestID, errMsg)
pm.updatePublishStatus(publishData, StatusFailed, errMsg)
pm.updatePublishStatus(publishData, StatusFailed, errMsg, "")
return &SingleResult{Success: false, Message: errMsg, RequestId: publishData.RequestID}
}
@ -355,7 +348,7 @@ func (pm *PublishManager) processTask(ctx context.Context, publishData *entitys.
if err != nil {
errMsg := fmt.Sprintf("准备文件失败: %v", err)
taskLogger.Printf("[任务 %s] ❌ %s", publishData.RequestID, errMsg)
pm.updatePublishStatus(publishData, StatusFailed, errMsg)
pm.updatePublishStatus(publishData, StatusFailed, errMsg, "")
return &SingleResult{Success: false, Message: errMsg, RequestId: publishData.RequestID}
}
defer pm.cleanupFiles(docPath, imgPath, taskLogger, publishData.RequestID)
@ -364,7 +357,7 @@ func (pm *PublishManager) processTask(ctx context.Context, publishData *entitys.
if err != nil {
errMsg := fmt.Sprintf("提取文档内容失败: %v", err)
taskLogger.Printf("[任务 %s] ❌ %s", publishData.RequestID, errMsg)
pm.updatePublishStatus(publishData, StatusFailed, errMsg)
pm.updatePublishStatus(publishData, StatusFailed, errMsg, "")
return &SingleResult{Success: false, Message: errMsg, RequestId: publishData.RequestID}
}
params.ImagePath = imgPath
@ -374,17 +367,37 @@ func (pm *PublishManager) processTask(ctx context.Context, publishData *entitys.
taskLogger.Printf("[任务 %s] 开始执行发布...", publishData.RequestID)
success, message := pub.PublishNote()
if success {
taskLogger.Printf("[任务 %s] ✅ 发布成功: %s", publishData.RequestID, message)
pm.updatePublishStatus(publishData, StatusSuccess, message)
} else {
taskLogger.Printf("[任务 %s] ❌ 发布失败: %s", publishData.RequestID, message)
pm.updatePublishStatus(publishData, StatusFailed, message)
}
taskLogger.Printf(strings.Repeat("=", 80))
taskLogger.Printf("任务结束 | RequestID: %s | 结果: %v", publishData.RequestID, success)
return &SingleResult{Success: success, Message: message, RequestId: publishData.RequestID}
res := &SingleResult{Success: success, Message: message, RequestId: publishData.RequestID}
pm.uploadToOss(ctx, logPath, fmt.Sprintf("%slog/%d_%s.log", pm.Conf.Oss.FilePath, publishData.ID, publishData.RequestID))
url, err := pm.uploadToOss(ctx, logPath, fmt.Sprintf("%s%d_%s.log", pm.Conf.Oss.FilePath, publishData.ID, publishData.RequestID))
if err != nil {
taskLogger.Printf("日志上传失败")
}
if success {
taskLogger.Printf("[任务 %s] ✅ 发布成功: %s", publishData.RequestID, message)
pm.updatePublishStatus(publishData, StatusSuccess, message, url)
} else {
taskLogger.Printf("[任务 %s] ❌ 发布失败: %s", publishData.RequestID, message)
pm.updatePublishStatus(publishData, StatusFailed, message, url)
}
return res
}
func (pm *PublishManager) uploadToOss(ctx context.Context, filePath string, path string) (url string, err error) {
client, err := utils_oss.NewClient(pm.Conf)
if err != nil {
return
}
fileBytes, err := os.ReadFile(filePath)
if err != nil {
return
}
url, err = client.UploadBytes(path, fileBytes)
return
}
// RetryTask 重试任务(非无头模式)
@ -533,15 +546,15 @@ func (pm *PublishManager) extractContent(docPath string, publisherClass *publish
}
// updatePublishStatus 更新发布状态
func (pm *PublishManager) updatePublishStatus(publishData *entitys.PublishTaskDetail, status int, message string) error {
func (pm *PublishManager) updatePublishStatus(publishData *entitys.PublishTaskDetail, status int, message string, log_url string) error {
if publishData.ID == 0 {
return fmt.Errorf("id不能为空")
}
var err error
if message != "" {
_, err = pm.db.Execute("UPDATE publish SET status = ?, msg = ? WHERE id=?", status, message, publishData.ID)
_, err = pm.db.Execute("UPDATE publish SET status = ?, msg = ?,log_url=? WHERE id=?", status, message, log_url, publishData.ID)
} else {
_, err = pm.db.Execute("UPDATE publish SET status = ? WHERE id=?", status, publishData.ID)
_, err = pm.db.Execute("UPDATE publish SET status = ?,log_url=? WHERE id=?", status, publishData.ID, log_url, log_url)
}
if err != nil {
log.Printf("更新发布状态失败: id=%s, status=%d, error=%v", publishData.ID, status, err)

View File

@ -5,7 +5,7 @@ import (
"fmt"
"geo/internal/config"
"log"
"path/filepath"
"os"
"strings"
"time"
@ -91,13 +91,15 @@ func (p *BaijiahaoPublisher) doPublish() (bool, string) {
name string
fn func() error
}{
{"修改文档名称", p.RenameDocument},
{"点击hover", p.clickHoverButton},
{"输入内容", p.inputContent},
{"输入标题", p.inputTitle},
//{"输入标题", p.inputTitle},
{"设置封面", p.uploadImage},
{"点击发布按钮", p.clickPublish},
//{"处理确认弹窗", p.handleConfirmModal},
}
defer os.Remove(p.SourcePath)
//https://baijiahao.baidu.com/builder/rc/clue?aside=0&footer=true&from=news&firstPublish=undefined&word_bag_id=null
for _, step := range steps {
if err := step.fn(); err != nil {
@ -182,7 +184,13 @@ func (p *BaijiahaoPublisher) inputTitle() error {
currentTitle, _ := titleInput.Text()
if currentTitle != "" {
p.LogInfo(fmt.Sprintf("清空当前标题: %s", currentTitle[:min(50, len(currentTitle))]))
p.ClearContentEditable(titleInput)
p.SleepMs(200)
err := titleInput.SelectAllText()
if err != nil {
}
p.SleepMs(200)
titleInput.MustInput(p.Title)
p.SleepMs(200)
}
titleInput.Input(p.Title)
@ -297,39 +305,39 @@ func (p *BaijiahaoPublisher) inputContent() error {
// 5. 等待导入成功
// 提取文件名(不含路径)
fileName := filepath.Base(p.SourcePath)
// 等待导入成功的提示
for i := 0; i < 30; i++ {
// 查找包含文件名的成功提示
successMsg, err := p.Page.ElementX(fmt.Sprintf("//*[contains(text(), '%s') and (contains(text(), '成功') or contains(text(), '导入'))]", fileName))
if err == nil && successMsg != nil {
text, _ := successMsg.Text()
p.LogInfo(fmt.Sprintf("文档导入成功: %s", text))
p.SleepMs(2000) // 等待内容加载完成
return nil
}
// 通用成功提示查找
successMsg, err = p.Page.ElementX("//*[contains(text(), '导入成功')]")
if err == nil && successMsg != nil {
text, _ := successMsg.Text()
p.LogInfo(fmt.Sprintf("文档导入成功: %s", text))
p.SleepMs(2000)
return nil
}
// 查找是否有错误提示
errorMsg, err := p.Page.ElementX("//*[contains(text(), '失败') or contains(text(), '错误')]")
if err == nil && errorMsg != nil {
text, _ := errorMsg.Text()
if strings.Contains(text, fileName) || strings.Contains(text, "导入") {
return fmt.Errorf("文档导入失败: %s", text)
}
}
p.SleepMs(500)
}
//fileName := filepath.Base(p.SourcePath)
//
//// 等待导入成功的提示
//for i := 0; i < 30; i++ {
// // 查找包含文件名的成功提示
// successMsg, err := p.Page.ElementX(fmt.Sprintf("//*[contains(text(), '%s') and (contains(text(), '成功') or contains(text(), '导入'))]", fileName))
// if err == nil && successMsg != nil {
// text, _ := successMsg.Text()
// p.LogInfo(fmt.Sprintf("文档导入成功: %s", text))
// p.SleepMs(2000) // 等待内容加载完成
// return nil
// }
//
// // 通用成功提示查找
// successMsg, err = p.Page.ElementX("//*[contains(text(), '导入成功')]")
// if err == nil && successMsg != nil {
// text, _ := successMsg.Text()
// p.LogInfo(fmt.Sprintf("文档导入成功: %s", text))
// p.SleepMs(2000)
// return nil
// }
//
// // 查找是否有错误提示
// errorMsg, err := p.Page.ElementX("//*[contains(text(), '失败') or contains(text(), '错误')]")
// if err == nil && errorMsg != nil {
// text, _ := errorMsg.Text()
// if strings.Contains(text, fileName) || strings.Contains(text, "导入") {
// return fmt.Errorf("文档导入失败: %s", text)
// }
// }
//
// p.SleepMs(500)
//}
// 虽然没有明确的成功提示,但等待几秒让内容加载
p.LogInfo("等待内容加载完成...")

View File

@ -5,6 +5,8 @@ import (
"encoding/json"
"fmt"
"geo/internal/entitys"
"geo/pkg"
"geo/utils/utils_oss"
"log"
"os"
"path/filepath"
@ -46,6 +48,7 @@ type BasePublisher struct {
MaxRetries int
RetryDelay int
ossClient *utils_oss.Client
}
// taskParams 任务参数结构体
@ -65,9 +68,6 @@ type TaskParams struct {
// NewBasePublisher 构造函数,增加 logger 参数
func NewBasePublisher(ctx context.Context, task *TaskParams, config *config.Config, logger *log.Logger) *BasePublisher {
cookiesDir := filepath.Join(config.Sys.CookiesDir, task.UserIndex)
os.MkdirAll(cookiesDir, 0755)
cookiesFile := filepath.Join(cookiesDir, task.PlatIndex+".json")
var baseLogger *log.Logger
var logFile *os.File
@ -87,28 +87,36 @@ func NewBasePublisher(ctx context.Context, task *TaskParams, config *config.Conf
baseLogger = log.New(logFile, "", log.LstdFlags)
}
return &BasePublisher{
ctx: ctx,
Headless: task.Headless,
Title: task.Title,
Content: task.Content,
Tags: task.Tags,
UserIndex: task.UserIndex,
PlatIndex: task.PlatIndex,
RequestID: task.RequestID,
ImagePath: task.ImagePath,
SourcePath: task.SourcePath,
Logger: baseLogger,
LogFile: logFile,
CookiesFile: cookiesFile,
PlatInfo: task.PublishData,
config: config,
LoginURL: task.PublishData.LoginUrl,
EditorURL: task.PublishData.EditUrl,
LoginedURL: task.PublishData.LoginedUrl,
MaxRetries: 3,
RetryDelay: 200,
base := &BasePublisher{
ctx: ctx,
Headless: task.Headless,
Title: task.Title,
Content: task.Content,
Tags: task.Tags,
UserIndex: task.UserIndex,
PlatIndex: task.PlatIndex,
RequestID: task.RequestID,
ImagePath: task.ImagePath,
SourcePath: task.SourcePath,
Logger: baseLogger,
LogFile: logFile,
PlatInfo: task.PublishData,
config: config,
LoginURL: task.PublishData.LoginUrl,
EditorURL: task.PublishData.EditUrl,
LoginedURL: task.PublishData.LoginedUrl,
MaxRetries: 3,
RetryDelay: 200,
}
base.CookiesFile = filepath.Join(base.cookiesDir(), task.PlatIndex+".json")
return base
}
func (b *BasePublisher) cookiesDir() string {
dir := filepath.Join(b.config.Sys.CookiesDir, b.UserIndex)
os.MkdirAll(dir, 0755)
return dir
}
func (b *BasePublisher) SetupDriver() error {
@ -185,21 +193,30 @@ func (b *BasePublisher) SaveCookies() error {
if err != nil {
return err
}
data, err := json.Marshal(cookies)
if err != nil {
return err
}
return os.WriteFile(b.CookiesFile, data, 0644)
}
func (b *BasePublisher) LoadCookies() error {
data, err := os.ReadFile(b.CookiesFile)
os.WriteFile(b.CookiesFile, data, 0644)
if err != nil {
return err
}
return b.uploadToOss(data)
}
func (b *BasePublisher) LoadCookies() error {
var data []byte
if _, err := os.Stat(b.CookiesFile); os.IsNotExist(err) {
_err := b.getCookieFromUrl()
if _err != nil {
return _err
}
}
data, err := os.ReadFile(b.CookiesFile)
if err != nil {
return err
}
var cookies []*proto.NetworkCookieParam
if err := json.Unmarshal(data, &cookies); err != nil {
return err
@ -430,3 +447,76 @@ func (b *BasePublisher) SafeElementInParent(parent *rod.Element, selector string
}
return children[0], nil
}
// RenameDocument 将指定绝对路径的文档更名为新名称(不含后缀)
// filePath: 文档的绝对路径
// newName: 希望的新文件名(不含后缀,例如 "newfile"
// 返回值: 新文件的绝对路径, 错误信息
func (b *BasePublisher) RenameDocument() error {
// 检查原文件是否存在
if _, err := os.Stat(b.SourcePath); os.IsNotExist(err) {
return fmt.Errorf("文件不存在: %s", b.SourcePath)
}
// 获取原文件的扩展名
ext := filepath.Ext(b.SourcePath)
// 如果新文件名已经包含扩展名,则不再添加
var fullNewName string
if strings.HasSuffix(b.Title, ext) {
fullNewName = b.Title
} else {
fullNewName = b.Title + ext
}
// 获取原文件所在目录
dir := filepath.Dir(b.Title)
// 构造新路径
newPath := filepath.Join(dir, fullNewName)
// 如果目标文件已存在,先删除它(实现覆盖)
if _, err := os.Stat(newPath); err == nil {
err := os.Remove(newPath)
if err != nil {
return fmt.Errorf("删除已存在的目标文件失败: %v", err)
}
}
// 执行重命名(移动)
err := os.Rename(b.SourcePath, newPath)
if err != nil {
return fmt.Errorf("重命名失败: %v", err)
}
// 返回更名后的绝对路径
absPath, err := filepath.Abs(newPath)
if err != nil {
return fmt.Errorf("获取绝对路径失败: %v", err)
}
b.SourcePath = absPath
return nil
}
func (b *BasePublisher) uploadToOss(data []byte) (err error) {
if b.ossClient == nil {
b.ossClient, err = utils_oss.NewClient(b.config)
if err != nil {
return
}
}
_, err = b.ossClient.UploadBytes(fmt.Sprintf("%scookie/%s/%s.json", b.config.Oss.FilePath, b.UserIndex, b.PlatIndex), data)
return
}
func (b *BasePublisher) getCookieFromUrl() (err error) {
b.LogInfo("正在加载cookie。。。。")
url := fmt.Sprintf("%s/%scookie/%s/%s.json", b.config.Oss.Domain, b.config.Oss.FilePath, b.UserIndex, b.PlatIndex)
cookieDir := b.cookiesDir()
_, err = pkg.DownloadFile(url, cookieDir, b.PlatIndex+".json")
if err != nil {
return err
}
b.LogInfo("cookie加载完成。。。。")
return nil
}

View File

@ -57,37 +57,139 @@ func (p *XiaohongshuPublisher) WaitLogin() (bool, string) {
return false, "登录超时"
}
func (p *XiaohongshuPublisher) inputContent() error {
p.LogInfo("输入文章内容...")
//func (p *XiaohongshuPublisher) inputContent() error {
// p.LogInfo("输入文章内容...")
//
// // 等待编辑器加载
// contentEditor, err := p.WaitForElementVisible(".tiptap.ProseMirror", 10)
// if err != nil {
// // 尝试其他选择器
// contentEditor, err = p.WaitForElementVisible("[contenteditable='true']", 10)
// if err != nil {
// return fmt.Errorf("未找到内容编辑器: %v", err)
// }
// }
//
// // 点击获取焦点 - 使用 Click 方法
// if err := contentEditor.Click(proto.InputMouseButtonLeft, 1); err != nil {
// return fmt.Errorf("点击编辑器失败: %v", err)
// }
// p.SleepMs(500)
//
// // 清空现有内容 - 使用 JavaScript 清空
// if err := p.ClearContentEditable(contentEditor); err != nil {
// p.LogInfo(fmt.Sprintf("清空编辑器失败: %v", err))
// }
// p.SleepMs(300)
//
// // 输入新内容 - 使用 JavaScript 设置内容
// if err := p.SetContentEditable(contentEditor, p.Content); err != nil {
// // 如果 JS 方式失败,尝试直接输入
// contentEditor.Input(p.Content)
// }
// p.LogInfo(fmt.Sprintf("内容已输入,长度: %d", len(p.Content)))
//
// return nil
//}
// 等待编辑器加载
contentEditor, err := p.WaitForElementVisible(".tiptap.ProseMirror", 10)
func (p *XiaohongshuPublisher) inputContent() error {
p.LogInfo("开始导入文档内容...")
// 1. 找到菜单容器并点击最后一个菜单项
menuContainer, err := p.WaitForElementVisible(".menu-items-container", 10)
if err != nil {
// 尝试其他选择器
contentEditor, err = p.WaitForElementVisible("[contenteditable='true']", 10)
if err != nil {
return fmt.Errorf("未找到内容编辑器: %v", err)
return fmt.Errorf("未找到菜单容器: %v", err)
}
p.LogInfo("找到菜单容器")
// 获取所有菜单项
menuItems, err := menuContainer.Elements(".menu-item")
if err != nil {
return fmt.Errorf("未找到菜单项: %v", err)
}
if len(menuItems) == 0 {
return fmt.Errorf("菜单容器中没有找到菜单项")
}
// 点击最后一个菜单项
lastMenuItem := menuItems[len(menuItems)-1]
if err := p.JSClick(lastMenuItem); err != nil {
return fmt.Errorf("点击最后一个菜单项失败: %v", err)
}
p.LogInfo("已点击最后一个菜单项")
p.SleepMs(500)
// 2. 等待导入文件模态框出现
var importModal *rod.Element
for i := 0; i < 10; i++ {
importModal, err = p.Page.Element("[class*='import-from-file-modal']")
if err == nil && importModal != nil {
visible, _ := importModal.Visible()
if visible {
p.LogInfo("找到导入文件模态框")
break
}
}
p.SleepMs(500)
}
if importModal == nil {
return fmt.Errorf("未找到导入文件模态框")
}
// 查找模态框内的内容容器
modalContent, err := importModal.Element(".d-modal-content")
if err != nil {
return fmt.Errorf("未找到模态框内容容器: %v", err)
}
p.LogInfo("找到模态框内容容器")
modalContent.MustClick()
p.SleepMs(300)
// 3. 查找隐藏的文件输入框
var fileInput *rod.Element
// 尝试多种选择器
selectors := []string{
"input[type='file'][accept*='.docx']",
"input[type='file'][accept*='.doc']",
"input[type='file']",
}
for _, selector := range selectors {
fileInput, err = modalContent.Element(selector)
if err == nil && fileInput != nil {
p.LogInfo(fmt.Sprintf("通过选择器找到文件输入框: %s", selector))
break
}
}
// 点击获取焦点 - 使用 Click 方法
if err := contentEditor.Click(proto.InputMouseButtonLeft, 1); err != nil {
return fmt.Errorf("点击编辑器失败: %v", err)
if fileInput == nil {
// 尝试在整个页面中查找
for _, selector := range selectors {
fileInput, err = p.Page.Element(selector)
if err == nil && fileInput != nil {
p.LogInfo(fmt.Sprintf("在全局中找到文件输入框: %s", selector))
break
}
}
}
p.SleepMs(500)
// 清空现有内容 - 使用 JavaScript 清空
if err := p.ClearContentEditable(contentEditor); err != nil {
p.LogInfo(fmt.Sprintf("清空编辑器失败: %v", err))
if fileInput == nil {
return fmt.Errorf("未找到文件输入框")
}
p.SleepMs(300)
// 输入新内容 - 使用 JavaScript 设置内容
if err := p.SetContentEditable(contentEditor, p.Content); err != nil {
// 如果 JS 方式失败,尝试直接输入
contentEditor.Input(p.Content)
// 4. 上传文件
if p.SourcePath == "" {
return fmt.Errorf("源文件路径为空")
}
p.LogInfo(fmt.Sprintf("内容已输入,长度: %d", len(p.Content)))
// 使用 SetFiles 方法上传文件
if err := fileInput.SetFiles([]string{p.SourcePath}); err != nil {
return fmt.Errorf("上传文件失败: %v", err)
}
p.LogInfo(fmt.Sprintf("已选择文件: %s", p.SourcePath))
// 等待文件上传完成
p.SleepMs(2000)
return nil
}

View File

@ -49,7 +49,7 @@ func (m *AppModule) Register(router fiber.Router) {
router.Post("/get_publish_list", vali(m.publishService.GetPublishList, &entitys.GetPublishListRequest{}))
router.Post("/login_platform", vali(m.loginService.LoginPlatform, &entitys.LoginPlatformRequest{}))
router.Post("/logout_platform", vali(m.loginService.LogoutPlatform, &entitys.LogoutPlatformRequest{}))
router.Get("/logs/:request_id", m.loginService.Log)
router.Get("/logs/:publish_id/:request_id", m.loginService.Log)
router.Post("/product/add", vali(m.productService.Add, &entitys.CreateProductRequest{}))
router.Post("/product/list", vali(m.productService.List, &entitys.ProductListRequest{}))

View File

@ -1,10 +1,12 @@
package service
import (
"fmt"
"geo/internal/manager"
"geo/internal/publisher"
"os"
"path/filepath"
"strconv"
"github.com/gofiber/fiber/v2"
@ -99,17 +101,38 @@ func (s *LoginService) ServeQrcode(c *fiber.Ctx, filename string) error {
}
func (s *LoginService) Log(c *fiber.Ctx) error {
var content []byte
r_id := c.Params("request_id", "")
if r_id == "" {
return errcode.ParamErr("request_id未传")
}
logFile := filepath.Join(s.cfg.Sys.LogsDir, r_id+".log")
if _, err := os.Stat(logFile); os.IsNotExist(err) {
return errcode.NotFound("未找到日志")
}
content, err := os.ReadFile(logFile)
idStr := c.Params("publish_id", "")
id, err := strconv.Atoi(idStr)
if err != nil {
return errcode.SysErr("读取日志失败")
// 转换失败处理
return errcode.ParamErr("idStr格式错误")
}
logFile := filepath.Join(s.cfg.Sys.LogsDir, fmt.Sprintf("%d_%s.log", id, r_id))
if _, err := os.Stat(logFile); !os.IsNotExist(err) {
content, err = os.ReadFile(logFile)
if err != nil {
return errcode.SysErr("读取日志失败")
}
} else {
publishData, _err := s.publishBiz.GetTaskByPublishId(c.UserContext(), id)
if _err != nil || len(publishData.LogUrl) == 0 {
return errcode.SysErr("读取日志失败")
}
logPath, _err := pkg.DownloadFile(publishData.LogUrl, s.cfg.Sys.LogsDir, fmt.Sprintf("%d_%s.log", id, r_id))
if _err != nil {
return errcode.SysErr("读取日志失败")
}
content, _err = os.ReadFile(logPath)
if _err != nil {
return errcode.SysErr("读取日志失败")
}
}
return pkg.HandleResponse(c, fiber.Map{"content": string(content)})

View File

@ -79,9 +79,16 @@ func (p *ProductSourceService) Create(c *fiber.Ctx, req *entitys.ProductSourceCr
if err != nil {
return err
}
contentByte := []byte(*content)
var resp entitys.BotChatResponse
if err := json.Unmarshal([]byte(*content), &resp); err != nil {
return errcode.SysErr("文章生成失败,请重试")
if err := json.Unmarshal(contentByte, &resp); err != nil {
contentStr, err := pkg.JsonRepair(*content)
if err != nil {
return errcode.SysErr("文章生成失败,请重试")
}
if err := json.Unmarshal([]byte(contentStr), &resp); err != nil {
return errcode.SysErr("文章生成失败,请重试")
}
}
docxUrl, err := p.productBiz.CreateAndUploadArticle(c.UserContext(), resp.Content, product)
if err != nil {
@ -187,12 +194,12 @@ func (p *ProductSourceService) Update(c *fiber.Ctx, req *entitys.ProductSourceUp
if err != nil {
return err
}
var update = &model.ProductSource{}
var update = map[string]any{}
if req.Title != nil {
update.Title = *req.Title
update["title"] = *req.Title
}
if req.Tag != nil {
update.Tag = strings.Join(*req.Tag, ",")
update["tag"] = strings.Join(*req.Tag, ",")
}
return p.productBiz.UpdateSourceById(c.UserContext(), req.SourceId, update)
@ -227,10 +234,10 @@ func (p *ProductSourceService) Publish(c *fiber.Ctx, req *entitys.ProductPublish
if product.Imgs == "" {
return errcode.NotFound("请先上传产品图片")
}
ptime, err := time.Parse(time.DateTime, req.PublishTime)
if err != nil {
return errcode.ParamErr("发布时间格式错误")
}
//ptime, err := time.Parse(time.DateTime, req.PublishTime)
//if err != nil {
// return errcode.ParamErr("发布时间格式错误")
//}
var validRecords = make([]*model.Publish, 0, len(req.Plat))
for _, v := range req.Plat {
validRecords = append(validRecords, &model.Publish{
@ -242,7 +249,7 @@ func (p *ProductSourceService) Publish(c *fiber.Ctx, req *entitys.ProductPublish
Type: source.SourceType,
PlatIndex: v,
URL: source.SourceURL,
PublishTime: ptime,
PublishTime: req.PublishTime.Time,
Img: strings.Split(product.Imgs, ",")[0],
TokenID: tokenInfo.ID,
})
@ -261,7 +268,8 @@ func (p *ProductSourceService) Publish(c *fiber.Ctx, req *entitys.ProductPublish
func (p *ProductSourceService) ArticalTypeList(c *fiber.Ctx) error {
cond := builder.NewCond().
And(builder.Eq{"status": 1})
list, err := p.articleTypeImpl.GetRange(c.UserContext(), &cond)
var list []*model.ArticleType
err := p.articleTypeImpl.GetRangeToMapStruct(c.UserContext(), &cond, &list)
if err != nil {
return err
}

View File

@ -178,3 +178,42 @@ func Md2wordFix(mdFile, outPutDIr string, img []string) (string, error) {
return result.Content, nil
}
func JsonRepair(badJson string) (string, error) {
baseDir, err := os.Getwd()
if err != nil {
return "", fmt.Errorf("获取工作目录失败: %w", err)
}
exePath := filepath.Join(baseDir, "plugins", "json_fix.exe")
// 3. 检查 exe 是否存在
if _, err = os.Stat(exePath); os.IsNotExist(err) {
return "", fmt.Errorf("exe不存在: %s", exePath)
}
//7. 构建命令行参数
args := []string{"-j", badJson}
cmd := exec.Command(exePath, args...)
output, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("执行失败: %v", err)
}
var result struct {
Success bool `json:"success"`
Content string `json:"content"`
Error string `json:"error"`
}
if err = json.Unmarshal(output, &result); err != nil {
return "", fmt.Errorf("解析结果失败: %v, 输出: %s", err, string(output))
}
if !result.Success {
return "", fmt.Errorf("转换失败: %s", result.Error)
}
return result.Content, nil
}

BIN
plugins/json_fix.exe Normal file

Binary file not shown.