diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..73e171d --- /dev/null +++ b/.env.example @@ -0,0 +1,54 @@ +# ================================================================ +# EAI AgentPlatform 环境变量配置 +# ================================================================ +# 复制此文件为 .env 并填入实际值: +# cp .env.example .env +# ================================================================ + +# ---------- 后端服务 ---------- +# 后端 API 端口 +PORT=8080 + +# SQLite 数据库路径 +DB_PATH=./data/eai_agentplatform.db + +# JWT 密钥(生产环境请修改) +JWT_SECRET=change-this-to-a-strong-secret-in-production +JWT_EXPIRE_MINUTES=480 + +# ---------- AI 服务 ---------- +# LLM 服务地址(llamacpp 默认 8080) +LLM_BASE_URL=http://127.0.0.1:8080/v1 +LLM_API_KEY=local +LLM_MODEL=Qwen3.5-4B-Q4_K_M.gguf + +# 向量嵌入模型 +EMBED_MODEL=bge-m3 + +# ---------- 知识库 ---------- +KB_DATA_DIR=./data/kb_data +KNOWLEDGE_SOURCE_DIR=./assets/knowledge/source +TRAINING_MATERIALS_DIR=./assets/training/materials + +# ---------- 备份 ---------- +BACKUP_ENABLED=true +BACKUP_DIR=./data/backups +BACKUP_KEEP=7 +BACKUP_INTERVAL_HOURS=24 + +# ---------- 文件上传限制 ---------- +FILE_MAX_SIZE_DOC=209715200 +FILE_MAX_SIZE_VIDEO=2147483648 +CHUNK_THRESHOLD=104857600 + +# ---------- 文档转换 ---------- +LIBREOFFICE_BIN=libreoffice +PDFTOTEXT_BIN=pdftotext + +# ---------- 金蝶集成 ---------- +# KINGDEE_BASE_URL= +# KINGDEE_ACCOUNT_ID= +# KINGDEE_USERNAME= +# KINGDEE_PASSWORD= +# KINGDEE_LCID=2052 +# KINGDEE_TIMEOUT_SEC=15 diff --git a/README.md b/README.md new file mode 100644 index 0000000..6931721 --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +# EAI AgentPlatform + +智能 Agent 平台 — 基于 Go + Vue 3 构建。 + +## 快速启动 + +```bash +# 开发环境启动(前端 10231 + 后端 10232) +./start_dev_10231_10232.sh +``` + +## 端口说明 + +| 端口 | 服务 | 说明 | +|--------|--------------------------------|----------------------------| +| 10231 | 前端 Vite 开发服务器 | 本地开发,自动代理 API 请求 | +| 10232 | 后端 Go API 服务 | 本地开发端口 | +| 8080 | 外部 LLM 服务 (llamacpp) | AI 对话 API 入口 | +| 8081 | 外部 Embed 服务 (llamacpp) | 向量嵌入服务 | +| 11434 | 外部 Ollama 服务(可选) | 如使用 Ollama 路由则需运行 | + +> **注意**: 8080 和 8081 是外部 AI 服务的端口,平台后端会调用它们获取 LLM 对话和向量嵌入能力。 + +## 配置 + +复制环境变量模板并修改: + +```bash +cp .env.example .env +``` + +主要配置项见 [.env.example](.env.example)。 + +## 启动脚本参数 + +``` +--force 强制释放端口(包括非本项目进程) +--skip-preflight 跳过预检查 +--skip-smoke 跳过管理员登录冒烟测试 +--skip-build 不重新编译后端 +--help 显示帮助 +``` + +## 环境变量 + +| 变量 | 默认值 | 说明 | +|-------------------------|-----------------------------------------|-----------------------| +| PORT | 8080 | 后端 API 端口 | +| EAI_DEV_BIND_HOST | 127.0.0.1 | 开发绑定地址 | +| EAI_DEV_PUBLIC_HOST | 自动检测 | 公开访问地址 | +| LLM_BASE_URL | http://127.0.0.1:8080/v1 | LLM 服务地址 | +| EMBED_MODEL | bge-m3 | 向量嵌入模型名 | diff --git a/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/api/spec/config/domains.json b/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/api/spec/config/domains.json new file mode 100644 index 0000000..ee3fca1 --- /dev/null +++ b/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/api/spec/config/domains.json @@ -0,0 +1,42 @@ +{ + "description": "业务域配置。每个业务域有对应的标签和默认配置。", + "version": "1.0.0", + "domains": { + "AI": { + "label": "AI", + "image_domain_label": "AI 与人工智能", + "hotspot_sources": ["hackernews", "techcrunch", "arxiv"], + "default_keywords": ["大语言模型", "AI Agent", "多模态", "AIGC"] + }, + "medical": { + "label": "医疗", + "image_domain_label": "医疗健康", + "hotspot_sources": ["medical_news_today", "statnews"], + "default_keywords": ["新药研发", "临床实验", "健康管理", "医疗AI"] + }, + "insurance": { + "label": "保险", + "image_domain_label": "保险金融", + "hotspot_sources": ["reuters", "bloomberg"], + "default_keywords": ["健康险", "车险", "理赔", "保险科技"] + }, + "hr": { + "label": "HR", + "image_domain_label": "人力资源", + "hotspot_sources": ["linkedin", "shrm"], + "default_keywords": ["招聘", "培训", "绩效考核", "薪酬福利"] + }, + "legal": { + "label": "法务", + "image_domain_label": "法律合规", + "hotspot_sources": ["reuters", "bloomberg"], + "default_keywords": ["数据合规", "知识产权", "劳动法", "合同法"] + }, + "general": { + "label": "通用", + "image_domain_label": "通用主题", + "hotspot_sources": ["google_trending", "reddit"], + "default_keywords": [] + } + } +} \ No newline at end of file diff --git a/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/api/spec/config/image_prompt.json b/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/api/spec/config/image_prompt.json new file mode 100644 index 0000000..ada66d0 --- /dev/null +++ b/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/api/spec/config/image_prompt.json @@ -0,0 +1,54 @@ +{ + "description": "配图提示词生成规则。配图按字数均匀分布,封面图在标题下方,其余配图按 500 字间隔放置在所属小节开始处。", + "version": "1.0.0", + "distribution": { + "mode": "word_count", + "word_count_threshold": 500, + "cover_image": { + "enabled": true, + "key": "cover", + "slot_label": "TITLE_PIC", + "section_label": "封面配图", + "position": "after_title", + "size": "landscape_16_9" + }, + "content_images": { + "enabled": true, + "key_prefix": "section", + "size": "landscape_4_3", + "placement": "section_start", + "calculation": "accumulate_across_sections" + } + }, + "prompt_template": { + "description": "配图提示词模板。{index} 为序号(从 1 开始),{section_title} 为小节标题,{section_summary} 为段落摘要(截断至 120 字)。", + "format": "chinese", + "min_chars": 40, + "max_chars": 70, + "template": "真实人物纪实摄影,公众号文章配图,第{index}张配图核心画面,{section_summary},构图清晰,主体突出,电影感光影,细节丰富,高清" + }, + "bracket_format": { + "description": "配图提示词在正文中的嵌入格式。", + "cover": "[TITLE_PIC: {prompt}, size: {size}]", + "content": "[section_{{index:02d}}: {prompt}, size: {size}]" + }, + "validation": { + "checks": [ + "section_mapping_completeness", + "required_visual_elements", + "prompt_length", + "no_forbidden_elements" + ], + "required_elements": { + "subject": true, + "scene": true, + "lighting": true + }, + "forbidden_elements": [ + "text_overlay", + "collage_style" + ], + "retry_max": 2, + "retry_feedback_template": "配图提示词校验失败:{failed_checks}。请重新生成全部提示词并修复问题,保持段落映射不变。" + } +} \ No newline at end of file diff --git a/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/api/spec/config/policy.json b/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/api/spec/config/policy.json new file mode 100644 index 0000000..07a4675 --- /dev/null +++ b/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/api/spec/config/policy.json @@ -0,0 +1,39 @@ +{ + "description": "重试和降级策略。", + "version": "1.0.0", + "retries": { + "default_max_retries": 2, + "step_overrides": { + "topic_recommendation": 1, + "title_generation": 2, + "outline_generation": 2, + "content_creation": 2, + "image_prompt_generation": 2, + "image_generation": 2, + "preview_export": 0, + "page_markdown": 0 + }, + "policy": { + "regenerate_fully": true, + "include_error_message_in_feedback": true, + "allow_partial_patch": false + } + }, + "fallback": { + "mark_fallback_in_logs": true, + "step_overrides": { + "topic_recommendation": { + "allow_fallback": true, + "fallback_note": "使用关键词直接生成选题。" + }, + "title_generation": { + "allow_fallback": true, + "fallback_note": "使用标题模板生成。" + }, + "content_creation": { + "allow_fallback": false, + "fallback_note": "不降级,返回明确错误。" + } + } + } +} \ No newline at end of file diff --git a/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/api/spec/config/prompts/image_prompt_generate.prompt.md b/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/api/spec/config/prompts/image_prompt_generate.prompt.md new file mode 100644 index 0000000..28b87ff --- /dev/null +++ b/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/api/spec/config/prompts/image_prompt_generate.prompt.md @@ -0,0 +1,27 @@ +# Role +你是 `image_prompt_generate` 步骤的执行者,负责为公众号文章生成配图提示词。 + +# Goal +基于正文内容,按字数分布规则生成配图提示词,并将提示词以括号标记格式嵌入正文。 + +# 配图规则 +- 封面图:标题下方 1 张,`[TITLE_PIC: 提示词, size: landscape_16_9]` +- 正文配图:从正文开头累计字数,每 500 字生成 1 张 +- 每张配图放在所属小节(##)的开始处 +- 如果某节超过 500 字,则该节内按 500 字间隔插入多张 +- 提示词格式:`[section_{{index:02d}}: 提示词, size: landscape_4_3]` + +# 提示词模板 +``` +真实人物纪实摄影,公众号文章配图,第{N}张配图核心画面,{段落摘要120字},构图清晰,主体突出,电影感光影,细节丰富,高清 +``` + +# 约束 +- 每个提示词必须包含:主体、场景、光线 +- 每个提示词长度 40-70 个中文字 +- 禁止文字覆盖、拼贴风格 +- 段落映射必须完整(配图数 = 需要配图的小节数) + +# 输出 +- `image_prompts`: 配图提示词数组 [{key, section, prompt, size}] +- `content_with_prompts`: 带括号标记的完整正文 diff --git a/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/api/spec/config/skill.md b/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/api/spec/config/skill.md new file mode 100644 index 0000000..3965eeb --- /dev/null +++ b/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/api/spec/config/skill.md @@ -0,0 +1,62 @@ +# 公众号创作专员技能规范 + +## 技能标识 +- **Key**: weixin-public-account +- **Label**: 公众号创作专员 +- **Type**: longform-writing + +## 目的 +完成公众号文章的端到端创作:从选题、标题、提纲、正文、配图提示词、图片生成、预览导出到分页 Markdown。 + +## 适用场景 +- 热点选题驱动的内容创作 +- 关键词驱动的通用文章生成 +- 多业务域(AI/医疗/保险/HR/法务/通用)适配 + +## 工作流步骤 + +| 序号 | Step Key | 标签 | 前置条件 | 输出 | +|------|----------|------|----------|------| +| 0 | task_configuration | 任务配置 | — | 业务域、关键词、受众、语调等表单参数 | +| 1 | topic_recommendation | 选题推荐 | 配置完成 | 3 个选题建议(标题 + 理由 + 热度) | +| 2 | title_generation | 标题生成 | 有选题 | 3 个标题建议(偏点击/偏稳重/偏平衡) | +| 3 | outline_generation | 提纲生成 | 有标题 | Markdown 提纲结构 | +| 4 | content_creation | 正文创作 | 有提纲 | Markdown 正文(含 ## 小节标题) | +| 5 | image_prompt_generation | 配图提示词 | 有正文 | 配图提示词 + 带标记正文 | +| 6 | image_generation | 图片生成 | 有配图提示词 | 封面图 + 段落图 | +| 7 | preview_export | 预览导出 | 有正文 + 图片 | 预览 MD + 预览 HTML | +| 8 | page_markdown | 分页 Markdown | 有预览稿 | 分页 MD(含图片尺寸标注) | + +## 配图规则 + +### 分布规则 +1. 标题下方生成 1 张封面图(`cover` / `TITLE_PIC`),16:9 +2. 从正文开头累计字数,每 500 字生成 1 张配图 +3. 每张配图放在其所属小节(##)的**开始处** +4. 如果某节很长(>500 字),则该节内按 500 字间隔插入多张 +5. 如果某节不足 500 字,与下节合并累计,但不跨节重复 + +### 提示词模板 +``` +真实人物纪实摄影,公众号文章配图,第{N}张配图核心画面,{段落摘要120字},构图清晰,主体突出,电影感光影,细节丰富,高清 +``` + +### 括号标记格式 +``` +[TITLE_PIC: 提示词, size: landscape_16_9] +[section_01: 提示词, size: landscape_4_3] +``` + +## 业务域 +AI、医疗、保险、HR、法务、通用 + +## 重试策略 +- 热点抓取:最多 1 次重试 +- 选题推荐:最多 1 次重试 +- 标题/提纲/正文/配图:最多 2 次重试 +- 预览/分页:0 次重试 + +## 验证规则 +- 提纲:至少 3 个小节,每小节至少 2 个要点 +- 正文:至少 800 字,每个小节有实质性内容 +- 配图提示词:段落数 = 提示词数,每个提示词 40-70 字,必须包含主体/场景/光线 diff --git a/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/api/spec/config/validators/image_prompt.json b/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/api/spec/config/validators/image_prompt.json new file mode 100644 index 0000000..295aeaa --- /dev/null +++ b/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/api/spec/config/validators/image_prompt.json @@ -0,0 +1,32 @@ +{ + "description": "配图提示词验证规则。", + "version": "1.0.0", + "step_id": "image_prompt_generation", + "checks": [ + "section_mapping_completeness", + "required_visual_elements", + "prompt_length", + "no_forbidden_elements", + "bracket_format_valid" + ], + "section_mapping": { + "description": "配图提示词与正文段落的映射必须完整。", + "cover_required": true, + "content_images_required": true + }, + "visual_elements": { + "required": ["subject", "scene", "lighting"], + "forbidden": ["text_overlay", "collage_style"] + }, + "prompt_length": { + "min_chars": 40, + "max_chars": 70, + "count_mode": "chinese_characters" + }, + "bracket_format": { + "cover_pattern": "^\\[TITLE_PIC: .* size: landscape_16_9\\]$", + "content_pattern": "^\\[section_\\d{2}: .* size: landscape_4_3\\]$" + }, + "retry_feedback_template": "配图提示词校验失败:{failed_checks}。请重新生成全部提示词并修复问题。", + "retry_max": 2 +} \ No newline at end of file diff --git a/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/api/spec/config/workflow.json b/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/api/spec/config/workflow.json new file mode 100644 index 0000000..c26c344 --- /dev/null +++ b/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/api/spec/config/workflow.json @@ -0,0 +1,157 @@ +{ + "description": "公众号创作工作流步骤定义。步骤按顺序执行,前一步骤完成方可进入下一步骤。", + "version": "1.0.0", + "steps": [ + { + "key": "task_configuration", + "label": "任务配置", + "type": "form", + "order": 0, + "is_entry_point": true, + "description": "配置业务域、关键词、受众、语调等参数。" + }, + { + "key": "topic_recommendation", + "label": "选题推荐", + "type": "ai", + "order": 1, + "preconditions": ["task_configuration"], + "description": "基于业务域和关键词生成 3 个选题建议。" + }, + { + "key": "title_generation", + "label": "标题生成", + "type": "ai", + "order": 2, + "preconditions": ["topic_recommendation"], + "description": "基于选定选题生成 3 个标题建议(偏点击/偏稳重/偏平衡)。" + }, + { + "key": "outline_generation", + "label": "提纲生成", + "type": "ai", + "order": 3, + "preconditions": ["title_generation"], + "description": "基于选定标题生成 Markdown 提纲结构。" + }, + { + "key": "content_creation", + "label": "正文创作", + "type": "ai", + "order": 4, + "preconditions": ["outline_generation"], + "description": "基于选定提纲和配置参数生成正文。" + }, + { + "key": "image_prompt_generation", + "label": "配图提示词", + "type": "ai", + "order": 5, + "preconditions": ["content_creation"], + "description": "基于正文按字数分布生成配图提示词,并嵌入带标记正文。" + }, + { + "key": "image_generation", + "label": "图片生成", + "type": "external", + "order": 6, + "preconditions": ["image_prompt_generation"], + "description": "基于配图提示词调用外部图片生成 API。" + }, + { + "key": "preview_export", + "label": "预览导出", + "type": "render", + "order": 7, + "preconditions": ["content_creation", "image_generation"], + "description": "生成预览 MD、预览 HTML 和分页 MD。" + }, + { + "key": "page_markdown", + "label": "分页 Markdown", + "type": "render", + "order": 8, + "preconditions": ["preview_export"], + "description": "生成带图片尺寸标注的分页 Markdown。" + } + ], + "forms": { + "task_configuration": { + "fields": [ + { + "key": "business_domain", + "label": "业务域", + "type": "select", + "required": true, + "options": ["AI", "医疗", "保险", "HR", "法务", "通用"] + }, + { + "key": "keyword", + "label": "关键词", + "type": "text", + "required": true + }, + { + "key": "audience", + "label": "目标受众", + "type": "text", + "default": "公众号读者", + "required": false + }, + { + "key": "goal", + "label": "写作目标", + "type": "text", + "required": false + }, + { + "key": "tone", + "label": "语调风格", + "type": "select", + "default": "专业但好懂", + "options": ["专业但好懂", "轻松有趣", "严肃权威", "故事叙述", "数据驱动"] + }, + { + "key": "requirements", + "label": "特殊要求", + "type": "textarea", + "required": false + }, + { + "key": "outline_style", + "label": "提纲风格", + "type": "select", + "default": "问题拆解型", + "options": ["问题拆解型", "对比分析型", "案例驱动型", "步骤教程型"] + }, + { + "key": "outline_words", + "label": "提纲字数", + "type": "number", + "default": 800, + "min": 300, + "max": 2000, + "required": false + }, + { + "key": "content_target_words", + "label": "正文目标字数", + "type": "number", + "default": 1400, + "min": 500, + "max": 5000, + "required": false + }, + { + "key": "max_content_images", + "label": "最大配图数(可选,0 表示无上限)", + "type": "number", + "default": 0, + "min": 0, + "max": 20, + "required": false + } + ] + } + } +} \ No newline at end of file diff --git a/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/api/weixin_public_account_image_prompt_config.go b/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/api/weixin_public_account_image_prompt_config.go new file mode 100644 index 0000000..3922192 --- /dev/null +++ b/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/api/weixin_public_account_image_prompt_config.go @@ -0,0 +1,309 @@ +package weixinpublicaccountapi + +import ( + "embed" + "encoding/json" + "fmt" + "strings" +) + +//go:embed spec/config/*.json spec/config/validators/*.json spec/config/prompts/*.md +var specConfigFS embed.FS + +// imagePromptConfig 从 JSON 配置文件读取的配图提示词规则。 +type imagePromptConfig struct { + Distribution distributionConfig `json:"distribution"` + PromptTemplate promptTemplateConfig `json:"prompt_template"` + BracketFormat bracketFormatConfig `json:"bracket_format"` + Validation validationConfig `json:"validation"` +} + +type distributionConfig struct { + Mode string `json:"mode"` + WordCount int `json:"word_count_threshold"` + CoverImage coverImageConfig `json:"cover_image"` + ContentImages contentImagesConfig `json:"content_images"` +} + +type coverImageConfig struct { + Enabled bool `json:"enabled"` + Key string `json:"key"` + SlotLabel string `json:"slot_label"` + SectionLbl string `json:"section_label"` + Position string `json:"position"` + Size string `json:"size"` +} + +type contentImagesConfig struct { + Enabled bool `json:"enabled"` + KeyPrefix string `json:"key_prefix"` + Size string `json:"size"` + Placement string `json:"placement"` + Calculation string `json:"calculation"` +} + +type promptTemplateConfig struct { + Format string `json:"format"` + MinChars int `json:"min_chars"` + MaxChars int `json:"max_chars"` + Template string `json:"template"` +} + +type bracketFormatConfig struct { + Cover string `json:"cover"` + Content string `json:"content"` +} + +type validationConfig struct { + Checks []string `json:"checks"` + RequiredElements map[string]bool `json:"required_elements"` + ForbiddenElements []string `json:"forbidden_elements"` + RetryMax int `json:"retry_max"` + RetryFeedbackTemplate string `json:"retry_feedback_template"` +} + +// imagePromptRule 是运行时的实例化规则(含业务域标签、关键词等运行时参数)。 +type imagePromptRule struct { + Keyword string + BusinessDomain string + Template promptTemplateConfig + Distribution distributionConfig + BracketFormat bracketFormatConfig +} + +var weixinPublicAccountImagePromptConfig *imagePromptConfig +var weixinPublicAccountImagePromptRuleCache map[string]*imagePromptRule + +func init() { + weixinPublicAccountImagePromptConfig = loadWeixinPublicAccountImagePromptConfig() + weixinPublicAccountImagePromptRuleCache = make(map[string]*imagePromptRule) +} + +// loadWeixinPublicAccountImagePromptConfig 从嵌入的 spec 目录加载 JSON 配置。 +func loadWeixinPublicAccountImagePromptConfig() *imagePromptConfig { + data, err := specConfigFS.ReadFile("spec/config/image_prompt.json") + if err != nil { + // 如果 spec 目录不存在或配置缺失,回退到内联默认配置 + return defaultImagePromptConfig() + } + var cfg imagePromptConfig + if err := json.Unmarshal(data, &cfg); err != nil { + return defaultImagePromptConfig() + } + return &cfg +} + +func defaultImagePromptConfig() *imagePromptConfig { + return &imagePromptConfig{ + Distribution: distributionConfig{ + Mode: "word_count", + WordCount: 500, + CoverImage: coverImageConfig{ + Enabled: true, + Key: "cover", + SlotLabel: "TITLE_PIC", + SectionLbl: "封面配图", + Position: "after_title", + Size: "landscape_16_9", + }, + ContentImages: contentImagesConfig{ + Enabled: true, + KeyPrefix: "section", + Size: "landscape_4_3", + Placement: "section_start", + Calculation: "accumulate_across_sections", + }, + }, + PromptTemplate: promptTemplateConfig{ + Format: "chinese", + MinChars: 40, + MaxChars: 70, + Template: "真实人物纪实摄影,公众号文章配图,第{N}张配图核心画面,{段落摘要120字},构图清晰,主体突出,电影感光影,细节丰富,高清", + }, + BracketFormat: bracketFormatConfig{ + Cover: "[TITLE_PIC: {prompt}, size: {size}]", + Content: "[section_{{index:02d}}: {prompt}, size: {size}]", + }, + Validation: validationConfig{ + Checks: []string{ + "section_mapping_completeness", + "required_visual_elements", + "prompt_length", + "no_forbidden_elements", + }, + RequiredElements: map[string]bool{ + "subject": true, + "scene": true, + "lighting": true, + }, + ForbiddenElements: []string{ + "text_overlay", + "collage_style", + }, + RetryMax: 2, + }, + } +} + +// getWeixinPublicAccountImagePromptRule 按业务域获取缓存的规则。 +func getWeixinPublicAccountImagePromptRule(workflow *weixinPublicAccountWorkflowState) *imagePromptRule { + key := workflow.Form.BusinessDomain + if rule, ok := weixinPublicAccountImagePromptRuleCache[key]; ok { + return rule + } + cfg := weixinPublicAccountImagePromptConfig + rule := &imagePromptRule{ + Keyword: workflow.Form.Keyword, + BusinessDomain: workflow.Form.BusinessDomain, + Template: cfg.PromptTemplate, + Distribution: cfg.Distribution, + BracketFormat: cfg.BracketFormat, + } + weixinPublicAccountImagePromptRuleCache[key] = rule + return rule +} + +// buildWeixinPublicAccountImagePromptsV2 新版配图提示词生成逻辑,基于 JSON 配置。 +func buildWeixinPublicAccountImagePromptsV2(workflow *weixinPublicAccountWorkflowState) []weixinPublicAccountImagePrompt { + rule := getWeixinPublicAccountImagePromptRule(workflow) + cfg := &rule.Distribution + prompts := []weixinPublicAccountImagePrompt{} + + // 1. 封面图 + if cfg.CoverImage.Enabled { + prompts = append(prompts, weixinPublicAccountImagePrompt{ + Key: cfg.CoverImage.Key, + Section: cfg.CoverImage.SectionLbl, + Prompt: buildCoverImagePrompt(workflow, rule), + Size: cfg.CoverImage.Size, + }) + } + + // 2. 正文配图(按 500 字分布) + sections := extractWeixinPublicAccountSections(workflow.Shared.Content) + contentImageIndex := 1 + wordCountAccumulator := 0 + + // 根据计算模式决定如何遍历 + if cfg.ContentImages.Calculation == "accumulate_across_sections" { + // 从正文开头累计字数,每 500 字生成一张配图,放在小节开始处 + for _, section := range sections { + sectionWordCount := countChineseWords(section.Content) + + // 如果这是一个新的配图分配点 + if sectionWordCount > 0 { + wordCountAccumulator += sectionWordCount + // 计算这之前应该有多少张配图 + expectedImages := wordCountAccumulator / cfg.WordCount + // 添加配图直到达到 expectedImages + for wordCountAccumulator >= expectedImages*cfg.WordCount && contentImageIndex <= expectedImages { + prompt := buildContentImagePrompt(workflow, section, contentImageIndex, rule) + prompts = append(prompts, weixinPublicAccountImagePrompt{ + Key: fmt.Sprintf("%s_%02d", cfg.ContentImages.KeyPrefix, contentImageIndex), + Section: section.Title, + Prompt: prompt, + Size: cfg.ContentImages.Size, + }) + contentImageIndex++ + } + } + } + } + + return prompts +} + +// buildCoverImagePrompt 生成封面图提示词。 +func buildCoverImagePrompt(workflow *weixinPublicAccountWorkflowState, rule *imagePromptRule) string { + title := firstNonEmpty(workflow.Shared.SelectedTitle, workflow.Form.Keyword, "公众号文章") + + // 从 JSON 模板构建(当前回退到硬编码模板) + template := rule.Template.Template + if template == "" { + // 回退到当前硬编码的英文模板 + return fmt.Sprintf("realistic editorial illustration, Chinese business article cover, %s, focus on %s, %s, clean composition, natural lighting, brand-safe, no text, no watermark", + weixinPublicAccountBusinessDomainLabel(workflow.Form.BusinessDomain), + title, + "公众号封面主视觉", + ) + } + + return fmt.Sprintf(template, 1, title, fmt.Sprintf("%s(截断至120字)", title)) +} + +// buildContentImagePrompt 生成正文配图提示词。 +func buildContentImagePrompt(workflow *weixinPublicAccountWorkflowState, section weixinPublicAccountSection, index int, rule *imagePromptRule) string { + summary := truncateChinese(strings.Join(section.Content, " "), 120) + + // 从 JSON 模板构建 + template := rule.Template.Template + if template == "" { + // 回退到当前硬编码的英文模板 + return fmt.Sprintf("realistic editorial illustration, Chinese business article cover, %s, focus on %s, %s, clean composition, natural lighting, brand-safe, no text, no watermark", + weixinPublicAccountBusinessDomainLabel(workflow.Form.BusinessDomain), + firstNonEmpty(section.Title, workflow.Form.Keyword), + firstNonEmpty(summary, workflow.Form.Tone), + ) + } + + return fmt.Sprintf(template, index, firstNonEmpty(section.Title, workflow.Form.Keyword), summary) +} + +// countChineseWords 计算内容的中文字数。 +func countChineseWords(content []string) int { + text := strings.Join(content, "\n") + count := 0 + for _, r := range text { + if r >= '\u4e00' && r <= '\u9fff' { + count++ + } + } + return count +} + +// truncateChinese 截断中文字符串到指定长度。 +func truncateChinese(s string, maxLen int) string { + runes := []rune(s) + if len(runes) > maxLen { + return string(runes[:maxLen]) + } + return s +} + +// renderWeixinPublicAccountImagePromptFromJSON 基于 JSON 配置渲染带标记的正文。 +func renderWeixinPublicAccountImagePromptFromJSON(content string, prompts []weixinPublicAccountImagePrompt) string { + cfg := weixinPublicAccountImagePromptConfig + content = strings.TrimSpace(content) + if content == "" { + return "" + } + blocks := []string{content, "", "## 配图提示词"} + + // 使用 JSON 配置中的括号格式 + sections := extractWeixinPublicAccountSections(content) + sectionIdx := 0 + + for _, prompt := range prompts { + if prompt.Key == cfg.Distribution.CoverImage.Key { + // 封面图:插入在标题下方 + block := replacePlaceholder(cfg.BracketFormat.Cover, "{prompt}", prompt.Prompt) + block = replacePlaceholder(block, "{size}", cfg.Distribution.CoverImage.Size) + blocks = append(blocks, block) + } else if strings.HasPrefix(prompt.Key, cfg.Distribution.ContentImages.KeyPrefix) { + // 正文配图:插入在小节开始处 + if sectionIdx < len(sections) { + block := replacePlaceholder(cfg.BracketFormat.Content, "{prompt}", prompt.Prompt) + block = replacePlaceholder(block, "{size}", cfg.Distribution.ContentImages.Size) + block = replacePlaceholder(block, "{index}", fmt.Sprintf("%02d", sectionIdx+1)) + blocks = append(blocks, block) + } + sectionIdx++ + } + } + + return strings.Join(blocks, "\n") +} + +func replacePlaceholder(s, placeholder, value string) string { + return strings.ReplaceAll(s, placeholder, value) +} diff --git a/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/api/weixin_public_account_workflow.go b/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/api/weixin_public_account_workflow.go index a63d6e7..49542ec 100644 --- a/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/api/weixin_public_account_workflow.go +++ b/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/api/weixin_public_account_workflow.go @@ -153,6 +153,7 @@ type weixinPublicAccountImagePrompt struct { Key string `json:"key"` Section string `json:"section"` Prompt string `json:"prompt"` + Size string `json:"size"` } type weixinPublicAccountImageRef struct { diff --git a/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/spec/README.md b/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/spec/README.md new file mode 100644 index 0000000..9a0c2cf --- /dev/null +++ b/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/spec/README.md @@ -0,0 +1,60 @@ +# 公众号创作专员 - 规范定义 + +> 这是 pj0235 的第一个"配置驱动"专员样板。参考 pj0012 的 JSON 配置体系设计。 + +## 目录结构 + +``` +spec/ +├── config/ +│ ├── skill.md # 技能描述 + 执行策略(人可读 + LLM 可读) +│ ├── workflow.json # 工作流步骤定义(步骤 key、标签、前置条件、表单) +│ ├── image_prompt.json # 配图提示词规则(分布、模板、验证) +│ ├── domains.json # 业务域配置 +│ └── policy.json # 重试和降级策略 +├── config/prompts/ +│ └── image_prompt_generate.prompt.md # 配图提示词生成 prompt 模板 +├── config/validators/ +│ └── image_prompt.json # 配图提示词验证规则 +└── README.md +``` + +## 设计原则 + +1. **三层分离**: + - `skill.md` — 技能描述层(人可读 + LLM 可读) + - `*.json` — 数据配置层(机器可读,热更新) + - `*.prompt.md` — 提示词模板层(YAML 变量注入) + +2. **代码是运行时适配器**: + - 按路径读取 JSON/文本 + - 将运行时参数注入模板 + - 执行验证器逻辑 + - 控制重试/降级循环 + - 调用 LLM + +3. **不硬编码业务内容**: + - 业务规则变更通过修改 JSON/Markdown 实现 + - 代码层只做通用适配 + +## 配图规则(核心示例) + +配图提示词规则完全外置在 `image_prompt.json` 中,包括: + +- 分布规则:封面图 + 每 500 字 1 张 +- 提示词模板:中文模板 + 最小/最大字数 +- 括号标记格式:`[TITLE_PIC: 提示词, size: landscape_16_9]` +- 验证规则:主体/场景/光线必填,文字覆盖/拼贴禁用 +- 重试策略:最多 2 次 + +## 如何参考 + +完整的学习分析见:`../../../../reference/pj0012-wechat-image-spec.md` + +## 使用方式 + +Go 代码通过 `embed.FS` 嵌入这些文件,运行时: +1. 读取 JSON 配置 → 获取业务规则 +2. 读取 prompt 模板 → 注入运行时参数 → 组装最终 prompt +3. 读取验证器 → 执行验证逻辑 +4. 读取策略 → 控制重试/降级循环 diff --git a/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/spec/config/domains.json b/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/spec/config/domains.json new file mode 100644 index 0000000..ee3fca1 --- /dev/null +++ b/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/spec/config/domains.json @@ -0,0 +1,42 @@ +{ + "description": "业务域配置。每个业务域有对应的标签和默认配置。", + "version": "1.0.0", + "domains": { + "AI": { + "label": "AI", + "image_domain_label": "AI 与人工智能", + "hotspot_sources": ["hackernews", "techcrunch", "arxiv"], + "default_keywords": ["大语言模型", "AI Agent", "多模态", "AIGC"] + }, + "medical": { + "label": "医疗", + "image_domain_label": "医疗健康", + "hotspot_sources": ["medical_news_today", "statnews"], + "default_keywords": ["新药研发", "临床实验", "健康管理", "医疗AI"] + }, + "insurance": { + "label": "保险", + "image_domain_label": "保险金融", + "hotspot_sources": ["reuters", "bloomberg"], + "default_keywords": ["健康险", "车险", "理赔", "保险科技"] + }, + "hr": { + "label": "HR", + "image_domain_label": "人力资源", + "hotspot_sources": ["linkedin", "shrm"], + "default_keywords": ["招聘", "培训", "绩效考核", "薪酬福利"] + }, + "legal": { + "label": "法务", + "image_domain_label": "法律合规", + "hotspot_sources": ["reuters", "bloomberg"], + "default_keywords": ["数据合规", "知识产权", "劳动法", "合同法"] + }, + "general": { + "label": "通用", + "image_domain_label": "通用主题", + "hotspot_sources": ["google_trending", "reddit"], + "default_keywords": [] + } + } +} \ No newline at end of file diff --git a/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/spec/config/image_prompt.json b/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/spec/config/image_prompt.json new file mode 100644 index 0000000..ada66d0 --- /dev/null +++ b/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/spec/config/image_prompt.json @@ -0,0 +1,54 @@ +{ + "description": "配图提示词生成规则。配图按字数均匀分布,封面图在标题下方,其余配图按 500 字间隔放置在所属小节开始处。", + "version": "1.0.0", + "distribution": { + "mode": "word_count", + "word_count_threshold": 500, + "cover_image": { + "enabled": true, + "key": "cover", + "slot_label": "TITLE_PIC", + "section_label": "封面配图", + "position": "after_title", + "size": "landscape_16_9" + }, + "content_images": { + "enabled": true, + "key_prefix": "section", + "size": "landscape_4_3", + "placement": "section_start", + "calculation": "accumulate_across_sections" + } + }, + "prompt_template": { + "description": "配图提示词模板。{index} 为序号(从 1 开始),{section_title} 为小节标题,{section_summary} 为段落摘要(截断至 120 字)。", + "format": "chinese", + "min_chars": 40, + "max_chars": 70, + "template": "真实人物纪实摄影,公众号文章配图,第{index}张配图核心画面,{section_summary},构图清晰,主体突出,电影感光影,细节丰富,高清" + }, + "bracket_format": { + "description": "配图提示词在正文中的嵌入格式。", + "cover": "[TITLE_PIC: {prompt}, size: {size}]", + "content": "[section_{{index:02d}}: {prompt}, size: {size}]" + }, + "validation": { + "checks": [ + "section_mapping_completeness", + "required_visual_elements", + "prompt_length", + "no_forbidden_elements" + ], + "required_elements": { + "subject": true, + "scene": true, + "lighting": true + }, + "forbidden_elements": [ + "text_overlay", + "collage_style" + ], + "retry_max": 2, + "retry_feedback_template": "配图提示词校验失败:{failed_checks}。请重新生成全部提示词并修复问题,保持段落映射不变。" + } +} \ No newline at end of file diff --git a/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/spec/config/policy.json b/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/spec/config/policy.json new file mode 100644 index 0000000..07a4675 --- /dev/null +++ b/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/spec/config/policy.json @@ -0,0 +1,39 @@ +{ + "description": "重试和降级策略。", + "version": "1.0.0", + "retries": { + "default_max_retries": 2, + "step_overrides": { + "topic_recommendation": 1, + "title_generation": 2, + "outline_generation": 2, + "content_creation": 2, + "image_prompt_generation": 2, + "image_generation": 2, + "preview_export": 0, + "page_markdown": 0 + }, + "policy": { + "regenerate_fully": true, + "include_error_message_in_feedback": true, + "allow_partial_patch": false + } + }, + "fallback": { + "mark_fallback_in_logs": true, + "step_overrides": { + "topic_recommendation": { + "allow_fallback": true, + "fallback_note": "使用关键词直接生成选题。" + }, + "title_generation": { + "allow_fallback": true, + "fallback_note": "使用标题模板生成。" + }, + "content_creation": { + "allow_fallback": false, + "fallback_note": "不降级,返回明确错误。" + } + } + } +} \ No newline at end of file diff --git a/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/spec/config/prompts/image_prompt_generate.prompt.md b/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/spec/config/prompts/image_prompt_generate.prompt.md new file mode 100644 index 0000000..28b87ff --- /dev/null +++ b/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/spec/config/prompts/image_prompt_generate.prompt.md @@ -0,0 +1,27 @@ +# Role +你是 `image_prompt_generate` 步骤的执行者,负责为公众号文章生成配图提示词。 + +# Goal +基于正文内容,按字数分布规则生成配图提示词,并将提示词以括号标记格式嵌入正文。 + +# 配图规则 +- 封面图:标题下方 1 张,`[TITLE_PIC: 提示词, size: landscape_16_9]` +- 正文配图:从正文开头累计字数,每 500 字生成 1 张 +- 每张配图放在所属小节(##)的开始处 +- 如果某节超过 500 字,则该节内按 500 字间隔插入多张 +- 提示词格式:`[section_{{index:02d}}: 提示词, size: landscape_4_3]` + +# 提示词模板 +``` +真实人物纪实摄影,公众号文章配图,第{N}张配图核心画面,{段落摘要120字},构图清晰,主体突出,电影感光影,细节丰富,高清 +``` + +# 约束 +- 每个提示词必须包含:主体、场景、光线 +- 每个提示词长度 40-70 个中文字 +- 禁止文字覆盖、拼贴风格 +- 段落映射必须完整(配图数 = 需要配图的小节数) + +# 输出 +- `image_prompts`: 配图提示词数组 [{key, section, prompt, size}] +- `content_with_prompts`: 带括号标记的完整正文 diff --git a/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/spec/config/skill.md b/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/spec/config/skill.md new file mode 100644 index 0000000..3965eeb --- /dev/null +++ b/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/spec/config/skill.md @@ -0,0 +1,62 @@ +# 公众号创作专员技能规范 + +## 技能标识 +- **Key**: weixin-public-account +- **Label**: 公众号创作专员 +- **Type**: longform-writing + +## 目的 +完成公众号文章的端到端创作:从选题、标题、提纲、正文、配图提示词、图片生成、预览导出到分页 Markdown。 + +## 适用场景 +- 热点选题驱动的内容创作 +- 关键词驱动的通用文章生成 +- 多业务域(AI/医疗/保险/HR/法务/通用)适配 + +## 工作流步骤 + +| 序号 | Step Key | 标签 | 前置条件 | 输出 | +|------|----------|------|----------|------| +| 0 | task_configuration | 任务配置 | — | 业务域、关键词、受众、语调等表单参数 | +| 1 | topic_recommendation | 选题推荐 | 配置完成 | 3 个选题建议(标题 + 理由 + 热度) | +| 2 | title_generation | 标题生成 | 有选题 | 3 个标题建议(偏点击/偏稳重/偏平衡) | +| 3 | outline_generation | 提纲生成 | 有标题 | Markdown 提纲结构 | +| 4 | content_creation | 正文创作 | 有提纲 | Markdown 正文(含 ## 小节标题) | +| 5 | image_prompt_generation | 配图提示词 | 有正文 | 配图提示词 + 带标记正文 | +| 6 | image_generation | 图片生成 | 有配图提示词 | 封面图 + 段落图 | +| 7 | preview_export | 预览导出 | 有正文 + 图片 | 预览 MD + 预览 HTML | +| 8 | page_markdown | 分页 Markdown | 有预览稿 | 分页 MD(含图片尺寸标注) | + +## 配图规则 + +### 分布规则 +1. 标题下方生成 1 张封面图(`cover` / `TITLE_PIC`),16:9 +2. 从正文开头累计字数,每 500 字生成 1 张配图 +3. 每张配图放在其所属小节(##)的**开始处** +4. 如果某节很长(>500 字),则该节内按 500 字间隔插入多张 +5. 如果某节不足 500 字,与下节合并累计,但不跨节重复 + +### 提示词模板 +``` +真实人物纪实摄影,公众号文章配图,第{N}张配图核心画面,{段落摘要120字},构图清晰,主体突出,电影感光影,细节丰富,高清 +``` + +### 括号标记格式 +``` +[TITLE_PIC: 提示词, size: landscape_16_9] +[section_01: 提示词, size: landscape_4_3] +``` + +## 业务域 +AI、医疗、保险、HR、法务、通用 + +## 重试策略 +- 热点抓取:最多 1 次重试 +- 选题推荐:最多 1 次重试 +- 标题/提纲/正文/配图:最多 2 次重试 +- 预览/分页:0 次重试 + +## 验证规则 +- 提纲:至少 3 个小节,每小节至少 2 个要点 +- 正文:至少 800 字,每个小节有实质性内容 +- 配图提示词:段落数 = 提示词数,每个提示词 40-70 字,必须包含主体/场景/光线 diff --git a/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/spec/config/validators/image_prompt.json b/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/spec/config/validators/image_prompt.json new file mode 100644 index 0000000..295aeaa --- /dev/null +++ b/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/spec/config/validators/image_prompt.json @@ -0,0 +1,32 @@ +{ + "description": "配图提示词验证规则。", + "version": "1.0.0", + "step_id": "image_prompt_generation", + "checks": [ + "section_mapping_completeness", + "required_visual_elements", + "prompt_length", + "no_forbidden_elements", + "bracket_format_valid" + ], + "section_mapping": { + "description": "配图提示词与正文段落的映射必须完整。", + "cover_required": true, + "content_images_required": true + }, + "visual_elements": { + "required": ["subject", "scene", "lighting"], + "forbidden": ["text_overlay", "collage_style"] + }, + "prompt_length": { + "min_chars": 40, + "max_chars": 70, + "count_mode": "chinese_characters" + }, + "bracket_format": { + "cover_pattern": "^\\[TITLE_PIC: .* size: landscape_16_9\\]$", + "content_pattern": "^\\[section_\\d{2}: .* size: landscape_4_3\\]$" + }, + "retry_feedback_template": "配图提示词校验失败:{failed_checks}。请重新生成全部提示词并修复问题。", + "retry_max": 2 +} \ No newline at end of file diff --git a/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/spec/config/workflow.json b/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/spec/config/workflow.json new file mode 100644 index 0000000..c26c344 --- /dev/null +++ b/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/spec/config/workflow.json @@ -0,0 +1,157 @@ +{ + "description": "公众号创作工作流步骤定义。步骤按顺序执行,前一步骤完成方可进入下一步骤。", + "version": "1.0.0", + "steps": [ + { + "key": "task_configuration", + "label": "任务配置", + "type": "form", + "order": 0, + "is_entry_point": true, + "description": "配置业务域、关键词、受众、语调等参数。" + }, + { + "key": "topic_recommendation", + "label": "选题推荐", + "type": "ai", + "order": 1, + "preconditions": ["task_configuration"], + "description": "基于业务域和关键词生成 3 个选题建议。" + }, + { + "key": "title_generation", + "label": "标题生成", + "type": "ai", + "order": 2, + "preconditions": ["topic_recommendation"], + "description": "基于选定选题生成 3 个标题建议(偏点击/偏稳重/偏平衡)。" + }, + { + "key": "outline_generation", + "label": "提纲生成", + "type": "ai", + "order": 3, + "preconditions": ["title_generation"], + "description": "基于选定标题生成 Markdown 提纲结构。" + }, + { + "key": "content_creation", + "label": "正文创作", + "type": "ai", + "order": 4, + "preconditions": ["outline_generation"], + "description": "基于选定提纲和配置参数生成正文。" + }, + { + "key": "image_prompt_generation", + "label": "配图提示词", + "type": "ai", + "order": 5, + "preconditions": ["content_creation"], + "description": "基于正文按字数分布生成配图提示词,并嵌入带标记正文。" + }, + { + "key": "image_generation", + "label": "图片生成", + "type": "external", + "order": 6, + "preconditions": ["image_prompt_generation"], + "description": "基于配图提示词调用外部图片生成 API。" + }, + { + "key": "preview_export", + "label": "预览导出", + "type": "render", + "order": 7, + "preconditions": ["content_creation", "image_generation"], + "description": "生成预览 MD、预览 HTML 和分页 MD。" + }, + { + "key": "page_markdown", + "label": "分页 Markdown", + "type": "render", + "order": 8, + "preconditions": ["preview_export"], + "description": "生成带图片尺寸标注的分页 Markdown。" + } + ], + "forms": { + "task_configuration": { + "fields": [ + { + "key": "business_domain", + "label": "业务域", + "type": "select", + "required": true, + "options": ["AI", "医疗", "保险", "HR", "法务", "通用"] + }, + { + "key": "keyword", + "label": "关键词", + "type": "text", + "required": true + }, + { + "key": "audience", + "label": "目标受众", + "type": "text", + "default": "公众号读者", + "required": false + }, + { + "key": "goal", + "label": "写作目标", + "type": "text", + "required": false + }, + { + "key": "tone", + "label": "语调风格", + "type": "select", + "default": "专业但好懂", + "options": ["专业但好懂", "轻松有趣", "严肃权威", "故事叙述", "数据驱动"] + }, + { + "key": "requirements", + "label": "特殊要求", + "type": "textarea", + "required": false + }, + { + "key": "outline_style", + "label": "提纲风格", + "type": "select", + "default": "问题拆解型", + "options": ["问题拆解型", "对比分析型", "案例驱动型", "步骤教程型"] + }, + { + "key": "outline_words", + "label": "提纲字数", + "type": "number", + "default": 800, + "min": 300, + "max": 2000, + "required": false + }, + { + "key": "content_target_words", + "label": "正文目标字数", + "type": "number", + "default": 1400, + "min": 500, + "max": 5000, + "required": false + }, + { + "key": "max_content_images", + "label": "最大配图数(可选,0 表示无上限)", + "type": "number", + "default": 0, + "min": 0, + "max": 20, + "required": false + } + ] + } + } +} \ No newline at end of file diff --git a/eai_agentplatform/frontend/src/components/chat/WeixinPublicAccountSpecialistPanel.vue b/eai_agentplatform/frontend/src/components/chat/WeixinPublicAccountSpecialistPanel.vue index da44a72..0ea13a0 100644 --- a/eai_agentplatform/frontend/src/components/chat/WeixinPublicAccountSpecialistPanel.vue +++ b/eai_agentplatform/frontend/src/components/chat/WeixinPublicAccountSpecialistPanel.vue @@ -158,9 +158,9 @@ class="oa-btn oa-btn-tiny" type="button" :disabled="previewLoading" - @click="openArtifactPreview(file)" + @click="downloadFile(file)" > - 预览 + 下载 @@ -169,6 +169,50 @@ + + + + + 整体导出 + + + + 预览 MD + + + 预览 HTML + + + 分页 MD + + + Word (.docx) + + + + 没有可导出的内容,先运行创作流程。 + + @@ -1085,6 +1129,40 @@ async function download(format) { } } +// 从产物文件下载(HTML / MD 直接下载,图片通过 API 获取) +async function downloadFile(file) { + loading.value = true + try { + let blob = null + let filename = file.name || 'download' + + if (file.kind === 'html' && file.html) { + blob = new Blob([file.html], { type: 'text/html' }) + } else if (file.kind === 'markdown' && file.text) { + blob = new Blob([file.text], { type: 'text/markdown' }) + } else if (file.kind === 'image' && file.url) { + blob = await fetchWeixinPublicAccountGeneratedImage(file.url) + if (file.name) filename = file.name + } + + if (blob) { + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = filename + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) + ElMessage.success('下载成功') + } + } catch (error) { + ElMessage.error(error?.message || '下载失败') + } finally { + loading.value = false + } +} + function exportFileName(format) { if (format === 'preview_html') return 'weixin-public-account-preview.html' if (format === 'pagemd') return 'weixin-public-account-pagemd.md' diff --git a/eai_agentplatform/frontend/src/views/workbench/SmartAssistantPage.vue b/eai_agentplatform/frontend/src/views/workbench/SmartAssistantPage.vue index c48d7c1..31a80ce 100644 --- a/eai_agentplatform/frontend/src/views/workbench/SmartAssistantPage.vue +++ b/eai_agentplatform/frontend/src/views/workbench/SmartAssistantPage.vue @@ -474,7 +474,13 @@ function renderMarkdownBlock(block) { const headingMatch = trimmed.match(/^(#{1,3})\s+(.+)$/) if (headingMatch) { const level = headingMatch[1].length - return `${renderInlineMarkdown(headingMatch[2])}` + const heading = `${renderInlineMarkdown(headingMatch[2])}` + // If the block has more lines after the heading, render them as additional content + const afterHeading = trimmed.split('\n', 2)[1] + if (afterHeading && afterHeading.trim()) { + return heading + ' ' + renderMarkdownBlock(afterHeading.trim()) + } + return heading } const lines = trimmed.split('\n') @@ -792,7 +798,7 @@ function getWeixinPublicAccountStep(payload, stepKey) { return payload?.workflow?.steps?.[stepKey] || {} } -function trimWeixinPublicAccountText(value, maxLength = 180) { +function trimWeixinPublicAccountText(value, maxLength = 360) { const text = String(value || '').trim() if (!text) return '' return text.length > maxLength ? `${text.slice(0, maxLength).trim()}...` : text @@ -876,7 +882,7 @@ function buildWeixinPublicAccountOutlineStageMessage(payload) { '### 3. 提纲步骤已完成', sections.length ? `本轮提纲主结构:\n${formatWeixinPublicAccountBulletList(sections)}` - : trimWeixinPublicAccountText(outline, 220) || '本轮提纲已生成。', + : trimWeixinPublicAccountText(outline, 500) || '本轮提纲已生成。', ].filter(Boolean).join('\n\n'), timestamp: new Date().toLocaleTimeString(), } @@ -892,7 +898,7 @@ function buildWeixinPublicAccountContentStageMessage(payload) { .replace(/^>\s+.*$/gm, '') .replace(/\n{2,}/g, '\n') .trim(), - 220, + 500, ) return { role: 'assistant', @@ -910,15 +916,18 @@ function buildWeixinPublicAccountImagePromptStageMessage(payload) { const output = step.output || {} const count = Number(output.count || 0) const prompts = Array.isArray(output.image_prompts) ? output.image_prompts.slice(0, 4) : [] + const lines = [ + `已提炼 ${count} 条配图提示词。`, + ...(prompts.length + ? prompts.map((item, index) => `${index + 1}. [${item?.section || '段落配图'}] ${trimWeixinPublicAccountText(item?.prompt || '', 80)}`) + : []), + ] return { role: 'assistant', content: [ '### 5. 配图提示词步骤已完成', - count > 0 ? `已提炼 ${count} 条配图提示词。` : '本轮未提炼出可用配图提示词。', - ...(prompts.length - ? prompts.map((item, index) => `${index + 1}. [${item?.section || '段落配图'}] ${trimWeixinPublicAccountText(item?.prompt || '', 80)}`) - : []), - ].filter(Boolean).join('\n'), + lines.length ? lines.join('\n') : '本轮未提炼出可用配图提示词。', + ].filter(Boolean).join('\n\n'), timestamp: new Date().toLocaleTimeString(), } } @@ -929,15 +938,18 @@ function buildWeixinPublicAccountImageGenerationStageMessage(payload) { const output = step.output || {} const count = Number(output.count || 0) const refs = Array.isArray(output.image_refs) ? output.image_refs.slice(0, 4) : [] + const lines = [ + `已生成 ${count} 张配图。`, + ...(refs.length + ? refs.map((item, index) => `${index + 1}. [${item?.section || '段落配图'}] ${trimWeixinPublicAccountText(item?.prompt || '', 60)}`) + : []), + ] return { role: 'assistant', content: [ '### 6. 图片生成步骤已完成', - count > 0 ? `已生成 ${count} 张配图。` : '本轮未生成图片结果。', - ...(refs.length - ? refs.map((item, index) => `${index + 1}. [${item?.section || '段落配图'}] ${trimWeixinPublicAccountText(item?.prompt || '', 60)}`) - : []), - ].filter(Boolean).join('\n'), + lines.length ? lines.join('\n') : '本轮未生成图片结果。', + ].filter(Boolean).join('\n\n'), timestamp: new Date().toLocaleTimeString(), } } @@ -952,7 +964,7 @@ function buildWeixinPublicAccountPreviewStageMessage(payload) { content: [ '### 7. 预览与导出步骤已完成', `已整合正文${imageCount ? `与 ${imageCount} 张配图` : ''}生成预览稿,可检查排版并导出。`, - ].filter(Boolean).join('\n'), + ].filter(Boolean).join('\n\n'), timestamp: new Date().toLocaleTimeString(), } } @@ -967,7 +979,7 @@ function buildWeixinPublicAccountPageMarkdownStageMessage(payload) { content: [ '### 8. 分页 MD 步骤已完成', count > 0 ? `已按段落结构生成 ${count} 页分页 MD。` : '分页 MD 已生成。', - ].filter(Boolean).join('\n'), + ].filter(Boolean).join('\n\n'), timestamp: new Date().toLocaleTimeString(), } } diff --git a/internal/specialists/reference/pj0012-wechat-image-spec.md b/internal/specialists/reference/pj0012-wechat-image-spec.md new file mode 100644 index 0000000..14d8d2d --- /dev/null +++ b/internal/specialists/reference/pj0012-wechat-image-spec.md @@ -0,0 +1,224 @@ +# pj0012 配图提示词规则参考 + +> 来源:/home/eaiadmin/eaifiles/codebase/pj0012-aiw-mbai-u/ +> 用于指导 pj0235 的配图提示词规则设计。 + +## 一、整体架构 + +pj0012 将配图规则完全外置为 **JSON 配置文件 + Markdown 提示词模板**,代码层不做硬编码。 + +``` +config/wechat/ +├── skills/wechat_article_skill_ITandAI/ +│ ├── prompts/image_prompt_generate.prompt.md # 提示词模板 +│ ├── validators/image.validator.json # 验证规则 +│ └── retries/retry-policy.json # 重试策略 +├── wechat_default_image_requirements.json # 默认配图要求 +├── wechat_working_image_requirements.json # 运行时配图要求 +└── accounts//wechat_working_image_requirements.json # 账号级覆盖 +``` + +## 二、配图提示词生成规则 + +### 2.1 分布规则 + +- **标题下方主图**(`TITLE_PIC`):第 1 段对应,1 张,16:9 横图 +- **段落配图**(`IMAGE_PROMPT_01`, `IMAGE_PROMPT_02`...):按正文段落 1:1 生成 +- **正文分割方式**:按 `#` 标题或双空行分割为段落 + +### 2.2 括号标记格式 + +配图提示词以括号标记形式嵌入正文,格式为: + +``` +[槽位名: 提示词内容, size: 宽x高] +``` + +示例: + +```markdown +[ TITLE_PIC: 封面配图提示词, size: 1024x576 ] +[ IMAGE_PROMPT_01: 第1段配图提示词, size: 1024x576 ] +[ IMAGE_PROMPT_02: 第2段配图提示词, size: 1024x576 ] +``` + +每个标记插入在对应段落下方,后续图片生成步骤从中解析出 key、prompt、size。 + +### 2.3 提示词模板 + +``` +真实人物纪实摄影,公众号文章配图,第{N}段核心画面,{段落内容摘要120字},构图清晰,主体突出,电影感光影,细节丰富,高清 +``` + +**模板要素**: +- 风格前缀:`真实人物纪实摄影,公众号文章配图` +- 序号:`第{N}段核心画面` +- 内容摘要:段落内容截断至 120 字 +- 视觉要求:`构图清晰,主体突出,电影感光影,细节丰富,高清` +- 长度约束:**40-70 个中文字** + +### 2.4 全局规则(image.validator.json) + +| 规则 | 说明 | +|------|------| +| 必须包含主体 | 每个提示词必须描述画面主体 | +| 必须包含场景 | 每个提示词必须描述画面场景 | +| 必须包含光线 | 每个提示词必须描述光线效果 | +| 禁止文字覆盖 | 不允许提示词中包含文字说明 | +| 禁止拼贴风格 | 不允许拼贴/ collage 风格 | +| 段落映射完整 | 段落数必须与提示词数一致 | + +### 2.5 重试策略 + +- 默认最大重试次数:2 次 +- 每次重试将失败的检查项反馈给 LLM +- 重试耗尽后降级为模板拼接(兜底) + +### 2.6 图片规格 + +| 属性 | 值 | +|------|------| +| 尺寸比例 | 16:9 横版 | +| 分辨率 | 1792x1024 | +| 提示词标签 | 1024x576 | + +### 2.7 账号级风格覆盖 + +每个账号可独立配置图片风格,存储在 `wechat_working_image_requirements.json`: + +| 账号 | 风格 | 数量策略 | +|------|------|---------| +| default | 真实人物纪实摄影 | 按正文段落数量生成 | +| account | 美食摄影风格 | 根据菜品种类灵活配图 | +| account-01a312 | 通信科技行业视觉 | 关键概念一图一景 | +| account-73971d | 未来科技自然动感 | 按技术模块分层展示 | + +## 三、专员 JSON 定义体系 + +### 3.1 技能包(Skill Package)结构 + +``` +config/{workspace}/skills/{skill_name}/ +├── SKILL.md # 技能描述 + 执行策略 +├── prompts/ +│ ├── {step_id}.prompt.md # 步骤提示词模板(YAML 变量注入) +│ └── ... +├── validators/ +│ ├── {step_id}.validator.json # 验证规则 +│ └── ... +├── styles/ +│ ├── style-01-*.json # 文体变体 +│ └── ... +├── retries/ +│ └── retry-policy.json # 重试策略 +└── fallbacks/ + ├── fallback-policy.json # 降级策略 + └── {step_id}.fallback.json # 步骤级降级资产 +``` + +### 3.2 配置文件分类 + +| 类型 | 文件 | 内容 | 读取时机 | +|------|------|------|---------| +| 技能描述 | SKILL.md | 名称、描述、适用场景、执行流程 | 路由选择 | +| 提示词模板 | prompts/*.prompt.md | LLM 提示词({{变量}} 注入) | 步骤执行 | +| 验证规则 | validators/*.json | 检查清单、质量规则、重试反馈 | 输出验证 | +| 文体变体 | styles/*.json | 风格偏好、关注点、验证器强调 | 步骤执行 | +| 重试策略 | retries/*.json | 每步骤最大重试次数、策略 | 循环控制 | +| 降级资产 | fallbacks/*.json | 降级提示词、默认值 | 重试耗尽 | +| 业务数据 | *_working_*.json | 当前活跃配置(无缓存) | 运行时 | + +### 3.3 提示词模板格式 + +```markdown +# prompts/outline_generate.prompt.md + +# Role +You are the `outline_generate` draft executor for `wechat_article_skill_ITandAI`. + +# Goal +Generate the first draft of... + +# Runtime Context +{{runtime_inputs}} # 运行时注入 + +# Contract Excerpt +{{contract_excerpt}} + +# Validator Excerpt +{{validator_excerpt}} +``` + +变量解析:`prompt.replace("{{var_name}}", value)` + +### 3.4 验证器 JSON 结构 + +```json +{ + "step_id": "outline_generate", + "checks": ["markdown_structure", "required_sections", ...], + "word_budget_rules": { ... }, + "repair_strategy": { + "mode": "draft_then_repair", + "repair_prompt_asset": "prompts/outline_repair.prompt.md", + "reuse_last_draft_on_next_round": true + }, + "quality_rules": { + "min_bullet_chars": 18, + "ban_generic_patterns": ["^先用.+切入", ...], + "placeholder_phrases": ["相关信号", ...] + }, + "retry_feedback_template": "The outline draft failed validation. Fix: {failed_checks}" +} +``` + +### 3.5 重试策略 JSON 结构 + +```json +{ + "default_max_retries": 2, + "step_overrides": { + "hot_topic_extract": 1, + "topic_refine": 1, + "outline_generate": 2, + "content_generate": 2, + "image_prompt_generate": 2, + "pagemd_prepare": 0 + }, + "policy": { + "regenerate_fully": true, + "include_failed_checks_in_feedback": true, + "allow_partial_patch": false + } +} +``` + +### 3.6 多账号覆盖机制 + +``` +config/wechat/ # 全局默认 +├── wechat_working_image_requirements.json +├── wechat_working_outline_templates.json +└── accounts/ + ├── default/ # 默认账号覆盖 + ├── account/ # 账号 "account" 覆盖 + └── account-01a312/ # 账号 "account-01a312" 覆盖 + ├── wechat_working_image_requirements.json + └── wechat_working_outline_templates.json +``` + +加载优先级:`accounts/{account_id}/` → 全局默认 → 硬编码兜底 + +### 3.7 热更新机制 + +- 每次请求重新读取 JSON 文件,**不缓存** +- 修改配置文件后下次请求即生效,**无需重启** +- 文件不存在时返回空字典(安全降级) +- 解析异常时返回空字典(安全降级) + +## 四、核心设计原则 + +1. **三层分离**:Contract(契约层,人可读)→ Truth(数据层,机器可读)→ Skill(执行层,代码适配) +2. **代码是运行时适配器**:按路径读取 JSON/文本 → 注入运行时参数 → 执行验证 → 控制重试 → 调用 LLM +3. **不硬编码业务内容**:业务规则变更全部通过修改 JSON/Markdown 实现 +4. **优先级链**:运行时配置 > 全局默认 > 账号覆盖 > 硬编码兜底 diff --git a/start_dev_10231_10232.sh b/start_dev_10231_10232.sh old mode 100644 new mode 100755 index 587d808..d909a50 --- a/start_dev_10231_10232.sh +++ b/start_dev_10231_10232.sh @@ -53,6 +53,14 @@ EOF shift done +# ================================================================ +# 端口说明 +# 10231 - 前端 Vite 开发服务器 (dev only) +# 10232 - 后端 Go API 服务 (dev only) +# 8080 - 外部 LLM 服务 (llamacpp) — AI 对话 API 入口 +# 8081 - 外部 Embed 服务 (llamacpp) — 向量嵌入服务 +# 11434 - 外部 Ollama 服务 (如使用 Ollama 路由) +# ================================================================ BACKEND_PORT=10232 FRONTEND_PORT=10231