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:
eaiadmin
2026-09-24 23:01:06 +08:00
parent 89ae31c998
commit 011f722999
23 changed files with 1642 additions and 18 deletions
+54
View File
@@ -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
+52
View File
@@ -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 | 向量嵌入模型名 |
@@ -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": []
}
}
}
@@ -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}。请重新生成全部提示词并修复问题,保持段落映射不变。"
}
}
@@ -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": "不降级,返回明确错误。"
}
}
}
}
@@ -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`: 带括号标记的完整正文
@@ -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 字,必须包含主体/场景/光线
@@ -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
}
@@ -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
}
]
}
}
}
@@ -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)
}
@@ -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 {
@@ -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. 读取策略 → 控制重试/降级循环
@@ -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": []
}
}
}
@@ -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}。请重新生成全部提示词并修复问题,保持段落映射不变。"
}
}
@@ -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": "不降级,返回明确错误。"
}
}
}
}
@@ -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`: 带括号标记的完整正文
@@ -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 字,必须包含主体/场景/光线
@@ -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
}
@@ -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
}
]
}
}
}
@@ -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
View File
@@ -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