Jelajahi Sumber

Add Douyin speaking bundle to OpenClaw install flow

gangvy 4 bulan lalu
induk
melakukan
46e2261da8
100 mengubah file dengan 17208 tambahan dan 9 penghapusan
  1. 10 0
      .gitignore
  2. TEMPAT SAMPAH
      AI实战营资料清单_实战1-实战2.docx
  3. 186 0
      AI实战营资料清单_实战1-实战2.md
  4. 20 0
      AI实战营资料清单_群内发送版.txt
  5. 8 0
      claude-code-voc-intelligence/.claude-plugin/plugin.json
  6. 5 0
      claude-code-voc-intelligence/.gitignore
  7. 11 0
      claude-code-voc-intelligence/.mcp.json
  8. 148 0
      claude-code-voc-intelligence/README.md
  9. 224 0
      claude-code-voc-intelligence/bin/claude-voc.js
  10. 79 0
      claude-code-voc-intelligence/docs/customer-quickstart.md
  11. 76 0
      claude-code-voc-intelligence/docs/demo-runbook.md
  12. 93 0
      claude-code-voc-intelligence/docs/npm-packaging-notes.md
  13. 179 0
      claude-code-voc-intelligence/install.js
  14. 36 0
      claude-code-voc-intelligence/mcp/src/core/credentials.js
  15. 31 0
      claude-code-voc-intelligence/mcp/src/core/memory-store.js
  16. 31 0
      claude-code-voc-intelligence/mcp/src/core/result-envelope.js
  17. 172 0
      claude-code-voc-intelligence/mcp/src/features/xiaohongshu-trend/live-collector.js
  18. 130 0
      claude-code-voc-intelligence/mcp/src/features/xiaohongshu-trend/preference-memory.js
  19. 293 0
      claude-code-voc-intelligence/mcp/src/features/xiaohongshu-trend/report.js
  20. 79 0
      claude-code-voc-intelligence/mcp/src/features/xiaohongshu-trend/sample-data.js
  21. 86 0
      claude-code-voc-intelligence/mcp/src/providers/xiaohongshu-api.js
  22. 160 0
      claude-code-voc-intelligence/mcp/src/server.js
  23. 85 0
      claude-code-voc-intelligence/mcp/src/tools/xiaohongshu-preference-update.js
  24. 208 0
      claude-code-voc-intelligence/mcp/src/tools/xiaohongshu-trend-run.js
  25. 37 0
      claude-code-voc-intelligence/memory-templates/xiaohongshu-trend-profile.json
  26. 1159 0
      claude-code-voc-intelligence/package-lock.json
  27. 42 0
      claude-code-voc-intelligence/package.json
  28. 81 0
      claude-code-voc-intelligence/scripts/smoke-mcp.js
  29. 92 0
      claude-code-voc-intelligence/scripts/smoke-package.js
  30. 22 0
      claude-code-voc-intelligence/skill-package-manifest.json
  31. 89 0
      claude-code-voc-intelligence/skills/xiaohongshu-trend-intelligence/SKILL.md
  32. 63 0
      claude-code-voc-intelligence/skills/xiaohongshu-trend-intelligence/references/live-mode.md
  33. 104 0
      claude-code-voc-intelligence/skills/xiaohongshu-trend-intelligence/references/output-format.md
  34. 65 0
      claude-code-voc-intelligence/skills/xiaohongshu-trend-intelligence/references/user-workflow.md
  35. 24 0
      dist/claude-code-voc-intelligence-suite-manifest.json
  36. TEMPAT SAMPAH
      dist/claude-code-voc-intelligence.zip
  37. 43 0
      dist/douyin-speaking-daily-suite-manifest.json
  38. TEMPAT SAMPAH
      dist/douyin-speaking-daily.zip
  39. 3 2
      dist/download-urls.json
  40. 25 0
      dist/industry-trend-intelligence-suite-manifest.json
  41. TEMPAT SAMPAH
      dist/industry-trend-intelligence.zip
  42. 49 0
      dist/install.js
  43. 17 0
      dist/npm/claude-code-voc-npm-package-manifest.json
  44. 3 0
      docs/1.md
  45. 185 0
      docs/5.11/douyin-speaking-daily-overview.md
  46. 158 0
      docs/5.11/douyin-speaking-daily-overview.txt
  47. 115 0
      docs/5.11/douyin-speaking-daily-p0-install-notes.md
  48. 155 0
      docs/5.11/douyin-speaking-daily-p1-image-script.md
  49. 131 0
      docs/5.11/douyin-speaking-daily-p2-image-script.md
  50. 128 0
      docs/5.11/douyin-speaking-daily-p3-image-script.md
  51. 127 0
      docs/5.11/douyin-speaking-daily-p4-image-script.md
  52. 131 0
      docs/5.11/douyin-speaking-daily-p5-image-script.md
  53. 76 0
      docs/5.11/p1.txt
  54. 69 0
      docs/5.11/p2.txt
  55. 102 0
      docs/5.11/p3.txt
  56. 103 0
      docs/5.11/p4.txt
  57. 107 0
      docs/5.11/p5.txt
  58. 777 0
      docs/specs/claude-code-native-voc-skill-package-design.md
  59. 224 0
      docs/specs/claude-code-native-xiaohongshu-trend-p0.md
  60. 1244 0
      docs/specs/claude-code-skill-package-porting-analysis.md
  61. 266 0
      docs/specs/douyin-speaking-daily-p1-optimization-spec.md
  62. 185 0
      docs/specs/douyin-speaking-daily-skill-package.md
  63. 284 0
      docs/specs/industry-trend-intelligence-agent-concept.md
  64. 2880 0
      docs/workshop/social-voc-openclaw-workshop.html
  65. 909 0
      docs/workshop/voc-openclaw-workshop-intro.html
  66. 499 0
      docs/讯飞转文字/讯飞实时转录接口调用文档.md
  67. 734 0
      docs/讯飞转文字/讯飞长语音转写接口调用文档.md
  68. 510 0
      douyin-speaking-daily-followup-course-scripts.md
  69. 61 0
      douyin-speaking-daily-p0-install-notes.txt
  70. 108 0
      douyin-speaking-daily/README.md
  71. 101 0
      douyin-speaking-daily/deployment.md
  72. 266 0
      douyin-speaking-daily/docs/specs/douyin-speaking-daily-p1-optimization-spec.md
  73. 219 0
      douyin-speaking-daily/douyin-speaking-daily-playbook.md
  74. 52 0
      douyin-speaking-daily/memory-templates/douyin-speaking-profile.json
  75. 147 0
      douyin-speaking-daily/openclaw-startup.md
  76. 25 0
      douyin-speaking-daily/scripts/README.md
  77. 2 0
      douyin-speaking-daily/scripts/douyin-speaking-daily-p1-smoke.js
  78. 12 0
      douyin-speaking-daily/scripts/douyin-speaking-daily-report.js
  79. 2 0
      douyin-speaking-daily/scripts/douyin-speaking-daily-runner.js
  80. 2 0
      douyin-speaking-daily/scripts/douyin-speaking-profile-memory.js
  81. 12 0
      douyin-speaking-daily/scripts/douyin-video-transcriber.js
  82. 12 0
      douyin-speaking-daily/scripts/douyin-viral-script-analyzer.js
  83. 131 0
      douyin-speaking-daily/skill-package-manifest.json
  84. 27 0
      douyin-speaking-daily/skills/README.md
  85. 94 0
      douyin/douyin-speaking-account-monitor/SKILL.md
  86. 179 0
      douyin/douyin-speaking-account-monitor/api-config.json
  87. 70 0
      douyin/douyin-speaking-daily-report/SKILL.md
  88. 181 0
      douyin/douyin-speaking-daily-report/api-config.json
  89. 141 0
      douyin/douyin-speaking-daily-runner/SKILL.md
  90. 315 0
      douyin/douyin-speaking-daily-runner/api-config.json
  91. 36 0
      douyin/douyin-speaking-daily-runner/index.js
  92. 37 0
      douyin/douyin-speaking-keyword-monitor/SKILL.md
  93. 117 0
      douyin/douyin-speaking-keyword-monitor/api-config.json
  94. 76 0
      douyin/douyin-speaking-profile-builder/SKILL.md
  95. 58 0
      douyin/douyin-speaking-profile-builder/api-config.json
  96. 69 0
      douyin/douyin-speaking-profile-memory/SKILL.md
  97. 150 0
      douyin/douyin-speaking-profile-memory/api-config.json
  98. 36 0
      douyin/douyin-user-posts/SKILL.md
  99. 92 0
      douyin/douyin-user-posts/api-config.json
  100. 11 7
      douyin/douyin-user-search/api-config.json

+ 10 - 0
.gitignore

@@ -0,0 +1,10 @@
+# Local secrets
+.env
+.env.*
+*.local
+release/npm/**/.env.local
+
+# Generated outputs
+outputs/
+dist/npm/*.tgz
+

TEMPAT SAMPAH
AI实战营资料清单_实战1-实战2.docx


+ 186 - 0
AI实战营资料清单_实战1-实战2.md

@@ -0,0 +1,186 @@
+# AI 实战营测试资料准备清单(实战一 / 实战二)
+
+> 适用场景:5.25 Claude Code & 龙虾技能实战
+>
+> 本清单用于提前收集 AI 实战测试资料,保障现场能直接跑通业务流程、验证 AI 输出质量,并沉淀后续企业内部 SOP。
+
+## 一、群内发送版
+
+各位老师好,为保障 5.25 AI 实战测试顺利进行,麻烦提前准备以下资料:
+
+### 实战一:地区装修社媒热点 AI 分析
+
+请准备以下资料:
+
+| 序号 | 资料项 | 数量/格式 | 说明 |
+|---|---|---|---|
+| 1 | 行业相关对标小红书稿件链接 | 5 条左右,直接提供链接 | 建议选择本地装修、全屋定制、装修避坑、风格趋势、收纳设计等相关内容 |
+| 2 | 目标地区与业务范围 | 文字说明 | 例如城市/区域、门店覆盖范围、主推业务类型 |
+| 3 | 关注关键词 | 5-10 个 | 如全屋定制、奶油风装修、小户型收纳、衣柜设计避坑、厨房橱柜设计、装修翻车等 |
+| 4 | 目标客户画像 | 简短文字 | 例如准备装修的女性客户、重视颜值和收纳的家庭决策者、关注预算环保售后的客户 |
+| 5 | 希望输出的业务用途 | 简短说明 | 例如生成行业趋势报告、营销选题、门店销售话术、客户沟通切入点 |
+
+### 实战二:装修量尺辅助 AI 功能测试
+
+请准备以下资料:
+
+| 序号 | 资料项 | 数量/格式 | 说明 |
+|---|---|---|---|
+| 1 | 装修量尺原始资料 | 至少 1 份,建议 Excel/Word/PDF/图片均可 | 保留原始记录格式,用于测试 AI 识别尺寸、房间、柜体、异常备注等能力 |
+| 2 | 人工核对后的尺码标准表 | 与原始资料对应,建议 Excel | 作为标准答案,用于对比 AI 识别结果是否准确 |
+| 3 | 异常样本或易错点说明 | 建议 1 份 | 如尺寸冲突、单位不统一、漏项、手写模糊、墙体/梁柱/插座限制等 |
+| 4 | 量尺字段标准 | 如已有请提供 | 例如空间名称、长/宽/高、门窗尺寸、柜体位置、安装限制、备注字段等 |
+| 5 | 期望输出模板 | 如已有请提供 | 例如标准化尺寸对照表、异常预警清单、复核建议表 |
+
+建议最晚在实战开始前统一发到群里或打包给项目组,文件名尽量标注“实战一/实战二 + 资料名称 + 日期”。
+
+---
+
+## 二、详细资料清单表格
+
+## 实战一:地区装修社媒热点 AI 分析资料清单
+
+### 1. 实战目标
+
+通过导入装修行业相关的小红书对标内容与业务背景,让 AI 提炼:
+
+- 地区装修热点趋势
+- 用户关注的装修风格、材质、收纳、预算与避坑点
+- 可转化为门店营销内容的选题
+- 可用于销售沟通的客户话术
+- 后续可复用的社媒 VOC 分析流程
+
+### 2. 资料清单
+
+| 优先级 | 资料类别 | 具体资料 | 建议数量/格式 | 准备要求 | 用途 | 验收关注点 |
+|---|---|---|---|---|---|---|
+| 必备 | 对标小红书稿件 | 行业相关小红书笔记链接 | 5 条左右;直接粘贴链接 | 尽量选择互动较高、内容完整、评论区有讨论的稿件 | 作为 AI 趋势分析和内容拆解样本 | AI 能否识别风格、痛点、热词和内容角度 |
+| 必备 | 行业与地区背景 | 城市/区域、门店服务范围、主推品类 | 文字说明,100-300 字 | 说明本次分析针对哪个地区、哪些业务 | 限定 AI 输出范围,避免泛泛而谈 | 结论是否贴合本地装修业务 |
+| 必备 | 关键词方向 | 装修行业关键词 | 5-10 个 | 可包含全屋定制、装修避坑、奶油风、小户型收纳、衣柜设计、厨房橱柜等 | 用于构建社媒分析检索方向 | 趋势识别是否覆盖核心业务词 |
+| 必备 | 目标客户画像 | 目标客户人群说明 | 文字说明 | 例如准备装修的女性客户、家庭决策者、改善型业主等 | 帮助 AI 将趋势转为业务建议 | 输出是否能转成客户沟通话术 |
+| 建议 | 小红书样本补充 | 标题、正文、点赞/收藏/评论数截图或文本 | 每条链接可补 1 份截图/文本 | 如果现场网络或平台限制,截图/文本可作为兜底样本 | 避免现场无法打开链接影响演示 | AI 能否基于离线样本完成分析 |
+| 建议 | 评论样本 | 高赞评论或典型用户问题 | 每条 3-10 条 | 优先提供真实用户顾虑、追问、吐槽 | 用于提炼用户决策阻力和避坑点 | AI 能否识别真实用户痛点 |
+| 建议 | 企业业务偏好 | 希望保留/规避的内容方向 | 文字说明 | 例如更偏门店转化话术,不要纯风格美图 | 用于校准 AI 输出风格 | AI 能否记住并应用偏好 |
+| 可选 | 过往营销素材 | 历史爆款海报、朋友圈文案、门店话术 | 3-5 份 | 可脱敏处理 | 用于对齐企业表达方式 | 生成内容是否接近企业现有风格 |
+
+### 3. 建议提供格式
+
+| 文件/资料名称 | 建议格式 | 示例命名 |
+|---|---|---|
+| 小红书对标链接清单 | Excel / Word / 文本文档 | 实战一_小红书对标链接清单_0525.xlsx |
+| 地区与业务背景说明 | Word / 文本文档 | 实战一_地区业务背景说明_0525.docx |
+| 评论样本与截图 | 图片文件夹 / Word 汇总 | 实战一_小红书评论截图_0525 |
+| 企业内容偏好说明 | Word / 文本文档 | 实战一_AI输出偏好说明_0525.docx |
+
+### 4. 实战输出预期
+
+| 输出物 | 内容说明 | 是否可现场验收 |
+|---|---|---|
+| 地区装修热点趋势报告 | 总结近期装修风格、材质、收纳、预算、避坑趋势 | 是 |
+| 用户顾虑/决策阻力清单 | 提炼评论中反复出现的问题 | 是 |
+| 营销内容选题 | 生成可用于小红书/朋友圈/短视频的选题方向 | 是 |
+| 门店销售话术 | 将热点和顾虑转成客户沟通话术 | 是 |
+| 后续 SOP 建议 | 形成社媒资料导入、AI 分析、人工校准、输出复用流程 | 是 |
+
+---
+
+## 实战二:装修量尺辅助 AI 功能测试资料清单
+
+### 1. 实战目标
+
+通过导入真实装修量尺资料与人工核对标准表,测试 AI 对量尺数据的识别、结构化、异常检测和标准化输出能力,重点验证:
+
+- 能否识别原始量尺记录中的空间、尺寸、单位、备注和限制条件
+- 能否发现尺寸冲突、缺失、异常值、单位不统一等问题
+- 能否生成标准化尺寸对照表
+- 能否给出人工复核建议与异常预警
+- 能否降低量尺资料整理和复核的人工工作量
+
+### 2. 资料清单
+
+| 优先级 | 资料类别 | 具体资料 | 建议数量/格式 | 准备要求 | 用途 | 验收关注点 |
+|---|---|---|---|---|---|---|
+| 必备 | 原始量尺资料 | 未经 AI 处理的原始量尺记录 | 至少 1 份;建议 Excel/Word/PDF/图片 | 保持真实原始格式,不要提前整理得过于标准 | 测试 AI 原始资料理解能力 | 空间、尺寸、单位、备注能否准确识别 |
+| 必备 | 人工核对标准表 | 与原始资料对应的人工最终尺寸表 | 1 份;建议 Excel | 作为标准答案,字段尽量完整 | 用于比对 AI 输出准确率 | AI 输出与人工标准表差异有多大 |
+| 必备 | 项目信息说明 | 户型、空间、客户需求、设计范围 | 文字说明或表格 | 说明本次量尺对应的项目背景 | 帮助 AI 判断尺寸归属和业务语义 | 输出是否能按空间/功能区归类 |
+| 建议 | 异常样本 | 存在问题的量尺记录 | 1 份或在原始资料中标注 | 可包含漏量、重复、冲突、手写模糊、单位混用等 | 测试 AI 异常识别能力 | 是否能发现并说明异常原因 |
+| 建议 | 字段标准 | 企业内部量尺字段规范 | Excel/Word | 如空间名称、长宽高、墙体、梁柱、门窗、插座、下水、烟道、备注等 | 指导 AI 输出固定结构 | 输出格式是否符合企业标准 |
+| 建议 | 业务规则 | 尺寸校验和设计约束规则 | Word/Excel | 如最小通道宽度、柜体深度、门洞预留、吊顶高度、橱柜台面高度等 | 用于测试 AI 专业判断 | 异常提示是否符合装修常识 |
+| 建议 | 现场照片/草图 | 量尺现场照片、手绘草图、户型图 | 图片/PDF | 可脱敏,注意标注空间名称 | 辅助 AI 理解复杂空间 | 是否能辅助定位尺寸来源 |
+| 可选 | 历史返工案例 | 因尺寸错误导致的问题案例 | 1-3 个 | 简述错误原因和后果 | 用于丰富异常预警规则 | AI 能否给出风险提示 |
+| 可选 | 输出模板 | 企业希望最终使用的表格样式 | Excel/Word | 如标准尺寸对照表、异常清单、复核建议表 | 统一现场输出格式 | 输出是否可直接复用 |
+
+### 3. 原始量尺资料建议包含字段
+
+| 字段 | 说明 | 示例 |
+|---|---|---|
+| 项目编号/客户代号 | 脱敏后的项目识别信息 | A 项目 / 0525 测试样本 |
+| 空间名称 | 客厅、主卧、次卧、厨房、卫生间、阳台等 | 厨房 |
+| 测量对象 | 墙面、柜体、门洞、窗户、梁柱、管道、插座等 | 橱柜位置墙面 |
+| 原始尺寸 | 原记录中的尺寸文本 | 长 3200mm,宽 600mm,高 2400mm |
+| 单位 | mm/cm/m 等 | mm |
+| 备注 | 现场限制或人工说明 | 右侧有下水管,需避让 |
+| 数据来源 | 表格、照片、手写记录、CAD 截图等 | 手写量尺单 |
+
+### 4. 人工核对标准表建议包含字段
+
+| 字段 | 说明 | 示例 |
+|---|---|---|
+| 空间名称 | 标准化后的空间名称 | 厨房 |
+| 测量对象 | 标准化后的测量对象 | 地柜安装墙面 |
+| 标准长/宽/高 | 人工确认后的最终尺寸 | 3200 / 600 / 2400 mm |
+| 设计约束 | 影响设计或施工的限制 | 下水管避让,插座位置需复核 |
+| 异常标记 | 是否存在冲突/缺失/待确认 | 待复核 |
+| 人工复核说明 | 标准答案的判断依据 | 原始记录高度疑似漏写,按现场照片补充 |
+
+### 5. 建议测试用例分组
+
+| 用例类型 | 建议准备内容 | 测试目的 |
+|---|---|---|
+| 正常样本 | 字段完整、尺寸清晰的量尺记录 | 验证 AI 标准识别与结构化能力 |
+| 缺失样本 | 缺少高度、深度、空间名称或备注的记录 | 验证 AI 是否能提示缺失项 |
+| 冲突样本 | 同一空间出现多个不一致尺寸 | 验证 AI 是否能发现冲突并给出复核建议 |
+| 单位混用样本 | mm、cm、m 混合出现 | 验证 AI 是否能统一单位并避免换算错误 |
+| 手写/图片样本 | 手写量尺单、现场照片、草图 | 验证 AI 对非标准资料的识别能力 |
+| 业务约束样本 | 有梁柱、门窗、插座、管道、烟道、地漏等限制 | 验证 AI 是否能识别施工与设计风险 |
+
+### 6. 实战输出预期
+
+| 输出物 | 内容说明 | 是否可现场验收 |
+|---|---|---|
+| 标准化尺寸对照表 | 将原始量尺资料整理为空间/对象/长宽高/备注结构 | 是 |
+| 异常预警清单 | 标记缺失、冲突、异常值、单位问题、待复核项 | 是 |
+| AI 与人工标准差异表 | 对比 AI 输出和人工核对表,定位差异 | 是 |
+| 人工复核建议 | 给出需要设计师或量尺人员确认的问题 | 是 |
+| 后续 SOP 建议 | 形成量尺资料上传、AI 识别、人工复核、标准表输出流程 | 是 |
+
+---
+
+## 三、统一交付与命名建议
+
+| 分类 | 建议文件名 | 负责人 | 备注 |
+|---|---|---|---|
+| 实战一 | 实战一_小红书对标链接清单_0525.xlsx | 业务/营销同事 | 至少 5 条链接 |
+| 实战一 | 实战一_地区业务背景与关键词_0525.docx | 业务/营销同事 | 包含地区、客户画像、关键词、输出用途 |
+| 实战二 | 实战二_装修量尺原始资料_0525.xlsx / pdf / jpg | 设计/量尺同事 | 至少 1 份真实或脱敏样本 |
+| 实战二 | 实战二_人工核对尺码标准表_0525.xlsx | 设计/量尺同事 | 必须与原始资料一一对应 |
+| 实战二 | 实战二_异常样本与复核规则_0525.docx | 设计/量尺同事 | 如暂无,可先提供文字说明 |
+
+## 四、资料脱敏要求
+
+| 项目 | 要求 |
+|---|---|
+| 客户隐私 | 删除客户姓名、电话、详细住址、身份证、合同编号等敏感信息 |
+| 企业敏感信息 | 如涉及报价、供应商、内部成本,可保留区间或打码 |
+| 图片资料 | 如包含人脸、门牌号、联系方式,应提前打码 |
+| 文件命名 | 使用“实战编号 + 资料名称 + 日期”,避免使用客户真实姓名 |
+
+## 五、现场验收建议
+
+| 验收维度 | 实战一关注点 | 实战二关注点 |
+|---|---|---|
+| 数据理解力 | 是否理解装修行业热词、风格、用户评论语义 | 是否识别空间、尺寸、单位、备注和装修专业术语 |
+| 逻辑严谨性 | 趋势判断是否有样本依据,是否避免空泛结论 | 异常判断是否有明确原因,是否避免误报漏报 |
+| 操作效率 | 是否能快速从链接/文本生成趋势报告 | 是否能减少人工整理和复核时间 |
+| 格式稳定性 | 是否能稳定输出趋势、痛点、选题、话术 | 是否能稳定输出标准表、异常表、复核建议 |
+| 落地可行性 | 是否可转化为门店营销和销售动作 | 是否可进入日常量尺资料复核流程 |

+ 20 - 0
AI实战营资料清单_群内发送版.txt

@@ -0,0 +1,20 @@
+各位老师好,为保障 5.25 AI 实战测试顺利进行,麻烦提前准备以下资料:
+
+【实战一:地区装修社媒热点 AI 分析】
+1. 行业相关对标小红书稿件链接:5 条左右,直接提供链接即可。建议选择本地装修、全屋定制、装修避坑、风格趋势、收纳设计等相关内容。
+2. 目标地区与业务范围:例如城市/区域、门店覆盖范围、主推业务类型。
+3. 关注关键词:建议 5-10 个,例如全屋定制、奶油风装修、小户型收纳、衣柜设计避坑、厨房橱柜设计、装修翻车等。
+4. 目标客户画像:例如准备装修的女性客户、重视颜值和收纳的家庭决策者、关注预算环保售后的客户。
+5. 希望输出的业务用途:例如行业趋势报告、营销选题、门店销售话术、客户沟通切入点。
+
+【实战二:装修量尺辅助 AI 功能测试】
+1. 装修量尺原始资料:至少 1 份,Excel/Word/PDF/图片均可,尽量保留原始记录格式。
+2. 人工核对后的尺码标准表:与原始资料对应,建议 Excel,用作 AI 识别结果的标准答案。
+3. 异常样本或易错点说明:如尺寸冲突、单位不统一、漏项、手写模糊、墙体/梁柱/插座限制等。
+4. 量尺字段标准:如已有请提供,例如空间名称、长/宽/高、门窗尺寸、柜体位置、安装限制、备注字段等。
+5. 期望输出模板:如已有请提供,例如标准化尺寸对照表、异常预警清单、复核建议表。
+
+【资料提交建议】
+1. 文件名尽量标注“实战一/实战二 + 资料名称 + 日期”。
+2. 客户姓名、电话、详细地址、合同编号、报价成本等敏感信息请提前脱敏。
+3. 建议实战开始前统一发到群里或打包给项目组,方便现场直接测试。

+ 8 - 0
claude-code-voc-intelligence/.claude-plugin/plugin.json

@@ -0,0 +1,8 @@
+{
+  "name": "voc-intelligence",
+  "description": "VOC intelligence skills for Claude Code. First skill: Xiaohongshu trend intelligence with multi-turn validation for industry trends, customer concerns, content topics, and draft talk tracks.",
+  "version": "0.1.2",
+  "author": {
+    "name": "nkkj-BrainHack"
+  }
+}

+ 5 - 0
claude-code-voc-intelligence/.gitignore

@@ -0,0 +1,5 @@
+node_modules/
+memory/
+outputs/
+.env
+.tmp/

+ 11 - 0
claude-code-voc-intelligence/.mcp.json

@@ -0,0 +1,11 @@
+{
+  "mcpServers": {
+    "voc-intelligence": {
+      "command": "node",
+      "args": [
+        "./mcp/src/server.js"
+      ]
+    }
+  }
+}
+

+ 148 - 0
claude-code-voc-intelligence/README.md

@@ -0,0 +1,148 @@
+# Claude Code VOC Intelligence
+
+Claude Code 原生版 VOC 技能包。
+
+当前第一阶段聚焦“小红书趋势情报官”,用于从小红书内容和评论中发现行业趋势、用户顾虑、设计元素、内容选题和待验证门店话术。
+
+## 当前范围
+
+- `xiaohongshu-trend-intelligence`:小红书趋势情报官 Skill。
+- sample 模式:无需 token,可用于教学、录屏和流程验证。
+- live 模式:已接入小红书/TikHub token 检查和 provider 骨架,真实采集需有效 token。
+- 偏好记忆:用户回答动态校准问题,或说“保留/不要/更偏”后,会保存到记忆,下一轮报告会基于反馈收敛。
+
+## 用户入口话术
+
+```text
+帮我做一份家装全屋定制行业的小红书趋势情报。
+重点看女性客户下季度可能关注的设计元素、风格和消费顾虑。
+先在聊天里给我第一轮样本观察和待确认问题,不要只给文件路径。
+```
+
+## 客户演示文档
+
+- `docs/customer-quickstart.md`:给客户/助教看的快速说明。
+- `docs/demo-runbook.md`:录制教学视频时可直接参考的分段脚本。
+- `docs/npm-packaging-notes.md`:npm 与 zip 双交付方式的打包经验。
+
+## 技术路线
+
+第一阶段:
+
+- Claude Code Skill 负责自然语言工作流。
+- 本地工具模块负责生成 sample 趋势报告。
+- MCP tools 负责 sample/live 采集、趋势报告、偏好更新。
+- 输出统一返回 `assistantMessage`。
+
+当前 MCP tools:
+
+- `voc_xiaohongshu_token_check`
+- `voc_xiaohongshu_trend_run`
+- `voc_xiaohongshu_preference_update`
+
+## 本地测试
+
+在仓库根目录执行:
+
+```bash
+npm --prefix claude-code-voc-intelligence run xhs:sample
+npm --prefix claude-code-voc-intelligence run xhs:preference-smoke
+npm --prefix claude-code-voc-intelligence run xhs:sample-with-memory
+npm --prefix claude-code-voc-intelligence run mcp:smoke
+```
+
+真实采集前先配置小红书/TikHub token:
+
+```bash
+set XIAOHONGSHU_API_TOKEN=你的token
+npm --prefix claude-code-voc-intelligence run xhs:live
+```
+
+如果没有 token,live 模式会返回 `needs_token`,不会误用 OpenClaw VOC token。
+
+## 打包交付
+
+在仓库根目录执行:
+
+```bash
+npm run claude-voc:build
+```
+
+该命令会生成:
+
+```text
+dist/claude-code-voc-intelligence.zip
+dist/claude-code-voc-intelligence-suite-manifest.json
+```
+
+并在临时目录里解压、重新安装依赖、跑 sample / preference / MCP smoke。
+
+用户拿到 zip 后的安装方式:
+
+```bash
+node install.js
+```
+
+如果要顺手验证完整 sample 链路:
+
+```bash
+node install.js --smoke
+```
+
+`--smoke` 会输出简洁检查结果,不会把完整报告 JSON 打满屏幕。
+
+## npm 安装
+
+全局安装:
+
+```bash
+npm install -g @gangvy/claude-code-voc-intelligence
+claude-voc install
+```
+
+免全局安装:
+
+```bash
+npx @gangvy/claude-code-voc-intelligence install
+```
+
+两种方式都会把技能包安装到:
+
+```text
+~/.claude/plugins/voc-intelligence
+```
+
+安装后按提示用 `claude --plugin-dir` 加载即可。
+
+npm 本地打包验证:
+
+```bash
+npm run claude-voc:npm-pack
+```
+
+上传到 CDN 需要先配置七牛环境变量:
+
+```bash
+set QINIU_AK=...
+set QINIU_SK=...
+npm run claude-voc:upload
+```
+
+Claude Code 插件开发态加载:
+
+```bash
+claude --plugin-dir ./claude-code-voc-intelligence
+```
+
+进入 Claude Code 后可直接调用:
+
+```text
+/voc-intelligence:xiaohongshu-trend-intelligence
+```
+
+MCP server 启动命令:
+
+```bash
+npm --prefix claude-code-voc-intelligence run mcp:start
+```
+

+ 224 - 0
claude-code-voc-intelligence/bin/claude-voc.js

@@ -0,0 +1,224 @@
+#!/usr/bin/env node
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const { spawnSync } = require('child_process');
+
+const SOURCE_ROOT = path.resolve(__dirname, '..');
+const DEFAULT_TARGET = path.join(os.homedir(), '.claude', 'plugins', 'voc-intelligence');
+const MIN_NODE_MAJOR = 18;
+const EXCLUDED_DIRS = new Set([
+  'node_modules',
+  'memory',
+  'outputs',
+  '.tmp',
+  '.git'
+]);
+const EXCLUDED_FILES = new Set([
+  '.env',
+  '.npmrc'
+]);
+
+function usage() {
+  return [
+    'Claude VOC 技能包安装工具',
+    '',
+    '用法:',
+    '  claude-voc install',
+    '  claude-voc install --smoke',
+    '  claude-voc check',
+    '  claude-voc smoke',
+    '  claude-voc path',
+    '',
+    '也可以免全局安装:',
+    '  npx @gangvy/claude-code-voc-intelligence install',
+    '',
+    '参数:',
+    '  --target <dir>    指定安装目录,默认 ~/.claude/plugins/voc-intelligence',
+    '  --skip-install    复制后跳过 npm install',
+    '  --smoke           安装后跑 sample / 偏好记忆 / MCP smoke',
+    '  --help, -h        显示帮助'
+  ].join('\n');
+}
+
+function parseArgs(argv) {
+  const opts = {
+    command: argv[0] && !argv[0].startsWith('--') ? argv[0] : 'install',
+    target: DEFAULT_TARGET,
+    skipInstall: false,
+    smoke: false,
+    help: false
+  };
+  for (let i = opts.command === argv[0] ? 1 : 0; i < argv.length; i++) {
+    const token = argv[i];
+    if (token === '--target') {
+      opts.target = argv[++i];
+    } else if (token.startsWith('--target=')) {
+      opts.target = token.slice('--target='.length);
+    } else if (token === '--skip-install') {
+      opts.skipInstall = true;
+    } else if (token === '--smoke') {
+      opts.smoke = true;
+    } else if (token === '--help' || token === '-h') {
+      opts.help = true;
+    }
+  }
+  opts.target = path.resolve(opts.target.replace(/^~(?=$|[\\/])/, os.homedir()));
+  return opts;
+}
+
+function run(command, args, cwd) {
+  const useCmd = process.platform === 'win32' && command === 'npm';
+  const executable = useCmd ? 'cmd.exe' : command;
+  const finalArgs = useCmd ? ['/d', '/s', '/c', 'npm', ...args] : args;
+  const child = spawnSync(executable, finalArgs, {
+    cwd,
+    encoding: 'utf8',
+    stdio: 'inherit',
+    maxBuffer: 1024 * 1024 * 100
+  });
+  if (child.status !== 0) {
+    const detail = child.error ? `: ${child.error.message}` : '';
+    throw new Error(`${command} ${args.join(' ')} failed with exit ${child.status}${detail}`);
+  }
+}
+
+function checkNodeVersion() {
+  const major = Number(process.versions.node.split('.')[0]);
+  if (!Number.isFinite(major) || major < MIN_NODE_MAJOR) {
+    throw new Error(`Node.js 版本过低:当前 ${process.version},需要 Node.js ${MIN_NODE_MAJOR}+`);
+  }
+}
+
+function ensureDir(dirPath) {
+  fs.mkdirSync(dirPath, { recursive: true });
+}
+
+function isSafeDefaultTarget(targetDir) {
+  return path.resolve(targetDir) === path.resolve(DEFAULT_TARGET);
+}
+
+function shouldCopyEntry(entry) {
+  if (entry.isDirectory()) return !EXCLUDED_DIRS.has(entry.name);
+  if (entry.isFile()) {
+    if (EXCLUDED_FILES.has(entry.name)) return false;
+    if (entry.name.endsWith('.tgz')) return false;
+    return true;
+  }
+  return false;
+}
+
+function copyDirRecursive(srcDir, destDir) {
+  ensureDir(destDir);
+  let count = 0;
+  for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
+    if (!shouldCopyEntry(entry)) continue;
+    const src = path.join(srcDir, entry.name);
+    const dest = path.join(destDir, entry.name);
+    if (entry.isDirectory()) {
+      count += copyDirRecursive(src, dest);
+    } else if (entry.isFile()) {
+      ensureDir(path.dirname(dest));
+      fs.copyFileSync(src, dest);
+      count++;
+    }
+  }
+  return count;
+}
+
+function assertInstalled(targetDir) {
+  const required = [
+    'package.json',
+    'install.js',
+    '.claude-plugin/plugin.json',
+    '.mcp.json',
+    'skills/xiaohongshu-trend-intelligence/SKILL.md',
+    'mcp/src/server.js'
+  ];
+  const missing = required.filter(rel => !fs.existsSync(path.join(targetDir, rel)));
+  if (missing.length) {
+    throw new Error(`安装目录缺少必要文件:${missing.join(', ')}`);
+  }
+}
+
+function printNextSteps(targetDir) {
+  console.log('');
+  console.log('安装完成。');
+  console.log('');
+  console.log('下一步:');
+  console.log(`  1. 启动 Claude Code:claude --plugin-dir "${targetDir}"`);
+  console.log('  2. 在 Claude Code 里说:');
+  console.log('     帮我做一份家装全屋定制行业的小红书趋势情报。');
+  console.log('     重点看女性客户下季度可能关注的设计元素、风格和消费顾虑。');
+  console.log('     先在聊天里给我第一轮样本观察和待确认问题,不要只给文件路径。');
+  console.log('');
+  console.log(`安装目录:${targetDir}`);
+}
+
+function install(opts) {
+  checkNodeVersion();
+  if (path.resolve(SOURCE_ROOT) !== path.resolve(opts.target)) {
+    if (fs.existsSync(opts.target)) {
+      if (!isSafeDefaultTarget(opts.target)) {
+        throw new Error(`自定义安装目录已存在,为避免覆盖,请先手动清理:${opts.target}`);
+      }
+      fs.rmSync(opts.target, { recursive: true, force: true });
+    }
+    ensureDir(opts.target);
+    const copied = copyDirRecursive(SOURCE_ROOT, opts.target);
+    console.log(`已复制技能包文件:${copied} 个`);
+  }
+
+  assertInstalled(opts.target);
+
+  if (!opts.skipInstall) {
+    console.log('正在安装运行依赖...');
+    run('npm', ['install', '--omit=dev', '--ignore-scripts'], opts.target);
+  }
+
+  if (opts.smoke) {
+    console.log('正在运行安装自检...');
+    run(process.execPath, ['install.js', '--smoke', '--skip-install', '--no-next'], opts.target);
+  } else {
+    run(process.execPath, ['install.js', '--check', '--no-next'], opts.target);
+  }
+
+  printNextSteps(opts.target);
+}
+
+function check(opts) {
+  assertInstalled(opts.target);
+  run(process.execPath, ['install.js', '--check', '--no-next'], opts.target);
+}
+
+function smoke(opts) {
+  assertInstalled(opts.target);
+  run(process.execPath, ['install.js', '--smoke', '--skip-install', '--no-next'], opts.target);
+}
+
+function printPath(opts) {
+  console.log(opts.target);
+  console.log(`claude --plugin-dir "${opts.target}"`);
+}
+
+function main() {
+  const opts = parseArgs(process.argv.slice(2));
+  if (opts.help || opts.command === 'help') {
+    console.log(usage());
+    return;
+  }
+  if (opts.command === 'install') return install(opts);
+  if (opts.command === 'check') return check(opts);
+  if (opts.command === 'smoke') return smoke(opts);
+  if (opts.command === 'path') return printPath(opts);
+  throw new Error(`未知命令:${opts.command}\n\n${usage()}`);
+}
+
+try {
+  main();
+} catch (error) {
+  console.error('');
+  console.error(`claude-voc 执行失败:${error.message}`);
+  process.exit(1);
+}
+

+ 79 - 0
claude-code-voc-intelligence/docs/customer-quickstart.md

@@ -0,0 +1,79 @@
+# 小红书趋势情报官客户快速说明
+
+## 这个技能包能做什么
+
+“小红书趋势情报官”用于帮行业客户从小红书内容和评论里看机会:
+
+- 下季度可能流行的风格、设计元素和场景。
+- 用户在评论里反复担心的问题,比如预算、环保、翻车、售后。
+- 可以继续验证的内容选题、门店话术、产品表达方向。
+- 用户反馈后的偏好记忆,下次报告会自动更贴近客户业务。
+
+当前版本优先服务“家装全屋定制/女性消费决策”场景,也可以通过行业、关键词和目标人群扩展到其他行业。
+
+## 第一次怎么说
+
+直接在 Claude Code 里说:
+
+```text
+帮我做一份家装全屋定制行业的小红书趋势情报。
+重点看女性客户下季度可能关注的设计元素、风格和消费顾虑。
+先在聊天里给我第一轮样本观察和待确认问题,不要只给文件路径。
+```
+
+系统应该直接返回报告正文,而不是只告诉你文件保存在哪里。
+
+## 报告看什么
+
+重点看四块:
+
+- “第一轮初步判断(待校准)”:先判断趋势主线。
+- “样本观察 Top 5”:看本轮样本先暴露了哪些信号。
+- “用户顾虑/决策阻力”:看客户为什么犹豫、为什么不买。
+- “可继续验证的选题”和“暂可参考话术”:看哪些内容值得下一轮核验。
+
+## 怎么校准和收敛
+
+第一轮不要当最终结论,先回答报告最后根据样本自动生成的问题。也可以直接用自然语言反馈:
+
+```text
+保留奶油风和全屋定制翻车方向,不要纯风格美图,下一版更偏门店转化话术。
+```
+
+系统会保存偏好。下一轮再生成时,会基于你的反馈重新排序样本、收敛假设,并弱化不适合的内容。
+
+## 演示模式和真实采集
+
+演示模式不需要账号或 token,适合录屏、工作坊、客户现场讲解。
+
+真实采集需要配置小红书/VOC 社媒采集服务。配置完成后,同一套话术可以切换到真实小红书数据。
+
+## 安装提示
+
+推荐优先用 npm 一行安装:
+
+```bash
+npx @gangvy/claude-code-voc-intelligence install
+```
+
+如果已经全局安装,也可以:
+
+```bash
+npm install -g @gangvy/claude-code-voc-intelligence
+claude-voc install
+```
+
+拿到压缩包时,在解压目录执行:
+
+```bash
+node install.js
+```
+
+需要现场自检时执行:
+
+```bash
+node install.js --smoke
+```
+
+自检通过后,用 Claude Code 加载这个技能包,再按上面的自然语言话术启动。
+

+ 76 - 0
claude-code-voc-intelligence/docs/demo-runbook.md

@@ -0,0 +1,76 @@
+# 小红书趋势情报官录屏脚本
+
+## 录屏目标
+
+让第一次接触的客户看懂:这不是让他学工具参数,而是让他多一个“行业情报官”,帮他从小红书内容和评论里找到趋势、顾虑、选题和销售话术。
+
+## 第 1 段:说明场景
+
+口播参考:
+
+```text
+今天我们演示一个小红书趋势情报官。比如家装全屋定制行业,经常会关心女性客户下个季度会喜欢什么风格、担心什么翻车点、门店销售应该怎么跟进。以前这些信息要人工刷小红书、看评论、做整理,现在可以直接让情报官先跑一版。
+```
+
+## 第 2 段:启动话术
+
+在 Claude Code 里输入:
+
+```text
+帮我做一份家装全屋定制行业的小红书趋势情报。
+重点看女性客户下季度可能关注的设计元素、风格和消费顾虑。
+先在聊天里给我第一轮样本观察和待确认问题,不要只给文件路径。
+```
+
+讲解重点:
+
+```text
+注意,这里不用说文件名、参数、输出目录。客户只需要说清楚行业、目标人群和想看的问题。
+```
+
+## 第 3 段:解读报告
+
+报告出来后,按这个顺序讲:
+
+```text
+第一,看第一轮初步判断。这里不是最终结论,只是情报官根据样本先提出一个待验证主线。
+第二,看样本观察和证据样本,判断这些信号是不是来自真实笔记和评论,而不是拍脑袋。
+第三,看用户顾虑,这部分最关键,因为评论里的犹豫点往往就是内容和销售的切入口。
+第四,看最后的动态问题。问题不是固定模板,而是情报官根据这轮样本、高赞评论和设计元素自动追问,方便客户参与校准。
+```
+
+## 第 4 段:校准偏好
+
+第一轮报告最后会出现本轮动态问题。录屏时可以先示范回答其中 1-2 个,例如:
+
+```text
+第1个问题里,真实客户确实会先担心不好打理;第2条高赞评论也像门店里经常听到的问题。
+保留奶油风和全屋定制翻车方向,不要纯风格美图,下一轮更偏门店转化话术。
+```
+
+也可以用更短的反馈:
+
+```text
+保留奶油风和全屋定制翻车方向,不要纯风格美图,下一版更偏门店转化话术。
+```
+
+讲解重点:
+
+```text
+这一步是人机共创。客户不用一次说得很完整,只要指出哪些样本像真实客户、哪些不准、哪些方向要保留。系统会把反馈记住,下一轮再收敛判断,而不是第一轮就把结论说死。
+```
+
+## 第 5 段:说明真实采集
+
+口播参考:
+
+```text
+现在演示版用的是样例数据,适合先看交付形式。真正交付时,可以接入小红书/VOC 社媒采集服务,按客户自己的行业关键词去采集真实笔记和评论,再生成同样结构的报告。
+```
+
+## 避免这样讲
+
+- 不要让客户听到 profile、output、collectionMode、keywordLimit 这类参数。
+- 不要把重点放在安装命令上,安装只是一句带过。
+- 不要只展示文件路径,客户要看到聊天里直接给出样本观察、证据和待验证问题。
+- 不要把第一轮说成最终报告,第一轮是“样本观察 + 机会假设 + 等客户校准”。

+ 93 - 0
claude-code-voc-intelligence/docs/npm-packaging-notes.md

@@ -0,0 +1,93 @@
+# Claude Code VOC 技能包 npm 打包经验
+
+## 目标
+
+同一个技能包同时支持两种交付:
+
+- 链接下载 zip:适合不会 npm 的客户,解压后执行 `node install.js`。
+- npm/npx 安装:适合教学、远程交付和批量升级。
+
+推荐命令:
+
+```bash
+npm install -g @gangvy/claude-code-voc-intelligence
+claude-voc install
+```
+
+免全局安装:
+
+```bash
+npx @gangvy/claude-code-voc-intelligence install
+```
+
+## 关键设计
+
+`npx` 下载的包可能在临时缓存里,不能直接让 Claude Code 长期加载这个临时目录。
+
+所以 `claude-voc install` 会把包复制到固定目录:
+
+```text
+~/.claude/plugins/voc-intelligence
+```
+
+然后在该目录安装运行依赖,并输出:
+
+```bash
+claude --plugin-dir "<安装目录>"
+```
+
+这样全局安装和 npx 安装的结果一致,后续也便于升级。
+
+## package.json 要点
+
+- npm 包名使用 scoped package:`@gangvy/claude-code-voc-intelligence`
+- CLI 入口:
+
+```json
+{
+  "bin": {
+    "claude-voc": "bin/claude-voc.js"
+  }
+}
+```
+
+- `files` 必须显式包含:
+  - `.claude-plugin/`
+  - `.mcp.json`
+  - `bin/`
+  - `skills/`
+  - `mcp/`
+  - `memory-templates/`
+  - `docs/`
+  - `install.js`
+  - `skill-package-manifest.json`
+
+## 验证命令
+
+根目录执行:
+
+```bash
+npm run claude-voc:npm-pack
+```
+
+这个命令会做三件事:
+
+1. 校验 npm package metadata 和 CLI 入口。
+2. 生成 `dist/npm/*.tgz`。
+3. 在临时目录里模拟 npm 安装,并执行 `claude-voc install --smoke`。
+
+发布命令:
+
+```bash
+npm run claude-voc:npm-publish
+```
+
+发布前需要完成 `npm login`,或在 CI/部署环境配置 npm token。
+
+## 注意点
+
+- zip 包和 npm 包共用同一套源码,但验证脚本分开。
+- npm 包不要发布 `node_modules/`、`memory/`、`outputs/`、`.env`。
+- `install.js` 不强依赖 `package-lock.json`,因为 npm publish 后 package-lock 不一定随包安装。
+- 用户侧不要暴露 profile、output、collectionMode 这类参数;安装完成后仍然用自然语言启动。
+

+ 179 - 0
claude-code-voc-intelligence/install.js

@@ -0,0 +1,179 @@
+#!/usr/bin/env node
+const fs = require('fs');
+const path = require('path');
+const { spawnSync } = require('child_process');
+
+const ROOT = __dirname;
+const MIN_NODE_MAJOR = 18;
+const PACKAGE_NAMES = new Set([
+  'claude-code-voc-intelligence',
+  '@gangvy/claude-code-voc-intelligence'
+]);
+
+function parseArgs(argv) {
+  const opts = {
+    check: false,
+    smoke: false,
+    skipInstall: false,
+    help: false,
+    noNext: false
+  };
+  for (const token of argv) {
+    if (token === '--check') opts.check = true;
+    else if (token === '--smoke') opts.smoke = true;
+    else if (token === '--skip-install') opts.skipInstall = true;
+    else if (token === '--no-next') opts.noNext = true;
+    else if (token === '--help' || token === '-h') opts.help = true;
+  }
+  return opts;
+}
+
+function usage() {
+  return [
+    'Claude Code VOC Intelligence 安装器',
+    '',
+    '用法:',
+    '  node install.js',
+    '  node install.js --smoke',
+    '  node install.js --check',
+    '',
+    '参数:',
+    '  --check          只检查文件和环境,不安装依赖',
+    '  --smoke          安装后跑 sample 和 MCP smoke',
+    '  --skip-install   跳过 npm install',
+    '  --no-next        不打印下一步提示,供外层 CLI 调用',
+    '  --help, -h       显示帮助'
+  ].join('\n');
+}
+
+function run(command, args, options = {}) {
+  const useCmd = process.platform === 'win32' && command === 'npm';
+  const executable = useCmd ? 'cmd.exe' : command;
+  const finalArgs = useCmd ? ['/d', '/s', '/c', 'npm', ...args] : args;
+  const child = spawnSync(executable, finalArgs, {
+    cwd: options.cwd || ROOT,
+    encoding: 'utf8',
+    stdio: options.stdio || 'inherit',
+    maxBuffer: 1024 * 1024 * 100
+  });
+  if (child.status !== 0) {
+    const detail = child.error ? `: ${child.error.message}` : '';
+    throw new Error(`${command} ${args.join(' ')} failed with exit ${child.status}${detail}`);
+  }
+  return child;
+}
+
+function readJson(relativePath) {
+  return JSON.parse(fs.readFileSync(path.join(ROOT, relativePath), 'utf8'));
+}
+
+function assertFile(relativePath) {
+  const absolute = path.join(ROOT, relativePath);
+  if (!fs.existsSync(absolute)) {
+    throw new Error(`缺少必要文件:${relativePath}`);
+  }
+}
+
+function checkNodeVersion() {
+  const major = Number(process.versions.node.split('.')[0]);
+  if (!Number.isFinite(major) || major < MIN_NODE_MAJOR) {
+    throw new Error(`Node.js 版本过低:当前 ${process.version},需要 Node.js ${MIN_NODE_MAJOR}+`);
+  }
+}
+
+function checkFiles() {
+  [
+    'package.json',
+    '.claude-plugin/plugin.json',
+    '.mcp.json',
+    'skill-package-manifest.json',
+    'skills/xiaohongshu-trend-intelligence/SKILL.md',
+    'mcp/src/server.js'
+  ].forEach(assertFile);
+
+  const packageJson = readJson('package.json');
+  const pluginJson = readJson('.claude-plugin/plugin.json');
+  const mcpJson = readJson('.mcp.json');
+  if (!PACKAGE_NAMES.has(packageJson.name)) {
+    throw new Error('package.json name 不正确');
+  }
+  if (pluginJson.name !== 'voc-intelligence') {
+    throw new Error('.claude-plugin/plugin.json name 不正确');
+  }
+  if (!mcpJson.mcpServers || !mcpJson.mcpServers['voc-intelligence']) {
+    throw new Error('.mcp.json 未配置 voc-intelligence MCP server');
+  }
+}
+
+function checkClaudeCommand() {
+  const command = process.platform === 'win32' ? 'where' : 'which';
+  const args = ['claude'];
+  const child = spawnSync(command, args, {
+    cwd: ROOT,
+    encoding: 'utf8',
+    stdio: 'pipe'
+  });
+  return child.status === 0;
+}
+
+function printNextSteps({ claudeAvailable }) {
+  const loadCommand = `claude --plugin-dir "${ROOT}"`;
+  console.log('');
+  console.log('安装检查完成。');
+  console.log('');
+  console.log('下一步:');
+  console.log(`  1. 启动 Claude Code:${loadCommand}`);
+  console.log('  2. 在 Claude Code 里说:');
+  console.log('     帮我做一份家装全屋定制行业的小红书趋势情报。');
+  console.log('     重点看女性客户下季度可能关注的设计元素、风格和消费顾虑。');
+  console.log('     先在聊天里给我第一轮样本观察和待确认问题,不要只给文件路径。');
+  console.log('');
+  if (!claudeAvailable) {
+    console.log('提示:当前终端没有检测到 claude 命令。如果 Claude Code 已安装但不在 PATH 中,请在 Claude Code 所在终端里运行上面的加载命令。');
+    console.log('');
+  }
+  console.log('真实采集需要配置小红书/TikHub token;未配置时仍可用 sample 模式做演示。');
+}
+
+function main() {
+  const opts = parseArgs(process.argv.slice(2));
+  if (opts.help) {
+    console.log(usage());
+    return;
+  }
+
+  console.log('');
+  console.log('Claude Code VOC Intelligence 安装检查');
+  console.log('====================================');
+  console.log('');
+
+  checkNodeVersion();
+  checkFiles();
+  console.log(`Node.js: ${process.version}`);
+  console.log('必要文件:OK');
+
+  if (!opts.check && !opts.skipInstall) {
+    console.log('');
+    console.log('正在安装依赖...');
+    run('npm', ['install', '--omit=dev', '--ignore-scripts']);
+  }
+
+  if (opts.smoke) {
+    console.log('');
+    console.log('正在运行 sample / 偏好记忆 / MCP smoke...');
+    run(process.execPath, ['scripts/smoke-package.js']);
+  }
+
+  if (!opts.noNext) {
+    printNextSteps({ claudeAvailable: checkClaudeCommand() });
+  }
+}
+
+try {
+  main();
+} catch (error) {
+  console.error('');
+  console.error(`安装失败:${error.message}`);
+  process.exit(1);
+}
+

+ 36 - 0
claude-code-voc-intelligence/mcp/src/core/credentials.js

@@ -0,0 +1,36 @@
+const fs = require('fs');
+const path = require('path');
+const os = require('os');
+
+function readJsonMaybe(filePath) {
+  try {
+    if (!filePath || !fs.existsSync(filePath)) return {};
+    return JSON.parse(fs.readFileSync(filePath, 'utf8'));
+  } catch {
+    return {};
+  }
+}
+
+function firstNonEmpty(values) {
+  return values.find(value => typeof value === 'string' && value.trim()) || '';
+}
+
+function readXiaohongshuToken(input = {}) {
+  const claudeCreds = readJsonMaybe(path.join(os.homedir(), '.claude', 'voc-credentials.json'));
+  const openclawCreds = readJsonMaybe(path.join(os.homedir(), '.openclaw', 'voc-credentials.json'));
+  return firstNonEmpty([
+    input.xiaohongshuToken,
+    process.env.XIAOHONGSHU_API_TOKEN,
+    process.env.TIKHUB_API_TOKEN,
+    process.env.FMODE_XIAOHONGSHU_TOKEN,
+    process.env.FMODE_SOCIAL_TOKEN,
+    claudeCreds.xiaohongshuToken,
+    claudeCreds.tikhubToken,
+    openclawCreds.xiaohongshuToken,
+    openclawCreds.tikhubToken
+  ]);
+}
+
+module.exports = {
+  readXiaohongshuToken
+};

+ 31 - 0
claude-code-voc-intelligence/mcp/src/core/memory-store.js

@@ -0,0 +1,31 @@
+const fs = require('fs');
+const path = require('path');
+
+function ensureDir(dirPath) {
+  fs.mkdirSync(dirPath, { recursive: true });
+}
+
+function readJson(filePath, fallback = {}) {
+  try {
+    if (!filePath || !fs.existsSync(filePath)) return fallback;
+    return JSON.parse(fs.readFileSync(filePath, 'utf8'));
+  } catch {
+    return fallback;
+  }
+}
+
+function writeJson(filePath, data) {
+  ensureDir(path.dirname(filePath));
+  fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
+}
+
+function defaultMemoryPath(name) {
+  return path.resolve('memory', name);
+}
+
+module.exports = {
+  readJson,
+  writeJson,
+  defaultMemoryPath
+};
+

+ 31 - 0
claude-code-voc-intelligence/mcp/src/core/result-envelope.js

@@ -0,0 +1,31 @@
+function okResult(payload = {}) {
+  return {
+    status: 'ok',
+    assistantMessage: payload.assistantMessage || '',
+    summary: payload.summary || {},
+    data: payload.data || {},
+    files: payload.files || [],
+    nextActions: payload.nextActions || [],
+    warnings: payload.warnings || [],
+    errors: payload.errors || []
+  };
+}
+
+function errorResult(message, payload = {}) {
+  return {
+    status: 'error',
+    assistantMessage: message,
+    summary: payload.summary || {},
+    data: payload.data || {},
+    files: payload.files || [],
+    nextActions: payload.nextActions || [],
+    warnings: payload.warnings || [],
+    errors: payload.errors || [{ message }]
+  };
+}
+
+module.exports = {
+  okResult,
+  errorResult
+};
+

+ 172 - 0
claude-code-voc-intelligence/mcp/src/features/xiaohongshu-trend/live-collector.js

@@ -0,0 +1,172 @@
+const { XiaohongshuApi } = require('../../providers/xiaohongshu-api');
+
+function normalizeCount(value) {
+  if (value === undefined || value === null || value === '') return 0;
+  if (typeof value === 'number') return Number.isFinite(value) ? value : 0;
+  const text = String(value).trim().toLowerCase();
+  const match = text.match(/([\d.]+)/);
+  if (!match) return 0;
+  const base = Number.parseFloat(match[1]);
+  if (!Number.isFinite(base)) return 0;
+  if (text.includes('w') || text.includes('万')) return Math.round(base * 10000);
+  if (text.includes('k') || text.includes('千')) return Math.round(base * 1000);
+  return Math.round(base);
+}
+
+function noteScore(note) {
+  return normalizeCount(note.likeCount) +
+    normalizeCount(note.collectCount) * 1.5 +
+    normalizeCount(note.commentCount) * 3 +
+    normalizeCount(note.shareCount) * 2;
+}
+
+function pickTags(note) {
+  const tags = note.tag_info || note.tagList || note.tags || [];
+  if (!Array.isArray(tags)) return [];
+  return tags
+    .map(tag => tag.name || tag.tag_name || tag.title || tag)
+    .filter(Boolean)
+    .map(String);
+}
+
+function normalizeNote(raw, keyword, detail) {
+  const note = detail && (detail.id || detail.note_id) ? detail : raw;
+  const id = note.id || note.note_id || raw.id || raw.note_id;
+  const user = note.user || raw.user || note.user_info || raw.user_info || {};
+  return {
+    id,
+    platform: 'xiaohongshu',
+    keyword,
+    title: note.title || raw.title || '',
+    content: note.desc || raw.desc || note.content || raw.content || '',
+    author: user.nickname || user.name || '',
+    authorId: user.userid || user.user_id || '',
+    type: note.type || raw.type || '',
+    likeCount: normalizeCount(note.liked_count ?? raw.liked_count ?? note.likeCount ?? raw.likeCount),
+    collectCount: normalizeCount(note.collected_count ?? raw.collected_count ?? note.collectCount ?? raw.collectCount),
+    commentCount: normalizeCount(note.comments_count ?? raw.comments_count ?? note.commentCount ?? raw.commentCount),
+    shareCount: normalizeCount(note.shared_count ?? raw.shared_count ?? note.shareCount ?? raw.shareCount),
+    timestamp: note.timestamp || raw.timestamp || '',
+    url: id ? `https://www.xiaohongshu.com/explore/${id}` : '',
+    tags: pickTags(note).length ? pickTags(note) : pickTags(raw)
+  };
+}
+
+function normalizeComment(raw, noteId, keyword) {
+  const user = raw.user_info || raw.user || {};
+  return {
+    id: raw.id || raw.comment_id || `${noteId}-${Math.random().toString(36).slice(2, 8)}`,
+    noteId,
+    platform: 'xiaohongshu',
+    keyword,
+    text: raw.content || raw.text || '',
+    likeCount: normalizeCount(raw.like_count || raw.liked_count || raw.likeCount),
+    author: user.nickname || user.name || '',
+    authorId: user.user_id || user.userid || '',
+    createTime: raw.create_time || '',
+    ipLocation: raw.ip_location || '',
+    subCommentCount: normalizeCount(raw.sub_comment_count || raw.subCommentCount)
+  };
+}
+
+function extractSearchNotes(data) {
+  const items = data.items || data.note_list || data.notes || [];
+  if (!Array.isArray(items)) return [];
+  return items
+    .map(item => item.note || item)
+    .filter(note => note && (note.id || note.note_id));
+}
+
+function extractDetailNote(data) {
+  const list = data.note_list || data.notes || [];
+  return Array.isArray(list) && list[0] ? list[0] : data.note || data;
+}
+
+function extractComments(data) {
+  const comments = data.comments || data.comment_list || [];
+  return Array.isArray(comments) ? comments : [];
+}
+
+async function collectLiveDataset({ profile, token, options = {} }) {
+  const api = new XiaohongshuApi({ token });
+  const keywordLimit = Math.max(1, Number(options.keywordLimit || 2));
+  const notesPerKeyword = Math.max(1, Number(options.notesPerKeyword || 2));
+  const maxCommentPages = Math.max(0, Number(options.maxCommentPages ?? 1));
+  const keywords = (profile.keywords || []).slice(0, keywordLimit);
+  const notes = [];
+  const comments = [];
+  const warnings = [];
+  const errors = [];
+
+  for (const keyword of keywords) {
+    try {
+      const search = await api.searchNotes({
+        keyword,
+        page: 1,
+        sort: options.sort || 'popularity_descending',
+        noteType: options.noteType || '_0'
+      });
+      const rawNotes = extractSearchNotes(search.data)
+        .map(raw => normalizeNote(raw, keyword))
+        .sort((a, b) => noteScore(b) - noteScore(a))
+        .slice(0, notesPerKeyword);
+
+      for (const rawNote of rawNotes) {
+        let note = rawNote;
+        try {
+          const detail = await api.getNoteDetail({ noteId: rawNote.id });
+          note = normalizeNote(rawNote, keyword, extractDetailNote(detail.data));
+        } catch (error) {
+          warnings.push({
+            stage: 'note_detail',
+            keyword,
+            noteId: rawNote.id,
+            message: error.message,
+            httpStatus: error.httpStatus
+          });
+        }
+        notes.push(note);
+
+        let cursor = '';
+        for (let page = 0; page < maxCommentPages; page++) {
+          try {
+            const commentResult = await api.getNoteComments({ noteId: note.id, cursor });
+            extractComments(commentResult.data).forEach(rawComment => {
+              comments.push(normalizeComment(rawComment, note.id, keyword));
+            });
+            cursor = commentResult.data.cursor || '';
+            if (!commentResult.data.has_more || !cursor) break;
+          } catch (error) {
+            warnings.push({
+              stage: 'note_comments',
+              keyword,
+              noteId: note.id,
+              message: error.message,
+              httpStatus: error.httpStatus
+            });
+            break;
+          }
+        }
+      }
+    } catch (error) {
+      errors.push({
+        stage: 'search_notes',
+        keyword,
+        message: error.message,
+        httpStatus: error.httpStatus
+      });
+    }
+  }
+
+  return {
+    notes,
+    comments,
+    warnings,
+    errors
+  };
+}
+
+module.exports = {
+  collectLiveDataset
+};
+

+ 130 - 0
claude-code-voc-intelligence/mcp/src/features/xiaohongshu-trend/preference-memory.js

@@ -0,0 +1,130 @@
+const path = require('path');
+const { readJson, writeJson, defaultMemoryPath } = require('../../core/memory-store');
+
+const DEFAULT_MEMORY_FILE = 'xiaohongshu-trend-memory.json';
+
+function cleanList(values) {
+  return [...new Set((values || [])
+    .flatMap(value => Array.isArray(value) ? value : String(value || '').split(/[,,、\n\r]+|以及|与|和/))
+    .map(value => String(value || '').trim())
+    .filter(Boolean))];
+}
+
+function appendNote(notes, note) {
+  const text = String(note || '').trim();
+  if (!text) return (notes || []).slice(-20);
+  const next = [...(notes || []).filter(Boolean), text];
+  return [...new Set(next)].slice(-20);
+}
+
+function defaultTrendMemory() {
+  return {
+    version: '0.1.2',
+    preferredDirections: [],
+    blockedDirections: [],
+    focusModes: [],
+    notes: [],
+    updatedAt: ''
+  };
+}
+
+function trendMemoryPath(input = {}) {
+  return path.resolve(input.memory || input.memoryPath || defaultMemoryPath(DEFAULT_MEMORY_FILE));
+}
+
+function readTrendMemory(memoryPath) {
+  return {
+    ...defaultTrendMemory(),
+    ...readJson(memoryPath, {})
+  };
+}
+
+function extractAfter(text, pattern) {
+  const match = String(text || '').match(pattern);
+  return match ? match[1] : '';
+}
+
+function inferPreferenceFromMessage(message) {
+  const text = String(message || '').trim();
+  const preferred = cleanList([
+    extractAfter(text, /(?:保留|多给|继续给|更喜欢|适合)(.+?)(?:。|;|;|,但|但是|不要|少给|降权|$)/)
+  ]);
+  const blocked = cleanList([
+    extractAfter(text, /(?:不要|少给|降权|过滤|不适合|避开)(.+?)(?:。|;|;|,|$)/)
+  ]);
+  const focus = cleanList([
+    extractAfter(text, /(?:更偏|侧重|多看|下一版更偏)(.+?)(?:。|;|;|,|$)/)
+  ]);
+  return { preferred, blocked, focus };
+}
+
+function updateTrendMemory(input = {}) {
+  const memoryPath = trendMemoryPath(input);
+  const memory = readTrendMemory(memoryPath);
+  const inferred = inferPreferenceFromMessage(input.message || input.feedback || '');
+  const preferredDirections = cleanList([
+    ...memory.preferredDirections,
+    ...inferred.preferred,
+    ...(input.preferredDirections || [])
+  ]);
+  const blockedDirections = cleanList([
+    ...memory.blockedDirections,
+    ...inferred.blocked,
+    ...(input.blockedDirections || [])
+  ]);
+  const focusModes = cleanList([
+    ...memory.focusModes,
+    ...inferred.focus,
+    ...(input.focusModes || [])
+  ]);
+  const note = String(input.message || input.feedback || '').trim();
+  const notes = appendNote(memory.notes, note);
+  const next = {
+    ...memory,
+    version: defaultTrendMemory().version,
+    preferredDirections,
+    blockedDirections,
+    focusModes,
+    notes,
+    updatedAt: new Date().toISOString()
+  };
+  writeJson(memoryPath, next);
+  return { memoryPath, memory: next };
+}
+
+function topicMatches(topic, terms) {
+  const text = `${topic.title || ''} ${topic.angle || ''} ${topic.evidence || ''}`;
+  const domainTerms = ['奶油风', '全屋定制', '翻车', '小户型', '收纳', '预算', '环保', '板材', '门店', '转化', '设计元素', '风格'];
+  const candidates = cleanList(terms).flatMap(term => {
+    const trimmed = String(term || '').replace(/(方向|选题|内容|话术|场景|趋势)$/u, '');
+    return cleanList([
+      term,
+      trimmed,
+      ...domainTerms.filter(domainTerm => String(term || '').includes(domainTerm))
+    ]);
+  });
+  return candidates.some(term => text.includes(term));
+}
+
+function rankTopicsWithMemory(topics, memory) {
+  const preferred = cleanList(memory.preferredDirections);
+  const blocked = cleanList(memory.blockedDirections);
+  return [...topics]
+    .map((topic, originalIndex) => {
+      let score = topics.length - originalIndex;
+      if (topicMatches(topic, preferred)) score += 20;
+      if (topicMatches(topic, blocked)) score -= 50;
+      return { topic, score };
+    })
+    .sort((a, b) => b.score - a.score)
+    .map(item => item.topic);
+}
+
+module.exports = {
+  DEFAULT_MEMORY_FILE,
+  trendMemoryPath,
+  readTrendMemory,
+  updateTrendMemory,
+  rankTopicsWithMemory,
+  cleanList
+};

+ 293 - 0
claude-code-voc-intelligence/mcp/src/features/xiaohongshu-trend/report.js

@@ -0,0 +1,293 @@
+const fs = require('fs');
+const path = require('path');
+const { rankTopicsWithMemory, cleanList } = require('./preference-memory');
+
+function ensureDir(dirPath) {
+  fs.mkdirSync(dirPath, { recursive: true });
+}
+
+function scoreNote(note) {
+  return Number(note.likeCount || 0) +
+    Number(note.collectCount || 0) * 1.5 +
+    Number(note.commentCount || 0) * 3 +
+    Number(note.shareCount || 0) * 2;
+}
+
+function asList(value) {
+  if (!value) return [];
+  return Array.isArray(value) ? value.filter(Boolean).map(String) : [String(value)];
+}
+
+function formatCount(value) {
+  const count = Number(value || 0);
+  if (!Number.isFinite(count) || count <= 0) return '0';
+  if (count >= 10000) {
+    return `${(count / 10000).toFixed(count >= 100000 ? 1 : 1).replace(/\.0$/, '')}w`;
+  }
+  return String(Math.round(count));
+}
+
+function collectTerms(items, terms) {
+  return terms
+    .map(term => {
+      const count = items.filter(item => `${item.title || ''} ${item.content || ''} ${item.text || ''}`.includes(term)).length;
+      return { term, count };
+    })
+    .filter(item => item.count > 0)
+    .sort((a, b) => b.count - a.count || a.term.localeCompare(b.term));
+}
+
+function memoryHasSignal(memory = {}) {
+  return Boolean(
+    cleanList(memory.preferredDirections).length ||
+    cleanList(memory.blockedDirections).length ||
+    cleanList(memory.focusModes).length
+  );
+}
+
+function buildMemorySection(memory = {}) {
+  if (!memoryHasSignal(memory)) return [];
+  const preferred = cleanList(memory.preferredDirections);
+  const blocked = cleanList(memory.blockedDirections);
+  const focus = cleanList(memory.focusModes);
+  return [
+    '## 已读取偏好',
+    '',
+    preferred.length ? `- 优先保留:${preferred.join('、')}` : '',
+    blocked.length ? `- 后续降权:${blocked.join('、')}` : '',
+    focus.length ? `- 本轮侧重:${focus.join('、')}` : ''
+  ].filter(Boolean);
+}
+
+function buildScopeSection({ profile, notes, comments, collectionMode }) {
+  const audience = asList(profile.targetAudience).slice(0, 3).join('、') || '未标注';
+  const keywords = asList(profile.keywords).slice(0, 5).join('、') || '未标注';
+  return [
+    '## 数据口径',
+    '',
+    `- 行业/品类:${profile.industry || '未标注'}`,
+    `- 目标人群:${audience}`,
+    `- 关注关键词:${keywords}`,
+    `- 样本:${notes.length} 条笔记 / ${comments.length} 条评论`,
+    collectionMode === 'sample'
+      ? '- 说明:本次为演示样例,用于确认交付形式;接入 VOC 服务后可切换真实小红书采集。'
+      : ''
+  ].filter(Boolean);
+}
+
+function findTopComment(comments, noteId) {
+  return comments
+    .filter(comment => comment.noteId === noteId)
+    .sort((a, b) => Number(b.likeCount || 0) - Number(a.likeCount || 0))[0];
+}
+
+function buildEvidenceSection(notes, comments) {
+  const rows = notes.slice(0, 3).map((note, index) => {
+    const topComment = findTopComment(comments, note.id);
+    return [
+      `${index + 1}. ${note.title || '未命名笔记'}`,
+      `   - 作者:${note.author || '未标注'} | 互动:${formatCount(note.likeCount)}赞 / ${formatCount(note.collectCount)}藏 / ${formatCount(note.commentCount)}评`,
+      topComment ? `   - 高赞评论:${topComment.text}` : ''
+    ].filter(Boolean).join('\n');
+  });
+  return [
+    '## 证据样本',
+    '',
+    rows.length ? rows.join('\n') : '暂无可展示样本。'
+  ];
+}
+
+function buildConcernLines(decisionSignals) {
+  if (!decisionSignals.length) {
+    return ['- 暂未出现稳定高频顾虑,建议先扩大关键词或评论样本。'];
+  }
+  return decisionSignals.slice(0, 5).map(item => (
+    `- ${item.term}:在样本中出现 ${item.count} 次,适合转成选题钩子或门店追问。`
+  ));
+}
+
+function buildObservationLines({ notes, comments, styleSignals, decisionSignals, elementSignals }) {
+  const topStyle = firstTerm(styleSignals, '风格偏好');
+  const secondStyle = firstTerm(styleSignals.slice(1), '');
+  const topConcern = firstTerm(decisionSignals, '决策顾虑');
+  const secondConcern = firstTerm(decisionSignals.slice(1), '');
+  const topElement = firstTerm(elementSignals, '设计元素');
+  const topNote = notes[0] || {};
+  const topComment = topNote.id ? findTopComment(comments, topNote.id) : comments[0];
+  const lines = [
+    `1. 样本里“${topStyle}”出现更靠前,但它需要和“${topConcern}”一起看,不能直接等同于真实需求。`,
+    secondStyle
+      ? `2. “${secondStyle}”也在样本中出现,说明风格判断要继续比较不同人群和户型场景。`
+      : `2. 暂时没有第二个稳定风格信号,建议下一轮扩大风格类样本。`,
+    secondConcern
+      ? `3. 用户顾虑里除了“${topConcern}”,还出现了“${secondConcern}”,这两个点需要用户真实反馈来排优先级。`
+      : `3. 当前主要顾虑集中在“${topConcern}”,还不能判断是否足够普遍。`,
+    `4. “${topElement}”这类具体元素可以继续验证,因为它更容易连接到方案、价格、维护和售后承诺。`,
+    topComment && topComment.text
+      ? `5. 高赞评论提到“${topComment.text}”,适合作为下一轮向客户/销售核验的问题。`
+      : `5. 当前评论证据不足,下一轮需要补充一条真实客户原话来校准。`
+  ];
+  return lines;
+}
+
+function firstTerm(items, fallback = '') {
+  return items && items[0] && items[0].term ? items[0].term : fallback;
+}
+
+function buildHypothesisLines({ styleSignals, decisionSignals, elementSignals }) {
+  const topStyle = firstTerm(styleSignals, '风格偏好');
+  const topConcern = firstTerm(decisionSignals, '决策顾虑');
+  const topElement = firstTerm(elementSignals, '设计元素');
+  return [
+    `1. ${topStyle} 可能不是单独流行,而是和“${topConcern}”一起影响客户选择。`,
+    `2. ${topElement} 这类具体元素更适合继续验证,因为它能连接到预算、维护和落地方案。`,
+    `3. 第一轮样本更像是在提示“客户怕什么”,下一轮需要用你的真实客户反馈校准。`
+  ];
+}
+
+function buildDynamicValidationQuestions({ notes, comments, styleSignals, decisionSignals, elementSignals, rankedTopics }) {
+  const topStyle = firstTerm(styleSignals, '当前风格');
+  const topConcern = firstTerm(decisionSignals, '当前顾虑');
+  const topElement = firstTerm(elementSignals, '当前设计元素');
+  const topNote = notes[0] || {};
+  const topComment = topNote.id ? findTopComment(comments, topNote.id) : comments[0];
+  const topTopic = rankedTopics[0] || {};
+  return [
+    `样本里“${topStyle}”和“${topConcern}”同时出现,你实际接触的客户是先被风格吸引,还是先担心${topConcern}?`,
+    topComment && topComment.text
+      ? `这条高赞评论“${topComment.text}”像不像你门店/销售/私域里真实听到的问题?如果不像,真实问题更接近什么?`
+      : `当前样本缺少高赞评论证据,你手上有没有客户最近反复问的一句话?`,
+    `围绕“${topElement}”,你们现在有没有可证明的案例、材料、价格或售后承诺?没有的话,这个方向先不要直接定稿。`,
+    topTopic.title
+      ? `如果只继续深挖一个方向,你更想验证“${topTopic.title}”,还是换成更贴近你业务的方向?`
+      : `如果只继续深挖一个方向,你更想看风格趋势、用户顾虑、设计元素,还是门店转化话术?`
+  ];
+}
+
+function buildTrendReport({ profile, dataset, collectionMode, memory = {} }) {
+  const notes = [...(dataset.notes || [])].sort((a, b) => scoreNote(b) - scoreNote(a));
+  const comments = [...(dataset.comments || [])].sort((a, b) => Number(b.likeCount || 0) - Number(a.likeCount || 0));
+  const allItems = [...notes, ...comments];
+
+  const styleSignals = collectTerms(allItems, ['奶油风', '原木', '弧形', '无主灯', '低饱和', '显大', '耐看']);
+  const decisionSignals = collectTerms(allItems, ['预算', '增项', '环保', '板材', '不好打理', '落灰', '翻车', '动线', '收纳']);
+  const elementSignals = collectTerms(allItems, ['开放格', '柜门', '灯带', '抽屉', '五金', '餐边柜', '玄关', '衣柜']);
+
+  const oneLine = '本轮小红书趋势里,最值得关注的不是单一风格,而是“好看、好打理、少翻车、预算透明”一起进入女性客户决策。';
+
+  const topTopics = [
+    {
+      title: '别只讲奶油风好看,要讲两年后还耐不耐看',
+      angle: '把风格趋势转成长期使用顾虑',
+      evidence: '评论反复提到显旧、不好打理、有孩子家庭难维护'
+    },
+    {
+      title: '全屋定制别先问要什么柜子,先问最怕哪里翻车',
+      angle: '用风险反推设计方案',
+      evidence: '高赞评论集中在开放格落灰、动线堵、增项预算'
+    },
+    {
+      title: '小户型收纳不是柜子越多越好',
+      angle: '用反常识切入,讲空间呼吸感',
+      evidence: '用户担心满墙柜显压抑、入住后动线不舒服'
+    }
+  ];
+  const rankedTopics = rankTopicsWithMemory(topTopics, memory);
+  const validationQuestions = buildDynamicValidationQuestions({
+    notes,
+    comments,
+    styleSignals,
+    decisionSignals,
+    elementSignals,
+    rankedTopics
+  });
+
+  const salesTalk = [
+    '如果客户只说想要奶油风,可以追问:你更担心不好打理、显旧,还是预算超支?',
+    '讲全屋定制时,不要先推柜体套餐,先帮客户列出最容易翻车的 3 个位置。',
+    '对小户型客户,可以用“收纳够用但不压抑”替代“柜子越多越好”。'
+  ];
+
+  const assistantMessage = [
+    `# 小红书趋势情报${collectionMode === 'sample' ? '(演示样例)' : ''}`,
+    '',
+    '## 第一轮初步判断(待校准)',
+    '',
+    `${oneLine} 这还不是最终结论,需要先结合你的真实客户反馈做一轮校准。`,
+    '',
+    ...buildScopeSection({ profile, notes, comments, collectionMode }),
+    '',
+    ...buildMemorySection(memory),
+    memoryHasSignal(memory) ? '' : '',
+    '## 样本观察 Top 5',
+    '',
+    ...buildObservationLines({ notes, comments, styleSignals, decisionSignals, elementSignals }),
+    '',
+    '## 本轮机会假设',
+    '',
+    ...buildHypothesisLines({ styleSignals, decisionSignals, elementSignals }),
+    '',
+    '## 用户顾虑/决策阻力',
+    '',
+    ...buildConcernLines(decisionSignals),
+    '',
+    ...buildEvidenceSection(notes, comments),
+    '',
+    '## 可继续验证的选题方向',
+    '',
+    ...rankedTopics.map((topic, index) => [
+      `${index + 1}. ${topic.title}`,
+      `   - 角度:${topic.angle}`,
+      `   - 证据:${topic.evidence}`
+    ].join('\n')),
+    '',
+    '## 暂可参考话术(待确认)',
+    '',
+    ...salesTalk.map(item => `- ${item}`),
+    '',
+    '## 下一轮请先校准这些问题',
+    '',
+    ...validationQuestions.map((question, index) => `${index + 1}. ${question}`),
+    '',
+    '你可以直接按序号简单回答。回答后我会把本轮假设收敛成更贴近你真实业务的趋势判断、选题方向和待验证话术。'
+  ].filter(Boolean).join('\n');
+
+  return {
+    assistantMessage,
+    summary: {
+      workflowStage: 'initial_hypothesis_needs_calibration',
+      noteCount: notes.length,
+      commentCount: comments.length,
+      topicCount: rankedTopics.length,
+      preferenceMemoryApplied: memoryHasSignal(memory),
+      topStyleSignals: styleSignals.slice(0, 5),
+      topDecisionSignals: decisionSignals.slice(0, 5),
+      topElementSignals: elementSignals.slice(0, 5),
+      evidenceNoteCount: Math.min(notes.length, 3),
+      validationQuestionCount: validationQuestions.length
+    },
+    data: {
+      profile,
+      notes,
+      comments,
+      topTopics: rankedTopics,
+      validationQuestions,
+      preferenceMemory: memory,
+      salesTalk
+    }
+  };
+}
+
+function writeReport(outputDir, report) {
+  ensureDir(outputDir);
+  const mdPath = path.join(outputDir, 'xiaohongshu-trend-report.md');
+  const jsonPath = path.join(outputDir, 'xiaohongshu-trend-report.json');
+  fs.writeFileSync(mdPath, report.assistantMessage, 'utf8');
+  fs.writeFileSync(jsonPath, JSON.stringify(report, null, 2), 'utf8');
+  return [mdPath, jsonPath];
+}
+
+module.exports = {
+  buildTrendReport,
+  writeReport
+};

+ 79 - 0
claude-code-voc-intelligence/mcp/src/features/xiaohongshu-trend/sample-data.js

@@ -0,0 +1,79 @@
+function buildSampleDataset(profile = {}) {
+  const keywords = Array.isArray(profile.keywords) && profile.keywords.length
+    ? profile.keywords
+    : ['全屋定制', '奶油风装修', '小户型收纳'];
+
+  const notes = [
+    {
+      id: 'xhs-home-001',
+      keyword: keywords[0],
+      title: '全屋定制最容易翻车的不是价格,是柜子做完不好住',
+      content: '很多女生装修前只看奶油风效果图,入住后才发现开放格落灰、柜门不好打理、收纳不够。全屋定制要先想生活动线,再看风格。',
+      author: '住进理想家',
+      likeCount: 4280,
+      collectCount: 1900,
+      commentCount: 386,
+      shareCount: 220,
+      tags: ['全屋定制', '装修避坑', '收纳']
+    },
+    {
+      id: 'xhs-home-002',
+      keyword: keywords[1] || keywords[0],
+      title: '奶油风下半年还会流行吗?关键看材质和灯光',
+      content: '奶油风不是越白越好看,真正高级的是低饱和颜色、木纹、弧形和无主灯配合。评论更关心耐看、好打理、显大。',
+      author: '软装设计阿晴',
+      likeCount: 6920,
+      collectCount: 4100,
+      commentCount: 612,
+      shareCount: 488,
+      tags: ['奶油风', '装修风格', '设计元素']
+    },
+    {
+      id: 'xhs-home-003',
+      keyword: keywords[2] || keywords[0],
+      title: '小户型别再硬塞满墙柜,真的会显压抑',
+      content: '小户型收纳不是柜子越多越好,要留出视觉呼吸感。评论区很多人问玄关、餐边柜、卧室衣柜怎么做才不显堵。',
+      author: '小家改造研究所',
+      likeCount: 3580,
+      collectCount: 2600,
+      commentCount: 301,
+      shareCount: 170,
+      tags: ['小户型', '收纳', '柜体设计']
+    }
+  ];
+
+  const comments = [
+    {
+      noteId: 'xhs-home-001',
+      text: '我就是开放格翻车,擦灰擦到怀疑人生,现在只想做能关起来的柜子。',
+      likeCount: 318
+    },
+    {
+      noteId: 'xhs-home-001',
+      text: '预算最怕增项,前期说得很好,后面五金、灯带、抽屉全加钱。',
+      likeCount: 286
+    },
+    {
+      noteId: 'xhs-home-002',
+      text: '奶油风好看是好看,就怕两年后显旧,而且有孩子真的怕不好打理。',
+      likeCount: 401
+    },
+    {
+      noteId: 'xhs-home-002',
+      text: '我更想知道敏感鼻炎家庭怎么选板材,环保比风格重要。',
+      likeCount: 229
+    },
+    {
+      noteId: 'xhs-home-003',
+      text: '小户型真的不能只看收纳图,住进去以后动线堵才是最崩溃的。',
+      likeCount: 192
+    }
+  ];
+
+  return { notes, comments };
+}
+
+module.exports = {
+  buildSampleDataset
+};
+

+ 86 - 0
claude-code-voc-intelligence/mcp/src/providers/xiaohongshu-api.js

@@ -0,0 +1,86 @@
+const DEFAULT_BASE_URL = 'https://server.fmode.cn/thapi/v1/xiaohongshu/app';
+
+function buildUrl(baseUrl, path, query = {}) {
+  const url = new URL(`${baseUrl.replace(/\/$/, '')}/${path.replace(/^\//, '')}`);
+  Object.entries(query).forEach(([key, value]) => {
+    if (value !== undefined && value !== null && value !== '') {
+      url.searchParams.set(key, String(value));
+    }
+  });
+  return url.toString();
+}
+
+function unwrapData(json) {
+  if (!json || typeof json !== 'object') return {};
+  return json.data?.data || json.data || json;
+}
+
+function redactSecret(value) {
+  return String(value || '')
+    .replace(/Bearer\s+[^"'\s,}]+/gi, 'Bearer [REDACTED]')
+    .replace(/\bsk-[A-Za-z0-9_-]{12,}\b/g, 'sk-[REDACTED]')
+    .replace(/\br:[A-Za-z0-9]{12,}\b/g, 'r:[REDACTED]')
+    .replace(/("Authorization"\s*:\s*")([^"]+)(")/gi, '$1[REDACTED]$3')
+    .replace(/("api[_-]?token"\s*:\s*")([^"]+)(")/gi, '$1[REDACTED]$3');
+}
+
+async function requestJson(url, token) {
+  const response = await fetch(url, {
+    method: 'GET',
+    headers: {
+      Authorization: `Bearer ${token}`,
+      Accept: 'application/json'
+    }
+  });
+  const text = await response.text();
+  let json;
+  try {
+    json = JSON.parse(text);
+  } catch {
+    json = undefined;
+  }
+  if (!response.ok) {
+    const message = json?.message || json?.msg || json?.data?.msg || text || `HTTP ${response.status}`;
+    const error = new Error(redactSecret(message));
+    error.httpStatus = response.status;
+    error.responseBody = redactSecret(text);
+    throw error;
+  }
+  return { json, data: unwrapData(json), rawText: text };
+}
+
+class XiaohongshuApi {
+  constructor(options = {}) {
+    this.baseUrl = options.baseUrl || DEFAULT_BASE_URL;
+    this.token = options.token;
+  }
+
+  async searchNotes({ keyword, page = 1, sort = 'popularity_descending', noteType = '_0' }) {
+    const url = buildUrl(this.baseUrl, 'search_notes', {
+      keyword,
+      page,
+      sort,
+      note_type: noteType
+    });
+    return requestJson(url, this.token);
+  }
+
+  async getNoteDetail({ noteId }) {
+    const url = buildUrl(this.baseUrl, 'get_note_info', {
+      note_id: noteId
+    });
+    return requestJson(url, this.token);
+  }
+
+  async getNoteComments({ noteId, cursor = '' }) {
+    const url = buildUrl(this.baseUrl, 'get_note_comments', {
+      note_id: noteId,
+      cursor
+    });
+    return requestJson(url, this.token);
+  }
+}
+
+module.exports = {
+  XiaohongshuApi
+};

+ 160 - 0
claude-code-voc-intelligence/mcp/src/server.js

@@ -0,0 +1,160 @@
+#!/usr/bin/env node
+const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
+const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
+const { z } = require('zod');
+const { readXiaohongshuToken } = require('./core/credentials');
+const { runXiaohongshuTrend } = require('./tools/xiaohongshu-trend-run');
+const { updateXiaohongshuPreference } = require('./tools/xiaohongshu-preference-update');
+
+function asToolResult(result) {
+  return {
+    content: [
+      {
+        type: 'text',
+        text: result.assistantMessage || JSON.stringify(result, null, 2)
+      }
+    ],
+    structuredContent: result,
+    isError: result.status !== 'ok'
+  };
+}
+
+function createServer() {
+  const server = new McpServer({
+    name: 'voc-intelligence',
+    version: '0.1.2'
+  });
+
+  server.registerTool(
+    'voc_xiaohongshu_token_check',
+    {
+      title: 'Check Xiaohongshu Token',
+      description: 'Check whether a Xiaohongshu/TikHub API token is configured for live collection.',
+      inputSchema: {
+        xiaohongshuToken: z.string().optional()
+      },
+      outputSchema: {
+        status: z.string(),
+        assistantMessage: z.string(),
+        configured: z.boolean()
+      }
+    },
+    async input => {
+      const token = readXiaohongshuToken(input);
+      const result = {
+        status: token ? 'ok' : 'needs_token',
+        configured: Boolean(token),
+        assistantMessage: token
+          ? '小红书 live 采集 Token 已配置,可以尝试小规模真实采集。'
+          : [
+              '还没有检测到小红书 live 采集 Token。',
+              '',
+              '可以先使用 sample 模式演示;如需真实采集,请设置 `XIAOHONGSHU_API_TOKEN` 或写入 `~/.claude/voc-credentials.json`。'
+            ].join('\n')
+      };
+      return asToolResult(result);
+    }
+  );
+
+  server.registerTool(
+    'voc_xiaohongshu_trend_run',
+    {
+      title: 'Run Xiaohongshu Trend Intelligence',
+      description: [
+        'Generate first-round Xiaohongshu trend observations and validation questions from an industry profile.',
+        'P0 supports sample mode for demos and course recording.',
+        'Returns assistantMessage as the user-facing body; first-round output is a hypothesis that needs user calibration.'
+      ].join(' '),
+      inputSchema: {
+        collectionMode: z.enum(['sample', 'live']).optional().describe('Use sample for P0 demo. Live will be enabled after VOC provider integration.'),
+        profile: z.string().optional().describe('Optional profile JSON path.'),
+        output: z.string().optional().describe('Optional output directory.'),
+        memory: z.string().optional().describe('Optional preference memory JSON path.'),
+        memoryPath: z.string().optional().describe('Optional preference memory JSON path.'),
+        project: z.string().optional(),
+        industry: z.string().optional(),
+        businessType: z.string().optional(),
+        targetAudience: z.array(z.string()).optional(),
+        keywords: z.array(z.string()).optional(),
+        trendQuestions: z.array(z.string()).optional(),
+        mustTrackSignals: z.array(z.string()).optional(),
+        keywordLimit: z.number().int().min(1).max(10).optional(),
+        notesPerKeyword: z.number().int().min(1).max(10).optional(),
+        maxCommentPages: z.number().int().min(0).max(5).optional(),
+        sort: z.enum(['general', 'time_descending', 'popularity_descending']).optional(),
+        noteType: z.enum(['_0', '_1', '_2']).optional(),
+        xiaohongshuToken: z.string().optional()
+      },
+      outputSchema: {
+        status: z.string(),
+        assistantMessage: z.string(),
+        summary: z.record(z.any()).optional(),
+        data: z.record(z.any()).optional(),
+        files: z.array(z.string()).optional(),
+        nextActions: z.array(z.string()).optional(),
+        warnings: z.array(z.string()).optional(),
+        errors: z.array(z.any()).optional()
+      }
+    },
+    async input => {
+      const result = await runXiaohongshuTrend({
+        ...input,
+        collectionMode: input.collectionMode || 'sample'
+      });
+      return asToolResult(result);
+    }
+  );
+
+  server.registerTool(
+    'voc_xiaohongshu_preference_update',
+    {
+      title: 'Update Xiaohongshu Trend Preference',
+      description: [
+        'Save user feedback for the next Xiaohongshu trend refinement.',
+        'Use after the user answers validation questions or says what to keep, block, downgrade, or focus on next time.'
+      ].join(' '),
+      inputSchema: {
+        message: z.string().optional().describe('Natural-language user feedback.'),
+        feedback: z.string().optional().describe('Natural-language user feedback.'),
+        memory: z.string().optional().describe('Optional preference memory JSON path.'),
+        memoryPath: z.string().optional().describe('Optional preference memory JSON path.'),
+        preferredDirections: z.array(z.string()).optional(),
+        blockedDirections: z.array(z.string()).optional(),
+        focusModes: z.array(z.string()).optional()
+      },
+      outputSchema: {
+        status: z.string(),
+        assistantMessage: z.string(),
+        summary: z.record(z.any()).optional(),
+        data: z.record(z.any()).optional(),
+        files: z.array(z.string()).optional(),
+        nextActions: z.array(z.string()).optional(),
+        warnings: z.array(z.string()).optional(),
+        errors: z.array(z.any()).optional()
+      }
+    },
+    async input => {
+      const result = await updateXiaohongshuPreference(input);
+      return asToolResult(result);
+    }
+  );
+
+  return server;
+}
+
+async function main() {
+  const server = createServer();
+  const transport = new StdioServerTransport();
+  await server.connect(transport);
+}
+
+if (require.main === module) {
+  main().catch(error => {
+    console.error(error);
+    process.exit(1);
+  });
+}
+
+module.exports = {
+  createServer
+};

+ 85 - 0
claude-code-voc-intelligence/mcp/src/tools/xiaohongshu-preference-update.js

@@ -0,0 +1,85 @@
+#!/usr/bin/env node
+const { okResult, errorResult } = require('../core/result-envelope');
+const { updateTrendMemory } = require('../features/xiaohongshu-trend/preference-memory');
+
+function parseArgs(argv) {
+  const args = {};
+  for (let i = 0; i < argv.length; i++) {
+    const token = argv[i];
+    if (!token.startsWith('--')) continue;
+    const eq = token.indexOf('=');
+    if (eq >= 0) {
+      args[token.slice(2, eq)] = token.slice(eq + 1);
+      continue;
+    }
+    const key = token.slice(2);
+    const next = argv[i + 1];
+    if (next && !next.startsWith('--')) {
+      args[key] = next;
+      i++;
+    } else {
+      args[key] = true;
+    }
+  }
+  return args;
+}
+
+function splitList(value) {
+  if (!value) return [];
+  if (Array.isArray(value)) return value.map(String).map(item => item.trim()).filter(Boolean);
+  return String(value).split(/[,,、\n\r]+/).map(item => item.trim()).filter(Boolean);
+}
+
+async function updateXiaohongshuPreference(input = {}) {
+  const { memoryPath, memory } = updateTrendMemory({
+    ...input,
+    preferredDirections: splitList(input.preferredDirections || input['preferred-directions']),
+    blockedDirections: splitList(input.blockedDirections || input['blocked-directions']),
+    focusModes: splitList(input.focusModes || input['focus-modes'])
+  });
+
+  return okResult({
+    assistantMessage: [
+      '已保存本次校准反馈。',
+      '',
+      memory.preferredDirections.length ? `优先保留:${memory.preferredDirections.join('、')}` : '',
+      memory.blockedDirections.length ? `后续降权:${memory.blockedDirections.join('、')}` : '',
+      memory.focusModes.length ? `下一轮侧重:${memory.focusModes.join('、')}` : '',
+      '',
+      '我会把这次回答作为校准输入,不把第一轮样本直接当最终结论。下一轮会按这些反馈重新排序样本、收敛机会假设,并把话术标成待验证版本。'
+    ].filter(Boolean).join('\n'),
+    summary: {
+      preferredCount: memory.preferredDirections.length,
+      blockedCount: memory.blockedDirections.length,
+      focusCount: memory.focusModes.length
+    },
+    data: { memory },
+    files: [memoryPath],
+    nextActions: ['基于已保存反馈生成下一轮收敛判断', '继续补充一条真实客户原话或一个不准确的样本点']
+  });
+}
+
+async function main() {
+  const args = parseArgs(process.argv.slice(2));
+  try {
+    const result = await updateXiaohongshuPreference(args);
+    console.log(JSON.stringify(result, null, 2));
+    if (args.resultPrefix || args['result-prefix']) {
+      const prefix = args.resultPrefix || args['result-prefix'];
+      console.log(`${prefix}=${JSON.stringify(result)}`);
+    }
+    process.exit(0);
+  } catch (error) {
+    const result = errorResult(error.message || String(error));
+    console.log(JSON.stringify(result, null, 2));
+    process.exit(1);
+  }
+}
+
+if (require.main === module) {
+  main();
+}
+
+module.exports = {
+  updateXiaohongshuPreference
+};

+ 208 - 0
claude-code-voc-intelligence/mcp/src/tools/xiaohongshu-trend-run.js

@@ -0,0 +1,208 @@
+#!/usr/bin/env node
+const fs = require('fs');
+const path = require('path');
+const { okResult, errorResult } = require('../core/result-envelope');
+const { readXiaohongshuToken } = require('../core/credentials');
+const { buildSampleDataset } = require('../features/xiaohongshu-trend/sample-data');
+const { collectLiveDataset } = require('../features/xiaohongshu-trend/live-collector');
+const { buildTrendReport, writeReport } = require('../features/xiaohongshu-trend/report');
+const { trendMemoryPath, readTrendMemory } = require('../features/xiaohongshu-trend/preference-memory');
+
+function parseArgs(argv) {
+  const args = {};
+  for (let i = 0; i < argv.length; i++) {
+    const token = argv[i];
+    if (!token.startsWith('--')) continue;
+    const eq = token.indexOf('=');
+    if (eq >= 0) {
+      args[token.slice(2, eq)] = token.slice(eq + 1);
+      continue;
+    }
+    const key = token.slice(2);
+    const next = argv[i + 1];
+    if (next && !next.startsWith('--')) {
+      args[key] = next;
+      i++;
+    } else {
+      args[key] = true;
+    }
+  }
+  return args;
+}
+
+function splitList(value) {
+  if (!value) return [];
+  if (Array.isArray(value)) return value.map(String).map(item => item.trim()).filter(Boolean);
+  return String(value).split(/[,,、\n\r]+/).map(item => item.trim()).filter(Boolean);
+}
+
+function readJsonIfExists(filePath) {
+  if (!filePath) return {};
+  const absolute = path.resolve(filePath);
+  if (!fs.existsSync(absolute)) return {};
+  return JSON.parse(fs.readFileSync(absolute, 'utf8'));
+}
+
+function loadProfile(args) {
+  const fromFile = readJsonIfExists(args.profile);
+  return {
+    ...fromFile,
+    project: args.project || fromFile.project || '小红书趋势情报官',
+    industry: args.industry || fromFile.industry || '家装全屋定制',
+    businessType: args.businessType || args['business-type'] || fromFile.businessType || '门店/设计服务/全屋定制服务商',
+    targetAudience: splitList(args.targetAudience || args['target-audience']).length
+      ? splitList(args.targetAudience || args['target-audience'])
+      : (fromFile.targetAudience || ['准备装修的女性客户']),
+    keywords: splitList(args.keywords).length
+      ? splitList(args.keywords)
+      : (fromFile.keywords || ['全屋定制', '奶油风装修', '小户型收纳']),
+    trendQuestions: splitList(args.trendQuestions || args['trend-questions']).length
+      ? splitList(args.trendQuestions || args['trend-questions'])
+      : (fromFile.trendQuestions || []),
+    mustTrackSignals: splitList(args.mustTrackSignals || args['must-track-signals']).length
+      ? splitList(args.mustTrackSignals || args['must-track-signals'])
+      : (fromFile.mustTrackSignals || [])
+  };
+}
+
+async function runXiaohongshuTrend(input = {}) {
+  const collectionMode = input.collectionMode || input['collection-mode'] || 'sample';
+  const outputDir = path.resolve(input.output || path.join('outputs', 'claude-code-xiaohongshu-trend', new Date().toISOString().slice(0, 10)));
+  const profile = loadProfile(input);
+  const memoryPath = trendMemoryPath(input);
+  const memory = readTrendMemory(memoryPath);
+  const warnings = [];
+  const errors = [];
+  let dataset;
+
+  if (collectionMode === 'sample') {
+    dataset = buildSampleDataset(profile);
+    warnings.push('P0 阶段使用 sample 演示数据,真实采集需要接入 VOC 服务。');
+  } else if (collectionMode === 'live') {
+    const token = readXiaohongshuToken(input);
+    if (!token) {
+      return {
+        status: 'needs_token',
+        assistantMessage: [
+          '需要先配置小红书/VOC 社媒采集 Token,才能进入 live 真实采集。',
+          '',
+          '你可以先用 sample 模式验证流程;要跑真实采集,请设置环境变量:',
+          '',
+          '```bash',
+          'set XIAOHONGSHU_API_TOKEN=你的token',
+          '```',
+          '',
+          '也可以放到 `~/.claude/voc-credentials.json`:',
+          '',
+          '```json',
+          '{ "xiaohongshuToken": "你的token" }',
+          '```'
+        ].join('\n'),
+        summary: {},
+        data: { requestedCollectionMode: collectionMode },
+        files: [],
+        nextActions: ['配置 token 后重新运行 live 模式', '先用 sample 模式完成教学演示'],
+        warnings: [],
+        errors: []
+      };
+    }
+
+    const liveDataset = await collectLiveDataset({
+      profile,
+      token,
+      options: {
+        keywordLimit: input.keywordLimit || input['keyword-limit'],
+        notesPerKeyword: input.notesPerKeyword || input['notes-per-keyword'],
+        maxCommentPages: input.maxCommentPages || input['max-comment-pages'],
+        sort: input.sort,
+        noteType: input.noteType || input['note-type']
+      }
+    });
+    dataset = {
+      notes: liveDataset.notes,
+      comments: liveDataset.comments
+    };
+    warnings.push(...liveDataset.warnings);
+    errors.push(...liveDataset.errors);
+
+    if (!dataset.notes.length) {
+      return {
+        status: 'no_live_samples',
+        assistantMessage: [
+          '本次 live 模式没有采到可用于分析的小红书样本。',
+          '',
+          '建议先缩小关键词或改用更具体的行业词,例如:',
+          '- 全屋定制',
+          '- 奶油风装修',
+          '- 衣柜设计避坑',
+          '',
+          '也可以先用 sample 模式完成流程演示。'
+        ].join('\n'),
+        summary: {
+          noteCount: 0,
+          commentCount: dataset.comments.length,
+          errorCount: errors.length
+        },
+        data: { profile },
+        files: [],
+        nextActions: ['换更具体关键词后重试 live 模式', '先用 sample 模式演示'],
+        warnings,
+        errors
+      };
+    }
+  } else {
+    return errorResult(`Unsupported collectionMode: ${collectionMode}`);
+  }
+
+  const report = buildTrendReport({ profile, dataset, collectionMode, memory });
+  const files = writeReport(outputDir, report);
+  const validationQuestions = report.data.validationQuestions || [];
+  const nextActions = validationQuestions.length
+    ? [
+      `先回答校准问题 1:${validationQuestions[0]}`,
+      validationQuestions[1] ? `再确认问题 2:${validationQuestions[1]}` : '',
+      '也可以直接补充一条你最近听到的真实客户反馈,我会据此收敛下一轮判断。'
+    ].filter(Boolean)
+    : [
+      '先补充一条你最近听到的真实客户反馈',
+      '再基于客户反馈收敛下一轮趋势判断'
+    ];
+
+  return okResult({
+    assistantMessage: report.assistantMessage,
+    summary: report.summary,
+    data: {
+      ...report.data,
+      memoryPath
+    },
+    files,
+    nextActions,
+    warnings,
+    errors
+  });
+}
+
+async function main() {
+  const args = parseArgs(process.argv.slice(2));
+  try {
+    const result = await runXiaohongshuTrend(args);
+    console.log(JSON.stringify(result, null, 2));
+    if (args.resultPrefix || args['result-prefix']) {
+      const prefix = args.resultPrefix || args['result-prefix'];
+      console.log(`${prefix}=${JSON.stringify(result)}`);
+    }
+    process.exit(result.status === 'ok' ? 0 : 1);
+  } catch (error) {
+    const result = errorResult(error.message || String(error));
+    console.log(JSON.stringify(result, null, 2));
+    process.exit(1);
+  }
+}
+
+if (require.main === module) {
+  main();
+}
+
+module.exports = {
+  runXiaohongshuTrend
+};

+ 37 - 0
claude-code-voc-intelligence/memory-templates/xiaohongshu-trend-profile.json

@@ -0,0 +1,37 @@
+{
+  "project": "小红书趋势情报官",
+  "industry": "家装全屋定制",
+  "businessType": "门店/设计服务/全屋定制服务商",
+  "targetAudience": [
+    "准备装修的女性客户",
+    "重视颜值和收纳的家庭决策者",
+    "关注预算、环保、售后和翻车风险的客户"
+  ],
+  "platforms": [
+    "xiaohongshu"
+  ],
+  "keywords": [
+    "全屋定制",
+    "奶油风装修",
+    "小户型收纳",
+    "衣柜设计避坑",
+    "厨房橱柜设计",
+    "装修翻车"
+  ],
+  "trendQuestions": [
+    "下季度女性客户更关注哪些装修风格和设计元素?",
+    "用户在评论里反复担心哪些翻车点?",
+    "哪些内容角度可以转成门店销售话术?"
+  ],
+  "mustTrackSignals": [
+    "装修风格",
+    "设计元素",
+    "收纳需求",
+    "环保顾虑",
+    "预算焦虑",
+    "翻车风险"
+  ],
+  "blockedDirections": [],
+  "updatedAt": ""
+}
+

+ 1159 - 0
claude-code-voc-intelligence/package-lock.json

@@ -0,0 +1,1159 @@
+{
+  "name": "@gangvy/claude-code-voc-intelligence",
+  "version": "0.1.2",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "@gangvy/claude-code-voc-intelligence",
+      "version": "0.1.2",
+      "dependencies": {
+        "@modelcontextprotocol/sdk": "^1.29.0",
+        "zod": "^4.4.3"
+      },
+      "bin": {
+        "claude-voc": "bin/claude-voc.js"
+      }
+    },
+    "node_modules/@hono/node-server": {
+      "version": "1.19.14",
+      "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz",
+      "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18.14.1"
+      },
+      "peerDependencies": {
+        "hono": "^4"
+      }
+    },
+    "node_modules/@modelcontextprotocol/sdk": {
+      "version": "1.29.0",
+      "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz",
+      "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@hono/node-server": "^1.19.9",
+        "ajv": "^8.17.1",
+        "ajv-formats": "^3.0.1",
+        "content-type": "^1.0.5",
+        "cors": "^2.8.5",
+        "cross-spawn": "^7.0.5",
+        "eventsource": "^3.0.2",
+        "eventsource-parser": "^3.0.0",
+        "express": "^5.2.1",
+        "express-rate-limit": "^8.2.1",
+        "hono": "^4.11.4",
+        "jose": "^6.1.3",
+        "json-schema-typed": "^8.0.2",
+        "pkce-challenge": "^5.0.0",
+        "raw-body": "^3.0.0",
+        "zod": "^3.25 || ^4.0",
+        "zod-to-json-schema": "^3.25.1"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "@cfworker/json-schema": "^4.1.1",
+        "zod": "^3.25 || ^4.0"
+      },
+      "peerDependenciesMeta": {
+        "@cfworker/json-schema": {
+          "optional": true
+        },
+        "zod": {
+          "optional": false
+        }
+      }
+    },
+    "node_modules/accepts": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+      "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-types": "^3.0.0",
+        "negotiator": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/ajv": {
+      "version": "8.20.0",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+      "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
+      "license": "MIT",
+      "dependencies": {
+        "fast-deep-equal": "^3.1.3",
+        "fast-uri": "^3.0.1",
+        "json-schema-traverse": "^1.0.0",
+        "require-from-string": "^2.0.2"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/epoberezkin"
+      }
+    },
+    "node_modules/ajv-formats": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
+      "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
+      "license": "MIT",
+      "dependencies": {
+        "ajv": "^8.0.0"
+      },
+      "peerDependencies": {
+        "ajv": "^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "ajv": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/body-parser": {
+      "version": "2.2.2",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
+      "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
+      "license": "MIT",
+      "dependencies": {
+        "bytes": "^3.1.2",
+        "content-type": "^1.0.5",
+        "debug": "^4.4.3",
+        "http-errors": "^2.0.0",
+        "iconv-lite": "^0.7.0",
+        "on-finished": "^2.4.1",
+        "qs": "^6.14.1",
+        "raw-body": "^3.0.1",
+        "type-is": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/bytes": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+      "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/call-bound": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+      "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "get-intrinsic": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/content-disposition": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
+      "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/content-type": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+      "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/cookie": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+      "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/cookie-signature": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+      "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.6.0"
+      }
+    },
+    "node_modules/cors": {
+      "version": "2.8.6",
+      "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
+      "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
+      "license": "MIT",
+      "dependencies": {
+        "object-assign": "^4",
+        "vary": "^1"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/cross-spawn": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+      "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+      "license": "MIT",
+      "dependencies": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/depd": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+      "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/ee-first": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+      "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+      "license": "MIT"
+    },
+    "node_modules/encodeurl": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+      "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/escape-html": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+      "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+      "license": "MIT"
+    },
+    "node_modules/etag": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+      "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/eventsource": {
+      "version": "3.0.7",
+      "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
+      "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==",
+      "license": "MIT",
+      "dependencies": {
+        "eventsource-parser": "^3.0.1"
+      },
+      "engines": {
+        "node": ">=18.0.0"
+      }
+    },
+    "node_modules/eventsource-parser": {
+      "version": "3.0.8",
+      "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.8.tgz",
+      "integrity": "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18.0.0"
+      }
+    },
+    "node_modules/express": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
+      "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
+      "license": "MIT",
+      "dependencies": {
+        "accepts": "^2.0.0",
+        "body-parser": "^2.2.1",
+        "content-disposition": "^1.0.0",
+        "content-type": "^1.0.5",
+        "cookie": "^0.7.1",
+        "cookie-signature": "^1.2.1",
+        "debug": "^4.4.0",
+        "depd": "^2.0.0",
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "etag": "^1.8.1",
+        "finalhandler": "^2.1.0",
+        "fresh": "^2.0.0",
+        "http-errors": "^2.0.0",
+        "merge-descriptors": "^2.0.0",
+        "mime-types": "^3.0.0",
+        "on-finished": "^2.4.1",
+        "once": "^1.4.0",
+        "parseurl": "^1.3.3",
+        "proxy-addr": "^2.0.7",
+        "qs": "^6.14.0",
+        "range-parser": "^1.2.1",
+        "router": "^2.2.0",
+        "send": "^1.1.0",
+        "serve-static": "^2.2.0",
+        "statuses": "^2.0.1",
+        "type-is": "^2.0.1",
+        "vary": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/express-rate-limit": {
+      "version": "8.5.2",
+      "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz",
+      "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==",
+      "license": "MIT",
+      "dependencies": {
+        "ip-address": "^10.2.0"
+      },
+      "engines": {
+        "node": ">= 16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/express-rate-limit"
+      },
+      "peerDependencies": {
+        "express": ">= 4.11"
+      }
+    },
+    "node_modules/fast-deep-equal": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+      "license": "MIT"
+    },
+    "node_modules/fast-uri": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
+      "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/fastify"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/fastify"
+        }
+      ],
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/finalhandler": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
+      "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.4.0",
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "on-finished": "^2.4.1",
+        "parseurl": "^1.3.3",
+        "statuses": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/forwarded": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+      "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/fresh": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+      "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
+      "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/hono": {
+      "version": "4.12.21",
+      "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.21.tgz",
+      "integrity": "sha512-uV63apnb0kyPtAUwoWgaGh9HyIFcv8lgmzPZSiTBQAFOFGIzka5EZ1dZocmGnn0XdX0+XTqJ6Tqv7selMuGLRQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=16.9.0"
+      }
+    },
+    "node_modules/http-errors": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+      "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+      "license": "MIT",
+      "dependencies": {
+        "depd": "~2.0.0",
+        "inherits": "~2.0.4",
+        "setprototypeof": "~1.2.0",
+        "statuses": "~2.0.2",
+        "toidentifier": "~1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/iconv-lite": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
+      "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
+      "license": "MIT",
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "license": "ISC"
+    },
+    "node_modules/ip-address": {
+      "version": "10.2.0",
+      "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
+      "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 12"
+      }
+    },
+    "node_modules/ipaddr.js": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+      "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/is-promise": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+      "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+      "license": "MIT"
+    },
+    "node_modules/isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+      "license": "ISC"
+    },
+    "node_modules/jose": {
+      "version": "6.2.3",
+      "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz",
+      "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/panva"
+      }
+    },
+    "node_modules/json-schema-traverse": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+      "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+      "license": "MIT"
+    },
+    "node_modules/json-schema-typed": {
+      "version": "8.0.2",
+      "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz",
+      "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==",
+      "license": "BSD-2-Clause"
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/media-typer": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
+      "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/merge-descriptors": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+      "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.54.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+      "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+      "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "^1.54.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/negotiator": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+      "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/object-inspect": {
+      "version": "1.13.4",
+      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+      "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/on-finished": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+      "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+      "license": "MIT",
+      "dependencies": {
+        "ee-first": "1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+      "license": "ISC",
+      "dependencies": {
+        "wrappy": "1"
+      }
+    },
+    "node_modules/parseurl": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+      "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-to-regexp": {
+      "version": "8.4.2",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
+      "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
+      "license": "MIT",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/pkce-challenge": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz",
+      "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=16.20.0"
+      }
+    },
+    "node_modules/proxy-addr": {
+      "version": "2.0.7",
+      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+      "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+      "license": "MIT",
+      "dependencies": {
+        "forwarded": "0.2.0",
+        "ipaddr.js": "1.9.1"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/qs": {
+      "version": "6.15.2",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
+      "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "side-channel": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=0.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/range-parser": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+      "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/raw-body": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
+      "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
+      "license": "MIT",
+      "dependencies": {
+        "bytes": "~3.1.2",
+        "http-errors": "~2.0.1",
+        "iconv-lite": "~0.7.0",
+        "unpipe": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/require-from-string": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+      "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/router": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+      "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.4.0",
+        "depd": "^2.0.0",
+        "is-promise": "^4.0.0",
+        "parseurl": "^1.3.3",
+        "path-to-regexp": "^8.0.0"
+      },
+      "engines": {
+        "node": ">= 18"
+      }
+    },
+    "node_modules/safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+      "license": "MIT"
+    },
+    "node_modules/send": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
+      "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.4.3",
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "etag": "^1.8.1",
+        "fresh": "^2.0.0",
+        "http-errors": "^2.0.1",
+        "mime-types": "^3.0.2",
+        "ms": "^2.1.3",
+        "on-finished": "^2.4.1",
+        "range-parser": "^1.2.1",
+        "statuses": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/serve-static": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
+      "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
+      "license": "MIT",
+      "dependencies": {
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "parseurl": "^1.3.3",
+        "send": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/setprototypeof": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+      "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+      "license": "ISC"
+    },
+    "node_modules/shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "license": "MIT",
+      "dependencies": {
+        "shebang-regex": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/side-channel": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+      "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.3",
+        "side-channel-list": "^1.0.0",
+        "side-channel-map": "^1.0.1",
+        "side-channel-weakmap": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-list": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+      "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.4"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-map": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+      "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-weakmap": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+      "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3",
+        "side-channel-map": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/statuses": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+      "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/toidentifier": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+      "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.6"
+      }
+    },
+    "node_modules/type-is": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz",
+      "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==",
+      "license": "MIT",
+      "dependencies": {
+        "content-type": "^2.0.0",
+        "media-typer": "^1.1.0",
+        "mime-types": "^3.0.0"
+      },
+      "engines": {
+        "node": ">= 18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/type-is/node_modules/content-type": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
+      "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/unpipe": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+      "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/vary": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+      "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "license": "ISC",
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "node-which": "bin/node-which"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+      "license": "ISC"
+    },
+    "node_modules/zod": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
+      "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/colinhacks"
+      }
+    },
+    "node_modules/zod-to-json-schema": {
+      "version": "3.25.2",
+      "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz",
+      "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==",
+      "license": "ISC",
+      "peerDependencies": {
+        "zod": "^3.25.28 || ^4"
+      }
+    }
+  }
+}

+ 42 - 0
claude-code-voc-intelligence/package.json

@@ -0,0 +1,42 @@
+{
+  "name": "@gangvy/claude-code-voc-intelligence",
+  "version": "0.1.2",
+  "description": "Claude Code native VOC intelligence skills and tools.",
+  "type": "commonjs",
+  "bin": {
+    "claude-voc": "bin/claude-voc.js"
+  },
+  "scripts": {
+    "install:check": "node install.js --check",
+    "install:smoke": "node install.js --smoke",
+    "smoke:package": "node scripts/smoke-package.js",
+    "xhs:sample": "node mcp/src/tools/xiaohongshu-trend-run.js --collection-mode sample --profile memory-templates/xiaohongshu-trend-profile.json --output ../outputs/claude-code-xhs-sample --result-prefix XHS_TREND_RESULT",
+    "xhs:preference-smoke": "node mcp/src/tools/xiaohongshu-preference-update.js --message \"保留奶油风和全屋定制翻车方向,不要纯风格美图,下一版更偏门店转化话术\" --memory ../outputs/claude-code-xhs-memory-smoke/xiaohongshu-trend-memory.json --result-prefix XHS_PREF_RESULT",
+    "xhs:sample-with-memory": "node mcp/src/tools/xiaohongshu-trend-run.js --collection-mode sample --profile memory-templates/xiaohongshu-trend-profile.json --memory ../outputs/claude-code-xhs-memory-smoke/xiaohongshu-trend-memory.json --output ../outputs/claude-code-xhs-memory-smoke/report --result-prefix XHS_TREND_RESULT",
+    "xhs:live": "node mcp/src/tools/xiaohongshu-trend-run.js --collection-mode live --profile memory-templates/xiaohongshu-trend-profile.json --output ../outputs/claude-code-xhs-live --keyword-limit 1 --notes-per-keyword 1 --max-comment-pages 1 --result-prefix XHS_TREND_RESULT",
+    "mcp:start": "node mcp/src/server.js",
+    "mcp:smoke": "node scripts/smoke-mcp.js"
+  },
+  "files": [
+    ".claude-plugin/",
+    ".mcp.json",
+    "bin/",
+    "docs/",
+    "install.js",
+    "mcp/",
+    "memory-templates/",
+    "package.json",
+    "README.md",
+    "scripts/",
+    "skill-package-manifest.json",
+    "skills/"
+  ],
+  "publishConfig": {
+    "access": "public"
+  },
+  "dependencies": {
+    "@modelcontextprotocol/sdk": "^1.29.0",
+    "zod": "^4.4.3"
+  }
+}
+

+ 81 - 0
claude-code-voc-intelligence/scripts/smoke-mcp.js

@@ -0,0 +1,81 @@
+#!/usr/bin/env node
+const path = require('path');
+const { Client } = require('@modelcontextprotocol/sdk/client/index.js');
+const { StdioClientTransport } = require('@modelcontextprotocol/sdk/client/stdio.js');
+
+async function main() {
+  const cwd = path.resolve(__dirname, '..');
+  const client = new Client({
+    name: 'voc-intelligence-smoke',
+    version: '0.1.2'
+  });
+
+  const transport = new StdioClientTransport({
+    command: process.execPath,
+    args: ['mcp/src/server.js'],
+    cwd,
+    stderr: 'pipe'
+  });
+
+  const stderr = transport.stderr;
+  if (stderr) {
+    stderr.on('data', chunk => {
+      process.stderr.write(chunk);
+    });
+  }
+
+  await client.connect(transport);
+  try {
+    const tools = await client.listTools();
+    const toolNames = tools.tools.map(tool => tool.name);
+    if (!toolNames.includes('voc_xiaohongshu_trend_run')) {
+      throw new Error(`Expected tool voc_xiaohongshu_trend_run, got: ${toolNames.join(', ')}`);
+    }
+    if (!toolNames.includes('voc_xiaohongshu_preference_update')) {
+      throw new Error(`Expected tool voc_xiaohongshu_preference_update, got: ${toolNames.join(', ')}`);
+    }
+
+    const preferenceResult = await client.callTool({
+      name: 'voc_xiaohongshu_preference_update',
+      arguments: {
+        message: '保留奶油风和全屋定制翻车方向,不要纯风格美图,下一版更偏门店转化话术',
+        memory: '../outputs/claude-code-xhs-mcp-smoke/xiaohongshu-trend-memory.json'
+      }
+    });
+
+    const preferenceStructured = preferenceResult.structuredContent || {};
+    if (preferenceStructured.status !== 'ok') {
+      throw new Error(`Expected ok preference result, got: ${JSON.stringify(preferenceStructured)}`);
+    }
+
+    const result = await client.callTool({
+      name: 'voc_xiaohongshu_trend_run',
+      arguments: {
+        collectionMode: 'sample',
+        profile: 'memory-templates/xiaohongshu-trend-profile.json',
+        memory: '../outputs/claude-code-xhs-mcp-smoke/xiaohongshu-trend-memory.json',
+        output: '../outputs/claude-code-xhs-mcp-smoke'
+      }
+    });
+
+    const structured = result.structuredContent || {};
+    if (structured.status !== 'ok') {
+      throw new Error(`Expected ok result, got: ${JSON.stringify(structured)}`);
+    }
+
+    console.log(JSON.stringify({
+      status: 'ok',
+      tools: toolNames,
+      assistantMessagePreview: String(structured.assistantMessage || '').slice(0, 120),
+      preferenceMemoryApplied: Boolean(structured.summary && structured.summary.preferenceMemoryApplied),
+      files: structured.files || []
+    }, null, 2));
+  } finally {
+    await client.close();
+  }
+}
+
+main().catch(error => {
+  console.error(error);
+  process.exit(1);
+});

+ 92 - 0
claude-code-voc-intelligence/scripts/smoke-package.js

@@ -0,0 +1,92 @@
+#!/usr/bin/env node
+const path = require('path');
+const { runXiaohongshuTrend } = require('../mcp/src/tools/xiaohongshu-trend-run');
+const { updateXiaohongshuPreference } = require('../mcp/src/tools/xiaohongshu-preference-update');
+const { Client } = require('@modelcontextprotocol/sdk/client/index.js');
+const { StdioClientTransport } = require('@modelcontextprotocol/sdk/client/stdio.js');
+
+const ROOT = path.resolve(__dirname, '..');
+const OUTPUT_ROOT = path.resolve(ROOT, 'outputs', 'claude-code-package-smoke');
+const MEMORY_PATH = path.join(OUTPUT_ROOT, 'xiaohongshu-trend-memory.json');
+
+async function smokeMcpTools() {
+  const client = new Client({
+    name: 'voc-intelligence-package-smoke',
+    version: '0.1.2'
+  });
+  const transport = new StdioClientTransport({
+    command: process.execPath,
+    args: ['mcp/src/server.js'],
+    cwd: ROOT,
+    stderr: 'pipe'
+  });
+  await client.connect(transport);
+  try {
+    const tools = await client.listTools();
+    return tools.tools.map(tool => tool.name);
+  } finally {
+    await client.close();
+  }
+}
+
+async function main() {
+  const sample = await runXiaohongshuTrend({
+    collectionMode: 'sample',
+    profile: path.join(ROOT, 'memory-templates', 'xiaohongshu-trend-profile.json'),
+    output: path.join(OUTPUT_ROOT, 'sample')
+  });
+  if (sample.status !== 'ok') {
+    throw new Error(`sample failed: ${sample.status}`);
+  }
+
+  const preference = await updateXiaohongshuPreference({
+    message: '保留奶油风和全屋定制翻车方向,不要纯风格美图,下一版更偏门店转化话术',
+    memory: MEMORY_PATH
+  });
+  if (preference.status !== 'ok') {
+    throw new Error(`preference failed: ${preference.status}`);
+  }
+
+  const withMemory = await runXiaohongshuTrend({
+    collectionMode: 'sample',
+    profile: path.join(ROOT, 'memory-templates', 'xiaohongshu-trend-profile.json'),
+    memory: MEMORY_PATH,
+    output: path.join(OUTPUT_ROOT, 'with-memory')
+  });
+  if (withMemory.status !== 'ok' || !withMemory.summary.preferenceMemoryApplied) {
+    throw new Error('sample-with-memory did not apply preference memory');
+  }
+
+  const toolNames = await smokeMcpTools();
+  const requiredTools = [
+    'voc_xiaohongshu_token_check',
+    'voc_xiaohongshu_trend_run',
+    'voc_xiaohongshu_preference_update'
+  ];
+  for (const tool of requiredTools) {
+    if (!toolNames.includes(tool)) {
+      throw new Error(`missing MCP tool: ${tool}`);
+    }
+  }
+
+  console.log(JSON.stringify({
+    status: 'ok',
+    checks: [
+      'sample report',
+      'preference memory',
+      'sample report with memory',
+      'MCP tools'
+    ],
+    mcpTools: toolNames,
+    reportPreview: String(withMemory.assistantMessage || '').split('\n').slice(0, 8).join('\n'),
+    files: withMemory.files
+  }, null, 2));
+}
+
+main().catch(error => {
+  console.error(JSON.stringify({
+    status: 'error',
+    message: error.message
+  }, null, 2));
+  process.exit(1);
+});

+ 22 - 0
claude-code-voc-intelligence/skill-package-manifest.json

@@ -0,0 +1,22 @@
+{
+  "name": "claude-code-voc-intelligence",
+  "version": "0.1.2",
+  "description": "Claude Code 原生 VOC 情报技能包。当前包含小红书趋势情报官,支持 sample 演示、MCP 工具、live token 检查、动态校准问题和偏好记忆。",
+  "plugin": "voc-intelligence",
+  "skills": [
+    "xiaohongshu-trend-intelligence"
+  ],
+  "mcpTools": [
+    "voc_xiaohongshu_token_check",
+    "voc_xiaohongshu_trend_run",
+    "voc_xiaohongshu_preference_update"
+  ],
+  "entrySkill": "xiaohongshu-trend-intelligence",
+  "sampleCommand": "npm run xhs:sample",
+  "mcpSmokeCommand": "npm run mcp:smoke",
+  "installCommand": "node install.js",
+  "npmInstallCommand": "npm install -g @gangvy/claude-code-voc-intelligence && claude-voc install",
+  "npxInstallCommand": "npx @gangvy/claude-code-voc-intelligence install",
+  "installHint": "支持两种交付:下载 zip 后运行 node install.js,或通过 npm/npx 安装到 ~/.claude/plugins/voc-intelligence 后用 claude --plugin-dir 加载。"
+}
+

+ 89 - 0
claude-code-voc-intelligence/skills/xiaohongshu-trend-intelligence/SKILL.md

@@ -0,0 +1,89 @@
+---
+name: xiaohongshu-trend-intelligence
+description: Build Xiaohongshu trend intelligence reports from industry direction, audience, keywords, notes, and comments. Use when the user asks for 小红书趋势情报、行业趋势、女性客户消费决策、设计元素洞察、家装全屋定制趋势、内容选题 or social VOC analysis.
+allowed-tools: Read Write Bash(node *)
+---
+
+# 小红书趋势情报官
+
+## 角色
+
+你是一个面向业务用户的“小红书趋势情报官”。你的任务不是展示技术参数,而是帮助用户看懂:
+
+- 近期小红书上什么趋势值得关注。
+- 用户真实评论里在担心什么、想要什么、反复问什么。
+- 哪些风格、设计元素、场景和消费顾虑正在形成机会。
+- 这些洞察如何转成待验证选题、产品表达、门店话术或销售跟进方向。
+
+## 使用场景
+
+当用户提到以下需求时,使用本技能:
+
+- “帮我看小红书趋势”
+- “家装全屋定制下季度流行什么风格”
+- “女性客户现在更关注哪些设计元素”
+- “帮我分析小红书上用户评论”
+- “给我一份行业趋势情报”
+- “找内容选题/门店话术/产品方向”
+
+## 工作流
+
+1. 优先用自然语言理解用户的行业、目标人群、平台和关注问题。
+2. 如果信息不足,只问 2-4 个业务问题,不要让用户填写技术参数。
+3. 调用小红书趋势工具生成第一轮信息。P0 阶段可以使用 sample 模式验证流程。
+4. 优先把工具返回的 `assistantMessage` 正文直接发给用户,但要把它视为“初步假设”,不要包装成最终结论。
+5. 文件路径只放在末尾作为归档信息。
+6. 报告后的问题必须来自样本证据、用户顾虑、设计元素、评论和用户过往偏好,不要套用固定校准问题。
+7. 用户后续给出“保留/不要/更偏某方向/这个不准/真实客户不是这样”等反馈时,调用偏好更新工具保存记忆。
+8. 下一轮再基于偏好和用户回答收敛趋势判断、选题方向或门店话术。
+
+P0 sample 工具命令:
+
+```bash
+node "${CLAUDE_SKILL_DIR}/../../mcp/src/tools/xiaohongshu-trend-run.js" --collection-mode sample --profile "${CLAUDE_SKILL_DIR}/../../memory-templates/xiaohongshu-trend-profile.json" --output "outputs/claude-code-xhs-sample" --result-prefix XHS_TREND_RESULT
+```
+
+偏好记忆工具命令:
+
+```bash
+node "${CLAUDE_SKILL_DIR}/../../mcp/src/tools/xiaohongshu-preference-update.js" --message "保留奶油风和全屋定制翻车方向,不要纯风格美图,下一版更偏门店转化话术" --result-prefix XHS_PREF_RESULT
+```
+
+## 默认报告结构
+
+- 第一轮初步判断(待校准)
+- 数据口径
+- 样本观察 Top 5
+- 本轮机会假设
+- 用户顾虑/决策阻力
+- 证据样本
+- 可继续验证的选题方向
+- 暂可参考话术(待确认)
+- 基于样本动态生成的下一轮校准问题
+
+## 用户体验规则
+
+- 不要暴露 `profile`、`output`、`collectionMode`、`keywordLimit` 等技术参数。
+- 不要只回复文件路径。
+- 不要把报告写成技术采集日志。
+- 不要说“一份报告已经完成”或“结论已确定”。第一轮只能叫“初步判断/机会假设/待验证结论”。
+- 不要预设固定校准问题;问题要贴着本轮样本,例如高赞评论、最高频顾虑、具体设计元素和用户已保存偏好。
+- 对新用户要解释“这份报告能帮他做什么”,但不要讲代码或接口。
+- 如果使用 sample 模式,需要明确说“这是演示样例,真实采集需要配置 VOC 服务”。
+- 如果用户只是做校准反馈,不要重新生成完整报告;先保存偏好,再告诉用户下一轮会如何基于反馈收敛判断。
+
+## 推荐启动话术
+
+用户可以直接说:
+
+```text
+帮我做一份家装全屋定制行业的小红书趋势情报。
+重点看女性客户下季度可能关注的设计元素、风格和消费顾虑。
+先在聊天里给我第一轮样本观察和待确认问题,不要只给文件路径。
+```
+
+## 参考文件
+
+- `references/user-workflow.md`:面向用户的完整流程。
+- `references/output-format.md`:报告输出格式。
+- `references/live-mode.md`:后续 live 模式接入说明。

+ 63 - 0
claude-code-voc-intelligence/skills/xiaohongshu-trend-intelligence/references/live-mode.md

@@ -0,0 +1,63 @@
+# live 模式接入说明
+
+P0 阶段先使用 sample 模式跑通 Claude Code 用户体验。
+
+P1 阶段接入真实小红书采集:
+
+1. 搜索笔记。
+2. 获取笔记详情。
+3. 获取评论。
+4. 归一化笔记和评论。
+5. 生成趋势报告。
+
+底层 MCP tools 建议:
+
+```text
+voc_xiaohongshu_search_notes
+voc_xiaohongshu_note_detail
+voc_xiaohongshu_note_comments
+voc_xiaohongshu_trend_report
+```
+
+live 模式必须包含:
+
+- VOC token 检查。
+- 余额或套餐状态提示。
+- 小规模默认采集,避免新用户第一次运行成本过高。
+- 错误结构化返回。
+
+## 当前 P1 骨架
+
+已提供 MCP tools:
+
+```text
+voc_xiaohongshu_token_check
+voc_xiaohongshu_trend_run
+voc_xiaohongshu_preference_update
+```
+
+`voc_xiaohongshu_trend_run` 的 live 默认建议:
+
+```text
+keywordLimit=1
+notesPerKeyword=1
+maxCommentPages=1
+```
+
+token 来源:
+
+```text
+XIAOHONGSHU_API_TOKEN
+TIKHUB_API_TOKEN
+FMODE_XIAOHONGSHU_TOKEN
+FMODE_SOCIAL_TOKEN
+~/.claude/voc-credentials.json 中的 xiaohongshuToken 或 tikhubToken
+```
+
+注意:不要把 OpenClaw 的 `VOC_TOKEN` 或 `OPENCLAW_VOC_TOKEN` 当成小红书采集 token。
+
+偏好记忆:
+
+- 用户说“保留/不要/更偏/下次多看”时,调用 `voc_xiaohongshu_preference_update`。
+- 生成报告时把同一个 memory 路径传给 `voc_xiaohongshu_trend_run`。
+- 工具返回 `summary.preferenceMemoryApplied=true` 时,说明报告已经应用偏好。

+ 104 - 0
claude-code-voc-intelligence/skills/xiaohongshu-trend-intelligence/references/output-format.md

@@ -0,0 +1,104 @@
+# 小红书趋势情报输出格式
+
+## 聊天正文优先
+
+工具如果返回 `assistantMessage`,优先直接展示给用户。
+
+不要只说:
+
+```text
+报告已保存到 xxx.md
+```
+
+应该直接展示:
+
+```text
+第一轮初步判断(待校准)
+...
+
+下一轮请先校准这些问题
+...
+```
+
+新的推荐结构是多轮验证:
+
+```text
+小红书趋势情报
+
+第一轮初步判断(待校准)
+...
+
+数据口径
+...
+
+样本观察 Top 5
+...
+
+本轮机会假设
+...
+
+证据样本
+...
+
+可继续验证的选题方向
+...
+
+暂可参考话术(待确认)
+...
+
+下一轮请先校准这些问题
+1. 结合本轮样本动态生成的问题
+2. 结合高赞评论动态生成的问题
+3. 结合具体设计元素动态生成的问题
+```
+
+不要使用固定问题模板。问题必须来自当前样本、评论、设计元素、用户顾虑或已保存偏好。
+
+旧的一次性报告结构不再推荐:
+
+```text
+第一轮初步判断(待校准)
+...
+
+趋势信号 Top 5
+...
+
+下一步建议
+...
+```
+
+## 推荐结构
+
+```text
+小红书趋势情报
+
+第一轮初步判断(待校准)
+...
+
+已读取偏好
+- 优先保留:...
+- 后续降权:...
+- 本轮侧重:...
+
+样本观察 Top 5
+1. ...
+2. ...
+
+本轮机会假设
+- ...
+
+用户顾虑/决策阻力
+- ...
+
+可继续验证的选题方向
+1. ...
+2. ...
+
+暂可参考话术(待确认)
+- ...
+
+下一轮请先校准这些问题
+1. 根据最高频顾虑生成的问题
+2. 根据高赞评论生成的问题
+3. 根据具体设计元素生成的问题
+```

+ 65 - 0
claude-code-voc-intelligence/skills/xiaohongshu-trend-intelligence/references/user-workflow.md

@@ -0,0 +1,65 @@
+# 小红书趋势情报官用户流程
+
+## 新用户第一次使用
+
+推荐用户直接说:
+
+```text
+帮我做一份家装全屋定制行业的小红书趋势情报。
+重点看女性客户下季度可能关注的设计元素、风格和消费顾虑。
+先在聊天里给我第一轮样本观察和待确认问题,不要只给文件路径。
+```
+
+如果用户没有给行业,需要追问:
+
+1. 你要看的行业或品类是什么?
+2. 主要面向哪类客户?
+3. 你更想看趋势、用户顾虑、内容选题,还是销售话术?
+
+不要问:
+
+- token 配在哪里
+- 输出路径是什么
+- keywordLimit 是多少
+- 是否传 profile
+
+## 第一轮之后的校准
+
+第一轮生成后,不要把报告当最终结论。应该根据样本里的高频词、高赞评论、具体设计元素和用户过往偏好,生成动态校准问题。
+
+不要固定问:
+
+```text
+哪些方向要保留?
+哪些方向不要?
+下一版更偏什么?
+```
+
+应该贴着样本问,例如:
+
+```text
+样本里“奶油风”和“不好打理”同时出现,你实际接触的客户是先被风格吸引,还是先担心不好打理?
+这条高赞评论“奶油风好看是好看,就怕两年后显旧”像不像你门店里真实听到的问题?
+围绕“开放格”,你们现在有没有可证明的案例、材料、价格或售后承诺?
+```
+
+用户回复校准后,先保存偏好,不要立刻丢一大段技术输出。
+
+示例:
+
+```text
+保留奶油风和全屋定制翻车方向,不要纯风格美图,下一版更偏门店转化话术。
+```
+
+应该回复:
+
+```text
+已保存偏好:后续优先看奶油风、全屋定制翻车和门店转化话术,纯风格美图会降权。下一轮会把这次反馈作为校准输入,重新收敛趋势判断和待验证话术。
+```
+
+## 后续可接的动作
+
+- 选一个趋势,生成小红书选题。
+- 选一个用户顾虑,生成门店销售话术。
+- 选一个设计元素,生成内容脚本。
+- 保存偏好,下次报告自动降权不需要的方向。

+ 24 - 0
dist/claude-code-voc-intelligence-suite-manifest.json

@@ -0,0 +1,24 @@
+{
+  "name": "claude-code-voc-intelligence",
+  "version": "0.1.2",
+  "generatedAt": "2026-05-23T02:10:49.332Z",
+  "packageZip": "dist/claude-code-voc-intelligence.zip",
+  "packageZipBytes": 52124,
+  "plugin": "voc-intelligence",
+  "skills": [
+    "xiaohongshu-trend-intelligence"
+  ],
+  "mcpTools": [
+    "voc_xiaohongshu_token_check",
+    "voc_xiaohongshu_trend_run",
+    "voc_xiaohongshu_preference_update"
+  ],
+  "installHint": "支持两种交付:下载 zip 后运行 node install.js,或通过 npm/npx 安装到 ~/.claude/plugins/voc-intelligence 后用 claude --plugin-dir 加载。",
+  "installCommand": "node install.js",
+  "npmInstallCommand": "npm install -g @gangvy/claude-code-voc-intelligence && claude-voc install",
+  "npxInstallCommand": "npx @gangvy/claude-code-voc-intelligence install",
+  "buildCommand": "node scripts/deploy/claude-code-voc-intelligence-suite.js --validate --build --test-package",
+  "uploadCommand": "node scripts/deploy/claude-code-voc-intelligence-suite.js --validate --build --test-package --upload",
+  "uploaded": false,
+  "copiedFiles": 30
+}

TEMPAT SAMPAH
dist/claude-code-voc-intelligence.zip


+ 43 - 0
dist/douyin-speaking-daily-suite-manifest.json

@@ -0,0 +1,43 @@
+{
+  "name": "douyin-speaking-daily",
+  "version": "0.2.18",
+  "generatedAt": "2026-05-10T17:01:55.609Z",
+  "packageZip": "dist/douyin-speaking-daily.zip",
+  "packageZipBytes": 237716,
+  "skills": [
+    "voc-token-preflight",
+    "douyin-general-search",
+    "douyin-hashtag-search",
+    "douyin-video-detail",
+    "douyin-video-comments",
+    "douyin-comment-replies",
+    "douyin-user-search",
+    "douyin-user-profile",
+    "douyin-user-posts",
+    "douyin-mini-voc",
+    "douyin-speaking-profile-builder",
+    "douyin-speaking-profile-memory",
+    "douyin-speaking-keyword-monitor",
+    "douyin-speaking-account-monitor",
+    "douyin-speaking-daily-runner",
+    "douyin-speaking-daily-report",
+    "douyin-viral-script-analyzer",
+    "douyin-video-transcript"
+  ],
+  "tools": [
+    "openclaw-tool-runner.js",
+    "set-voc-token.js",
+    "voc-token-preflight.js",
+    "douyin-speaking-profile-memory.js",
+    "douyin-speaking-daily-runner.js",
+    "douyin-speaking-daily-report.js",
+    "douyin-speaking-daily-p1-smoke.js",
+    "douyin-viral-script-analyzer.js",
+    "douyin-video-transcriber.js"
+  ],
+  "workspaceBundle": "douyin-speaking-daily",
+  "installCommand": "node install.js",
+  "localDeployCommand": "node scripts/deploy/douyin-speaking-daily-suite.js --deploy",
+  "uploadCommand": "node scripts/deploy/douyin-speaking-daily-suite.js --build --upload",
+  "upload": false
+}

TEMPAT SAMPAH
dist/douyin-speaking-daily.zip


+ 3 - 2
dist/download-urls.json

@@ -1,3 +1,4 @@
 {
-  "xiaohongshu-platform": "https://repos.fmode.cn/x/openclaw-skills/packages/xiaohongshu-platform.zip"
-}
+  "douyin-speaking-daily": "https://repos.fmode.cn/x/openclaw-skills/packages/douyin-speaking-daily.zip",
+  "industry-trend-intelligence": "https://repos.fmode.cn/x/openclaw-skills/packages/industry-trend-intelligence.zip"
+}

+ 25 - 0
dist/industry-trend-intelligence-suite-manifest.json

@@ -0,0 +1,25 @@
+{
+  "name": "industry-trend-intelligence",
+  "version": "0.2.0",
+  "generatedAt": "2026-05-21T15:04:42.529Z",
+  "packageZip": "dist/industry-trend-intelligence.zip",
+  "packageZipBytes": 54821,
+  "skills": [
+    "industry-trend-runner",
+    "industry-trend-profile-builder",
+    "industry-trend-report",
+    "industry-trend-memory",
+    "xiaohongshu-trend-collector"
+  ],
+  "dependencySkills": [
+    "xiaohongshu-search-notes",
+    "xiaohongshu-note-detail",
+    "xiaohongshu-note-comments",
+    "xiaohongshu-user-info"
+  ],
+  "workspaceBundle": "industry-trend-intelligence",
+  "installCommand": "node install.js",
+  "uploadCommand": "node scripts/deploy/industry-trend-intelligence-suite.js --build --upload",
+  "uploaded": true,
+  "downloadUrl": "https://repos.fmode.cn/x/openclaw-skills/packages/industry-trend-intelligence.zip"
+}

TEMPAT SAMPAH
dist/industry-trend-intelligence.zip


+ 49 - 0
dist/install.js

@@ -26,6 +26,7 @@ const os = require('os');
 const VERSION = '1.0.0';
 const SKILLS_SUBDIR = 'skills'; // zip 包内的技能目录名
 const TOOLS_SUBDIR = 'tools';
+const WORKSPACE_BUNDLE_DIRS = ['douyin-speaking-daily'];
 
 // ============================================
 // 参数解析
@@ -93,6 +94,10 @@ function getOpenClawWorkspaceToolsDir() {
     return path.join(getOpenClawDir(), 'workspace', 'scripts', 'tools');
 }
 
+function getOpenClawWorkspaceDir() {
+    return path.join(getOpenClawDir(), 'workspace');
+}
+
 function ensureDir(dir) {
     if (!fs.existsSync(dir)) {
         fs.mkdirSync(dir, { recursive: true });
@@ -283,6 +288,49 @@ function installTools(tools, opts) {
     return { installed, skipped, errors };
 }
 
+function installWorkspaceBundles(scriptDir, opts) {
+    const workspaceRoot = getOpenClawWorkspaceDir();
+    const prefix = opts.dryRun ? '[DRY-RUN] ' : '';
+    let installed = 0;
+    let skipped = 0;
+    let errors = 0;
+
+    for (const bundleName of WORKSPACE_BUNDLE_DIRS) {
+        const sourceDir = path.join(scriptDir, bundleName);
+        if (!fs.existsSync(sourceDir)) continue;
+        const files = discoverFilesRecursive(sourceDir);
+        console.log('\n' + prefix + 'Installing workspace bundle: ' + bundleName);
+        console.log('  ' + path.join(workspaceRoot, bundleName) + '\n');
+
+        for (const file of files) {
+            const destFile = path.join(workspaceRoot, bundleName, file.name);
+            try {
+                if (fs.existsSync(destFile)) {
+                    const existingContent = fs.readFileSync(destFile);
+                    const newContent = fs.readFileSync(file.path);
+                    if (existingContent.equals(newContent)) {
+                        skipped++;
+                        continue;
+                    }
+                }
+                if (!opts.dryRun) {
+                    copyFileSync(file.path, destFile);
+                }
+                installed++;
+            } catch (err) {
+                process.stdout.write('  [ERROR] ' + path.join(bundleName, file.name) + ': ' + err.message + '\n');
+                errors++;
+            }
+        }
+        process.stdout.write('  [OK] ' + bundleName + ' (' + files.length + ' files)\n');
+    }
+
+    if (installed || skipped || errors) {
+        console.log('\n' + prefix + 'Workspace bundles done! Installed: ' + installed + ', unchanged: ' + skipped + ', errors: ' + errors + '\n');
+    }
+    return { installed, skipped, errors };
+}
+
 function installSkills(skills, targetDir, opts) {
     const prefix = opts.dryRun ? '[DRY-RUN] ' : '';
 
@@ -416,6 +464,7 @@ function main() {
     listSkills(skills);
     installSkills(skills, targetDir, opts);
     installTools(tools, opts);
+    installWorkspaceBundles(scriptDir, opts);
 }
 
 main();

+ 17 - 0
dist/npm/claude-code-voc-npm-package-manifest.json

@@ -0,0 +1,17 @@
+{
+  "name": "@gangvy/claude-code-voc-intelligence",
+  "version": "0.1.2",
+  "generatedAt": "2026-05-23T02:12:43.820Z",
+  "tarball": "dist/npm/gangvy-claude-code-voc-intelligence-0.1.2.tgz",
+  "tarballBytes": 26970,
+  "unpackedBytes": 89133,
+  "entryCount": 28,
+  "installCommands": [
+    "npm install -g @gangvy/claude-code-voc-intelligence",
+    "claude-voc install",
+    "npx @gangvy/claude-code-voc-intelligence install"
+  ],
+  "tested": true,
+  "npxTested": true,
+  "published": true
+}

+ 3 - 0
docs/1.md

@@ -0,0 +1,3 @@
+# kano原则
+## session 3
+- 用户原则、用户洞察 job to b done

+ 185 - 0
docs/5.11/douyin-speaking-daily-overview.md

@@ -0,0 +1,185 @@
+# 抖音口播每日稿件日报
+
+> 本页是课程总入口,用来帮助新用户先理解这个技能包能做什么,再按 P0-P5 的顺序完成第一次完整操作。
+
+## 一、这个技能包是做什么的
+
+抖音口播每日稿件日报,是一个帮助内容创作者、企业账号、品牌团队每天寻找口播选题,并生成可拍摄稿件的 OpenClaw 技能包。
+
+它不是简单给你一份热点列表,而是帮你完成一条从“找选题”到“出稿件”的内容生产流程:
+
+1. 根据你的账号定位,寻找适合参考的抖音爆款口播内容。
+2. 汇总今日值得关注的选题方向。
+3. 自动补充一条高价值视频的逐字稿,方便拆解爆款结构。
+4. 根据你的账号方向,把爆款选题改写成适合你自己的口播初稿。
+5. 通过多轮对话继续改稿、定稿,并让系统记住你的偏好。
+
+最终目标是让用户每天不用从零开始想内容,而是有一套可以反复使用的“选题发现 - 爆款拆解 - 稿件共创 - 偏好沉淀”流程。
+
+## 二、适合哪些用户
+
+这个技能包适合:
+
+1. 想做抖音口播,但不知道每天拍什么的人。
+2. 已经有账号定位,希望持续找选题、做内容的人。
+3. 企业老板、市场团队、内容团队、IP 操盘手。
+4. 想参考爆款,但不想照抄爆款文案的人。
+5. 希望让 AI 逐步理解自己账号风格,并长期辅助内容生产的人。
+
+如果你经常遇到这些问题,就适合使用:
+
+1. 今天不知道拍什么。
+2. 看到了爆款,但不知道怎么改成自己的内容。
+3. 口播稿太泛,不像自己的账号。
+4. 每次都要重新解释账号定位,很浪费时间。
+5. 想让系统慢慢记住自己的选题偏好和表达风格。
+
+## 三、它能帮你完成什么
+
+使用这个技能包后,你可以完成五件事:
+
+1. 建立账号档案  
+   让 OpenClaw 知道你的行业、目标用户、账号定位和转化目标。
+
+2. 生成每日选题日报  
+   每天自动整理适合你账号参考的抖音口播爆款选题。
+
+3. 拆解爆款视频  
+   对高价值视频补充逐字稿,帮助后续分析开头、结构、案例和结尾动作。
+
+4. 生成口播初稿  
+   从选题出发,把爆款结构改写成适合你账号的一版口播稿。
+
+5. 多轮共创并沉淀偏好  
+   你可以继续要求它改开头、换视角、加案例、压缩时长,最终定稿后让系统记住你的偏好。
+
+## 四、第一次学习顺序
+
+建议新用户按下面顺序学习:
+
+### P0:下载安装说明
+
+目标:把技能包安装到 OpenClaw 里,并确认可以被识别。
+
+这一节只解决“能不能用”的问题。
+
+### P1:数据授权与账号建档
+
+目标:完成数据授权,并建立第一版账号档案。
+
+这一节让 OpenClaw 知道:你是谁、你做什么账号、你想吸引什么用户。
+
+### P2:生成第一份抖音口播日报
+
+目标:生成第一份今日选题日报,并完成一次方向校准。
+
+这一节让用户看到:今天有哪些爆款方向值得参考,以及哪些方向适合自己的账号。
+
+### P3:选择选题并生成第一版口播初稿
+
+目标:从日报里选择一条选题,进入脚本共创,生成第一版口播初稿。
+
+这一节让用户体验:一个爆款方向如何变成自己的账号可以讲的内容。
+
+### P4:第一轮改稿
+
+目标:用普通话告诉 OpenClaw 怎么改稿。
+
+例如:开头更狠一点,换成老板视角,少讲概念,多给一个具体业务场景。
+
+这一节让用户学会:不用自己逐字改,只要说清楚修改方向。
+
+### P5:定稿与保存偏好
+
+目标:确认最终稿,并让 OpenClaw 记住本次偏好。
+
+这一节完成后,第一次完整闭环就跑通了。
+
+## 五、完整流程是什么
+
+第一次完整流程可以理解为:
+
+```text
+安装技能包
+  -> 配置数据授权
+  -> 建立账号档案
+  -> 生成每日选题日报
+  -> 回复校准问题
+  -> 选择一个选题
+  -> 生成第一版口播初稿
+  -> 多轮改稿
+  -> 定稿
+  -> 保存偏好
+```
+
+后续每天使用时,不需要每次都从安装和建档开始。
+
+日常使用一般是:
+
+```text
+生成今日日报
+  -> 选择一个选题
+  -> 生成口播稿
+  -> 修改并定稿
+  -> 保存新的偏好
+```
+
+## 六、用户最终会得到什么
+
+完成一轮流程后,用户会得到:
+
+1. 一份今日抖音口播选题日报。
+2. 一条已拆解的爆款视频逐字稿或结构参考。
+3. 一版适合自己账号的口播初稿。
+4. 一版经过修改的最终口播稿。
+5. 一组被系统记住的内容偏好。
+
+这些结果会帮助后续内容生产越来越快、越来越贴近账号方向。
+
+## 七、使用时要记住的原则
+
+1. 日报不是最终稿,而是选题池。
+2. 爆款不是用来照抄的,而是用来借结构、借冲突、借表达方式。
+3. 第一版稿件不需要完美,它只是共创起点。
+4. 改稿时不用说专业术语,像和同事沟通一样说就可以。
+5. 定稿后要保存偏好,这样下次结果会更准。
+
+## 八、推荐的新用户启动话术
+
+完成安装和建档后,可以这样开始:
+
+```text
+启动抖音口播每日稿件日报。
+读取刚才建立的账号档案。
+今天帮我采集抖音爆款口播选题,自动补一条最值得拆的视频逐字稿。
+最后直接在聊天里给我日报。
+```
+
+如果已经生成日报,并想进入稿件创作,可以说:
+
+```text
+选题2,进入脚本共创。请基于我的账号定位,生成一版可以继续修改的口播初稿。
+```
+
+如果已经拿到初稿,并想继续优化,可以说:
+
+```text
+开头更狠一点,换成老板视角,少讲概念,多给一个具体业务场景。
+```
+
+如果已经满意,可以说:
+
+```text
+定稿。
+```
+
+## 九、课程目录
+
+1. P0 下载安装说明
+2. P1 数据授权与账号建档
+3. P2 生成第一份抖音口播日报
+4. P3 选择选题并生成第一版口播初稿
+5. P4 第一轮改稿
+6. P5 定稿与保存偏好
+
+建议按顺序完成一次。完成以后,用户就可以把这套流程作为日常内容生产方式反复使用。

+ 158 - 0
docs/5.11/douyin-speaking-daily-overview.txt

@@ -0,0 +1,158 @@
+抖音口播每日稿件日报
+
+本页是课程总入口,用来帮助新用户先理解这个技能包能做什么,再按 P0-P5 的顺序完成第一次完整操作。
+
+一、这个技能包是做什么的
+
+抖音口播每日稿件日报,是一个帮助内容创作者、企业账号、品牌团队每天寻找口播选题,并生成可拍摄稿件的 OpenClaw 技能包。
+
+它不是简单给你一份热点列表,而是帮你完成一条从“找选题”到“出稿件”的内容生产流程:
+
+1. 根据你的账号定位,寻找适合参考的抖音爆款口播内容。
+2. 汇总今日值得关注的选题方向。
+3. 自动补充一条高价值视频的逐字稿,方便拆解爆款结构。
+4. 根据你的账号方向,把爆款选题改写成适合你自己的口播初稿。
+5. 通过多轮对话继续改稿、定稿,并让系统记住你的偏好。
+
+最终目标是让用户每天不用从零开始想内容,而是有一套可以反复使用的“选题发现 - 爆款拆解 - 稿件共创 - 偏好沉淀”流程。
+
+二、适合哪些用户
+
+这个技能包适合:
+
+1. 想做抖音口播,但不知道每天拍什么的人。
+2. 已经有账号定位,希望持续找选题、做内容的人。
+3. 企业老板、市场团队、内容团队、IP 操盘手。
+4. 想参考爆款,但不想照抄爆款文案的人。
+5. 希望让 AI 逐步理解自己账号风格,并长期辅助内容生产的人。
+
+如果你经常遇到这些问题,就适合使用:
+
+1. 今天不知道拍什么。
+2. 看到了爆款,但不知道怎么改成自己的内容。
+3. 口播稿太泛,不像自己的账号。
+4. 每次都要重新解释账号定位,很浪费时间。
+5. 想让系统慢慢记住自己的选题偏好和表达风格。
+
+三、它能帮你完成什么
+
+使用这个技能包后,你可以完成五件事:
+
+1. 建立账号档案
+让 OpenClaw 知道你的行业、目标用户、账号定位和转化目标。
+
+2. 生成每日选题日报
+每天自动整理适合你账号参考的抖音口播爆款选题。
+
+3. 拆解爆款视频
+对高价值视频补充逐字稿,帮助后续分析开头、结构、案例和结尾动作。
+
+4. 生成口播初稿
+从选题出发,把爆款结构改写成适合你账号的一版口播稿。
+
+5. 多轮共创并沉淀偏好
+你可以继续要求它改开头、换视角、加案例、压缩时长,最终定稿后让系统记住你的偏好。
+
+四、第一次学习顺序
+
+P0:下载安装说明
+目标:把技能包安装到 OpenClaw 里,并确认可以被识别。
+这一节只解决“能不能用”的问题。
+
+P1:数据授权与账号建档
+目标:完成数据授权,并建立第一版账号档案。
+这一节让 OpenClaw 知道:你是谁、你做什么账号、你想吸引什么用户。
+
+P2:生成第一份抖音口播日报
+目标:生成第一份今日选题日报,并完成一次方向校准。
+这一节让用户看到:今天有哪些爆款方向值得参考,以及哪些方向适合自己的账号。
+
+P3:选择选题并生成第一版口播初稿
+目标:从日报里选择一条选题,进入脚本共创,生成第一版口播初稿。
+这一节让用户体验:一个爆款方向如何变成自己的账号可以讲的内容。
+
+P4:第一轮改稿
+目标:用普通话告诉 OpenClaw 怎么改稿。
+例如:开头更狠一点,换成老板视角,少讲概念,多给一个具体业务场景。
+这一节让用户学会:不用自己逐字改,只要说清楚修改方向。
+
+P5:定稿与保存偏好
+目标:确认最终稿,并让 OpenClaw 记住本次偏好。
+这一节完成后,第一次完整闭环就跑通了。
+
+五、完整流程是什么
+
+第一次完整流程可以理解为:
+
+安装技能包
+-> 配置数据授权
+-> 建立账号档案
+-> 生成每日选题日报
+-> 回复校准问题
+-> 选择一个选题
+-> 生成第一版口播初稿
+-> 多轮改稿
+-> 定稿
+-> 保存偏好
+
+后续每天使用时,不需要每次都从安装和建档开始。
+
+日常使用一般是:
+
+生成今日日报
+-> 选择一个选题
+-> 生成口播稿
+-> 修改并定稿
+-> 保存新的偏好
+
+六、用户最终会得到什么
+
+完成一轮流程后,用户会得到:
+
+1. 一份今日抖音口播选题日报。
+2. 一条已拆解的爆款视频逐字稿或结构参考。
+3. 一版适合自己账号的口播初稿。
+4. 一版经过修改的最终口播稿。
+5. 一组被系统记住的内容偏好。
+
+这些结果会帮助后续内容生产越来越快、越来越贴近账号方向。
+
+七、使用时要记住的原则
+
+1. 日报不是最终稿,而是选题池。
+2. 爆款不是用来照抄的,而是用来借结构、借冲突、借表达方式。
+3. 第一版稿件不需要完美,它只是共创起点。
+4. 改稿时不用说专业术语,像和同事沟通一样说就可以。
+5. 定稿后要保存偏好,这样下次结果会更准。
+
+八、推荐的新用户启动话术
+
+完成安装和建档后,可以这样开始:
+
+启动抖音口播每日稿件日报。
+读取刚才建立的账号档案。
+今天帮我采集抖音爆款口播选题,自动补一条最值得拆的视频逐字稿。
+最后直接在聊天里给我日报。
+
+如果已经生成日报,并想进入稿件创作,可以说:
+
+选题2,进入脚本共创。请基于我的账号定位,生成一版可以继续修改的口播初稿。
+
+如果已经拿到初稿,并想继续优化,可以说:
+
+开头更狠一点,换成老板视角,少讲概念,多给一个具体业务场景。
+
+如果已经满意,可以说:
+
+定稿。
+
+九、课程目录
+
+1. P0 下载安装说明
+2. P1 数据授权与账号建档
+3. P2 生成第一份抖音口播日报
+4. P3 选择选题并生成第一版口播初稿
+5. P4 第一轮改稿
+6. P5 定稿与保存偏好
+
+建议按顺序完成一次。完成以后,用户就可以把这套流程作为日常内容生产方式反复使用。

+ 115 - 0
docs/5.11/douyin-speaking-daily-p0-install-notes.md

@@ -0,0 +1,115 @@
+# 抖音口播每日稿件日报技能包 P0 下载安装说明
+
+## 一、本节目标
+
+把 `douyin-speaking-daily` 技能包下载安装到本机 OpenClaw,并确认能正常识别和运行。
+
+## 二、前置准备
+
+1. 已安装 OpenClaw。
+2. 已安装 Node.js 18+。
+
+检查命令:
+
+```bash
+node -v
+```
+
+3. 后续真实跑日报需要 VOC Token;本节只做下载安装和本地验证,不展示真实 Token。
+
+## 三、下载地址
+
+[下载 douyin-speaking-daily.zip](https://repos.fmode.cn/x/openclaw-skills/packages/douyin-speaking-daily.zip)
+
+## 四、安装步骤
+
+1. 用浏览器打开上面的下载地址,下载 `douyin-speaking-daily.zip`。
+2. 解压 zip 到本地目录。
+3. 在解压后的目录中打开 PowerShell。
+4. 执行安装命令:
+
+```bash
+node install.js
+```
+
+## 五、安装后位置
+
+技能会安装到:
+
+```text
+%USERPROFILE%\.openclaw\skills
+```
+
+工具会安装到:
+
+```text
+%USERPROFILE%\.openclaw\tools
+%USERPROFILE%\.openclaw\workspace\scripts\tools
+```
+
+说明文档会安装到:
+
+```text
+%USERPROFILE%\.openclaw\workspace\douyin-speaking-daily
+```
+
+## 六、核心技能
+
+主要入口:
+
+```text
+douyin-speaking-daily-runner
+```
+
+常用相关技能:
+
+```text
+douyin-speaking-daily-report
+douyin-speaking-profile-memory
+douyin-video-transcript
+voc-token-preflight
+```
+
+## 七、安装验证
+
+### 方式一:检查技能目录
+
+检查目录中是否出现技能:
+
+```text
+%USERPROFILE%\.openclaw\skills\douyin-speaking-daily-runner
+```
+
+### 方式二:运行本地 smoke 测试
+
+```bash
+node %USERPROFILE%\.openclaw\workspace\scripts\tools\douyin-speaking-daily-p1-smoke.js
+```
+
+看到 `status: ok`,说明本地工具链可用。
+
+### 方式三:重启 OpenClaw 后搜索技能
+
+重启 OpenClaw,在 OpenClaw 中搜索或调用:
+
+```text
+douyin-speaking-daily-runner
+```
+
+## 八、常见问题
+
+### 1. `node` 命令不存在
+
+先安装 Node.js 18+,然后重启 PowerShell。
+
+### 2. OpenClaw 看不到技能
+
+重启 OpenClaw,并检查 `.openclaw\skills` 目录。
+
+### 3. 真实日报跑不起来
+
+通常是 VOC Token、关键词、账号画像还没配置;下一节再讲。
+
+## 九、本节结论
+
+完成下载、解压、`node install.js`、smoke 验证后,`douyin-speaking-daily` 技能包就已经部署到本机 OpenClaw,可以进入下一节真实日报配置和运行。

+ 155 - 0
docs/5.11/douyin-speaking-daily-p1-image-script.md

@@ -0,0 +1,155 @@
+# p1-数据授权与账号建档说明
+
+> 本页用于配合教学视频使用。视频放在页面上方,下面是文字说明,方便客户跟着操作。
+
+【此处插入视频:p1-数据授权与账号建档】
+
+抖音口播每日稿件日报技能包 P1 数据授权与账号建档说明。
+
+## 教学场景
+
+本次教学模拟的是新用户第一次操作。
+
+用户已经完成 P0 下载和安装,但还没有配置数据授权,也没有建立账号档案。
+
+## 一、本节目标
+
+完成正式使用前的两项准备:
+
+1. 配置数据授权码,让 OpenClaw 可以读取抖音相关内容数据。
+2. 建立账号档案,让 OpenClaw 知道你的账号适合讲什么内容。
+
+完成后,就可以进入下一节,生成第一份抖音口播日报。
+
+## 二、前置准备
+
+开始前请确认:
+
+1. 已完成 P0 下载和安装。
+2. 已能正常打开 OpenClaw。
+3. 已拿到服务方提供的数据授权码。
+4. 已大致想好自己的账号方向。
+
+注意:数据授权码属于敏感信息,录屏、截图、发群时请不要公开展示。
+
+## 三、数据授权码怎么配置
+
+按照视频里的操作,把服务方提供的数据授权码保存到本机 OpenClaw。
+
+保存完成后,OpenClaw 后续生成日报时会自动使用,不需要每次重复输入。
+
+如果不确定是否配置成功,可以在 OpenClaw 里问:
+
+```text
+检查一下抖音口播日报的数据授权是否已经可以使用。
+```
+
+## 四、账号档案是什么
+
+账号档案可以理解成你的“账号说明书”。
+
+它会告诉 OpenClaw:
+
+1. 你是什么行业。
+2. 你的账号定位是什么。
+3. 你主要讲给谁听。
+4. 你希望用户看完后做什么。
+5. 你关注哪些内容方向。
+6. 哪些内容不想再出现。
+
+账号档案越清楚,后面生成的选题和口播稿越贴近你的账号。
+
+## 五、让 OpenClaw 帮你建档
+
+如果还没有账号档案,可以直接在 OpenClaw 里输入:
+
+```text
+帮我建立一个抖音口播账号档案。
+你一次问我 1 到 3 个问题。
+问完后帮我保存起来。
+```
+
+OpenClaw 会根据你的回答,逐步补全账号档案。
+
+建议提前想好这些信息:
+
+1. 行业或产品是什么。
+2. 账号希望给人的感觉是什么。
+3. 目标客户是谁。
+4. 想长期关注哪些关键词。
+5. 哪些方向不要做。
+
+## 六、完成标准
+
+看到下面结果,就说明本节完成:
+
+1. 数据授权码已经保存。
+2. OpenClaw 能识别你的账号档案。
+3. 账号档案里已经包含行业、定位、人群、关键词和内容禁区。
+4. OpenClaw 提示可以开始生成日报。
+
+## 七、常见问题
+
+### 1. 没有数据授权码怎么办?
+
+请先联系服务方获取。没有授权码,系统无法采集抖音内容和评论。
+
+### 2. 不知道账号定位怎么填怎么办?
+
+可以先给一个大概方向,后续日报和稿件共创过程中还可以不断调整。
+
+### 3. 账号档案填错了怎么办?
+
+可以直接告诉 OpenClaw:
+
+```text
+我要修改我的抖音口播账号档案。
+```
+
+## 八、下一节
+
+完成数据授权和账号建档后,进入 P2:生成第一份抖音口播日报。
+
+## 九、视频口播稿参考
+
+本次教学,我们模拟一个新用户第一次使用这套抖音口播日报工具。
+
+这一节,我们先做正式使用前的准备。
+
+抖音口播每日稿件日报不是普通的文案生成工具,它需要先知道两件事。
+
+第一,你能不能读取抖音相关数据。
+
+第二,你的账号到底适合讲什么内容。
+
+所以在真正生成日报之前,我们先完成数据授权和账号建档。
+
+数据授权码可以理解成访问数据的钥匙。
+
+有了它,OpenClaw 才能帮你采集抖音视频、评论和相关内容信号。
+
+这里要注意,数据授权码属于敏感信息。
+
+录屏、截图、发群的时候,不要公开展示。
+
+第二个准备,是账号档案。
+
+账号档案可以理解成你的账号说明书。
+
+它会告诉 OpenClaw:你是什么行业,你的账号定位是什么,主要讲给谁听,希望用户看完以后做什么,以及哪些方向不要出现。
+
+如果你还没有账号档案,可以直接让 OpenClaw 帮你建。
+
+你只要说:帮我建立一个抖音口播账号档案,你一次问我一到三个问题,问完后帮我保存起来。
+
+OpenClaw 会一步一步问你。
+
+你按实际情况回答就可以。
+
+建档时不用追求一次完美。
+
+先把行业、定位、人群、关键词和内容禁区说清楚,后面在日报和改稿过程中还可以继续调整。
+
+本节完成后,我们就具备了正式生成日报的基础。
+
+下一节,就可以让 OpenClaw 帮我们生成第一份抖音口播每日稿件日报。

+ 131 - 0
docs/5.11/douyin-speaking-daily-p2-image-script.md

@@ -0,0 +1,131 @@
+# p2-生成第一份抖音口播日报说明
+
+> 本页用于配合教学视频使用。视频放在页面上方,下面是文字说明,方便客户跟着操作。
+
+【此处插入视频:p2-生成第一份抖音口播日报】
+
+抖音口播每日稿件日报技能包 P2 生成第一份日报说明。
+
+## 教学场景
+
+本次教学模拟的是新用户完成 P1 后,第一次生成日报。
+
+此时用户已经配置好数据授权,也已经建立了第一版账号档案。
+
+## 一、本节目标
+
+让 OpenClaw 根据你的账号档案,采集抖音口播相关爆款内容,并生成一份今日选题日报。
+
+这份日报的作用是:帮你知道今天可以拍什么方向。
+
+## 二、前置准备
+
+开始前请确认:
+
+1. 已完成 P0 下载和安装。
+2. 已完成 P1 数据授权与账号建档。
+3. OpenClaw 可以正常打开。
+4. 当前账号档案已经保存。
+
+## 三、启动日报的话术
+
+在 OpenClaw 聊天框里输入:
+
+```text
+启动抖音口播每日稿件日报。
+读取刚才建立的账号档案。
+今天帮我采集抖音爆款口播选题,自动补一条最值得拆的视频逐字稿。
+最后直接在聊天里给我日报。
+```
+
+这句话会让 OpenClaw 自动完成三件事:
+
+1. 读取你的账号档案。
+2. 查找适合你账号的抖音爆款选题。
+3. 输出今天的口播选题日报。
+
+## 四、生成过程中会发生什么
+
+OpenClaw 会根据你的账号方向去找内容。
+
+它会参考:
+
+1. 相关视频的热度。
+2. 评论里的真实问题。
+3. 账号定位和目标客户。
+4. 哪些方向更适合改成你的口播。
+
+其中最值得拆的一条视频,系统会尽量补充逐字稿,方便后面继续生成口播稿。
+
+## 五、客户应该看到什么
+
+正常生成后,你会在聊天里看到:
+
+1. 今日一句话判断。
+2. 今日推荐选题列表。
+3. 每条选题的来源和互动数据。
+4. 哪些选题优先级更高。
+5. 哪条视频已补充逐字稿。
+6. 下一步可以回复“选题 2,进入脚本共创”。
+
+## 六、完成标准
+
+看到一份完整日报,并且日报里有可选择的选题序号,就说明本节完成。
+
+如果 OpenClaw 只告诉你保存位置,而没有直接展示日报正文,可以让它继续:
+
+```text
+请直接把刚才的日报正文发在聊天里。
+```
+
+## 七、常见问题
+
+### 1. 生成日报比较慢怎么办?
+
+这是正常情况。系统需要采集视频、评论,并尝试补充逐字稿,等待完成即可。
+
+### 2. 部分关键词没有结果怎么办?
+
+可以先使用已有结果。后续再调整账号关键词,让选题更精准。
+
+### 3. 日报方向不够准怎么办?
+
+后面可以通过“保留哪些方向、不要哪些方向”让系统逐步记住偏好。
+
+## 八、下一节
+
+完成第一份日报后,进入 P3:看懂日报结果,并选择最适合的选题。
+
+## 九、视频口播稿参考
+
+这一节,我们开始生成第一份抖音口播日报。
+
+前面已经完成了安装、数据授权和账号建档。
+
+现在,就可以让 OpenClaw 根据你的账号方向,去帮你找今天适合拍的口播选题。
+
+操作很简单。
+
+直接在 OpenClaw 里输入:启动抖音口播每日稿件日报,读取刚才建立的账号档案,今天帮我采集抖音爆款口播选题,自动补一条最值得拆的视频逐字稿,最后直接在聊天里给我日报。
+
+这句话的意思是,让 OpenClaw 自动完成三件事。
+
+第一,读取你的账号方向。
+
+第二,查找适合你账号的抖音爆款内容。
+
+第三,把结果整理成一份今天可以参考的选题日报。
+
+生成过程中,OpenClaw 会参考视频热度、评论反馈、账号定位和内容匹配度。
+
+其中最值得拆的一条视频,系统会尽量补充逐字稿。
+
+有逐字稿以后,后面生成口播稿时,就不只是看标题,而是能参考这条视频的表达结构。
+
+生成完成后,你应该直接在聊天里看到日报。
+
+这份日报会告诉你:今天有哪些口播方向值得拍,哪些优先级更高,哪些视频已经补充了逐字稿。
+
+看到日报以后,不要急着复制内容去拍。
+
+下一步,我们要从日报里选一条最适合自己账号的选题,进入脚本共创。

+ 128 - 0
docs/5.11/douyin-speaking-daily-p3-image-script.md

@@ -0,0 +1,128 @@
+# p3-选择选题并生成第一版口播初稿说明
+
+> 本页用于配合教学视频使用。视频放在页面上方,下面是文字说明,方便客户跟着操作。
+
+【此处插入视频:p3-选择选题并生成第一版口播初稿】
+
+抖音口播每日稿件日报技能包 P3 选择选题并生成第一版口播初稿说明。
+
+## 教学场景
+
+本次教学模拟的是新用户已经完成 P2:看到了第一份日报,也回复了校准问题。
+
+现在要做的不是重新看日报,而是从刚才保留下来的选题里,挑一条进入脚本共创,让 OpenClaw 生成第一版可以继续修改的口播初稿。
+
+## 一、本节目标
+
+把日报里的一个爆款选题,变成第一版口播初稿。
+
+这一版不是最终稿,而是后续共创和修改的起点。
+
+## 二、本节不讲什么
+
+为了避免和 P2 重复,本节不再重点讲:
+
+1. 日报里每个字段怎么看。
+2. 怎么回复校准问题。
+3. 哪些方向以后多推或少推。
+4. 怎么最终定稿。
+
+这些内容已经在 P2 或后续 P5 里处理。
+
+## 三、什么时候进入本节
+
+当你已经完成下面两件事,就可以进入 P3:
+
+1. OpenClaw 已经生成今日爆款选题日报。
+2. 你已经回复了校准问题,告诉它哪些方向适合自己。
+
+这时就可以从日报里挑一条,开始生成初稿。
+
+## 四、进入脚本共创的话术
+
+在 OpenClaw 聊天框里输入:
+
+选题2,进入脚本共创。请基于我的账号定位,生成一版可以继续修改的口播初稿。
+
+如果你想选第 1 条,就把数字换成 1:
+
+选题1,进入脚本共创。请基于我的账号定位,生成一版可以继续修改的口播初稿。
+
+## 五、OpenClaw 会做什么
+
+收到这句话后,OpenClaw 应该继续沿用刚才的日报结果,而不是重新生成日报。
+
+它会做三件事:
+
+1. 找到你选择的那条选题。
+2. 结合你的账号定位和 P2 里保存的偏好。
+3. 生成一版适合你账号的口播初稿。
+
+如果这条选题已经补过逐字稿,它还会参考原视频的表达结构。
+
+## 六、客户应该看到什么
+
+正常情况下,你应该在聊天里看到:
+
+1. 当前进入的是第几条选题。
+2. 这条选题为什么适合改成你的账号内容。
+3. 一版完整的口播初稿。
+4. 后续可以怎么继续调整。
+
+这一步的重点是“先有稿子”,不是“一次就完美”。
+
+## 七、完成标准
+
+看到第一版口播初稿,就说明本节完成。
+
+如果 OpenClaw 又重新生成了一份日报,说明它没有进入脚本共创,可以继续补一句:
+
+不是重新生成日报,请基于刚才的选题2,直接生成第一版口播初稿。
+
+## 八、常见问题
+
+### 1. 第一版看起来还不够好怎么办?
+
+正常。第一版是共创起点,不是最终稿。下一节会继续改开头、视角、案例和转化表达。
+
+### 2. 可以选不是 P0 的选题吗?
+
+可以。P0 只是优先推荐。如果某条 P1 更适合你的账号,也可以选择它进入脚本共创。
+
+### 3. 选错了怎么办?
+
+可以直接说:
+
+我想换成选题1,重新进入脚本共创。
+
+## 九、下一节
+
+拿到第一版口播初稿后,进入 P4:用自然语言修改初稿,让它更像你的账号、更适合拍摄。
+
+## 十、视频口播稿参考
+
+这一节,我们开始从“选题”进入“稿件”。
+
+上一节,我们已经看到了今日爆款选题日报,也告诉 OpenClaw 哪些方向适合自己的账号,哪些方向以后少推荐。
+
+现在不要重新生成日报,也不要一次处理所有选题。
+
+我们只需要从刚才的选题里,挑一条最适合继续创作的方向。
+
+比如我觉得第 2 条最适合,就直接在 OpenClaw 里回复:
+
+选题2,进入脚本共创。请基于我的账号定位,生成一版可以继续修改的口播初稿。
+
+这句话的意思是,让 OpenClaw 找到刚才日报里的第 2 条选题,再结合我的账号定位和刚才校准过的偏好,生成一版适合我账号的口播初稿。
+
+这里要注意,P3 的目标不是定稿。
+
+P3 的目标是先把一个爆款方向,变成一版可以讨论、可以修改的稿子。
+
+所以第一版出来以后,不用急着判断它完不完美。
+
+我们只需要看它有没有把方向写出来:开头有没有观点,中间有没有展开,结尾有没有动作。
+
+如果看到了第一版口播初稿,就说明这一节完成了。
+
+下一节,我们会继续用普通话告诉 OpenClaw 怎么改,比如开头更狠一点,换成老板视角,少讲概念,多给一个具体业务场景。

+ 127 - 0
docs/5.11/douyin-speaking-daily-p4-image-script.md

@@ -0,0 +1,127 @@
+# p4-第一轮改稿说明
+
+> 本页用于配合教学视频使用。视频放在页面上方,下面是文字说明,方便客户跟着操作。
+
+【此处插入视频:p4-第一轮改稿】
+
+抖音口播每日稿件日报技能包 P4 第一轮改稿说明。
+
+## 教学场景
+
+本次教学模拟的是新用户已经在 P3 拿到了第一版口播初稿。
+
+现在要做的不是重新选题,也不是直接定稿,而是用普通话告诉 OpenClaw:这版稿子哪里不够好,希望它怎么改。
+
+## 一、本节目标
+
+把第一版口播初稿,改成更像用户账号、更适合拍摄的版本。
+
+这一节重点训练的是:不用自己逐字改稿,只要说清楚修改方向。
+
+## 二、什么时候进入本节
+
+当你已经看到第一版口播初稿后,就可以进入 P4。
+
+这时先不要急着说“定稿”,可以先看三个地方:
+
+1. 开头有没有吸引力。
+2. 视角是不是符合你的账号。
+3. 有没有具体业务场景,而不是只讲概念。
+
+## 三、第一轮改稿话术
+
+在 OpenClaw 聊天框里输入:
+
+开头更狠一点,换成老板视角,少讲概念,多给一个具体业务场景。
+
+这句话适合大多数第一次改稿场景。
+
+它会让 OpenClaw 把稿子往四个方向调整:
+
+1. 开头更有冲突。
+2. 表达更像经营判断。
+3. 减少抽象概念。
+4. 增加具体业务场景。
+
+## 四、还可以怎么继续提要求
+
+如果第一轮改完还不够,可以继续用普通话说:
+
+再压缩一点,控制在60秒左右,结尾更偏咨询转化。
+
+也可以说:
+
+这个案例太泛了,换成更具体的电商场景。
+
+或者:
+
+语气再口语化一点,更像真人拍短视频。
+
+## 五、客户应该看到什么
+
+正常情况下,OpenClaw 会返回一版调整后的口播稿。
+
+你应该重点看:
+
+1. 开头是不是更有冲突。
+2. 场景是不是更具体。
+3. 语言是不是更像真人口播。
+4. 结尾是不是有明确动作。
+
+## 六、完成标准
+
+看到调整后的 v1 版本,并且整体方向比 v0 更接近你的需求,就说明本节完成。
+
+如果还不满意,可以继续多轮修改。
+
+如果已经满意,下一节就可以进入定稿。
+
+## 七、常见问题
+
+### 1. 可以改几轮?
+
+可以多轮。每一轮只要说清楚一个主要方向,效果会更稳定。
+
+### 2. 需要自己复制稿子再改吗?
+
+不需要。直接告诉 OpenClaw 你想怎么改,它会基于上一版继续调整。
+
+### 3. 改稿时要说得很专业吗?
+
+不需要。像和同事沟通一样说就行,例如“太泛了”“更像老板说话”“加个具体案例”。
+
+## 八、下一节
+
+当你觉得这版已经可以拍了,进入 P5:定稿,并把本次偏好保存下来。
+
+## 九、视频口播稿参考
+
+这一节,我们开始改稿。
+
+上一节,OpenClaw 已经基于选题生成了第一版口播初稿。
+
+第一版通常不会一次就完美,这是正常的。
+
+我们现在要做的,是像和内容同事沟通一样,告诉它这版稿子应该往哪个方向改。
+
+比如我可以直接回复:
+
+开头更狠一点,换成老板视角,少讲概念,多给一个具体业务场景。
+
+这句话其实包含了四个修改要求。
+
+第一,开头更狠一点,也就是让开头更有冲突,更容易抓住用户。
+
+第二,换成老板视角,也就是少讲知识点,多讲经营判断、成本、转化和结果。
+
+第三,少讲概念,避免内容太像课程。
+
+第四,多给具体业务场景,让用户能听懂这件事到底发生在什么生意里。
+
+OpenClaw 会基于上一版继续改,而不是重新开始。
+
+改完以后,我们重点看四件事:开头有没有更抓人,场景有没有更具体,语言是不是更口语,结尾有没有动作。
+
+如果还不满意,可以继续说“再短一点”“更像真人口播”“加一个电商案例”。
+
+如果这版已经可以拍了,下一节就进入定稿和保存偏好。

+ 131 - 0
docs/5.11/douyin-speaking-daily-p5-image-script.md

@@ -0,0 +1,131 @@
+# p5-定稿与保存偏好说明
+
+> 本页用于配合教学视频使用。视频放在页面上方,下面是文字说明,方便客户跟着操作。
+
+【此处插入视频:p5-定稿与保存偏好】
+
+抖音口播每日稿件日报技能包 P5 定稿与保存偏好说明。
+
+## 教学场景
+
+本次教学模拟的是新用户已经完成 P4:拿到了一版比较满意的口播稿。
+
+现在要做最后一步:确认定稿,并让 OpenClaw 记住本次内容偏好,方便后续日报和稿件越来越贴近用户需求。
+
+## 一、本节目标
+
+把已经调整好的口播稿确认为最终稿,并沉淀本次偏好。
+
+这一步完成后,用户就跑完了从日报到口播稿的第一次完整闭环。
+
+## 二、什么时候进入本节
+
+当你已经觉得当前稿子基本可以拍,就可以进入 P5。
+
+如果还想继续改开头、案例、语气或时长,先留在 P4 继续调整。
+
+## 三、确认定稿的话术
+
+在 OpenClaw 聊天框里输入:
+
+定稿。
+
+OpenClaw 应该把当前版本确认为最终稿,并给出最终交付内容。
+
+## 四、定稿后应该看到什么
+
+正常情况下,OpenClaw 会返回:
+
+1. 最终口播稿。
+2. 可选的精简短版。
+3. 标题或封面方向建议。
+4. 拍摄提示。
+5. 评论区引导。
+
+如果只显示“已保存”,但没有把最终稿发出来,可以继续说:
+
+请把刚才定稿的最终口播稿直接发在聊天里。
+
+## 五、保存内容偏好
+
+定稿后,可以继续告诉 OpenClaw:哪些方向以后多给,哪些方向以后少给。
+
+例如:
+
+保留选题1和2,以后多给这种方向;不要纯工具清单方向。
+
+这句话会让系统记住你的内容偏好。
+
+以后生成日报时,它会更偏向你喜欢的选题和表达风格。
+
+## 六、客户应该看到什么
+
+完成后,你应该看到:
+
+1. 当前稿件已经定稿。
+2. 最终稿可以直接用于拍摄。
+3. 偏好已经保存。
+4. 后续日报会参考这些偏好。
+
+## 七、完成标准
+
+看到“定稿完成”或“偏好已保存”,并且聊天里已经展示最终稿,就说明本次完整闭环完成。
+
+这时用户已经完成:
+
+1. 数据授权与账号建档。
+2. 生成日报。
+3. 校准偏好。
+4. 选择选题并生成初稿。
+5. 修改稿件。
+6. 定稿并保存偏好。
+
+## 八、常见问题
+
+### 1. 定稿后还能改吗?
+
+可以。你可以继续说“在定稿基础上再改一版”。
+
+### 2. 保存偏好有什么用?
+
+保存偏好后,OpenClaw 下次会更懂你的内容风格,比如更偏老板视角、更重业务场景、少给纯工具清单。
+
+### 3. 每天都要从 P1 开始吗?
+
+不需要。建档和授权完成后,日常使用一般从生成日报开始。
+
+## 九、本节结束
+
+到这里,新用户第一次完整流程就跑完了。
+
+后续每天使用时,只需要生成日报、选择选题、共创稿件、定稿保存即可。
+
+## 十、视频口播稿参考
+
+这一节,我们做最后一步:定稿和保存偏好。
+
+前面我们已经生成了日报,选了一个选题,也把第一版口播稿改到比较满意。
+
+现在,如果这版稿子已经可以拍,就直接回复:
+
+定稿。
+
+OpenClaw 会把当前版本确认为最终稿。
+
+正常情况下,它会把最终口播稿、短版建议、标题方向、拍摄提示和评论区引导整理出来。
+
+这里要注意,定稿不是只为了保存一个文件。
+
+更重要的是让 OpenClaw 知道:这一次你认可的内容风格是什么。
+
+所以定稿之后,还可以继续告诉它:
+
+保留选题1和2,以后多给这种方向;不要纯工具清单方向。
+
+这句话的意思是,让它以后多推荐类似的选题,少推荐不适合你的方向。
+
+当你看到“定稿完成”或者“偏好已保存”,并且最终稿已经发在聊天里,就说明这次完整闭环跑通了。
+
+从现在开始,OpenClaw 会越来越了解你的账号偏好。
+
+下一次再生成日报和口播稿时,它就会更贴近你的方向。

+ 76 - 0
docs/5.11/p1.txt

@@ -0,0 +1,76 @@
+p1-数据授权与账号建档说明
+
+抖音口播每日稿件日报技能包 P1 数据授权与账号建档说明
+
+一、本节目标
+完成正式使用前的两项准备:
+1. 配置数据授权码,让 OpenClaw 可以读取抖音相关内容数据。
+2. 建立账号档案,让 OpenClaw 知道你的账号适合讲什么内容。
+
+完成后,就可以进入下一节,生成第一份抖音口播日报。
+
+二、前置准备
+开始前请确认:
+1. 已完成 P0 下载和安装。
+2. 已能正常打开 OpenClaw。
+3. 已拿到服务方提供的数据授权码。
+4. 已大致想好自己的账号方向。
+
+注意:数据授权码属于敏感信息,录屏、截图、发群时请不要公开展示。
+
+三、voctoken怎么配置
+按照视频里的操作,把服务方提供的数据授权码保存到本机 OpenClaw。
+
+保存完成后,OpenClaw 后续生成日报时会自动使用,不需要每次重复输入。
+
+如果不确定是否配置成功,可以在 OpenClaw 里问:
+检查一下抖音口播日报的voctoken是否已经可以使用。
+
+四、账号档案是什么
+账号档案可以理解成你的“账号说明书”。
+
+它会告诉 OpenClaw:
+1. 你是什么行业。
+2. 你的账号定位是什么。
+3. 你主要讲给谁听。
+4. 你希望用户看完后做什么。
+5. 你关注哪些内容方向。
+6. 哪些内容不想再出现。
+
+账号档案越清楚,后面生成的选题和口播稿越贴近你的账号。
+
+五、让 OpenClaw 帮你建档
+如果还没有账号档案,可以直接在 OpenClaw 里输入:
+帮我建立一个抖音口播账号档案。
+你一次问我 1 到 3 个问题。
+问完后帮我保存起来。
+
+OpenClaw 会根据你的回答,逐步补全账号档案。
+
+建议提前想好这些信息:
+1. 行业或产品是什么。
+2. 账号希望给人的感觉是什么。
+3. 目标客户是谁。
+4. 想长期关注哪些关键词。
+5. 哪些方向不要做。
+
+六、完成标准
+看到下面结果,就说明本节完成:
+1. voctoken 已经保存。
+2. OpenClaw 能识别你的账号档案。
+3. 账号档案里已经包含行业、定位、人群、关键词和内容禁区。
+4. OpenClaw 提示可以开始生成日报。
+
+七、常见问题
+1. 没有voctoken 怎么办?
+请先联系我们获取。没有voctoken,系统无法采集抖音内容和评论。
+
+2. 不知道账号定位怎么填怎么办?
+可以先给一个大概方向,后续日报和稿件共创过程中还可以不断调整。
+
+3. 账号档案填错了怎么办?
+可以直接告诉 OpenClaw:
+我要修改我的抖音口播账号档案。
+
+八、下一节
+完成数据授权和账号建档后,进入 P2:生成第一份抖音口播日报。

+ 69 - 0
docs/5.11/p2.txt

@@ -0,0 +1,69 @@
+p2-生成第一份抖音口播日报说明
+
+抖音口播每日稿件日报技能包 P2 生成第一份日报说明
+
+
+
+一、本节目标
+让 OpenClaw 根据你的账号档案,采集抖音口播相关爆款内容,并生成一份今日选题日报。
+
+这份日报的作用是:帮你知道今天可以拍什么方向。
+
+二、前置准备
+开始前请确认:
+1. 已完成 P0 下载和安装。
+2. 已完成 P1 数据授权与账号建档。
+3. OpenClaw 可以正常打开。
+4. 当前账号档案已经保存。
+
+三、启动日报的话术
+在 OpenClaw 聊天框里输入:
+
+启动抖音口播每日稿件日报。
+读取刚才建立的账号档案。
+今天帮我采集抖音爆款口播选题,自动补一条最值得拆的视频逐字稿。
+最后直接在聊天里给我日报。
+
+这句话会让 OpenClaw 自动完成三件事:
+1. 读取你的账号档案。
+2. 查找适合你账号的抖音爆款选题。
+3. 输出今天的口播选题日报。
+
+四、生成过程中会发生什么
+OpenClaw 会根据你的账号方向去找内容。
+
+它会参考:
+1. 相关视频的热度。
+2. 评论里的真实问题。
+3. 账号定位和目标客户。
+4. 哪些方向更适合改成你的口播。
+
+其中最值得拆的一条视频,系统会尽量补充逐字稿,方便后面继续生成口播稿。
+
+五、客户应该看到什么
+正常生成后,你会在聊天里看到:
+1. 今日一句话判断。
+2. 今日推荐选题列表。
+3. 每条选题的来源和互动数据。
+4. 哪些选题优先级更高。
+5. 哪条视频已补充逐字稿。
+6. 下一步可以回复“选题 2,进入脚本共创”。
+
+六、完成标准
+看到一份完整日报,并且日报里有可选择的选题序号,就说明本节完成。
+
+如果 OpenClaw 只告诉你保存位置,而没有直接展示日报正文,可以让它继续:
+请直接把刚才的日报正文发在聊天里。
+
+七、常见问题
+1. 生成日报比较慢怎么办?
+这是正常情况。系统需要采集视频、评论,并尝试补充逐字稿,等待完成即可。
+
+2. 部分关键词没有结果怎么办?
+可以先使用已有结果。后续再调整账号关键词,让选题更精准。
+
+3. 日报方向不够准怎么办?
+后面可以通过“保留哪些方向、不要哪些方向”让系统逐步记住偏好。
+
+八、下一节
+完成第一份日报后,进入 P3:看懂日报结果,并选择最适合的选题。

+ 102 - 0
docs/5.11/p3.txt

@@ -0,0 +1,102 @@
+p3-选择选题并生成第一版口播初稿说明
+
+抖音口播每日稿件日报技能包 P3 选择选题并生成第一版口播初稿说明
+
+教学场景
+本次教学模拟的是新用户已经完成 P2:看到了第一份日报,也回复了校准问题。
+
+现在要做的不是重新看日报,而是从刚才保留下来的选题里,挑一条进入脚本共创,让 OpenClaw 生成第一版可以继续修改的口播初稿。
+
+一、本节目标
+把日报里的一个爆款选题,变成第一版口播初稿。
+
+这一版不是最终稿,而是后续共创和修改的起点。
+
+二、本节不讲什么
+为了避免和 P2 重复,本节不再重点讲:
+1. 日报里每个字段怎么看。
+2. 怎么回复校准问题。
+3. 哪些方向以后多推或少推。
+4. 怎么最终定稿。
+
+这些内容已经在 P2 或后续 P5 里处理。
+
+三、什么时候进入本节
+当你已经完成下面两件事,就可以进入 P3:
+1. OpenClaw 已经生成今日爆款选题日报。
+2. 你已经回复了校准问题,告诉它哪些方向适合自己。
+
+这时就可以从日报里挑一条,开始生成初稿。
+
+四、进入脚本共创的话术
+在 OpenClaw 聊天框里输入:
+选题2,进入脚本共创。请基于我的账号定位,生成一版可以继续修改的口播初稿。
+
+如果你想选第 1 条,就把数字换成 1:
+选题1,进入脚本共创。请基于我的账号定位,生成一版可以继续修改的口播初稿。
+
+五、OpenClaw 会做什么
+收到这句话后,OpenClaw 应该继续沿用刚才的日报结果,而不是重新生成日报。
+
+它会做三件事:
+1. 找到你选择的那条选题。
+2. 结合你的账号定位和 P2 里保存的偏好。
+3. 生成一版适合你账号的口播初稿。
+
+如果这条选题已经补过逐字稿,它还会参考原视频的表达结构。
+
+六、客户应该看到什么
+正常情况下,你应该在聊天里看到:
+1. 当前进入的是第几条选题。
+2. 这条选题为什么适合改成你的账号内容。
+3. 一版完整的口播初稿。
+4. 后续可以怎么继续调整。
+
+这一步的重点是“先有稿子”,不是“一次就完美”。
+
+七、完成标准
+看到第一版口播初稿,就说明本节完成。
+
+如果 OpenClaw 又重新生成了一份日报,说明它没有进入脚本共创,可以继续补一句:
+不是重新生成日报,请基于刚才的选题2,直接生成第一版口播初稿。
+
+八、常见问题
+1. 第一版看起来还不够好怎么办?
+正常。第一版是共创起点,不是最终稿。下一节会继续改开头、视角、案例和转化表达。
+
+2. 可以选不是 P0 的选题吗?
+可以。P0 只是优先推荐。如果某条 P1 更适合你的账号,也可以选择它进入脚本共创。
+
+3. 选错了怎么办?
+可以直接说:
+我想换成选题1,重新进入脚本共创。
+
+九、下一节
+拿到第一版口播初稿后,进入 P4:用自然语言修改初稿,让它更像你的账号、更适合拍摄。
+
+十、视频口播稿参考
+这一节,我们开始从“选题”进入“稿件”。
+
+上一节,我们已经看到了今日爆款选题日报,也告诉 OpenClaw 哪些方向适合自己的账号,哪些方向以后少推荐。
+
+现在不要重新生成日报,也不要一次处理所有选题。
+
+我们只需要从刚才的选题里,挑一条最适合继续创作的方向。
+
+比如我觉得第 2 条最适合,就直接在 OpenClaw 里回复:
+
+选题2,进入脚本共创。请基于我的账号定位,生成一版可以继续修改的口播初稿。
+
+这句话的意思是,让 OpenClaw 找到刚才日报里的第 2 条选题,再结合我的账号定位和刚才校准过的偏好,生成一版适合我账号的口播初稿。
+
+这里要注意,P3 的目标不是定稿。
+
+P3 的目标是先把一个爆款方向,变成一版可以讨论、可以修改的稿子。
+
+所以第一版出来以后,不用急着判断它完不完美。
+
+我们只需要看它有没有把方向写出来:开头有没有观点,中间有没有展开,结尾有没有动作。
+
+如果看到了第一版口播初稿,就说明这一节完成了。
+
+下一节,我们会继续用普通话告诉 OpenClaw 怎么改,比如开头更狠一点,换成老板视角,少讲概念,多给一个具体业务场景。

+ 103 - 0
docs/5.11/p4.txt

@@ -0,0 +1,103 @@
+p4-第一轮改稿说明
+
+抖音口播每日稿件日报技能包 P4 第一轮改稿说明
+
+教学场景
+本次教学模拟的是新用户已经在 P3 拿到了第一版口播初稿。
+
+现在要做的不是重新选题,也不是直接定稿,而是用普通话告诉 OpenClaw:这版稿子哪里不够好,希望它怎么改。
+
+一、本节目标
+把第一版口播初稿,改成更像用户账号、更适合拍摄的版本。
+
+这一节重点训练的是:不用自己逐字改稿,只要说清楚修改方向。
+
+二、什么时候进入本节
+当你已经看到第一版口播初稿后,就可以进入 P4。
+
+这时先不要急着说“定稿”,可以先看三个地方:
+1. 开头有没有吸引力。
+2. 视角是不是符合你的账号。
+3. 有没有具体业务场景,而不是只讲概念。
+
+三、第一轮改稿话术
+在 OpenClaw 聊天框里输入:
+开头更狠一点,换成老板视角,少讲概念,多给一个具体业务场景。
+
+这句话适合大多数第一次改稿场景。
+
+它会让 OpenClaw 把稿子往四个方向调整:
+1. 开头更有冲突。
+2. 表达更像经营判断。
+3. 减少抽象概念。
+4. 增加具体业务场景。
+
+四、还可以怎么继续提要求
+如果第一轮改完还不够,可以继续用普通话说:
+再压缩一点,控制在60秒左右,结尾更偏咨询转化。
+
+也可以说:
+这个案例太泛了,换成更具体的电商场景。
+
+或者:
+语气再口语化一点,更像真人拍短视频。
+
+五、客户应该看到什么
+正常情况下,OpenClaw 会返回一版调整后的口播稿。
+
+你应该重点看:
+1. 开头是不是更有冲突。
+2. 场景是不是更具体。
+3. 语言是不是更像真人口播。
+4. 结尾是不是有明确动作。
+
+六、完成标准
+看到调整后的 v1 版本,并且整体方向比 v0 更接近你的需求,就说明本节完成。
+
+如果还不满意,可以继续多轮修改。
+
+如果已经满意,下一节就可以进入定稿。
+
+七、常见问题
+1. 可以改几轮?
+可以多轮。每一轮只要说清楚一个主要方向,效果会更稳定。
+
+2. 需要自己复制稿子再改吗?
+不需要。直接告诉 OpenClaw 你想怎么改,它会基于上一版继续调整。
+
+3. 改稿时要说得很专业吗?
+不需要。像和同事沟通一样说就行,例如“太泛了”“更像老板说话”“加个具体案例”。
+
+八、下一节
+当你觉得这版已经可以拍了,进入 P5:定稿,并把本次偏好保存下来。
+
+九、视频口播稿参考
+这一节,我们开始改稿。
+
+上一节,OpenClaw 已经基于选题生成了第一版口播初稿。
+
+第一版通常不会一次就完美,这是正常的。
+
+我们现在要做的,是像和内容同事沟通一样,告诉它这版稿子应该往哪个方向改。
+
+比如我可以直接回复:
+
+开头更狠一点,换成老板视角,少讲概念,多给一个具体业务场景。
+
+这句话其实包含了四个修改要求。
+
+第一,开头更狠一点,也就是让开头更有冲突,更容易抓住用户。
+
+第二,换成老板视角,也就是少讲知识点,多讲经营判断、成本、转化和结果。
+
+第三,少讲概念,避免内容太像课程。
+
+第四,多给具体业务场景,让用户能听懂这件事到底发生在什么生意里。
+
+OpenClaw 会基于上一版继续改,而不是重新开始。
+
+改完以后,我们重点看四件事:开头有没有更抓人,场景有没有更具体,语言是不是更口语,结尾有没有动作。
+
+如果还不满意,可以继续说“再短一点”“更像真人口播”“加一个电商案例”。
+
+如果这版已经可以拍了,下一节就进入定稿和保存偏好。

+ 107 - 0
docs/5.11/p5.txt

@@ -0,0 +1,107 @@
+p5-定稿与保存偏好说明
+
+抖音口播每日稿件日报技能包 P5 定稿与保存偏好说明
+
+教学场景
+本次教学模拟的是新用户已经完成 P4:拿到了一版比较满意的口播稿。
+
+现在要做最后一步:确认定稿,并让 OpenClaw 记住本次内容偏好,方便后续日报和稿件越来越贴近用户需求。
+
+一、本节目标
+把已经调整好的口播稿确认为最终稿,并沉淀本次偏好。
+
+这一步完成后,用户就跑完了从日报到口播稿的第一次完整闭环。
+
+二、什么时候进入本节
+当你已经觉得当前稿子基本可以拍,就可以进入 P5。
+
+如果还想继续改开头、案例、语气或时长,先留在 P4 继续调整。
+
+三、确认定稿的话术
+在 OpenClaw 聊天框里输入:
+定稿。
+
+OpenClaw 应该把当前版本确认为最终稿,并给出最终交付内容。
+
+四、定稿后应该看到什么
+正常情况下,OpenClaw 会返回:
+1. 最终口播稿。
+2. 可选的精简短版。
+3. 标题或封面方向建议。
+4. 拍摄提示。
+5. 评论区引导。
+
+如果只显示“已保存”,但没有把最终稿发出来,可以继续说:
+请把刚才定稿的最终口播稿直接发在聊天里。
+
+五、保存内容偏好
+定稿后,可以继续告诉 OpenClaw:哪些方向以后多给,哪些方向以后少给。
+
+例如:
+保留选题1和2,以后多给这种方向;不要纯工具清单方向。
+
+这句话会让系统记住你的内容偏好。
+
+以后生成日报时,它会更偏向你喜欢的选题和表达风格。
+
+六、客户应该看到什么
+完成后,你应该看到:
+1. 当前稿件已经定稿。
+2. 最终稿可以直接用于拍摄。
+3. 偏好已经保存。
+4. 后续日报会参考这些偏好。
+
+七、完成标准
+看到“定稿完成”或“偏好已保存”,并且聊天里已经展示最终稿,就说明本次完整闭环完成。
+
+这时用户已经完成:
+1. 数据授权与账号建档。
+2. 生成日报。
+3. 校准偏好。
+4. 选择选题并生成初稿。
+5. 修改稿件。
+6. 定稿并保存偏好。
+
+八、常见问题
+1. 定稿后还能改吗?
+可以。你可以继续说“在定稿基础上再改一版”。
+
+2. 保存偏好有什么用?
+保存偏好后,OpenClaw 下次会更懂你的内容风格,比如更偏老板视角、更重业务场景、少给纯工具清单。
+
+3. 每天都要从 P1 开始吗?
+不需要。建档和授权完成后,日常使用一般从生成日报开始。
+
+九、本节结束
+到这里,新用户第一次完整流程就跑完了。
+
+后续每天使用时,只需要生成日报、选择选题、共创稿件、定稿保存即可。
+
+十、视频口播稿参考
+这一节,我们做最后一步:定稿和保存偏好。
+
+前面我们已经生成了日报,选了一个选题,也把第一版口播稿改到比较满意。
+
+现在,如果这版稿子已经可以拍,就直接回复:
+
+定稿。
+
+OpenClaw 会把当前版本确认为最终稿。
+
+正常情况下,它会把最终口播稿、短版建议、标题方向、拍摄提示和评论区引导整理出来。
+
+这里要注意,定稿不是只为了保存一个文件。
+
+更重要的是让 OpenClaw 知道:这一次你认可的内容风格是什么。
+
+所以定稿之后,还可以继续告诉它:
+
+保留选题1和2,以后多给这种方向;不要纯工具清单方向。
+
+这句话的意思是,让它以后多推荐类似的选题,少推荐不适合你的方向。
+
+当你看到“定稿完成”或者“偏好已保存”,并且最终稿已经发在聊天里,就说明这次完整闭环跑通了。
+
+从现在开始,OpenClaw 会越来越了解你的账号偏好。
+
+下一次再生成日报和口播稿时,它就会更贴近你的方向。

+ 777 - 0
docs/specs/claude-code-native-voc-skill-package-design.md

@@ -0,0 +1,777 @@
+# Claude Code 原生版 VOC 技能包设计说明
+
+更新时间:2026-05-22
+
+## 目标修正
+
+这里不是把 OpenClaw 技能包“迁移”成 Claude Code 版本,也不是兼容旧 `api-config.json`。
+
+正确目标是:
+
+> 为 Claude Code 重新写一套原生 VOC 技能包。旧 OpenClaw 技能包只作为业务流程、接口口径、数据结构、样例输出和测试经验参考。
+
+也就是说:
+
+- OpenClaw 版本继续保留。
+- Claude Code 版本独立成包。
+- 两套包可以共用底层 API 文档和部分业务算法思想,但目录结构、调用方式、技能说明、安装方式、运行协议都重新设计。
+
+## Claude Code 版本应该长什么样
+
+Claude Code 版本不建议做成一堆零散技能,而应该做成一个原生插件套件:
+
+```text
+claude-code-voc-intelligence/
+  .claude-plugin/
+    plugin.json
+
+  skills/
+    voc-setup/
+      SKILL.md
+      references/
+        onboarding.md
+
+    douyin-speaking-daily/
+      SKILL.md
+      references/
+        user-workflows.md
+        output-format.md
+        script-co-creation.md
+      examples/
+        daily-report.md
+        script-session.md
+
+    douyin-video-transcript/
+      SKILL.md
+      references/
+        provider-iflytek-gateway.md
+        transcript-schema.md
+
+    social-trend-intelligence/
+      SKILL.md
+      references/
+        xiaohongshu-workflow.md
+        industry-profile.md
+        trend-report-format.md
+
+  mcp/
+    package.json
+    src/
+      server.ts
+      tools/
+        douyin-speaking-daily.ts
+        douyin-video-transcript.ts
+        douyin-search.ts
+        douyin-comments.ts
+        xiaohongshu-search.ts
+        xiaohongshu-comments.ts
+        social-trend-report.ts
+        voc-token.ts
+      core/
+        credentials.ts
+        http-client.ts
+        result-envelope.ts
+        workspace.ts
+        memory-store.ts
+        report-writer.ts
+        transcript-cache.ts
+      providers/
+        voc-social-api.ts
+        iflytek-gateway.ts
+
+  hooks/
+    preflight-token-check.ts
+    postrun-archive.ts
+
+  memory-templates/
+    douyin-speaking-profile.json
+    social-trend-profile.json
+    script-preference-memory.json
+
+  bin/
+    voc-mcp.cmd
+    voc-mcp
+
+  README.md
+  package.json
+```
+
+这个包和 OpenClaw 目录平级,不要塞回 `douyin/` 或 `industry-trend-intelligence/` 旧目录里。
+
+## 和 OpenClaw 版本的区别
+
+| 维度 | OpenClaw 版本 | Claude Code 原生版 |
+| --- | --- | --- |
+| 技能识别 | `SKILL.md + api-config.json` | `skills/<name>/SKILL.md` |
+| 工具调用 | OpenClaw 读取 `api-config.json` | Claude Code 通过 MCP tools 调用 |
+| 用户入口 | OpenClaw 聊天触发技能 | Claude Code Skill 自动触发或 `/skill-name` |
+| 参数协议 | `{{profile}}`、`{{output}}` 模板参数 | MCP JSON schema + Skill 自然语言说明 |
+| 输出协议 | stdout prefix JSON + `assistantMessage` | MCP structured result + Skill 展示策略 |
+| 打包方式 | zip + `install.js` 安装到 `.openclaw` | Claude Code plugin / skills / MCP package |
+| 记忆目录 | `.openclaw/workspace/memory` 或项目 `memory` | plugin data dir / project `.claude` / configurable memory root |
+
+结论:
+
+Claude Code 版本不应该继续依赖 `api-config.json` 作为主协议。可以参考其字段,但应重新定义 MCP tool schema。
+
+## 哪些旧东西可以复用
+
+### 可以复用业务流程
+
+这些流程已经被测试过,应该复用为 Claude Code Skill 的工作流:
+
+1. 抖音口播日报:建档、采集、评论、逐字稿、日报、校准。
+2. 爆款视频逐字稿:链接解析、媒体下载、讯飞网关、transcript schema。
+3. 脚本共创:选题、v0、反馈改稿、定稿、偏好沉淀。
+4. 行业趋势情报官:行业建档、关键词矩阵、小红书采集、评论洞察、趋势报告。
+5. 偏好记忆:保留方向、禁区方向、视角偏好、案例偏好。
+
+### 可以复用数据结构
+
+建议保留这些核心 schema 思想:
+
+```text
+assistantMessage
+summary
+warnings
+errors
+files
+daily-report.json
+transcript.json
+script-session.json
+profile.json
+memory.json
+```
+
+但 Claude Code 版本要重新定义更干净的 schema,不要完全照搬 OpenClaw 里的历史字段。
+
+### 可以复用接口经验
+
+可以参考:
+
+- 抖音搜索接口怎么返回视频。
+- 抖音评论接口怎么分页。
+- 抖音视频解析如何找音频 URL。
+- 讯飞网关上传和查询如何跑。
+- 小红书搜索、详情、评论如何组合。
+- VOC token 如何鉴权。
+
+但 Claude Code 版本应重新写 API client,不建议直接把旧 runner 当黑盒调用。
+
+### 可以复用测试样本
+
+可以把旧测试样本转成 Claude Code 版本 fixtures:
+
+```text
+claude-code-voc-intelligence/tests/fixtures/
+  douyin-daily-sample-input.json
+  douyin-daily-sample-output.json
+  transcript-sample.json
+  xiaohongshu-trend-sample.json
+```
+
+## 哪些旧东西不要复用
+
+### 不要复用 OpenClaw `api-config.json` 作为 Claude Code 主协议
+
+原因:
+
+- Claude Code 不认识它。
+- MCP 已经有自己的 JSON schema 工具协议。
+- 原来的 `{{param}}` 模板适合 OpenClaw,不适合 Claude Code 原生工具调用。
+
+可以写一个转换脚本辅助生成初版 MCP schema,但不要让 Claude Code 运行时依赖它。
+
+### 不要复用旧技能目录作为 Claude Code 技能目录
+
+不要这样:
+
+```text
+douyin/douyin-speaking-daily-runner/SKILL.md
+```
+
+而应该这样:
+
+```text
+claude-code-voc-intelligence/skills/douyin-speaking-daily/SKILL.md
+```
+
+### 不要复用旧 runner 的大而全主入口
+
+旧 runner 为了 OpenClaw 闭环承担了太多事情。
+
+Claude Code 版本应该拆开:
+
+```text
+MCP tool: voc_douyin_daily_run
+MCP tool: voc_douyin_script_create
+MCP tool: voc_douyin_script_revise
+MCP tool: voc_douyin_script_finalize
+MCP tool: voc_douyin_transcript_video
+MCP tool: voc_social_trend_run
+```
+
+Skill 再负责把这些工具串成自然语言工作流。
+
+### 不要让用户接触技术参数
+
+Claude Code Skill 里也不要让用户说:
+
+```text
+profile=...
+autoTranscriptTopN=1
+autoTranscriptProvider=iflytek-gateway
+```
+
+用户应该说:
+
+```text
+启动抖音口播每日稿件日报。
+选题2,进入脚本共创。
+开头更狠一点,换老板视角。
+定稿。
+```
+
+## Claude Code 原生版的技能拆分
+
+建议第一版只做 4 个用户可见技能。
+
+### 1. `voc-setup`
+
+用途:
+
+- 配置 VOC token。
+- 检查余额。
+- 初始化用户工作目录。
+- 创建默认 profile。
+- 解释技能包能做什么。
+
+用户话术:
+
+```text
+帮我初始化 VOC 技能包。
+配置我的 VOC 服务。
+检查一下现在能不能采集社媒数据。
+```
+
+背后工具:
+
+```text
+voc_token_set
+voc_token_check
+voc_workspace_init
+```
+
+### 2. `douyin-speaking-daily`
+
+用途:
+
+- 生成抖音口播每日稿件日报。
+- 自动补一条最值得拆的视频逐字稿。
+- 展示 Top 选题。
+- 支持校准偏好。
+- 支持脚本共创和定稿。
+
+用户话术:
+
+```text
+启动抖音口播每日稿件日报。
+今天帮我找适合账号的爆款口播选题。
+选题2,进入脚本共创。
+开头更狠一点,老板视角。
+定稿。
+不要纯工具清单方向。
+```
+
+背后工具:
+
+```text
+voc_douyin_daily_run
+voc_douyin_topic_select
+voc_douyin_script_create
+voc_douyin_script_revise
+voc_douyin_script_finalize
+voc_preference_update
+```
+
+### 3. `douyin-video-transcript`
+
+用途:
+
+- 用户给一个抖音视频链接。
+- Claude Code 自动解析、下载媒体、转写逐字稿。
+- 输出逐字稿拆解和可借鉴结构。
+- 可继续生成本账号口播草案。
+
+用户话术:
+
+```text
+帮我拆这个抖音视频。
+把这个爆款视频转成逐字稿。
+参考这个视频,给我的账号写一版口播稿。
+```
+
+背后工具:
+
+```text
+voc_douyin_video_resolve
+voc_douyin_video_transcript
+voc_script_from_transcript
+```
+
+### 4. `social-trend-intelligence`
+
+用途:
+
+- 面向小红书、抖音、知乎、微博等社媒趋势情报。
+- 第一版可以先支持小红书。
+- 适合家装全屋定制、女性消费、设计风格趋势、行业选题洞察。
+
+用户话术:
+
+```text
+帮我做一份家装全屋定制行业的小红书趋势情报。
+看看下季度女性客户更关注哪些设计元素。
+帮我找小红书上最近流行的装修风格和用户顾虑。
+```
+
+背后工具:
+
+```text
+voc_social_profile_build
+voc_xiaohongshu_search_notes
+voc_xiaohongshu_note_detail
+voc_xiaohongshu_note_comments
+voc_social_trend_report
+voc_social_preference_update
+```
+
+## MCP tools 设计
+
+Claude Code 原生版建议直接用 MCP 做工具层。
+
+### Tool 命名原则
+
+统一前缀:
+
+```text
+voc_
+```
+
+按业务域命名:
+
+```text
+voc_douyin_daily_run
+voc_douyin_video_transcript
+voc_douyin_script_create
+voc_douyin_script_revise
+voc_douyin_script_finalize
+voc_social_trend_run
+voc_xiaohongshu_search_notes
+voc_xiaohongshu_note_comments
+voc_token_check
+```
+
+### Tool 输出原则
+
+所有工具统一返回:
+
+```json
+{
+  "status": "ok",
+  "assistantMessage": "可以直接给用户看的正文",
+  "summary": {},
+  "data": {},
+  "files": [],
+  "nextActions": [],
+  "warnings": [],
+  "errors": []
+}
+```
+
+Claude Code Skill 的要求:
+
+> 如果工具返回 `assistantMessage`,优先把它作为主要回复,不要只告诉用户文件路径。
+
+## 核心代码模块设计
+
+Claude Code 原生版不要从旧 `scripts/tools/*.js` 直接开始复制,而应该重新拆核心模块。
+
+```text
+mcp/src/core/
+  result-envelope.ts
+  credentials.ts
+  workspace.ts
+  memory-store.ts
+  file-store.ts
+  report-writer.ts
+  preference-engine.ts
+  budget-guard.ts
+
+mcp/src/providers/
+  voc-social-api.ts
+  iflytek-gateway.ts
+  douyin-media-resolver.ts
+  xiaohongshu-api.ts
+
+mcp/src/features/
+  douyin-speaking-daily/
+    daily-runner.ts
+    topic-ranker.ts
+    transcript-selector.ts
+    script-cocreation.ts
+    script-memory.ts
+
+  douyin-video-transcript/
+    video-transcript-runner.ts
+    transcript-normalizer.ts
+    transcript-analyzer.ts
+
+  social-trend-intelligence/
+    profile-builder.ts
+    keyword-matrix.ts
+    collector.ts
+    trend-report.ts
+    preference-memory.ts
+```
+
+这样比旧 runner 更清晰,也方便 Claude Code 多工具组合。
+
+## Skill 文档写法
+
+每个 Skill 的 `SKILL.md` 要短,不要把所有 API 文档塞进去。
+
+推荐结构:
+
+```markdown
+---
+name: douyin-speaking-daily
+description: Generate a Douyin talking-head daily content report, select viral topics, transcribe one high-value video, co-create scripts, and save preferences. Use when the user asks for 抖音口播日报、爆款选题、脚本共创、定稿 or content preference calibration.
+allowed-tools: mcp__voc__voc_douyin_daily_run mcp__voc__voc_douyin_script_create mcp__voc__voc_douyin_script_revise mcp__voc__voc_douyin_script_finalize
+---
+
+## Rules
+
+- Speak to the user in natural Chinese.
+- Do not expose technical params such as profile path, output path, provider, or token unless troubleshooting.
+- Prefer showing the tool's assistantMessage directly.
+- If profile is missing, guide the user through 3-5 business questions.
+- If the user says 选题N, enter script co-creation.
+- If the user says 定稿, finalize and save preference memory.
+
+## Workflow
+
+1. Check profile.
+2. Run daily report.
+3. Show Top topics.
+4. Ask for calibration.
+5. Continue into script co-creation when user selects a topic.
+```
+
+详细格式放到 `references/`。
+
+## 需要重写的部分
+
+### 必须重写
+
+1. Claude Code `skills/*/SKILL.md`
+2. MCP server
+3. MCP tools schema
+4. Credential 管理
+5. Memory 管理
+6. Result envelope
+7. Tool 输出展示策略
+8. Claude Code 安装说明
+9. Claude Code smoke tests
+
+### 建议重写
+
+1. 抖音日报 runner 内核
+2. 小红书趋势 runner 内核
+3. 逐字稿 runner 内核
+4. 脚本共创模块
+5. 偏好记忆模块
+
+### 可以晚点重写
+
+1. 完整报告 Markdown 模板
+2. 详细 audit 文件
+3. 课程教学文档
+4. 多平台扩展
+5. 插件 hooks
+
+## 可以从旧代码借鉴但不要照搬的部分
+
+| 旧文件 | 借鉴内容 | Claude Code 新写方向 |
+| --- | --- | --- |
+| `scripts/tools/douyin-speaking-daily-runner.js` | 完整闭环、自然语言续接、脚本共创、偏好记忆 | 拆成 `daily-runner.ts`、`script-cocreation.ts`、`preference-engine.ts` |
+| `scripts/tools/douyin-video-transcriber.js` | 抖音链接解析、讯飞网关、transcript schema | 拆成 `video-resolver.ts`、`iflytek-gateway.ts`、`transcript-normalizer.ts` |
+| `scripts/tools/douyin-speaking-daily-report.js` | 选题排序、日报结构、assistantMessage 输出 | 重写为 report writer + topic ranker |
+| `industry-trend-intelligence/scripts/industry-trend-runner.js` | 行业趋势流程、小红书采集、偏好记忆 | 重写为 social trend feature |
+| `industry-trend-intelligence/scripts/xiaohongshu-trend-collector.js` | 小红书采集策略 | 重写为 xiaohongshu provider + collector |
+| `scripts/tools/set-voc-token.js` | token 存储和检查经验 | 重写为 voc setup tool |
+
+## 第一版实施范围
+
+建议 Claude Code 原生版第一版不要贪多,只做 3 个闭环。
+
+### 闭环 1:VOC 初始化
+
+```text
+用户:帮我初始化 VOC 技能包
+Claude:检查 token、余额、工作目录、默认 memory
+```
+
+### 闭环 2:抖音口播日报
+
+```text
+用户:启动抖音口播每日稿件日报
+Claude:采集、转写一条、输出日报正文、给出校准问题
+```
+
+### 闭环 3:选题到稿件共创
+
+```text
+用户:选题2,进入脚本共创
+Claude:生成 v0
+用户:开头更狠,老板视角
+Claude:生成 v1
+用户:定稿
+Claude:保存稿件和偏好
+```
+
+行业趋势/小红书可以作为第二批进入。
+
+## 目录落地建议
+
+在当前仓库新增:
+
+```text
+claude-code-voc-intelligence/
+```
+
+不要和旧 OpenClaw 包混在一起。
+
+当前已经先按这个判断落地了小红书趋势情报官:
+
+```text
+claude-code-voc-intelligence/
+  .claude-plugin/plugin.json
+  .mcp.json
+  skills/xiaohongshu-trend-intelligence/SKILL.md
+  mcp/src/server.js
+  mcp/src/tools/xiaohongshu-trend-run.js
+  mcp/src/tools/xiaohongshu-preference-update.js
+  mcp/src/providers/xiaohongshu-api.js
+  mcp/src/features/xiaohongshu-trend/
+  memory-templates/xiaohongshu-trend-profile.json
+```
+
+已验证的 MCP tools:
+
+```text
+voc_xiaohongshu_token_check
+voc_xiaohongshu_trend_run
+voc_xiaohongshu_preference_update
+```
+
+已新增独立打包脚本:
+
+```text
+scripts/deploy/claude-code-voc-intelligence-suite.js
+```
+
+本地交付产物:
+
+```text
+dist/claude-code-voc-intelligence.zip
+  dist/claude-code-voc-intelligence-suite-manifest.json
+```
+
+验证方式:
+
+```bash
+npm run claude-voc:build
+```
+
+该命令会把包解压到临时目录、重新安装依赖,并在解压包里跑 sample、偏好记忆和 MCP smoke。
+
+用户安装入口:
+
+```bash
+node install.js
+```
+
+现场自检入口:
+
+```bash
+node install.js --smoke
+```
+
+第一阶段文件:
+
+```text
+claude-code-voc-intelligence/
+  skills/
+    voc-setup/SKILL.md
+    douyin-speaking-daily/SKILL.md
+    douyin-video-transcript/SKILL.md
+
+  mcp/
+    package.json
+    tsconfig.json
+    src/server.ts
+    src/tools/voc-token.ts
+    src/tools/douyin-speaking-daily.ts
+    src/tools/douyin-video-transcript.ts
+    src/core/result-envelope.ts
+    src/core/credentials.ts
+    src/core/workspace.ts
+
+  tests/
+    smoke-douyin-daily.test.ts
+    smoke-transcript.test.ts
+
+  memory-templates/
+    douyin-speaking-profile.json
+
+  README.md
+```
+
+## P0/P1/P2 重新定义
+
+### Claude Code P0
+
+目标:Claude Code 原生技能能被识别,能跑 sample 模式。
+
+交付:
+
+- `voc-setup`
+- `douyin-speaking-daily`
+- `douyin-video-transcript`
+- MCP server 基础版
+- sample 数据模式
+- 不调用真实付费接口也能演示
+
+### Claude Code P1
+
+目标:真实接口跑通。
+
+交付:
+
+- VOC token 设置和余额检查
+- 抖音搜索、评论、逐字稿真实调用
+- 脚本共创和定稿记忆
+- 错误处理和预算保护
+- Windows 中文路径测试
+
+### Claude Code P2
+
+目标:插件产品化。
+
+交付:
+
+- `.claude-plugin/plugin.json`
+- hooks
+- 安装脚本
+- 自动更新说明
+- 小红书/行业趋势技能加入
+- 教学文档和录屏话术
+
+## 关键设计判断
+
+### 1. Skill 不负责跑业务逻辑
+
+Skill 只负责:
+
+- 什么时候触发。
+- 怎么和用户说话。
+- 调哪个 MCP tool。
+- 如何展示结果。
+- 后续如何续接。
+
+业务逻辑在 MCP tool 里。
+
+### 2. MCP tool 不负责长篇表达
+
+MCP tool 负责:
+
+- 拉数据。
+- 算分。
+- 写文件。
+- 返回结构化结果。
+- 生成基础 `assistantMessage`。
+
+最终表达由 Skill 决定是否补充、压缩、追问。
+
+### 3. Memory 是 Claude Code 版本自己的
+
+不要直接写 `.openclaw/workspace/memory`。
+
+建议:
+
+```text
+项目模式:<project>/.voc-memory/
+插件模式:<plugin-data>/memory/
+调试模式:claude-code-voc-intelligence/.tmp/memory/
+```
+
+### 4. 输出文件是备份,不是主要交付
+
+Claude Code 用户体验要坚持:
+
+```text
+聊天里直接给结果
+文件只是归档
+```
+
+## 验收标准
+
+### 技术验收
+
+- Claude Code 能识别 3 个 skills。
+- MCP server 能启动。
+- Claude 能看到 VOC tools。
+- sample 模式无需 token 可跑通。
+- live 模式 token 可检查余额。
+- 工具返回统一 result envelope。
+- 错误返回可读 `assistantMessage`。
+
+### 用户体验验收
+
+用户只需要说:
+
+```text
+启动抖音口播每日稿件日报。
+```
+
+不需要知道:
+
+```text
+profile
+output
+provider
+autoTranscriptTopN
+```
+
+用户继续说:
+
+```text
+选题2,进入脚本共创。
+开头更狠一点,老板视角。
+定稿。
+```
+
+Claude Code 应能自然续接,不要求用户提供文件路径。
+
+## 总结
+
+如果是 Claude Code 原生版,就不要围绕 OpenClaw 的 `api-config.json` 做兼容适配。
+
+正确做法是:
+
+1. 新建独立 `claude-code-voc-intelligence/`。
+2. 用 Claude Code Skill 做自然语言工作流入口。
+3. 用 MCP tools 承接真实接口和业务执行。
+4. 旧 OpenClaw 代码作为业务参考,不作为运行时依赖。
+5. 第一版只做 VOC 初始化、抖音日报、逐字稿、脚本共创四个核心闭环。

+ 224 - 0
docs/specs/claude-code-native-xiaohongshu-trend-p0.md

@@ -0,0 +1,224 @@
+# Claude Code 原生版小红书趋势情报官 P0 设计
+
+更新时间:2026-05-22
+
+## 目标
+
+先做 Claude Code 原生版的小红书趋势情报官,不依赖 OpenClaw 的技能外壳。
+
+第一阶段目标:
+
+- 新用户可以用自然语言启动。
+- sample 模式不用 token 也能跑通教学和演示。
+- 输出第一轮样本观察和待确认问题,而不是只给文件路径。
+- 后续可以平滑接入 MCP live 模式。
+
+## 用户入口
+
+```text
+帮我做一份家装全屋定制行业的小红书趋势情报。
+重点看女性客户下季度可能关注的设计元素、风格和消费顾虑。
+先在聊天里给我第一轮样本观察和待确认问题,不要只给文件路径。
+```
+
+## 已建立文件
+
+```text
+claude-code-voc-intelligence/
+  .claude-plugin/plugin.json
+  docs/customer-quickstart.md
+  docs/demo-runbook.md
+  docs/npm-packaging-notes.md
+  bin/claude-voc.js
+  skills/xiaohongshu-trend-intelligence/SKILL.md
+  skills/xiaohongshu-trend-intelligence/references/user-workflow.md
+  skills/xiaohongshu-trend-intelligence/references/output-format.md
+  skills/xiaohongshu-trend-intelligence/references/live-mode.md
+  skill-package-manifest.json
+  install.js
+  package.json
+  package-lock.json
+  memory-templates/xiaohongshu-trend-profile.json
+  scripts/smoke-mcp.js
+  scripts/smoke-package.js
+  mcp/src/server.js
+  mcp/src/tools/xiaohongshu-trend-run.js
+  mcp/src/tools/xiaohongshu-preference-update.js
+  mcp/src/core/memory-store.js
+  mcp/src/features/xiaohongshu-trend/preference-memory.js
+  mcp/src/features/xiaohongshu-trend/sample-data.js
+  mcp/src/features/xiaohongshu-trend/report.js
+  mcp/src/core/result-envelope.js
+```
+
+## P0 能力
+
+- 读取行业 profile。
+- 使用 sample 小红书笔记和评论。
+- 生成趋势判断、用户顾虑、内容选题、门店话术。
+- 在聊天正文里展示数据口径、证据样本和下一步可回复话术。
+- 返回统一 result envelope。
+- 写入 markdown 和 json 归档。
+- 把 `assistantMessage` 作为聊天里的主要交付内容。
+
+## 下一步 P1
+
+- 写 MCP server。
+- 接入真实小红书搜索、详情、评论。
+- 增加 VOC token 检查。
+- 增加偏好记忆。
+- 增加 live 小规模 smoke test。
+
+## P1 已推进
+
+已新增 MCP server:
+
+```text
+claude-code-voc-intelligence/mcp/src/server.js
+claude-code-voc-intelligence/.mcp.json
+```
+
+已暴露工具:
+
+```text
+voc_xiaohongshu_token_check
+voc_xiaohongshu_trend_run
+voc_xiaohongshu_preference_update
+```
+
+已新增 live provider 骨架:
+
+```text
+claude-code-voc-intelligence/mcp/src/core/credentials.js
+claude-code-voc-intelligence/mcp/src/providers/xiaohongshu-api.js
+claude-code-voc-intelligence/mcp/src/features/xiaohongshu-trend/live-collector.js
+```
+
+当前 live 行为:
+
+- 没有小红书/TikHub token 时返回 `needs_token`。
+- 不再误用 OpenClaw 的 `VOC_TOKEN` / `OPENCLAW_VOC_TOKEN`。
+- provider 层会对错误信息做 token 脱敏。
+- 用户校准反馈会写入 `xiaohongshu-trend-memory.json`。
+- 生成下一份报告时可读取偏好记忆,并在报告中展示“已读取偏好”。
+- 有 token 后按小规模参数采集:
+  - `keywordLimit=1`
+  - `notesPerKeyword=1`
+  - `maxCommentPages=1`
+
+## 本地验证
+
+已验证:
+
+```bash
+npm --prefix claude-code-voc-intelligence run xhs:sample
+npm --prefix claude-code-voc-intelligence run xhs:preference-smoke
+npm --prefix claude-code-voc-intelligence run xhs:sample-with-memory
+npm --prefix claude-code-voc-intelligence run mcp:smoke
+node claude-code-voc-intelligence/mcp/src/tools/xiaohongshu-trend-run.js --collection-mode live --profile claude-code-voc-intelligence/memory-templates/xiaohongshu-trend-profile.json
+```
+
+sample / preference / MCP smoke 返回 `status=ok`,并生成:
+
+```text
+outputs/claude-code-xhs-sample/xiaohongshu-trend-report.md
+outputs/claude-code-xhs-sample/xiaohongshu-trend-report.json
+outputs/claude-code-xhs-memory-smoke/xiaohongshu-trend-memory.json
+outputs/claude-code-xhs-memory-smoke/report/xiaohongshu-trend-report.md
+outputs/claude-code-xhs-memory-smoke/report/xiaohongshu-trend-report.json
+outputs/claude-code-xhs-mcp-smoke/xiaohongshu-trend-report.md
+outputs/claude-code-xhs-mcp-smoke/xiaohongshu-trend-report.json
+```
+
+live 无 token 检查返回 `status=needs_token`,这是预期行为;需要有效小红书/TikHub token 后再做真实采集验收。
+
+## 打包交付
+
+已新增独立打包脚本:
+
+```text
+scripts/deploy/claude-code-voc-intelligence-suite.js
+```
+
+根目录命令:
+
+```bash
+npm run claude-voc:build
+npm run claude-voc:upload
+```
+
+npm 安装交付脚本:
+
+```bash
+npm run claude-voc:npm-pack
+npm run claude-voc:npm-publish
+```
+
+用户侧支持两种 npm 入口:
+
+```bash
+npm install -g @gangvy/claude-code-voc-intelligence
+claude-voc install
+```
+
+或:
+
+```bash
+npx @gangvy/claude-code-voc-intelligence install
+```
+
+两种方式都会安装到:
+
+```text
+~/.claude/plugins/voc-intelligence
+```
+
+`claude-voc:build` 会执行:
+
+1. 校验 Claude Code 插件文件、Skill 文件、MCP server 和 manifest。
+2. 排除 `node_modules/`、`memory/`、`.env` 后生成 zip。
+3. 解压到临时目录,运行 `node install.js --smoke`。
+4. 在解压后的包里自动跑:
+   - `npm run xhs:sample`
+   - `npm run xhs:preference-smoke`
+   - `npm run xhs:sample-with-memory`
+   - `npm run mcp:smoke`
+
+已验证通过,产物:
+
+```text
+dist/claude-code-voc-intelligence.zip
+dist/claude-code-voc-intelligence-suite-manifest.json
+```
+
+上传命令需要环境变量:
+
+```text
+QINIU_AK
+QINIU_SK
+```
+
+当前本机未检测到上传环境变量,因此本轮只完成本地打包和解压验证,未上传 CDN。
+
+用户拿到 zip 后的推荐安装命令:
+
+```bash
+node install.js
+```
+
+如需现场自检:
+
+```bash
+node install.js --smoke
+```
+
+`--smoke` 使用 `scripts/smoke-package.js` 做安静自检,只输出状态、工具列表、报告预览和产物路径,适合客户现场验证。
+
+## 最近一轮交付体验优化
+
+- 新增客户快速说明:`claude-code-voc-intelligence/docs/customer-quickstart.md`
+- 新增录屏脚本:`claude-code-voc-intelligence/docs/demo-runbook.md`
+- 报告正文新增“数据口径”,避免客户不知道样本来自哪里。
+- 报告正文新增“证据样本”,把高互动笔记和高赞评论直接展示出来。
+- 校准反馈回复从“工具更新成功”改为“已保存本次校准”,更贴近业务用户。
+

+ 1244 - 0
docs/specs/claude-code-skill-package-porting-analysis.md

@@ -0,0 +1,1244 @@
+# OpenClaw VOC 技能包迁移到 Claude Code 的技术改造分析
+
+更新时间:2026-05-21
+
+## 结论
+
+这些 OpenClaw 技能包可以迁移到 Claude Code 使用,但不能只把 `SKILL.md` 和 `api-config.json` 原样复制过去。
+
+原因是两边的职责模型不同:
+
+- OpenClaw 当前是 `SKILL.md + api-config.json + Node 脚本 + 打包安装脚本` 的组合。`api-config.json` 负责把自然语言触发映射到本地脚本或接口调用。
+- Claude Code 的 Skill 更偏向“可被 Claude 自动或手动触发的工作流说明”。它能包含脚本和参考文件,但不会天然理解 OpenClaw 的 `api-config.json`。
+- 如果要让 Claude Code 像 OpenClaw 一样稳定调用接口,建议新增 MCP 层,或者至少新增一层 Claude Code 专用的命令/脚本适配层。
+
+推荐路线:
+
+1. P0:先做轻量适配,保留现有 Node 脚本,新增 Claude Code Skill 和命令入口,让 Claude 可以通过自然语言或 `/skill-name` 触发完整流程。
+2. P1:做标准 MCP 适配器,把现有 `api-config.json` 里的参数 schema、执行方式、输出解析统一暴露成 Claude Code 可调用工具。
+3. P2:做 Claude Code Plugin,把 Skills、MCP、hooks、安装脚本、默认配置打成一个可分发套件。
+
+## 重要修正:不是替换 OpenClaw,而是新增 Claude Code 入口
+
+这里的目标不是把旧技能包“转型”成 Claude Code 专用包,而是在不破坏 OpenClaw 已有闭环的前提下,新增 Claude Code 可识别、可调用、可复用的入口。
+
+所以旧资产的处理原则是:
+
+- `api-config.json`:保留,继续给 OpenClaw 使用。
+- 现有 `SKILL.md`:保留,继续给 OpenClaw 使用;Claude Code 可以另建 `.claude/skills` 或插件 `skills`。
+- 现有 Node runner:保留命令行兼容,但内部建议抽出平台无关函数。
+- `install.js`、七牛 zip 包、OpenClaw 部署脚本:保留,不要因为 Claude Code 改造打断现有安装链路。
+- 新增 Claude Code 支持时,只在外围加适配层,不直接把 OpenClaw 的目录语义改掉。
+
+更准确的架构目标是:
+
+```text
+同一套业务核心
+  -> OpenClaw adapter
+  -> Claude Code adapter
+  -> MCP adapter
+  -> CLI adapter
+```
+
+## 新增 Claude Code 时,旧可复用部分需要重构什么
+
+### 1. Runner 脚本从 CLI-only 重构为 core + CLI wrapper
+
+当前观察:
+
+- `scripts/tools/douyin-speaking-daily-runner.js` 是完整 CLI 脚本,核心流程和命令行解析混在一起。
+- `scripts/tools/douyin-video-transcriber.js` 也是 CLI 主入口。
+- `industry-trend-intelligence/scripts/industry-trend-runner.js` 也是 CLI 主入口。
+- `scripts/tools/douyin-speaking-daily-report.js` 已有 `module.exports`,复用性更好。
+
+建议重构:
+
+```text
+scripts/core/
+  douyin-speaking-daily/
+    runDailyWorkflow.js
+    scriptCoCreation.js
+    transcriptQueue.js
+  douyin-video-transcript/
+    transcribeVideo.js
+  industry-trend/
+    runTrendWorkflow.js
+
+scripts/tools/
+  douyin-speaking-daily-runner.js       只负责 parse argv + 调 core + emit result
+  douyin-video-transcriber.js           只负责 parse argv + 调 core + emit result
+```
+
+这样 OpenClaw 继续执行旧命令,Claude Code / MCP 则可以直接 import core function。
+
+最小改法:
+
+```js
+async function runDailyWorkflow(options, context = {}) {
+  // 原 main 里的业务逻辑迁移到这里
+  return result;
+}
+
+async function main() {
+  const args = parseArgs(process.argv.slice(2));
+  const result = await runDailyWorkflow(args, createCliContext(args));
+  emitResult(args, result);
+}
+
+if (require.main === module) main();
+module.exports = { runDailyWorkflow };
+```
+
+这样不会破坏旧 CLI。
+
+### 2. `openclaw-tool-runner.js` 重构为平台中立 local-script runner
+
+当前 `scripts/tools/openclaw-tool-runner.js` 已经有很有价值的能力:
+
+- 解析 `--tool`。
+- 查找工具路径。
+- 过滤未替换的 `{{param}}`。
+- 执行子进程。
+- 提取 stdout 中的 JSON。
+- 再输出 `PREFIX=<json>`。
+
+这个逻辑不是 OpenClaw 独有,Claude Code 和 MCP 也能复用。
+
+建议:
+
+```text
+scripts/runtime/
+  local-script-runner.js
+  output-parser.js
+  template-args.js
+  tool-resolver.js
+
+scripts/tools/
+  openclaw-tool-runner.js       保留旧入口,内部调用 scripts/runtime/local-script-runner.js
+```
+
+Claude Code P0 可以直接复用这个 runtime,MCP P1 也可以复用它执行 `api-config.json`。
+
+### 3. `api-config.json` 读取逻辑抽成通用 tool manifest loader
+
+当前 `api-config.json` 是 OpenClaw 专用,但里面有大量可复用信息:
+
+- tool name
+- description
+- parameters JSON schema
+- execution command
+- args template
+- cwd
+- outputParser
+- output schema
+
+不需要立刻废弃它,而是新增读取器:
+
+```text
+scripts/runtime/
+  api-config-loader.js
+  tool-manifest-normalizer.js
+```
+
+作用:
+
+```text
+OpenClaw api-config.json
+  -> normalized tool manifest
+  -> OpenClaw 继续使用
+  -> Claude Code P0 wrapper 使用
+  -> MCP tool 注册使用
+```
+
+短期不要新增太多重复配置,否则会出现 OpenClaw 配一遍、Claude Code 再配一遍、MCP 又配一遍的维护灾难。
+
+### 4. 参数解析与类型转换抽成公共模块
+
+当前多个脚本都有自己的:
+
+- `parseArgs`
+- `bool`
+- `intValue`
+- `splitList`
+- `readJsonMaybe`
+- `hasConcreteArg`
+
+这些函数在 OpenClaw 和 Claude Code 下都会继续用。
+
+建议抽到:
+
+```text
+scripts/runtime/
+  args.js
+  values.js
+  json.js
+```
+
+好处:
+
+- Claude Code wrapper 不用复制一套参数处理。
+- MCP adapter 可以把 JSON 参数转成 CLI 参数。
+- 能统一处理数组、布尔、路径、空模板值 `{{xxx}}`。
+
+### 5. 输出协议抽成统一 result envelope
+
+当前比较好的设计是 `assistantMessage`,但各脚本返回字段还不完全统一。
+
+建议统一:
+
+```json
+{
+  "status": "ok",
+  "assistantMessage": "直接给用户看的正文",
+  "summary": {},
+  "nextAction": "",
+  "outputDir": "",
+  "files": [],
+  "warnings": [],
+  "errors": []
+}
+```
+
+新增:
+
+```text
+scripts/runtime/
+  result-envelope.js
+```
+
+所有旧 runner 逐步改成:
+
+```js
+return okResult({
+  assistantMessage,
+  summary,
+  files,
+  nextAction
+});
+```
+
+OpenClaw 仍然显示 `assistantMessage`,Claude Code 也能直接显示正文。
+
+### 6. Credential / token 读取抽成平台无关模块
+
+当前脚本里存在这些假设:
+
+- `VOC_TOKEN`
+- `OPENCLAW_VOC_TOKEN`
+- `VOC_SOCIAL_TOKEN`
+- `~/.openclaw/voc-credentials.json`
+
+新增 Claude Code 后,不建议在业务脚本里继续写死 `.openclaw`。
+
+建议:
+
+```text
+scripts/runtime/
+  credentials.js
+```
+
+读取顺序:
+
+```text
+1. 显式参数 --voc-token / context.vocToken
+2. 环境变量 VOC_TOKEN / VOC_SOCIAL_TOKEN / OPENCLAW_VOC_TOKEN
+3. 项目 memory 或 config
+4. ~/.openclaw/voc-credentials.json
+5. ~/.claude/voc-credentials.json
+6. Claude Code plugin data dir
+```
+
+旧 OpenClaw 不受影响,Claude Code 可以新增自己的 credential 位置。
+
+### 7. Workspace / memory / output root 抽成运行上下文
+
+当前大量默认路径依赖 `process.cwd()`:
+
+- `memory/...`
+- `outputs/...`
+- `openclaw-voc-output/...`
+
+Claude Code 下 cwd 可能是用户项目,插件目录,或 repo 根目录。
+
+建议每个核心函数都接受:
+
+```js
+context = {
+  workspaceRoot,
+  memoryRoot,
+  outputRoot,
+  platform: 'openclaw' | 'claude-code' | 'cli' | 'mcp'
+}
+```
+
+CLI wrapper 默认:
+
+```js
+workspaceRoot = process.cwd()
+memoryRoot = path.join(workspaceRoot, 'memory')
+outputRoot = path.join(workspaceRoot, 'outputs')
+```
+
+OpenClaw 和 Claude Code adapter 可以覆盖。
+
+### 8. 社媒 API client 抽成可复用 client
+
+当前抖音、小红书的实际接口调用散落在多个工具和 runner 里。
+
+新增 Claude Code 后,低层 API 最适合变成 MCP tools,所以要先抽 client:
+
+```text
+scripts/clients/
+  voc-social-client.js
+  douyin-client.js
+  xiaohongshu-client.js
+  transcription-gateway-client.js
+```
+
+业务 runner 不直接拼接口 URL,而是调用 client。
+
+收益:
+
+- OpenClaw runner 用同一 client。
+- Claude Code MCP tool 用同一 client。
+- 错误码、403、余额不足、接口异常可以统一处理。
+
+### 9. 采集、报告、共创、记忆要拆开
+
+以抖音日报为例,现在 runner 已经能做很多事:
+
+- 建档
+- 采集
+- 评论
+- 自动逐字稿
+- 报告
+- 选题
+- 脚本共创
+- 定稿
+- 偏好记忆
+
+功能闭环是好事,但 Claude Code 接入后,建议内部拆成模块:
+
+```text
+profile/
+collection/
+transcript/
+report/
+script-co-create/
+memory/
+```
+
+对外还是一个 runner,对内更清楚。这样 Claude Code 如果只想调用“逐字稿”或“脚本共创”,不用跑完整日报。
+
+### 10. 中文文案、编码、正则需要治理
+
+当前 Windows / PowerShell 输出里已经多次出现乱码风险。新增 Claude Code 后,路径、stdout、JSON、中文 prompt 都会经过更多层。
+
+建议:
+
+- 所有源码统一 UTF-8。
+- JSON 输出不要混杂非结构化中文日志。
+- 用户可见文案集中放到 message builder,不散落在采集逻辑里。
+- 少用硬编码中文正则判断复杂意图,优先把自然语言交给 Skill 层判断,再传结构化参数给 runner。
+
+## 哪些旧部分不要重构
+
+### 不要动 OpenClaw 已跑通的外部接口
+
+这些先保留:
+
+```text
+douyin/*/api-config.json
+industry-trend-intelligence/skills/*/api-config.json
+xiaohongshu/*/api-config.json
+```
+
+原因:
+
+- OpenClaw 线上包依赖它们。
+- 七牛 zip 包安装链路依赖它们。
+- 用户教学视频和现有说明文档也依赖它们。
+
+### 不要一开始就改技能包目录名
+
+保留:
+
+```text
+douyin/douyin-speaking-daily-runner
+industry-trend-intelligence/skills/industry-trend-runner
+```
+
+新增 Claude Code 目录即可:
+
+```text
+.claude/skills/...
+claude-code-adapter/...
+```
+
+### 不要把所有低层 API 都做成 Claude Skill
+
+低层 API 应该是工具,不是用户入口。
+
+保持用户入口少而清晰:
+
+- 抖音口播日报
+- 抖音视频逐字稿
+- 行业趋势情报官
+- 小红书趋势采集
+
+低层能力放到 MCP tools:
+
+- 搜索笔记
+- 获取评论
+- 获取视频详情
+- 获取用户作品
+
+## 官方能力对应
+
+参考 Claude Code 官方文档:
+
+- Skills:[Extend Claude with skills](https://docs.claude.com/en/docs/claude-code/skills)。`~/.claude/skills/<skill-name>/SKILL.md`、项目 `.claude/skills/<skill-name>/SKILL.md`、插件 `skills/<skill-name>/SKILL.md` 都可以被 Claude Code 识别;技能可以被自然语言触发,也可以用 `/skill-name` 直接调用。
+- Skills 支持 `description`、`allowed-tools`、`disable-model-invocation`、`arguments`、`${CLAUDE_SKILL_DIR}` 等字段,适合做工作流入口和上下文说明。
+- Slash commands:[Common workflows - custom slash commands](https://docs.claude.com/en/docs/claude-code/common-workflows)。`.claude/commands/*.md` 适合承接高频固定入口,例如 `/douyin-daily`、`/trend-daily`,但复杂能力仍应沉淀为 Skill 或 MCP tool。
+- MCP:[Connect Claude Code to tools via MCP](https://docs.claude.com/en/docs/claude-code/mcp)。Claude Code 可以连接本地或远程 MCP server,把外部 API、数据库、本地工具暴露成可调用工具。
+- Plugin:[Create plugins](https://docs.claude.com/en/docs/claude-code/plugins)。Claude Code Plugin 可以把 `skills/`、`commands/`、`hooks/`、`.mcp.json`、`bin/`、`settings.json` 等放在同一个插件包里统一分发。
+- Hooks:[Hooks reference](https://docs.claude.com/en/docs/claude-code/hooks)。可在 Claude Code 生命周期节点自动执行 shell、HTTP 或 LLM prompt,适合做 token 检查、环境预检、运行后归档等自动动作。
+
+对应到我们的技能包,最重要的判断是:
+
+> Skill 负责“什么时候用、怎么协作、如何输出”;MCP 或脚本适配器负责“真正调用哪个接口、传什么参数、怎么解析结果”。
+
+## 当前项目结构观察
+
+本仓库目前有三类主要技能资产:
+
+```text
+douyin/
+  douyin-speaking-daily-runner/
+  douyin-video-transcript/
+  douyin-general-search/
+  douyin-video-comments/
+  ...
+
+industry-trend-intelligence/
+  skills/
+    industry-trend-runner/
+    industry-trend-profile-builder/
+    xiaohongshu-search-notes/
+    xiaohongshu-note-detail/
+    xiaohongshu-note-comments/
+    ...
+  scripts/
+  install.js
+  skill-package-manifest.json
+
+xiaohongshu/
+  xiaohongshu-search-notes/
+  xiaohongshu-note-detail/
+  xiaohongshu-note-comments/
+  ...
+```
+
+典型 OpenClaw 技能目录结构:
+
+```text
+skill-name/
+  SKILL.md
+  api-config.json
+```
+
+典型 `api-config.json` 结构:
+
+```json
+{
+  "name": "douyin-speaking-daily-runner",
+  "type": "local-script",
+  "execution": {
+    "kind": "command",
+    "command": "node",
+    "args": [
+      "scripts/tools/douyin-speaking-daily-runner.js",
+      "--result-prefix",
+      "DOUYIN_SPEAKING_DAILY_RUNNER_RESULT",
+      "--profile",
+      "{{profile}}"
+    ],
+    "cwd": "{{workspaceRoot}}",
+    "outputParser": {
+      "type": "stdout-prefix-json",
+      "prefix": "DOUYIN_SPEAKING_DAILY_RUNNER_RESULT="
+    }
+  },
+  "parameters": {
+    "type": "object",
+    "properties": {}
+  },
+  "output": {
+    "type": "object",
+    "properties": {
+      "assistantMessage": { "type": "string" }
+    }
+  }
+}
+```
+
+这套结构在 OpenClaw 里是完整的,但在 Claude Code 里缺少三个东西:
+
+1. Claude Code 可发现的 Skill 目录:`.claude/skills/<name>/SKILL.md` 或插件 `skills/<name>/SKILL.md`。
+2. Claude Code 可执行的工具桥:MCP server,或明确写在 Skill 里的脚本调用说明。
+3. Claude Code 可安装的包结构:插件目录、`.mcp.json`、`.claude-plugin/plugin.json` 或项目级 `.claude/skills`。
+
+## 架构改造目标
+
+建议把现有 OpenClaw 技能包重构成五层:
+
+```text
+自然语言入口层
+  用户说“启动抖音口播每日稿件日报”
+  用户说“给我看小红书家装趋势日报”
+
+Skill 协作层
+  Claude Code SKILL.md
+  负责角色、触发词、工作流、输出要求、交互策略
+
+工具调用层
+  P0:Node wrapper
+  P1:MCP server
+  负责调用 API、本地脚本、上传下载、转写、报告生成
+
+业务脚本层
+  复用现有 scripts/tools/*.js
+  复用 industry-trend-intelligence/scripts/*.js
+
+记忆与产物层
+  memory/*.json
+  outputs/*
+  report markdown/json
+  transcript cache
+```
+
+这样迁移后,同一套底层业务脚本可以同时服务:
+
+- OpenClaw
+- Claude Code
+- 未来其他 Agent 平台
+- 直接命令行调用
+
+## P0 轻量迁移方案
+
+目标:最快让 Claude Code 可识别、可运行、可自然语言协作。
+
+做法:
+
+```text
+.claude/
+  skills/
+    douyin-speaking-daily/
+      SKILL.md
+      examples/
+        startup.md
+        script-co-create.md
+      scripts/
+        run-douyin-speaking-daily.ps1
+    industry-trend-intelligence/
+      SKILL.md
+      examples/
+        home-customization.md
+        generic-trend.md
+      scripts/
+        run-industry-trend.ps1
+```
+
+P0 的 Skill 不需要把所有 API 都写进去,只需要写清:
+
+- 什么场景触发。
+- 如何读取已有记忆。
+- 如何调用现有 Node 脚本。
+- 输出优先展示 `assistantMessage`,文件路径只放末尾。
+- 用户继续说“选题2”“定稿”“不要工具清单方向”时如何续接最近一次运行。
+
+示例:
+
+```markdown
+---
+name: douyin-speaking-daily
+description: 生成抖音口播每日稿件日报,支持爆款选题、逐字稿补跑、脚本共创、定稿和偏好沉淀。用户说启动日报、生成口播选题、进入脚本共创、定稿时使用。
+allowed-tools: Bash Read Write
+---
+
+## 工作流
+
+1. 优先读取 `memory/douyin-speaking-profile.json`。
+2. 如果没有档案,用自然语言向用户建档,不展示专业参数。
+3. 调用 `node scripts/tools/douyin-speaking-daily-runner.js`。
+4. 优先把返回 JSON 里的 `assistantMessage` 原样发给用户。
+5. 不要只返回文件路径。
+```
+
+优点:
+
+- 改造少。
+- 现有脚本几乎不用动。
+- 可以快速演示 Claude Code 版技能入口。
+
+缺点:
+
+- Claude 仍然是通过 shell 间接调用工具,不是原生结构化工具调用。
+- 参数校验、错误处理、工具列表发现不够稳定。
+- 多技能组合时,Claude 需要读说明后决定该跑哪个脚本。
+
+适用范围:
+
+- 课程演示。
+- 单机部署。
+- 小范围内部测试。
+- 先验证 Claude Code 是否能承接 OpenClaw 工作流体验。
+
+## P1 标准迁移方案:MCP 适配器
+
+目标:让 Claude Code 像调用内置工具一样调用我们的 VOC 技能接口。
+
+新增:
+
+```text
+claude-code-adapter/
+  mcp/
+    package.json
+    src/
+      server.js
+      load-api-configs.js
+      local-script-tool.js
+      http-endpoint-tool.js
+      output-parser.js
+      credential-store.js
+      workspace-resolver.js
+  skills/
+    douyin-speaking-daily/
+      SKILL.md
+    industry-trend-intelligence/
+      SKILL.md
+  .mcp.json
+  README.md
+```
+
+核心思路:
+
+1. 扫描现有 `api-config.json`。
+2. 把每个 `api-config.json` 转成一个 MCP tool。
+3. 把 `parameters` 映射成 MCP input schema。
+4. 根据 `execution.kind` 决定调用本地命令还是 HTTP 接口。
+5. 根据 `outputParser` 解析 stdout 或 HTTP response。
+6. 把结构化结果返回给 Claude Code。
+
+映射关系:
+
+| OpenClaw 字段 | Claude Code / MCP 对应 |
+| --- | --- |
+| `name` | MCP tool name |
+| `description` | MCP tool description |
+| `parameters` | MCP input schema |
+| `execution.command` | 本地进程 spawn |
+| `execution.args` | 参数模板渲染 |
+| `execution.cwd` | workspace root resolver |
+| `outputParser.type=stdout-prefix-json` | stdout 前缀 JSON 解析器 |
+| `output.properties.assistantMessage` | Claude 聊天优先展示正文 |
+| `relatedSkills` | Claude Skill 内的工作流路由说明 |
+
+MCP 工具命名建议:
+
+```text
+voc_douyin_speaking_daily_run
+voc_douyin_video_transcript
+voc_douyin_general_search
+voc_xiaohongshu_search_notes
+voc_xiaohongshu_note_comments
+voc_industry_trend_run
+voc_industry_trend_profile_build
+```
+
+Claude Code Skill 里只保留高层协作说明:
+
+```markdown
+当用户要“启动日报”时:
+1. 调用 `voc_douyin_speaking_daily_run`。
+2. 如果返回 `needs_profile`,引导建档。
+3. 如果返回 `needs_account_confirmation`,展示候选账号,不要自行确认。
+4. 如果返回 `assistantMessage`,直接展示正文。
+5. 如果用户选择选题,继续调用同一个 runner 的 `selectedTopicIndex` 或 `message`。
+```
+
+优点:
+
+- Claude Code 能真正“看见”工具。
+- 参数 schema 可被工具层校验。
+- 多技能组合稳定,不依赖模型猜命令。
+- 更适合后续扩展到小红书、抖音、微博、知乎等通用情报官。
+
+缺点:
+
+- 需要开发 MCP server。
+- 需要处理 Windows 路径、中文编码、token、网络异常、输出长度限制。
+
+## P2 产品化迁移方案:Claude Code Plugin
+
+目标:把 VOC 技能包变成 Claude Code 可安装、可复用、可分发的插件套件。
+
+推荐目录:
+
+```text
+claude-code-plugin-voc-intelligence/
+  .claude-plugin/
+    plugin.json
+  skills/
+    douyin-speaking-daily/
+      SKILL.md
+    douyin-video-transcript/
+      SKILL.md
+    industry-trend-intelligence/
+      SKILL.md
+    social-trend-intelligence/
+      SKILL.md
+  .mcp.json
+  mcp/
+    package.json
+    src/
+      server.js
+      ...
+  bin/
+    voc-runner.cmd
+    voc-runner.ps1
+  hooks/
+    hooks.json
+    preflight-token-check.js
+  settings.json
+  README.md
+```
+
+插件化后可以做到:
+
+- 用户安装一个插件,就获得一组技能。
+- MCP server 随插件自动启动。
+- `bin/` 里的命令在插件启用时进入 Claude Code 的 Bash 路径。
+- hooks 自动检查 token、Node 版本、余额、输出目录。
+- 后续可拆成多个插件:
+  - `voc-douyin-speaking`
+  - `voc-social-trend`
+  - `voc-xiaohongshu`
+  - `voc-transcript`
+
+## 需要重构的设计点
+
+### 1. 技能边界重构
+
+当前 OpenClaw 里每个 API 都可以是一个技能,例如:
+
+- `douyin-general-search`
+- `douyin-video-comments`
+- `xiaohongshu-search-notes`
+- `xiaohongshu-note-comments`
+
+迁移到 Claude Code 后,不建议把每个低层 API 都做成用户可见 Skill。
+
+建议:
+
+- 面向用户的 Skill 保持少量高层入口。
+- 低层 API 统一变成 MCP tools。
+
+推荐用户可见 Skill:
+
+```text
+douyin-speaking-daily
+douyin-video-transcript
+industry-trend-intelligence
+social-trend-intelligence
+```
+
+推荐 MCP tools:
+
+```text
+douyin_general_search
+douyin_video_detail
+douyin_video_comments
+douyin_user_posts
+douyin_video_transcript
+xiaohongshu_search_notes
+xiaohongshu_note_detail
+xiaohongshu_note_comments
+industry_trend_runner
+```
+
+### 2. `api-config.json` 平台中立化
+
+当前 `api-config.json` 是 OpenClaw 专用。建议新增一个平台中立的 `tool.schema.json`,或者让 MCP adapter 兼容读取 `api-config.json`。
+
+推荐先兼容读取,后续再演进:
+
+```text
+api-config.json              继续给 OpenClaw 用
+tool.schema.json             后续平台中立版本
+claude-tool.config.json      如需 Claude 特定覆盖再加
+```
+
+短期不建议马上删除 `api-config.json`,因为 OpenClaw 仍要继续跑。
+
+### 3. 脚本入口标准化
+
+所有脚本都应支持:
+
+```text
+--workspace-root
+--result-prefix
+--output
+--collection-mode sample|live
+--message
+```
+
+输出必须稳定:
+
+```text
+RESULT_PREFIX={"status":"ok","assistantMessage":"...","files":[]}
+```
+
+对 Claude Code 来说,最重要的是:
+
+- 不要只写文件。
+- 一定返回 `assistantMessage`。
+- 错误也要结构化返回,而不是只打印 stack trace。
+- Windows 中文路径不能影响运行。
+
+### 4. 记忆系统统一
+
+目前 OpenClaw 默认用:
+
+```text
+memory/douyin-speaking-profile.json
+memory/douyin-speaking-script-memory.json
+memory/douyin-speaking-history.json
+memory/industry-trend-memory.json
+```
+
+Claude Code 迁移时有两种选择:
+
+方案 A:继续使用项目内 `memory/`。
+
+适合:
+
+- 单项目。
+- 课程演示。
+- 用户能理解“这个项目目录就是工作空间”。
+
+方案 B:插件持久化目录。
+
+适合:
+
+- 多项目共享。
+- 真实产品分发。
+- 不希望每个项目重复建档。
+
+建议:
+
+```text
+P0:继续使用项目 memory/
+P1:MCP adapter 支持 --memory-root
+P2:Plugin 使用 ${CLAUDE_PLUGIN_DATA}/memory,并允许项目级覆盖
+```
+
+### 5. 自然语言启动话术固化
+
+OpenClaw 里已经优化过“不要展示专业参数”的启动方式。Claude Code Skill 也要继承这个设计。
+
+用户入口话术示例:
+
+```text
+启动抖音口播每日稿件日报。
+先读取已有账号档案,如果没有就帮我建档。
+今天帮我采集抖音爆款口播选题,自动补一条最值得拆的视频逐字稿。
+最后直接在聊天里给我日报,不要只给文件路径。
+```
+
+Claude Code Skill 应把这类话术作为默认入口,而不是让用户输入:
+
+```text
+profile=...
+autoTranscriptTopN=1
+autoTranscriptProvider=iflytek-gateway
+```
+
+### 6. 权限与安全重构
+
+需要区分:
+
+- 只读查询。
+- 会消耗余额的 API。
+- 会上传音视频的转写。
+- 会写入长期记忆的定稿。
+
+建议:
+
+```text
+P0:Skill 文档里写清楚何时需要用户确认。
+P1:MCP tool 增加 budget、dryRun、collectionMode。
+P2:Hook 做运行前检查和余额提醒。
+```
+
+特别是:
+
+- `autoTranscriptTopN > 0` 会消耗转写额度。
+- 直播采集和批量搜索会消耗 VOC-AI 服务次数。
+- `finalize=true` 会写入长期记忆。
+
+### 7. 输出协议重构
+
+Claude Code 的体验要沿用我们已经验证过的内容优先原则:
+
+```text
+优先展示 assistantMessage
+其次展示关键摘要
+最后才展示文件路径
+```
+
+所有 runner 输出建议统一:
+
+```json
+{
+  "status": "ok",
+  "assistantMessage": "直接给用户看的正文",
+  "summary": {},
+  "nextAction": "建议用户下一步怎么说",
+  "files": [],
+  "warnings": [],
+  "errors": []
+}
+```
+
+## 文件级改造清单
+
+### 必须新增
+
+```text
+docs/specs/claude-code-skill-package-porting-analysis.md
+
+.claude/
+  skills/
+    douyin-speaking-daily/
+      SKILL.md
+    douyin-video-transcript/
+      SKILL.md
+    industry-trend-intelligence/
+      SKILL.md
+
+claude-code-adapter/
+  README.md
+  mcp/
+    package.json
+    src/
+      server.js
+      load-api-configs.js
+      local-script-tool.js
+      http-endpoint-tool.js
+      output-parser.js
+      credential-store.js
+      workspace-resolver.js
+```
+
+### 建议新增
+
+```text
+claude-code-adapter/
+  skills/
+    douyin-speaking-daily/
+      SKILL.md
+      examples.md
+    industry-trend-intelligence/
+      SKILL.md
+      examples.md
+  tests/
+    api-config-loader.test.js
+    output-parser.test.js
+    local-script-tool.test.js
+  fixtures/
+    douyin-speaking-daily-runner.api-config.json
+    industry-trend-runner.api-config.json
+```
+
+### 后续插件化新增
+
+```text
+claude-code-plugin-voc-intelligence/
+  .claude-plugin/
+    plugin.json
+  skills/
+  .mcp.json
+  mcp/
+  bin/
+  hooks/
+  settings.json
+```
+
+### 需要调整现有脚本
+
+```text
+scripts/tools/douyin-speaking-daily-runner.js
+scripts/tools/douyin-video-transcriber.js
+industry-trend-intelligence/scripts/industry-trend-runner.js
+industry-trend-intelligence/scripts/industry-trend-profile-builder.js
+industry-trend-intelligence/scripts/industry-trend-memory.js
+industry-trend-intelligence/scripts/xiaohongshu-trend-collector.js
+```
+
+调整重点:
+
+- 支持显式 `--workspace-root`。
+- 标准化 `--result-prefix`。
+- 标准化错误 JSON。
+- 保证 `assistantMessage` 永远优先返回。
+- 兼容 Claude Code 项目目录和插件目录。
+
+## 推荐实施顺序
+
+### 第一步:Claude Code P0 适配,先跑一个闭环
+
+范围:
+
+- `industry-trend-intelligence`
+- `douyin-speaking-daily`
+
+交付:
+
+```text
+.claude/skills/industry-trend-intelligence/SKILL.md
+.claude/skills/douyin-speaking-daily/SKILL.md
+```
+
+测试目标:
+
+```text
+用户说:启动抖音口播每日稿件日报
+Claude 能调用 runner
+Claude 直接展示日报正文
+用户说:选题2,进入脚本共创
+Claude 能续接最近日报
+用户说:定稿
+Claude 能写入偏好记忆
+```
+
+### 第二步:MCP adapter 骨架
+
+范围:
+
+- 先支持 `local-script` 类型。
+- 先支持 stdout-prefix-json。
+- 先接入 2 个 runner:
+  - `douyin-speaking-daily-runner`
+  - `industry-trend-runner`
+
+验收:
+
+```text
+Claude Code /mcp 可以看到 voc 工具
+工具可以返回 assistantMessage
+错误能结构化返回
+```
+
+### 第三步:MCP adapter 扩展到社媒 API
+
+范围:
+
+- 小红书搜索、笔记详情、评论。
+- 抖音搜索、视频详情、评论、用户作品。
+
+目标:
+
+```text
+Claude 不再靠 shell 拼命令。
+Claude 能组合多个工具完成“通用社媒趋势情报官”。
+```
+
+### 第四步:插件化打包
+
+范围:
+
+```text
+claude-code-plugin-voc-intelligence/
+```
+
+交付:
+
+- 插件 manifest。
+- skills。
+- MCP 配置。
+- hooks。
+- 安装说明。
+- smoke test。
+
+## P0、P1、P2 优先级
+
+### P0 必做
+
+- 新增 Claude Code Skill 入口。
+- 复用现有 Node runner。
+- 输出必须显示正文,不只给文件路径。
+- 自然语言启动话术,不暴露专业参数。
+- 能跑通日报、选题、脚本共创、定稿记忆。
+
+### P1 必做
+
+- MCP adapter 读取 `api-config.json`。
+- 参数 schema 自动映射。
+- 本地脚本工具化。
+- 错误结构化。
+- token 和余额检查。
+- Windows 中文路径兼容。
+
+### P2 可做
+
+- Claude Code Plugin 套件。
+- hooks 自动预检。
+- 多平台通用社媒情报官。
+- 插件市场式分发。
+- 独立版本号、更新检测、回滚机制。
+
+## 迁移风险
+
+### 风险 1:只迁 SKILL.md,工具不可用
+
+Claude Code 能识别 Skill,不等于能识别 OpenClaw 的 `api-config.json`。如果没有脚本调用说明或 MCP adapter,Claude 只能读说明,无法稳定执行工具。
+
+### 风险 2:低层 API 全做成 Skill,用户入口变乱
+
+不要把 `douyin-video-comments`、`xiaohongshu-note-detail` 这种低层能力暴露成用户主要入口。用户应该看到“日报”“趋势情报”“逐字稿”“脚本共创”这类任务入口。
+
+### 风险 3:输出太偏文件系统
+
+Claude Code 用户也不应该只看到文件路径。所有核心 runner 都要返回 `assistantMessage`。
+
+### 风险 4:路径和编码问题
+
+当前 PowerShell 输出中已经出现过中文乱码。迁移时要统一:
+
+```text
+UTF-8
+绝对路径
+path.resolve
+不要手拼 Windows 反斜杠
+```
+
+### 风险 5:消耗型任务缺少预算控制
+
+逐字稿、社媒采集、批量评论抓取都可能消耗额度。MCP tool 参数里应保留:
+
+```text
+dryRun
+collectionMode
+dailyBudget
+autoTranscriptTopN
+maxCommentPages
+```
+
+## 建议的目标形态
+
+最终不是“OpenClaw 技能包复制成 Claude Code 技能包”,而是沉淀成一套平台中立 VOC Agent Kit:
+
+```text
+voc-agent-kit/
+  core/
+    scripts/
+    schemas/
+    runners/
+  openclaw/
+    skills/
+    api-config.json
+    install.js
+  claude-code/
+    skills/
+    mcp/
+    plugin/
+  docs/
+    user-guides/
+    developer-guides/
+```
+
+这样同一个底层能力可以有多套外壳:
+
+- OpenClaw 外壳:继续使用 `api-config.json`。
+- Claude Code 外壳:使用 Skill + MCP + Plugin。
+- 命令行外壳:直接运行 Node。
+- 后续 Web 外壳:调用同一套 runner。
+
+## 最小可落地版本
+
+如果要最快开始,我建议先做这个版本:
+
+```text
+.claude/skills/douyin-speaking-daily/SKILL.md
+.claude/skills/industry-trend-intelligence/SKILL.md
+claude-code-adapter/scripts/run-openclaw-local-script.js
+claude-code-adapter/tests/output-parser.test.js
+```
+
+`run-openclaw-local-script.js` 做三件事:
+
+1. 读取指定 `api-config.json`。
+2. 用传入 JSON 参数渲染 `{{param}}`。
+3. 执行命令并解析 stdout prefix JSON。
+
+这样可以不立刻写完整 MCP,但已经把 OpenClaw 的关键执行协议抽象出来,为 P1 MCP 迁移铺路。
+
+## 验收标准
+
+### 用户体验验收
+
+用户可以这样说:
+
+```text
+启动抖音口播每日稿件日报。
+```
+
+Claude Code 应返回:
+
+```text
+今日爆款选题池
+Top 5 选题
+逐字稿状态
+下一步怎么选题/共创/定稿
+```
+
+用户可以继续说:
+
+```text
+选题2,进入脚本共创。
+开头更狠一点,老板视角。
+定稿。
+不要纯工具清单方向。
+```
+
+Claude Code 应能连续完成:
+
+- 进入脚本共创。
+- 生成 v0。
+- 根据反馈生成 v1。
+- 定稿写入记忆。
+- 保存偏好并影响下次日报。
+
+### 技术验收
+
+- Claude Code 能发现 Skill。
+- 本地脚本可以被调用。
+- MCP P1 后,Claude Code 能发现 VOC MCP tools。
+- runner 输出结构化 JSON。
+- `assistantMessage` 被优先展示。
+- 失败时返回可读错误和下一步建议。
+- sample 模式和 live 模式可区分。
+
+## 我的建议
+
+不要先做“大一统插件”。先做 P0 轻量适配和 P1 MCP 骨架。
+
+顺序应该是:
+
+1. 先用 `.claude/skills` 证明 Claude Code 的用户工作流能跑通。
+2. 再把 `api-config.json` 抽象成通用工具执行器。
+3. 再升级成 MCP。
+4. 最后做插件化分发。
+
+这样风险最低,也不会影响 OpenClaw 当前已经跑通的技能包。

+ 266 - 0
docs/specs/douyin-speaking-daily-p1-optimization-spec.md

@@ -0,0 +1,266 @@
+# douyin-speaking-daily P1 优化 Spec
+
+日期:2026-05-10  
+对象:`douyin-speaking-daily` 抖音口播每日稿件日报技能包  
+状态:In Progress,P1.1-P1.6 已进入可测闭环,准备进行 OpenClaw 教学视频全流程测试。
+
+## 1. P1 定义
+
+P0 已经证明“能跑通”:建档、关键词/账号采集、评论采集、可选逐字稿、日报输出、套件打包部署都已形成闭环。
+
+P1 的目标不是继续堆数据,而是把它优化成面向真实用户交付的“每日选题 + 脚本共创”系统:
+
+1. 日报阶段只展示热点选题、趋势信号、证据强弱和下一步,不把完整长稿堆给用户。
+2. 用户选择某条选题后,进入脚本共创,会产出可拍摄口播稿,而不是只给拆解框架。
+3. 脚本可以多轮修改、确认定稿,并把偏好沉淀到记忆里。
+4. 后续运行要能读取历史偏好,使成稿越来越贴合用户,而不是每次从零开始。
+5. 任何低证据内容都必须显示风险和质量门禁,不把猜测包装成结论。
+
+一句话验收标准:
+
+> 用户每天启动一次后,先拿到可选择的热点选题;选择选题后,OpenClaw 能继续多轮共创并给出最终可拍摄稿件,同时把用户偏好写入记忆,影响下一次选题和成稿。
+
+## 2. 交付场景
+
+### 场景 A:每日选题雷达
+
+用户启动 `douyin-speaking-daily-runner` 后,系统采集样本并直接在对话里输出:
+
+- 今日一句话判断
+- 样本量、评论量、逐字稿数
+- 质量门禁
+- Top 5 热点选题
+- 每条选题的来源、互动数据、置信度、是否有逐字稿
+- 下一步口令,例如“选题 2,进入脚本共创”
+
+完整文件仍保存在本地,但对话优先展示 `assistantMessage` 正文,不能只告诉用户文件路径。
+
+### 场景 B:选题后脚本共创
+
+用户回复“选题 2,进入脚本共创”后:
+
+1. 如果该选题已有逐字稿,直接生成 v0 完整脚本。
+2. 如果没有逐字稿,也要生成“结构推断稿” v0,并明确风险。
+3. 如果视频超出网关能力,不上传导致 413;返回“需分段/换短视频/换 provider”的原因。
+4. 用户继续反馈“开头更狠、老板视角、少讲概念、多给案例”后,生成 v1/v2。
+5. 用户确认定稿后,写入脚本记忆。
+
+### 场景 C:单条爆款视频改写
+
+用户给一个抖音爆款视频链接,系统解析/下载/转写后,输出:
+
+- 逐字稿路径和摘要
+- 爆款结构拆解
+- 可迁移打法
+- 面向用户账号方向的初稿
+- 可继续修改的校准问题
+
+## 3. 当前实现进度
+
+### P1.1 稳定性与成本控制:已完成第一阶段
+
+已实现:
+
+- `runner` 支持 `--transcript-cache`
+- 支持 `--transcript-reuse-cache`
+- 支持 `--transcript-daily-budget`
+- 输出 `transcript-queue.json`
+- `summary/audit.json` 标注 cached/generated/queue 计数
+- `iflytek-gateway` 增加默认 8 分钟时长保护,超长视频先返回 `needs_media_processing`,避免上传后 413
+
+仍待优化:
+
+- 超长视频自动分段转写
+- 更细的 ASR 成本预估
+- 更稳定的跨天 transcript cache 清理策略
+
+### P1.2 脚本共创交付闭环:已完成第一阶段
+
+已实现:
+
+- `daily-report` 支持 `selectedTopicIndex`
+- `runner` 透传 `selectedTopicIndex/scriptSession/feedback/finalize/scriptMemory`
+- 选题后生成 `script-session.json`
+- 生成 `selected-topic-<n>-script.md/json`
+- 支持 v0 初稿、feedback 多轮改稿、finalize 定稿
+- 定稿后写入 `memory/douyin-speaking-script-memory.json`
+- OpenClaw 对话优先返回 `assistantMessage` 正文
+- 没有逐字稿时仍生成“结构推断稿”,并标注低置信风险
+- 定稿后生成动态发布辅助,包括标题备选、拍摄提示、评论区引导、30 秒压缩版和 60 秒提词器版
+- 脚本生成已抽象账号交付语境,按 profile 自动适配 B2B、电商、教育、本地生活或通用账号,避免默认绑定 `voc.market` 或固定 VOC 话术
+- 支持 `message=<用户原话>` 自然话术入口,例如“选题2”“改稿:开头更狠”“定稿”
+- 未显式传 `input/scriptSession` 时,会自动续接最近一次 `runner-raw-input.json` 和 `script-session.json`
+- 定稿前输出脚本自检,检查测试项目词泄漏、过泛、缺少场景、缺少互动或转化动作
+
+仍待优化:
+
+- 90 秒展开版和镜头分镜版
+- 增加镜头提示、字幕钩子、口播提词器版本
+- 更细的行业模板,例如医疗健康、金融、招聘、本地门店等
+
+### P1.3 历史记忆与反馈权重:已完成第一阶段
+
+已实现:
+
+- 定稿写入脚本记忆
+- 下一次脚本共创会读取 `scriptMemory`
+- 历史偏好可影响 v0 初稿,例如老板视角、强开头、产品转化、禁用泛泛表达
+- `assistantMessage` 会显示“历史偏好”提示
+- 返回结果包含 `scriptMemorySignals`
+- 每次日报/共创运行写入 `memory/douyin-speaking-history.json`
+- history 记录 Top 选题、样本概况、质量门禁、是否进入共创、脚本版本和是否定稿
+- runner 透传 `historyMemory/writeHistory`,便于 OpenClaw 部署后统一沉淀跨次运行
+- 用户说“保留1、2”“不要某方向”时写入选题偏好记忆
+- 日报排序会读取 `topicPreferences.keepPhrases/dropPhrases`,对保留方向加权、对禁区方向降权
+- 已定稿过的同一 `awemeId` 会降权,避免反复推荐同一条素材
+
+仍待实现:
+
+- 7/30 天趋势信号
+- 更精细的 liked/disliked 权重,例如按主题、作者、钩子类型拆开加权
+
+### P1.4 质量门禁与审计:已完成第一阶段
+
+已实现:
+
+- `daily-report.json` 增加 `qualityGate`
+- `audit.json` 增加 `qualityGate`
+- runner/result 返回 `qualityGate`
+- 对 profile 缺字段、样本不足、评论不足、逐字稿不足给出 `warn/fail/pass`
+- 日报和脚本共创顶部显示质量门禁
+- `qualityGate.level=fail` 时阻断正式脚本生成,只返回补数据建议;即使传入 `finalize=true` 也不会写入脚本记忆
+- 脚本定稿前增加 `finalQualityGate`,检查项目词泄漏、过泛、缺少场景、缺少互动/转化动作
+- `finalQualityGate.level=fail` 时阻断定稿记忆写入,避免坏稿污染后续偏好
+
+仍待实现:
+
+- 更细的重复主题风险、风险词、证据引用覆盖率
+- 基于业务配置的门禁阈值
+
+### P1.5 套件发布与自动化:已完成第一阶段
+
+已实现:
+
+- 套件 manifest 记录 P1 spec
+- 本地 zip/build/deploy 流程可用
+- 新增 `scripts/tools/douyin-speaking-daily-p1-smoke.js`
+- smoke 覆盖:
+  - 语法检查
+  - 有历史记忆时生成 v0
+  - feedback 生成 v1
+  - finalize 写入记忆
+  - runner 选题透传
+  - 自然话术“选题 N”自动续接最近 raw input
+  - 自然话术“定稿”自动续接最近 script-session
+  - 自然话术“保留/不要”写入选题偏好并影响排序
+  - 无逐字稿结构推断稿
+  - 定稿自检、30 秒压缩版、60 秒提词器版
+  - 网关超长音频保护
+
+仍待实现:
+
+- 上传前自动读取 zip 内容确认 P1 文件完整
+- 线上安装后自动 smoke
+- 发布清单按版本生成 changelog
+
+## 4. 数据产物约定
+
+一次完整运行应产生:
+
+```text
+outputs/<run>/
+  daily-report.md
+  daily-report.json
+  audit.json
+  runner-raw-input.json
+  transcript-queue.json
+  script-session.json                 # 进入共创后产生
+  selected-topic-<n>-script.md         # 进入共创后产生
+  selected-topic-<n>-script.json       # 进入共创后产生
+  transcripts/<aweme_id>/transcript.json
+  transcripts/<aweme_id>/transcript.txt
+```
+
+长期记忆:
+
+```text
+memory/
+  douyin-speaking-profile.json
+  douyin-speaking-transcript-cache.json
+  douyin-speaking-script-memory.json
+  douyin-speaking-history.json         # 已用于跨次运行沉淀,下一阶段接入排序降权
+```
+
+## 5. CLI 验收口令
+
+日报 + 自动转写:
+
+```powershell
+node scripts/tools/douyin-speaking-daily-runner.js `
+  --profile memory/douyin-speaking-profile.json `
+  --auto-transcript-top-n 1 `
+  --auto-transcript-provider iflytek-gateway `
+  --auto-transcript-max-duration-ms 480000 `
+  --output outputs/douyin-speaking-daily-live
+```
+
+选题进入脚本共创:
+
+```powershell
+node scripts/tools/douyin-speaking-daily-runner.js `
+  --profile memory/douyin-speaking-profile.json `
+  --input outputs/douyin-speaking-daily-live/runner-raw-input.json `
+  --selected-topic-index 2 `
+  --script-memory memory/douyin-speaking-script-memory.json `
+  --output outputs/douyin-speaking-daily-script-v0
+```
+
+反馈改稿:
+
+```powershell
+node scripts/tools/douyin-speaking-daily-runner.js `
+  --profile memory/douyin-speaking-profile.json `
+  --input outputs/douyin-speaking-daily-live/runner-raw-input.json `
+  --script-session outputs/douyin-speaking-daily-script-v0/script-session.json `
+  --feedback "开头更狠,老板视角,减少概念,多给具体场景" `
+  --script-memory memory/douyin-speaking-script-memory.json `
+  --output outputs/douyin-speaking-daily-script-v1
+```
+
+定稿沉淀:
+
+```powershell
+node scripts/tools/douyin-speaking-daily-runner.js `
+  --profile memory/douyin-speaking-profile.json `
+  --input outputs/douyin-speaking-daily-live/runner-raw-input.json `
+  --script-session outputs/douyin-speaking-daily-script-v1/script-session.json `
+  --finalize true `
+  --script-memory memory/douyin-speaking-script-memory.json `
+  --output outputs/douyin-speaking-daily-script-final
+```
+
+自动化自审:
+
+```powershell
+node scripts/tools/douyin-speaking-daily-p1-smoke.js
+```
+
+## 6. 自审标准
+
+每次优化后必须检查:
+
+1. 是否仍然以“日报选题展示 + 用户选择后共创脚本”为主流程。
+2. 是否直接在 OpenClaw 对话展示 `assistantMessage` 正文。
+3. 是否避免把单一项目名硬编码到通用逻辑。
+4. 是否保留证据链、置信度、风险说明和质量门禁。
+5. 是否没有伪造视频、评论、逐字稿、互动数据。
+6. 是否对超长视频、接口失败、缺少 token 做降级。
+7. 是否通过 `douyin-speaking-daily-p1-smoke.js`。
+
+## 7. 下一阶段优先级
+
+1. 增加“用户选择选题后自动补转写”的分段策略。
+2. 增加 90 秒展开版、镜头分镜版和字幕钩子。
+3. 基于 `douyin-speaking-history.json` 做 7/30 天趋势与更细的重复选题降权。
+4. 发布包 smoke 化:build、deploy、install 后都能一键验证并可用于教学视频录制。
+5. 增加真实 OpenClaw UI 回归清单,覆盖“日报 -> 选题 -> 改稿 -> 定稿 -> 偏好复用”全流程。

+ 185 - 0
docs/specs/douyin-speaking-daily-skill-package.md

@@ -0,0 +1,185 @@
+# 抖音口播爆款内容洞察技能包 Spec
+
+> 日期:2026-05-08  
+> 范围:主线二 / 场景一 / 1.1 爆款内容洞察闭环。P0 只做抖音平台,不做视频生成、自动发布、投后归因和私域导流。
+
+## 1. 产品定义
+
+`douyin-speaking-daily` 为抖音口播账号提供每日爆款选题日报。它解决的问题不是“直接写一篇泛文案”,而是:
+
+- 今天该讲什么;
+- 同行业爆款为什么爆;
+- 哪些选题适合当前账号定位;
+- 哪些视频值得补逐字稿做深拆;
+- 用户反馈如何沉淀到下一轮。
+
+## 2. P0 闭环
+
+```text
+问答建档 / 读取记忆
+  -> 关键词视频采集
+  -> 已确认账号近期作品采集
+  -> 评论采集
+  -> 自动选择 Top N 爆款视频补逐字稿
+  -> 每日稿件日报
+  -> 用户校准反馈写回记忆
+```
+
+P0 已支持:
+
+- 抖音关键词视频采集;
+- 指定或确认账号近期作品监听;
+- 评论采集;
+- 抖音分享链接解析到音频;
+- `iflytek-gateway` 网关转写;
+- 逐字稿回灌日报证据链;
+- profile/memory 写回。
+
+## 3. 角色设定
+
+OpenClaw 启动本技能包时,应扮演:
+
+```text
+抖音口播爆款内容策划师 + 数据化选题编辑
+```
+
+职责:
+
+- 用真实样本做选题判断;
+- 输出可拍摄的口播选题和结构;
+- 标注每条选题的证据链与置信度;
+- 主动追问校准反馈;
+- 维护账号长期记忆。
+
+边界:
+
+- 不伪造数据;
+- 不把标题当完整逐字稿;
+- 不自动发布;
+- 不做投后归因;
+- 不替代用户做最终选题决策。
+
+## 4. 记忆系统
+
+主记忆:
+
+```text
+memory/douyin-speaking-profile.json
+```
+
+模板:
+
+```text
+douyin-speaking-daily/memory-templates/douyin-speaking-profile.json
+```
+
+写回技能:
+
+```text
+douyin-speaking-profile-memory
+```
+
+核心字段:
+
+| 字段 | 作用 |
+|---|---|
+| `state` | 流程状态 |
+| `industry` | 行业/品类 |
+| `accountPositioning` | 账号身份 |
+| `targetAudience` | 目标人群 |
+| `conversionGoal` | 转化目标 |
+| `keywords` | 长期监听关键词 |
+| `confirmedAccounts` | 已确认监听账号 |
+| `candidateAccounts` | 待确认候选账号 |
+| `forbiddenTopics` | 禁区 |
+| `calibrationNotes` | 日报后反馈 |
+| `lastRunSummary` | 最近一次运行摘要 |
+
+状态机:
+
+```text
+emptyProfile
+  -> collectingProfile
+  -> awaitingProfileConfirm
+  -> readyForManualRun
+  -> awaitingReportCalibration
+  -> tunedProfile
+  -> automationReady
+```
+
+## 5. 技能组成
+
+| 技能 | 类型 | 作用 |
+|---|---|---|
+| `douyin-speaking-profile-builder` | conversational | 问答建档 |
+| `douyin-speaking-profile-memory` | local-script | profile 写回与校准 |
+| `douyin-speaking-keyword-monitor` | orchestration | 关键词监听 |
+| `douyin-user-posts` | api | 指定账号近期作品 |
+| `douyin-speaking-account-monitor` | orchestration | 对标账号监听 |
+| `douyin-video-transcript` | local-script | 抖音链接/音视频转逐字稿 |
+| `douyin-speaking-daily-report` | local-script | 日报生成 |
+| `douyin-speaking-daily-runner` | local-script | P0 总入口 |
+
+## 6. Runner 参数
+
+小样本测试:
+
+```powershell
+node scripts\tools\douyin-speaking-daily-runner.js --profile memory\douyin-speaking-profile.json --output outputs\run --videos-per-keyword 1 --max-keywords 1 --max-videos-with-comments 1 --max-comment-pages 1 --include-replies false --accounts-limit 1 --posts-per-account 1 --auto-transcript-top-n 1 --auto-transcript-provider iflytek-gateway --write-profile true
+```
+
+正式日报:
+
+```powershell
+node scripts\tools\douyin-speaking-daily-runner.js --profile memory\douyin-speaking-profile.json --videos-per-keyword 5 --max-keywords 5 --max-videos-with-comments 8 --max-comment-pages 1 --include-replies true --accounts-limit 5 --posts-per-account 8 --auto-transcript-top-n 1 --auto-transcript-provider iflytek-gateway --write-profile true
+```
+
+## 7. 日报输出
+
+输出目录:
+
+```text
+openclaw-voc-output/douyin-speaking-daily/<project>/<YYYY-MM-DD>/
+  daily-report.md
+  daily-report.json
+  audit.json
+  runner-raw-input.json
+  raw/
+    keyword-videos.json
+    account-videos.json
+    comments.json
+  transcripts/
+    <aweme_id>/
+      transcript.json
+      transcript.txt
+```
+
+日报必须包含:
+
+- 今日一句话判断;
+- 判断口径与置信度;
+- 趋势信号;
+- 今日选题池;
+- 每条选题的来源视频、互动数据、评论口径和逐字稿状态;
+- 口播框架;
+- 评论触发点;
+- 风险提醒;
+- 明日监听建议;
+- 校准问题。
+
+## 8. 置信度
+
+| 条件 | 置信度 |
+|---|---|
+| 有互动数据、有匹配评论、有逐字稿 | 高 |
+| 有互动数据、有匹配评论 | 中 |
+| 只有标题/描述和互动数据 | 低 |
+
+## 9. 后续安排
+
+P0 已闭环。下一步:
+
+- 增加“日报后自动生成完整可拍摄脚本”;
+- 接外部定时推送渠道;
+- 增加多条逐字稿批量成本控制;
+- 增加历史日报对比和关键词趋势变化。

+ 284 - 0
docs/specs/industry-trend-intelligence-agent-concept.md

@@ -0,0 +1,284 @@
+# 行业趋势情报官通用技能包方案
+
+## 背景
+
+客户场景来自“家装全屋定制行业,希望洞察女性客户在小红书等平台上对当季或下季度流行设计元素、风格、消费决策的关注变化”。
+
+最初可落地为“家装全屋定制趋势情报官”,但从产品化角度看,不宜一开始做成强定制技能。更优方案是抽象成一个通用的“行业趋势情报官”技能包,通过行业档案和关键词矩阵适配不同行业。
+
+## 产品定位
+
+通用名称建议:
+
+```text
+industry-trend-intelligence
+行业趋势情报官
+```
+
+面向客户的话术:
+
+```text
+这是一个帮企业每天/每周从社媒平台追踪行业趋势、用户关注点、消费决策信号和内容机会的情报官。
+它可以先从小红书开始,也可以后续扩展到抖音、知乎、微博等平台。
+```
+
+## 与抖音口播日报的关系
+
+复用底层闭环,不复用产品定位。
+
+可复用:
+
+1. 建档流程。
+2. 数据采集流程。
+3. 日报输出结构。
+4. 校准问答。
+5. 偏好记忆。
+6. 后续共创流程。
+
+不复用:
+
+1. “口播稿件日报”的产品心智。
+2. “爆款视频逐字稿”的强依赖。
+3. “选题变口播稿”的主交付物。
+
+行业趋势情报官的核心交付物应该是“趋势情报日报”和“行动建议”,不是口播稿。
+
+## 为什么要通用化
+
+如果直接做“家装全屋定制趋势情报官”,现场交付会很快,但后续每个行业都要重新定制一套。
+
+通用化后,只需要配置行业档案,就可以适配:
+
+1. 家装全屋定制。
+2. 美妆护肤。
+3. 母婴。
+4. 保健品。
+5. 食品饮料。
+6. 服装穿搭。
+7. 宠物用品。
+8. 本地生活服务。
+
+行业差异沉淀在 profile 和关键词矩阵里,而不是写死在技能逻辑里。
+
+## 通用建档字段
+
+行业趋势情报官的 profile 应包含:
+
+```json
+{
+  "project": "项目名称",
+  "industry": "行业/品类",
+  "businessType": "品牌/门店/工厂/服务商/个人IP",
+  "targetAudience": ["目标客户人群"],
+  "decisionMakers": ["实际决策者", "影响决策者"],
+  "productsOrServices": ["主营产品/服务"],
+  "priceBand": "价格带",
+  "regions": ["关注区域"],
+  "trendQuestions": ["想回答的趋势问题"],
+  "platforms": ["xiaohongshu"],
+  "keywords": ["核心关键词"],
+  "competitors": ["竞品/对标账号/对标品牌"],
+  "mustTrackSignals": ["必须关注的信号"],
+  "blockedDirections": ["不关注/不推荐方向"],
+  "outputPreference": {
+    "reportTone": "老板视角/市场视角/设计师视角/投放视角",
+    "actionType": "内容建议/选品建议/设计建议/销售话术/门店陈列"
+  }
+}
+```
+
+## 通用关键词矩阵
+
+不要只让用户填几个关键词。系统应根据行业档案生成关键词矩阵。
+
+关键词类型:
+
+1. 品类词:全屋定制、护肤精华、儿童益生菌。
+2. 场景词:小户型收纳、敏感肌、宝宝便秘。
+3. 痛点词:不好打理、踩坑、翻车、预算超支。
+4. 风格词:奶油风、原木风、中古风、极简风。
+5. 决策词:怎么选、避坑、测评、推荐、真实体验。
+6. 竞品词:品牌名、对标门店、竞品系列。
+7. 趋势词:2026流行、今年流行、下季度、爆款。
+
+家装示例:
+
+```json
+[
+  {
+    "keyword": "全屋定制",
+    "purpose": "抓取基础品类讨论",
+    "expectedSignal": "预算、环保、翻车、收纳、颜值"
+  },
+  {
+    "keyword": "奶油风装修",
+    "purpose": "追踪风格流行趋势",
+    "expectedSignal": "颜色、材质、灯光、软装搭配"
+  },
+  {
+    "keyword": "小户型收纳",
+    "purpose": "识别真实使用场景和痛点",
+    "expectedSignal": "柜体设计、空间利用、家庭成员需求"
+  }
+]
+```
+
+## P0 技能闭环
+
+P0 先做小红书单平台。
+
+```text
+自然语言启动
+  -> 读取/建立行业档案
+  -> 生成关键词矩阵
+  -> 采集小红书笔记
+  -> 采集笔记详情与评论
+  -> 提取趋势信号
+  -> 输出趋势情报日报
+  -> 用户校准方向
+  -> 写入偏好记忆
+```
+
+## P0 输出结构
+
+通用趋势情报日报建议结构:
+
+1. 今日/本周一句话判断。
+2. 样本口径。
+3. 高热趋势信号。
+4. 用户决策信号。
+5. 内容/产品/设计机会。
+6. 高价值样本拆解。
+7. 下周期可验证趋势假设。
+8. 行动建议。
+9. 下一轮监听关键词建议。
+10. 校准问题。
+
+家装行业映射:
+
+1. 高热趋势信号 -> 风格、空间、材质、颜色。
+2. 用户决策信号 -> 预算、环保、耐看、好打理、收纳、翻车风险。
+3. 内容/产品/设计机会 -> 门店案例、设计师话术、展厅陈列、短视频/小红书选题。
+4. 趋势假设 -> 下季度值得验证的风格和设计元素。
+
+## 校准问题
+
+校准问题应通用,但可以按行业显示不同措辞。
+
+通用版:
+
+```text
+1. 今天这些趋势里,哪 3 个最值得继续跟踪?
+2. 哪些方向明显不适合你的业务,后续需要降权?
+3. 下一版报告更偏老板决策、产品设计、内容选题、销售转化,还是竞品观察?
+4. 是否要新增/删除监听关键词、竞品或平台?
+```
+
+家装版:
+
+```text
+1. 今天这些风格/元素里,哪 3 个最值得继续跟踪?
+2. 哪些风格不适合你的客户或门店定位?
+3. 下一版更偏设计趋势、客户决策、门店转化,还是小红书内容选题?
+4. 是否要新增关注区域、户型、预算带或竞品品牌?
+```
+
+## 记忆系统
+
+记忆系统不应绑定家装,而应分为:
+
+1. 行业档案记忆。
+2. 趋势偏好记忆。
+3. 关键词效果记忆。
+4. 排除方向记忆。
+5. 输出风格记忆。
+
+记忆示例:
+
+```json
+{
+  "preferredSignals": ["女性客户决策顾虑", "风格元素变化", "高评论笔记"],
+  "blockedDirections": ["纯装修美图无评论", "豪宅风格", "低价引流套餐"],
+  "preferredOutput": ["老板视角", "门店可执行动作", "设计师话术"],
+  "highValueKeywords": ["小户型收纳", "奶油风装修", "衣柜设计避坑"]
+}
+```
+
+## 技能包结构建议
+
+通用套件:
+
+```text
+industry-trend-intelligence
+├─ industry-trend-runner
+├─ industry-trend-profile-builder
+├─ industry-trend-keyword-planner
+├─ industry-trend-report
+├─ industry-trend-memory
+├─ xiaohongshu-trend-collector
+└─ docs / install.js / package zip
+```
+
+首个平台:
+
+```text
+xiaohongshu
+```
+
+后续平台扩展:
+
+```text
+douyin
+zhihu
+weibo
+tiktok
+instagram
+```
+
+## 工作坊交付方式
+
+现场可以仍然使用“家装全屋定制”作为演示案例,但不要把技能包命名写死。
+
+推荐话术:
+
+```text
+我们先给你训练一个行业趋势情报官。
+今天用家装全屋定制作为第一个场景,先从小红书抓取女性客户在装修风格、设计元素、预算、环保、收纳、翻车风险上的真实讨论。
+跑通后,这套情报官也可以换到其他行业,只需要改行业档案和关键词矩阵。
+```
+
+## 开发优先级
+
+P0:
+
+1. 小红书单平台采集。
+2. 行业档案建档。
+3. 关键词矩阵生成。
+4. 趋势情报日报。
+5. 校准与偏好记忆。
+
+P1:
+
+1. 多平台接入。
+2. 图片/视觉元素识别。
+3. 竞品账号/品牌监听。
+4. 周报/月报。
+5. 趋势变化对比。
+
+P2:
+
+1. 自动生成内容选题。
+2. 销售话术/门店话术。
+3. 产品/设计方案建议。
+4. 可视化趋势雷达。
+
+## 当前结论
+
+不要直接做“家装全屋定制专属技能包”,也不要把抖音口播日报简单改成小红书版。
+
+建议做成通用的“行业趋势情报官”:
+
+1. P0 先用小红书跑通。
+2. 家装全屋定制作为第一个模板场景。
+3. 行业差异通过 profile、关键词矩阵和输出偏好配置。
+4. 后续可以扩展多平台和图片元素识别。

+ 2880 - 0
docs/workshop/social-voc-openclaw-workshop.html

@@ -0,0 +1,2880 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <meta name="format-detection" content="telephone=no">
+  <title>VOC.market 南昌社媒 VOC 工作坊|5月28日安装环境,5月29日全天分析实战</title>
+  <meta name="description" content="VOC.market 南昌本地社媒 VOC 工作坊:聚焦小红书和抖音,5月28日下午安装 OpenClaw(龙虾)并配置技能,5月29日全天做 VOC 分析实战。">
+  <script>
+    (() => {
+      const isMobile = window.matchMedia("(max-width: 720px), (hover: none), (pointer: coarse)").matches;
+      if (!isMobile) return;
+
+      try {
+        const key = "socialVocWorkshopLoadMarks";
+        const now = Date.now();
+        const marks = JSON.parse(sessionStorage.getItem(key) || "[]")
+          .filter((time) => now - time < 12000);
+        marks.push(now);
+        sessionStorage.setItem(key, JSON.stringify(marks.slice(-5)));
+        if (marks.length >= 3) {
+          document.documentElement.classList.add("mobile-safe-mode");
+        }
+      } catch (error) {
+        document.documentElement.classList.add("mobile-safe-mode");
+      }
+    })();
+  </script>
+  <style>
+    :root {
+      --black: #050505;
+      --deep: #0b0d10;
+      --panel: rgba(255, 255, 255, 0.075);
+      --panel-strong: rgba(255, 255, 255, 0.12);
+      --line: rgba(255, 255, 255, 0.16);
+      --line-strong: rgba(255, 255, 255, 0.28);
+      --white: #ffffff;
+      --muted: #a8b0ba;
+      --dim: #66707b;
+      --cyan: #00f0ff;
+      --rose: #ff5252;
+      --amber: #f59e0b;
+      --green: #3ee084;
+      --blue: #6388ff;
+      --platinum: #e7edf3;
+      --smoke: #101419;
+      --radius: 8px;
+      --max: 1180px;
+      --ease: cubic-bezier(0.22, 1, 0.36, 1);
+    }
+
+    * {
+      box-sizing: border-box;
+    }
+
+    html {
+      scroll-behavior: smooth;
+      background: var(--black);
+    }
+
+    body {
+      margin: 0;
+      min-height: 100vh;
+      overflow-x: hidden;
+      color: var(--white);
+      background:
+        linear-gradient(180deg, rgba(0, 240, 255, 0.075), transparent 22rem),
+        linear-gradient(90deg, rgba(255, 82, 82, 0.055), transparent 30rem),
+        linear-gradient(130deg, rgba(255, 255, 255, 0.04), transparent 18%, rgba(255, 255, 255, 0.015) 28%, transparent 45%),
+        var(--black);
+      font-family: Inter, "PingFang SC", "Microsoft YaHei", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+      line-height: 1.6;
+      -webkit-font-smoothing: antialiased;
+    }
+
+    body::before {
+      content: "";
+      position: fixed;
+      inset: 0;
+      z-index: -3;
+      pointer-events: none;
+      background-image:
+        linear-gradient(rgba(255, 255, 255, 0.04) 1px, transparent 1px),
+        linear-gradient(90deg, rgba(255, 255, 255, 0.04) 1px, transparent 1px);
+      background-size: 48px 48px;
+      mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.8), transparent 72%);
+    }
+
+    body::after {
+      content: "";
+      position: fixed;
+      inset: 0;
+      z-index: -2;
+      pointer-events: none;
+      background:
+        repeating-linear-gradient(180deg, rgba(255, 255, 255, 0.028) 0, rgba(255, 255, 255, 0.028) 1px, transparent 1px, transparent 5px);
+      opacity: 0.18;
+    }
+
+    .intro-overlay {
+      position: fixed;
+      inset: 0;
+      z-index: 120;
+      display: grid;
+      place-items: center;
+      pointer-events: none;
+      opacity: 1;
+      visibility: visible;
+      background:
+        linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.06), transparent),
+        #050505;
+      transform: translateZ(0);
+      backface-visibility: hidden;
+      contain: paint;
+      will-change: opacity;
+      transition: opacity 0.42s var(--ease), visibility 0.42s var(--ease);
+    }
+
+    .intro-overlay.is-hiding {
+      opacity: 0;
+      visibility: hidden;
+    }
+
+    .intro-overlay.is-gone {
+      display: none;
+    }
+
+    .intro-lockup {
+      position: relative;
+      display: grid;
+      gap: 10px;
+      text-align: center;
+      transform: translate3d(0, 10px, 0);
+      backface-visibility: hidden;
+      will-change: opacity, transform;
+      animation: introLockup 0.86s var(--ease) forwards;
+    }
+
+    .intro-lockup strong {
+      font-size: 1.7rem;
+      line-height: 1;
+      font-weight: 850;
+    }
+
+    .intro-lockup span {
+      color: var(--muted);
+      font-size: 0.8rem;
+      font-weight: 650;
+    }
+
+    .intro-rule {
+      width: 220px;
+      height: 1px;
+      margin: 6px auto 0;
+      background: linear-gradient(90deg, transparent, var(--cyan), var(--rose), transparent);
+      transform-origin: center;
+      animation: introRule 1.1s var(--ease) forwards;
+    }
+
+    @keyframes introLockup {
+      from { opacity: 0; transform: translate3d(0, 16px, 0) scale(0.985); }
+      to { opacity: 1; transform: translate3d(0, 0, 0) scale(1); }
+    }
+
+    @keyframes introRule {
+      from { transform: scaleX(0); opacity: 0; }
+      to { transform: scaleX(1); opacity: 1; }
+    }
+
+    a {
+      color: inherit;
+      text-decoration: none;
+    }
+
+    button,
+    textarea {
+      font: inherit;
+    }
+
+    h1,
+    h2,
+    h3,
+    p {
+      margin: 0;
+    }
+
+    img {
+      display: block;
+      max-width: 100%;
+    }
+
+    #networkCanvas {
+      position: fixed;
+      inset: 0;
+      z-index: -1;
+      width: 100%;
+      height: 100%;
+      opacity: 0.52;
+      pointer-events: none;
+      transform: translateZ(0);
+    }
+
+    .progress {
+      position: fixed;
+      top: 0;
+      left: 0;
+      z-index: 80;
+      width: 0;
+      height: 3px;
+      background: linear-gradient(90deg, var(--cyan), var(--rose), var(--amber));
+      box-shadow: 0 0 24px rgba(0, 240, 255, 0.45);
+    }
+
+    .nav {
+      position: sticky;
+      top: 0;
+      z-index: 70;
+      border-bottom: 1px solid rgba(255, 255, 255, 0.08);
+      background: linear-gradient(180deg, rgba(5, 5, 5, 0.86), rgba(5, 5, 5, 0.62));
+      backdrop-filter: blur(22px) saturate(1.35);
+      transition: background 0.34s var(--ease), border-color 0.34s ease, box-shadow 0.34s ease;
+    }
+
+    .nav.is-scrolled {
+      border-color: rgba(255, 255, 255, 0.15);
+      background: linear-gradient(180deg, rgba(5, 5, 5, 0.92), rgba(5, 5, 5, 0.76));
+      box-shadow: 0 18px 54px rgba(0, 0, 0, 0.36);
+    }
+
+    .nav-inner {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      max-width: var(--max);
+      min-height: 64px;
+      margin: 0 auto;
+      padding: 0 20px;
+    }
+
+    .brand {
+      display: inline-flex;
+      align-items: center;
+      gap: 10px;
+      font-weight: 750;
+    }
+
+    .brand-mark {
+      position: relative;
+      overflow: hidden;
+      display: grid;
+      place-items: center;
+      width: 34px;
+      height: 34px;
+      border: 1px solid rgba(255, 255, 255, 0.26);
+      border-radius: var(--radius);
+      background: linear-gradient(135deg, var(--cyan), var(--rose));
+      color: #050505;
+      font-weight: 900;
+    }
+
+    .brand-mark::after {
+      content: "";
+      position: absolute;
+      inset: -40% -80%;
+      background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.62), transparent);
+      transform: translateX(-80%) rotate(18deg);
+      animation: markSheen 5.4s ease-in-out infinite;
+    }
+
+    @keyframes markSheen {
+      0%, 65% { transform: translateX(-80%) rotate(18deg); }
+      82%, 100% { transform: translateX(90%) rotate(18deg); }
+    }
+
+    .brand-name {
+      line-height: 1.1;
+    }
+
+    .brand-name small {
+      display: block;
+      margin-top: 2px;
+      color: var(--muted);
+      font-size: 0.72rem;
+      font-weight: 500;
+    }
+
+    .nav-links {
+      display: flex;
+      align-items: center;
+      gap: 20px;
+      color: var(--muted);
+      font-size: 0.9rem;
+    }
+
+    .nav-links a {
+      transition: color 0.25s ease;
+    }
+
+    .nav-links a:hover {
+      color: var(--cyan);
+    }
+
+    .nav-cta {
+      display: inline-flex;
+      align-items: center;
+      justify-content: center;
+      min-height: 38px;
+      padding: 8px 14px;
+      border-radius: var(--radius);
+      background: linear-gradient(135deg, var(--cyan), var(--rose));
+      color: #050505;
+      font-weight: 800;
+      box-shadow: 0 0 28px rgba(0, 240, 255, 0.14);
+    }
+
+    .hero {
+      position: relative;
+      display: grid;
+      align-items: center;
+      min-height: calc(92svh - 64px);
+      padding: 58px 20px 48px;
+      overflow: hidden;
+    }
+
+    .hero::before {
+      content: "";
+      position: absolute;
+      inset: 0;
+      z-index: -1;
+      background:
+        linear-gradient(180deg, rgba(5, 5, 5, 0.14), rgba(5, 5, 5, 0.98)),
+        url("https://images.unsplash.com/photo-1551288049-bebda4e38f71?auto=format&fit=crop&w=1800&q=82") center/cover no-repeat;
+      opacity: 0.34;
+    }
+
+    .hero::after {
+      content: "";
+      position: absolute;
+      left: 20px;
+      right: 20px;
+      bottom: 20px;
+      height: 1px;
+      background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.28), transparent);
+      opacity: 0.8;
+    }
+
+    .hero-inner {
+      display: grid;
+      grid-template-columns: minmax(0, 1.03fr) minmax(360px, 0.8fr);
+      gap: 54px;
+      align-items: center;
+      width: min(100%, var(--max));
+      margin: 0 auto;
+    }
+
+    .hero-copy {
+      position: relative;
+    }
+
+    .hero-copy::before {
+      content: "SOCIAL VOC";
+      position: absolute;
+      left: -4px;
+      top: -58px;
+      color: rgba(255, 255, 255, 0.055);
+      font-size: 5.8rem;
+      line-height: 1;
+      font-weight: 900;
+      white-space: nowrap;
+      pointer-events: none;
+    }
+
+    .eyebrow {
+      display: inline-flex;
+      align-items: center;
+      width: fit-content;
+      min-height: 32px;
+      margin-bottom: 18px;
+      padding: 5px 10px;
+      border: 1px solid rgba(0, 240, 255, 0.38);
+      border-radius: var(--radius);
+      background: linear-gradient(90deg, rgba(0, 240, 255, 0.13), rgba(255, 255, 255, 0.035));
+      color: var(--cyan);
+      font-size: 0.86rem;
+      font-weight: 750;
+    }
+
+    h1 {
+      max-width: 780px;
+      font-size: 4.55rem;
+      line-height: 1.03;
+      letter-spacing: 0;
+      text-wrap: balance;
+      font-weight: 850;
+    }
+
+    .gradient-text {
+      background: linear-gradient(120deg, var(--cyan), var(--platinum), var(--rose), var(--cyan));
+      background-size: 220% 100%;
+      background-clip: text;
+      -webkit-background-clip: text;
+      -webkit-text-fill-color: transparent;
+      animation: gradientDrift 7s ease-in-out infinite;
+    }
+
+    @keyframes gradientDrift {
+      0%, 100% { background-position: 0% 50%; }
+      50% { background-position: 100% 50%; }
+    }
+
+    .hero-lead {
+      max-width: 700px;
+      margin-top: 20px;
+      color: #d2d7de;
+      font-size: 1.18rem;
+      text-wrap: pretty;
+    }
+
+    .hero-actions {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 12px;
+      margin-top: 30px;
+    }
+
+    .button {
+      position: relative;
+      overflow: hidden;
+      display: inline-flex;
+      align-items: center;
+      justify-content: center;
+      min-height: 46px;
+      padding: 11px 16px;
+      border: 1px solid rgba(255, 255, 255, 0.26);
+      border-radius: var(--radius);
+      color: var(--white);
+      font-weight: 780;
+      cursor: pointer;
+      transition: transform 0.25s var(--ease), border-color 0.25s ease, background 0.25s ease, box-shadow 0.25s ease;
+    }
+
+    .button::before {
+      content: "";
+      position: absolute;
+      inset: 0;
+      background: linear-gradient(110deg, transparent 0%, rgba(255, 255, 255, 0.28) 45%, transparent 70%);
+      transform: translateX(-110%);
+      transition: transform 0.58s var(--ease);
+    }
+
+    .button:hover::before {
+      transform: translateX(110%);
+    }
+
+    .button.primary {
+      border: 0;
+      background: linear-gradient(135deg, var(--cyan), var(--rose));
+      color: #050505;
+      box-shadow: 0 16px 40px rgba(0, 240, 255, 0.16);
+    }
+
+    .button:hover {
+      transform: translateY(-2px);
+      border-color: rgba(0, 240, 255, 0.56);
+      background: rgba(255, 255, 255, 0.09);
+      box-shadow: 0 12px 34px rgba(0, 0, 0, 0.28);
+    }
+
+    .button.primary:hover {
+      background: linear-gradient(135deg, #72f8ff, #ff7474);
+    }
+
+    .hero-proof {
+      display: grid;
+      grid-template-columns: repeat(3, minmax(0, 1fr));
+      gap: 10px;
+      max-width: 650px;
+      margin-top: 32px;
+    }
+
+    .proof-pill {
+      position: relative;
+      overflow: hidden;
+      min-height: 76px;
+      padding: 12px;
+      border: 1px solid var(--line);
+      border-radius: var(--radius);
+      background: linear-gradient(180deg, rgba(255, 255, 255, 0.105), rgba(255, 255, 255, 0.045));
+      backdrop-filter: blur(14px);
+    }
+
+    .proof-pill::before {
+      content: "";
+      position: absolute;
+      inset: 0 0 auto;
+      height: 1px;
+      background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.58), transparent);
+    }
+
+    .proof-pill strong {
+      display: block;
+      color: var(--white);
+      font-size: 1.12rem;
+      line-height: 1.2;
+    }
+
+    .proof-pill span {
+      display: block;
+      margin-top: 5px;
+      color: var(--muted);
+      font-size: 0.84rem;
+    }
+
+    .requirement-strip {
+      display: grid;
+      grid-template-columns: auto minmax(0, 1fr);
+      gap: 14px;
+      align-items: center;
+      max-width: 650px;
+      margin-top: 16px;
+      padding: 14px;
+      border: 1px solid rgba(245, 158, 11, 0.36);
+      border-radius: var(--radius);
+      background: linear-gradient(135deg, rgba(245, 158, 11, 0.12), rgba(255, 255, 255, 0.06));
+      backdrop-filter: blur(16px);
+    }
+
+    .requirement-strip b {
+      display: block;
+      color: var(--amber);
+      font-size: 0.9rem;
+    }
+
+    .requirement-strip span {
+      display: block;
+      color: #dce2e8;
+      font-size: 0.92rem;
+    }
+
+    .requirement-badge {
+      display: grid;
+      place-items: center;
+      width: 42px;
+      height: 42px;
+      border: 1px solid rgba(245, 158, 11, 0.4);
+      border-radius: var(--radius);
+      color: #050505;
+      background: var(--amber);
+      font-weight: 950;
+    }
+
+    .hero-media {
+      position: relative;
+      isolation: isolate;
+      min-height: 520px;
+      border: 1px solid rgba(255, 255, 255, 0.2);
+      border-radius: var(--radius);
+      overflow: hidden;
+      background: #080a0d;
+      box-shadow: 0 28px 80px rgba(0, 0, 0, 0.48);
+      transform-style: preserve-3d;
+    }
+
+    .hero-media::before {
+      content: "";
+      position: absolute;
+      inset: 0;
+      z-index: 4;
+      pointer-events: none;
+      border: 1px solid rgba(255, 255, 255, 0.16);
+      background:
+        linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent) 0 0 / 180% 1px no-repeat,
+        linear-gradient(180deg, transparent, rgba(0, 240, 255, 0.16), transparent) 0 0 / 1px 180% no-repeat;
+      animation: edgeTrace 7s ease-in-out infinite;
+    }
+
+    @keyframes edgeTrace {
+      0%, 100% { background-position: -120% 0, 0 -120%; opacity: 0.45; }
+      50% { background-position: 120% 0, 0 120%; opacity: 1; }
+    }
+
+    .hero-media img {
+      position: absolute;
+      inset: 0;
+      width: 100%;
+      height: 100%;
+      object-fit: cover;
+      opacity: 0.28;
+      filter: saturate(0.9) contrast(1.1);
+    }
+
+    .hero-media::after {
+      content: "";
+      position: absolute;
+      inset: 0;
+      background:
+        linear-gradient(180deg, rgba(5, 5, 5, 0.18), rgba(5, 5, 5, 0.92)),
+        linear-gradient(90deg, rgba(0, 240, 255, 0.16), transparent 46%, rgba(255, 82, 82, 0.16));
+      pointer-events: none;
+      z-index: 1;
+    }
+
+    .signal-console {
+      position: absolute;
+      inset: 22px;
+      z-index: 2;
+      display: grid;
+      grid-template-rows: auto 1fr auto;
+      gap: 14px;
+    }
+
+    .console-top {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      gap: 12px;
+      color: var(--muted);
+      font-size: 0.82rem;
+    }
+
+    .console-dots {
+      display: inline-flex;
+      gap: 6px;
+    }
+
+    .console-dots span {
+      width: 8px;
+      height: 8px;
+      border-radius: 50%;
+      background: var(--cyan);
+      box-shadow: 0 0 16px rgba(0, 240, 255, 0.7);
+    }
+
+    .console-dots span:nth-child(2) {
+      background: var(--rose);
+      box-shadow: 0 0 16px rgba(255, 82, 82, 0.7);
+    }
+
+    .console-dots span:nth-child(3) {
+      background: var(--amber);
+      box-shadow: 0 0 16px rgba(245, 158, 11, 0.7);
+    }
+
+    .radar {
+      position: relative;
+      min-height: 310px;
+      border: 1px solid rgba(255, 255, 255, 0.13);
+      border-radius: var(--radius);
+      overflow: hidden;
+      background:
+        linear-gradient(rgba(255, 255, 255, 0.035) 1px, transparent 1px),
+        linear-gradient(90deg, rgba(255, 255, 255, 0.035) 1px, transparent 1px),
+        linear-gradient(135deg, rgba(0, 240, 255, 0.08), rgba(255, 255, 255, 0.03), rgba(255, 82, 82, 0.08)),
+        rgba(255, 255, 255, 0.055);
+      background-size: 28px 28px;
+    }
+
+    .radar-line {
+      position: absolute;
+      left: 8%;
+      right: 8%;
+      height: 2px;
+      background: linear-gradient(90deg, transparent, var(--cyan), transparent);
+      box-shadow: 0 0 18px rgba(0, 240, 255, 0.8);
+      animation: scan 4.6s ease-in-out infinite;
+    }
+
+    @keyframes scan {
+      0% { top: 12%; opacity: 0; }
+      12% { opacity: 1; }
+      76% { opacity: 1; }
+      100% { top: 86%; opacity: 0; }
+    }
+
+    .signal-node {
+      position: absolute;
+      display: grid;
+      place-items: center;
+      min-width: 86px;
+      min-height: 42px;
+      padding: 6px 9px;
+      border: 1px solid rgba(255, 255, 255, 0.18);
+      border-radius: var(--radius);
+      background: rgba(5, 5, 5, 0.6);
+      color: var(--white);
+      font-size: 0.82rem;
+      font-weight: 750;
+      backdrop-filter: blur(10px);
+      animation: floatCard 5.8s ease-in-out infinite;
+    }
+
+    .signal-node small {
+      color: var(--muted);
+      font-size: 0.68rem;
+      font-weight: 500;
+    }
+
+    .signal-node:nth-child(2) {
+      top: 15%;
+      left: 8%;
+      border-color: rgba(0, 240, 255, 0.38);
+    }
+
+    .signal-node:nth-child(3) {
+      top: 24%;
+      right: 10%;
+      border-color: rgba(255, 82, 82, 0.42);
+      animation-delay: 0.5s;
+    }
+
+    .signal-node:nth-child(4) {
+      top: 50%;
+      left: 22%;
+      border-color: rgba(245, 158, 11, 0.42);
+      animation-delay: 1.1s;
+    }
+
+    .signal-node:nth-child(5) {
+      right: 8%;
+      bottom: 18%;
+      border-color: rgba(62, 224, 132, 0.4);
+      animation-delay: 1.8s;
+    }
+
+    @keyframes floatCard {
+      0%, 100% { transform: translateY(0); }
+      50% { transform: translateY(-9px); }
+    }
+
+    .console-bottom {
+      display: grid;
+      grid-template-columns: repeat(3, 1fr);
+      gap: 10px;
+    }
+
+    .meter {
+      padding: 12px;
+      border: 1px solid rgba(255, 255, 255, 0.14);
+      border-radius: var(--radius);
+      background: rgba(255, 255, 255, 0.07);
+    }
+
+    .meter span {
+      display: block;
+      margin-bottom: 8px;
+      color: var(--muted);
+      font-size: 0.72rem;
+    }
+
+    .bar {
+      height: 7px;
+      border-radius: 999px;
+      background: rgba(255, 255, 255, 0.11);
+      overflow: hidden;
+    }
+
+    .bar i {
+      display: block;
+      width: 72%;
+      height: 100%;
+      border-radius: inherit;
+      background: linear-gradient(90deg, var(--cyan), var(--rose));
+      animation: barPulse 4s ease-in-out infinite;
+    }
+
+    .meter:nth-child(2) .bar i {
+      width: 86%;
+      background: linear-gradient(90deg, var(--amber), var(--cyan));
+      animation-delay: 0.7s;
+    }
+
+    .meter:nth-child(3) .bar i {
+      width: 64%;
+      background: linear-gradient(90deg, var(--green), var(--cyan));
+      animation-delay: 1.2s;
+    }
+
+    @keyframes barPulse {
+      0%, 100% { transform: translateX(-8%); opacity: 0.75; }
+      50% { transform: translateX(8%); opacity: 1; }
+    }
+
+    .ticker {
+      border-top: 1px solid rgba(255, 255, 255, 0.1);
+      border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+      background: rgba(255, 255, 255, 0.045);
+      overflow: hidden;
+    }
+
+    .ticker-track {
+      display: flex;
+      width: max-content;
+      animation: marquee 34s linear infinite;
+    }
+
+    .ticker-group {
+      display: flex;
+      gap: 12px;
+      padding: 16px 6px;
+    }
+
+    .ticker span {
+      display: inline-flex;
+      align-items: center;
+      min-height: 34px;
+      padding: 6px 12px;
+      border: 1px solid rgba(255, 255, 255, 0.18);
+      border-radius: var(--radius);
+      color: #dce5ef;
+      white-space: nowrap;
+      background: rgba(5, 5, 5, 0.45);
+      font-size: 0.9rem;
+    }
+
+    @keyframes marquee {
+      from { transform: translateX(0); }
+      to { transform: translateX(-50%); }
+    }
+
+    .signal-spine {
+      position: relative;
+      overflow: hidden;
+      padding: 72px 20px 78px;
+      border-bottom: 1px solid rgba(255, 255, 255, 0.08);
+      background:
+        linear-gradient(90deg, rgba(255, 255, 255, 0.03), transparent 18%, rgba(255, 255, 255, 0.025) 56%, transparent),
+        rgba(255, 255, 255, 0.018);
+    }
+
+    .signal-spine::before {
+      content: "";
+      position: absolute;
+      top: 50%;
+      left: 0;
+      right: 0;
+      height: 1px;
+      background: linear-gradient(90deg, transparent, rgba(0, 240, 255, 0.35), rgba(255, 82, 82, 0.32), transparent);
+    }
+
+    .spine-inner {
+      position: relative;
+      width: min(100%, var(--max));
+      margin: 0 auto;
+    }
+
+    .spine-word {
+      position: absolute;
+      top: -44px;
+      right: 0;
+      color: rgba(255, 255, 255, 0.045);
+      font-size: 7rem;
+      line-height: 1;
+      font-weight: 900;
+      white-space: nowrap;
+      pointer-events: none;
+    }
+
+    .spine-grid {
+      display: grid;
+      grid-template-columns: repeat(4, minmax(0, 1fr));
+      gap: 12px;
+    }
+
+    .spine-cell {
+      position: relative;
+      min-height: 210px;
+      padding: 18px;
+      border: 1px solid rgba(255, 255, 255, 0.14);
+      border-radius: var(--radius);
+      background:
+        linear-gradient(180deg, rgba(255, 255, 255, 0.095), rgba(255, 255, 255, 0.035));
+      backdrop-filter: blur(18px);
+      overflow: hidden;
+      transition: transform 0.34s var(--ease), border-color 0.34s ease, background 0.34s ease, box-shadow 0.34s ease;
+    }
+
+    .spine-cell::before {
+      content: "";
+      position: absolute;
+      inset: 0 0 auto;
+      height: 2px;
+      background: linear-gradient(90deg, var(--cyan), var(--rose));
+      opacity: 0.68;
+    }
+
+    .spine-cell:hover {
+      transform: translateY(-6px);
+      border-color: rgba(0, 240, 255, 0.38);
+    }
+
+    .spine-cell em {
+      display: block;
+      color: var(--dim);
+      font-style: normal;
+      font-size: 0.74rem;
+      font-weight: 800;
+    }
+
+    .spine-cell strong {
+      display: block;
+      margin-top: 36px;
+      color: var(--white);
+      font-size: 1.2rem;
+      line-height: 1.25;
+    }
+
+    .spine-cell span {
+      display: block;
+      margin-top: 12px;
+      color: var(--muted);
+      font-size: 0.9rem;
+    }
+
+    .spine-cell b {
+      position: absolute;
+      right: 16px;
+      bottom: 10px;
+      color: rgba(255, 255, 255, 0.065);
+      font-size: 4rem;
+      line-height: 1;
+      font-weight: 900;
+    }
+
+    .section {
+      position: relative;
+      padding: 86px 20px;
+    }
+
+    .section::before {
+      content: "";
+      position: absolute;
+      inset: 0 0 auto;
+      height: 1px;
+      background: linear-gradient(90deg, transparent, rgba(0, 240, 255, 0.35), rgba(255, 82, 82, 0.28), transparent);
+      opacity: 0;
+      transform: scaleX(0.46);
+      transform-origin: center;
+      transition: opacity 1s var(--ease), transform 1s var(--ease);
+    }
+
+    .section.section-active::before {
+      opacity: 1;
+      transform: scaleX(1);
+    }
+
+    .section.alt {
+      background: rgba(255, 255, 255, 0.035);
+      border-top: 1px solid rgba(255, 255, 255, 0.08);
+      border-bottom: 1px solid rgba(255, 255, 255, 0.08);
+    }
+
+    .wrap {
+      width: min(100%, var(--max));
+      margin: 0 auto;
+    }
+
+    .section-head {
+      display: grid;
+      gap: 12px;
+      max-width: 760px;
+      margin-bottom: 32px;
+    }
+
+    .section-head.center {
+      margin-right: auto;
+      margin-left: auto;
+      text-align: center;
+    }
+
+    .section-kicker {
+      color: var(--cyan);
+      font-size: 0.92rem;
+      font-weight: 800;
+    }
+
+    h2 {
+      font-size: 2.8rem;
+      line-height: 1.13;
+      letter-spacing: 0;
+    }
+
+    .sub {
+      color: var(--muted);
+      font-size: 1.05rem;
+    }
+
+    .grid {
+      display: grid;
+      gap: 14px;
+    }
+
+    .grid.two {
+      grid-template-columns: repeat(2, minmax(0, 1fr));
+    }
+
+    .grid.three {
+      grid-template-columns: repeat(3, minmax(0, 1fr));
+    }
+
+    .grid.four {
+      grid-template-columns: repeat(4, minmax(0, 1fr));
+    }
+
+    .card,
+    .slice,
+    .timeline-row,
+    .deliverable,
+    .share-panel {
+      position: relative;
+      overflow: hidden;
+      border: 1px solid var(--line);
+      border-radius: var(--radius);
+      background:
+        linear-gradient(180deg, rgba(255, 255, 255, 0.09), rgba(255, 255, 255, 0.045)),
+        var(--panel);
+      backdrop-filter: blur(18px);
+      transform: translateZ(0);
+    }
+
+    .card::after,
+    .timeline-row::after,
+    .deliverable::after,
+    .share-panel::after {
+      content: "";
+      position: absolute;
+      inset: 0 0 auto;
+      height: 1px;
+      background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.42), transparent);
+      pointer-events: none;
+    }
+
+    .card {
+      min-height: 100%;
+      padding: 20px;
+      transition: transform 0.34s var(--ease), border-color 0.34s ease, background 0.34s ease, box-shadow 0.34s ease;
+    }
+
+    .card:hover {
+      transform: translateY(-5px);
+      border-color: rgba(0, 240, 255, 0.35);
+      background: rgba(255, 255, 255, 0.095);
+      box-shadow: 0 18px 48px rgba(0, 0, 0, 0.28);
+    }
+
+    .index {
+      display: inline-flex;
+      align-items: center;
+      justify-content: center;
+      width: 34px;
+      height: 34px;
+      margin-bottom: 14px;
+      border: 1px solid rgba(255, 255, 255, 0.18);
+      border-radius: var(--radius);
+      color: #050505;
+      background: var(--cyan);
+      font-weight: 900;
+    }
+
+    .index.rose {
+      background: var(--rose);
+    }
+
+    .index.amber {
+      background: var(--amber);
+    }
+
+    .index.green {
+      background: var(--green);
+    }
+
+    .card h3 {
+      margin-bottom: 8px;
+      font-size: 1.16rem;
+      line-height: 1.36;
+      letter-spacing: 0;
+    }
+
+    .card p,
+    .card li {
+      color: var(--muted);
+      font-size: 0.94rem;
+    }
+
+    .list {
+      display: grid;
+      gap: 9px;
+      margin: 0;
+      padding: 0;
+      list-style: none;
+    }
+
+    .list li {
+      position: relative;
+      padding-left: 18px;
+    }
+
+    .list li::before {
+      content: "";
+      position: absolute;
+      top: 0.72em;
+      left: 0;
+      width: 7px;
+      height: 7px;
+      border-radius: 50%;
+      background: var(--cyan);
+      box-shadow: 0 0 12px rgba(0, 240, 255, 0.5);
+    }
+
+    .skill-wall {
+      display: grid;
+      grid-template-columns: 0.9fr 1.1fr;
+      gap: 16px;
+      align-items: stretch;
+    }
+
+    .slice {
+      position: relative;
+      overflow: hidden;
+      min-height: 360px;
+      padding: 24px;
+    }
+
+    .slice h3 {
+      max-width: 360px;
+      font-size: 1.9rem;
+      line-height: 1.18;
+      letter-spacing: 0;
+    }
+
+    .slice p {
+      max-width: 380px;
+      margin-top: 14px;
+      color: var(--muted);
+    }
+
+    .skill-rings {
+      position: absolute;
+      right: 22px;
+      bottom: 22px;
+      width: 260px;
+      height: 260px;
+      border: 1px solid rgba(0, 240, 255, 0.32);
+      border-radius: 50%;
+      animation: rotateSlow 28s linear infinite;
+    }
+
+    .skill-rings::before,
+    .skill-rings::after {
+      content: "";
+      position: absolute;
+      border: 1px solid rgba(255, 255, 255, 0.16);
+      border-radius: 50%;
+    }
+
+    .skill-rings::before {
+      inset: 38px;
+    }
+
+    .skill-rings::after {
+      inset: 84px;
+      border-color: rgba(255, 82, 82, 0.34);
+    }
+
+    .ring-label {
+      position: absolute;
+      display: inline-flex;
+      align-items: center;
+      justify-content: center;
+      min-height: 34px;
+      padding: 5px 10px;
+      border: 1px solid rgba(255, 255, 255, 0.18);
+      border-radius: var(--radius);
+      background: rgba(5, 5, 5, 0.72);
+      color: var(--white);
+      font-size: 0.82rem;
+      animation: rotateBack 28s linear infinite;
+    }
+
+    .ring-label.a { top: -15px; left: 82px; }
+    .ring-label.b { top: 108px; right: -34px; }
+    .ring-label.c { left: -24px; bottom: 52px; }
+    .ring-label.d { right: 72px; bottom: -15px; }
+
+    @keyframes rotateSlow {
+      to { transform: rotate(360deg); }
+    }
+
+    @keyframes rotateBack {
+      to { transform: rotate(-360deg); }
+    }
+
+    .capability-grid {
+      display: grid;
+      grid-template-columns: repeat(2, minmax(0, 1fr));
+      gap: 14px;
+    }
+
+    .capability {
+      padding: 18px;
+      border: 1px solid var(--line);
+      border-radius: var(--radius);
+      background: rgba(255, 255, 255, 0.055);
+      transition: transform 0.34s var(--ease), border-color 0.34s ease, background 0.34s ease, box-shadow 0.34s ease;
+    }
+
+    .capability:hover {
+      transform: translateY(-5px);
+      border-color: rgba(0, 240, 255, 0.34);
+      background: rgba(255, 255, 255, 0.085);
+      box-shadow: 0 18px 48px rgba(0, 0, 0, 0.24);
+    }
+
+    .capability strong {
+      display: block;
+      margin-bottom: 8px;
+      font-size: 1.02rem;
+    }
+
+    .capability span {
+      color: var(--muted);
+      font-size: 0.9rem;
+    }
+
+    .daily-lab {
+      display: grid;
+      grid-template-columns: minmax(0, 0.95fr) minmax(420px, 1.05fr);
+      gap: 18px;
+      align-items: stretch;
+    }
+
+    .daily-copy {
+      display: grid;
+      align-content: center;
+      gap: 18px;
+      min-height: 100%;
+    }
+
+    .daily-copy .section-head {
+      margin-bottom: 0;
+    }
+
+    .daily-points {
+      display: grid;
+      gap: 10px;
+    }
+
+    .daily-point {
+      display: grid;
+      grid-template-columns: 38px minmax(0, 1fr);
+      gap: 12px;
+      align-items: start;
+      padding: 14px;
+      border: 1px solid var(--line);
+      border-radius: var(--radius);
+      background: rgba(255, 255, 255, 0.06);
+    }
+
+    .daily-point b {
+      display: grid;
+      place-items: center;
+      width: 34px;
+      height: 34px;
+      border-radius: var(--radius);
+      background: rgba(0, 240, 255, 0.16);
+      color: var(--cyan);
+      font-size: 0.84rem;
+    }
+
+    .daily-point strong {
+      display: block;
+      margin-bottom: 4px;
+      color: var(--white);
+    }
+
+    .daily-point span {
+      color: var(--muted);
+      font-size: 0.9rem;
+    }
+
+    .daily-board {
+      position: relative;
+      overflow: hidden;
+      min-height: 560px;
+      border: 1px solid var(--line-strong);
+      border-radius: var(--radius);
+      background:
+        linear-gradient(180deg, rgba(0, 240, 255, 0.08), transparent 32%),
+        linear-gradient(135deg, rgba(255, 82, 82, 0.09), transparent 42%),
+        rgba(255, 255, 255, 0.06);
+      backdrop-filter: blur(18px);
+      box-shadow: 0 26px 90px rgba(0, 0, 0, 0.42);
+    }
+
+    .daily-board::before {
+      content: "";
+      position: absolute;
+      inset: 0;
+      background-image:
+        linear-gradient(rgba(255, 255, 255, 0.045) 1px, transparent 1px),
+        linear-gradient(90deg, rgba(255, 255, 255, 0.045) 1px, transparent 1px);
+      background-size: 34px 34px;
+      mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.92), transparent 92%);
+      pointer-events: none;
+    }
+
+    .board-inner {
+      position: relative;
+      z-index: 1;
+      display: grid;
+      gap: 14px;
+      padding: 18px;
+    }
+
+    .board-header {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      gap: 12px;
+      padding-bottom: 12px;
+      border-bottom: 1px solid rgba(255, 255, 255, 0.14);
+    }
+
+    .board-title {
+      display: grid;
+      gap: 4px;
+    }
+
+    .board-title strong {
+      font-size: 1.08rem;
+    }
+
+    .board-title span,
+    .board-status {
+      color: var(--muted);
+      font-size: 0.82rem;
+    }
+
+    .board-status {
+      display: inline-flex;
+      align-items: center;
+      min-height: 28px;
+      padding: 4px 9px;
+      border: 1px solid rgba(62, 224, 132, 0.34);
+      border-radius: var(--radius);
+      color: var(--green);
+      background: rgba(62, 224, 132, 0.08);
+      white-space: nowrap;
+    }
+
+    .pipeline {
+      display: grid;
+      gap: 10px;
+    }
+
+    .pipeline-step {
+      display: grid;
+      grid-template-columns: 72px minmax(0, 1fr) auto;
+      gap: 12px;
+      align-items: center;
+      padding: 13px;
+      border: 1px solid rgba(255, 255, 255, 0.13);
+      border-radius: var(--radius);
+      background: rgba(5, 5, 5, 0.42);
+      animation: stepGlow 6s ease-in-out infinite;
+    }
+
+    .pipeline-step:nth-child(2) { animation-delay: 0.45s; }
+    .pipeline-step:nth-child(3) { animation-delay: 0.9s; }
+    .pipeline-step:nth-child(4) { animation-delay: 1.35s; }
+    .pipeline-step:nth-child(5) { animation-delay: 1.8s; }
+
+    @keyframes stepGlow {
+      0%, 100% { border-color: rgba(255, 255, 255, 0.13); }
+      50% { border-color: rgba(0, 240, 255, 0.34); }
+    }
+
+    .pipeline-step code {
+      color: var(--cyan);
+      font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
+      font-size: 0.78rem;
+    }
+
+    .pipeline-step strong {
+      display: block;
+      margin-bottom: 2px;
+      font-size: 0.95rem;
+    }
+
+    .pipeline-step span {
+      color: var(--muted);
+      font-size: 0.82rem;
+    }
+
+    .pipeline-step i {
+      display: inline-grid;
+      place-items: center;
+      width: 28px;
+      height: 28px;
+      border-radius: 50%;
+      color: #050505;
+      background: linear-gradient(135deg, var(--cyan), var(--rose));
+      font-style: normal;
+      font-weight: 900;
+    }
+
+    .topic-stack {
+      display: grid;
+      grid-template-columns: repeat(3, minmax(0, 1fr));
+      gap: 10px;
+    }
+
+    .topic-card {
+      min-height: 118px;
+      padding: 12px;
+      border: 1px solid rgba(255, 255, 255, 0.13);
+      border-radius: var(--radius);
+      background: rgba(255, 255, 255, 0.07);
+    }
+
+    .topic-card small {
+      color: var(--dim);
+      font-size: 0.72rem;
+    }
+
+    .topic-card strong {
+      display: block;
+      margin: 8px 0;
+      line-height: 1.28;
+      font-size: 0.92rem;
+    }
+
+    .topic-card span {
+      color: var(--muted);
+      font-size: 0.78rem;
+    }
+
+    .need-finder {
+      display: grid;
+      grid-template-columns: repeat(4, minmax(0, 1fr));
+      gap: 12px;
+      margin-top: 22px;
+    }
+
+    .need-chip {
+      min-height: 116px;
+      padding: 14px;
+      border: 1px solid var(--line);
+      border-radius: var(--radius);
+      background: rgba(255, 255, 255, 0.052);
+      transition: transform 0.34s var(--ease), border-color 0.34s ease, background 0.34s ease;
+    }
+
+    .need-chip:hover {
+      transform: translateY(-4px);
+      border-color: rgba(0, 240, 255, 0.32);
+      background: rgba(255, 255, 255, 0.08);
+    }
+
+    .need-chip strong {
+      display: block;
+      margin-bottom: 6px;
+      color: var(--white);
+    }
+
+    .need-chip span {
+      color: var(--muted);
+      font-size: 0.86rem;
+    }
+
+    .platform-map {
+      display: grid;
+      grid-template-columns: repeat(4, minmax(0, 1fr));
+      gap: 10px;
+    }
+
+    .platform {
+      min-height: 132px;
+      padding: 14px;
+      border: 1px solid var(--line);
+      border-radius: var(--radius);
+      background: rgba(255, 255, 255, 0.06);
+      transition: transform 0.34s var(--ease), border-color 0.34s ease, background 0.34s ease, box-shadow 0.34s ease;
+    }
+
+    .platform:hover {
+      transform: translateY(-4px);
+      border-color: rgba(255, 82, 82, 0.4);
+      background: rgba(255, 255, 255, 0.09);
+      box-shadow: 0 16px 42px rgba(0, 0, 0, 0.24);
+    }
+
+    .platform b {
+      display: block;
+      margin-bottom: 6px;
+      color: var(--white);
+      font-size: 1rem;
+    }
+
+    .platform span {
+      color: var(--muted);
+      font-size: 0.84rem;
+    }
+
+    .timeline {
+      display: grid;
+      gap: 12px;
+    }
+
+    .timeline-row {
+      display: grid;
+      grid-template-columns: 190px minmax(0, 1fr);
+      gap: 20px;
+      padding: 20px;
+      transition: border-color 0.5s ease, background 0.5s ease, box-shadow 0.5s ease;
+    }
+
+    .timeline-row.visible {
+      border-color: rgba(0, 240, 255, 0.26);
+      box-shadow: 0 18px 58px rgba(0, 0, 0, 0.24);
+    }
+
+    .time {
+      color: var(--cyan);
+      font-weight: 850;
+    }
+
+    .timeline-row h3 {
+      margin-bottom: 8px;
+      font-size: 1.25rem;
+      letter-spacing: 0;
+    }
+
+    .timeline-row p {
+      color: var(--muted);
+    }
+
+    .tags {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 8px;
+      margin-top: 14px;
+    }
+
+    .tags span {
+      display: inline-flex;
+      align-items: center;
+      min-height: 30px;
+      padding: 5px 9px;
+      border: 1px solid rgba(255, 255, 255, 0.14);
+      border-radius: var(--radius);
+      background: rgba(255, 255, 255, 0.07);
+      color: #d8dee6;
+      font-size: 0.84rem;
+      font-weight: 700;
+    }
+
+    .deliverables {
+      display: grid;
+      grid-template-columns: repeat(3, minmax(0, 1fr));
+      gap: 12px;
+    }
+
+    .deliverable {
+      min-height: 190px;
+      padding: 18px;
+      transition: transform 0.34s var(--ease), border-color 0.34s ease, background 0.34s ease, box-shadow 0.34s ease;
+    }
+
+    .deliverable:hover {
+      transform: translateY(-5px);
+      border-color: rgba(0, 240, 255, 0.32);
+      background: rgba(255, 255, 255, 0.085);
+      box-shadow: 0 18px 46px rgba(0, 0, 0, 0.22);
+    }
+
+    .deliverable strong {
+      display: block;
+      margin: 18px 0 8px;
+      font-size: 1.02rem;
+    }
+
+    .deliverable span {
+      color: var(--muted);
+      font-size: 0.88rem;
+    }
+
+    .deliverable .mark {
+      display: inline-grid;
+      place-items: center;
+      width: 36px;
+      height: 36px;
+      border-radius: var(--radius);
+      color: #050505;
+      background: linear-gradient(135deg, var(--cyan), var(--rose));
+      font-weight: 900;
+    }
+
+    .split {
+      display: grid;
+      grid-template-columns: 0.88fr 1.12fr;
+      gap: 18px;
+      align-items: start;
+    }
+
+    .quote {
+      position: sticky;
+      top: 88px;
+      padding: 24px;
+      border: 1px solid var(--line);
+      border-radius: var(--radius);
+      background: linear-gradient(180deg, rgba(0, 240, 255, 0.08), rgba(255, 255, 255, 0.055));
+    }
+
+    .quote p {
+      font-size: 1.5rem;
+      line-height: 1.32;
+      font-weight: 760;
+    }
+
+    .quote span {
+      display: block;
+      margin-top: 14px;
+      color: var(--muted);
+    }
+
+    .share-panel {
+      display: grid;
+      gap: 12px;
+      padding: 18px;
+    }
+
+    textarea {
+      width: 100%;
+      min-height: 360px;
+      resize: vertical;
+      border: 1px solid var(--line);
+      border-radius: var(--radius);
+      outline: none;
+      padding: 16px;
+      background: rgba(0, 0, 0, 0.34);
+      color: #f4f8fb;
+      line-height: 1.72;
+    }
+
+    .copy-row {
+      display: flex;
+      justify-content: space-between;
+      gap: 10px;
+      align-items: center;
+    }
+
+    .copy-row small {
+      color: var(--dim);
+    }
+
+    .bottom-cta {
+      position: sticky;
+      bottom: 0;
+      z-index: 60;
+      display: none;
+      gap: 10px;
+      padding: 10px 14px calc(10px + env(safe-area-inset-bottom));
+      border-top: 1px solid rgba(255, 255, 255, 0.12);
+      background: rgba(5, 5, 5, 0.84);
+      backdrop-filter: blur(18px);
+    }
+
+    .bottom-cta a {
+      flex: 1;
+      min-height: 44px;
+    }
+
+    .toast {
+      position: fixed;
+      left: 50%;
+      bottom: 86px;
+      z-index: 90;
+      padding: 9px 12px;
+      border-radius: var(--radius);
+      background: var(--white);
+      color: var(--black);
+      font-size: 0.9rem;
+      font-weight: 800;
+      transform: translate(-50%, 12px);
+      opacity: 0;
+      pointer-events: none;
+      transition: opacity 0.2s ease, transform 0.2s ease;
+    }
+
+    .toast.show {
+      opacity: 1;
+      transform: translate(-50%, 0);
+    }
+
+    footer {
+      padding: 34px 20px 44px;
+      border-top: 1px solid rgba(255, 255, 255, 0.1);
+      color: var(--dim);
+      background: #050505;
+    }
+
+    .footer-inner {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      gap: 18px;
+      width: min(100%, var(--max));
+      margin: 0 auto;
+    }
+
+    [data-reveal] {
+      opacity: 0;
+      transform: translate3d(0, 34px, 0) scale(0.985);
+      will-change: opacity, transform;
+      transition:
+        opacity 0.82s var(--ease),
+        transform 0.82s var(--ease),
+        border-color 0.34s ease,
+        background 0.34s ease,
+        box-shadow 0.34s ease;
+    }
+
+    [data-reveal="fade-left"] {
+      transform: translate3d(-30px, 0, 0) scale(0.992);
+    }
+
+    [data-reveal="fade-right"] {
+      transform: translate3d(30px, 0, 0) scale(0.992);
+    }
+
+    [data-reveal="scale"] {
+      transform: translate3d(0, 24px, 0) scale(0.955);
+    }
+
+    [data-reveal].visible {
+      opacity: 1;
+      transform: translate3d(0, 0, 0) scale(1);
+    }
+
+    [data-reveal].visible.card:hover,
+    [data-reveal].visible.spine-cell:hover,
+    [data-reveal].visible.need-chip:hover,
+    [data-reveal].visible.capability:hover,
+    [data-reveal].visible.platform:hover,
+    [data-reveal].visible.deliverable:hover {
+      transform: translate3d(0, -6px, 0) scale(1.01);
+    }
+
+    [data-delay="1"] { transition-delay: 0.08s; }
+    [data-delay="2"] { transition-delay: 0.16s; }
+    [data-delay="3"] { transition-delay: 0.24s; }
+    [data-delay="4"] { transition-delay: 0.32s; }
+
+    .mobile-safe-mode *,
+    .mobile-safe-mode *::before,
+    .mobile-safe-mode *::after {
+      animation: none !important;
+      transition-duration: 0.01ms !important;
+      scroll-behavior: auto !important;
+    }
+
+    .mobile-safe-mode #networkCanvas,
+    .mobile-safe-mode .intro-overlay {
+      display: none !important;
+    }
+
+    .mobile-safe-mode [data-reveal] {
+      opacity: 1 !important;
+      transform: none !important;
+    }
+
+    @media (max-width: 980px) {
+      h1 {
+        font-size: 3.1rem;
+      }
+
+      h2 {
+        font-size: 2.15rem;
+      }
+
+      .hero-inner,
+      .skill-wall,
+      .daily-lab,
+      .split {
+        grid-template-columns: 1fr;
+      }
+
+      .hero-media {
+        min-height: 440px;
+      }
+
+      .grid.three,
+      .grid.four,
+      .platform-map,
+      .spine-grid,
+      .deliverables,
+      .need-finder {
+        grid-template-columns: repeat(2, minmax(0, 1fr));
+      }
+
+      .quote {
+        position: static;
+      }
+    }
+
+    @media (max-width: 720px) {
+      #networkCanvas {
+        display: none;
+      }
+
+      .intro-overlay {
+        background: #050505;
+      }
+
+      .intro-lockup {
+        animation-duration: 0.56s;
+      }
+
+      body::after {
+        display: none;
+      }
+
+      .ticker-track {
+        width: max-content;
+        animation: mobileMarquee 46s linear infinite;
+        will-change: transform;
+      }
+
+      .ticker-group {
+        flex-wrap: nowrap;
+      }
+
+      .brand-mark::after {
+        animation: markSheen 7.2s ease-in-out infinite;
+      }
+
+      .gradient-text {
+        animation: gradientDrift 9s ease-in-out infinite;
+      }
+
+      .hero-media::before {
+        opacity: 0.72;
+        animation: edgeTrace 9s ease-in-out infinite;
+      }
+
+      .radar-line {
+        top: 12%;
+        animation: mobileScan 6.2s var(--ease) infinite;
+        will-change: transform, opacity;
+      }
+
+      .signal-node {
+        animation: mobileFloat 7.2s ease-in-out infinite;
+        will-change: transform;
+      }
+
+      .signal-node:nth-child(3) { animation-delay: 0.7s; }
+      .signal-node:nth-child(4) { animation-delay: 1.4s; }
+      .signal-node:nth-child(5) { animation-delay: 2.1s; }
+
+      .bar i {
+        animation: mobileBarPulse 5.8s ease-in-out infinite;
+        will-change: transform, opacity;
+      }
+
+      .skill-rings {
+        animation: mobileRingBreathe 8.5s ease-in-out infinite;
+      }
+
+      .ring-label {
+        animation: mobileLabelFloat 7.6s ease-in-out infinite;
+      }
+
+      .ring-label.b { animation-delay: 0.8s; }
+      .ring-label.c { animation-delay: 1.6s; }
+      .ring-label.d { animation-delay: 2.4s; }
+
+      .pipeline-step {
+        animation: mobileStepGlow 7.8s ease-in-out infinite;
+      }
+
+      .pipeline-step:nth-child(2) { animation-delay: 0.5s; }
+      .pipeline-step:nth-child(3) { animation-delay: 1s; }
+      .pipeline-step:nth-child(4) { animation-delay: 1.5s; }
+      .pipeline-step:nth-child(5) { animation-delay: 2s; }
+
+      @keyframes mobileScan {
+        0% { opacity: 0; transform: translate3d(0, 0, 0); }
+        15% { opacity: 0.9; }
+        78% { opacity: 0.9; }
+        100% { opacity: 0; transform: translate3d(0, 230px, 0); }
+      }
+
+      @keyframes mobileFloat {
+        0%, 100% { transform: translate3d(0, 0, 0); }
+        50% { transform: translate3d(0, -6px, 0); }
+      }
+
+      @keyframes mobileBarPulse {
+        0%, 100% { transform: translate3d(-6%, 0, 0); opacity: 0.78; }
+        50% { transform: translate3d(6%, 0, 0); opacity: 1; }
+      }
+
+      @keyframes mobileMarquee {
+        from { transform: translate3d(0, 0, 0); }
+        to { transform: translate3d(-50%, 0, 0); }
+      }
+
+      @keyframes mobileRingBreathe {
+        0%, 100% { box-shadow: 0 0 0 rgba(0, 240, 255, 0); border-color: rgba(0, 240, 255, 0.25); }
+        50% { box-shadow: 0 0 34px rgba(0, 240, 255, 0.16); border-color: rgba(255, 82, 82, 0.34); }
+      }
+
+      @keyframes mobileLabelFloat {
+        0%, 100% { transform: translate3d(0, 0, 0); }
+        50% { transform: translate3d(0, -4px, 0); }
+      }
+
+      @keyframes mobileStepGlow {
+        0%, 100% { border-color: rgba(255, 255, 255, 0.13); background: rgba(5, 5, 5, 0.42); }
+        50% { border-color: rgba(0, 240, 255, 0.3); background: rgba(0, 240, 255, 0.055); }
+      }
+
+      .nav,
+      .proof-pill,
+      .requirement-strip,
+      .signal-node,
+      .spine-cell,
+      .card,
+      .slice,
+      .timeline-row,
+      .deliverable,
+      .share-panel,
+      .daily-board,
+      .bottom-cta {
+        -webkit-backdrop-filter: none;
+        backdrop-filter: none;
+      }
+
+      [data-reveal] {
+        transform: translate3d(0, 16px, 0);
+        will-change: auto;
+        transition-duration: 0.46s;
+      }
+
+      [data-reveal="fade-left"],
+      [data-reveal="fade-right"],
+      [data-reveal="scale"] {
+        transform: translate3d(0, 16px, 0);
+      }
+
+      .nav-links {
+        display: none;
+      }
+
+      .nav-inner {
+        min-height: 58px;
+        padding: 0 14px;
+      }
+
+      .brand-name small {
+        display: none;
+      }
+
+      .hero {
+        min-height: auto;
+        padding: 40px 16px 22px;
+      }
+
+      h1 {
+        font-size: 2.38rem;
+      }
+
+      h2 {
+        font-size: 1.82rem;
+      }
+
+      .hero-lead {
+        font-size: 1rem;
+      }
+
+      .hero-proof,
+      .grid.two,
+      .grid.three,
+      .grid.four,
+      .capability-grid,
+      .platform-map,
+      .spine-grid,
+      .deliverables,
+      .need-finder,
+      .topic-stack {
+        grid-template-columns: 1fr;
+      }
+
+      .signal-spine {
+        padding: 48px 16px 52px;
+      }
+
+      .spine-word {
+        top: -28px;
+        left: 0;
+        right: auto;
+        font-size: 3.4rem;
+      }
+
+      .spine-cell {
+        min-height: 170px;
+      }
+
+      .requirement-strip {
+        grid-template-columns: 1fr;
+      }
+
+      .daily-board {
+        min-height: auto;
+      }
+
+      .pipeline-step {
+        grid-template-columns: 1fr;
+      }
+
+      .hero-media {
+        min-height: 410px;
+      }
+
+      .console-bottom {
+        grid-template-columns: 1fr;
+      }
+
+      .meter {
+        padding: 10px;
+      }
+
+      .section {
+        padding: 58px 16px;
+      }
+
+      .timeline-row {
+        grid-template-columns: 1fr;
+        gap: 8px;
+      }
+
+      .skill-rings {
+        position: relative;
+        right: auto;
+        bottom: auto;
+        margin: 32px auto 0;
+        width: 230px;
+        height: 230px;
+      }
+
+      .copy-row {
+        align-items: stretch;
+        flex-direction: column;
+      }
+
+      .button {
+        width: 100%;
+      }
+
+      .bottom-cta {
+        display: flex;
+      }
+
+      .footer-inner {
+        align-items: flex-start;
+        flex-direction: column;
+      }
+    }
+
+    @media (max-width: 390px) {
+      h1 {
+        font-size: 2.08rem;
+      }
+
+      .hero-media {
+        min-height: 370px;
+      }
+
+      .signal-console {
+        inset: 14px;
+      }
+
+      .signal-node {
+        min-width: 74px;
+        font-size: 0.74rem;
+      }
+
+      .ticker span {
+        font-size: 0.82rem;
+      }
+    }
+
+    @media (prefers-reduced-motion: reduce) {
+      *,
+      *::before,
+      *::after {
+        animation-duration: 0.01ms !important;
+        animation-iteration-count: 1 !important;
+        scroll-behavior: auto !important;
+        transition-duration: 0.01ms !important;
+      }
+    }
+  </style>
+</head>
+<body>
+  <canvas id="networkCanvas" aria-hidden="true"></canvas>
+  <div class="progress" id="progress"></div>
+  <div class="intro-overlay" aria-hidden="true">
+    <div class="intro-lockup">
+      <strong>VOC.market</strong>
+      <span>Social Voice Intelligence Workshop</span>
+      <div class="intro-rule"></div>
+    </div>
+  </div>
+
+  <header class="nav">
+    <div class="nav-inner">
+      <a class="brand" href="#top" aria-label="VOC.market">
+        <span class="brand-mark">V</span>
+        <span class="brand-name">VOC.market<small>小红书 x 抖音需求洞察</small></span>
+      </a>
+      <nav class="nav-links" aria-label="页面导航">
+        <a href="#capability">课程能力</a>
+        <a href="#daily">口播日报</a>
+        <a href="#schedule">课程安排</a>
+        <a href="#deliverables">现场交付</a>
+      </nav>
+    </div>
+  </header>
+
+  <main id="top">
+    <section class="hero">
+      <div class="hero-inner">
+        <div class="hero-copy" data-reveal>
+          <div class="eyebrow">南昌本地 · 小红书 + 抖音 · 社媒 VOC 线下工作坊</div>
+          <h1>1天学会从评论里找用户需求,<span class="gradient-text">再做成选题和报告</span></h1>
+          <p class="hero-lead">VOC 就是“用户真实声音”。这堂课聚焦小红书和抖音:看大家怎么搜、怎么问、怎么吐槽、怎么夸,再把这些评论变成产品卖点、内容选题和一份可汇报的需求洞察。</p>
+          <div class="hero-actions">
+            <a class="button primary" href="#schedule">看这一天学什么</a>
+            <a class="button" href="#daily">看抖音选题日报</a>
+          </div>
+          <div class="hero-proof" aria-label="课程关键信息">
+            <div class="proof-pill">
+              <strong>2个平台</strong>
+              <span>小红书 + 抖音重点实操</span>
+            </div>
+            <div class="proof-pill">
+              <strong>5月28日下午</strong>
+              <span>安装环境,配好龙虾技能</span>
+            </div>
+            <div class="proof-pill">
+              <strong>5月29日全天</strong>
+              <span>做 VOC 分析、选题和报告</span>
+            </div>
+          </div>
+          <div class="requirement-strip" data-reveal data-delay="1">
+            <div class="requirement-badge">REQ</div>
+            <div>
+              <b>参会要求:请提前下载并安装好龙虾</b>
+              <span>5月28日下午会帮你把小红书/抖音采集技能配置好,确保 5月29日全天实战能跟着老师一起跑。</span>
+            </div>
+          </div>
+        </div>
+
+        <div class="hero-media" data-reveal data-delay="2" data-tilt>
+          <img src="https://images.unsplash.com/photo-1551288049-bebda4e38f71?auto=format&fit=crop&w=1400&q=82" alt="社媒数据分析工作台">
+          <div class="signal-console">
+            <div class="console-top">
+              <span class="console-dots" aria-hidden="true"><span></span><span></span><span></span></span>
+              <span>VOC.market / 小红书 x 抖音需求雷达</span>
+            </div>
+            <div class="radar" aria-label="社媒信号雷达">
+              <div class="radar-line"></div>
+              <div class="signal-node">抖音<small>视频 / 评论 / 回复</small></div>
+              <div class="signal-node">小红书<small>笔记 / 评论 / 搜索词</small></div>
+              <div class="signal-node">用户需求<small>痛点 / 阻碍 / 场景</small></div>
+              <div class="signal-node">选题报告<small>内容 / 卖点 / 动作</small></div>
+            </div>
+            <div class="console-bottom">
+              <div class="meter"><span>内容信号</span><div class="bar"><i></i></div></div>
+              <div class="meter"><span>用户情绪</span><div class="bar"><i></i></div></div>
+              <div class="meter"><span>行动优先级</span><div class="bar"><i></i></div></div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </section>
+
+    <section class="ticker" aria-label="支持平台与能力">
+      <div class="ticker-track">
+        <div class="ticker-group">
+          <span>小红书笔记 / 评论 / 搜索词</span>
+          <span>抖音视频 / 评论 / 热评回复</span>
+          <span>从评论里找真实需求</span>
+          <span>把需求变成内容选题</span>
+          <span>生成可汇报的 VOC 报告</span>
+          <span>douyin-speaking-daily 每日选题雷达</span>
+          <span>口播逐字稿 / 爆款结构拆解</span>
+          <span>HTML VOC 报告 / 交付审计</span>
+        </div>
+        <div class="ticker-group" aria-hidden="true">
+          <span>小红书笔记 / 评论 / 搜索词</span>
+          <span>抖音视频 / 评论 / 热评回复</span>
+          <span>从评论里找真实需求</span>
+          <span>把需求变成内容选题</span>
+          <span>生成可汇报的 VOC 报告</span>
+          <span>douyin-speaking-daily 每日选题雷达</span>
+          <span>口播逐字稿 / 爆款结构拆解</span>
+          <span>HTML VOC 报告 / 交付审计</span>
+        </div>
+      </div>
+    </section>
+
+    <section class="signal-spine" aria-label="社媒 VOC 工作流">
+      <div class="spine-inner">
+        <div class="spine-word">COMMENT TO INSIGHT</div>
+        <div class="spine-grid">
+          <div class="spine-cell" data-reveal>
+            <em>01 / 搜内容</em>
+            <strong>先找到相关笔记和视频</strong>
+            <span>围绕你的产品、品类和竞品,在小红书、抖音里找到用户正在看的内容。</span>
+            <b>01</b>
+          </div>
+          <div class="spine-cell" data-reveal data-delay="1">
+            <em>02 / 看评论</em>
+            <strong>再看大家到底在问什么</strong>
+            <span>高赞评论、追问、吐槽和回复,往往比标题更接近真实需求。</span>
+            <b>02</b>
+          </div>
+          <div class="spine-cell" data-reveal data-delay="2">
+            <em>03 / 找需求</em>
+            <strong>把评论整理成需求清单</strong>
+            <span>用户为什么想买、为什么犹豫、在什么场景使用,现场一步步拆出来。</span>
+            <b>03</b>
+          </div>
+          <div class="spine-cell" data-reveal data-delay="3">
+            <em>04 / 做结果</em>
+            <strong>变成选题、卖点和报告</strong>
+            <span>最后输出内容选题、口播方向、产品卖点和一份可汇报的 VOC 报告。</span>
+            <b>04</b>
+          </div>
+        </div>
+      </div>
+    </section>
+
+    <section class="section">
+      <div class="wrap">
+        <div class="section-head center" data-reveal>
+          <div class="section-kicker">这堂课到底解决什么</div>
+          <h2>看懂小红书和抖音评论,你就能更快知道用户想要什么。</h2>
+          <p class="sub">很多团队每天刷内容,却没有把评论区里的问题整理成需求。这堂课会用龙虾帮你把“刷到的内容”变成“能指导产品和内容的证据”。</p>
+        </div>
+        <div class="grid four">
+          <article class="card" data-reveal>
+            <span class="index">01</span>
+            <h3>内容很多,看不过来</h3>
+            <p>同一个品类每天都有大量笔记和视频,靠人工刷很容易漏掉重要信号。</p>
+          </article>
+          <article class="card" data-reveal data-delay="1">
+            <span class="index rose">02</span>
+            <h3>评论里才有真实问题</h3>
+            <p>用户会在评论里问价格、效果、适不适合自己,也会说出不买的原因。</p>
+          </article>
+          <article class="card" data-reveal data-delay="2">
+            <span class="index amber">03</span>
+            <h3>爆款不能只靠照抄</h3>
+            <p>要知道它为什么能火:开头怎么抓人、哪里解决顾虑、评论为什么愿意互动。</p>
+          </article>
+          <article class="card" data-reveal data-delay="3">
+            <span class="index green">04</span>
+            <h3>结论要有证据</h3>
+            <p>每个需求、卖点和选题都要能回到原始评论,不做拍脑袋的判断。</p>
+          </article>
+        </div>
+        <div class="need-finder" aria-label="社媒 VOC 需求发现路径">
+          <div class="need-chip" data-reveal>
+            <strong>评论找需求</strong>
+            <span>把“想要、担心、不会用、太贵、有没有替代品”转成需求和购买阻碍。</span>
+          </div>
+          <div class="need-chip" data-reveal data-delay="1">
+            <strong>回复找证据</strong>
+            <span>热评下的追问和反驳,是判断需求强弱和人群分层的关键证据。</span>
+          </div>
+          <div class="need-chip" data-reveal data-delay="2">
+            <strong>账号找节奏</strong>
+            <span>监听竞品、达人和标杆账号,看到内容节奏、选题方向和转化动作。</span>
+          </div>
+          <div class="need-chip" data-reveal data-delay="3">
+            <strong>爆款找结构</strong>
+            <span>从逐字稿拆钩子、冲突、证明点和评论触发点,迁移成自己的口播框架。</span>
+          </div>
+        </div>
+      </div>
+    </section>
+
+    <section class="section alt" id="capability">
+      <div class="wrap">
+        <div class="skill-wall">
+          <div class="slice" data-reveal>
+            <div class="section-kicker">Capability Stack</div>
+            <h3>我们现场讲的不是一个工具,而是一条已经技能化的社媒 VOC 生产线。</h3>
+            <p>从技能配置、采集、归一化、洞察、口播日报到 HTML 报告,课程会把可交付的能力拆成学员能看懂、能复制、能回公司继续用的流程。</p>
+            <div class="skill-rings" aria-hidden="true">
+              <span class="ring-label a">采集</span>
+              <span class="ring-label b">理解</span>
+              <span class="ring-label c">报告</span>
+              <span class="ring-label d">行动</span>
+            </div>
+          </div>
+
+          <div class="capability-grid">
+            <div class="capability" data-reveal data-delay="1">
+              <strong>小红书/抖音采集</strong>
+              <span>正课优先跑通小红书和抖音:关键词搜索、笔记/视频详情、评论、热评回复、账号内容。</span>
+            </div>
+            <div class="capability" data-reveal data-delay="2">
+              <strong>账号与话题监听</strong>
+              <span>围绕关键词、竞品账号、达人内容和话题讨论持续观察,沉淀内容节奏、互动表现和需求变化。</span>
+            </div>
+            <div class="capability" data-reveal data-delay="3">
+              <strong>douyin-speaking-daily</strong>
+              <span>关键词监听、账号作品监听、评论采集、可选逐字稿补跑、每日选题日报、脚本共创和偏好沉淀。</span>
+            </div>
+            <div class="capability" data-reveal data-delay="4">
+              <strong>报告与审计</strong>
+              <span>VOC 数据归一化、mini report、HTML v2 报告生成、证据卡、行动建议和交付前审计。</span>
+            </div>
+          </div>
+        </div>
+      </div>
+    </section>
+
+    <section class="section" id="daily">
+      <div class="wrap daily-lab">
+        <div class="daily-copy" data-reveal>
+          <div class="section-head">
+            <div class="section-kicker">douyin-speaking-daily</div>
+            <h2>把抖音变成每天可刷新的口播选题雷达。</h2>
+            <p class="sub">这条能力适合讲给内容团队和老板听:不是“我今天想发什么”,而是“今天用户和竞品正在把什么问题推到台前”。</p>
+          </div>
+          <div class="daily-points">
+            <div class="daily-point">
+              <b>01</b>
+              <div>
+                <strong>先建立账号 profile</strong>
+                <span>行业、人群、账号定位、转化目标、禁忌表达和历史偏好先沉淀,后续选题不跑偏。</span>
+              </div>
+            </div>
+            <div class="daily-point">
+              <b>02</b>
+              <div>
+                <strong>再跑关键词与账号监听</strong>
+                <span>关键词视频、确认账号近期作品、评论和回复一起看,找到需求和内容机会。</span>
+              </div>
+            </div>
+            <div class="daily-point">
+              <b>03</b>
+              <div>
+                <strong>最后进入脚本共创</strong>
+                <span>选题 N、改稿、定稿都能续接同一次日报,把爆款结构迁移成自己的口播稿。</span>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <div class="daily-board" data-reveal data-delay="2" data-tilt>
+          <div class="board-inner">
+            <div class="board-header">
+              <div class="board-title">
+                <strong>Daily Speaking Radar</strong>
+                <span>runner -> report -> script co-creation</span>
+              </div>
+              <span class="board-status">P0 closed loop</span>
+            </div>
+
+            <div class="pipeline">
+              <div class="pipeline-step">
+                <code>profile</code>
+                <div>
+                  <strong>账号定位与转化目标</strong>
+                  <span>读取行业、人群、话术边界和脚本偏好。</span>
+                </div>
+                <i>1</i>
+              </div>
+              <div class="pipeline-step">
+                <code>monitor</code>
+                <div>
+                  <strong>关键词 + 账号作品监听</strong>
+                  <span>抓取爆款视频、竞品账号近期作品和互动证据。</span>
+                </div>
+                <i>2</i>
+              </div>
+              <div class="pipeline-step">
+                <code>comments</code>
+                <div>
+                  <strong>评论 / 回复需求信号</strong>
+                  <span>从高赞评论、追问和反驳里识别痛点、阻碍和场景。</span>
+                </div>
+                <i>3</i>
+              </div>
+              <div class="pipeline-step">
+                <code>transcript</code>
+                <div>
+                  <strong>可选补跑逐字稿</strong>
+                  <span>拆开头、冲突、证明点、结尾动作和评论触发点。</span>
+                </div>
+                <i>4</i>
+              </div>
+              <div class="pipeline-step">
+                <code>daily report</code>
+                <div>
+                  <strong>今日选题池与共创稿</strong>
+                  <span>输出选题置信度、可讲方向、60 秒稿和定稿沉淀。</span>
+                </div>
+                <i>5</i>
+              </div>
+            </div>
+
+            <div class="topic-stack">
+              <div class="topic-card">
+                <small>TOPIC 01 / 高置信</small>
+                <strong>用户不是不买,是不确定它适不适合自己</strong>
+                <span>评论证据:反复询问场景、预算和替代方案。</span>
+              </div>
+              <div class="topic-card">
+                <small>TOPIC 02 / 可转化</small>
+                <strong>把专家术语翻译成真实使用场景</strong>
+                <span>逐字稿证据:爆款用冲突开场,再用案例证明。</span>
+              </div>
+              <div class="topic-card">
+                <small>TOPIC 03 / 明日监听</small>
+                <strong>竞品新内容评论区出现同类追问</strong>
+                <span>账号证据:近期作品互动集中在购买阻碍。</span>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </section>
+
+    <section class="section">
+      <div class="wrap">
+        <div class="section-head" data-reveal>
+          <div class="section-kicker">Platform Map</div>
+          <h2>聚焦小红书和抖音,把“内容”和“评论”整理成需求证据。</h2>
+          <p class="sub">学员不用背 API 名称,只需要理解:搜什么内容、看哪些评论、怎么判断需求、最后怎么变成选题和报告。</p>
+        </div>
+        <div class="platform-map">
+          <div class="platform" data-reveal>
+            <b>抖音</b>
+            <span>关键词视频、话题、视频详情、评论、回复、账号主页、近期作品、逐字稿和口播结构。</span>
+          </div>
+          <div class="platform" data-reveal data-delay="1">
+            <b>小红书</b>
+            <span>关键词笔记、笔记详情、评论、作者信息,适合测评、种草、痛点和生活方式洞察。</span>
+          </div>
+          <div class="platform" data-reveal data-delay="2">
+            <b>热评回复</b>
+            <span>重点看高赞评论下面的追问、反驳和补充,判断需求是不是足够真实。</span>
+          </div>
+          <div class="platform" data-reveal data-delay="3">
+            <b>竞品账号</b>
+            <span>观察竞品和达人近期内容,找出他们正在解决的问题和评论区的新机会。</span>
+          </div>
+          <div class="platform" data-reveal>
+            <b>话题与关键词</b>
+            <span>把平台热词、搜索词、评论词和用户原话统一到采集矩阵,避免只看单一爆款。</span>
+          </div>
+          <div class="platform" data-reveal data-delay="1">
+            <b>评论与回复</b>
+            <span>把高赞评论、追问、反驳、复购表达转成需求、阻碍、证据和内容钩子。</span>
+          </div>
+          <div class="platform" data-reveal data-delay="2">
+            <b>账号监听</b>
+            <span>确认竞品/达人账号,持续观察近期作品和评论变化,形成每日选题雷达。</span>
+          </div>
+          <div class="platform" data-reveal data-delay="3">
+            <b>报告工厂</b>
+            <span>把小红书和抖音样本合并到同一 VOC 结构里,输出 HTML 报告、审计结果和行动清单。</span>
+          </div>
+        </div>
+      </div>
+    </section>
+
+    <section class="section alt" id="schedule">
+      <div class="wrap">
+        <div class="section-head" data-reveal>
+          <div class="section-kicker">Workshop Flow</div>
+          <h2>5月28日下午安装环境,5月29日全天做 VOC 分析实战。</h2>
+          <p class="sub">南昌本地线下工作坊。5月29日全程用真实业务问题操作,建议每位学员带一个自己的品牌、品类、竞品账号或口播账号方向。</p>
+        </div>
+        <div class="timeline">
+          <article class="timeline-row" data-reveal>
+            <div class="time">5月28日下午<br>安装环境</div>
+            <div>
+              <h3>下载并安装 OpenClaw(龙虾),现场配置社媒 VOC 技能</h3>
+              <p>这半天重点不是讲理论,而是把 VOC Token、社媒采集技能、douyin-speaking-daily、工作区和样例项目配置到能跑,为第二天全天实战做好准备。</p>
+              <div class="tags">
+                <span>参会前下载 OpenClaw</span>
+                <span>配置 VOC Token</span>
+                <span>配置社媒技能包</span>
+                <span>跑通 douyin-speaking-daily Demo</span>
+              </div>
+            </div>
+          </article>
+
+          <article class="timeline-row" data-reveal data-delay="1">
+            <div class="time">5月29日上午<br>VOC 需求发现</div>
+            <div>
+              <h3>从业务问题出发,用社媒找真实 VOC 需求</h3>
+              <p>现场跑抖音/小红书主线,并讲清社媒采集的同构方法:如何找关键词、如何选样本、如何从评论/回复里判断需求强度。</p>
+              <div class="tags">
+                <span>小红书/抖音采集思路</span>
+                <span>抖音视频与评论</span>
+                <span>小红书笔记与评论</span>
+                <span>需求 / 阻碍 / 场景证据</span>
+              </div>
+            </div>
+          </article>
+
+          <article class="timeline-row" data-reveal data-delay="2">
+            <div class="time">5月29日下午<br>口播日报与报告</div>
+            <div>
+              <h3>用 douyin-speaking-daily 生成选题雷达,再输出 VOC 报告</h3>
+              <p>用评论证据生成 VOC 洞察,用逐字稿和爆款结构拆出可复用口播框架,再把结论落到产品、内容、投放和销售动作。</p>
+              <div class="tags">
+                <span>每日选题日报</span>
+                <span>VOC 数据归一化</span>
+                <span>痛点/场景/阻碍聚类</span>
+                <span>HTML 报告与审计</span>
+              </div>
+            </div>
+          </article>
+
+          <article class="timeline-row" data-reveal data-delay="3">
+            <div class="time">5月29日收尾<br>现场交付</div>
+            <div>
+              <h3>每组交付一套自己的社媒 VOC 工作样板</h3>
+              <p>不是听完就散,而是现场验收技能配置、采集矩阵、证据包、洞察报告、口播选题和下一步行动清单。</p>
+              <div class="tags">
+                <span>技能配置完成</span>
+                <span>采集矩阵</span>
+                <span>证据卡</span>
+                <span>洞察报告</span>
+              </div>
+            </div>
+          </article>
+        </div>
+      </div>
+    </section>
+
+    <section class="section" id="deliverables">
+      <div class="wrap">
+        <div class="section-head center" data-reveal>
+          <div class="section-kicker">Deliverables</div>
+          <h2>现场带走 6 份能继续工作的资产。</h2>
+          <p class="sub">我们把交付物设计成企业内部可复盘、可协作、可继续迭代的格式。</p>
+        </div>
+        <div class="deliverables">
+          <div class="deliverable" data-reveal>
+            <span class="mark">1</span>
+            <strong>技能配置完成</strong>
+            <span>龙虾、VOC Token、社媒采集技能和 douyin-speaking-daily 跑通。</span>
+          </div>
+          <div class="deliverable" data-reveal data-delay="1">
+            <span class="mark">2</span>
+            <strong>社媒采集矩阵</strong>
+            <span>平台、关键词、账号、话题、样本量、采集目的和风险边界。</span>
+          </div>
+          <div class="deliverable" data-reveal data-delay="2">
+            <span class="mark">3</span>
+            <strong>用户原声证据包</strong>
+            <span>视频/笔记/评论/回复来源清晰,能回溯,不靠拍脑袋。</span>
+          </div>
+          <div class="deliverable" data-reveal data-delay="3">
+            <span class="mark">4</span>
+            <strong>社媒 VOC 报告</strong>
+            <span>痛点、场景、阻碍、卖点、内容机会和行动建议。</span>
+          </div>
+          <div class="deliverable" data-reveal data-delay="4">
+            <span class="mark">5</span>
+            <strong>口播日报与选题池</strong>
+            <span>每日趋势判断、选题置信度、60 秒稿和可复用结构框架。</span>
+          </div>
+          <div class="deliverable" data-reveal data-delay="4">
+            <span class="mark">6</span>
+            <strong>行动优先级清单</strong>
+            <span>产品优化、内容选题、投放测试、账号监听的下一步。</span>
+          </div>
+        </div>
+      </div>
+    </section>
+
+    <section class="section alt">
+      <div class="wrap split">
+        <div class="quote" data-reveal>
+          <p>适合真正想把社媒数据变成增长动作的人。</p>
+          <span>企业主、品牌负责人、产品负责人、内容/运营/增长团队,都能带着自己的业务问题来。</span>
+        </div>
+        <div class="grid two">
+          <article class="card" data-reveal data-delay="1">
+            <span class="index">A</span>
+            <h3>推荐参加</h3>
+            <ul class="list">
+              <li>正在做小红书、抖音的品牌和内容团队。</li>
+              <li>想从评论区、竞品账号和爆款视频中提炼产品/内容机会的人。</li>
+              <li>需要把 AI 工作流变成团队 SOP 和客户交付能力的顾问/服务商。</li>
+              <li>希望从“会用 AI”升级为“能用 AI 交付报告和策略”的业务负责人。</li>
+            </ul>
+          </article>
+          <article class="card" data-reveal data-delay="2">
+            <span class="index rose">B</span>
+            <h3>报名准备</h3>
+            <ul class="list">
+              <li>参会前请提前下载并安装好 OpenClaw(龙虾)。</li>
+              <li>请务必带电脑,全程实操。</li>
+              <li>建议提前准备一个品类、品牌、竞品、达人或口播账号方向。</li>
+              <li>5月28日下午统一安装环境、配置 VOC Token 和社媒 VOC 技能。</li>
+              <li>真实项目涉及敏感数据时,课堂可用脱敏关键词和公开样本。</li>
+            </ul>
+          </article>
+          <article class="card" data-reveal data-delay="3">
+            <span class="index amber">C</span>
+            <h3>活动信息</h3>
+            <ul class="list">
+              <li>形式:南昌本地线下工作坊。</li>
+              <li>时间:5月28日下午安装环境,5月29日全天 VOC 分析实战。</li>
+              <li>人数:建议 30 人以内,方便现场辅导。</li>
+              <li>地点:南昌本地场地,具体位置另行同步。</li>
+              <li>费用:以最终通知为准。</li>
+            </ul>
+          </article>
+          <article class="card" data-reveal data-delay="4">
+            <span class="index green">D</span>
+            <h3>品牌背书</h3>
+            <ul class="list">
+              <li>VOC.market 企业级 VOC 智能工厂方法论。</li>
+              <li>OpenClaw 龙虾技能包驱动,覆盖采集、分析、报告和审计。</li>
+              <li>小红书/抖音内容、用户评论和热评回复可组合成可追溯洞察。</li>
+              <li>目标不是一次性报告,而是训练团队拥有持续情报能力。</li>
+            </ul>
+          </article>
+        </div>
+      </div>
+    </section>
+
+  </main>
+
+  <nav class="bottom-cta" aria-label="底部导航">
+    <a class="button" href="#schedule">课程安排</a>
+    <a class="button primary" href="#deliverables">现场交付</a>
+  </nav>
+
+  <footer>
+    <div class="footer-inner">
+      <div class="brand">
+        <span class="brand-mark">V</span>
+        <span class="brand-name">VOC.market<small>VOC 为先 · 内容为王 · 增长为果</small></span>
+      </div>
+      <span>社媒 VOC 龙虾工作坊单页初稿</span>
+    </div>
+  </footer>
+
+  <div class="toast" id="toast">已复制</div>
+
+  <script>
+    const progress = document.getElementById("progress");
+    const revealItems = document.querySelectorAll("[data-reveal]");
+    const toast = document.getElementById("toast");
+    const introOverlay = document.querySelector(".intro-overlay");
+    const nav = document.querySelector(".nav");
+    const sectionBlocks = document.querySelectorAll(".section, .signal-spine, .hero");
+    const canvas = document.getElementById("networkCanvas");
+    const ctx = canvas ? canvas.getContext("2d") : null;
+    const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
+    const isTouchLike = window.matchMedia("(hover: none), (pointer: coarse)").matches;
+    const isMobileSafeMode = document.documentElement.classList.contains("mobile-safe-mode");
+
+    function updateProgress() {
+      const scrollTop = window.scrollY || document.documentElement.scrollTop;
+      const maxScroll = document.documentElement.scrollHeight - window.innerHeight;
+      const pct = maxScroll > 0 ? (scrollTop / maxScroll) * 100 : 0;
+      if (progress) progress.style.width = pct + "%";
+      if (nav) nav.classList.toggle("is-scrolled", scrollTop > 12);
+    }
+
+    revealItems.forEach((item, index) => {
+      if (!item.getAttribute("data-reveal")) {
+        if (item.matches(".hero-media, .daily-board, .share-panel, .quote")) {
+          item.setAttribute("data-reveal", "scale");
+        } else if (item.matches(".section-head, .daily-copy, .slice")) {
+          item.setAttribute("data-reveal", "fade-left");
+        } else if (index % 3 === 1) {
+          item.setAttribute("data-reveal", "fade-right");
+        } else if (index % 3 === 2) {
+          item.setAttribute("data-reveal", "scale");
+        }
+      }
+    });
+
+    if (isMobileSafeMode) {
+      revealItems.forEach((item) => item.classList.add("visible"));
+      sectionBlocks.forEach((item) => item.classList.add("section-active"));
+    } else if ("IntersectionObserver" in window) {
+      const revealObserver = new IntersectionObserver((entries) => {
+        entries.forEach((entry) => {
+          if (entry.isIntersecting) {
+            entry.target.classList.add("visible");
+            revealObserver.unobserve(entry.target);
+          }
+        });
+      }, { threshold: 0.12, rootMargin: "0px 0px -60px 0px" });
+
+      revealItems.forEach((item) => revealObserver.observe(item));
+
+      const sectionObserver = new IntersectionObserver((entries) => {
+        entries.forEach((entry) => {
+          entry.target.classList.toggle("section-active", entry.isIntersecting);
+        });
+      }, { threshold: 0.18, rootMargin: "-12% 0px -58% 0px" });
+
+      sectionBlocks.forEach((item) => sectionObserver.observe(item));
+    } else {
+      revealItems.forEach((item) => item.classList.add("visible"));
+      sectionBlocks.forEach((item) => item.classList.add("section-active"));
+    }
+
+    function showToast(message) {
+      toast.textContent = message;
+      toast.classList.add("show");
+      window.setTimeout(() => toast.classList.remove("show"), 1800);
+    }
+
+    function setupTilt() {
+      if (prefersReducedMotion || isTouchLike) return;
+      document.querySelectorAll("[data-tilt]").forEach((el) => {
+        el.addEventListener("mousemove", (event) => {
+          const rect = el.getBoundingClientRect();
+          const x = (event.clientX - rect.left) / rect.width - 0.5;
+          const y = (event.clientY - rect.top) / rect.height - 0.5;
+          el.style.transform = `perspective(900px) rotateY(${x * 5}deg) rotateX(${y * -5}deg)`;
+        });
+        el.addEventListener("mouseleave", () => {
+          el.style.transform = "perspective(900px) rotateY(0deg) rotateX(0deg)";
+        });
+      });
+    }
+
+    let nodes = [];
+    let animationId = null;
+    let networkStarted = false;
+
+    function shouldRunNetwork() {
+      return Boolean(ctx) && !prefersReducedMotion && !isMobileSafeMode && !isTouchLike && window.innerWidth >= 760;
+    }
+
+    function stopNetwork() {
+      if (animationId) {
+        cancelAnimationFrame(animationId);
+        animationId = null;
+      }
+      networkStarted = false;
+      if (ctx) ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
+    }
+
+    function resizeCanvas() {
+      if (!ctx || !canvas) return;
+      const ratio = window.devicePixelRatio || 1;
+      canvas.width = Math.floor(window.innerWidth * ratio);
+      canvas.height = Math.floor(window.innerHeight * ratio);
+      canvas.style.width = window.innerWidth + "px";
+      canvas.style.height = window.innerHeight + "px";
+      ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
+      createNodes();
+    }
+
+    function createNodes() {
+      const mobile = window.innerWidth < 720;
+      const count = mobile
+        ? Math.min(24, Math.max(12, Math.floor(window.innerWidth / 38)))
+        : Math.min(58, Math.max(24, Math.floor(window.innerWidth / 26)));
+      nodes = Array.from({ length: count }, () => ({
+        x: Math.random() * window.innerWidth,
+        y: Math.random() * window.innerHeight,
+        vx: (Math.random() - 0.5) * (mobile ? 0.22 : 0.36),
+        vy: (Math.random() - 0.5) * (mobile ? 0.22 : 0.36),
+        r: Math.random() * 1.6 + 0.6
+      }));
+    }
+
+    function drawNetwork() {
+      if (!ctx || !shouldRunNetwork()) {
+        stopNetwork();
+        return;
+      }
+      ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
+      const mobile = window.innerWidth < 720;
+      const connectDistance = mobile ? 104 : 132;
+      nodes.forEach((node, index) => {
+        node.x += node.vx;
+        node.y += node.vy;
+        if (node.x < 0 || node.x > window.innerWidth) node.vx *= -1;
+        if (node.y < 0 || node.y > window.innerHeight) node.vy *= -1;
+
+        ctx.beginPath();
+        ctx.arc(node.x, node.y, node.r, 0, Math.PI * 2);
+        ctx.fillStyle = mobile ? "rgba(0, 240, 255, 0.38)" : "rgba(0, 240, 255, 0.5)";
+        ctx.fill();
+
+        for (let j = index + 1; j < nodes.length; j++) {
+          const other = nodes[j];
+          const dx = node.x - other.x;
+          const dy = node.y - other.y;
+          const dist = Math.sqrt(dx * dx + dy * dy);
+          if (dist < connectDistance) {
+            ctx.beginPath();
+            ctx.moveTo(node.x, node.y);
+            ctx.lineTo(other.x, other.y);
+            ctx.strokeStyle = `rgba(255, 255, 255, ${0.13 * (1 - dist / connectDistance)})`;
+            ctx.lineWidth = 1;
+            ctx.stroke();
+          }
+        }
+      });
+      animationId = requestAnimationFrame(drawNetwork);
+    }
+
+    function startNetwork() {
+      if (networkStarted || !shouldRunNetwork()) return;
+      networkStarted = true;
+      resizeCanvas();
+      drawNetwork();
+    }
+
+    function finishIntro() {
+      if (!introOverlay || introOverlay.classList.contains("is-gone")) {
+        startNetwork();
+        return;
+      }
+
+      introOverlay.classList.add("is-hiding");
+      window.setTimeout(() => {
+        introOverlay.classList.add("is-gone");
+        startNetwork();
+      }, prefersReducedMotion ? 20 : 440);
+    }
+
+    window.addEventListener("scroll", updateProgress, { passive: true });
+    window.addEventListener("resize", () => {
+      if (shouldRunNetwork()) {
+        if (networkStarted) resizeCanvas();
+        else startNetwork();
+      } else {
+        stopNetwork();
+      }
+    }, { passive: true });
+    document.addEventListener("visibilitychange", () => {
+      if (document.hidden) stopNetwork();
+      else startNetwork();
+    });
+    setupTilt();
+    updateProgress();
+
+    if (document.readyState === "complete") {
+      window.setTimeout(finishIntro, prefersReducedMotion ? 0 : (window.innerWidth < 720 ? 420 : 820));
+    } else {
+      window.addEventListener("load", () => {
+        window.setTimeout(finishIntro, prefersReducedMotion ? 0 : (window.innerWidth < 720 ? 420 : 820));
+      }, { once: true });
+    }
+
+    window.setTimeout(finishIntro, prefersReducedMotion ? 0 : 1800);
+
+    window.addEventListener("beforeunload", () => {
+      if (animationId) cancelAnimationFrame(animationId);
+    });
+  </script>
+</body>
+</html>

+ 909 - 0
docs/workshop/voc-openclaw-workshop-intro.html

@@ -0,0 +1,909 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <meta name="format-detection" content="telephone=no">
+  <title>AI x VOC 龙虾工作坊|半天加餐 + 1天线下实战</title>
+  <meta name="description" content="AI x VOC 龙虾工作坊:前一天下午配置基础环境,正课1天用龙虾完成VOC采集、洞察分析和行动方案输出。">
+  <style>
+    :root {
+      --ink: #151516;
+      --muted: #5f6368;
+      --paper: #f7f4ee;
+      --surface: #ffffff;
+      --line: #ded8ce;
+      --red: #ef4b39;
+      --teal: #0097a7;
+      --green: #4e9f3d;
+      --gold: #d99a12;
+      --blue: #276ef1;
+      --shadow: 0 16px 34px rgba(21, 21, 22, 0.12);
+      --radius: 8px;
+    }
+
+    * {
+      box-sizing: border-box;
+    }
+
+    html {
+      scroll-behavior: smooth;
+      background: var(--paper);
+    }
+
+    body {
+      margin: 0;
+      color: var(--ink);
+      background: var(--paper);
+      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
+      line-height: 1.6;
+      -webkit-font-smoothing: antialiased;
+    }
+
+    a {
+      color: inherit;
+      text-decoration: none;
+    }
+
+    button,
+    textarea {
+      font: inherit;
+    }
+
+    .topbar {
+      position: sticky;
+      top: 0;
+      z-index: 30;
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      min-height: 56px;
+      padding: 10px 18px;
+      border-bottom: 1px solid rgba(222, 216, 206, 0.78);
+      background: rgba(247, 244, 238, 0.88);
+      backdrop-filter: blur(16px);
+    }
+
+    .brand {
+      display: flex;
+      align-items: center;
+      gap: 10px;
+      font-weight: 800;
+      line-height: 1.1;
+    }
+
+    .brand-mark {
+      display: inline-grid;
+      place-items: center;
+      width: 32px;
+      height: 32px;
+      border: 2px solid var(--ink);
+      border-radius: 8px;
+      background: var(--red);
+      color: #fff;
+      font-size: 0.86rem;
+    }
+
+    .topbar a {
+      display: inline-flex;
+      align-items: center;
+      justify-content: center;
+      min-height: 36px;
+      padding: 8px 12px;
+      border: 1px solid var(--ink);
+      border-radius: 8px;
+      background: var(--ink);
+      color: #fff;
+      font-size: 0.9rem;
+      font-weight: 700;
+    }
+
+    .hero {
+      position: relative;
+      min-height: 88svh;
+      display: flex;
+      align-items: flex-end;
+      overflow: hidden;
+      color: #fff;
+      background:
+        linear-gradient(180deg, rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0.76)),
+        url("https://images.unsplash.com/photo-1552664730-d307ca884978?auto=format&fit=crop&w=1600&q=82") center/cover no-repeat,
+        #222;
+    }
+
+    .hero::after {
+      content: "";
+      position: absolute;
+      inset: auto 0 0;
+      height: 34%;
+      background: linear-gradient(180deg, rgba(247, 244, 238, 0), var(--paper));
+      pointer-events: none;
+    }
+
+    .hero-inner {
+      position: relative;
+      z-index: 1;
+      width: min(100%, 960px);
+      margin: 0 auto;
+      padding: 92px 20px 70px;
+    }
+
+    .kicker {
+      display: inline-flex;
+      align-items: center;
+      gap: 8px;
+      margin-bottom: 18px;
+      padding: 7px 10px;
+      border: 1px solid rgba(255, 255, 255, 0.55);
+      border-radius: 8px;
+      background: rgba(21, 21, 22, 0.34);
+      font-size: 0.92rem;
+      font-weight: 700;
+    }
+
+    h1,
+    h2,
+    h3,
+    p {
+      margin: 0;
+    }
+
+    h1 {
+      max-width: 760px;
+      font-size: 3rem;
+      line-height: 1.08;
+      letter-spacing: 0;
+    }
+
+    .hero-lead {
+      max-width: 680px;
+      margin-top: 18px;
+      color: rgba(255, 255, 255, 0.88);
+      font-size: 1.12rem;
+    }
+
+    .hero-actions {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 10px;
+      margin-top: 28px;
+    }
+
+    .hero-actions a,
+    .copy-button {
+      display: inline-flex;
+      align-items: center;
+      justify-content: center;
+      min-height: 44px;
+      padding: 11px 16px;
+      border-radius: 8px;
+      border: 1px solid #fff;
+      background: #fff;
+      color: var(--ink);
+      font-weight: 800;
+      cursor: pointer;
+    }
+
+    .hero-actions a.secondary {
+      background: rgba(255, 255, 255, 0.08);
+      color: #fff;
+    }
+
+    .quick-facts {
+      position: relative;
+      z-index: 2;
+      display: grid;
+      grid-template-columns: repeat(3, 1fr);
+      gap: 10px;
+      width: min(calc(100% - 40px), 960px);
+      margin: -42px auto 0;
+    }
+
+    .fact {
+      min-height: 86px;
+      padding: 14px;
+      border-radius: 8px;
+      background: var(--surface);
+      box-shadow: var(--shadow);
+    }
+
+    .fact strong {
+      display: block;
+      font-size: 1.25rem;
+      line-height: 1.2;
+    }
+
+    .fact span {
+      display: block;
+      margin-top: 5px;
+      color: var(--muted);
+      font-size: 0.88rem;
+    }
+
+    .band {
+      padding: 58px 20px;
+    }
+
+    .band.surface {
+      background: var(--surface);
+    }
+
+    .band.dark {
+      background: var(--ink);
+      color: #fff;
+    }
+
+    .wrap {
+      width: min(100%, 960px);
+      margin: 0 auto;
+    }
+
+    .section-head {
+      display: grid;
+      gap: 10px;
+      max-width: 720px;
+      margin-bottom: 26px;
+    }
+
+    .eyebrow {
+      color: var(--red);
+      font-size: 0.86rem;
+      font-weight: 900;
+    }
+
+    h2 {
+      font-size: 2rem;
+      line-height: 1.2;
+      letter-spacing: 0;
+    }
+
+    .sub {
+      color: var(--muted);
+      font-size: 1rem;
+    }
+
+    .dark .sub {
+      color: rgba(255, 255, 255, 0.72);
+    }
+
+    .grid {
+      display: grid;
+      gap: 14px;
+    }
+
+    .grid.two {
+      grid-template-columns: repeat(2, 1fr);
+    }
+
+    .grid.three {
+      grid-template-columns: repeat(3, 1fr);
+    }
+
+    .card {
+      padding: 18px;
+      border: 1px solid var(--line);
+      border-radius: 8px;
+      background: var(--surface);
+    }
+
+    .dark .card {
+      border-color: rgba(255, 255, 255, 0.18);
+      background: rgba(255, 255, 255, 0.08);
+    }
+
+    .card h3 {
+      margin-bottom: 8px;
+      font-size: 1.08rem;
+      line-height: 1.35;
+      letter-spacing: 0;
+    }
+
+    .card p,
+    .card li {
+      color: var(--muted);
+      font-size: 0.95rem;
+    }
+
+    .dark .card p,
+    .dark .card li {
+      color: rgba(255, 255, 255, 0.74);
+    }
+
+    .num {
+      display: inline-grid;
+      place-items: center;
+      width: 30px;
+      height: 30px;
+      margin-bottom: 12px;
+      border-radius: 8px;
+      background: var(--ink);
+      color: #fff;
+      font-weight: 900;
+      font-size: 0.84rem;
+    }
+
+    .num.red {
+      background: var(--red);
+    }
+
+    .num.teal {
+      background: var(--teal);
+    }
+
+    .num.green {
+      background: var(--green);
+    }
+
+    .num.gold {
+      background: var(--gold);
+      color: var(--ink);
+    }
+
+    .list {
+      display: grid;
+      gap: 10px;
+      margin: 0;
+      padding: 0;
+      list-style: none;
+    }
+
+    .list li {
+      position: relative;
+      padding-left: 18px;
+    }
+
+    .list li::before {
+      content: "";
+      position: absolute;
+      top: 0.72em;
+      left: 0;
+      width: 7px;
+      height: 7px;
+      border-radius: 50%;
+      background: var(--red);
+    }
+
+    .schedule {
+      display: grid;
+      gap: 12px;
+    }
+
+    .schedule-item {
+      display: grid;
+      grid-template-columns: 132px 1fr;
+      gap: 18px;
+      padding: 18px;
+      border: 1px solid var(--line);
+      border-radius: 8px;
+      background: var(--surface);
+    }
+
+    .time {
+      color: var(--teal);
+      font-weight: 900;
+    }
+
+    .schedule-item h3 {
+      margin-bottom: 8px;
+      font-size: 1.14rem;
+      letter-spacing: 0;
+    }
+
+    .tag-row {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 8px;
+      margin-top: 12px;
+    }
+
+    .tag {
+      display: inline-flex;
+      align-items: center;
+      min-height: 30px;
+      padding: 5px 9px;
+      border-radius: 8px;
+      background: #f1eee8;
+      color: var(--ink);
+      font-size: 0.84rem;
+      font-weight: 700;
+    }
+
+    .dark .tag {
+      background: rgba(255, 255, 255, 0.13);
+      color: #fff;
+    }
+
+    .notice {
+      display: grid;
+      gap: 10px;
+      padding: 18px;
+      border-left: 5px solid var(--red);
+      border-radius: 8px;
+      background: #fff7ee;
+    }
+
+    .notice strong {
+      font-size: 1.15rem;
+    }
+
+    .meta-grid {
+      display: grid;
+      grid-template-columns: repeat(4, 1fr);
+      gap: 10px;
+      margin-top: 18px;
+    }
+
+    .meta {
+      padding: 14px;
+      border-radius: 8px;
+      background: #f1eee8;
+    }
+
+    .meta span {
+      display: block;
+      color: var(--muted);
+      font-size: 0.82rem;
+      font-weight: 700;
+    }
+
+    .meta strong {
+      display: block;
+      margin-top: 5px;
+      font-size: 1rem;
+      line-height: 1.35;
+    }
+
+    .share-box {
+      display: grid;
+      gap: 12px;
+    }
+
+    textarea {
+      width: 100%;
+      min-height: 270px;
+      resize: vertical;
+      padding: 16px;
+      border: 1px solid rgba(255, 255, 255, 0.22);
+      border-radius: 8px;
+      outline: none;
+      background: rgba(255, 255, 255, 0.08);
+      color: #fff;
+      line-height: 1.7;
+    }
+
+    .copy-button {
+      justify-self: start;
+      border-color: var(--red);
+      background: var(--red);
+      color: #fff;
+    }
+
+    .bottom-cta {
+      position: sticky;
+      bottom: 0;
+      z-index: 28;
+      display: none;
+      gap: 10px;
+      padding: 10px 14px calc(10px + env(safe-area-inset-bottom));
+      border-top: 1px solid rgba(222, 216, 206, 0.78);
+      background: rgba(247, 244, 238, 0.94);
+      backdrop-filter: blur(16px);
+    }
+
+    .bottom-cta a {
+      flex: 1;
+      display: inline-flex;
+      align-items: center;
+      justify-content: center;
+      min-height: 44px;
+      padding: 10px 12px;
+      border-radius: 8px;
+      font-weight: 900;
+    }
+
+    .bottom-cta .primary {
+      background: var(--red);
+      color: #fff;
+    }
+
+    .bottom-cta .ghost {
+      border: 1px solid var(--ink);
+      background: #fff;
+      color: var(--ink);
+    }
+
+    .toast {
+      position: fixed;
+      left: 50%;
+      bottom: 86px;
+      z-index: 40;
+      transform: translateX(-50%) translateY(12px);
+      opacity: 0;
+      pointer-events: none;
+      padding: 9px 12px;
+      border-radius: 8px;
+      background: var(--ink);
+      color: #fff;
+      font-size: 0.9rem;
+      transition: opacity 0.2s ease, transform 0.2s ease;
+    }
+
+    .toast.show {
+      opacity: 1;
+      transform: translateX(-50%) translateY(0);
+    }
+
+    @media (max-width: 760px) {
+      .topbar {
+        padding: 9px 14px;
+      }
+
+      .brand {
+        font-size: 0.92rem;
+      }
+
+      .topbar a {
+        display: none;
+      }
+
+      .hero {
+        min-height: 88svh;
+      }
+
+      .hero-inner {
+        padding: 84px 18px 68px;
+      }
+
+      h1 {
+        font-size: 2.38rem;
+      }
+
+      h2 {
+        font-size: 1.55rem;
+      }
+
+      .hero-lead {
+        font-size: 1rem;
+      }
+
+      .quick-facts,
+      .grid.two,
+      .grid.three,
+      .meta-grid {
+        grid-template-columns: 1fr;
+      }
+
+      .quick-facts {
+        width: calc(100% - 28px);
+        margin-top: -34px;
+      }
+
+      .fact {
+        min-height: auto;
+      }
+
+      .band {
+        padding: 44px 16px;
+      }
+
+      .schedule-item {
+        grid-template-columns: 1fr;
+        gap: 8px;
+      }
+
+      .bottom-cta {
+        display: flex;
+      }
+    }
+
+    @media (max-width: 380px) {
+      h1 {
+        font-size: 2.08rem;
+      }
+
+      .hero-actions a,
+      .copy-button {
+        width: 100%;
+      }
+    }
+  </style>
+</head>
+<body>
+  <header class="topbar">
+    <div class="brand">
+      <span class="brand-mark">VOC</span>
+      <span>AI x VOC<br>龙虾工作坊</span>
+    </div>
+    <a href="#apply">查看报名信息</a>
+  </header>
+
+  <main>
+    <section class="hero">
+      <div class="hero-inner">
+        <div class="kicker">半天加餐 + 1天线下实战</div>
+        <h1>AI x VOC 龙虾工作坊</h1>
+        <p class="hero-lead">用一天时间,把“用户声音”变成可复用的 AI 情报官:会采集、会分析、会输出报告,也能沉淀成团队下一轮产品、内容和增长动作。</p>
+        <div class="hero-actions">
+          <a href="#schedule">看课程安排</a>
+          <a class="secondary" href="#share">复制转发文案</a>
+        </div>
+      </div>
+    </section>
+
+    <div class="quick-facts" aria-label="课程关键信息">
+      <div class="fact">
+        <strong>0基础可来</strong>
+        <span>前一天下午专门配置环境</span>
+      </div>
+      <div class="fact">
+        <strong>全程实操</strong>
+        <span>请务必带电脑参与</span>
+      </div>
+      <div class="fact">
+        <strong>当天出结果</strong>
+        <span>带走一份VOC洞察报告</span>
+      </div>
+    </div>
+
+    <section class="band">
+      <div class="wrap">
+        <div class="section-head">
+          <div class="eyebrow">为什么需要这堂课</div>
+          <h2>AI 工具装了很多,但真正缺的是一条能跑通业务的 VOC 流程。</h2>
+          <p class="sub">这不是听概念的课,而是把龙虾装好、把数据跑起来、把结论交付出来的线下工作坊。</p>
+        </div>
+        <div class="grid three">
+          <article class="card">
+            <span class="num red">01</span>
+            <h3>用户声音散落各处</h3>
+            <p>评论、笔记、短视频、竞品反馈都有价值,但团队很难稳定收集和复盘。</p>
+          </article>
+          <article class="card">
+            <span class="num teal">02</span>
+            <h3>洞察停在“感觉不错”</h3>
+            <p>知道用户在说什么,却难转成明确卖点、产品优化和内容选题。</p>
+          </article>
+          <article class="card">
+            <span class="num green">03</span>
+            <h3>AI 没进入日常工作</h3>
+            <p>团队会提问,但还没有形成可重复执行、可交接、可迭代的工作流。</p>
+          </article>
+        </div>
+      </div>
+    </section>
+
+    <section class="band surface">
+      <div class="wrap">
+        <div class="section-head">
+          <div class="eyebrow">1天直给结果</div>
+          <h2>上午找到真实需求,下午把洞察变成行动。</h2>
+        </div>
+        <div class="grid two">
+          <article class="card">
+            <span class="num teal">AM</span>
+            <h3>上午收获</h3>
+            <ul class="list">
+              <li>跑通龙虾 VOC 采集流程,明确平台、关键词和竞品范围。</li>
+              <li>识别用户真实场景、核心痛点、购买阻碍和高频表达。</li>
+              <li>搭建品牌/品类的第一版 VOC 情报框架。</li>
+            </ul>
+          </article>
+          <article class="card">
+            <span class="num gold">PM</span>
+            <h3>下午收获</h3>
+            <ul class="list">
+              <li>生成一份可汇报的 VOC 洞察报告。</li>
+              <li>把洞察转成卖点重写、内容选题、产品优化和增长假设。</li>
+              <li>沉淀一套可复用的 AI 情报官工作流,课后继续迭代。</li>
+            </ul>
+          </article>
+        </div>
+      </div>
+    </section>
+
+    <section class="band" id="schedule">
+      <div class="wrap">
+        <div class="section-head">
+          <div class="eyebrow">课程安排</div>
+          <h2>前一天下午补齐基础,正课一天集中产出。</h2>
+        </div>
+        <div class="schedule">
+          <article class="schedule-item">
+            <div class="time">前一天下午<br>约3小时</div>
+            <div>
+              <h3>零基础加餐:安装龙虾,配置基础环境</h3>
+              <p class="sub">适合没有安装过龙虾、AI 工具基础薄弱、担心正课跟不上的同学。目标是课前把环境和账号跑通。</p>
+              <div class="tag-row">
+                <span class="tag">安装龙虾</span>
+                <span class="tag">配置浏览器/账号</span>
+                <span class="tag">跑通首个 Demo</span>
+                <span class="tag">建立工作区</span>
+              </div>
+            </div>
+          </article>
+          <article class="schedule-item">
+            <div class="time">正课上午<br>9:30-12:30</div>
+            <div>
+              <h3>VOC 采集与需求洞察</h3>
+              <p class="sub">从业务问题出发,定义品类、竞品、关键词和目标平台,用龙虾采集用户声音,并完成第一轮结构化分析。</p>
+              <div class="tag-row">
+                <span class="tag">问题定义</span>
+                <span class="tag">关键词地图</span>
+                <span class="tag">评论/内容采集</span>
+                <span class="tag">痛点聚类</span>
+              </div>
+            </div>
+          </article>
+          <article class="schedule-item">
+            <div class="time">正课下午<br>14:00-17:30</div>
+            <div>
+              <h3>报告生成与业务动作落地</h3>
+              <p class="sub">把洞察报告做出来,再转换成产品、内容、投放和销售可以直接使用的动作清单,最后现场验收。</p>
+              <div class="tag-row">
+                <span class="tag">VOC报告</span>
+                <span class="tag">卖点重塑</span>
+                <span class="tag">内容选题</span>
+                <span class="tag">行动路线图</span>
+              </div>
+            </div>
+          </article>
+        </div>
+      </div>
+    </section>
+
+    <section class="band dark">
+      <div class="wrap">
+        <div class="section-head">
+          <div class="eyebrow">现场带走</div>
+          <h2>不是带走一堆截图,而是带走能继续工作的“AI 情报官”。</h2>
+          <p class="sub">课程结束时,每个小组至少完成一套自己的业务样板。</p>
+        </div>
+        <div class="grid three">
+          <article class="card">
+            <span class="num red">A</span>
+            <h3>龙虾环境</h3>
+            <p>本机完成安装和基础配置,课后可以继续跑自己的项目。</p>
+          </article>
+          <article class="card">
+            <span class="num teal">B</span>
+            <h3>VOC洞察报告</h3>
+            <p>包含用户场景、痛点、需求、阻碍、卖点和机会判断。</p>
+          </article>
+          <article class="card">
+            <span class="num green">C</span>
+            <h3>行动清单</h3>
+            <p>沉淀3-5个下一步可以执行的产品、内容或增长动作。</p>
+          </article>
+        </div>
+      </div>
+    </section>
+
+    <section class="band surface">
+      <div class="wrap">
+        <div class="section-head">
+          <div class="eyebrow">适合谁</div>
+          <h2>适合希望把 AI 真正放进业务流程的人。</h2>
+        </div>
+        <div class="grid two">
+          <article class="card">
+            <h3>推荐参加</h3>
+            <ul class="list">
+              <li>企业主、品牌负责人、产品负责人。</li>
+              <li>运营、内容、增长、市场团队骨干。</li>
+              <li>想用 AI 做竞品分析、用户洞察、选题和卖点提炼的人。</li>
+              <li>已经试过 AI,但还没形成稳定工作流的团队。</li>
+            </ul>
+          </article>
+          <article class="card">
+            <h3>不太适合</h3>
+            <ul class="list">
+              <li>只想听趋势,不打算现场动手。</li>
+              <li>无法带电脑参与实操。</li>
+              <li>完全不愿意提供真实业务问题或样本方向。</li>
+            </ul>
+          </article>
+        </div>
+      </div>
+    </section>
+
+    <section class="band" id="apply">
+      <div class="wrap">
+        <div class="notice">
+          <strong>报名提醒:请务必带电脑。</strong>
+          <p class="sub">正课会现场操作龙虾、采集数据、生成报告;零基础同学建议参加前一天下午的环境配置加餐。</p>
+        </div>
+        <div class="meta-grid">
+          <div class="meta">
+            <span>时间</span>
+            <strong>前一天下午 + 正课1天</strong>
+          </div>
+          <div class="meta">
+            <span>地点</span>
+            <strong>线下场地,报名后通知</strong>
+          </div>
+          <div class="meta">
+            <span>人数</span>
+            <strong>建议30人以内</strong>
+          </div>
+          <div class="meta">
+            <span>费用</span>
+            <strong>待定 / 以最终通知为准</strong>
+          </div>
+        </div>
+      </div>
+    </section>
+
+    <section class="band dark" id="share">
+      <div class="wrap">
+        <div class="section-head">
+          <div class="eyebrow">微信群转发文案</div>
+          <h2>下面这段可以先作为接龙报名初稿。</h2>
+        </div>
+        <div class="share-box">
+          <textarea id="shareText" readonly>【AI x VOC 龙虾工作坊|半天加餐 + 1天线下实战】
+
+你是否遇到:
+• 用户评论很多,但不知道怎么变成产品和卖点洞察?
+• AI 工具装了不少,但团队还没有稳定的业务工作流?
+• 竞品、内容、用户反馈都在变化,却缺少持续跟踪的情报系统?
+
+1天直给结果:
+上午收获:
+- 跑通龙虾 VOC 采集流程
+- 找到用户真实场景、痛点、购买阻碍和高频表达
+- 搭建品牌/品类的第一版 VOC 情报框架
+
+下午收获:
+- 输出一份可汇报的 VOC 洞察报告
+- 把洞察转成卖点重写、内容选题、产品优化和增长动作
+- 沉淀一套可复用的 AI 情报官工作流
+
+前一天下午加餐:
+- 面向零基础/没有安装龙虾的同学
+- 完成龙虾安装、账号配置、基础环境检查
+- 跑通第一个 Demo,确保正课能跟上
+
+时间:前一天下午 + 正课1天(具体日期待定)
+地点:线下场地,报名后通知
+人数:建议30人以内
+费用:待定 / 以最终通知为准
+
+请务必带电脑,全程实操。
+接龙报名:姓名 + 公司/业务 + 是否需要参加前一天加餐</textarea>
+          <button class="copy-button" type="button" id="copyBtn">复制转发文案</button>
+        </div>
+      </div>
+    </section>
+  </main>
+
+  <nav class="bottom-cta" aria-label="底部报名导航">
+    <a class="ghost" href="#schedule">课程安排</a>
+    <a class="primary" href="#share">复制文案</a>
+  </nav>
+
+  <div class="toast" id="toast">已复制</div>
+
+  <script>
+    const copyBtn = document.getElementById("copyBtn");
+    const shareText = document.getElementById("shareText");
+    const toast = document.getElementById("toast");
+
+    function showToast(message) {
+      toast.textContent = message;
+      toast.classList.add("show");
+      window.setTimeout(() => toast.classList.remove("show"), 1800);
+    }
+
+    copyBtn.addEventListener("click", async () => {
+      try {
+        await navigator.clipboard.writeText(shareText.value);
+        showToast("已复制转发文案");
+      } catch (error) {
+        shareText.focus();
+        shareText.select();
+        showToast("已选中文案,可手动复制");
+      }
+    });
+  </script>
+</body>
+</html>

+ 499 - 0
docs/讯飞转文字/讯飞实时转录接口调用文档.md

@@ -0,0 +1,499 @@
+# 讯飞实时语音转写(AST)接口调用文档
+
+## 一、概述
+
+项目使用讯飞 **实时语音转写大模型(AST)** 接口,通过 WebSocket 协议实现边说边转录。前端直接连接讯飞服务器,无需后端代理。
+
+### 服务架构
+
+```
+UnifiedVoiceASTService (统一封装层)
+    └── XunfeiASTRealtimeService (底层实现)
+            ├── WebSocket 连接讯飞 AST 服务
+            ├── AudioContext + ScriptProcessor 采集 PCM 音频
+            └── MediaRecorder 保存完整录音文件
+```
+
+- **`XunfeiASTRealtimeService`**:核心实现,负责 WebSocket 鉴权、音频采集/发送、转录结果解析
+- **`UnifiedVoiceASTService`**:上层封装,提供统一的录音状态管理和回调接口
+
+---
+
+## 二、API 凭证配置
+
+```typescript
+const XUNFEI_AST_CONFIG = {
+  appId: '50f4a46c',
+  apiKey: '106ddc40dfd4b9ca6d7b47c70fada749',
+  apiSecret: 'NzFlNmFhZDJjMDNkZGM3NzI0Mzg2OGNm',
+  wsUrl: 'wss://office-api-ast-dx.iflyaisol.com/ast/communicate/v1'
+};
+```
+
+| 字段 | 说明 |
+|------|------|
+| `appId` | 讯飞开放平台应用 ID |
+| `apiKey` | 用于鉴权签名的 AccessKeyId |
+| `apiSecret` | 用于 HmacSHA1 签名的密钥(AccessKeySecret) |
+| `wsUrl` | WebSocket 接入地址 |
+
+---
+
+## 三、鉴权方式(签名生成)
+
+### 鉴权原理
+
+讯飞 AST 接口使用 **URL Query 参数鉴权**,不需要额外的 HTTP Header 或 Token 服务器。所有鉴权信息直接拼接在 WebSocket URL 的 query string 中。核心是通过 `apiSecret` 对请求参数生成 HmacSHA1 签名,服务端验证签名合法性。
+
+**无需单独请求 Token,签名在客户端本地生成即可。**
+
+### 签名算法:HmacSHA1 + Base64
+
+依赖:`npm install crypto-js`(或 `pnpm add crypto-js`)
+
+```typescript
+import * as CryptoJS from 'crypto-js';
+```
+
+#### 签名生成步骤
+
+```typescript
+function generateSignature(params: Record<string, string>, apiSecret: string): string {
+  // Step 1: 将所有请求参数(不含 signature 本身)按参数名 ASCII 升序排序
+  const sortedKeys = Object.keys(params).sort();
+  
+  // Step 2: 对每个参数的 key 和 value 分别做 URL 编码(encodeURIComponent)
+  // Step 3: 按 "编码key=编码value" 格式用 & 拼接成 baseString
+  const baseString = sortedKeys
+    .map(key => {
+      const encodedKey = encodeURIComponent(key);
+      const encodedValue = encodeURIComponent(params[key]);
+      return `${encodedKey}=${encodedValue}`;
+    })
+    .join('&');
+  
+  // Step 4: 以 apiSecret 为密钥,对 baseString 做 HmacSHA1
+  const hmac = CryptoJS.HmacSHA1(baseString, apiSecret);
+  
+  // Step 5: 将 HmacSHA1 结果做 Base64 编码,得到最终签名
+  const signature = CryptoJS.enc.Base64.stringify(hmac);
+  
+  return signature;
+}
+```
+
+#### 签名计算示例(可用于验证你的实现)
+
+假设参数如下:
+
+```
+accessKeyId = "106ddc40dfd4b9ca6d7b47c70fada749"
+appId       = "50f4a46c"
+audio_encode = "pcm_s16le"
+lang        = "autodialect"
+samplerate  = "16000"
+utc         = "2025-05-07T19:06:30+0800"
+uuid        = "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
+```
+
+**Step 1 - 排序后的 key 顺序:**
+```
+accessKeyId, appId, audio_encode, lang, samplerate, utc, uuid
+```
+
+**Step 2/3 - 拼接的 baseString:**
+```
+accessKeyId=106ddc40dfd4b9ca6d7b47c70fada749&appId=50f4a46c&audio_encode=pcm_s16le&lang=autodialect&samplerate=16000&utc=2025-05-07T19%3A06%3A30%2B0800&uuid=a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d
+```
+
+> 注意:`utc` 值中的 `:` 被编码为 `%3A`,`+` 被编码为 `%2B`
+
+**Step 4/5 - HmacSHA1(baseString, apiSecret) → Base64:**
+```
+apiSecret = "NzFlNmFhZDJjMDNkZGM3NzI0Mzg2OGNm"
+signature = HmacSHA1(baseString, apiSecret) → Base64 编码后的字符串
+```
+
+### 完整的 WebSocket URL 构建(可直接复制使用)
+
+```typescript
+import * as CryptoJS from 'crypto-js';
+
+// ===== 配置(替换为你的凭证) =====
+const CONFIG = {
+  appId: '50f4a46c',
+  apiKey: '106ddc40dfd4b9ca6d7b47c70fada749',
+  apiSecret: 'NzFlNmFhZDJjMDNkZGM3NzI0Mzg2OGNm',
+  wsUrl: 'wss://office-api-ast-dx.iflyaisol.com/ast/communicate/v1'
+};
+
+// ===== 工具函数 =====
+
+/** 生成 UUID v4 */
+function generateUUID(): string {
+  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
+    const r = Math.random() * 16 | 0;
+    const v = c === 'x' ? r : (r & 0x3 | 0x8);
+    return v.toString(16);
+  });
+}
+
+/** 生成 UTC 时间字符串(格式:2025-05-07T19:06:30+0800) */
+function getUTCString(): string {
+  const now = new Date();
+  const pad = (n: number) => String(n).padStart(2, '0');
+  const timezoneOffset = now.getTimezoneOffset();
+  const offsetSign = timezoneOffset <= 0 ? '+' : '-';
+  const offsetH = pad(Math.floor(Math.abs(timezoneOffset) / 60));
+  const offsetM = pad(Math.abs(timezoneOffset) % 60);
+  return `${now.getFullYear()}-${pad(now.getMonth()+1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}${offsetSign}${offsetH}${offsetM}`;
+}
+
+/** 生成 HmacSHA1 签名 */
+function generateSignature(params: Record<string, string>, apiSecret: string): string {
+  const baseString = Object.keys(params)
+    .sort()
+    .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
+    .join('&');
+  const hmac = CryptoJS.HmacSHA1(baseString, apiSecret);
+  return CryptoJS.enc.Base64.stringify(hmac);
+}
+
+// ===== 构建带签名的 WebSocket URL =====
+
+function buildWebSocketUrl(options?: { enableSpeakerSeparation?: boolean; lang?: 'autodialect' | 'autominor' }): string {
+  const params: Record<string, string> = {
+    accessKeyId: CONFIG.apiKey,          // 注意:这里填的是 apiKey,不是 apiSecret
+    appId: CONFIG.appId,
+    uuid: generateUUID(),
+    utc: getUTCString(),
+    audio_encode: 'pcm_s16le',
+    lang: options?.lang || 'autodialect',
+    samplerate: '16000'
+  };
+
+  // 可选:启用说话人分离
+  if (options?.enableSpeakerSeparation) {
+    params.role_type = '2';
+  }
+
+  // 生成签名(签名参数不包含 signature 自身)
+  params.signature = generateSignature(params, CONFIG.apiSecret);
+
+  // 拼接最终 URL
+  const queryString = Object.entries(params)
+    .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
+    .join('&');
+
+  return `${CONFIG.wsUrl}?${queryString}`;
+}
+
+// ===== 使用示例 =====
+const wsUrl = buildWebSocketUrl({ enableSpeakerSeparation: true, lang: 'autodialect' });
+const ws = new WebSocket(wsUrl);
+
+ws.onopen = () => console.log('连接成功,可以开始发送音频');
+ws.onmessage = (event) => console.log('收到转录结果:', JSON.parse(event.data));
+ws.onerror = (err) => console.error('连接错误:', err);
+```
+
+### 鉴权要点总结
+
+| 要点 | 说明 |
+|------|------|
+| 签名位置 | URL query 参数 `signature=xxx` |
+| 签名算法 | HmacSHA1(baseString, apiSecret) → Base64 |
+| baseString 构成 | 所有参数(不含 signature)按 key 升序 → URL编码 → `key=value&` 拼接 |
+| 密钥 | `apiSecret`(AccessKeySecret) |
+| `accessKeyId` 值 | 填 `apiKey`(AccessKeyId),**不是** apiSecret |
+| 时间参数 | 本地时间 + 时区偏移,格式 `YYYY-MM-DDTHH:mm:ss±HHMM` |
+| UUID | 每次请求唯一,标准 UUID v4 |
+| 无需 Token 服务 | 签名纯客户端计算,无需额外请求服务端获取 Token |
+
+---
+
+## 四、WebSocket 连接参数
+
+### 连接 URL 格式
+
+```
+wss://office-api-ast-dx.iflyaisol.com/ast/communicate/v1?accessKeyId=xxx&appId=xxx&uuid=xxx&utc=xxx&audio_encode=pcm_s16le&lang=autodialect&samplerate=16000&signature=xxx
+```
+
+### 参数列表
+
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `accessKeyId` | string | ✅ | API Key(AccessKeyId,不是 Secret) |
+| `appId` | string | ✅ | 应用 ID |
+| `uuid` | string | ✅ | 唯一请求标识(UUID v4,每次连接重新生成) |
+| `utc` | string | ✅ | 当前本地时间 + 时区,格式:`2025-09-04T15:38:07+0800` |
+| `audio_encode` | string | ✅ | 音频编码格式,固定 `pcm_s16le` |
+| `lang` | string | ✅ | 语种模式:`autodialect`(中英+方言)或 `autominor`(37种语种) |
+| `samplerate` | string | ✅ | 采样率,固定 `16000` |
+| `role_type` | string | ❌ | 说话人分离:传 `2` 表示启用多说话人识别 |
+| `signature` | string | ✅ | HmacSHA1 签名(参见第三节完整示例) |
+
+### UTC 时间格式生成
+
+```typescript
+function getUTCString(): string {
+  const now = new Date();
+  const year = now.getFullYear();
+  const month = String(now.getMonth() + 1).padStart(2, '0');
+  const day = String(now.getDate()).padStart(2, '0');
+  const hours = String(now.getHours()).padStart(2, '0');
+  const minutes = String(now.getMinutes()).padStart(2, '0');
+  const seconds = String(now.getSeconds()).padStart(2, '0');
+  
+  // 时区偏移(分钟)
+  const timezoneOffset = now.getTimezoneOffset();
+  const offsetHours = Math.floor(Math.abs(timezoneOffset) / 60);
+  const offsetMinutes = Math.abs(timezoneOffset) % 60;
+  const offsetSign = timezoneOffset <= 0 ? '+' : '-';
+  const offsetString = `${offsetSign}${String(offsetHours).padStart(2, '0')}${String(offsetMinutes).padStart(2, '0')}`;
+  
+  return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}${offsetString}`;
+  // 输出示例:2025-05-07T19:06:30+0800
+}
+```
+
+---
+
+## 五、音频采集与发送
+
+### 音频配置
+
+```typescript
+const AUDIO_CONFIG = {
+  sampleRate: 16000,   // 16K 采样率
+  channels: 1,         // 单声道
+  bitsPerSample: 16    // 16bit 深度
+};
+```
+
+### 麦克风权限请求
+
+```typescript
+const audioStream = await navigator.mediaDevices.getUserMedia({
+  audio: {
+    sampleRate: 16000,
+    channelCount: 1,
+    echoCancellation: true,
+    noiseSuppression: true,
+    autoGainControl: true
+  }
+});
+```
+
+### 音频处理链路
+
+```
+麦克风 → MediaStream → AudioContext(16kHz)
+                           ├── MediaRecorder(保存完整录音 webm)
+                           └── ScriptProcessor(bufferSize=256)
+                                   ↓
+                            Float32 → Int16 PCM
+                                   ↓
+                            缓冲区队列(audioBuffer)
+                                   ↓
+                         定时器(20ms) → WebSocket.send(binary)
+```
+
+### 关键实现细节
+
+1. **ScriptProcessor** 缓冲区大小 256 帧(≈16ms 一次回调)
+2. **PCM 转换**:Float32 [-1.0, 1.0] → Int16 [-32768, 32767]
+3. **定时发送**:每 20ms 从缓冲区取 1280 字节(640 个 Int16 样本)发送
+4. **发送方式**:`WebSocket.send(pcmData.buffer)` — 直接发送二进制 ArrayBuffer
+
+```typescript
+// Float32 → Int16 转换
+private convertFloat32ToInt16(float32Array: Float32Array): Int16Array {
+  const int16Array = new Int16Array(float32Array.length);
+  for (let i = 0; i < float32Array.length; i++) {
+    const s = Math.max(-1, Math.min(1, float32Array[i]));
+    int16Array[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
+  }
+  return int16Array;
+}
+```
+
+---
+
+## 六、WebSocket 消息收发
+
+### 发送:音频数据(Binary)
+
+每 20ms 发送一次 1280 字节的 PCM 二进制数据:
+
+```typescript
+this.ws.send(pcmData.buffer);  // ArrayBuffer
+```
+
+### 发送:结束标识(JSON)
+
+录音结束时发送:
+
+```typescript
+const endMessage = JSON.stringify({
+  end: true,
+  sessionId: this.sessionId  // UUID
+});
+this.ws.send(endMessage);
+```
+
+### 接收:转录结果(JSON)
+
+服务端返回的消息结构:
+
+```json
+{
+  "msg_type": "result",
+  "res_type": "asr",
+  "data": {
+    "cn": {
+      "st": {
+        "type": "0",       // "0"=最终结果, "1"=中间结果
+        "rt": [
+          {
+            "rl": 1,       // 说话人ID(role_type=2时)
+            "ws": [
+              {
+                "cw": [
+                  { "w": "你好" }
+                ]
+              }
+            ]
+          }
+        ]
+      }
+    },
+    "ls": false            // true=最后一帧
+  }
+}
+```
+
+### 错误消息
+
+```json
+{
+  "msg_type": "result",
+  "res_type": "frc",
+  "data": {
+    "code": "error_code",
+    "desc": "错误描述"
+  }
+}
+```
+
+---
+
+## 七、转录结果解析
+
+### 文本提取逻辑
+
+```
+data.cn.st.rt[] → 遍历每个 rtItem
+    rtItem.ws[] → 遍历每个 wsItem
+        wsItem.cw[] → 遍历每个 cwItem
+            cwItem.w → 拼接为完整文本
+```
+
+### 结果类型判断
+
+| `st.type` | 含义 | 处理方式 |
+|-----------|------|---------|
+| `"0"` | 确定性结果(最终) | 替换前一个中间结果,追加到 finalText |
+| `"1"` | 中间结果(临时) | 替换最后一个中间结果,更新 currentText |
+
+### 说话人分离
+
+- 当 `role_type=2` 时启用
+- 说话人 ID 从 `rtItem.rl` / `wsItem.rl` / `cwItem.rl` 字段获取
+- 自动维护 `speakerMap`: `speaker_1` → `说话人1`, `speaker_2` → `说话人2`...
+
+---
+
+## 八、完整调用流程
+
+```
+1. preconnectWebSocket(enableSpeakerSeparation, astLang)
+   ├── 生成 UUID、UTC 时间
+   ├── 构建参数对象
+   ├── generateSignature(params)  → HmacSHA1 签名
+   ├── 拼接 WebSocket URL + query params
+   └── new WebSocket(wsUrl)  → 等待 onopen
+
+2. startRecording(options)
+   ├── getUserMedia()  → 获取麦克风权限
+   ├── new AudioContext({ sampleRate: 16000 })
+   ├── new MediaRecorder(stream)  → 录音保存
+   ├── createScriptProcessor(256, 1, 1)  → 实时采集
+   ├── onaudioprocess → Float32→Int16 → 推入 audioBuffer
+   ├── startAudioSendTimer()  → 每20ms发送1280字节
+   └── 复用或新建 WebSocket 连接
+
+3. [实时转录中]
+   ├── 定时器 → sendBufferedAudio() → ws.send(binary)
+   ├── ws.onmessage → handleWebSocketMessage()
+   │       ├── msg_type=result, res_type=asr → 解析文本
+   │       └── msg_type=result, res_type=frc → 错误处理
+   └── 回调:onTextUpdate / onSegment
+
+4. stopRecording() / stopRecordingLocalFirst()
+   ├── mediaRecorder.stop()
+   ├── ws.send({ end: true })  → 结束标识
+   ├── 停止定时器、断开音频节点
+   ├── 等待最终结果(300ms~2s)
+   ├── ws.close()
+   ├── 合并 audioChunks → Blob → 上传
+   └── 保存到 ChatVoice 表(Parse)
+```
+
+---
+
+## 九、语种参数说明
+
+| 值 | 说明 |
+|----|------|
+| `autodialect` | 自动识别中文、英文及中文方言(默认) |
+| `autominor` | 自动识别约 37 种语种(国际化场景) |
+
+通过 `ASTRecordingOptions.astLang` 或 `preconnectWebSocket(_, astLang)` 指定。
+
+---
+
+## 十、依赖
+
+| 依赖 | 用途 |
+|------|------|
+| `crypto-js` | HmacSHA1 签名 + Base64 编码 |
+| `@angular/core` | Injectable / signal / inject / effect |
+| Web API: `navigator.mediaDevices.getUserMedia` | 麦克风权限 |
+| Web API: `AudioContext` + `ScriptProcessorNode` | 音频实时处理 |
+| Web API: `MediaRecorder` | 录音文件保存 |
+| Web API: `WebSocket` | 与讯飞服务通信 |
+
+---
+
+## 十一、关键配置常量
+
+```typescript
+const CHUNK_SIZE = 1280;       // 每次发送字节数
+const SEND_INTERVAL = 20;     // 发送间隔(ms)
+const WS_TIMEOUT = 5000;      // WebSocket 连接超时(ms)
+const STOP_WAIT = 2000;       // 停止后等待最终结果(ms)
+const BUFFER_SIZE = 256;      // ScriptProcessor 帧数
+```
+
+---
+
+## 十二、错误处理
+
+1. **WebSocket 连接超时**(5秒)→ 更新 state.error,reject Promise
+2. **WebSocket 错误** → 关闭连接,触发 onError 回调
+3. **转录错误**(`res_type=frc`)→ 关闭连接,更新 state
+4. **麦克风权限被拒** → startRecording 返回 false
+5. **音频文件为空** → 抛出错误,通知上层
+6. **录音时长为 0/Infinity** → 使用 `Date.now() - recordingStartTime` 兜底

+ 734 - 0
docs/讯飞转文字/讯飞长语音转写接口调用文档.md

@@ -0,0 +1,734 @@
+# 讯飞长语音转写(IST)接口调用文档
+
+## 一、概述
+
+项目使用讯飞 **长语音转写(IST)** 接口,通过 HTTP REST API 实现离线音频文件的转写。与实时转录(AST)不同,长语音转写采用「上传 → 轮询」模式,适合处理已录制完成的长音频文件。
+
+### 与实时转录(AST)的区别
+
+| 对比项 | 实时转录(AST) | 长语音转写(IST) |
+|--------|----------------|------------------|
+| 协议 | WebSocket 流式 | HTTP REST(上传+轮询) |
+| 延迟 | 实时(边说边转) | 离线(分钟级) |
+| 音频来源 | 麦克风实时采集 | 已录制完成的音频文件 |
+| 适用场景 | 对话、语音输入 | 现场录音、长音频回溯 |
+| 说话人分离 | role_type=2 | roleType=1(通用)/ 3(声纹) |
+
+### 服务架构
+
+```
+前端 Angular App
+    └── IflytekLongTranscriptionService (前端服务)
+            ├── 音频转码(WebM → PCM16 WAV 16kHz 单声道)
+            ├── 上传到网关 → /api/apig/transcription/upload
+            └── 轮询结果 → /api/apig/transcription/result
+                    ↓
+API 网关(server.fmode.cn)
+    └── iflytek-long-transcription-server.js (Node.js 后端)
+            ├── POST /api/iflytek/upload → 讯飞 IST /v2/upload
+            └── POST /api/iflytek/result → 讯飞 IST /v2/getResult
+```
+
+**关键差异**:长语音转写需要后端代理(Node.js 服务器),因为:
+1. 签名中需要包含文件大小、时长等参数
+2. 讯飞 IST 接口要求 `application/octet-stream` 的原始二进制上传
+3. 签名放在 HTTP Header(`signature`)中,不是 URL query
+
+---
+
+## 二、API 凭证配置
+
+```javascript
+const IFLYTEK_CONFIG = {
+  appId: '50f4a46c',
+  accessKeyId: '106ddc40dfd4b9ca6d7b47c70fada749',    // API Key
+  accessKeySecret: 'NzFlNmFhZDJjMDNkZGM3NzI0Mzg2OGNm', // API Secret
+  uploadUrl: 'https://office-api-ist-dx.iflyaisol.com/v2/upload',
+  resultUrl: 'https://office-api-ist-dx.iflyaisol.com/v2/getResult'
+};
+```
+
+| 字段 | 说明 |
+|------|------|
+| `appId` | 讯飞开放平台应用 ID |
+| `accessKeyId` | API Key(AccessKeyId) |
+| `accessKeySecret` | API Secret(用于 HmacSHA1 签名) |
+| `uploadUrl` | 长语音转写上传接口地址 |
+| `resultUrl` | 长语音转写结果查询接口地址 |
+
+### 环境变量配置(后端 .env)
+
+```env
+IFLYTEK_APP_ID=50f4a46c
+IFLYTEK_API_KEY=106ddc40dfd4b9ca6d7b47c70fada749
+IFLYTEK_API_SECRET=NzFlNmFhZDJjMDNkZGM3NzI0Mzg2OGNm
+LONG_TRANSCRIPTION_PORT=3100
+IFLYTEK_IST_LANGUAGE=autodialect
+```
+
+---
+
+## 三、鉴权方式(签名生成)
+
+### 鉴权原理
+
+讯飞 IST 接口的鉴权方式与实时 AST 类似,但有一个关键区别:
+
+| 对比 | AST 实时转录 | IST 长语音转写 |
+|------|-------------|---------------|
+| 签名位置 | URL query 参数 `signature=xxx` | **HTTP Header** `signature: xxx` |
+| URL 编码风格 | 标准 `encodeURIComponent` | **Java URLEncoder 风格**(空格→`+`,特殊字符额外编码) |
+| 签名参与参数 | URL query 中的所有参数 | URL query 中的所有参数(不含 signature) |
+
+**签名放在 HTTP Header 中,不在 URL query 里。**
+
+### Java URLEncoder 风格编码
+
+讯飞 IST 接口要求使用 Java 风格的 URL 编码(与标准 `encodeURIComponent` 不同):
+
+```javascript
+/**
+ * Java URLEncoder 风格编码
+ * - 空格编码为 + (标准编码为 %20)
+ * - 对 ~ ! * ( ) 进行百分号编码(标准不编码这些)
+ */
+function javaUrlEncode(value) {
+  return encodeURIComponent(String(value))
+    .replace(/%20/g, '+')      // 空格 → +
+    .replace(/!/g, '%21')      // ! → %21
+    .replace(/\*/g, '%2A')     // * → %2A
+    .replace(/\(/g, '%28')     // ( → %28
+    .replace(/\)/g, '%29')     // ) → %29
+    .replace(/~/g, '%7E');     // ~ → %7E
+}
+```
+
+### 签名生成算法
+
+```javascript
+const crypto = require('crypto');
+
+/**
+ * 生成签名(与讯飞 Java SDK 行为一致)
+ * @param {string} accessKeySecret - API Secret
+ * @param {Object} queryParams - URL query 中的所有参数(不含 signature)
+ * @returns {string} Base64 编码的 HmacSHA1 签名
+ */
+function generateSignature(accessKeySecret, queryParams) {
+  // Step 1: 移除 signature 参数(如果存在)
+  const params = { ...queryParams };
+  delete params.signature;
+
+  // Step 2: 按参数名 ASCII 升序排序
+  const sortedKeys = Object.keys(params).sort();
+
+  // Step 3: 构建 baseString(键和值都用 Java 风格 URL 编码)
+  const pairs = [];
+  for (const key of sortedKeys) {
+    const value = params[key];
+    if (value !== undefined && value !== null && value !== '') {
+      const encodedKey = javaUrlEncode(key);
+      const encodedValue = javaUrlEncode(value);
+      pairs.push(`${encodedKey}=${encodedValue}`);
+    }
+  }
+  const baseString = pairs.join('&');
+
+  // Step 4: HmacSHA1(baseString, accessKeySecret) → Base64
+  const hmac = crypto.createHmac('sha1', Buffer.from(accessKeySecret, 'utf8'));
+  hmac.update(baseString, 'utf8');
+  const signature = hmac.digest('base64');
+
+  return signature;
+}
+```
+
+### 签名计算示例
+
+假设上传参数如下:
+
+```
+appId           = "50f4a46c"
+accessKeyId     = "106ddc40dfd4b9ca6d7b47c70fada749"
+dateTime        = "2025-05-07T19:06:30+0800"
+signatureRandom = "AbCdEfGh12345678"
+fileSize        = "1048576"
+fileName        = "recording.wav"
+language        = "autodialect"
+duration        = "120000"
+pd              = "com"
+roleType        = "1"
+roleNum         = "0"
+```
+
+**Step 1 - 排序后的 key 顺序:**
+```
+accessKeyId, appId, dateTime, duration, fileName, fileSize, language, pd, roleNum, roleType, signatureRandom
+```
+
+**Step 2 - 构建 baseString(Java URL 编码):**
+```
+accessKeyId=106ddc40dfd4b9ca6d7b47c70fada749&appId=50f4a46c&dateTime=2025-05-07T19%3A06%3A30%2B0800&duration=120000&fileName=recording.wav&fileSize=1048576&language=autodialect&pd=com&roleNum=0&roleType=1&signatureRandom=AbCdEfGh12345678
+```
+
+> 注意:`dateTime` 中的 `:` → `%3A`,`+` → `%2B`(Java URL 编码规则)
+
+**Step 3 - 生成签名:**
+```
+signature = HmacSHA1(baseString, "NzFlNmFhZDJjMDNkZGM3NzI0Mzg2OGNm") → Base64
+```
+
+### 与 AST 实时转录签名的核心差异
+
+| 差异点 | AST 实时转录 | IST 长语音转写 |
+|--------|-------------|---------------|
+| URL 编码 | `encodeURIComponent`(标准) | `javaUrlEncode`(Java 风格:空格→+) |
+| 签名放置 | URL query: `?...&signature=xxx` | HTTP Header: `signature: xxx` |
+| 依赖库 | 前端 `crypto-js` | 后端 Node.js `crypto` 模块 |
+| 空值处理 | 所有参数都参与 | 跳过 undefined/null/空字符串 |
+
+---
+
+## 四、上传接口详解
+
+### 接口地址
+
+```
+POST https://office-api-ist-dx.iflyaisol.com/v2/upload?{queryParams}
+```
+
+### 请求格式
+
+- **URL Query**:所有业务参数(用 Java 风格 URL 编码)
+- **HTTP Header**:`signature: {签名}` + `Content-Type: application/octet-stream`
+- **Body**:音频文件的原始二进制数据
+
+### 完整请求参数
+
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `appId` | string | ✅ | 应用 ID |
+| `accessKeyId` | string | ✅ | API Key |
+| `dateTime` | string | ✅ | 当前时间,格式:`2025-05-07T19:06:30+0800` |
+| `signatureRandom` | string | ✅ | 16位随机字符串(防重放) |
+| `fileSize` | string | ✅ | 文件大小(字节) |
+| `fileName` | string | ✅ | 文件名(含扩展名) |
+| `language` | string | ✅ | 语种:`autodialect`(中英+方言)或 `autominor`(37语种) |
+| `duration` | string | ✅ | 音频时长(**毫秒**),必须与真实时长一致 |
+| `pd` | string | ❌ | 应用领域,默认 `com`(通用) |
+| `roleType` | string | ❌ | 说话人分离:`0`=关闭,`1`=通用角色分离,`3`=声纹角色分离 |
+| `roleNum` | string | ❌ | 说话人数量:`0`=盲分,`1~10`=已知人数 |
+
+### ⚠️ 关键注意事项
+
+1. **`duration` 必须准确**:与真实音频时长不符会导致 `failType=5`(音频校验失败)
+2. **`fileSize` 必须准确**:与实际上传的字节数一致
+3. **音频格式要求**:推荐 PCM WAV(16kHz/16bit/单声道),WebM 等格式可能不兼容
+
+### 响应格式
+
+**成功:**
+```json
+{
+  "code": "000000",
+  "content": {
+    "orderId": "DKHJQ20250507...",
+    "taskEstimateTime": 30000
+  }
+}
+```
+
+**失败:**
+```json
+{
+  "code": "100003",
+  "descInfo": "duration is empty"
+}
+```
+
+### 完整上传代码示例(Node.js 后端)
+
+```javascript
+const crypto = require('crypto');
+const axios = require('axios');
+const fs = require('fs');
+
+const CONFIG = {
+  appId: '50f4a46c',
+  accessKeyId: '106ddc40dfd4b9ca6d7b47c70fada749',
+  accessKeySecret: 'NzFlNmFhZDJjMDNkZGM3NzI0Mzg2OGNm',
+  uploadUrl: 'https://office-api-ist-dx.iflyaisol.com/v2/upload'
+};
+
+function javaUrlEncode(value) {
+  return encodeURIComponent(String(value))
+    .replace(/%20/g, '+')
+    .replace(/!/g, '%21')
+    .replace(/\*/g, '%2A')
+    .replace(/\(/g, '%28')
+    .replace(/\)/g, '%29')
+    .replace(/~/g, '%7E');
+}
+
+function generateSignature(accessKeySecret, queryParams) {
+  const params = { ...queryParams };
+  delete params.signature;
+  const sortedKeys = Object.keys(params).sort();
+  const pairs = [];
+  for (const key of sortedKeys) {
+    const value = params[key];
+    if (value !== undefined && value !== null && value !== '') {
+      pairs.push(`${javaUrlEncode(key)}=${javaUrlEncode(value)}`);
+    }
+  }
+  const baseString = pairs.join('&');
+  const hmac = crypto.createHmac('sha1', Buffer.from(accessKeySecret, 'utf8'));
+  hmac.update(baseString, 'utf8');
+  return hmac.digest('base64');
+}
+
+function generateRandomString(length = 16) {
+  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+  let result = '';
+  for (let i = 0; i < length; i++) {
+    result += chars.charAt(Math.floor(Math.random() * chars.length));
+  }
+  return result;
+}
+
+function formatDateTime() {
+  const now = new Date();
+  const pad = (n) => String(n).padStart(2, '0');
+  return `${now.getFullYear()}-${pad(now.getMonth()+1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}+0800`;
+}
+
+/**
+ * 上传音频文件到讯飞 IST
+ * @param {Buffer} fileBuffer - 音频文件二进制数据
+ * @param {string} fileName - 文件名
+ * @param {number} durationMs - 音频时长(毫秒)
+ * @param {object} options - 可选参数
+ * @returns {Promise<{orderId: string, estimateTime: number}>}
+ */
+async function uploadToIflytek(fileBuffer, fileName, durationMs, options = {}) {
+  const queryParams = {
+    appId: CONFIG.appId,
+    accessKeyId: CONFIG.accessKeyId,
+    dateTime: formatDateTime(),
+    signatureRandom: generateRandomString(16),
+    fileSize: String(fileBuffer.length),
+    fileName: fileName,
+    language: options.language || 'autodialect',
+    duration: String(durationMs),
+    pd: options.pd || 'com',
+    roleType: String(options.roleType ?? 1),  // 默认开启通用角色分离
+    roleNum: String(options.roleNum ?? 0)     // 默认盲分
+  };
+
+  // 生成签名
+  const signature = generateSignature(CONFIG.accessKeySecret, queryParams);
+
+  // 构建 URL(参数用 Java 风格编码)
+  const queryString = Object.keys(queryParams)
+    .map(key => `${key}=${javaUrlEncode(queryParams[key])}`)
+    .join('&');
+  const fullUrl = `${CONFIG.uploadUrl}?${queryString}`;
+
+  // 发送请求:签名放在 Header 中
+  const response = await axios.post(fullUrl, fileBuffer, {
+    headers: {
+      'Content-Type': 'application/octet-stream',
+      signature: signature  // ⚠️ 签名在 Header 中,不在 URL query 中
+    },
+    maxBodyLength: Infinity,
+    maxContentLength: Infinity
+  });
+
+  if (response.data.code === '000000') {
+    return {
+      orderId: response.data.content.orderId,
+      estimateTime: response.data.content.taskEstimateTime
+    };
+  }
+
+  throw new Error(`上传失败: ${response.data.code} - ${response.data.descInfo}`);
+}
+
+// ===== 使用示例 =====
+const fileBuffer = fs.readFileSync('./recording.wav');
+const result = await uploadToIflytek(fileBuffer, 'recording.wav', 120000, {
+  roleType: 1,  // 开启说话人分离
+  roleNum: 2    // 已知2个说话人
+});
+console.log('订单ID:', result.orderId);
+```
+
+---
+
+## 五、查询结果接口详解
+
+### 接口地址
+
+```
+POST https://office-api-ist-dx.iflyaisol.com/v2/getResult?{queryParams}
+```
+
+### 请求格式
+
+- **URL Query**:查询参数(Java 风格 URL 编码)
+- **HTTP Header**:`signature: {签名}` + `Content-Type: application/json`
+- **Body**:空 JSON `{}`
+
+### 查询参数
+
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `accessKeyId` | string | ✅ | API Key |
+| `dateTime` | string | ✅ | 当前时间 |
+| `signatureRandom` | string | ✅ | 16位随机字符串 |
+| `orderId` | string | ✅ | 上传时返回的订单 ID |
+| `resultType` | string | ✅ | 固定 `transfer` |
+
+### 响应状态
+
+| `orderInfo.status` | 含义 | 处理方式 |
+|--------------------|------|---------|
+| `4` | 转写完成 | 解析 `orderResult` 获取文本 |
+| `3` | 处理中 | 等待后继续轮询 |
+| `-1` | 失败 | 检查 `failType` |
+| 其他 | 排队中 | 继续轮询 |
+
+### 失败类型(failType)
+
+| failType | 说明 |
+|----------|------|
+| `5` | 音频校验失败(duration 与真实时长不符) |
+| 其他 | 参考讯飞官方文档 |
+
+### 完整查询代码示例
+
+```javascript
+/**
+ * 查询长语音转写结果
+ * @param {string} orderId - 上传时返回的订单 ID
+ * @returns {Promise<{status: string, text?: string, segments?: Array}>}
+ */
+async function queryResult(orderId) {
+  const queryParams = {
+    accessKeyId: CONFIG.accessKeyId,
+    dateTime: formatDateTime(),
+    signatureRandom: generateRandomString(16),
+    orderId: orderId,
+    resultType: 'transfer'
+  };
+
+  const signature = generateSignature(CONFIG.accessKeySecret, queryParams);
+
+  const queryString = Object.keys(queryParams)
+    .map(key => `${key}=${javaUrlEncode(queryParams[key])}`)
+    .join('&');
+  const fullUrl = `${CONFIG.resultUrl}?${queryString}`;
+
+  const response = await axios.post(fullUrl, {}, {
+    headers: {
+      'Content-Type': 'application/json',
+      signature: signature
+    }
+  });
+
+  const data = response.data;
+  if (data.code !== '000000') {
+    throw new Error(`查询失败: ${data.code} - ${data.descInfo}`);
+  }
+
+  const orderInfo = data.content.orderInfo;
+
+  if (orderInfo.status === 4) {
+    // 转写完成,解析结果
+    return {
+      status: 'completed',
+      ...parseTranscriptionResult(data.content.orderResult)
+    };
+  }
+
+  if (orderInfo.status === 3) {
+    return { status: 'processing' };
+  }
+
+  if (orderInfo.status === -1) {
+    return { status: 'failed', failType: orderInfo.failType };
+  }
+
+  return { status: 'pending' };
+}
+```
+
+---
+
+## 六、转写结果解析
+
+### 原始返回数据结构
+
+讯飞 IST 的 `orderResult` 是一个 JSON 字符串,结构如下:
+
+```json
+{
+  "lattice": [
+    {
+      "json_1best": "{\"st\":{\"bg\":\"0\",\"ed\":\"3200\",\"rl\":\"1\",\"rt\":[{\"ws\":[{\"cw\":[{\"w\":\"你好\",\"wp\":\"n\"}]}]}]}}"
+    },
+    {
+      "json_1best": "{\"st\":{\"bg\":\"3300\",\"ed\":\"6500\",\"rl\":\"2\",\"rt\":[{\"ws\":[{\"cw\":[{\"w\":\"你好啊\",\"wp\":\"n\"}]}]}]}}"
+    }
+  ]
+}
+```
+
+### 解析后的数据结构
+
+```typescript
+interface Segment {
+  text: string;           // 该段文本
+  startMs: number | null; // 开始时间(毫秒)
+  endMs: number | null;   // 结束时间(毫秒)
+  speakerId: string | null; // 说话人编号(如 "1", "2")
+}
+```
+
+### 解析逻辑
+
+```javascript
+function parseTranscriptionResult(orderResult) {
+  if (!orderResult) return { text: '', segments: [] };
+
+  const result = JSON.parse(orderResult);
+  const lattice = result.lattice || [];
+
+  const rawSegments = [];
+  const fullTextParts = [];
+
+  for (const item of lattice) {
+    const json1best = JSON.parse(item.json_1best);
+    const st = json1best.st;
+    const rt = Array.isArray(st?.rt) ? st.rt : [];
+
+    // 拼接文本:rt[] → ws[] → cw[] → w
+    let sentenceText = '';
+    for (const rtItem of rt) {
+      const ws = Array.isArray(rtItem?.ws) ? rtItem.ws : [];
+      for (const wsItem of ws) {
+        const cw = Array.isArray(wsItem?.cw) ? wsItem.cw : [];
+        const first = cw[0];
+        if (first && first.w) {
+          sentenceText += String(first.w);
+        }
+      }
+    }
+
+    const trimmed = sentenceText.trim();
+    if (!trimmed) continue;
+
+    // 时间戳:st.bg(开始)、st.ed(结束),单位毫秒
+    const startMs = st?.bg != null ? Math.round(Number(st.bg)) : null;
+    const endMs = st?.ed != null ? Math.round(Number(st.ed)) : null;
+
+    // 说话人:st.rl(正整数编号)
+    const speakerId = st?.rl != null ? String(st.rl) : null;
+
+    rawSegments.push({ text: trimmed, startMs, endMs, speakerId });
+    fullTextParts.push(trimmed);
+  }
+
+  // 同一说话人连续发言整合为一段
+  const segments = [];
+  let current = null;
+
+  for (const seg of rawSegments) {
+    if (!current) {
+      current = { ...seg };
+    } else if (current.speakerId === seg.speakerId) {
+      current.text += seg.text;
+      if (seg.endMs != null) current.endMs = seg.endMs;
+    } else {
+      segments.push(current);
+      current = { ...seg };
+    }
+  }
+  if (current) segments.push(current);
+
+  return {
+    text: fullTextParts.join(' ').trim(),
+    segments
+  };
+}
+```
+
+---
+
+## 七、前端调用方式
+
+前端通过 `IflytekLongTranscriptionService`(Angular 服务)调用,自动处理音频转码、上传、轮询:
+
+```typescript
+import { IflytekLongTranscriptionService } from './iflytek-long-transcription.service';
+
+// 注入服务
+private readonly transcriptionService = inject(IflytekLongTranscriptionService);
+
+// 方式1:从 Blob URL 转写
+const result = await this.transcriptionService.transcribeFromBlobUrl(blobUrl, {
+  durationMs: 120000,              // 音频时长(毫秒),建议传入
+  roleNum: 0,                      // 0=盲分,1~10=已知人数
+  enableSpeakerSeparation: true,   // 是否启用说话人分离
+  poll: {
+    maxAttempts: 30,               // 最大轮询次数
+    intervalMs: 4000               // 轮询间隔(毫秒)
+  },
+  onProgress: (partial) => {
+    console.log('进度:', partial);
+  }
+});
+
+// 方式2:从 Blob 对象转写
+const result = await this.transcriptionService.transcribeFromBlob(audioBlob, {
+  durationMs: 60000
+});
+
+// 结果
+if (result) {
+  console.log('全文:', result.text);
+  console.log('片段:', result.segments);
+  // segments: [{ text, startMs, endMs, speakerId }, ...]
+}
+```
+
+### 前端请求路径
+
+前端不直接请求讯飞 API,而是通过 API 网关代理:
+
+| 前端请求 | 网关转发 | 最终到达 |
+|---------|---------|---------|
+| `POST {baseUrl}/api/apig/transcription/upload` | → | 后端 `/api/iflytek/upload` → 讯飞 `/v2/upload` |
+| `POST {baseUrl}/api/apig/transcription/result` | → | 后端 `/api/iflytek/result` → 讯飞 `/v2/getResult` |
+
+- **baseUrl**:`https://server.fmode.cn`(生产环境)
+- **认证**:HTTP Header `Authorization: Bearer {Parse Session Token}`
+
+### 前端音频转码
+
+上传前会自动将原始音频(WebM/OGG等)转码为讯飞要求的格式:
+
+```
+原始 Blob(WebM/OGG)
+    → AudioContext.decodeAudioData()  解码
+    → 下混为单声道
+    → OfflineAudioContext 重采样到 16kHz
+    → 构建 WAV 文件头(PCM16LE)
+    → 输出 audio/wav Blob
+```
+
+---
+
+## 八、完整调用流程
+
+```
+1. 前端录音结束,得到音频 Blob
+
+2. 前端 IflytekLongTranscriptionService.transcribeFromBlob(blob, options)
+   ├── convertToPcm16WavMono16k(blob) → 转码为 WAV
+   ├── FormData: { audio: wavBlob, durationMs, roleType, roleNum }
+   └── POST /api/apig/transcription/upload (带 Bearer Token)
+
+3. 网关转发到后端 /api/iflytek/upload
+   ├── 接收 multipart/form-data
+   ├── 计算/验证 duration
+   ├── 构建 queryParams(含 appId, fileSize, duration, roleType...)
+   ├── generateSignature(accessKeySecret, queryParams) → Java风格签名
+   ├── POST 讯飞 /v2/upload?{queryParams}
+   │   ├── Header: Content-Type: application/octet-stream
+   │   ├── Header: signature: {签名}
+   │   └── Body: 音频文件二进制
+   └── 返回 { orderId, estimateTime }
+
+4. 前端轮询结果(默认每4秒一次,最多30次)
+   └── POST /api/apig/transcription/result { orderId }
+
+5. 网关转发到后端 /api/iflytek/result
+   ├── 构建 queryParams(含 orderId, resultType: 'transfer')
+   ├── generateSignature(accessKeySecret, queryParams)
+   ├── POST 讯飞 /v2/getResult?{queryParams}
+   │   ├── Header: Content-Type: application/json
+   │   ├── Header: signature: {签名}
+   │   └── Body: {}
+   └── 返回 { status, text, segments }
+
+6. 前端收到 status='completed' → 停止轮询,返回结果
+```
+
+---
+
+## 九、后端服务启动
+
+### 依赖安装
+
+```bash
+npm install express multer cors axios dotenv get-audio-duration
+```
+
+### 启动服务
+
+```bash
+node src/services/iflytek-long-transcription-server.js
+```
+
+### 可用接口
+
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| GET | `/health` | 健康检查 |
+| POST | `/api/iflytek/upload` | 上传音频(multipart/form-data, field: `audio`) |
+| POST | `/api/iflytek/result` | 查询结果(body: `{ orderId }`) |
+| GET | `/api/iflytek/test-role-separation` | 测试角色分离签名 |
+
+---
+
+## 十、鉴权要点总结
+
+| 要点 | 说明 |
+|------|------|
+| 签名位置 | **HTTP Header** `signature: xxx`(不在 URL query 中) |
+| 签名算法 | HmacSHA1(baseString, accessKeySecret) → Base64 |
+| URL 编码 | **Java URLEncoder 风格**(空格→+,~!*()→%编码) |
+| baseString | 所有 query 参数(不含 signature)按 key 升序 → Java编码 → `key=value&` 拼接 |
+| 空值跳过 | undefined / null / 空字符串的参数不参与签名 |
+| 每次请求 | 需要新的 `dateTime` + `signatureRandom`(防重放) |
+| 后端代理 | 必须通过后端完成签名和请求(Secret 不暴露给前端) |
+| 时长要求 | `duration` 必须与实际音频时长一致(毫秒),否则 failType=5 |
+
+---
+
+## 十一、常见错误
+
+| 错误码 | 说明 | 解决方式 |
+|--------|------|---------|
+| `100003` | `duration is empty` | 确保 duration 参数非空且为有效毫秒数 |
+| failType=5 | 音频校验失败(duration 不匹配) | 传入精确的音频时长 |
+| 签名错误 | `signature verify failed` | 检查 Java URL 编码逻辑、参数排序、accessKeySecret |
+| 文件过大 | 超出限制 | 限制 100MB 以内 |
+
+---
+
+## 十二、关键配置常量
+
+```javascript
+const PORT = 3100;                    // 服务端口
+const FILE_SIZE_LIMIT = 100 * 1024 * 1024;  // 100MB 文件大小限制
+const DEFAULT_ROLE_TYPE = 1;          // 默认开启通用角色分离
+const DEFAULT_ROLE_NUM = 0;           // 默认盲分(未知说话人数)
+const DEFAULT_LANGUAGE = 'autodialect'; // 默认中英+方言
+const DEFAULT_PD = 'com';             // 默认通用领域
+
+// 前端轮询配置
+const POLL_MAX_ATTEMPTS = 30;         // 最大轮询次数
+const POLL_INTERVAL_MS = 4000;        // 轮询间隔(毫秒)
+```

+ 510 - 0
douyin-speaking-daily-followup-course-scripts.md

@@ -0,0 +1,510 @@
+# douyin-speaking-daily 后续课程录制脚本
+
+> P0「下载安装说明」已录制完成。后续建议录制 5 条主线短视频,每条 3-5 分钟;另加 1 条可选排错/进阶视频。
+
+---
+
+# 总体拆分建议
+
+## 主线 5 条
+
+1. P1:首次使用前配置:VOC Token + 账号档案
+2. P2:第一次跑通抖音口播日报
+3. P3:读懂日报输出和本地文件
+4. P4:从日报选题进入脚本共创
+5. P5:自然话术改稿、定稿和偏好记忆
+
+## 可选 1 条
+
+6. P6:常见问题排查和自动转写说明
+
+## 录制原则
+
+- 每条视频只解决一个动作闭环。
+- 每条控制 3-5 分钟。
+- 不在视频里展示真实 Token。
+- 尽量用 OpenClaw 自然语言演示,少讲代码。
+- 每条结尾都告诉观众下一条会做什么。
+
+---
+
+# P1:首次使用前配置:VOC Token + 账号档案
+
+## 视频目标
+
+让观众知道:安装完成后,真正跑日报前需要先准备 VOC Token 和账号画像文件。
+
+## 建议时长
+
+3-5 分钟
+
+## 屏幕准备
+
+打开:
+
+```text
+%USERPROFILE%\.openclaw\workspace\douyin-speaking-daily
+```
+
+重点展示:
+
+```text
+openclaw-startup.md
+memory-templates\douyin-speaking-profile.json
+```
+
+## 录制步骤
+
+1. 说明 P0 已经完成安装。
+2. 展示真实跑数据需要 VOC Token。
+3. 展示 Token 设置命令,但不要输入真实 Token。
+4. 展示账号画像记忆文件的用途。
+5. 在 OpenClaw 里演示建档启动话术。
+
+## 屏幕命令
+
+```powershell
+node %USERPROFILE%\.openclaw\tools\set-voc-token.js <your-voc-token>
+```
+
+## OpenClaw 启动话术
+
+```text
+启动 douyin-speaking-daily,先帮我建立一个抖音口播账号档案。你一次问 1-3 个问题,问完后写入 memory/douyin-speaking-profile.json。
+```
+
+## 口播脚本
+
+这一节我们做安装之后的第一步准备。上一节已经把技能包装到 OpenClaw 里了,但要真正跑抖音数据,还需要两个东西:第一个是 VOC Token,第二个是账号画像。
+
+VOC Token 是用来访问数据接口的。这里我不会展示真实 Token,实际使用时可以通过这个命令写入本机 OpenClaw:
+
+```text
+node %USERPROFILE%\.openclaw\tools\set-voc-token.js <your-voc-token>
+```
+
+第二个是账号画像。这个技能包不是随便生成热点,而是根据你的账号定位、目标人群、关键词、转化目标来筛选选题。所以它需要一个记忆文件,默认位置是:
+
+```text
+memory/douyin-speaking-profile.json
+```
+
+如果你还没有这个文件,可以直接在 OpenClaw 里说:
+
+```text
+启动 douyin-speaking-daily,先帮我建立一个抖音口播账号档案。你一次问 1-3 个问题,问完后写入 memory/douyin-speaking-profile.json。
+```
+
+这一步完成后,OpenClaw 就知道你是谁、做什么账号、关注哪些关键词、哪些方向不要碰。下一节我们就用这个账号档案,跑第一份抖音口播日报。
+
+## 结尾引导
+
+下一条视频:第一次跑通抖音口播日报。
+
+---
+
+# P2:第一次跑通抖音口播日报
+
+## 视频目标
+
+让观众完成第一次日报生成,看到 OpenClaw 返回日报摘要和本地输出文件。
+
+## 建议时长
+
+4-5 分钟
+
+## 屏幕准备
+
+打开 OpenClaw。
+
+准备好 profile:
+
+```text
+memory/douyin-speaking-profile.json
+```
+
+## 推荐演示话术
+
+```text
+用 memory/douyin-speaking-profile.json 跑今天的抖音口播爆款日报,采集关键词、确认账号近期作品和评论,并输出日报。
+```
+
+如果要演示完整闭环加自动转写,用:
+
+```text
+用 douyin-speaking-daily 跑完整闭环,自动给互动分最高的 1 条视频补逐字稿,再回灌生成最终日报。
+```
+
+## 低成本测试参数说明
+
+如果担心正式采集时间长,可以说明小样本测试参数:
+
+```text
+videosPerKeyword=1
+maxKeywords=1
+maxVideosWithComments=1
+maxCommentPages=1
+includeReplies=false
+accountsLimit=1
+postsPerAccount=1
+autoTranscriptTopN=1
+autoTranscriptProvider=iflytek-gateway
+```
+
+## 录制步骤
+
+1. 打开 OpenClaw。
+2. 输入日报启动话术。
+3. 等待技能运行。
+4. 展示返回的日报摘要。
+5. 指出输出目录里有完整文件。
+
+## 口播脚本
+
+这一节我们跑第一份抖音口播日报。前面已经完成了安装、Token 和账号画像准备,现在就可以让 OpenClaw 调用 `douyin-speaking-daily-runner`。
+
+最简单的说法是:
+
+```text
+用 memory/douyin-speaking-profile.json 跑今天的抖音口播爆款日报,采集关键词、确认账号近期作品和评论,并输出日报。
+```
+
+这里它会做几件事:第一,根据账号画像里的关键词去抓相关视频;第二,根据对标账号抓近期作品;第三,抓评论信号;第四,把这些数据整理成日报。
+
+如果你希望完整一点,还可以让它自动给互动分最高的一条视频补逐字稿:
+
+```text
+用 douyin-speaking-daily 跑完整闭环,自动给互动分最高的 1 条视频补逐字稿,再回灌生成最终日报。
+```
+
+第一次演示建议用小样本参数,这样跑得快,也更适合教学。我们先不追求抓很多数据,而是先确认整个链路能跑通。
+
+看到 OpenClaw 返回日报摘要,就说明主流程已经通了。下一节我们不急着改稿,先看日报里面到底有哪些内容,以及本地输出文件在哪里。
+
+## 结尾引导
+
+下一条视频:读懂日报输出和本地文件。
+
+---
+
+# P3:读懂日报输出和本地文件
+
+## 视频目标
+
+让观众知道日报结果怎么看,输出文件分别有什么用。
+
+## 建议时长
+
+3-5 分钟
+
+## 屏幕准备
+
+打开最近一次输出目录,例如:
+
+```text
+outputs\douyin-speaking-daily-demo
+```
+
+或 OpenClaw workspace 下的输出目录。
+
+重点展示:
+
+```text
+daily-report.md
+daily-report.json
+audit.json
+runner-raw-input.json
+script-session.json
+transcripts\
+```
+
+## 录制步骤
+
+1. 展示 OpenClaw 里的日报摘要。
+2. 打开 `daily-report.md`。
+3. 解释日报结构:热点、评论、选题、风险、建议。
+4. 展示 `runner-raw-input.json` 是原始采集数据。
+5. 展示 `audit.json` 是运行和质量检查信息。
+6. 说明后续改稿会从日报选题进入脚本共创。
+
+## 口播脚本
+
+这一节我们看日报结果。OpenClaw 对话里会直接给一个摘要,但真正完整的报告,会保存到本地文件里。
+
+最重要的是这个文件:
+
+```text
+daily-report.md
+```
+
+它适合人直接阅读,里面通常会包含当天值得关注的视频、评论信号、选题建议、账号适配判断,以及可以继续共创的口播方向。
+
+第二个文件是:
+
+```text
+daily-report.json
+```
+
+它是结构化结果,后面如果要做自动化、二次分析,主要看它。
+
+第三个是:
+
+```text
+runner-raw-input.json
+```
+
+这个是原始采集数据。后面自然话术续接、重新生成报告、复盘问题,经常会用到它。
+
+如果开启了自动转写,还会看到:
+
+```text
+transcripts
+```
+
+这里会保存逐字稿结果。
+
+这一节的重点是:OpenClaw 里看到的是摘要,本地文件里保存的是完整交付物。下一节我们就从日报里的某一个选题开始,进入脚本共创。
+
+## 结尾引导
+
+下一条视频:从日报选题进入脚本共创。
+
+---
+
+# P4:从日报选题进入脚本共创
+
+## 视频目标
+
+演示用户不传复杂参数,只用自然话术选择一个选题,生成第一版口播稿。
+
+## 建议时长
+
+3-5 分钟
+
+## 屏幕准备
+
+OpenClaw 保持在上一次日报会话后。
+
+## 推荐演示话术
+
+```text
+选题2,进入脚本共创
+```
+
+## 录制步骤
+
+1. 回到刚生成的日报。
+2. 指出日报里有多个选题。
+3. 直接输入自然话术:`选题2,进入脚本共创`。
+4. 展示 OpenClaw 自动续接最近日报。
+5. 展示生成的 v0 共创稿。
+6. 说明此时已经生成 `script-session.json`。
+
+## 口播脚本
+
+这一节我们从日报进入脚本共创。以前很多工具要你复制文件路径、传 report、传 input、传 session,非常麻烦。这个技能包现在支持自然话术。
+
+比如我看完日报以后,觉得第二个选题不错,我不用传路径,直接说:
+
+```text
+选题2,进入脚本共创
+```
+
+OpenClaw 会自动找到最近一次日报和原始数据,然后进入第二个选题的脚本共创。
+
+这里生成的是第一版草稿,也可以理解成 v0。它不是最终稿,而是用来继续修改的基础稿。
+
+同时本地会生成一个会话文件:
+
+```text
+script-session.json
+```
+
+这个文件记录了当前选题、脚本版本和后续反馈。所以下一条视频里,我们就可以继续说“开头更狠”“老板视角”“少讲概念”,系统会在当前会话上继续改,而不是重新生成一份。
+
+## 结尾引导
+
+下一条视频:自然话术改稿、定稿和偏好记忆。
+
+---
+
+# P5:自然话术改稿、定稿和偏好记忆
+
+## 视频目标
+
+演示 P1.6 核心体验:自然话术改稿、自动续接、定稿质量门禁、最终交付格式、偏好写回。
+
+## 建议时长
+
+4-5 分钟
+
+## 屏幕准备
+
+保持 P4 的脚本共创会话。
+
+## 推荐演示话术
+
+第一句改稿:
+
+```text
+开头更狠,老板视角,少讲概念,多给一个具体场景
+```
+
+第二句定稿:
+
+```text
+定稿
+```
+
+第三句偏好沉淀:
+
+```text
+保留1、2,不要AI工具清单方向
+```
+
+## 录制步骤
+
+1. 在 P4 的共创稿后继续输入改稿话术。
+2. 展示生成 v1/v2,不丢失之前版本。
+3. 输入 `定稿`。
+4. 展示定稿自检。
+5. 展示最终交付格式:70 秒、30 秒、60 秒提词器、标题、钩子、评论区引导、拍摄提示。
+6. 输入偏好沉淀话术。
+7. 说明下次选题排序会受记忆影响。
+
+## 口播脚本
+
+这一节是这个技能包最好用的部分:自然话术改稿和定稿。
+
+上一节我们已经选了一个题,并生成了第一版共创稿。现在我不需要传 `scriptSession` 路径,也不需要告诉它哪个文件,只要继续说:
+
+```text
+开头更狠,老板视角,少讲概念,多给一个具体场景
+```
+
+它会自动续接当前脚本会话,在原来的稿子基础上生成新版本。这里的关键是:它不是重新从 v0 开始,而是保留前面的版本继续改。
+
+改到差不多以后,我直接说:
+
+```text
+定稿
+```
+
+定稿前它会做一次质量自检,检查有没有项目名泄漏、是不是太泛、像不像口播、有没有具体场景、有没有互动或者转化动作。
+
+定稿后输出不是一段普通文本,而是一套可拍摄交付物,包括:70 秒正式口播稿、30 秒短版、60 秒提词器版、标题、前三秒钩子、评论区引导和拍摄提示。
+
+最后我们还可以把偏好沉淀下来,比如:
+
+```text
+保留1、2,不要AI工具清单方向
+```
+
+这样下次再跑日报时,系统会对保留方向加权,对不要的方向降权。也就是说,这个技能包不是一次性生成,而是会越用越贴近你的账号。
+
+## 结尾引导
+
+到这里,已经完成从下载安装到跑日报、选题、改稿、定稿、记忆沉淀的完整闭环。后面可以单独录一条常见问题和自动转写说明。
+
+---
+
+# P6 可选:常见问题排查和自动转写说明
+
+## 视频目标
+
+解释最常见的失败原因,以及自动转写什么时候用。
+
+## 建议时长
+
+3-5 分钟
+
+## 适合放在最后
+
+这条不是主线必看课,但适合降低用户使用门槛。
+
+## 常见问题
+
+### 1. 缺少 VOC Token
+
+现象:提示缺少 VOC Token。
+
+解决:
+
+```powershell
+node %USERPROFILE%\.openclaw\tools\set-voc-token.js <your-voc-token>
+```
+
+### 2. 找不到账号或需要确认账号
+
+说明:
+
+如果返回 `needs_account_confirmation`,说明系统找到了候选账号,但需要用户确认哪些是对标账号。
+
+### 3. OpenClaw 看不到技能
+
+解决:
+
+- 重启 OpenClaw。
+- 检查:
+
+```text
+%USERPROFILE%\.openclaw\skills
+```
+
+### 4. 自动转写失败
+
+说明:
+
+自动转写依赖视频可访问、音频可解析、服务可用。失败时不影响日报主流程,只会标记逐字稿缺口。
+
+## 口播脚本
+
+这一节讲几个常见问题。第一个是缺少 VOC Token。如果日报不能拉取数据,首先检查 Token 有没有设置。可以用本机工具设置,不要在视频或群里公开真实 Token。
+
+第二个是账号确认。有时候你给的是账号昵称或者方向,系统会找到多个候选账号。这不是失败,而是需要你确认哪个账号才是对标账号。
+
+第三个是 OpenClaw 看不到技能。通常是安装后没有重启,或者没有安装到 `.openclaw/skills` 目录。重启 OpenClaw 后再看。
+
+第四个是自动转写。转写是增强能力,不是日报主流程的硬依赖。如果转写失败,日报仍然可以基于标题、互动、评论和账号作品生成,只是会标记逐字稿缺口。
+
+这条视频的重点是:先判断是安装问题、Token 问题、账号确认问题,还是转写增强能力的问题,不要一失败就以为整个技能包不能用。
+
+---
+
+# 推荐发布顺序
+
+1. P0:下载安装到部署验证
+2. P1:首次使用前配置:VOC Token + 账号档案
+3. P2:第一次跑通抖音口播日报
+4. P3:读懂日报输出和本地文件
+5. P4:从日报选题进入脚本共创
+6. P5:自然话术改稿、定稿和偏好记忆
+7. P6:常见问题排查和自动转写说明(可选)
+
+---
+
+# 每条视频标题建议
+
+## P1 标题
+
+安装完还不能直接跑?3 分钟配置抖音口播日报技能包的 Token 和账号画像
+
+## P2 标题
+
+第一次跑通抖音口播日报:让 OpenClaw 自动采集热点、评论和对标账号
+
+## P3 标题
+
+日报生成后看哪里?3 分钟读懂 douyin-speaking-daily 的输出文件
+
+## P4 标题
+
+不用传路径,直接说“选题2”:用 OpenClaw 进入口播脚本共创
+
+## P5 标题
+
+从改稿到定稿:自然话术生成 70 秒口播稿、短版和提词器版
+
+## P6 标题
+
+抖音口播日报技能包常见问题:Token、账号确认、自动转写怎么排查

+ 61 - 0
douyin-speaking-daily-p0-install-notes.txt

@@ -0,0 +1,61 @@
+抖音口播每日稿件日报技能包 P0 下载安装说明
+
+一、本节目标
+把 douyin-speaking-daily 技能包下载安装到本机 OpenClaw,并确认能正常识别和运行。
+
+二、前置准备
+1. 已安装 OpenClaw。
+2. 已安装 Node.js 18+。
+   检查命令:node -v
+3. 后续真实跑日报需要 VOC Token;本节只做下载安装和本地验证,不展示真实 Token。
+
+三、下载地址
+https://repos.fmode.cn/x/openclaw-skills/packages/douyin-speaking-daily.zip
+
+四、安装步骤
+1. 用浏览器打开上面的下载地址,下载 douyin-speaking-daily.zip。
+2. 解压 zip 到本地目录。
+3. 在解压后的目录中打开 PowerShell。
+4. 执行安装命令:
+   node install.js
+
+五、安装后位置
+技能会安装到:
+%USERPROFILE%\.openclaw\skills
+
+工具会安装到:
+%USERPROFILE%\.openclaw\tools
+%USERPROFILE%\.openclaw\workspace\scripts\tools
+
+说明文档会安装到:
+%USERPROFILE%\.openclaw\workspace\douyin-speaking-daily
+
+六、核心技能
+主要入口:
+douyin-speaking-daily-runner
+
+常用相关技能:
+douyin-speaking-daily-report
+douyin-speaking-profile-memory
+douyin-video-transcript
+voc-token-preflight
+
+七、安装验证
+方式一:检查目录中是否出现技能:
+%USERPROFILE%\.openclaw\skills\douyin-speaking-daily-runner
+
+方式二:运行本地 smoke 测试:
+node %USERPROFILE%\.openclaw\workspace\scripts\tools\douyin-speaking-daily-p1-smoke.js
+
+看到 status: ok,说明本地工具链可用。
+
+方式三:重启 OpenClaw,在 OpenClaw 中搜索或调用:
+douyin-speaking-daily-runner
+
+八、常见问题
+1. node 命令不存在:先安装 Node.js 18+,然后重启 PowerShell。
+2. OpenClaw 看不到技能:重启 OpenClaw,并检查 .openclaw\skills 目录。
+3. 真实日报跑不起来:通常是 VOC Token、关键词、账号画像还没配置;下一节再讲。
+
+九、本节结论
+完成下载、解压、node install.js、smoke 验证后,douyin-speaking-daily 技能包就已经部署到本机 OpenClaw,可以进入下一节真实日报配置和运行。

+ 108 - 0
douyin-speaking-daily/README.md

@@ -0,0 +1,108 @@
+# 抖音口播每日稿件日报技能包
+
+> 范围:主线二 / 场景一 / 1.1 爆款内容洞察闭环。P0 只做抖音平台,不做视频生成、自动发布、投后分析和私域导流。
+
+这个目录是 `douyin-speaking-daily` 的独立操作入口,把问答建档、记忆写回、关键词监听、账号监听、爆款拆解、逐字稿转写和每日稿件日报收在一起,方便按技能包管理。
+
+## 文件结构
+
+```text
+douyin-speaking-daily/
+├── README.md
+├── douyin-speaking-daily-playbook.md
+├── openclaw-startup.md
+├── skill-package-manifest.json
+├── memory-templates/
+│   └── douyin-speaking-profile.json
+├── scripts/
+│   ├── README.md
+│   ├── douyin-speaking-profile-memory.js
+│   ├── douyin-speaking-daily-runner.js
+│   ├── douyin-speaking-daily-report.js
+│   ├── douyin-viral-script-analyzer.js
+│   └── douyin-video-transcriber.js
+└── skills/
+    └── README.md
+```
+
+## OpenClaw 使用顺序
+
+1. 读取 `openclaw-startup.md`,确认角色设定、记忆系统和启动话术。
+2. 读取 `douyin-speaking-daily-playbook.md`,按状态推进。
+3. 没有 profile 时,使用 `douyin-speaking-profile-builder` 问答建档。
+4. 有 profile 时,优先用 `douyin-speaking-daily-runner` 跑 P0 闭环。
+5. 需要完整闭环时,给 runner 传 `autoTranscriptTopN=1`,自动给互动分最高的视频补逐字稿。
+6. 若返回 `needs_account_confirmation`,用 `douyin-speaking-profile-memory` 把候选账号确认进 `confirmedAccounts`。
+7. 日报输出后收集校准反馈,写回 `calibrationNotes`、`forbiddenTopics`、`keywords` 或 `confirmedAccounts`。
+
+## 打包部署
+
+独立套件的打包、上传和部署说明见:
+
+```text
+douyin-speaking-daily/deployment.md
+```
+
+常用命令:
+
+```powershell
+npm run douyin-speaking-daily:validate
+npm run douyin-speaking-daily:build
+npm run douyin-speaking-daily:deploy:dry
+npm run douyin-speaking-daily:deploy
+```
+
+## 记忆系统
+
+主记忆文件:
+
+```text
+memory/douyin-speaking-profile.json
+```
+
+模板:
+
+```text
+douyin-speaking-daily/memory-templates/douyin-speaking-profile.json
+```
+
+记忆写回入口:
+
+```text
+douyin/douyin-speaking-profile-memory/
+scripts/tools/douyin-speaking-profile-memory.js
+```
+
+## P0 总入口
+
+```text
+scripts/tools/douyin-speaking-daily-runner.js
+```
+
+小样本完整闭环示例:
+
+```powershell
+node scripts\tools\douyin-speaking-daily-runner.js --profile memory\douyin-speaking-profile.json --output outputs\run --videos-per-keyword 1 --max-keywords 1 --max-videos-with-comments 1 --max-comment-pages 1 --include-replies false --accounts-limit 1 --posts-per-account 1 --auto-transcript-top-n 1 --auto-transcript-provider iflytek-gateway --write-profile true
+```
+
+## 源码位置
+
+| 模块 | 源码 |
+|---|---|
+| 账号建档 | `douyin/douyin-speaking-profile-builder/` |
+| 记忆写回 | `douyin/douyin-speaking-profile-memory/` |
+| 关键词监听 | `douyin/douyin-speaking-keyword-monitor/` |
+| 账号近期作品 | `douyin/douyin-user-posts/` |
+| 账号监听 | `douyin/douyin-speaking-account-monitor/` |
+| P0 总入口 | `douyin/douyin-speaking-daily-runner/` |
+| 日报生成 | `douyin/douyin-speaking-daily-report/` |
+| 爆款拆解 | `douyin/douyin-viral-script-analyzer/` |
+| 逐字稿转写 | `douyin/douyin-video-transcript/` |
+| 技术 spec | `docs/specs/douyin-speaking-daily-skill-package.md` |
+
+## P0 输出
+
+- OpenClaw 当前对话中的日报摘要。
+- 工作区文件:`openclaw-voc-output/douyin-speaking-daily/<project>/<date>/daily-report.md`。
+- 同目录 `daily-report.json`、`audit.json` 和 `runner-raw-input.json` 用于复盘。
+- 开启自动转写时,产出 `transcripts/<aweme_id>/transcript.json` 和 `transcript.txt`。

+ 101 - 0
douyin-speaking-daily/deployment.md

@@ -0,0 +1,101 @@
+# 打包、上传和部署
+
+`douyin-speaking-daily` 可以作为独立套件发布。套件包含:
+
+- `douyin/` 下的相关 OpenClaw skills;
+- `scripts/tools/` 下的本地执行工具;
+- `douyin-speaking-daily/` 工作区入口、playbook、启动话术和 memory 模板;
+- 安装脚本 `install.js`;
+- 发布包 `dist/douyin-speaking-daily.zip`;
+- 发布清单 `dist/douyin-speaking-daily-suite-manifest.json`。
+
+## 本地校验
+
+```powershell
+npm run douyin-speaking-daily:validate
+```
+
+会检查:
+
+- 必要文件是否存在;
+- skill 的 `api-config.json` 是否能解析;
+- runner/report/memory/transcriber 等脚本是否通过 `node --check`;
+- `skill-package-manifest.json` 是否能解析。
+
+## 本地打包
+
+```powershell
+npm run douyin-speaking-daily:build
+```
+
+等价于:
+
+```powershell
+node scripts\deploy\douyin-speaking-daily-suite.js --build
+```
+
+产物:
+
+```text
+dist/douyin-speaking-daily.zip
+dist/douyin-speaking-daily-suite-manifest.json
+```
+
+## 上传 CDN
+
+```powershell
+npm run douyin-speaking-daily:upload
+```
+
+上传依赖 `scripts/deploy/package-and-upload.js`,需要可用的七牛配置:
+
+```text
+QINIU_AK
+QINIU_SK
+```
+
+不传 `--upload` 时只会本地打包,不会访问 CDN。
+
+## 本地部署到 OpenClaw
+
+先预览:
+
+```powershell
+npm run douyin-speaking-daily:deploy:dry
+```
+
+实际部署:
+
+```powershell
+npm run douyin-speaking-daily:deploy
+```
+
+部署内容:
+
+- skills -> `~/.openclaw/skills/<skill-name>/`
+- tools -> `~/.openclaw/tools/`
+- tools mirror -> `~/.openclaw/workspace/scripts/tools/`
+- suite workspace -> `~/.openclaw/workspace/douyin-speaking-daily/`
+
+部署后重启 OpenClaw gateway。
+
+## Zip 安装
+
+拿到 `dist/douyin-speaking-daily.zip` 后:
+
+```powershell
+Expand-Archive dist\douyin-speaking-daily.zip -DestinationPath .tmp-douyin-speaking-daily
+node .tmp-douyin-speaking-daily\install.js
+```
+
+首次使用前设置 VOC token:
+
+```powershell
+node ~/.openclaw/tools/set-voc-token.js <your-session-token>
+```
+
+## OpenClaw 启动语
+
+```text
+用 douyin-speaking-daily 跑完整闭环,自动给互动分最高的 1 条视频补逐字稿,再回灌生成最终日报。
+```

+ 266 - 0
douyin-speaking-daily/docs/specs/douyin-speaking-daily-p1-optimization-spec.md

@@ -0,0 +1,266 @@
+# douyin-speaking-daily P1 优化 Spec
+
+日期:2026-05-10  
+对象:`douyin-speaking-daily` 抖音口播每日稿件日报技能包  
+状态:In Progress,P1.1-P1.6 已进入可测闭环,准备进行 OpenClaw 教学视频全流程测试。
+
+## 1. P1 定义
+
+P0 已经证明“能跑通”:建档、关键词/账号采集、评论采集、可选逐字稿、日报输出、套件打包部署都已形成闭环。
+
+P1 的目标不是继续堆数据,而是把它优化成面向真实用户交付的“每日选题 + 脚本共创”系统:
+
+1. 日报阶段只展示热点选题、趋势信号、证据强弱和下一步,不把完整长稿堆给用户。
+2. 用户选择某条选题后,进入脚本共创,会产出可拍摄口播稿,而不是只给拆解框架。
+3. 脚本可以多轮修改、确认定稿,并把偏好沉淀到记忆里。
+4. 后续运行要能读取历史偏好,使成稿越来越贴合用户,而不是每次从零开始。
+5. 任何低证据内容都必须显示风险和质量门禁,不把猜测包装成结论。
+
+一句话验收标准:
+
+> 用户每天启动一次后,先拿到可选择的热点选题;选择选题后,OpenClaw 能继续多轮共创并给出最终可拍摄稿件,同时把用户偏好写入记忆,影响下一次选题和成稿。
+
+## 2. 交付场景
+
+### 场景 A:每日选题雷达
+
+用户启动 `douyin-speaking-daily-runner` 后,系统采集样本并直接在对话里输出:
+
+- 今日一句话判断
+- 样本量、评论量、逐字稿数
+- 质量门禁
+- Top 5 热点选题
+- 每条选题的来源、互动数据、置信度、是否有逐字稿
+- 下一步口令,例如“选题 2,进入脚本共创”
+
+完整文件仍保存在本地,但对话优先展示 `assistantMessage` 正文,不能只告诉用户文件路径。
+
+### 场景 B:选题后脚本共创
+
+用户回复“选题 2,进入脚本共创”后:
+
+1. 如果该选题已有逐字稿,直接生成 v0 完整脚本。
+2. 如果没有逐字稿,也要生成“结构推断稿” v0,并明确风险。
+3. 如果视频超出网关能力,不上传导致 413;返回“需分段/换短视频/换 provider”的原因。
+4. 用户继续反馈“开头更狠、老板视角、少讲概念、多给案例”后,生成 v1/v2。
+5. 用户确认定稿后,写入脚本记忆。
+
+### 场景 C:单条爆款视频改写
+
+用户给一个抖音爆款视频链接,系统解析/下载/转写后,输出:
+
+- 逐字稿路径和摘要
+- 爆款结构拆解
+- 可迁移打法
+- 面向用户账号方向的初稿
+- 可继续修改的校准问题
+
+## 3. 当前实现进度
+
+### P1.1 稳定性与成本控制:已完成第一阶段
+
+已实现:
+
+- `runner` 支持 `--transcript-cache`
+- 支持 `--transcript-reuse-cache`
+- 支持 `--transcript-daily-budget`
+- 输出 `transcript-queue.json`
+- `summary/audit.json` 标注 cached/generated/queue 计数
+- `iflytek-gateway` 增加默认 8 分钟时长保护,超长视频先返回 `needs_media_processing`,避免上传后 413
+
+仍待优化:
+
+- 超长视频自动分段转写
+- 更细的 ASR 成本预估
+- 更稳定的跨天 transcript cache 清理策略
+
+### P1.2 脚本共创交付闭环:已完成第一阶段
+
+已实现:
+
+- `daily-report` 支持 `selectedTopicIndex`
+- `runner` 透传 `selectedTopicIndex/scriptSession/feedback/finalize/scriptMemory`
+- 选题后生成 `script-session.json`
+- 生成 `selected-topic-<n>-script.md/json`
+- 支持 v0 初稿、feedback 多轮改稿、finalize 定稿
+- 定稿后写入 `memory/douyin-speaking-script-memory.json`
+- OpenClaw 对话优先返回 `assistantMessage` 正文
+- 没有逐字稿时仍生成“结构推断稿”,并标注低置信风险
+- 定稿后生成动态发布辅助,包括标题备选、拍摄提示、评论区引导、30 秒压缩版和 60 秒提词器版
+- 脚本生成已抽象账号交付语境,按 profile 自动适配 B2B、电商、教育、本地生活或通用账号,避免默认绑定 `voc.market` 或固定 VOC 话术
+- 支持 `message=<用户原话>` 自然话术入口,例如“选题2”“改稿:开头更狠”“定稿”
+- 未显式传 `input/scriptSession` 时,会自动续接最近一次 `runner-raw-input.json` 和 `script-session.json`
+- 定稿前输出脚本自检,检查测试项目词泄漏、过泛、缺少场景、缺少互动或转化动作
+
+仍待优化:
+
+- 90 秒展开版和镜头分镜版
+- 增加镜头提示、字幕钩子、口播提词器版本
+- 更细的行业模板,例如医疗健康、金融、招聘、本地门店等
+
+### P1.3 历史记忆与反馈权重:已完成第一阶段
+
+已实现:
+
+- 定稿写入脚本记忆
+- 下一次脚本共创会读取 `scriptMemory`
+- 历史偏好可影响 v0 初稿,例如老板视角、强开头、产品转化、禁用泛泛表达
+- `assistantMessage` 会显示“历史偏好”提示
+- 返回结果包含 `scriptMemorySignals`
+- 每次日报/共创运行写入 `memory/douyin-speaking-history.json`
+- history 记录 Top 选题、样本概况、质量门禁、是否进入共创、脚本版本和是否定稿
+- runner 透传 `historyMemory/writeHistory`,便于 OpenClaw 部署后统一沉淀跨次运行
+- 用户说“保留1、2”“不要某方向”时写入选题偏好记忆
+- 日报排序会读取 `topicPreferences.keepPhrases/dropPhrases`,对保留方向加权、对禁区方向降权
+- 已定稿过的同一 `awemeId` 会降权,避免反复推荐同一条素材
+
+仍待实现:
+
+- 7/30 天趋势信号
+- 更精细的 liked/disliked 权重,例如按主题、作者、钩子类型拆开加权
+
+### P1.4 质量门禁与审计:已完成第一阶段
+
+已实现:
+
+- `daily-report.json` 增加 `qualityGate`
+- `audit.json` 增加 `qualityGate`
+- runner/result 返回 `qualityGate`
+- 对 profile 缺字段、样本不足、评论不足、逐字稿不足给出 `warn/fail/pass`
+- 日报和脚本共创顶部显示质量门禁
+- `qualityGate.level=fail` 时阻断正式脚本生成,只返回补数据建议;即使传入 `finalize=true` 也不会写入脚本记忆
+- 脚本定稿前增加 `finalQualityGate`,检查项目词泄漏、过泛、缺少场景、缺少互动/转化动作
+- `finalQualityGate.level=fail` 时阻断定稿记忆写入,避免坏稿污染后续偏好
+
+仍待实现:
+
+- 更细的重复主题风险、风险词、证据引用覆盖率
+- 基于业务配置的门禁阈值
+
+### P1.5 套件发布与自动化:已完成第一阶段
+
+已实现:
+
+- 套件 manifest 记录 P1 spec
+- 本地 zip/build/deploy 流程可用
+- 新增 `scripts/tools/douyin-speaking-daily-p1-smoke.js`
+- smoke 覆盖:
+  - 语法检查
+  - 有历史记忆时生成 v0
+  - feedback 生成 v1
+  - finalize 写入记忆
+  - runner 选题透传
+  - 自然话术“选题 N”自动续接最近 raw input
+  - 自然话术“定稿”自动续接最近 script-session
+  - 自然话术“保留/不要”写入选题偏好并影响排序
+  - 无逐字稿结构推断稿
+  - 定稿自检、30 秒压缩版、60 秒提词器版
+  - 网关超长音频保护
+
+仍待实现:
+
+- 上传前自动读取 zip 内容确认 P1 文件完整
+- 线上安装后自动 smoke
+- 发布清单按版本生成 changelog
+
+## 4. 数据产物约定
+
+一次完整运行应产生:
+
+```text
+outputs/<run>/
+  daily-report.md
+  daily-report.json
+  audit.json
+  runner-raw-input.json
+  transcript-queue.json
+  script-session.json                 # 进入共创后产生
+  selected-topic-<n>-script.md         # 进入共创后产生
+  selected-topic-<n>-script.json       # 进入共创后产生
+  transcripts/<aweme_id>/transcript.json
+  transcripts/<aweme_id>/transcript.txt
+```
+
+长期记忆:
+
+```text
+memory/
+  douyin-speaking-profile.json
+  douyin-speaking-transcript-cache.json
+  douyin-speaking-script-memory.json
+  douyin-speaking-history.json         # 已用于跨次运行沉淀,下一阶段接入排序降权
+```
+
+## 5. CLI 验收口令
+
+日报 + 自动转写:
+
+```powershell
+node scripts/tools/douyin-speaking-daily-runner.js `
+  --profile memory/douyin-speaking-profile.json `
+  --auto-transcript-top-n 1 `
+  --auto-transcript-provider iflytek-gateway `
+  --auto-transcript-max-duration-ms 480000 `
+  --output outputs/douyin-speaking-daily-live
+```
+
+选题进入脚本共创:
+
+```powershell
+node scripts/tools/douyin-speaking-daily-runner.js `
+  --profile memory/douyin-speaking-profile.json `
+  --input outputs/douyin-speaking-daily-live/runner-raw-input.json `
+  --selected-topic-index 2 `
+  --script-memory memory/douyin-speaking-script-memory.json `
+  --output outputs/douyin-speaking-daily-script-v0
+```
+
+反馈改稿:
+
+```powershell
+node scripts/tools/douyin-speaking-daily-runner.js `
+  --profile memory/douyin-speaking-profile.json `
+  --input outputs/douyin-speaking-daily-live/runner-raw-input.json `
+  --script-session outputs/douyin-speaking-daily-script-v0/script-session.json `
+  --feedback "开头更狠,老板视角,减少概念,多给具体场景" `
+  --script-memory memory/douyin-speaking-script-memory.json `
+  --output outputs/douyin-speaking-daily-script-v1
+```
+
+定稿沉淀:
+
+```powershell
+node scripts/tools/douyin-speaking-daily-runner.js `
+  --profile memory/douyin-speaking-profile.json `
+  --input outputs/douyin-speaking-daily-live/runner-raw-input.json `
+  --script-session outputs/douyin-speaking-daily-script-v1/script-session.json `
+  --finalize true `
+  --script-memory memory/douyin-speaking-script-memory.json `
+  --output outputs/douyin-speaking-daily-script-final
+```
+
+自动化自审:
+
+```powershell
+node scripts/tools/douyin-speaking-daily-p1-smoke.js
+```
+
+## 6. 自审标准
+
+每次优化后必须检查:
+
+1. 是否仍然以“日报选题展示 + 用户选择后共创脚本”为主流程。
+2. 是否直接在 OpenClaw 对话展示 `assistantMessage` 正文。
+3. 是否避免把单一项目名硬编码到通用逻辑。
+4. 是否保留证据链、置信度、风险说明和质量门禁。
+5. 是否没有伪造视频、评论、逐字稿、互动数据。
+6. 是否对超长视频、接口失败、缺少 token 做降级。
+7. 是否通过 `douyin-speaking-daily-p1-smoke.js`。
+
+## 7. 下一阶段优先级
+
+1. 增加“用户选择选题后自动补转写”的分段策略。
+2. 增加 90 秒展开版、镜头分镜版和字幕钩子。
+3. 基于 `douyin-speaking-history.json` 做 7/30 天趋势与更细的重复选题降权。
+4. 发布包 smoke 化:build、deploy、install 后都能一键验证并可用于教学视频录制。
+5. 增加真实 OpenClaw UI 回归清单,覆盖“日报 -> 选题 -> 改稿 -> 定稿 -> 偏好复用”全流程。

+ 219 - 0
douyin-speaking-daily/douyin-speaking-daily-playbook.md

@@ -0,0 +1,219 @@
+---
+name: douyin-speaking-daily-playbook
+description: 抖音口播每日稿件日报技能包入口路由,指导 Agent 按建档、采集、转写、日报、校准的顺序推进。
+version: 0.2.0
+author: nkkj-BrainHack
+---
+
+# 抖音口播每日稿件日报 Playbook
+
+## 角色
+
+你是“抖音口播爆款内容策划师 + 数据化选题编辑”。你的任务不是泛泛写脚本,而是基于抖音真实样本,为一个具体账号每天产出口播选题日报。
+
+工作原则:
+
+- 先读记忆,再行动。
+- 缺什么问什么,一次只问 1-3 个关键问题。
+- 用关键词视频、对标账号近期作品、评论和逐字稿做证据链。
+- 没有逐字稿时只做结构推断,不把标题或描述当完整口播稿。
+- 用户反馈必须写回记忆,下一轮继承。
+
+## 记忆加载
+
+优先读取:
+
+```text
+memory/douyin-speaking-profile.json
+```
+
+没有 profile 时,读取模板:
+
+```text
+douyin-speaking-daily/memory-templates/douyin-speaking-profile.json
+```
+
+记忆写回统一走:
+
+```text
+douyin-speaking-profile-memory
+```
+
+关键状态:
+
+| state | 下一步 |
+|---|---|
+| `emptyProfile` | 启动问答建档 |
+| `collectingProfile` | 补齐缺失字段 |
+| `awaitingProfileConfirm` | 复述配置,等待用户确认 |
+| `readyForManualRun` | 运行日报闭环 |
+| `needs_account_confirmation` | 展示候选账号,等待确认监听池 |
+| `awaitingReportCalibration` | 收集“像/不像/不要/新增”反馈 |
+| `tunedProfile` | 继续跑下一轮日报 |
+| `automationReady` | 后续接定时任务和外部推送 |
+
+## 首轮建档问题
+
+首次启动时先问:
+
+```text
+老板,先把这只抖音口播账号对准。我先问 3 个问题:
+1. 这个账号服务什么行业或品类?
+2. 你希望它主要帮你涨粉、获客、卖产品,还是建立品牌认知?
+3. 给我 3-5 个你想长期监听的关键词。
+```
+
+关键词补齐后继续问账号监听方式:
+
+```text
+账号监听你想怎么做?
+1. 给我具体对标账号、主页链接、昵称或 sec_user_id,我做精确监听;
+2. 只给一个方向,比如“AI创业老板IP”,我先搜索候选账号,整理后让你确认监听池。
+```
+
+候选账号不能直接当确认账号使用,必须先让用户确认。
+
+## P0 闭环
+
+推荐总入口:
+
+```text
+douyin-speaking-daily-runner
+```
+
+完整路径:
+
+```text
+profile
+  -> douyin-speaking-daily-runner
+  -> 关键词视频 / 已确认账号近期作品 / 评论
+  -> 可选 autoTranscriptTopN 补逐字稿
+  -> douyin-speaking-daily-report
+  -> douyin-speaking-profile-memory 写回状态和校准
+```
+
+小样本测试参数:
+
+```text
+--profile memory/douyin-speaking-profile.json
+--videos-per-keyword 1
+--max-keywords 1
+--max-videos-with-comments 1
+--max-comment-pages 1
+--include-replies false
+--accounts-limit 1
+--posts-per-account 1
+--auto-transcript-top-n 1
+--auto-transcript-provider iflytek-gateway
+--write-profile true
+```
+
+正式日报参数:
+
+```text
+--profile memory/douyin-speaking-profile.json
+--videos-per-keyword 5
+--max-keywords 5
+--max-videos-with-comments 8
+--max-comment-pages 1
+--include-replies true
+--accounts-limit 5
+--posts-per-account 8
+--auto-transcript-top-n 1
+--auto-transcript-provider iflytek-gateway
+--write-profile true
+```
+
+## 日报必须包含
+
+- 今日一句话判断
+- 判断口径与置信度
+- 趋势信号
+- 今日选题池
+- 每条选题的来源视频、互动数据、评论口径、逐字稿状态
+- 口播框架
+- 评论触发点
+- 风险提醒
+- 明日监听建议
+- 校准问题
+
+置信度规则:
+
+- 有互动数据 + 有匹配评论 + 有逐字稿:高
+- 有互动数据 + 有匹配评论:中
+- 只有标题/描述和互动数据:低
+
+## 校准话术
+
+日报后必须追问:
+
+```text
+这批选题里:
+1. 哪 3 条最像你的账号,后续要提高权重?
+2. 哪些方向不要再出现,要写入禁区或降权?
+3. 哪些表达太泛、太危险或不符合你的身份?
+4. 是否要新增/删除监听关键词或对标账号?
+```
+
+用户反馈写入 `calibrationNotes`、`forbiddenTopics`、`keywords` 或 `confirmedAccounts`。
+
+## 教学视频测试流程
+
+录制教学视频时建议按下面顺序演示,重点展示“少传路径、直接对话”的体验:
+
+1. 启动日报:
+
+```text
+启动抖音口播每日稿件日报。
+先读取已有账号档案,如果没有就帮我建档。
+今天帮我采集抖音爆款口播选题,自动补一条最值得拆的视频逐字稿。
+最后直接在聊天里给我日报,不要只给文件路径。
+```
+
+录屏讲解口径:这一句会自动读取账号档案、采集热点样本、补跑 1 条逐字稿,并把日报正文直接发在聊天里。
+
+2. 选择选题:
+
+```text
+选题2,进入脚本共创
+```
+
+此时工具可通过 `message=选题2,进入脚本共创` 自动续接最近一次 `runner-raw-input.json`。
+
+3. 多轮改稿:
+
+```text
+开头更狠,老板视角,少讲概念,多给一个具体场景
+```
+
+4. 定稿:
+
+```text
+定稿
+```
+
+此时工具会自动续接最近一次 `script-session.json`,输出最终稿、30 秒压缩版、60 秒提词器版、标题备选、拍摄提示、评论区引导和定稿自检。
+
+5. 反馈选题偏好:
+
+```text
+保留1、2,不要工具清单方向
+```
+
+该反馈会写入脚本记忆里的选题偏好,下次日报排序会对保留方向加权、对“工具清单方向”降权。
+
+6. 复测偏好是否生效:
+
+```text
+重新生成今天的口播选题日报,沿用我刚才保存的偏好。
+```
+
+录屏讲解口径:这一轮应该更偏“强冲突、老板视角、具体业务场景”,纯工具清单类选题会降权。
+
+## 启动话术
+
+常用启动语见:
+
+```text
+douyin-speaking-daily/openclaw-startup.md
+```

+ 52 - 0
douyin-speaking-daily/memory-templates/douyin-speaking-profile.json

@@ -0,0 +1,52 @@
+{
+  "state": "emptyProfile",
+  "projectName": "",
+  "platform": "douyin",
+  "industry": "",
+  "accountPositioning": "",
+  "targetAudience": "",
+  "conversionGoal": "",
+  "coreOffer": "",
+  "contentPillars": [],
+  "keywords": [],
+  "accountMonitorMode": "",
+  "referenceAccounts": [],
+  "accountDiscoveryDirection": "",
+  "candidateAccounts": [],
+  "confirmedAccounts": [],
+  "forbiddenTopics": [],
+  "tone": "",
+  "dailyOutputCount": 8,
+  "preferenceWeights": {
+    "keywords": {},
+    "accounts": {},
+    "hookTypes": {},
+    "contentPillars": {},
+    "topicTags": {},
+    "negativeTags": {}
+  },
+  "scriptDraftPolicy": {
+    "enabled": true,
+    "count": 3,
+    "variants": ["60s"],
+    "tone": "",
+    "ctaStyle": ""
+  },
+  "transcriptPolicy": {
+    "provider": "iflytek-gateway",
+    "topN": 1,
+    "maxDurationMs": 600000,
+    "dailyBudget": 1,
+    "reuseCache": true,
+    "cachePath": "memory/douyin-speaking-transcript-cache.json"
+  },
+  "deliveryPreferences": {
+    "channel": "local",
+    "time": "",
+    "format": ["md", "json"]
+  },
+  "calibrationNotes": [],
+  "lastReportAt": "",
+  "automationReady": false,
+  "updatedAt": ""
+}

+ 147 - 0
douyin-speaking-daily/openclaw-startup.md

@@ -0,0 +1,147 @@
+---
+name: douyin-speaking-daily-openclaw-startup
+description: OpenClaw 启动 douyin-speaking-daily 技能包时的角色设定、记忆系统和启动话术。
+version: 0.2.1
+---
+
+# OpenClaw 启动说明
+
+## 角色设定
+
+启动 `douyin-speaking-daily` 时,OpenClaw 应进入“抖音口播爆款内容策划师 + 数据化选题编辑”的角色:
+
+- 目标:每天为一个明确账号产出可拍摄的抖音口播选题日报。
+- 判断方式:用关键词视频、对标账号近期作品、评论信号和逐字稿共同判断,不靠空想选题。
+- 编辑口径:优先寻找有冲突、有具体人群、有可复述钩子、有评论反馈的内容。
+- 边界:P0 只做抖音爆款洞察、口播拆解和日报,不做视频生成、自动发布、投后归因和私域转化。
+- 纪律:没有采集到的数据必须标注缺口,不能把视频标题当完整口播逐字稿。
+
+## 记忆系统
+
+主记忆文件:
+
+```text
+memory/douyin-speaking-profile.json
+```
+
+模板文件:
+
+```text
+douyin-speaking-daily/memory-templates/douyin-speaking-profile.json
+```
+
+记忆由 `douyin-speaking-profile-memory` 维护,核心字段如下:
+
+- `state`:当前流程阶段,如 `emptyProfile`、`readyForManualRun`、`awaitingReportCalibration`、`tunedProfile`。
+- `projectName`、`industry`、`accountPositioning`、`targetAudience`、`conversionGoal`:账号基础定位。
+- `keywords`:长期监听关键词。
+- `referenceAccounts`、`candidateAccounts`、`confirmedAccounts`:对标账号和已确认监听池。
+- `forbiddenTopics`:禁区和降权方向。
+- `contentPillars`、`tone`、`coreOffer`:内容栏目、语气和转化承接。
+- `calibrationNotes`:日报后用户反馈,用于下一轮加权或降权。
+- `lastRunSummary`、`lastOutputDir`、`lastReportAt`:最近一次运行结果。
+
+写回入口:
+
+```text
+node scripts/tools/douyin-speaking-profile-memory.js --mode update ...
+node scripts/tools/douyin-speaking-profile-memory.js --mode confirm-accounts ...
+node scripts/tools/douyin-speaking-profile-memory.js --mode calibrate ...
+```
+
+## 启动话术
+
+首次建档:
+
+```text
+启动 douyin-speaking-daily,先帮我建立一个抖音口播账号档案。你一次问 1-3 个问题,问完后写入 memory/douyin-speaking-profile.json。
+```
+
+已有档案,跑每日爆款日报:
+
+```text
+用 memory/douyin-speaking-profile.json 跑今天的抖音口播爆款日报,采集关键词、确认账号近期作品和评论,并输出日报。
+```
+
+跑完整闭环,并自动给 Top 1 爆款视频补逐字稿:
+
+```text
+用 douyin-speaking-daily 跑完整闭环,自动给互动分最高的 1 条视频补逐字稿,再回灌生成最终日报。
+```
+
+只测试转写:
+
+```text
+用 douyin-video-transcript 把这个抖音链接解析成音频,并通过 iflytek-gateway 输出逐字稿。
+```
+
+日报后校准:
+
+```text
+这次日报里,第 1 和第 3 条更像我的账号,第 2 条不要再出;新增关键词“AI获客”,禁区加入“情绪鸡汤”。
+```
+
+## Account Discovery Troubleshooting
+
+When discovering reference accounts from broad keywords:
+
+- First use `douyin-general-search` and extract candidate accounts from video authors.
+- Then use `douyin-user-search` as a supplemental exact account search.
+- If the API returns HTTP 200 with candidates, show the candidates and ask the user to confirm. Do not call this an APIGAuth/company binding issue.
+- Only mention token/auth/company problems when token preflight is not `valid`, or the API response is HTTP 401/403 or explicitly says auth/quota/company failed.
+- If the candidates are weak, ask the user for a narrower keyword, a known account nickname/homepage link, or `sec_user_id`.
+
+## 推荐调用参数
+
+小样本测试:
+
+```text
+--profile memory/douyin-speaking-profile.json
+--videos-per-keyword 1
+--max-keywords 1
+--max-videos-with-comments 1
+--max-comment-pages 1
+--include-replies false
+--accounts-limit 1
+--posts-per-account 1
+--auto-transcript-top-n 1
+--auto-transcript-provider iflytek-gateway
+--write-profile true
+```
+
+正式日报:
+
+```text
+--profile memory/douyin-speaking-profile.json
+--videos-per-keyword 5
+--max-keywords 5
+--max-videos-with-comments 8
+--max-comment-pages 1
+--include-replies true
+--accounts-limit 5
+--posts-per-account 8
+--auto-transcript-top-n 1
+--auto-transcript-provider iflytek-gateway
+--write-profile true
+```
+
+## 状态推进
+
+```text
+emptyProfile
+  -> 问答建档
+  -> readyForManualRun
+  -> daily-runner
+  -> awaitingReportCalibration
+  -> calibrate
+  -> tunedProfile
+```
+
+如果出现 `needs_account_confirmation`,先让用户确认候选账号,再写入 `confirmedAccounts` 后重跑。
+
+确认账号后的 P0 启动规则:
+
+- 不要把“补抓账号主页资料”作为启动日报的前置步骤。
+- 不要逐个打开 `https://www.douyin.com/user/...` 主页链接补资料;桌面网页 404/风控不代表数据闭环失败。
+- `douyin-user-profile` 是可选画像补充,失败时只标注“账号主页资料补全失败,已跳过”。
+- 核心闭环直接调用 `douyin-speaking-daily-runner`,由 runner 拉 `douyin-user-posts`、评论、可选逐字稿并生成日报。

+ 25 - 0
douyin-speaking-daily/scripts/README.md

@@ -0,0 +1,25 @@
+# 脚本入口
+
+这里放 `douyin-speaking-daily` 技能包的脚本入口。为避免源码分裂,这些脚本会代理到仓库内正式工具:
+
+| 本包入口 | 正式工具 |
+|---|---|
+| `douyin-speaking-profile-memory.js` | `scripts/tools/douyin-speaking-profile-memory.js` |
+| `douyin-speaking-daily-runner.js` | `scripts/tools/douyin-speaking-daily-runner.js` |
+| `douyin-speaking-daily-report.js` | `scripts/tools/douyin-speaking-daily-report.js` |
+| `douyin-viral-script-analyzer.js` | `scripts/tools/douyin-viral-script-analyzer.js` |
+| `douyin-video-transcriber.js` | `scripts/tools/douyin-video-transcriber.js` |
+
+示例:
+
+```powershell
+node douyin-speaking-daily\scripts\douyin-speaking-daily-runner.js --project demo --industry AI创业 --keywords AI工具,AI创业
+node douyin-speaking-daily\scripts\douyin-speaking-daily-runner.js --profile memory\douyin-speaking-profile.json --auto-transcript-top-n 1 --auto-transcript-provider iflytek-gateway
+node douyin-speaking-daily\scripts\douyin-speaking-profile-memory.js --mode confirm-accounts --profile memory\douyin-speaking-profile.json --candidates outputs\run\account-candidates.json --select 1,2
+node douyin-speaking-daily\scripts\douyin-speaking-daily-report.js --project demo --industry AI创业 --keywords AI工具,AI创业
+node douyin-speaking-daily\scripts\douyin-viral-script-analyzer.js --text "为什么你的视频没人看?不是剪辑问题,是开头没有制造冲突。"
+node douyin-speaking-daily\scripts\douyin-video-transcriber.js --provider manual --text "这里粘贴口播逐字稿"
+node douyin-speaking-daily\scripts\douyin-video-transcriber.js --provider none --douyin-url "https://v.douyin.com/xxx/" --download-media true
+node douyin-speaking-daily\scripts\douyin-video-transcriber.js --provider iflytek-gateway --douyin-url "https://v.douyin.com/xxx/"
+node douyin-speaking-daily\scripts\douyin-video-transcriber.js --provider iflytek-ist --file .\sample.wav --duration-ms 120000
+```

+ 2 - 0
douyin-speaking-daily/scripts/douyin-speaking-daily-p1-smoke.js

@@ -0,0 +1,2 @@
+#!/usr/bin/env node
+require('../../scripts/tools/douyin-speaking-daily-p1-smoke.js');

+ 12 - 0
douyin-speaking-daily/scripts/douyin-speaking-daily-report.js

@@ -0,0 +1,12 @@
+#!/usr/bin/env node
+const path = require('path');
+const { spawnSync } = require('child_process');
+
+const toolPath = path.resolve(__dirname, '..', '..', 'scripts', 'tools', 'douyin-speaking-daily-report.js');
+const result = spawnSync(process.execPath, [toolPath, ...process.argv.slice(2)], {
+  stdio: 'inherit',
+  cwd: process.cwd(),
+  env: process.env
+});
+
+process.exit(result.status === null ? 1 : result.status);

+ 2 - 0
douyin-speaking-daily/scripts/douyin-speaking-daily-runner.js

@@ -0,0 +1,2 @@
+#!/usr/bin/env node
+require('../../scripts/tools/douyin-speaking-daily-runner.js');

+ 2 - 0
douyin-speaking-daily/scripts/douyin-speaking-profile-memory.js

@@ -0,0 +1,2 @@
+#!/usr/bin/env node
+require('../../scripts/tools/douyin-speaking-profile-memory.js');

+ 12 - 0
douyin-speaking-daily/scripts/douyin-video-transcriber.js

@@ -0,0 +1,12 @@
+#!/usr/bin/env node
+const path = require('path');
+const { spawnSync } = require('child_process');
+
+const toolPath = path.resolve(__dirname, '..', '..', 'scripts', 'tools', 'douyin-video-transcriber.js');
+const result = spawnSync(process.execPath, [toolPath, ...process.argv.slice(2)], {
+  stdio: 'inherit',
+  cwd: process.cwd(),
+  env: process.env
+});
+
+process.exit(result.status === null ? 1 : result.status);

+ 12 - 0
douyin-speaking-daily/scripts/douyin-viral-script-analyzer.js

@@ -0,0 +1,12 @@
+#!/usr/bin/env node
+const path = require('path');
+const { spawnSync } = require('child_process');
+
+const toolPath = path.resolve(__dirname, '..', '..', 'scripts', 'tools', 'douyin-viral-script-analyzer.js');
+const result = spawnSync(process.execPath, [toolPath, ...process.argv.slice(2)], {
+  stdio: 'inherit',
+  cwd: process.cwd(),
+  env: process.env
+});
+
+process.exit(result.status === null ? 1 : result.status);

+ 131 - 0
douyin-speaking-daily/skill-package-manifest.json

@@ -0,0 +1,131 @@
+{
+  "name": "douyin-speaking-daily",
+  "displayName": "抖音口播每日稿件日报技能包",
+  "version": "0.2.18",
+  "scope": "主线二 / 场景一 / 1.1 爆款内容洞察闭环,P0 只做抖音平台",
+  "status": "p0-closed-loop",
+  "entry": "douyin-speaking-daily-playbook.md",
+  "startup": "openclaw-startup.md",
+  "deployment": "deployment.md",
+  "spec": "docs/specs/douyin-speaking-daily-skill-package.md",
+  "p1Spec": "douyin-speaking-daily/docs/specs/douyin-speaking-daily-p1-optimization-spec.md",
+  "rolePreset": {
+    "name": "抖音口播爆款内容策划师 + 数据化选题编辑",
+    "goal": "基于抖音真实样本、评论信号和逐字稿,为一个具体账号每天产出口播选题日报。",
+    "boundaries": [
+      "P0 只做抖音爆款洞察、口播拆解和日报",
+      "不做视频生成、自动发布、投后归因和私域转化",
+      "没有采集到的数据必须标注缺口,不能伪造证据链"
+    ]
+  },
+  "memorySystem": {
+    "primaryFile": "memory/douyin-speaking-profile.json",
+    "transcriptCache": "memory/douyin-speaking-transcript-cache.json",
+    "scriptMemory": "memory/douyin-speaking-script-memory.json",
+    "template": "douyin-speaking-daily/memory-templates/douyin-speaking-profile.json",
+    "writerSkill": "douyin-speaking-profile-memory",
+    "states": [
+      "emptyProfile",
+      "collectingProfile",
+      "awaitingProfileConfirm",
+      "readyForManualRun",
+      "needs_account_confirmation",
+      "awaitingReportCalibration",
+      "tunedProfile",
+      "automationReady"
+    ]
+  },
+  "activationPhrases": [
+    "启动 douyin-speaking-daily,先帮我建立一个抖音口播账号档案。",
+    "用 memory/douyin-speaking-profile.json 跑今天的抖音口播爆款日报。",
+    "用 douyin-speaking-daily 跑完整闭环,自动给互动分最高的 1 条视频补逐字稿,再回灌生成最终日报。",
+    "用 douyin-video-transcript 把这个抖音链接解析成音频,并通过 iflytek-gateway 输出逐字稿。"
+  ],
+  "skills": [
+    {
+      "name": "douyin-speaking-profile-builder",
+      "path": "douyin/douyin-speaking-profile-builder",
+      "role": "问答建档"
+    },
+    {
+      "name": "douyin-speaking-profile-memory",
+      "path": "douyin/douyin-speaking-profile-memory",
+      "role": "profile 记忆写回与校准"
+    },
+    {
+      "name": "douyin-speaking-keyword-monitor",
+      "path": "douyin/douyin-speaking-keyword-monitor",
+      "role": "关键词监听"
+    },
+    {
+      "name": "douyin-user-posts",
+      "path": "douyin/douyin-user-posts",
+      "role": "账号近期作品原子接口"
+    },
+    {
+      "name": "douyin-speaking-account-monitor",
+      "path": "douyin/douyin-speaking-account-monitor",
+      "role": "账号监听"
+    },
+    {
+      "name": "douyin-speaking-daily-runner",
+      "path": "douyin/douyin-speaking-daily-runner",
+      "role": "P0 可执行总入口,支持自动补逐字稿"
+    },
+    {
+      "name": "douyin-speaking-daily-report",
+      "path": "douyin/douyin-speaking-daily-report",
+      "role": "每日稿件日报"
+    },
+    {
+      "name": "douyin-viral-script-analyzer",
+      "path": "douyin/douyin-viral-script-analyzer",
+      "role": "爆款口播结构拆解"
+    },
+    {
+      "name": "douyin-video-transcript",
+      "path": "douyin/douyin-video-transcript",
+      "role": "逐字稿转写"
+    }
+  ],
+  "scriptEntrypoints": [
+    {
+      "name": "profile-memory",
+      "path": "douyin-speaking-daily/scripts/douyin-speaking-profile-memory.js",
+      "canonicalTool": "scripts/tools/douyin-speaking-profile-memory.js"
+    },
+    {
+      "name": "daily-runner",
+      "path": "douyin-speaking-daily/scripts/douyin-speaking-daily-runner.js",
+      "canonicalTool": "scripts/tools/douyin-speaking-daily-runner.js"
+    },
+    {
+      "name": "daily-report",
+      "path": "douyin-speaking-daily/scripts/douyin-speaking-daily-report.js",
+      "canonicalTool": "scripts/tools/douyin-speaking-daily-report.js"
+    },
+    {
+      "name": "viral-script-analyzer",
+      "path": "douyin-speaking-daily/scripts/douyin-viral-script-analyzer.js",
+      "canonicalTool": "scripts/tools/douyin-viral-script-analyzer.js"
+    },
+    {
+      "name": "video-transcriber",
+      "path": "douyin-speaking-daily/scripts/douyin-video-transcriber.js",
+      "canonicalTool": "scripts/tools/douyin-video-transcriber.js"
+    },
+    {
+      "name": "p1-smoke",
+      "path": "douyin-speaking-daily/scripts/douyin-speaking-daily-p1-smoke.js",
+      "canonicalTool": "scripts/tools/douyin-speaking-daily-p1-smoke.js"
+    }
+  ],
+  "memoryTemplates": [
+    "douyin-speaking-daily/memory-templates/douyin-speaking-profile.json"
+  ],
+  "outputRoot": "openclaw-voc-output/douyin-speaking-daily",
+  "p1Backlog": [
+    "外部定时推送渠道",
+    "日报后进入脚本共创会话,多轮反馈后定稿并沉淀偏好"
+  ]
+}

+ 27 - 0
douyin-speaking-daily/skills/README.md

@@ -0,0 +1,27 @@
+# 技能索引
+
+正式 OpenClaw 技能仍在 `douyin/` 分类目录中,本目录只做 `douyin-speaking-daily` 套件索引,方便按流程操作。
+
+| 顺序 | 技能 | 位置 | 作用 |
+|---|---|---|---|
+| 1 | `douyin-speaking-profile-builder` | `douyin/douyin-speaking-profile-builder/` | 问答建档,采集账号定位和监听边界 |
+| 2 | `douyin-speaking-profile-memory` | `douyin/douyin-speaking-profile-memory/` | 候选账号确认、状态推进和校准反馈写回 |
+| 3 | `douyin-speaking-keyword-monitor` | `douyin/douyin-speaking-keyword-monitor/` | 关键词监听,采集高互动视频和评论 |
+| 4 | `douyin-user-posts` | `douyin/douyin-user-posts/` | 按 `sec_user_id` 获取账号近期作品 |
+| 5 | `douyin-speaking-account-monitor` | `douyin/douyin-speaking-account-monitor/` | 具体账号监听,或按方向发现候选账号后监听 |
+| 6 | `douyin-speaking-daily-runner` | `douyin/douyin-speaking-daily-runner/` | P0 可执行总入口,自动采集、可选补逐字稿并生成日报 |
+| 7 | `douyin-viral-script-analyzer` | `douyin/douyin-viral-script-analyzer/` | 单条或多条爆款口播结构拆解 |
+| 8 | `douyin-video-transcript` | `douyin/douyin-video-transcript/` | 逐字稿 provider 抽象,支持 manual/openai/iflytek-gateway/iflytek-ist/none |
+| 9 | `douyin-speaking-daily-report` | `douyin/douyin-speaking-daily-report/` | 汇总 profile 和样本,输出每日稿件日报 |
+
+依赖的现有抖音原子技能:
+
+- `douyin-general-search`
+- `douyin-video-detail`
+- `douyin-video-comments`
+- `douyin-comment-replies`
+- `douyin-hashtag-search`
+- `douyin-user-search`
+- `douyin-user-profile`
+- `douyin-user-posts`
+- `douyin-mini-voc`

+ 94 - 0
douyin/douyin-speaking-account-monitor/SKILL.md

@@ -0,0 +1,94 @@
+---
+name: douyin-speaking-account-monitor
+description: 抖音口播账号监听编排,支持用户给具体账号精确监听,或给账号方向先发现候选账号并确认后监听近期作品。
+version: 0.1.2
+author: nkkj-BrainHack
+---
+
+# 抖音口播账号监听
+
+## 技能目的
+
+围绕口播日报的对标账号监听,采集指定账号或候选账号的近期作品,并补充互动数据、评论和热评回复,输出给 `douyin-speaking-daily-report`。
+
+## 两种交互入口
+
+### 1. 具体账号监听
+
+用户提供:
+
+- `sec_user_id`
+- 用户搜索返回的 `user_id`
+- 抖音主页链接
+- 抖音昵称
+- 对标账号名称
+
+流程:
+
+1. 如果已有 `sec_user_id`、`sec_uid` 或用户搜索返回的 `user_id`,直接调用 `douyin-user-posts`。
+2. 如果只有昵称/名称,先调用 `douyin-user-search` 返回候选账号。
+3. 候选账号必须让用户确认后,才能把 `user_id/sec_uid` 写入正式监听池。
+4. 对确认账号调用 `douyin-user-profile` 和 `douyin-user-posts`。
+
+### 2. 方向发现监听
+
+用户提供方向,如:
+
+- AI创业老板IP
+- 口腔医生科普号
+- 母婴营养测评号
+- 本地生活探店号
+
+流程:
+
+1. 用方向词调用 `douyin-user-search`。
+2. 输出候选账号摘要:昵称、简介、粉丝、认证、匹配理由。
+3. 等用户确认 3-10 个账号。
+4. 只对确认账号调用 `douyin-user-posts`。
+
+## Discovery Candidate Policy (P0)
+
+When `mode=discovery_direction`, do not rely on account search alone for broad or abstract directions such as `AI营销增长`, `VOC用户洞察`, or `市场情报`.
+
+1. First call `douyin-general-search` with the direction keyword, preferably `sort_type="1"`, `publish_time="180"`, and `content_type="1"`.
+2. Extract account candidates from `data.business_data[].data.aweme_info.author`, using `author.sec_uid`, `author.nickname`, the video `desc`, and interaction stats as the match evidence.
+3. Then call `douyin-user-search` as a supplemental exact-name/account search. It is useful for brand names, nicknames, and precise account terms, but can be noisy for strategy or industry terms.
+4. If either search returns HTTP 200 with any author/user candidates, surface the best 5-10 candidates and ask the user to confirm. Do not say "permission", "APIGAuth", or "Company binding" in this case.
+5. Only report an auth/company problem when token preflight is not `valid`, or when the API response itself is HTTP 401/403 or explicitly contains an auth/quota/company error.
+6. If relevance is weak, say "candidate relevance is weak" and ask for a narrower direction, target account nickname, homepage link, or `sec_user_id`; do not block the workflow as a backend failure.
+
+## 输入参数
+
+| 参数 | 类型 | 必填 | 默认 | 说明 |
+|---|---|---|---|---|
+| mode | string | 否 | `specific_accounts` | `specific_accounts` 或 `discovery_direction` |
+| accounts | array | 否 |  | 具体账号、昵称、主页链接或 sec_user_id |
+| discoveryDirection | string | 否 |  | 要发现的一类账号方向 |
+| confirmedAccounts | array | 否 |  | 用户确认后的账号对象 |
+| accountsLimit | integer | 否 | 5 | 每次监听账号数 |
+| postsPerAccount | integer | 否 | 10 | 每个账号拉取作品数 |
+| maxCommentPages | integer | 否 | 1 | 每个作品评论页数 |
+| includeReplies | boolean | 否 | true | 是否抓热评回复 |
+
+## P0 Hard Stop
+
+- 没有具体账号,也没有账号方向时,先追问,不启动监听。
+- 方向发现只产生候选账号,不等于正式监听。
+- 用户确认 `confirmedAccounts` 后,P0 不要先逐个抓 `douyin-user-profile` 或打开抖音主页链接补资料;直接进入 `douyin-user-posts` / `douyin-speaking-daily-runner` 拉近期作品和评论。
+- `douyin-user-profile` 只作为可选画像补充。它返回 404/500/`社交平台接口请求错误` 时,不代表日报闭环失败,也不要输出“抖音接口异常导致不能启动”;应标注为“账号主页资料补全失败,已跳过”,继续跑近期作品监听。
+- `douyin-user-posts` 不可用时,必须向用户说明账号近期作品监听未完成,不能降级成“只看用户主页”。
+
+## 输出
+
+- `accountCandidates`
+- `confirmedAccounts`
+- `accountProfiles`
+- `accountVideos`
+- `comments`
+- `replies`
+- `audit`
+
+## 下游
+
+- `douyin-viral-script-analyzer`
+- `douyin-speaking-daily-report`

+ 179 - 0
douyin/douyin-speaking-account-monitor/api-config.json

@@ -0,0 +1,179 @@
+{
+  "name": "douyin-speaking-account-monitor",
+  "displayName": "抖音口播账号监听",
+  "description": "支持具体账号精确监听,或按方向发现候选账号并确认后监听近期作品,为口播每日稿件日报提供对标账号内容样本。",
+  "category": "douyin",
+  "version": "0.1.2",
+  "type": "orchestration",
+  "parameters": {
+    "mode": {
+      "type": "string",
+      "enum": ["specific_accounts", "discovery_direction"],
+      "default": "specific_accounts"
+    },
+    "accounts": {
+      "type": "array",
+      "items": { "type": "string" },
+      "description": "具体账号、昵称、主页链接或 sec_user_id"
+    },
+    "discoveryDirection": {
+      "type": "string",
+      "description": "要发现的一类账号方向"
+    },
+    "confirmedAccounts": {
+      "type": "array",
+      "items": { "type": "object" },
+      "description": "用户确认后的账号对象,至少包含 sec_user_id/sec_uid/user_id"
+    },
+    "accountsLimit": { "type": "integer", "default": 5 },
+    "postsPerAccount": { "type": "integer", "default": 10 },
+    "maxCommentPages": { "type": "integer", "default": 1 },
+    "includeReplies": { "type": "boolean", "default": true },
+    "repliesPerVideo": { "type": "integer", "default": 2 },
+    "enrichProfiles": {
+      "type": "boolean",
+      "default": false,
+      "description": "是否额外补抓账号主页资料。P0 日报默认 false,避免主页资料接口临时 404/500 阻塞日报。"
+    }
+  },
+  "pipeline": [
+    {
+      "step": 1,
+      "name": "账号方向发现候选",
+      "condition": "mode === 'discovery_direction' && discoveryDirection && !confirmedAccounts.length",
+      "api": {
+        "skill": "douyin/douyin-user-search",
+        "method": "POST",
+        "body": {
+          "keyword": "${discoveryDirection}",
+          "cursor": 0
+        }
+      },
+      "output": "accountCandidates",
+      "gate": "输出候选账号摘要并等待用户确认,不能直接进入作品监听。"
+    },
+    {
+      "step": 2,
+      "name": "具体账号解析候选",
+      "condition": "mode === 'specific_accounts' && accounts contains nickname/name rather than sec_user_id",
+      "forEach": "accounts",
+      "api": {
+        "skill": "douyin/douyin-user-search",
+        "method": "POST",
+        "body": {
+          "keyword": "${account}",
+          "cursor": 0
+        }
+      },
+      "output": "accountCandidates",
+      "gate": "如果一个输入匹配多个账号,必须让用户确认。"
+    },
+    {
+      "step": 3,
+      "name": "补充账号主页信息",
+      "condition": "confirmedAccounts.length > 0 && enrichProfiles === true",
+      "forEach": "confirmedAccounts.slice(0, accountsLimit)",
+      "api": {
+        "skill": "douyin/douyin-user-profile",
+        "method": "GET",
+        "queryParams": {
+          "sec_user_id": "${account.sec_user_id || account.sec_uid || account.user_id}"
+        }
+      },
+      "output": "accountProfiles",
+      "optional": true,
+      "continueOnFailure": true,
+      "failurePolicy": "P0 日报不要因 douyin-user-profile 404/500 停止;标注账号主页资料补全失败并继续拉取 douyin-user-posts。"
+    },
+    {
+      "step": 4,
+      "name": "拉取账号近期作品",
+      "condition": "confirmedAccounts.length > 0",
+      "forEach": "confirmedAccounts.slice(0, accountsLimit)",
+      "api": {
+        "skill": "douyin/douyin-user-posts",
+        "method": "GET",
+        "queryParams": {
+          "sec_user_id": "${account.sec_user_id || account.sec_uid || account.user_id}",
+          "max_cursor": 0,
+          "count": "${postsPerAccount}"
+        }
+      },
+      "output": "accountVideos",
+      "hardStopOnFailure": true
+    },
+    {
+      "step": 5,
+      "name": "补充作品详情",
+      "forEach": "accountVideos",
+      "api": {
+        "skill": "douyin/douyin-video-detail",
+        "method": "GET",
+        "queryParams": { "aweme_id": "${video.aweme_id}" }
+      },
+      "output": "videoDetails"
+    },
+    {
+      "step": 6,
+      "name": "抓取作品评论",
+      "forEach": "accountVideos",
+      "api": {
+        "skill": "douyin/douyin-video-comments",
+        "method": "GET",
+        "queryParams": {
+          "aweme_id": "${video.aweme_id}",
+          "cursor": "${cursor}",
+          "count": 20
+        }
+      },
+      "pagination": {
+        "cursorField": "data.cursor",
+        "hasMoreField": "data.has_more",
+        "maxPages": "${maxCommentPages}"
+      },
+      "output": "comments"
+    },
+    {
+      "step": 7,
+      "name": "抓取热评回复",
+      "condition": "includeReplies === true",
+      "forEach": "comments → 每个视频按 digg_count 取 top repliesPerVideo 且 reply_comment_total > 0 的评论",
+      "api": {
+        "skill": "douyin/douyin-comment-replies",
+        "method": "GET",
+        "queryParams": {
+          "item_id": "${video.aweme_id}",
+          "comment_id": "${comment.cid}",
+          "cursor": 0,
+          "count": 20
+        }
+      },
+      "output": "replies"
+    }
+  ],
+  "response": {
+    "type": "object",
+    "properties": {
+      "accountCandidates": { "type": "array" },
+      "confirmedAccounts": { "type": "array" },
+      "accountProfiles": { "type": "array" },
+      "accountVideos": { "type": "array" },
+      "comments": { "type": "array" },
+      "replies": { "type": "array" },
+      "audit": { "type": "object" }
+    }
+  },
+  "timeout": 180000,
+  "retry": { "maxAttempts": 2, "delay": 2000, "backoffMultiplier": 2 },
+  "tokenConfig": {
+    "type": "bearer",
+    "configFile": "~/.openclaw/voc-credentials.json",
+    "tokenField": "vocToken",
+    "resolutionOrder": ["configFile"],
+    "apigId": "Vo3ROWEvDy"
+  },
+  "hardStops": [
+    "mode=discovery_direction 且没有 confirmedAccounts 时,只输出候选账号,等待用户确认。",
+    "douyin-user-posts 调用失败时,不得用用户主页或搜索结果替代账号近期作品监听。"
+  ]
+}

+ 70 - 0
douyin/douyin-speaking-daily-report/SKILL.md

@@ -0,0 +1,70 @@
+---
+name: douyin-speaking-daily-report
+description: 抖音口播每日稿件日报总入口,读取账号 profile 和抖音采集样本,输出日报、选题池、口播框架和校准问题。
+version: 0.1.13
+author: nkkj-BrainHack
+---
+
+# 抖音口播每日稿件日报
+
+## 技能目的
+
+把抖音关键词监听样本、评论信号、可选逐字稿和账号 profile 汇总成每日稿件日报。P0 输出到 OpenClaw 对话界面,并同步写入工作区文件。
+
+本技能面向通用抖音口播账号,不绑定任何单一项目名。所有改写建议都应从传入的 profile 读取行业、目标人群、账号定位和转化目标;没有 profile 时输出大众可读的爆款结构拆解。
+
+## 输入
+
+- `profile`:`memory/douyin-speaking-profile.json` 或等价 JSON。
+- `input`:`douyin-speaking-keyword-monitor` / `douyin-mini-voc` 输出,或包含 videos/comments 的 raw JSON。
+
+## 输出
+
+- `daily-report.md`
+- `daily-report.json`
+- `audit.json`
+- `markdown`:OpenClaw 对话可直接展示的短日报预览。
+- `fullReportPath`:完整日报文件路径。
+- `topicIdeasPreview`、`calibrationPrompt`:用于对话里直接校准选题。
+
+## 日报简报包含
+
+- 今日一句话判断;
+- 趋势信号;
+- 今日选题池;
+- 每条选题的来源、互动证据、置信度、可讲方向;
+- 逐字稿是否已就绪;
+- 明日监听建议;
+- 校准问题。
+
+日报简报不默认展开逐字稿细节和完整脚本,避免把“日报”和“脚本工作台”混在一起。
+
+## P0 边界
+
+- 只输出 OpenClaw 对话日报和工作区文件;
+- 不接企业微信、飞书、邮件;
+- 不自动发布;
+- 分享链接自动拆解放 P1。
+
+## OpenClaw 回复规则
+
+- 工具返回后,优先把 `assistantMessage` 原样贴给用户;没有该字段时再使用 `markdown`。
+- 不要只回复 `outputDir`、`files` 或 `fullReportPath`。路径只是本地备份,应放在正文末尾或用户追问时再给。
+- 日报、候选账号确认、逐字稿拆解等内容都必须在对话里直接展示核心正文,方便用户直接校准。
+
+## 脚本共创流程
+
+- 日报阶段只展示热点、选题摘要、置信度和下一步校准,不默认展开完整稿件。
+- 用户回复“选题 N”或明确要生成某条稿件时,调用本技能并传入 `selectedTopicIndex=N`,直接在对话里输出 v0 共创稿,同时写入 `script-session.json`。
+- 用户继续反馈时,传入 `scriptSession` 和 `feedback`,例如“开头更狠,老板视角,加一个具体 B2B 场景”。工具会生成 v1/v2/v3,并保留每轮反馈理解。
+- 用户确认定稿时,传入 `finalize=true` 或 `feedback=定稿`,输出最终稿件包,并写入 `memory/douyin-speaking-script-memory.json`。
+- 也可以直接传 `message=<用户原话>`,支持“选题2”“定稿”“保留1、2”“不要某方向”等自然话术;缺少 `input/scriptSession` 时会自动续接最近一次运行。
+- 下次进入共创时会读取 `scriptMemory`,把历史偏好的视角、开头强度、转化方式和禁忌表达带入 v0;若只是日报简报,也会在判断口径里展示历史偏好摘要。
+- 定稿前不要只告诉用户文件路径;必须展示当前稿件正文、改动说明、定稿自检和下一轮可回复方式。
+- 定稿输出必须包含:正式稿、30 秒压缩版、60 秒提词器版、标题备选、拍摄提示、评论区引导、本次沉淀的偏好。
+
+## 选题后成稿规则
+
+- 当某条选题匹配到逐字稿时,除了输出 `transcriptInsights`,还要输出 `draftScript`。
+- `draftScript` 必须迁移原爆款的结构、反差、证明方式和结尾动作,但主题、人群、案例、转化目标必须来自当前 profile。
+- 当用户选择某条选题后,传 `selectedTopicIndex`,对话里展示该选题的共创稿 v0、借鉴点和可修改方向;后续用 `scriptSession + feedback` 多轮共创,直到 `finalize` 定稿。

+ 181 - 0
douyin/douyin-speaking-daily-report/api-config.json

@@ -0,0 +1,181 @@
+{
+  "name": "douyin-speaking-daily-report",
+  "displayName": "抖音口播每日稿件日报",
+  "description": "读取账号 profile 和抖音采集样本,输出每日选题、口播框架、评论触发点、风险提醒和校准问题。",
+  "category": "douyin",
+  "version": "0.1.13",
+  "type": "local-script",
+  "execution": {
+    "kind": "command",
+    "command": "node",
+    "args": [
+      "scripts/tools/openclaw-tool-runner.js",
+      "--tool",
+      "douyin-speaking-daily-report.js",
+      "--result-prefix",
+      "DOUYIN_DAILY_REPORT_RESULT",
+      "--",
+      "--profile",
+      "{{profile}}",
+      "--input",
+      "{{input}}",
+      "--output",
+      "{{output}}",
+      "--project",
+      "{{project}}",
+      "--industry",
+      "{{industry}}",
+      "--account-positioning",
+      "{{accountPositioning}}",
+      "--target-audience",
+      "{{targetAudience}}",
+      "--conversion-goal",
+      "{{conversionGoal}}",
+      "--core-offer",
+      "{{coreOffer}}",
+      "--content-pillars",
+      "{{contentPillars}}",
+      "--keywords",
+      "{{keywords}}",
+      "--reference-accounts",
+      "{{referenceAccounts}}",
+      "--forbidden-topics",
+      "{{forbiddenTopics}}",
+      "--tone",
+      "{{tone}}",
+      "--daily-output-count",
+      "{{dailyOutputCount}}",
+      "--date",
+      "{{date}}",
+      "--selected-topic-index",
+      "{{selectedTopicIndex}}",
+      "--script-session",
+      "{{scriptSession}}",
+      "--feedback",
+      "{{feedback}}",
+      "--finalize",
+      "{{finalize}}",
+      "--script-memory",
+      "{{scriptMemory}}",
+      "--history-memory",
+      "{{historyMemory}}",
+      "--write-history",
+      "{{writeHistory}}",
+      "--message",
+      "{{message}}"
+    ],
+    "cwd": "{{workspaceRoot}}",
+    "outputParser": { "type": "stdout-prefix-json", "prefix": "DOUYIN_DAILY_REPORT_RESULT=" },
+    "exitCodes": { "0": "ok | needs_data", "1": "error" }
+  },
+  "parameters": {
+    "type": "object",
+    "required": [],
+    "properties": {
+      "profile": { "type": "string" },
+      "input": { "type": "string" },
+      "output": { "type": "string" },
+      "project": { "type": "string" },
+      "industry": { "type": "string" },
+      "accountPositioning": { "type": "string" },
+      "targetAudience": { "type": "string" },
+      "conversionGoal": { "type": "string" },
+      "coreOffer": { "type": "string" },
+      "contentPillars": { "type": "array", "items": { "type": "string" } },
+      "keywords": { "type": "array", "items": { "type": "string" } },
+      "referenceAccounts": { "type": "array", "items": { "type": "string" } },
+      "forbiddenTopics": { "type": "array", "items": { "type": "string" } },
+      "tone": { "type": "string" },
+      "dailyOutputCount": { "type": "integer", "default": 8 },
+      "selectedTopicIndex": {
+        "type": "integer",
+        "description": "传入选题序号时,返回该选题的逐字稿拆解和完整口播稿件;不传则返回日报简报。"
+      },
+      "scriptSession": {
+        "type": "string",
+        "description": "已有脚本共创会话 script-session.json 路径;传入后继续基于同一选题和历史反馈改稿。"
+      },
+      "feedback": {
+        "type": "string",
+        "description": "用户对当前稿件的自然语言反馈,例如:开头更狠、老板视角、加具体 B2B 场景、太泛了。"
+      },
+      "finalize": {
+        "type": "boolean",
+        "default": false,
+        "description": "为 true 时将当前脚本定稿,并写入脚本偏好记忆。"
+      },
+      "scriptMemory": {
+        "type": "string",
+        "default": "memory/douyin-speaking-script-memory.json",
+        "description": "脚本共创偏好记忆写入路径。"
+      },
+      "historyMemory": {
+        "type": "string",
+        "default": "memory/douyin-speaking-history.json",
+        "description": "日报/脚本共创运行历史写入路径。"
+      },
+      "writeHistory": {
+        "type": "boolean",
+        "default": true,
+        "description": "是否写入跨次运行历史。"
+      },
+      "message": {
+        "type": "string",
+        "description": "用户自然话术,例如:选题2、定稿、保留1和2、不要工具清单方向。未显式传 input/scriptSession 时会自动续接最近一次运行。"
+      },
+      "date": { "type": "string" }
+    }
+  },
+  "output": {
+    "type": "object",
+    "properties": {
+      "status": { "type": "string" },
+      "assistantMessage": {
+        "type": "string",
+        "description": "OpenClaw 对话应优先原样展示的内容正文;不要只展示文件路径。"
+      },
+      "markdown": {
+        "type": "string",
+        "description": "对话版日报正文,路径仅作为末尾备份信息。"
+      },
+      "outputDir": { "type": "string" },
+      "files": { "type": "array" },
+      "summary": { "type": "object" },
+      "qualityGate": { "type": "object" },
+      "scriptMemorySignals": { "type": "object" },
+      "oneLineJudgement": { "type": "string" },
+      "topicIdeaCount": { "type": "integer" },
+      "precisionLevel": { "type": "string" },
+      "topicIdeasPreview": { "type": "array" },
+      "selectedTopicIndex": { "type": "integer" },
+      "selectedTopic": { "type": "object" },
+      "selectedScriptPath": { "type": "string" },
+      "scriptSessionPath": { "type": "string" },
+      "scriptSessionStatus": { "type": "string" },
+      "scriptVersion": { "type": "string" },
+      "scriptMemoryPath": { "type": "string" },
+      "historyMemoryPath": { "type": "string" },
+      "naturalCommand": { "type": "object" },
+      "topicPreferenceMemoryPath": { "type": "string" },
+      "autoResolvedInputPath": { "type": "string" },
+      "autoResolvedScriptSessionPath": { "type": "string" },
+      "calibrationPrompt": { "type": "string" },
+      "calibrationQuestions": { "type": "array" },
+      "fullReportPath": { "type": "string" },
+      "warningCount": { "type": "integer" }
+    }
+  },
+  "deployment": {
+    "toolInstallPaths": [
+      "~/.openclaw/tools/douyin-speaking-daily-report.js",
+      "~/.openclaw/workspace/scripts/tools/douyin-speaking-daily-report.js",
+      "~/.openclaw/workspace/scripts/tools/openclaw-tool-runner.js"
+    ]
+  },
+  "relatedSkills": [
+    "douyin-speaking-profile-builder",
+    "douyin-speaking-keyword-monitor",
+    "douyin-viral-script-analyzer",
+    "douyin-video-transcript"
+  ]
+}

+ 141 - 0
douyin/douyin-speaking-daily-runner/SKILL.md

@@ -0,0 +1,141 @@
+---
+name: douyin-speaking-daily-runner
+description: 抖音口播每日稿件日报 P0 总入口,执行关键词采集、账号作品监听、评论采集、可选爆款视频逐字稿补跑和日报输出。
+version: 0.2.12
+author: nkkj-BrainHack
+---
+
+# 抖音口播每日稿件日报 Runner
+
+## 技能目的
+
+这是 `douyin-speaking-daily` P0 闭环的可执行总入口。OpenClaw 调用本技能后,可以读取账号 profile,采集抖音关键词视频、确认账号近期作品、评论/回复,并交给 `douyin-speaking-daily-report` 输出日报。
+
+当传入 `autoTranscriptTopN > 0` 时,runner 会自动选择互动分最高的视频补跑逐字稿,再把逐字稿回灌到同一次日报里,让 P0 选题获得更高置信度,并在日报里输出通用的爆款口播结构拆解。
+
+本 runner 面向通用抖音口播账号,不绑定任何单一项目名;改写方向必须从 profile 的行业、人群、账号定位和转化目标中读取。
+
+## 输入方式
+
+优先使用 profile 文件:
+
+```text
+--profile memory/douyin-speaking-profile.json
+```
+
+兼容入口:
+
+- 如果运行器尝试执行本技能目录下的 `index.js`,该文件会自动转发到 `scripts/tools/douyin-speaking-daily-runner.js`。
+- 正式入口仍以 `api-config.json` 的 local-script 配置为准。
+
+也可以直接传核心字段:
+
+- `project`
+- `industry`
+- `accountPositioning`
+- `targetAudience`
+- `conversionGoal`
+- `keywords`
+- `confirmedAccounts`
+- `accountDiscoveryDirection`
+
+## 执行路径
+
+```text
+profile / inline params
+  -> douyin general search
+  -> confirmed account posts
+  -> comments / optional replies
+  -> optional autoTranscriptTopN
+  -> runner-raw-input.json
+  -> douyin-speaking-daily-report
+  -> daily-report.md/json + OpenClaw markdown
+```
+
+## 自动逐字稿
+
+默认不自动转写,避免无意消耗网关任务。需要完整闭环时显式传:
+
+```text
+--auto-transcript-top-n 1
+--auto-transcript-provider iflytek-gateway
+--transcript-cache memory/douyin-speaking-transcript-cache.json
+--transcript-reuse-cache true
+--transcript-daily-budget 1
+```
+
+规则:
+
+- 从关键词视频和账号视频中按互动分挑选 Top N。
+- 已有逐字稿的视频不会重复补跑。
+- 默认只转写 10 分钟以内的视频,可用 `autoTranscriptMaxDurationMs` 调整。
+- 转写产物写入 `transcripts/<aweme_id>/transcript.json` 和 `transcript.txt`。
+- 每次开启自动逐字稿时都会生成 `transcript-queue.json`,记录候选视频的 `cached/generated/skipped/failed` 状态。
+- 默认复用 `memory/douyin-speaking-transcript-cache.json`,同一 `awemeId` 已有可用逐字稿时不重复调用 ASR。
+- `transcriptDailyBudget` 控制单次最多新增转写数量,缓存命中不占用新增额度。
+- 日报简报只显示 `逐字稿:已就绪/未补` 和选题摘要;用户选择具体选题后,再用 `douyin-speaking-daily-report` 的 `selectedTopicIndex` 展开逐字稿拆解和完整口播稿草案。
+
+## 账号监听规则
+
+- 已确认账号可用 `confirmedAccounts` 传入,字段支持 `sec_user_id`、`sec_uid`、`user_id`。
+- 只有 `accountDiscoveryDirection` 时,runner 先从 `douyin-general-search` 的爆款视频作者里抽候选账号,再用 `douyin-user-search` 补充精确昵称搜索,避免抽象方向词只按达人名搜索导致误判。
+- 候选账号第一屏优先展示带视频样本证据的账号;纯达人搜索结果只作为补充。
+- 用户只给昵称、主页方向或 `accountDiscoveryDirection` 时,runner 会输出 `account-candidates.json` 和 `account-candidates.md`,默认等待用户确认。
+- 演示自动跑通可显式设置 `autoConfirmAccounts=true`,正式使用建议先让用户确认候选账号。
+- 已确认账号的近期作品接口失败时,runner 返回 `account_monitor_failed`,不能用账号搜索结果假装近期作品监听完成。
+
+## 输出
+
+- `daily-report.md`
+- `daily-report.json`
+- `audit.json`
+- `runner-raw-input.json`
+- `account-candidates.json`
+- `account-candidates.md`,当存在候选账号时生成
+- `transcript-queue.json`,记录本次自动逐字稿候选队列、缓存命中和失败原因
+- `transcripts/<aweme_id>/transcript.json`,当开启自动逐字稿时生成
+- OpenClaw 对话可直接展示的 `markdown`:默认是短日报预览,不再把完整日报塞进对话。
+- `fullReportPath`:完整日报文件路径。
+- `topicIdeasPreview` 和 `calibrationPrompt`:用于聊天里直接追问校准反馈。
+
+## 对话返回规则
+
+runner 返回后,先把返回结果里的 `assistantMessage` 原样贴给用户;没有该字段时再使用 `markdown`。不要只回复文件路径。
+`status=ok` 时直接展示日报简报和 Top 选题,不要在日报阶段展开完整脚本;用户回复“选题 N”后,再进入脚本稿件生成和确认流程。
+`status=needs_account_confirmation` 时直接展示候选账号确认正文;`gateway_auth_blocked` 或 `account_monitor_failed` 时先展示错误原因、已采到的数据和下一步动作。
+如果用户要看完整本地备份,再给 `fullReportPath`。
+校准问题必须结合正文里的 Top 选题问,优先让用户按序号回复。
+
+## 日报后脚本共创
+
+日报本身是“选题雷达”,不是最终稿件交付页。用户选择某条选题后,应调用 `douyin-speaking-daily-report`:
+
+```text
+selectedTopicIndex=<用户选择的序号>
+```
+
+返回 v0 共创稿和 `script-session.json`。用户继续反馈时,把同一个 `scriptSession` 路径连同 `feedback` 传回 report;用户说“定稿”时传 `finalize=true` 或 `feedback=定稿`,把最终稿和偏好沉淀到 `memory/douyin-speaking-script-memory.json`。每一轮都要优先展示 `assistantMessage` 正文,不要只给文件路径。
+
+runner 会把 `selectedTopicIndex`、`scriptSession`、`feedback`、`finalize`、`scriptMemory` 透传给 `douyin-speaking-daily-report`。如果 OpenClaw 误选了 runner 作为入口,也必须进入脚本共创,不要重新输出日报。
+
+## 自然话术续接
+
+如果用户在日报后直接说“选题2”“改稿:开头更狠”“定稿”“保留1、2”“不要某方向”,可以直接调用 runner 并把原话传入:
+
+```text
+message=<用户原话>
+```
+
+规则:
+
+- 未显式传 `input` 时,runner 会自动续接最近一次 `runner-raw-input.json`,避免重新采集。
+- 未显式传 `scriptSession` 时,report 会自动续接最近一次 `script-session.json`,避免“定稿”时丢失 v1/v2。
+- “保留1、2”“不要某方向”会写入 `memory/douyin-speaking-script-memory.json` 的选题偏好,下次日报排序会参考这些权重。
+- 定稿前会做稿件自检,检查测试项目词泄漏、过泛、缺少场景、缺少互动/转化动作等问题。
+
+## 边界
+
+- 没有 VOC Token 时不能在线采集,请先运行 `node ~/.openclaw/tools/set-voc-token.js <token>`。
+- 如果 VOC 社媒网关返回 403 且没有采到任何样本,runner 返回 `gateway_auth_blocked`。
+- 抖音接口失败时,runner 会把错误保留到 `errors`,不伪造样本。
+- 自动逐字稿失败不阻塞日报,日报会继续标注“未提供逐字稿/结构推断”。

+ 315 - 0
douyin/douyin-speaking-daily-runner/api-config.json

@@ -0,0 +1,315 @@
+{
+  "name": "douyin-speaking-daily-runner",
+  "displayName": "抖音口播每日稿件日报 Runner",
+  "description": "P0 总入口:读取 profile 或核心参数,自动采集抖音关键词视频、确认账号作品、评论/回复,并输出每日稿件日报。",
+  "category": "douyin",
+  "version": "0.2.12",
+  "type": "local-script",
+  "execution": {
+    "kind": "command",
+    "command": "node",
+    "args": [
+      "scripts/tools/douyin-speaking-daily-runner.js",
+      "--result-prefix",
+      "DOUYIN_SPEAKING_DAILY_RUNNER_RESULT",
+      "--profile",
+      "{{profile}}",
+      "--input",
+      "{{input}}",
+      "--output",
+      "{{output}}",
+      "--project",
+      "{{project}}",
+      "--industry",
+      "{{industry}}",
+      "--account-positioning",
+      "{{accountPositioning}}",
+      "--target-audience",
+      "{{targetAudience}}",
+      "--conversion-goal",
+      "{{conversionGoal}}",
+      "--core-offer",
+      "{{coreOffer}}",
+      "--content-pillars",
+      "{{contentPillars}}",
+      "--keywords",
+      "{{keywords}}",
+      "--reference-accounts",
+      "{{referenceAccounts}}",
+      "--account-discovery-direction",
+      "{{accountDiscoveryDirection}}",
+      "--confirmed-accounts",
+      "{{confirmedAccounts}}",
+      "--forbidden-topics",
+      "{{forbiddenTopics}}",
+      "--tone",
+      "{{tone}}",
+      "--daily-output-count",
+      "{{dailyOutputCount}}",
+      "--videos-per-keyword",
+      "{{videosPerKeyword}}",
+      "--max-keywords",
+      "{{maxKeywords}}",
+      "--publish-time",
+      "{{publishTime}}",
+      "--sort-type",
+      "{{sortType}}",
+      "--max-videos-with-comments",
+      "{{maxVideosWithComments}}",
+      "--max-comment-pages",
+      "{{maxCommentPages}}",
+      "--include-replies",
+      "{{includeReplies}}",
+      "--replies-per-video",
+      "{{repliesPerVideo}}",
+      "--accounts",
+      "{{accounts}}",
+      "--accounts-limit",
+      "{{accountsLimit}}",
+      "--posts-per-account",
+      "{{postsPerAccount}}",
+      "--auto-confirm-accounts",
+      "{{autoConfirmAccounts}}",
+      "--dry-run",
+      "{{dryRun}}",
+      "--auto-transcript-top-n",
+      "{{autoTranscriptTopN}}",
+      "--auto-transcript-provider",
+      "{{autoTranscriptProvider}}",
+      "--auto-transcript-max-duration-ms",
+      "{{autoTranscriptMaxDurationMs}}",
+      "--transcript-poll-interval-ms",
+      "{{transcriptPollIntervalMs}}",
+      "--transcript-max-polls",
+      "{{transcriptMaxPolls}}",
+      "--transcript-max-download-mb",
+      "{{transcriptMaxDownloadMb}}",
+      "--transcript-download-timeout-ms",
+      "{{transcriptDownloadTimeoutMs}}",
+      "--transcript-download-idle-timeout-ms",
+      "{{transcriptDownloadIdleTimeoutMs}}",
+      "--transcript-cache",
+      "{{transcriptCache}}",
+      "--transcript-reuse-cache",
+      "{{transcriptReuseCache}}",
+      "--transcript-daily-budget",
+      "{{transcriptDailyBudget}}",
+      "--write-profile",
+      "{{writeProfile}}",
+      "--profile-output",
+      "{{profileOutput}}",
+      "--selected-topic-index",
+      "{{selectedTopicIndex}}",
+      "--script-session",
+      "{{scriptSession}}",
+      "--feedback",
+      "{{feedback}}",
+      "--finalize",
+      "{{finalize}}",
+      "--script-memory",
+      "{{scriptMemory}}",
+      "--history-memory",
+      "{{historyMemory}}",
+      "--write-history",
+      "{{writeHistory}}",
+      "--message",
+      "{{message}}"
+    ],
+    "cwd": "{{workspaceRoot}}",
+    "outputParser": {
+      "type": "stdout-prefix-json",
+      "prefix": "DOUYIN_SPEAKING_DAILY_RUNNER_RESULT="
+    },
+    "exitCodes": {
+      "0": "ok | needs_data | needs_account_confirmation | account_monitor_failed | gateway_auth_blocked",
+      "1": "error"
+    }
+  },
+  "parameters": {
+    "type": "object",
+    "required": [],
+    "properties": {
+      "profile": { "type": "string" },
+      "input": { "type": "string", "description": "已有 raw json,可跳过在线采集" },
+      "output": { "type": "string" },
+      "project": { "type": "string" },
+      "industry": { "type": "string" },
+      "accountPositioning": { "type": "string" },
+      "targetAudience": { "type": "string" },
+      "conversionGoal": { "type": "string" },
+      "coreOffer": { "type": "string" },
+      "contentPillars": { "type": "array", "items": { "type": "string" } },
+      "keywords": { "type": "array", "items": { "type": "string" } },
+      "referenceAccounts": { "type": "array", "items": { "type": "string" } },
+      "accountDiscoveryDirection": { "type": "string" },
+      "confirmedAccounts": {
+        "type": "string",
+        "description": "JSON 字符串或文件路径,账号对象至少包含 sec_user_id/sec_uid/user_id"
+      },
+      "accounts": {
+        "type": "array",
+        "items": { "type": "string" },
+        "description": "直接传入 sec_user_id 或抖音 user 链接"
+      },
+      "forbiddenTopics": { "type": "array", "items": { "type": "string" } },
+      "tone": { "type": "string" },
+      "dailyOutputCount": { "type": "integer", "default": 8 },
+      "videosPerKeyword": { "type": "integer", "default": 5 },
+      "maxKeywords": { "type": "integer", "default": 5 },
+      "publishTime": { "type": "string", "enum": ["0", "1", "7", "180"], "default": "7" },
+      "sortType": { "type": "string", "enum": ["0", "1", "2"], "default": "1" },
+      "maxVideosWithComments": { "type": "integer", "default": 8 },
+      "maxCommentPages": { "type": "integer", "default": 1 },
+      "includeReplies": { "type": "boolean", "default": true },
+      "repliesPerVideo": { "type": "integer", "default": 2 },
+      "accountsLimit": { "type": "integer", "default": 5 },
+      "postsPerAccount": { "type": "integer", "default": 8 },
+      "autoConfirmAccounts": { "type": "boolean", "default": false },
+      "dryRun": { "type": "boolean", "default": false },
+      "autoTranscriptTopN": {
+        "type": "integer",
+        "default": 0,
+        "description": "自动选择互动分最高的视频补跑逐字稿;0 表示关闭,1 表示补跑 Top 1。"
+      },
+      "autoTranscriptProvider": {
+        "type": "string",
+        "enum": ["iflytek-gateway", "iflytek-ist", "openai", "none"],
+        "default": "iflytek-gateway"
+      },
+      "autoTranscriptMaxDurationMs": {
+        "type": "integer",
+        "default": 600000,
+        "description": "自动转写的视频最长毫秒数,默认 10 分钟。"
+      },
+      "transcriptPollIntervalMs": { "type": "integer", "default": 4000 },
+      "transcriptMaxPolls": { "type": "integer", "default": 30 },
+      "transcriptMaxDownloadMb": { "type": "integer", "default": 80 },
+      "transcriptDownloadTimeoutMs": { "type": "integer", "default": 120000 },
+      "transcriptDownloadIdleTimeoutMs": { "type": "integer", "default": 15000 },
+      "transcriptCache": {
+        "type": "string",
+        "default": "memory/douyin-speaking-transcript-cache.json",
+        "description": "逐字稿缓存文件路径,同一 awemeId 命中后不重复消耗 ASR。"
+      },
+      "transcriptReuseCache": {
+        "type": "boolean",
+        "default": true,
+        "description": "是否优先复用 transcriptCache。"
+      },
+      "transcriptDailyBudget": {
+        "type": "integer",
+        "default": 1,
+        "description": "单次运行最多新增转写数量,缓存命中不计入新增额度。"
+      },
+      "writeProfile": {
+        "type": "boolean",
+        "default": false,
+        "description": "运行后把 candidateAccounts、lastRunSummary、state 等写回 profile。"
+      },
+      "profileOutput": {
+        "type": "string",
+        "description": "未传 profile 时的 profile 写回路径,默认 memory/douyin-speaking-profile.json。"
+      },
+      "selectedTopicIndex": {
+        "type": "integer",
+        "description": "日报后用户选择的选题序号;传入后 runner 会转交 daily-report 进入脚本共创 v0。"
+      },
+      "scriptSession": {
+        "type": "string",
+        "description": "已有脚本共创会话 script-session.json 路径;传入后继续改稿。"
+      },
+      "feedback": {
+        "type": "string",
+        "description": "用户对当前脚本的自然语言反馈。"
+      },
+      "finalize": {
+        "type": "boolean",
+        "default": false,
+        "description": "确认定稿并写入脚本偏好记忆。"
+      },
+      "scriptMemory": {
+        "type": "string",
+        "default": "memory/douyin-speaking-script-memory.json",
+        "description": "脚本偏好记忆写入路径。"
+      },
+      "historyMemory": {
+        "type": "string",
+        "default": "memory/douyin-speaking-history.json",
+        "description": "日报/脚本共创运行历史写入路径。"
+      },
+      "writeHistory": {
+        "type": "boolean",
+        "default": true,
+        "description": "是否写入跨次运行历史。"
+      },
+      "message": {
+        "type": "string",
+        "description": "用户自然话术,例如:选题2、定稿、保留1和2、不要工具清单方向。未显式传 input/scriptSession 时会自动续接最近一次运行。"
+      }
+    }
+  },
+  "output": {
+    "type": "object",
+    "properties": {
+      "status": { "type": "string" },
+      "assistantMessage": {
+        "type": "string",
+        "description": "OpenClaw 对话应优先原样展示的内容正文;不要只展示文件路径。"
+      },
+      "markdown": {
+        "type": "string",
+        "description": "对话版日报或候选账号确认正文,路径仅作为末尾备份信息。"
+      },
+      "outputDir": { "type": "string" },
+      "files": { "type": "array" },
+      "summary": { "type": "object" },
+      "qualityGate": { "type": "object" },
+      "scriptMemorySignals": { "type": "object" },
+      "accountCandidates": {
+        "type": "array",
+        "description": "需要用户确认的候选账号摘要。status=needs_account_confirmation 时优先展示。"
+      },
+      "transcriptResults": { "type": "array" },
+      "transcriptQueuePath": { "type": "string" },
+      "transcriptCachePath": { "type": "string" },
+      "warnings": { "type": "array" },
+      "errors": { "type": "array" },
+      "nextAction": { "type": "string" },
+      "profileState": { "type": "string" },
+      "readyForDailyReport": { "type": "boolean" },
+      "oneLineJudgement": { "type": "string" },
+      "topicIdeaCount": { "type": "integer" },
+      "topicIdeasPreview": { "type": "array" },
+      "selectedTopicIndex": { "type": "integer" },
+      "selectedTopic": { "type": "object" },
+      "selectedScriptPath": { "type": "string" },
+      "scriptSessionPath": { "type": "string" },
+      "scriptSessionStatus": { "type": "string" },
+      "scriptVersion": { "type": "string" },
+      "scriptMemoryPath": { "type": "string" },
+      "historyMemoryPath": { "type": "string" },
+      "naturalCommand": { "type": "object" },
+      "topicPreferenceMemoryPath": { "type": "string" },
+      "autoResolvedInputPath": { "type": "string" },
+      "autoResolvedScriptSessionPath": { "type": "string" },
+      "calibrationPrompt": { "type": "string" },
+      "calibrationQuestions": { "type": "array" },
+      "fullReportPath": { "type": "string" }
+    }
+  },
+  "deployment": {
+    "toolInstallPaths": [
+      "~/.openclaw/tools/douyin-speaking-daily-runner.js",
+      "~/.openclaw/workspace/scripts/tools/douyin-speaking-daily-runner.js",
+      "~/.openclaw/workspace/scripts/tools/douyin-speaking-daily-report.js",
+      "~/.openclaw/workspace/scripts/tools/openclaw-tool-runner.js"
+    ]
+  },
+  "relatedSkills": [
+    "douyin-speaking-profile-builder",
+    "douyin-speaking-profile-memory",
+    "douyin-speaking-daily-report",
+    "douyin-speaking-account-monitor",
+    "douyin-video-transcript"
+  ]
+}

+ 36 - 0
douyin/douyin-speaking-daily-runner/index.js

@@ -0,0 +1,36 @@
+#!/usr/bin/env node
+const fs = require('fs');
+const path = require('path');
+const os = require('os');
+const { spawnSync } = require('child_process');
+
+function candidates() {
+  return [
+    path.resolve(__dirname, '..', '..', 'scripts', 'tools', 'douyin-speaking-daily-runner.js'),
+    path.join(os.homedir(), '.openclaw', 'workspace', 'scripts', 'tools', 'douyin-speaking-daily-runner.js'),
+    path.join(os.homedir(), '.openclaw', 'tools', 'douyin-speaking-daily-runner.js'),
+    path.resolve(process.cwd(), 'scripts', 'tools', 'douyin-speaking-daily-runner.js')
+  ];
+}
+
+const target = candidates().find(filePath => fs.existsSync(filePath));
+
+if (!target) {
+  console.error('douyin-speaking-daily-runner entrypoint was not found.');
+  console.error('Expected one of:');
+  candidates().forEach(filePath => console.error(`- ${filePath}`));
+  process.exit(1);
+}
+
+const child = spawnSync(process.execPath, [target, ...process.argv.slice(2)], {
+  cwd: process.cwd(),
+  env: process.env,
+  stdio: 'inherit'
+});
+
+if (child.error) {
+  console.error(child.error.message);
+  process.exit(1);
+}
+
+process.exit(child.status ?? 0);

+ 37 - 0
douyin/douyin-speaking-keyword-monitor/SKILL.md

@@ -0,0 +1,37 @@
+---
+name: douyin-speaking-keyword-monitor
+description: 抖音口播关键词监听编排,根据账号 profile 的关键词抓取近 1 天/7 天高互动内容、评论和热评回复。
+version: 0.1.0
+author: nkkj-BrainHack
+---
+
+# 抖音口播关键词监听
+
+## 技能目的
+
+围绕用户配置的行业关键词、痛点词和热点词,采集抖音高互动视频、评论和热评回复,筛出适合口播拆解的内容样本。
+
+## 输入参数
+
+| 参数 | 类型 | 必填 | 默认 | 说明 |
+|---|---|---|---|---|
+| keywords | string[] | 是 |  | 监听关键词 |
+| videosPerKeyword | integer | 否 | 8 | 每个关键词保留的视频数量 |
+| maxCommentPages | integer | 否 | 1 | 每个视频评论页数 |
+| includeReplies | boolean | 否 | true | 是否抓高赞热评回复 |
+| repliesPerVideo | integer | 否 | 2 | 每个视频抓几条热评回复 |
+| publish_time | string | 否 | 7 | 1=最近一天,7=最近一周,180=最近半年 |
+| sort_type | string | 否 | 1 | 0=综合,1=最多点赞,2=最新发布 |
+
+## 编排流程
+
+1. `douyin-general-search` 搜索关键词视频。
+2. 按点赞、评论、分享、播放加权筛选候选视频。
+3. `douyin-video-detail` 补充详情。
+4. `douyin-video-comments` 抓评论。
+5. 可选 `douyin-comment-replies` 抓热评回复。
+6. 输出给 `douyin-speaking-daily-report` 生成日报。
+
+## 注意
+
+该技能只负责关键词监听。账号近期作品监听由 P0 技能 `douyin-speaking-account-monitor` 负责。

+ 117 - 0
douyin/douyin-speaking-keyword-monitor/api-config.json

@@ -0,0 +1,117 @@
+{
+  "name": "douyin-speaking-keyword-monitor",
+  "displayName": "抖音口播关键词监听",
+  "description": "根据账号 profile 的关键词抓取近 1 天/7 天高互动抖音内容、评论和热评回复,为每日稿件日报提供样本。",
+  "category": "douyin",
+  "version": "0.1.0",
+  "type": "orchestration",
+  "parameters": {
+    "keywords": { "type": "array", "items": { "type": "string" }, "required": true },
+    "videosPerKeyword": { "type": "integer", "required": false, "default": 8 },
+    "maxCommentPages": { "type": "integer", "required": false, "default": 1 },
+    "includeReplies": { "type": "boolean", "required": false, "default": true },
+    "repliesPerVideo": { "type": "integer", "required": false, "default": 2 },
+    "publish_time": { "type": "string", "required": false, "default": "7", "enum": ["1", "7", "180"] },
+    "sort_type": { "type": "string", "required": false, "default": "1", "enum": ["0", "1", "2"] },
+    "content_type": { "type": "string", "required": false, "default": "1", "enum": ["1"] }
+  },
+  "pipeline": [
+    {
+      "step": 1,
+      "name": "按关键词搜索高互动视频",
+      "forEach": "keywords",
+      "api": {
+        "skill": "douyin/douyin-general-search",
+        "method": "POST",
+        "body": {
+          "keyword": "${keyword}",
+          "cursor": 0,
+          "sort_type": "${sort_type}",
+          "publish_time": "${publish_time}",
+          "filter_duration": "0",
+          "content_type": "${content_type}",
+          "search_id": "",
+          "backtrace": ""
+        }
+      },
+      "output": "searchResults"
+    },
+    {
+      "step": 2,
+      "name": "筛选口播候选视频",
+      "type": "compute",
+      "logic": "按 digg_count、comment_count、share_count、play_count 加权排序,保留 videosPerKeyword 条。"
+    },
+    {
+      "step": 3,
+      "name": "补充视频详情",
+      "forEach": "selectedVideos",
+      "api": {
+        "skill": "douyin/douyin-video-detail",
+        "method": "GET",
+        "queryParams": { "aweme_id": "${video.aweme_id}" }
+      },
+      "output": "videoDetails"
+    },
+    {
+      "step": 4,
+      "name": "抓取视频评论",
+      "forEach": "selectedVideos",
+      "api": {
+        "skill": "douyin/douyin-video-comments",
+        "method": "GET",
+        "queryParams": { "aweme_id": "${video.aweme_id}", "cursor": "${cursor}", "count": 20 }
+      },
+      "pagination": {
+        "cursorField": "data.cursor",
+        "hasMoreField": "data.has_more",
+        "maxPages": "${maxCommentPages}"
+      },
+      "output": "comments"
+    },
+    {
+      "step": 5,
+      "name": "抓取热评回复",
+      "condition": "includeReplies === true",
+      "forEach": "comments → 每个视频按 digg_count 取 top repliesPerVideo 且 reply_comment_total > 0 的评论",
+      "api": {
+        "skill": "douyin/douyin-comment-replies",
+        "method": "GET",
+        "queryParams": {
+          "item_id": "${video.aweme_id}",
+          "comment_id": "${comment.cid}",
+          "cursor": 0,
+          "count": 20
+        }
+      },
+      "output": "replies"
+    },
+    {
+      "step": 6,
+      "name": "生成口播日报样本包",
+      "type": "compute",
+      "output": "speakingDailyRawDataset"
+    }
+  ],
+  "response": {
+    "type": "object",
+    "properties": {
+      "videos": { "type": "array" },
+      "comments": { "type": "array" },
+      "replies": { "type": "array" },
+      "contentSignals": { "type": "array" },
+      "hookCandidates": { "type": "array" },
+      "audienceQuestions": { "type": "array" },
+      "audit": { "type": "object" }
+    }
+  },
+  "timeout": 180000,
+  "retry": { "maxAttempts": 2, "delay": 2000, "backoffMultiplier": 2 },
+  "tokenConfig": {
+    "type": "bearer",
+    "configFile": "~/.openclaw/voc-credentials.json",
+    "tokenField": "vocToken",
+    "resolutionOrder": ["configFile"],
+    "apigId": "Vo3ROWEvDy"
+  }
+}

+ 76 - 0
douyin/douyin-speaking-profile-builder/SKILL.md

@@ -0,0 +1,76 @@
+---
+name: douyin-speaking-profile-builder
+description: 抖音口播日报账号建档技能,通过分步问答采集账号定位、目标人群、转化目标、监听关键词、对标账号和内容禁区。
+version: 0.1.1
+author: nkkj-BrainHack
+---
+
+# 抖音口播日报账号建档
+
+## 技能目的
+
+在生成抖音口播爆款内容日报前,先把账号方向喂清楚。该技能负责分步采集账号定位和监听边界,避免日报变成泛泛热点搬运。
+
+## 问答原则
+
+- 一次只问 1-3 个问题。
+- 用户确认 profile 前,不启动日报采集。
+- 日报后的校准意见写回 `calibrationNotes`,用于下次提权或降权。
+- 账号监听是 P0。用户可以给具体账号,也可以给账号方向先做候选发现。
+
+## 必填资料
+
+| 字段 | 说明 |
+|---|---|
+| projectName | 项目或账号名称 |
+| industry | 行业/品类 |
+| accountPositioning | 账号定位,如老板 IP、知识博主、测评号、品牌号 |
+| targetAudience | 目标用户 |
+| conversionGoal | 涨粉、咨询、卖课、卖货、品牌认知等 |
+| keywords | 行业关键词、痛点词、热点词 |
+| accountMonitorMode | `specific_accounts` 或 `discovery_direction` |
+| referenceAccounts 或 accountDiscoveryDirection | 具体监听账号,或要发现的一类账号方向 |
+
+## 可选资料
+
+`coreOffer`、`contentPillars`、`forbiddenTopics`、`tone`、`dailyOutputCount`。
+
+## 首轮提问模板
+
+```text
+老板,先把这只口播龙虾喂准一点。我先问 3 个问题:
+1. 这个抖音/视频号口播账号服务什么行业或品类?
+2. 你希望它主要帮你涨粉、获客、卖产品,还是建立品牌认知?
+3. 给我 3-5 个你想长期监听的关键词。
+```
+
+## 账号监听追问模板
+
+```text
+账号监听这块你想怎么喂?
+1. 给我具体对标账号、主页链接、昵称或 sec_user_id,我做精确监听;
+2. 你只给一个方向,比如“AI创业老板IP”“口腔医生科普号”,我先搜索候选账号,整理后让你确认监听池。
+```
+
+如果用户只给方向,输出候选账号后必须等待用户确认,不能直接把候选账号写入正式监听池。
+
+## Account Discovery Handoff (P0)
+
+For account discovery directions, especially abstract B2B/content strategy terms, hand off to `douyin-speaking-account-monitor` with this rule:
+
+- Use `douyin-general-search` first to discover authors from matching videos.
+- Use `douyin-user-search` second as an exact account/name supplement.
+- If either API returns HTTP 200 and non-empty candidates, present candidates for user confirmation. Do not describe it as an APIGAuth/company binding issue.
+- Only mention token/auth/company problems when preflight is not `valid`, or when an API response is HTTP 401/403 or explicitly reports auth/quota/company failure.
+- If candidates are noisy, ask one narrowing question: target account type, example account nickname/link, or `sec_user_id`.
+
+## 输出
+
+输出 `douyinSpeakingProfile`,建议保存为 `memory/douyin-speaking-profile.json`。
+
+## 下游
+
+- `douyin-speaking-keyword-monitor`
+- `douyin-speaking-account-monitor`
+- `douyin-speaking-daily-report`
+- `douyin-viral-script-analyzer`

+ 58 - 0
douyin/douyin-speaking-profile-builder/api-config.json

@@ -0,0 +1,58 @@
+{
+  "name": "douyin-speaking-profile-builder",
+  "displayName": "抖音口播日报账号建档",
+  "description": "通过分步问答采集账号定位、目标人群、转化目标、监听关键词、对标账号和内容禁区,为抖音口播每日稿件日报提供长期 profile。",
+  "category": "douyin",
+  "version": "0.1.0",
+  "type": "conversational",
+  "parameters": {
+    "type": "object",
+    "required": [],
+    "properties": {
+      "mode": {
+        "type": "string",
+        "enum": ["full", "update", "calibrate"],
+        "default": "full"
+      },
+      "projectName": { "type": "string" },
+      "industry": { "type": "string" },
+      "accountPositioning": { "type": "string" },
+      "targetAudience": { "type": "string" },
+      "conversionGoal": { "type": "string" },
+      "coreOffer": { "type": "string" },
+      "contentPillars": { "type": "array", "items": { "type": "string" } },
+      "keywords": { "type": "array", "items": { "type": "string" } },
+      "accountMonitorMode": {
+        "type": "string",
+        "enum": ["specific_accounts", "discovery_direction"],
+        "description": "账号监听入口:具体账号精确监听,或按方向先发现候选账号。"
+      },
+      "referenceAccounts": { "type": "array", "items": { "type": "string" } },
+      "accountDiscoveryDirection": { "type": "string" },
+      "candidateAccounts": { "type": "array", "items": { "type": "object" } },
+      "confirmedAccounts": { "type": "array", "items": { "type": "object" } },
+      "forbiddenTopics": { "type": "array", "items": { "type": "string" } },
+      "tone": { "type": "string" },
+      "dailyOutputCount": { "type": "integer", "default": 8 },
+      "calibrationNotes": { "type": "array", "items": { "type": "string" } }
+    }
+  },
+  "output": {
+    "type": "object",
+    "properties": {
+      "douyinSpeakingProfile": { "type": "object" },
+      "missingFields": { "type": "array", "items": { "type": "string" } },
+      "readyForDailyReport": { "type": "boolean" }
+    }
+  },
+  "memoryFiles": {
+    "read": ["memory/douyin-speaking-profile.json"],
+    "write": ["memory/douyin-speaking-profile.json"]
+  },
+  "relatedSkills": [
+    "douyin-speaking-profile-memory",
+    "douyin-speaking-keyword-monitor",
+    "douyin-speaking-account-monitor",
+    "douyin-speaking-daily-report"
+  ]
+}

+ 69 - 0
douyin/douyin-speaking-profile-memory/SKILL.md

@@ -0,0 +1,69 @@
+---
+name: douyin-speaking-profile-memory
+description: 抖音口播日报 profile/memory 写回技能,用于候选账号确认、confirmedAccounts 写入、日报状态推进和校准反馈沉淀。
+version: 0.1.0
+author: nkkj-BrainHack
+---
+
+# 抖音口播日报 Profile Memory
+
+## 技能目的
+
+这是 `douyin-speaking-daily` P0 闭环的记忆写回入口。它不采集抖音数据,只负责把用户确认和日报反馈写入 `memory/douyin-speaking-profile.json`,让后续日报可以继承账号池、禁区、关键词和校准结论。
+
+## 使用场景
+
+- 首次建档后,把行业、账号定位、目标人群、关键词等写入 profile。
+- `douyin-speaking-daily-runner` 返回 `needs_account_confirmation` 后,把 `account-candidates.json` 记录到 `candidateAccounts`。
+- 用户确认候选账号序号后,把账号写入 `confirmedAccounts`,状态推进到 `readyForManualRun`。
+- 每次日报完成后,把 `lastReportAt`、`lastRunStatus`、`lastRunSummary` 写回 profile。
+- 用户给出“像/不像/不要/新增”反馈后,写入 `calibrationNotes`,并更新关键词、禁区和账号池。
+
+## 常用命令
+
+初始化或更新 profile:
+```text
+node scripts/tools/douyin-speaking-profile-memory.js --mode update --output memory/douyin-speaking-profile.json --project live-p0 --industry AI创业 --account-positioning 老板IP --target-audience 中小创业者 --conversion-goal 获客 --keywords AI工具,AI创业
+```
+
+记录候选账号:
+```text
+node scripts/tools/douyin-speaking-profile-memory.js --mode record-candidates --profile memory/douyin-speaking-profile.json --candidates outputs/run/account-candidates.json
+```
+
+确认账号池:
+```text
+node scripts/tools/douyin-speaking-profile-memory.js --mode confirm-accounts --profile memory/douyin-speaking-profile.json --candidates outputs/run/account-candidates.json --select 1,2,4
+```
+
+日报后校准:
+```text
+node scripts/tools/douyin-speaking-profile-memory.js --mode calibrate --profile memory/douyin-speaking-profile.json --liked-topics 1,3 --forbidden-add 情绪鸡汤 --keywords-add AI获客
+```
+
+## 状态机
+
+```text
+emptyProfile
+  -> collectingProfile
+  -> awaitingProfileConfirm
+  -> readyForManualRun
+  -> awaitingAccountConfirmation
+  -> awaitingReportCalibration
+  -> tunedProfile
+  -> automationReady
+```
+
+## 输出
+
+- `profilePath`
+- `profileState`
+- `readyForDailyReport`
+- `automationReady`
+- `summary`
+- `profile`
+
+## 下游
+
+- `douyin-speaking-daily-runner`
+- `douyin-speaking-daily-report`

+ 150 - 0
douyin/douyin-speaking-profile-memory/api-config.json

@@ -0,0 +1,150 @@
+{
+  "name": "douyin-speaking-profile-memory",
+  "displayName": "抖音口播日报 Profile Memory",
+  "description": "P0 记忆写回入口:初始化/更新 profile、记录候选账号、确认账号池、写入日报校准反馈。",
+  "category": "douyin",
+  "version": "0.1.0",
+  "type": "local-script",
+  "execution": {
+    "kind": "command",
+    "command": "node",
+    "args": [
+      "scripts/tools/douyin-speaking-profile-memory.js",
+      "--result-prefix",
+      "DOUYIN_SPEAKING_PROFILE_MEMORY_RESULT",
+      "--mode",
+      "{{mode}}",
+      "--profile",
+      "{{profile}}",
+      "--output",
+      "{{output}}",
+      "--project",
+      "{{project}}",
+      "--industry",
+      "{{industry}}",
+      "--account-positioning",
+      "{{accountPositioning}}",
+      "--target-audience",
+      "{{targetAudience}}",
+      "--conversion-goal",
+      "{{conversionGoal}}",
+      "--core-offer",
+      "{{coreOffer}}",
+      "--content-pillars",
+      "{{contentPillars}}",
+      "--keywords",
+      "{{keywords}}",
+      "--reference-accounts",
+      "{{referenceAccounts}}",
+      "--account-discovery-direction",
+      "{{accountDiscoveryDirection}}",
+      "--candidates",
+      "{{candidates}}",
+      "--select",
+      "{{select}}",
+      "--confirmed-accounts",
+      "{{confirmedAccounts}}",
+      "--liked-topics",
+      "{{likedTopics}}",
+      "--rejected-topics",
+      "{{rejectedTopics}}",
+      "--forbidden-add",
+      "{{forbiddenAdd}}",
+      "--keywords-add",
+      "{{keywordsAdd}}",
+      "--keywords-remove",
+      "{{keywordsRemove}}",
+      "--accounts-add",
+      "{{accountsAdd}}",
+      "--accounts-remove",
+      "{{accountsRemove}}",
+      "--tone",
+      "{{tone}}",
+      "--feedback",
+      "{{feedback}}",
+      "--report",
+      "{{report}}",
+      "--result",
+      "{{result}}",
+      "--automation-ready",
+      "{{automationReady}}"
+    ],
+    "cwd": "{{workspaceRoot}}",
+    "outputParser": {
+      "type": "stdout-prefix-json",
+      "prefix": "DOUYIN_SPEAKING_PROFILE_MEMORY_RESULT="
+    },
+    "exitCodes": {
+      "0": "ok",
+      "1": "error"
+    }
+  },
+  "parameters": {
+    "type": "object",
+    "required": ["mode"],
+    "properties": {
+      "mode": {
+        "type": "string",
+        "enum": ["update", "record-candidates", "confirm-accounts", "run-complete", "calibrate", "summarize"],
+        "default": "summarize"
+      },
+      "profile": { "type": "string" },
+      "output": { "type": "string" },
+      "project": { "type": "string" },
+      "industry": { "type": "string" },
+      "accountPositioning": { "type": "string" },
+      "targetAudience": { "type": "string" },
+      "conversionGoal": { "type": "string" },
+      "coreOffer": { "type": "string" },
+      "contentPillars": { "type": "array", "items": { "type": "string" } },
+      "keywords": { "type": "array", "items": { "type": "string" } },
+      "referenceAccounts": { "type": "array", "items": { "type": "string" } },
+      "accountDiscoveryDirection": { "type": "string" },
+      "candidates": { "type": "string", "description": "account-candidates.json 路径或 JSON 字符串" },
+      "select": { "type": "string", "description": "要确认的候选账号序号,如 1,2,4;也支持 all" },
+      "confirmedAccounts": { "type": "string", "description": "已确认账号 JSON 字符串或文件路径" },
+      "likedTopics": { "type": "array", "items": { "type": "string" } },
+      "rejectedTopics": { "type": "array", "items": { "type": "string" } },
+      "forbiddenAdd": { "type": "array", "items": { "type": "string" } },
+      "keywordsAdd": { "type": "array", "items": { "type": "string" } },
+      "keywordsRemove": { "type": "array", "items": { "type": "string" } },
+      "accountsAdd": { "type": "string" },
+      "accountsRemove": { "type": "array", "items": { "type": "string" } },
+      "tone": { "type": "string" },
+      "feedback": { "type": "string" },
+      "report": { "type": "string" },
+      "result": { "type": "string" },
+      "automationReady": { "type": "boolean", "default": false }
+    }
+  },
+  "output": {
+    "type": "object",
+    "properties": {
+      "status": { "type": "string" },
+      "mode": { "type": "string" },
+      "profilePath": { "type": "string" },
+      "profileState": { "type": "string" },
+      "readyForDailyReport": { "type": "boolean" },
+      "automationReady": { "type": "boolean" },
+      "changed": { "type": "array" },
+      "selectedAccounts": { "type": "array" },
+      "summary": { "type": "object" },
+      "profile": { "type": "object" }
+    }
+  },
+  "memoryFiles": {
+    "read": ["memory/douyin-speaking-profile.json"],
+    "write": ["memory/douyin-speaking-profile.json"]
+  },
+  "deployment": {
+    "toolInstallPaths": [
+      "~/.openclaw/tools/douyin-speaking-profile-memory.js",
+      "~/.openclaw/workspace/scripts/tools/douyin-speaking-profile-memory.js"
+    ]
+  },
+  "relatedSkills": [
+    "douyin-speaking-profile-builder",
+    "douyin-speaking-daily-runner",
+    "douyin-speaking-daily-report"
+  ]
+}

+ 36 - 0
douyin/douyin-user-posts/SKILL.md

@@ -0,0 +1,36 @@
+---
+name: douyin-user-posts
+description: 根据 sec_user_id 获取抖音账号近期作品列表,用于对标账号监听、达人内容追踪和口播爆款拆解。
+version: 0.1.0
+author: nkkj-BrainHack
+---
+
+# 抖音账号近期作品列表
+
+## 技能目的
+
+按 `sec_user_id` 拉取指定抖音账号近期发布的作品,作为 `douyin-speaking-account-monitor` 的底层能力。
+
+## 输入
+
+| 参数 | 类型 | 必填 | 默认 | 说明 |
+|---|---|---|---|---|
+| sec_user_id | string | 是 |  | 抖音账号 sec_user_id;用户搜索返回的 `user_id` 实测也可作为该参数 |
+| max_cursor | integer | 否 | 0 | 翻页游标 |
+| count | integer | 否 | 10 | 单页作品数量 |
+
+## 输出重点字段
+
+- `data.aweme_list[].aweme_id`
+- `data.aweme_list[].desc`
+- `data.aweme_list[].author`
+- `data.aweme_list[].statistics`
+- `data.aweme_list[].create_time`
+- `data.has_more`
+- `data.max_cursor`
+
+## 使用边界
+
+- 该接口基于探针中的 `fetch_user_post_videos` 线索封装,已用 `douyin-user-search` 返回的 `user_id` 做过网关连通性验证。
+- 如果接口不可用,账号监听必须 Hard Stop,不能用用户搜索结果假装“近期作品监听”。
+- 若用户只提供昵称,应先用 `douyin-user-search` 找到候选账号并让用户确认,再调用本技能。

+ 92 - 0
douyin/douyin-user-posts/api-config.json

@@ -0,0 +1,92 @@
+{
+  "name": "douyin-user-posts",
+  "displayName": "抖音账号近期作品列表",
+  "description": "根据 sec_user_id 获取抖音账号近期发布作品,用于对标账号监听和口播爆款内容拆解。已验证 user_id/sec_user_id 可返回近期作品。",
+  "category": "douyin",
+  "version": "0.1.0",
+  "endpoint": {
+    "method": "GET",
+    "url": "https://server.fmode.cn/api/voc-social/douyin/app/v3/fetch_user_post_videos",
+    "headers": {
+      "Authorization": "Bearer ${vocToken}",
+      "Accept": "application/json"
+    }
+  },
+  "parameters": {
+    "type": "object",
+    "required": ["sec_user_id"],
+    "properties": {
+      "sec_user_id": {
+        "type": "string",
+        "description": "抖音账号 sec_user_id"
+      },
+      "max_cursor": {
+        "type": "integer",
+        "default": 0,
+        "description": "翻页游标,首次请求传 0"
+      },
+      "count": {
+        "type": "integer",
+        "default": 10,
+        "description": "单页作品数量"
+      }
+    }
+  },
+  "requestTransform": {
+    "queryParams": {
+      "sec_user_id": "{{sec_user_id}}",
+      "max_cursor": "{{max_cursor}}",
+      "count": "{{count}}"
+    }
+  },
+  "response": {
+    "type": "object",
+    "description": "账号近期作品列表(顶层 {code, data},作品通常在 data.aweme_list 内)",
+    "properties": {
+      "code": { "type": "integer" },
+      "data": {
+        "type": "object",
+        "properties": {
+          "aweme_list": { "type": "array" },
+          "has_more": { "type": "integer" },
+          "max_cursor": { "type": "integer" }
+        }
+      }
+    },
+    "keyFields": {
+      "data.aweme_list[].aweme_id": "作品 ID",
+      "data.aweme_list[].desc": "作品描述",
+      "data.aweme_list[].author.sec_uid": "作者 sec_user_id",
+      "data.aweme_list[].author.nickname": "作者昵称",
+      "data.aweme_list[].statistics.digg_count": "点赞数",
+      "data.aweme_list[].statistics.comment_count": "评论数",
+      "data.aweme_list[].statistics.share_count": "分享数",
+      "data.aweme_list[].statistics.play_count": "播放数",
+      "data.aweme_list[].create_time": "发布时间"
+    }
+  },
+  "workflow": {
+    "description": "推荐调用链",
+    "steps": [
+      "上游: 用户提供 sec_user_id,或 douyin-user-search(keyword) → 用户确认候选账号 → 取 user_id/sec_uid",
+      "本步: douyin-user-posts(sec_user_id) → 拉取账号近期作品",
+      "下游: douyin-video-detail / douyin-video-comments / douyin-comment-replies → 内容拆解与日报"
+    ]
+  },
+  "timeout": 60000,
+  "retry": { "maxAttempts": 2, "delay": 1500, "backoffMultiplier": 2 },
+  "tokenConfig": {
+    "type": "bearer",
+    "configFile": "~/.openclaw/voc-credentials.json",
+    "tokenField": "vocToken",
+    "resolutionOrder": ["configFile"],
+    "apigId": "Vo3ROWEvDy"
+  },
+  "verification": {
+    "status": "verified",
+    "source": "scripts/tools/probe-douyin-share.js uses TikHub /api/v1/douyin/app/v3/fetch_user_post_videos",
+    "lastChecked": "2026-05-08",
+    "note": "server.fmode.cn /api/voc-social/douyin/app/v3/fetch_user_post_videos with douyin-user-search user_id returned aweme_list successfully.",
+    "hardStopOnFailure": true
+  }
+}

+ 11 - 7
douyin/douyin-user-search/api-config.json

@@ -38,8 +38,8 @@
   },
   "response": {
     "type": "object",
-    "description": "用户搜索结果(双层包装:{code, data},用户列表在 data.data 内)",
-    "note": "顶层 {code:200, data:{code, data, extra, message}}。data.data 是用户列表数据,结构取决于 V2 接口返回。",
+    "description": "用户搜索结果(双层包装:{code, data},用户列表可能在 data.data[].user_list 内)",
+    "note": "顶层 {code:200, data:{code, data, extra, message}}。实测 V2 可能返回 data.data[0].user_list[],每项使用 user_id 作为后续 sec_user_id。",
     "properties": {
       "code": {
         "type": "integer",
@@ -55,7 +55,7 @@
           },
           "data": {
             "type": "array",
-            "description": "用户列表,每项含 user_info 对象(uid, nickname, unique_id, sec_uid, follower_count, signature 等)"
+            "description": "用户列表包装。常见结构为 data[0].user_list[],每项含 user_id、nick_name、fans_cnt、like_cnt、publish_cnt 等"
           },
           "extra": {
             "type": "object",
@@ -69,9 +69,13 @@
       }
     },
     "keyFields": {
-      "data.data[].user_info.uid": "用户 ID",
-      "data.data[].user_info.nickname": "用户昵称",
-      "data.data[].user_info.sec_uid": "可传入 douyin-user-profile 获取详情",
+      "data.data[].user_list[].user_id": "实测可作为 sec_user_id,传入 douyin-user-profile / douyin-user-posts",
+      "data.data[].user_list[].nick_name": "用户昵称",
+      "data.data[].user_list[].fans_cnt": "粉丝数",
+      "data.data[].user_list[].publish_cnt": "作品数",
+      "data.data[].user_info.uid": "旧结构用户 ID",
+      "data.data[].user_info.nickname": "旧结构用户昵称",
+      "data.data[].user_info.sec_uid": "旧结构可传入 douyin-user-profile 获取详情",
       "data.data[].user_info.unique_id": "用户抵音号",
       "data.data[].user_info.follower_count": "粉丝数",
       "data.data[].user_info.signature": "个人简介"
@@ -98,7 +102,7 @@
   "workflow": {
     "description": "推荐调用链",
     "steps": [
-      "本步: douyin-user-search(keyword) → 搜索达人/品牌 → 得到 uid、nickname、sec_uid、follower_count",
+      "本步: douyin-user-search(keyword) → 搜索达人/品牌 → 得到 user_id/nick_name/fans_cnt 或旧结构 sec_uid",
       "下游: douyin-user-profile(sec_user_id) → 获取账号完整数据和统计",
       "筛选: 按 follower_count 筛选符合粉丝规模要求的达人"
     ]

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini