Files
ai-agent/workflow/service/einograph/lambda_func.go
2026-06-10 15:29:21 +08:00

410 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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(`<div class="img-box"><img src="{{IMG_URL_%d}}"></div>`, i))
}
mode := req.Mode
// 核心:先用 embed 读取技能文案(完全不依赖运行目录)
skillContent, err := skill.ReadSkillMD()
prompt := ""
if len(skillContent) != 0 && err == nil {
prompt = skillContent
}
// 根据模式拼接(动态图片数量 + 动态比例 + 严格数量控制)
switch mode {
case "混合模式(文案 + 图片)":
s1 := `1. 整个输出只能有 %d 个 <html> 标签和 %d 个</html> 标签`
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结构强制模板】
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
* { margin:0; padding:0; box-sizing:border-box; font-family:system-ui, -apple-system, sans-serif; }
body { background:#F0F2F5; padding:16px; }
.card { background:#fff; border-radius:16px; padding:18px; max-width:500px; margin:0 auto; }
.title { font-size:22px; font-weight:700; margin-bottom:14px; line-height:1.4; }
/* 动态图片比例:由参数决定 */
.img-box {
width:100%%;
aspect-ratio:%s;
border-radius:12px;
overflow:hidden;
margin-bottom:14px;
}
.img-box img { width:100%%; height:100%%; object-fit:cover; display:block; }
.content { font-size:16px; line-height:1.7; color:#333; margin-bottom:20px; }
.content p { margin:8px 0; }
.tags { display:flex; flex-wrap:wrap; gap:8px; margin-top:10px; }
.tag { background:#f5f5f5; padding:6px 12px; border-radius:20px; font-size:14px; color:#666; }
</style>
</head>
<body>
<div class="card">
<h2 class="title">%s</h2>
<!-- 图片数量由前端参数决定,直接使用传入的图片,不准修改 -->
%s
<div class="content">
请按照小红书风格创作文案分段清晰、使用emoji、重点内容加粗、排版美观
</div>
<div class="tags">
<span class="tag">#干货分享</span>
<span class="tag">#实用技巧</span>
<span class="tag">#知识科普</span>
</div>
</div>
</body>
</html>
【📌 必须遵守的细节规则】
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 个 <html> 标签和 %d 个</html> 标签`
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结构强制模板】
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
* { margin:0; padding:0; box-sizing:border-box; font-family:system-ui, -apple-system, sans-serif; }
body { background:#F0F2F5; padding:16px; }
.card { background:#fff; border-radius:16px; padding:18px; max-width:500px; margin:0 auto; }
.title { font-size:22px; font-weight:700; margin-bottom:14px; }
.content { font-size:16px; line-height:1.7; color:#333; margin-bottom:20px; }
.tags { display:flex; flex-wrap:wrap; gap:8px; }
.tag { background:#f5f5f5; padding:6px 12px; border-radius:20px; font-size:14px; color:#666; }
</style>
</head>
<body>
<div class="card">
<h2 class="title">%s</h2>
<div class="content">按照小红书风格创作文案:分段+emoji+重点加粗,排版美观</div>
<div class="tags">
<span class="tag">#干货分享</span>
<span class="tag">#实用技巧</span>
</div>
</div>
</body>
</html>
【📌 必须遵守的细节规则】
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条上传成功\nhtml%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
}