feat: 专员 JSON 配置体系 + 配图提示词规则重构
- 新增配图提示词 JSON 规则(image_prompt.json):封面图 + 每 500 字按字数分布 - 新增 Go 配置加载器(weixin_public_account_image_prompt_config.go):从嵌入的 spec 目录读取 JSON 配置 - 新增 weixin_public_account 完整 spec 目录:skill.md, workflow.json, domains.json, policy.json, validators/ - 新增 pj0012 参考文件(pj0012-wechat-image-spec.md):配图规则 + 专员 JSON 定义体系 - 产物 Tab 添加下载按钮 + 整体导出区 - 修复前端 Markdown 渲染 bug + 步骤消息分段问题 所有专员定义向 JSON 配置迁移,以 weixin_public_account 为样板。
This commit is contained in:
@@ -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
|
||||||
@@ -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 | 向量嵌入模型名 |
|
||||||
+42
@@ -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": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+54
@@ -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}。请重新生成全部提示词并修复问题,保持段落映射不变。"
|
||||||
|
}
|
||||||
|
}
|
||||||
+39
@@ -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": "不降级,返回明确错误。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+27
@@ -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`: 带括号标记的完整正文
|
||||||
+62
@@ -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 字,必须包含主体/场景/光线
|
||||||
+32
@@ -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
|
||||||
|
}
|
||||||
+157
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+309
@@ -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)
|
||||||
|
}
|
||||||
+1
@@ -153,6 +153,7 @@ type weixinPublicAccountImagePrompt struct {
|
|||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
Section string `json:"section"`
|
Section string `json:"section"`
|
||||||
Prompt string `json:"prompt"`
|
Prompt string `json:"prompt"`
|
||||||
|
Size string `json:"size"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type weixinPublicAccountImageRef struct {
|
type weixinPublicAccountImageRef struct {
|
||||||
|
|||||||
+60
@@ -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. 读取策略 → 控制重试/降级循环
|
||||||
+42
@@ -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": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+54
@@ -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}。请重新生成全部提示词并修复问题,保持段落映射不变。"
|
||||||
|
}
|
||||||
|
}
|
||||||
+39
@@ -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": "不降级,返回明确错误。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+27
@@ -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`: 带括号标记的完整正文
|
||||||
+62
@@ -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 字,必须包含主体/场景/光线
|
||||||
+32
@@ -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
|
||||||
|
}
|
||||||
+157
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+80
-2
@@ -158,9 +158,9 @@
|
|||||||
class="oa-btn oa-btn-tiny"
|
class="oa-btn oa-btn-tiny"
|
||||||
type="button"
|
type="button"
|
||||||
:disabled="previewLoading"
|
:disabled="previewLoading"
|
||||||
@click="openArtifactPreview(file)"
|
@click="downloadFile(file)"
|
||||||
>
|
>
|
||||||
预览
|
下载
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -169,6 +169,50 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- 整体导出区:所有格式一次导出 -->
|
||||||
|
<section class="oa-stack-section">
|
||||||
|
<div class="oa-stack-head">
|
||||||
|
<span class="oa-stack-title">整体导出</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="hasTask" class="oa-export-row">
|
||||||
|
<button
|
||||||
|
class="oa-btn"
|
||||||
|
type="button"
|
||||||
|
:disabled="loading"
|
||||||
|
@click="download('preview_md')"
|
||||||
|
>
|
||||||
|
预览 MD
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="oa-btn"
|
||||||
|
type="button"
|
||||||
|
:disabled="loading"
|
||||||
|
@click="download('preview_html')"
|
||||||
|
>
|
||||||
|
预览 HTML
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="oa-btn"
|
||||||
|
type="button"
|
||||||
|
:disabled="loading"
|
||||||
|
@click="download('pagemd')"
|
||||||
|
>
|
||||||
|
分页 MD
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="oa-btn"
|
||||||
|
type="button"
|
||||||
|
:disabled="loading"
|
||||||
|
@click="download('docx')"
|
||||||
|
>
|
||||||
|
Word (.docx)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-else class="oa-empty oa-empty-inline">
|
||||||
|
没有可导出的内容,先运行创作流程。
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@@ -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) {
|
function exportFileName(format) {
|
||||||
if (format === 'preview_html') return 'weixin-public-account-preview.html'
|
if (format === 'preview_html') return 'weixin-public-account-preview.html'
|
||||||
if (format === 'pagemd') return 'weixin-public-account-pagemd.md'
|
if (format === 'pagemd') return 'weixin-public-account-pagemd.md'
|
||||||
|
|||||||
@@ -474,7 +474,13 @@ function renderMarkdownBlock(block) {
|
|||||||
const headingMatch = trimmed.match(/^(#{1,3})\s+(.+)$/)
|
const headingMatch = trimmed.match(/^(#{1,3})\s+(.+)$/)
|
||||||
if (headingMatch) {
|
if (headingMatch) {
|
||||||
const level = headingMatch[1].length
|
const level = headingMatch[1].length
|
||||||
return `<h${level}>${renderInlineMarkdown(headingMatch[2])}</h${level}>`
|
const heading = `<h${level}>${renderInlineMarkdown(headingMatch[2])}</h${level}>`
|
||||||
|
// 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')
|
const lines = trimmed.split('\n')
|
||||||
@@ -792,7 +798,7 @@ function getWeixinPublicAccountStep(payload, stepKey) {
|
|||||||
return payload?.workflow?.steps?.[stepKey] || {}
|
return payload?.workflow?.steps?.[stepKey] || {}
|
||||||
}
|
}
|
||||||
|
|
||||||
function trimWeixinPublicAccountText(value, maxLength = 180) {
|
function trimWeixinPublicAccountText(value, maxLength = 360) {
|
||||||
const text = String(value || '').trim()
|
const text = String(value || '').trim()
|
||||||
if (!text) return ''
|
if (!text) return ''
|
||||||
return text.length > maxLength ? `${text.slice(0, maxLength).trim()}...` : text
|
return text.length > maxLength ? `${text.slice(0, maxLength).trim()}...` : text
|
||||||
@@ -876,7 +882,7 @@ function buildWeixinPublicAccountOutlineStageMessage(payload) {
|
|||||||
'### 3. 提纲步骤已完成',
|
'### 3. 提纲步骤已完成',
|
||||||
sections.length
|
sections.length
|
||||||
? `本轮提纲主结构:\n${formatWeixinPublicAccountBulletList(sections)}`
|
? `本轮提纲主结构:\n${formatWeixinPublicAccountBulletList(sections)}`
|
||||||
: trimWeixinPublicAccountText(outline, 220) || '本轮提纲已生成。',
|
: trimWeixinPublicAccountText(outline, 500) || '本轮提纲已生成。',
|
||||||
].filter(Boolean).join('\n\n'),
|
].filter(Boolean).join('\n\n'),
|
||||||
timestamp: new Date().toLocaleTimeString(),
|
timestamp: new Date().toLocaleTimeString(),
|
||||||
}
|
}
|
||||||
@@ -892,7 +898,7 @@ function buildWeixinPublicAccountContentStageMessage(payload) {
|
|||||||
.replace(/^>\s+.*$/gm, '')
|
.replace(/^>\s+.*$/gm, '')
|
||||||
.replace(/\n{2,}/g, '\n')
|
.replace(/\n{2,}/g, '\n')
|
||||||
.trim(),
|
.trim(),
|
||||||
220,
|
500,
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
@@ -910,15 +916,18 @@ function buildWeixinPublicAccountImagePromptStageMessage(payload) {
|
|||||||
const output = step.output || {}
|
const output = step.output || {}
|
||||||
const count = Number(output.count || 0)
|
const count = Number(output.count || 0)
|
||||||
const prompts = Array.isArray(output.image_prompts) ? output.image_prompts.slice(0, 4) : []
|
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 {
|
return {
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: [
|
content: [
|
||||||
'### 5. 配图提示词步骤已完成',
|
'### 5. 配图提示词步骤已完成',
|
||||||
count > 0 ? `已提炼 ${count} 条配图提示词。` : '本轮未提炼出可用配图提示词。',
|
lines.length ? lines.join('\n') : '本轮未提炼出可用配图提示词。',
|
||||||
...(prompts.length
|
].filter(Boolean).join('\n\n'),
|
||||||
? prompts.map((item, index) => `${index + 1}. [${item?.section || '段落配图'}] ${trimWeixinPublicAccountText(item?.prompt || '', 80)}`)
|
|
||||||
: []),
|
|
||||||
].filter(Boolean).join('\n'),
|
|
||||||
timestamp: new Date().toLocaleTimeString(),
|
timestamp: new Date().toLocaleTimeString(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -929,15 +938,18 @@ function buildWeixinPublicAccountImageGenerationStageMessage(payload) {
|
|||||||
const output = step.output || {}
|
const output = step.output || {}
|
||||||
const count = Number(output.count || 0)
|
const count = Number(output.count || 0)
|
||||||
const refs = Array.isArray(output.image_refs) ? output.image_refs.slice(0, 4) : []
|
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 {
|
return {
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: [
|
content: [
|
||||||
'### 6. 图片生成步骤已完成',
|
'### 6. 图片生成步骤已完成',
|
||||||
count > 0 ? `已生成 ${count} 张配图。` : '本轮未生成图片结果。',
|
lines.length ? lines.join('\n') : '本轮未生成图片结果。',
|
||||||
...(refs.length
|
].filter(Boolean).join('\n\n'),
|
||||||
? refs.map((item, index) => `${index + 1}. [${item?.section || '段落配图'}] ${trimWeixinPublicAccountText(item?.prompt || '', 60)}`)
|
|
||||||
: []),
|
|
||||||
].filter(Boolean).join('\n'),
|
|
||||||
timestamp: new Date().toLocaleTimeString(),
|
timestamp: new Date().toLocaleTimeString(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -952,7 +964,7 @@ function buildWeixinPublicAccountPreviewStageMessage(payload) {
|
|||||||
content: [
|
content: [
|
||||||
'### 7. 预览与导出步骤已完成',
|
'### 7. 预览与导出步骤已完成',
|
||||||
`已整合正文${imageCount ? `与 ${imageCount} 张配图` : ''}生成预览稿,可检查排版并导出。`,
|
`已整合正文${imageCount ? `与 ${imageCount} 张配图` : ''}生成预览稿,可检查排版并导出。`,
|
||||||
].filter(Boolean).join('\n'),
|
].filter(Boolean).join('\n\n'),
|
||||||
timestamp: new Date().toLocaleTimeString(),
|
timestamp: new Date().toLocaleTimeString(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -967,7 +979,7 @@ function buildWeixinPublicAccountPageMarkdownStageMessage(payload) {
|
|||||||
content: [
|
content: [
|
||||||
'### 8. 分页 MD 步骤已完成',
|
'### 8. 分页 MD 步骤已完成',
|
||||||
count > 0 ? `已按段落结构生成 ${count} 页分页 MD。` : '分页 MD 已生成。',
|
count > 0 ? `已按段落结构生成 ${count} 页分页 MD。` : '分页 MD 已生成。',
|
||||||
].filter(Boolean).join('\n'),
|
].filter(Boolean).join('\n\n'),
|
||||||
timestamp: new Date().toLocaleTimeString(),
|
timestamp: new Date().toLocaleTimeString(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/<account_id>/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. **优先级链**:运行时配置 > 全局默认 > 账号覆盖 > 硬编码兜底
|
||||||
Regular → Executable
+8
@@ -53,6 +53,14 @@ EOF
|
|||||||
shift
|
shift
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# ================================================================
|
||||||
|
# 端口说明
|
||||||
|
# 10231 - 前端 Vite 开发服务器 (dev only)
|
||||||
|
# 10232 - 后端 Go API 服务 (dev only)
|
||||||
|
# 8080 - 外部 LLM 服务 (llamacpp) — AI 对话 API 入口
|
||||||
|
# 8081 - 外部 Embed 服务 (llamacpp) — 向量嵌入服务
|
||||||
|
# 11434 - 外部 Ollama 服务 (如使用 Ollama 路由)
|
||||||
|
# ================================================================
|
||||||
BACKEND_PORT=10232
|
BACKEND_PORT=10232
|
||||||
FRONTEND_PORT=10231
|
FRONTEND_PORT=10231
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user