package einograph import ( "ai-agent/workflow/model/dto" "ai-agent/workflow/skill" "context" "fmt" "io" "net/http" "strings" commonHttp "gitea.redpowerfuture.com/red-future/common/http" "gitea.redpowerfuture.com/red-future/common/utils" "github.com/cloudwego/eino/schema" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/glog" "github.com/gogf/gf/v2/util/gconv" ) // 全局保存请求(最简单、最稳定、Eino 必不报错) var currentReq *dto.CreationInput // ==================== 1. 输入适配 ==================== func InputAdaptLambda(ctx context.Context, input any) (any, error) { // 直接保存到全局 req, ok := input.(*dto.CreationInput) if !ok { return nil, fmt.Errorf("input must be CreationInput") } currentReq = req return input, nil } // ==================== 2. 构建Prompt ==================== func BuildPromptLambda(ctx context.Context, input any) (any, error) { req, ok := input.(*dto.CreationInput) if !ok { return nil, fmt.Errorf("input must be CreationInput") } count := req.Count if req.Count > 3 { req.Count = 3 } imagePerPost := req.ImagePerPost if req.ImagePerPost > 3 { imagePerPost = 3 } var imgPlaceholder strings.Builder for i := 1; i <= imagePerPost; i++ { imgPlaceholder.WriteString(fmt.Sprintf(`
`, i)) } mode := req.Mode // 核心:先用 embed 读取技能文案(完全不依赖运行目录) skillContent, err := skill.ReadSkillMD() prompt := "" if len(skillContent) != 0 && err == nil { prompt = skillContent } // 根据模式拼接(动态图片数量 + 动态比例 + 严格数量控制) switch mode { case "混合模式(文案 + 图片)": s1 := `1. 整个输出只能有 %d 个 标签和 %d 个 标签` var a = fmt.Sprintf(`%s%d%d`, s1, count, count) s2 := `2. 生成完成后立即停止,不续写、不扩展、不重复 3. 不要输出任何解释、前言、后语 4. 只输出纯HTML代码 5. 内容绝对不能重复!` var b = fmt.Sprintf(`%s%s`, a, s2) s3 := `6. 多个HTML文件之间必须用 --- 分隔,分隔符前后不能有任何多余文字!` if count > 1 { b = fmt.Sprintf(`%s%s`, b, s3) } prompt = fmt.Sprintf(`%s 【🔴 最高优先级强制规则】 %s 【📄 HTML结构强制模板】

%s

%s
请按照小红书风格创作文案:分段清晰、使用emoji、重点内容加粗、排版美观
#干货分享 #实用技巧 #知识科普
【📌 必须遵守的细节规则】 1. 所有图片必须使用 .img-box 容器,不变形、不拉伸。 2. 文案必须分段清晰、美观、小红书风格、使用emoji、重点内容加粗,排版符合小红书风格。 3. 图片占位符里有几张图,就生成几张,不要自己额外添加或删除。 4. 标签自动根据主题生成,无需外部参数。 内容信息: 主题:%s 标题:%s 风格:%s `, prompt, b, req.ImageRatio, req.Title, imgPlaceholder.String(), // 你传入的图片(多张自动适配) req.Theme, req.Title, req.Style, ) case "纯文案模式": s1 := `1. 整个输出只能有 %d 个 标签和 %d 个 标签` var a = fmt.Sprintf(`%s%d%d`, s1, count, count) s2 := `2. 生成完成后立即停止,不续写、不扩展、不重复 3. 不要输出任何解释、前言、后语 4. 只输出纯HTML代码 5. 内容绝对不能重复!` var b = fmt.Sprintf(`%s%s`, a, s2) s3 := `6. 多个HTML文件之间必须用 --- 分隔,分隔符前后不能有任何多余文字!` if count > 1 { b = fmt.Sprintf(`%s%s`, b, s3) } prompt = fmt.Sprintf(`%s 【🔴 最高优先级强制规则】 %s 【📄 HTML结构强制模板】

%s

按照小红书风格创作文案:分段+emoji+重点加粗,排版美观
#干货分享 #实用技巧
【📌 必须遵守的细节规则】 2. 必须分段清晰、美观、小红书风格、使用emoji、重点内容加粗,排版符合小红书风格。 3. 标签自动根据主题生成,无需外部参数。 内容:主题=%s,标题=%s,风格=%s `, prompt, b, req.Title, req.Theme, req.Title, req.Style, ) case "纯图片模式": s1 := `1. 必须**严格且只能生成 %d 组绘图关键词**,绝对不能多生成一组,也不能少生成一组!` var a = fmt.Sprintf(`%s%d`, s1, count) s2 := `2. 生成完成后立即停止,不续写、不扩展、不重复 3. 所有输出内容只能是关键词本身,**绝对不能出现任何解释、说明、额外文字**! 4. 内容绝对不能重复!` var b = fmt.Sprintf(`%s%s`, a, s2) s3 := `5. 多组关键词之间必须用 --- 分隔,分隔符前后不能有任何多余文字!` if count > 1 { b = fmt.Sprintf(`%s%s`, b, s3) } prompt = fmt.Sprintf(`%s 【🔴 最高优先级强制规则】 %s 内容:主题:%s,比例:%s,风格:%s `, prompt, b, req.Theme, req.ImageRatio, req.Style, ) default: return nil, fmt.Errorf("不支持模式") } if !g.IsEmpty(req.Desc) { prompt = fmt.Sprintf(`%s,要求:%s`, prompt, req.Desc) } return []*schema.Message{schema.UserMessage(prompt)}, nil } // ==================== 4. 生成+上传(完全不解析input!) ==================== func GenerateImageLambda(ctx context.Context, input any) (any, error) { msg, ok := input.(*schema.Message) if !ok { return nil, fmt.Errorf("input must be *schema.Message") } optimizedText := strings.TrimSpace(msg.Content) list := strings.Split(optimizedText, "---") var items []string for _, s := range list { if s = strings.TrimSpace(s); s != "" { items = append(items, s) } } imgAddressPrefix, err := utils.GetFileAddressPrefix(ctx) if err != nil { return nil, err } mode := currentReq.Mode imagePerPost := currentReq.ImagePerPost if currentReq.ImagePerPost > 3 { imagePerPost = 3 } // ============================================== // ✅ 核心修复:构建正确的绘图关键词(主题+标题+风格+比例) // ============================================== prompt := fmt.Sprintf( "%s,%s,%s,风格:%s,比例:%s", currentReq.ContentType, currentReq.Theme, currentReq.Title, currentReq.Style, currentReq.ImageRatio, ) if !g.IsEmpty(currentReq.Desc) { prompt = fmt.Sprintf(`%s,要求:%s`, prompt, currentReq.Desc) } // 初始化返回结果 var output dto.CreationOutput for idx, item := range items { base := fmt.Sprintf("%s_%d", currentReq.Title, idx+1) dir := currentReq.Theme num := imagePerPost var uploadItem dto.ImageUploadItem uploadItem.Title = currentReq.Title uploadItem.Index = idx + 1 uploadItem.Theme = currentReq.Theme uploadItem.ContentType = currentReq.ContentType switch mode { case "混合模式(文案 + 图片)": // 生成并上传多张图片 var imageUrls []string imgMap := make(map[int]string) for i := 1; i <= num; i++ { url, _ := GenerateRealImage(prompt) imageUrls = append(imageUrls, url) // 收集图片URL bs, _ := getImageBytesFromURL(url) uploadResp, err := Upload(ctx, &dto.UploadFileBytesReq{ FileName: fmt.Sprintf("%s_%d.png", base, i), FileBytes: bs, FileStoreURL: dir, }) // 如果Upload返回了最终访问URL,替换成真实返回值 if err != nil { return nil, err } imageUrls[i-1] = uploadResp.FileURL imgMap[i] = uploadResp.FileURL } uploadItem.ImageUrls = imageUrls // 存入结构体 // 替换HTML final := item if imagePerPost > 1 { for i := 1; i <= num; i++ { final = strings.ReplaceAll(final, fmt.Sprintf("{{IMG_URL_%d}}", i), imgAddressPrefix+imgMap[i]) } } else { for i := 1; i <= num; i++ { final = strings.ReplaceAll(final, fmt.Sprintf("{{IMG_URL_%d}}", idx+1), imgAddressPrefix+imgMap[i]) } } htmlUploadResp, err := Upload(ctx, &dto.UploadFileBytesReq{ FileName: base + ".html", FileBytes: []byte(final), FileStoreURL: dir, }) // 保存HTML上传地址 if err != nil { return nil, err } uploadItem.HtmlFileUrl = htmlUploadResp.FileURL case "纯图片模式": var realImageUrls []string for i := 1; i <= num; i++ { tempUrl, _ := GenerateRealImage(prompt) bs, _ := getImageBytesFromURL(tempUrl) uploadResp, err := Upload(ctx, &dto.UploadFileBytesReq{ FileName: fmt.Sprintf("%s_%d.png", base, i), FileBytes: bs, FileStoreURL: dir, }) if err != nil { return nil, err } realImageUrls = append(realImageUrls, uploadResp.FileURL) } uploadItem.ImageUrls = realImageUrls case "纯文案模式": htmlResp, err := Upload(ctx, &dto.UploadFileBytesReq{ FileName: base + ".html", FileBytes: []byte(item), FileStoreURL: dir, }) if err != nil { return nil, err } uploadItem.HtmlFileUrl = htmlResp.FileURL } output.Items = append(output.Items, uploadItem) fmt.Printf("✅ 第%d条上传成功\n,html:%s\n图片:%s\n", idx+1, gconv.String(uploadItem.HtmlFileUrl), gconv.String(uploadItem.ImageUrls)) } // 统计成功数量 output.SuccessCount = len(output.Items) return output, nil } // ==================== 工具 ==================== func getImageBytesFromURL(url string) ([]byte, error) { resp, err := http.Get(url) if err != nil { return nil, err } defer resp.Body.Close() return io.ReadAll(resp.Body) } func Upload(ctx context.Context, req *dto.UploadFileBytesReq) (*dto.UploadFileBytesRes, error) { headers := make(map[string]string) if r := g.RequestFromCtx(ctx); r != nil { for k, v := range r.Header { headers[k] = v[0] } } res := &dto.UploadFileBytesRes{} err := commonHttp.Post(ctx, "oss/file/uploadFileBytes", headers, res, req) if err != nil { glog.Error(ctx, err) return nil, err } return res, nil }