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
@@ -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"`
Section string `json:"section"`
Prompt string `json:"prompt"`
Size string `json:"size"`
}
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"
type="button"
:disabled="previewLoading"
@click="openArtifactPreview(file)"
@click="downloadFile(file)"
>
预览
下载
</button>
</div>
</div>
@@ -169,6 +169,50 @@
</template>
</div>
</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 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) {
if (format === 'preview_html') return 'weixin-public-account-preview.html'
if (format === 'pagemd') return 'weixin-public-account-pagemd.md'
@@ -474,7 +474,13 @@ function renderMarkdownBlock(block) {
const headingMatch = trimmed.match(/^(#{1,3})\s+(.+)$/)
if (headingMatch) {
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')
@@ -792,7 +798,7 @@ function getWeixinPublicAccountStep(payload, stepKey) {
return payload?.workflow?.steps?.[stepKey] || {}
}
function trimWeixinPublicAccountText(value, maxLength = 180) {
function trimWeixinPublicAccountText(value, maxLength = 360) {
const text = String(value || '').trim()
if (!text) return ''
return text.length > maxLength ? `${text.slice(0, maxLength).trim()}...` : text
@@ -876,7 +882,7 @@ function buildWeixinPublicAccountOutlineStageMessage(payload) {
'### 3. 提纲步骤已完成',
sections.length
? `本轮提纲主结构:\n${formatWeixinPublicAccountBulletList(sections)}`
: trimWeixinPublicAccountText(outline, 220) || '本轮提纲已生成。',
: trimWeixinPublicAccountText(outline, 500) || '本轮提纲已生成。',
].filter(Boolean).join('\n\n'),
timestamp: new Date().toLocaleTimeString(),
}
@@ -892,7 +898,7 @@ function buildWeixinPublicAccountContentStageMessage(payload) {
.replace(/^>\s+.*$/gm, '')
.replace(/\n{2,}/g, '\n')
.trim(),
220,
500,
)
return {
role: 'assistant',
@@ -910,15 +916,18 @@ function buildWeixinPublicAccountImagePromptStageMessage(payload) {
const output = step.output || {}
const count = Number(output.count || 0)
const prompts = Array.isArray(output.image_prompts) ? output.image_prompts.slice(0, 4) : []
const lines = [
`已提炼 ${count} 条配图提示词。`,
...(prompts.length
? prompts.map((item, index) => `${index + 1}. [${item?.section || '段落配图'}] ${trimWeixinPublicAccountText(item?.prompt || '', 80)}`)
: []),
]
return {
role: 'assistant',
content: [
'### 5. 配图提示词步骤已完成',
count > 0 ? `已提炼 ${count} 条配图提示词。` : '本轮未提炼出可用配图提示词。',
...(prompts.length
? prompts.map((item, index) => `${index + 1}. [${item?.section || '段落配图'}] ${trimWeixinPublicAccountText(item?.prompt || '', 80)}`)
: []),
].filter(Boolean).join('\n'),
lines.length ? lines.join('\n') : '本轮未提炼出可用配图提示词。',
].filter(Boolean).join('\n\n'),
timestamp: new Date().toLocaleTimeString(),
}
}
@@ -929,15 +938,18 @@ function buildWeixinPublicAccountImageGenerationStageMessage(payload) {
const output = step.output || {}
const count = Number(output.count || 0)
const refs = Array.isArray(output.image_refs) ? output.image_refs.slice(0, 4) : []
const lines = [
`已生成 ${count} 张配图。`,
...(refs.length
? refs.map((item, index) => `${index + 1}. [${item?.section || '段落配图'}] ${trimWeixinPublicAccountText(item?.prompt || '', 60)}`)
: []),
]
return {
role: 'assistant',
content: [
'### 6. 图片生成步骤已完成',
count > 0 ? `已生成 ${count} 张配图。` : '本轮未生成图片结果。',
...(refs.length
? refs.map((item, index) => `${index + 1}. [${item?.section || '段落配图'}] ${trimWeixinPublicAccountText(item?.prompt || '', 60)}`)
: []),
].filter(Boolean).join('\n'),
lines.length ? lines.join('\n') : '本轮未生成图片结果。',
].filter(Boolean).join('\n\n'),
timestamp: new Date().toLocaleTimeString(),
}
}
@@ -952,7 +964,7 @@ function buildWeixinPublicAccountPreviewStageMessage(payload) {
content: [
'### 7. 预览与导出步骤已完成',
`已整合正文${imageCount ? `与 ${imageCount} 张配图` : ''}生成预览稿,可检查排版并导出。`,
].filter(Boolean).join('\n'),
].filter(Boolean).join('\n\n'),
timestamp: new Date().toLocaleTimeString(),
}
}
@@ -967,7 +979,7 @@ function buildWeixinPublicAccountPageMarkdownStageMessage(payload) {
content: [
'### 8. 分页 MD 步骤已完成',
count > 0 ? `已按段落结构生成 ${count} 页分页 MD。` : '分页 MD 已生成。',
].filter(Boolean).join('\n'),
].filter(Boolean).join('\n\n'),
timestamp: new Date().toLocaleTimeString(),
}
}