Просмотр исходного кода

Add VOC business workflow and report quality gates

gangvy 3 месяцев назад
Родитель
Сommit
c31f710f5b
100 измененных файлов с 12366 добавлено и 787 удалено
  1. 7 0
      .claude/settings.json
  2. 1 0
      .playwright-cli/console-2026-05-25T04-08-37-573Z.log
  3. 575 0
      .playwright-cli/page-2026-05-25T04-08-37-800Z.yml
  4. BIN
      .playwright-cli/page-2026-05-25T04-11-40-815Z.png
  5. BIN
      .playwright-cli/page-2026-05-25T04-12-29-139Z.png
  6. BIN
      .playwright-cli/page-2026-05-25T04-12-56-823Z.png
  7. 575 0
      .playwright-cli/page-2026-05-25T04-13-46-837Z.yml
  8. 134 0
      NPM技能包发布管理.md
  9. 8 0
      claude-code-measurement-marker-check/.claude-plugin/plugin.json
  10. 74 0
      claude-code-measurement-marker-check/README.md
  11. 334 0
      claude-code-measurement-marker-check/bin/claude-measure-marker.js
  12. 23 0
      claude-code-measurement-marker-check/package.json
  13. 49 0
      claude-code-measurement-marker-check/scripts/smoke.js
  14. 13 0
      claude-code-measurement-marker-check/skill-package-manifest.json
  15. 125 0
      claude-code-measurement-marker-check/skills/measurement-marker-check/SKILL.md
  16. 43 0
      claude-code-measurement-marker-check/skills/measurement-marker-check/references/fmode-vision-reference.md
  17. 32 0
      claude-code-measurement-marker-check/test-fixtures/chain-test-input.json
  18. 414 0
      claude-code-measurement-marker-check/tools/measurement-marker-check-run.js
  19. 2 2
      claude-code-voc-intelligence/.claude-plugin/plugin.json
  20. 1 0
      claude-code-voc-intelligence/.gitignore
  21. 4 4
      claude-code-voc-intelligence/.mcp.json
  22. 267 82
      claude-code-voc-intelligence/README.md
  23. 598 32
      claude-code-voc-intelligence/bin/claude-voc.js
  24. 194 0
      claude-code-voc-intelligence/docs/capability-map.md
  25. 491 0
      claude-code-voc-intelligence/docs/context-budget-implementation-plan.md
  26. 25 61
      claude-code-voc-intelligence/docs/customer-quickstart.md
  27. 43 0
      claude-code-voc-intelligence/docs/deliverables/competitor-map-report.md
  28. 45 0
      claude-code-voc-intelligence/docs/deliverables/seven-day-content-plan.md
  29. 49 0
      claude-code-voc-intelligence/docs/deliverables/single-problem-action-plan.md
  30. 41 0
      claude-code-voc-intelligence/docs/deliverables/trend-insight-report.md
  31. 42 0
      claude-code-voc-intelligence/docs/deliverables/voc-issue-pool-report.md
  32. 19 64
      claude-code-voc-intelligence/docs/demo-runbook.md
  33. 50 0
      claude-code-voc-intelligence/docs/live-manual-acceptance-checklist.md
  34. 86 0
      claude-code-voc-intelligence/docs/payment-package-links.md
  35. 59 31
      claude-code-voc-intelligence/install.js
  36. 55 6
      claude-code-voc-intelligence/mcp/src/core/credentials.js
  37. 106 0
      claude-code-voc-intelligence/mcp/src/core/evidence-quality.js
  38. 117 0
      claude-code-voc-intelligence/mcp/src/core/payment-links.js
  39. 259 0
      claude-code-voc-intelligence/mcp/src/features/douyin-trend/live-collector.js
  40. 130 0
      claude-code-voc-intelligence/mcp/src/features/douyin-trend/preference-memory.js
  41. 318 0
      claude-code-voc-intelligence/mcp/src/features/douyin-trend/report.js
  42. 103 0
      claude-code-voc-intelligence/mcp/src/features/douyin-trend/sample-data.js
  43. 79 0
      claude-code-voc-intelligence/mcp/src/features/douyin-trend/signal-extractor.js
  44. 1 0
      claude-code-voc-intelligence/mcp/src/features/douyin-trend/topic-builder.js
  45. 214 0
      claude-code-voc-intelligence/mcp/src/features/voc-business-workflow/business-workflow.js
  46. 198 0
      claude-code-voc-intelligence/mcp/src/features/voc-competitor-map/competitor-map.js
  47. 206 0
      claude-code-voc-intelligence/mcp/src/features/voc-content-plan/content-plan.js
  48. 391 0
      claude-code-voc-intelligence/mcp/src/features/voc-issue-pool/issue-pool.js
  49. 662 0
      claude-code-voc-intelligence/mcp/src/features/voc-problem-deep-dive/deep-dive.js
  50. 293 0
      claude-code-voc-intelligence/mcp/src/features/voc-speaking-script/speaking-script.js
  51. 156 0
      claude-code-voc-intelligence/mcp/src/features/xiaohongshu-trend/assets.js
  52. 335 0
      claude-code-voc-intelligence/mcp/src/features/xiaohongshu-trend/industry-presets.js
  53. 129 7
      claude-code-voc-intelligence/mcp/src/features/xiaohongshu-trend/live-collector.js
  54. 1 1
      claude-code-voc-intelligence/mcp/src/features/xiaohongshu-trend/preference-memory.js
  55. 125 101
      claude-code-voc-intelligence/mcp/src/features/xiaohongshu-trend/report.js
  56. 70 70
      claude-code-voc-intelligence/mcp/src/features/xiaohongshu-trend/sample-data.js
  57. 77 0
      claude-code-voc-intelligence/mcp/src/features/xiaohongshu-trend/signal-extractor.js
  58. 84 0
      claude-code-voc-intelligence/mcp/src/features/xiaohongshu-trend/topic-builder.js
  59. 140 0
      claude-code-voc-intelligence/mcp/src/providers/douyin-api.js
  60. 22 6
      claude-code-voc-intelligence/mcp/src/providers/xiaohongshu-api.js
  61. 435 10
      claude-code-voc-intelligence/mcp/src/server.js
  62. 70 0
      claude-code-voc-intelligence/mcp/src/tools/douyin-preference-update.js
  63. 354 0
      claude-code-voc-intelligence/mcp/src/tools/douyin-trend-run.js
  64. 52 0
      claude-code-voc-intelligence/mcp/src/tools/voc-business-workflow-run.js
  65. 59 0
      claude-code-voc-intelligence/mcp/src/tools/voc-competitor-map-run.js
  66. 59 0
      claude-code-voc-intelligence/mcp/src/tools/voc-content-plan-run.js
  67. 59 0
      claude-code-voc-intelligence/mcp/src/tools/voc-issue-pool-run.js
  68. 59 0
      claude-code-voc-intelligence/mcp/src/tools/voc-problem-deep-dive-run.js
  69. 59 0
      claude-code-voc-intelligence/mcp/src/tools/voc-speaking-script-run.js
  70. 193 45
      claude-code-voc-intelligence/mcp/src/tools/xiaohongshu-trend-run.js
  71. 33 0
      claude-code-voc-intelligence/memory-templates/douyin-trend-profile.json
  72. 18 22
      claude-code-voc-intelligence/memory-templates/xiaohongshu-trend-profile.json
  73. 2 2
      claude-code-voc-intelligence/package-lock.json
  74. 27 4
      claude-code-voc-intelligence/package.json
  75. 126 0
      claude-code-voc-intelligence/scripts/smoke-cross-industry.js
  76. 194 3
      claude-code-voc-intelligence/scripts/smoke-mcp.js
  77. 651 4
      claude-code-voc-intelligence/scripts/smoke-package.js
  78. 44 6
      claude-code-voc-intelligence/skill-package-manifest.json
  79. 61 0
      claude-code-voc-intelligence/skills/douyin-trend-intelligence/SKILL.md
  80. 27 0
      claude-code-voc-intelligence/skills/douyin-trend-intelligence/references/live-mode.md
  81. 32 0
      claude-code-voc-intelligence/skills/douyin-trend-intelligence/references/output-format.md
  82. 24 0
      claude-code-voc-intelligence/skills/douyin-trend-intelligence/references/user-workflow.md
  83. 61 0
      claude-code-voc-intelligence/skills/voc-business-workflow/SKILL.md
  84. 60 0
      claude-code-voc-intelligence/skills/voc-competitor-map/SKILL.md
  85. 25 0
      claude-code-voc-intelligence/skills/voc-competitor-map/references/restaurant-competitor-framework.md
  86. 44 0
      claude-code-voc-intelligence/skills/voc-competitor-map/references/restaurant-differentiation-patterns.md
  87. 56 0
      claude-code-voc-intelligence/skills/voc-content-plan/SKILL.md
  88. 41 0
      claude-code-voc-intelligence/skills/voc-content-plan/references/restaurant-content-framework.md
  89. 34 0
      claude-code-voc-intelligence/skills/voc-content-plan/references/restaurant-speaking-script-patterns.md
  90. 74 0
      claude-code-voc-intelligence/skills/voc-issue-pool/SKILL.md
  91. 70 0
      claude-code-voc-intelligence/skills/voc-problem-deep-dive/SKILL.md
  92. 99 0
      claude-code-voc-intelligence/skills/voc-speaking-script/SKILL.md
  93. 32 60
      claude-code-voc-intelligence/skills/xiaohongshu-trend-intelligence/SKILL.md
  94. 43 41
      claude-code-voc-intelligence/skills/xiaohongshu-trend-intelligence/references/live-mode.md
  95. 30 66
      claude-code-voc-intelligence/skills/xiaohongshu-trend-intelligence/references/output-format.md
  96. 38 43
      claude-code-voc-intelligence/skills/xiaohongshu-trend-intelligence/references/user-workflow.md
  97. 25 7
      dist/claude-code-voc-intelligence-suite-manifest.json
  98. BIN
      dist/claude-code-voc-intelligence.zip
  99. 9 7
      dist/npm/claude-code-voc-npm-package-manifest.json
  100. 293 0
      docs/5.24/task/20260524VOC首次课程培训问题记录-解析报告.md

+ 7 - 0
.claude/settings.json

@@ -0,0 +1,7 @@
+{
+  "permissions": {
+    "allow": [
+      "Bash(node \"e:/workspace/openclaw-voc-skill/claude-code-voc-intelligence/mcp/src/tools/xiaohongshu-trend-run.js\" --collection-mode live --profile \"e:/workspace/openclaw-voc-skill/claude-code-voc-intelligence/memory-templates/xiaohongshu-trend-profile.json\" --output \"outputs/claude-code-xhs-live\" --keyword-limit 1 --notes-per-keyword 1 --max-comment-pages 1 --assistant-message-only)"
+    ]
+  }
+}

+ 1 - 0
.playwright-cli/console-2026-05-25T04-08-37-573Z.log

@@ -0,0 +1 @@
+[     181ms] [ERROR] Failed to load resource: the server responded with a status of 404 (File not found) @ http://127.0.0.1:8123/favicon.ico:0

+ 575 - 0
.playwright-cli/page-2026-05-25T04-08-37-800Z.yml

@@ -0,0 +1,575 @@
+- generic [ref=e2]:
+  - generic [ref=e4]:
+    - generic [ref=e5]:
+      - heading "玻璃明细数据报表" [level=1] [ref=e6]
+      - paragraph [ref=e7]: 以“料单编号聚合 + 厚度 SKU 归并 + 标签并行统计”为主线,整合来源表中的有效明细。 已优先合并以 LM- 开头的同编号产品,并把 超白钢化 这类厚度 SKU 统一到产品级别。
+      - generic [ref=e8]:
+        - generic [ref=e9]: 文件:玻璃明细.xlsx
+        - generic [ref=e10]: 有效明细 206 行
+        - generic [ref=e11]: 产品级 92 组
+        - generic [ref=e12]: 已剔除 11 条汇总/空白行
+    - generic [ref=e13]:
+      - generic [ref=e14]:
+        - generic [ref=e15]: 总数量
+        - strong [ref=e16]: 14,152
+      - generic [ref=e17]:
+        - generic [ref=e18]: 总面积
+        - strong [ref=e19]: 7,727.92 ㎡
+      - generic [ref=e20]:
+        - generic [ref=e21]: 总金额
+        - strong [ref=e22]: ¥1,704,644.06
+      - generic [ref=e23]:
+        - generic [ref=e24]: 来源表
+        - strong [ref=e25]: "10"
+  - generic [ref=e26]:
+    - generic [ref=e27]:
+      - heading "统计概览" [level=2] [ref=e28]
+      - paragraph [ref=e29]: 先看整体规模,再往下看产品级聚合、标签维度和来源表分布。
+    - generic [ref=e31]:
+      - generic [ref=e32]:
+        - generic [ref=e33]: 有效记录
+        - strong [ref=e34]: "206"
+        - emphasis [ref=e35]: 去掉总计/空白行后
+      - generic [ref=e36]:
+        - generic [ref=e37]: 产品级分组
+        - strong [ref=e38]: "92"
+        - emphasis [ref=e39]: 同编号、同家族归并
+      - generic [ref=e40]:
+        - generic [ref=e41]: 原始SKU数
+        - strong [ref=e42]: "126"
+        - emphasis [ref=e43]: 去重后的原始名称数
+      - generic [ref=e44]:
+        - generic [ref=e45]: 夹丝命中
+        - strong [ref=e46]: "68"
+        - emphasis [ref=e47]: 可与其他标签重叠
+      - generic [ref=e48]:
+        - generic [ref=e49]: 透明命中
+        - strong [ref=e50]: "29"
+        - emphasis [ref=e51]: 可与其他标签重叠
+      - generic [ref=e52]:
+        - generic [ref=e53]: 烤漆/艺术
+        - strong [ref=e54]: 34 / 86
+        - emphasis [ref=e55]: 双维并行统计
+  - generic [ref=e56]:
+    - generic [ref=e57]:
+      - heading "规则聚合重点" [level=2] [ref=e58]
+      - paragraph [ref=e59]: 以下两组是这次报表里最关键的归并逻辑,和你的统计规则一一对应。
+    - generic [ref=e61]:
+      - generic [ref=e62]:
+        - generic [ref=e63]:
+          - generic [ref=e64]:
+            - generic [ref=e65]: LM 编号聚合
+            - generic [ref=e66]: LM-012
+          - generic [ref=e67]: 3 个 SKU
+        - generic [ref=e68]:
+          - generic [ref=e69]:
+            - generic [ref=e70]: 数量
+            - strong [ref=e71]: "52"
+          - generic [ref=e72]:
+            - generic [ref=e73]: 面积
+            - strong [ref=e74]: 61.76 ㎡
+          - generic [ref=e75]:
+            - generic [ref=e76]: 金额
+            - strong [ref=e77]: ¥41,124.00
+          - generic [ref=e78]:
+            - generic [ref=e79]: 来源
+            - strong [ref=e80]: 吴江
+        - generic [ref=e81]:
+          - generic [ref=e82]: LM-012单面双绒+超白夹丝透明钢化(4+4mm) ×2
+          - generic [ref=e83]: LM-012单面双绒+超白透明夹丝玻璃钢化(4+4mm) ×1
+          - generic [ref=e84]: LM-012单面双绒+超白玉砂夹丝玻璃钢化(4+4mm) ×1
+      - generic [ref=e85]:
+        - generic [ref=e86]:
+          - generic [ref=e87]:
+            - generic [ref=e88]: 超白钢化家族
+            - generic [ref=e89]: 超白钢化
+          - generic [ref=e90]: 2 个 SKU
+        - generic [ref=e91]:
+          - generic [ref=e92]:
+            - generic [ref=e93]: 数量
+            - strong [ref=e94]: "213"
+          - generic [ref=e95]:
+            - generic [ref=e96]: 面积
+            - strong [ref=e97]: 125.34 ㎡
+          - generic [ref=e98]:
+            - generic [ref=e99]: 金额
+            - strong [ref=e100]: ¥28,421.00
+          - generic [ref=e101]:
+            - generic [ref=e102]: 来源
+            - strong [ref=e103]: 太仓云瑞
+        - generic [ref=e104]:
+          - generic [ref=e105]: 超白钢化 ×4
+          - generic [ref=e106]: 4mm超白钢化(单面夹丝7063) ×1
+  - generic [ref=e107]:
+    - generic [ref=e108]:
+      - heading "产品级别 Top 10" [level=2] [ref=e109]
+      - paragraph [ref=e110]: 按金额排序后的产品级统计,已把同编号和同家族 SKU 聚合到一条产品线。
+    - generic [ref=e112]:
+      - generic [ref=e113]:
+        - generic [ref=e115]:
+          - heading "产品金额排行" [level=3] [ref=e116]
+          - paragraph [ref=e117]: 看哪个产品级组合贡献了最多销售额。
+        - img "bar chart" [ref=e118]:
+          - generic [ref=e120]: ¥130,196.79
+          - generic [ref=e121]: ¥260,393.57
+          - generic [ref=e122]: ¥390,590.36
+          - generic [ref=e123]: ¥520,787.15
+          - generic [ref=e124]:
+            - generic [ref=e125]: 蓝星灰钢化
+            - generic [ref=e128]: ¥520,787.15
+          - generic [ref=e129]:
+            - generic [ref=e130]: 蓝星灰镀膜钢化
+            - generic [ref=e133]: ¥149,054.35
+          - generic [ref=e134]:
+            - generic [ref=e135]: 超白玻钢化
+            - generic [ref=e138]: ¥144,285.72
+          - generic [ref=e139]:
+            - generic [ref=e140]: 蓝星灰钢化烤漆
+            - generic [ref=e143]: ¥136,356.15
+          - generic [ref=e144]:
+            - generic [ref=e145]: LM-035
+            - generic [ref=e148]: ¥84,769.78
+          - generic [ref=e149]:
+            - generic [ref=e150]: 欧茶钢化
+            - generic [ref=e153]: ¥61,140.84
+          - generic [ref=e154]:
+            - generic [ref=e155]: LM-012
+            - generic [ref=e158]: ¥41,124.00
+          - generic [ref=e159]:
+            - generic [ref=e160]: 无铜镜
+            - generic [ref=e163]: ¥38,074.00
+          - generic [ref=e164]:
+            - generic [ref=e165]: 蓝星灰镀膜钢化烤漆
+            - generic [ref=e168]: ¥34,745.70
+          - generic [ref=e169]:
+            - generic [ref=e170]: LM-001
+            - generic [ref=e173]: ¥29,881.14
+      - generic [ref=e174]:
+        - generic [ref=e176]:
+          - heading "来源表金额排行" [level=3] [ref=e177]
+          - paragraph [ref=e178]: 不同 sheet 的金额贡献和有效行数分布。
+        - img "bar chart" [ref=e179]:
+          - generic [ref=e181]: ¥137,287.32
+          - generic [ref=e182]: ¥274,574.63
+          - generic [ref=e183]: ¥411,861.95
+          - generic [ref=e184]: ¥549,149.26
+          - generic [ref=e185]:
+            - generic [ref=e186]: 透明玻璃
+            - generic [ref=e189]: ¥549,149.26
+          - generic [ref=e190]:
+            - generic [ref=e191]: 太仓云瑞
+            - generic [ref=e194]: ¥399,567.35
+          - generic [ref=e195]:
+            - generic [ref=e196]: 夹丝玻璃
+            - generic [ref=e199]: ¥225,707.32
+          - generic [ref=e200]:
+            - generic [ref=e201]: 伟刚
+            - generic [ref=e204]: ¥140,874.79
+          - generic [ref=e205]:
+            - generic [ref=e206]: 晶博耀
+            - generic [ref=e209]: ¥118,594.28
+          - generic [ref=e210]:
+            - generic [ref=e211]: 联营
+            - generic [ref=e214]: ¥75,602.80
+          - generic [ref=e215]:
+            - generic [ref=e216]: 尚匡玻璃
+            - generic [ref=e219]: ¥72,935.81
+          - generic [ref=e220]:
+            - generic [ref=e221]: 吴江
+            - generic [ref=e224]: ¥61,532.00
+          - generic [ref=e225]:
+            - generic [ref=e226]: 艺术玻璃
+            - generic [ref=e229]: ¥42,027.33
+          - generic [ref=e230]:
+            - generic [ref=e231]: 烤漆玻璃
+            - generic [ref=e234]: ¥18,653.12
+  - generic [ref=e235]:
+    - generic [ref=e236]:
+      - heading "标签维度" [level=2] [ref=e237]
+      - paragraph [ref=e238]: 四个维度按名称关键词和专类 sheet 共同归类,标签之间允许重叠,因此合计不会等于总量。
+    - generic [ref=e239]:
+      - generic [ref=e240]:
+        - generic [ref=e242]:
+          - heading "四大标签金额对比" [level=3] [ref=e243]
+          - paragraph [ref=e244]: 从多个来源里单独看夹丝、透明、烤漆、艺术玻璃。
+        - img "bar chart" [ref=e245]:
+          - generic [ref=e247]: ¥154,521.07
+          - generic [ref=e248]: ¥309,042.13
+          - generic [ref=e249]: ¥463,563.20
+          - generic [ref=e250]: ¥618,084.26
+          - generic [ref=e251]:
+            - generic [ref=e252]: 夹丝
+            - generic [ref=e255]: ¥432,872.65
+          - generic [ref=e256]:
+            - generic [ref=e257]: 透明
+            - generic [ref=e260]: ¥618,084.26
+          - generic [ref=e261]:
+            - generic [ref=e262]: 烤漆
+            - generic [ref=e265]: ¥243,889.34
+          - generic [ref=e266]:
+            - generic [ref=e267]: 艺术玻璃
+            - generic [ref=e270]: ¥237,040.29
+      - generic [ref=e271]:
+        - generic [ref=e272]:
+          - generic [ref=e273]:
+            - generic [ref=e274]:
+              - generic [ref=e275]: 夹丝
+              - generic [ref=e276]: 68 行 · 698 件 · ¥432,872.65
+            - generic [ref=e277]: 25.4%
+          - generic [ref=e278]:
+            - generic [ref=e279]:
+              - generic [ref=e280]: 超白玻钢化
+              - generic [ref=e283]: ¥144,285.72
+            - generic [ref=e284]:
+              - generic [ref=e285]: LM-035
+              - generic [ref=e288]: ¥84,769.78
+            - generic [ref=e289]:
+              - generic [ref=e290]: LM-012
+              - generic [ref=e293]: ¥41,124.00
+            - generic [ref=e294]:
+              - generic [ref=e295]: LM-001
+              - generic [ref=e298]: ¥29,881.14
+            - generic [ref=e299]:
+              - generic [ref=e300]: LM-002
+              - generic [ref=e303]: ¥24,856.00
+        - generic [ref=e304]:
+          - generic [ref=e305]:
+            - generic [ref=e306]:
+              - generic [ref=e307]: 透明
+              - generic [ref=e308]: 29 行 · 3,538 件 · ¥618,084.26
+            - generic [ref=e309]: 36.3%
+          - generic [ref=e310]:
+            - generic [ref=e311]:
+              - generic [ref=e312]: 蓝星灰钢化
+              - generic [ref=e315]: ¥328,571.65
+            - generic [ref=e316]:
+              - generic [ref=e317]: 蓝星灰镀膜钢化
+              - generic [ref=e320]: ¥98,889.55
+            - generic [ref=e321]:
+              - generic [ref=e322]: 欧茶钢化
+              - generic [ref=e325]: ¥55,805.99
+            - generic [ref=e326]:
+              - generic [ref=e327]: LM-012
+              - generic [ref=e330]: ¥39,594.00
+            - generic [ref=e331]:
+              - generic [ref=e332]: LM-031
+              - generic [ref=e335]: ¥23,858.00
+        - generic [ref=e336]:
+          - generic [ref=e337]:
+            - generic [ref=e338]:
+              - generic [ref=e339]: 烤漆
+              - generic [ref=e340]: 34 行 · 3,599 件 · ¥243,889.34
+            - generic [ref=e341]: 14.3%
+          - generic [ref=e342]:
+            - generic [ref=e343]:
+              - generic [ref=e344]: 蓝星灰钢化烤漆
+              - generic [ref=e347]: ¥136,356.15
+            - generic [ref=e348]:
+              - generic [ref=e349]: 蓝星灰镀膜钢化烤漆
+              - generic [ref=e352]: ¥34,745.70
+            - generic [ref=e353]:
+              - generic [ref=e354]: 欧茶钢化烤漆
+              - generic [ref=e357]: ¥24,308.10
+            - generic [ref=e358]:
+              - generic [ref=e359]: 超白钢化烤漆
+              - generic [ref=e362]: ¥8,704.35
+            - generic [ref=e363]:
+              - generic [ref=e364]: 超白一号平四边
+              - generic [ref=e367]: ¥7,698.78
+        - generic [ref=e368]:
+          - generic [ref=e369]:
+            - generic [ref=e370]:
+              - generic [ref=e371]: 艺术玻璃
+              - generic [ref=e372]: 86 行 · 2,134 件 · ¥237,040.29
+            - generic [ref=e373]: 13.9%
+          - generic [ref=e374]:
+            - generic [ref=e375]:
+              - generic [ref=e376]: 欧洲茶玻平四边钢化
+              - generic [ref=e379]: ¥24,556.24
+            - generic [ref=e380]:
+              - generic [ref=e381]: 超白长虹玉砂玻璃钢化
+              - generic [ref=e384]: ¥18,064.56
+            - generic [ref=e385]:
+              - generic [ref=e386]: 5+5热弯夹丝玻璃
+              - generic [ref=e389]: ¥15,399.26
+            - generic [ref=e390]:
+              - generic [ref=e391]: 超白一号平四边
+              - generic [ref=e394]: ¥15,397.56
+            - generic [ref=e395]:
+              - generic [ref=e396]: 蓝星灰切磨烤灰漆
+              - generic [ref=e399]: ¥14,200.00
+  - generic [ref=e400]:
+    - generic [ref=e401]:
+      - heading "产品级明细表" [level=2] [ref=e402]
+      - paragraph [ref=e403]: 按产品级统计前 15 条,便于回看每个聚合组对应了哪些原始 SKU。
+    - table [ref=e406]:
+      - rowgroup [ref=e407]:
+        - row "排名 产品级名称 原始SKU SKU数 明细行 数量 面积 金额 主来源" [ref=e408]:
+          - columnheader "排名" [ref=e409]
+          - columnheader "产品级名称" [ref=e410]
+          - columnheader "原始SKU" [ref=e411]
+          - columnheader "SKU数" [ref=e412]
+          - columnheader "明细行" [ref=e413]
+          - columnheader "数量" [ref=e414]
+          - columnheader "面积" [ref=e415]
+          - columnheader "金额" [ref=e416]
+          - columnheader "主来源" [ref=e417]
+      - rowgroup [ref=e418]:
+        - row "1 蓝星灰钢化 蓝星灰钢化 1 3 6,377 3,211.87 ㎡ ¥520,787.15 太仓云瑞" [ref=e419]:
+          - cell "1" [ref=e420]
+          - cell "蓝星灰钢化" [ref=e421]
+          - cell "蓝星灰钢化" [ref=e422]
+          - cell "1" [ref=e423]
+          - cell "3" [ref=e424]
+          - cell "6,377" [ref=e425]
+          - cell "3,211.87 ㎡" [ref=e426]
+          - cell "¥520,787.15" [ref=e427]
+          - cell "太仓云瑞" [ref=e428]
+        - row "2 蓝星灰镀膜钢化 蓝星灰镀膜钢化 1 2 526 418.04 ㎡ ¥149,054.35 太仓云瑞" [ref=e429]:
+          - cell "2" [ref=e430]
+          - cell "蓝星灰镀膜钢化" [ref=e431]
+          - cell "蓝星灰镀膜钢化" [ref=e432]
+          - cell "1" [ref=e433]
+          - cell "2" [ref=e434]
+          - cell "526" [ref=e435]
+          - cell "418.04 ㎡" [ref=e436]
+          - cell "¥149,054.35" [ref=e437]
+          - cell "太仓云瑞" [ref=e438]
+        - row "3 超白玻钢化 2+2mm超白玻钢化(夹丝002);2+2mm超白玻钢化(夹丝012);2+2mm超白玻钢化(夹丝013) 等 4 个SKU 4 8 328 319.06 ㎡ ¥144,285.72 晶博耀" [ref=e439]:
+          - cell "3" [ref=e440]
+          - cell "超白玻钢化" [ref=e441]
+          - cell "2+2mm超白玻钢化(夹丝002);2+2mm超白玻钢化(夹丝012);2+2mm超白玻钢化(夹丝013) 等 4 个SKU" [ref=e442]
+          - cell "4" [ref=e443]
+          - cell "8" [ref=e444]
+          - cell "328" [ref=e445]
+          - cell "319.06 ㎡" [ref=e446]
+          - cell "¥144,285.72" [ref=e447]
+          - cell "晶博耀" [ref=e448]
+        - row "4 蓝星灰钢化烤漆 蓝星灰钢化烤漆 1 1 2,371 872.95 ㎡ ¥136,356.15 太仓云瑞" [ref=e449]:
+          - cell "4" [ref=e450]
+          - cell "蓝星灰钢化烤漆" [ref=e451]
+          - cell "蓝星灰钢化烤漆" [ref=e452]
+          - cell "1" [ref=e453]
+          - cell "1" [ref=e454]
+          - cell "2,371" [ref=e455]
+          - cell "872.95 ㎡" [ref=e456]
+          - cell "¥136,356.15" [ref=e457]
+          - cell "太仓云瑞" [ref=e458]
+        - row "5 LM-035 LM-035夹丝8MM夹丝正面超白夹丝LM-035,背面贴白色不透光膜(此玻璃贴墙上,背面不透光);LM-035夹丝(2+2超白钢化)(四周香槟烤漆2·3公分);LM-035夹丝钢化4mm超白+4mm超白夹丝钢化 等 10 个SKU 10 10 50 87.15 ㎡ ¥84,769.78 尚匡玻璃" [ref=e459]:
+          - cell "5" [ref=e460]
+          - cell "LM-035" [ref=e461]
+          - cell "LM-035夹丝8MM夹丝正面超白夹丝LM-035,背面贴白色不透光膜(此玻璃贴墙上,背面不透光);LM-035夹丝(2+2超白钢化)(四周香槟烤漆2·3公分);LM-035夹丝钢化4mm超白+4mm超白夹丝钢化 等 10 个SKU" [ref=e462]
+          - cell "10" [ref=e463]
+          - cell "10" [ref=e464]
+          - cell "50" [ref=e465]
+          - cell "87.15 ㎡" [ref=e466]
+          - cell "¥84,769.78" [ref=e467]
+          - cell "尚匡玻璃" [ref=e468]
+        - row "6 欧茶钢化 欧茶钢化 1 3 348 167.83 ㎡ ¥61,140.84 太仓云瑞" [ref=e469]:
+          - cell "6" [ref=e470]
+          - cell "欧茶钢化" [ref=e471]
+          - cell "欧茶钢化" [ref=e472]
+          - cell "1" [ref=e473]
+          - cell "3" [ref=e474]
+          - cell "348" [ref=e475]
+          - cell "167.83 ㎡" [ref=e476]
+          - cell "¥61,140.84" [ref=e477]
+          - cell "太仓云瑞" [ref=e478]
+        - row "7 LM-012 LM-012单面双绒+超白夹丝透明钢化(4+4mm);LM-012单面双绒+超白透明夹丝玻璃钢化(4+4mm);LM-012单面双绒+超白玉砂夹丝玻璃钢化(4+4mm) 3 4 52 61.76 ㎡ ¥41,124.00 吴江" [ref=e479]:
+          - cell "7" [ref=e480]
+          - cell "LM-012" [ref=e481]
+          - cell "LM-012单面双绒+超白夹丝透明钢化(4+4mm);LM-012单面双绒+超白透明夹丝玻璃钢化(4+4mm);LM-012单面双绒+超白玉砂夹丝玻璃钢化(4+4mm)" [ref=e482]
+          - cell "3" [ref=e483]
+          - cell "4" [ref=e484]
+          - cell "52" [ref=e485]
+          - cell "61.76 ㎡" [ref=e486]
+          - cell "¥41,124.00" [ref=e487]
+          - cell "吴江" [ref=e488]
+        - row "8 无铜镜 无铜镜 1 2 492 368.54 ㎡ ¥38,074.00 太仓云瑞" [ref=e489]:
+          - cell "8" [ref=e490]
+          - cell "无铜镜" [ref=e491]
+          - cell "无铜镜" [ref=e492]
+          - cell "1" [ref=e493]
+          - cell "2" [ref=e494]
+          - cell "492" [ref=e495]
+          - cell "368.54 ㎡" [ref=e496]
+          - cell "¥38,074.00" [ref=e497]
+          - cell "太仓云瑞" [ref=e498]
+        - row "9 蓝星灰镀膜钢化烤漆 蓝星灰镀膜钢化烤漆 1 1 129 109.89 ㎡ ¥34,745.70 太仓云瑞" [ref=e499]:
+          - cell "9" [ref=e500]
+          - cell "蓝星灰镀膜钢化烤漆" [ref=e501]
+          - cell "蓝星灰镀膜钢化烤漆" [ref=e502]
+          - cell "1" [ref=e503]
+          - cell "1" [ref=e504]
+          - cell "129" [ref=e505]
+          - cell "109.89 ㎡" [ref=e506]
+          - cell "¥34,745.70" [ref=e507]
+          - cell "太仓云瑞" [ref=e508]
+        - row "10 LM-001 LM-001(4MM单层油砂钢化夹丝玻璃);LM-001超白双绒夹丝玻璃钢化4-4mm;LM-001超白双绒夹丝玻璃钢化-C646锁-下口1000 等 6 个SKU 6 6 49 58.76 ㎡ ¥29,881.14 尚匡玻璃" [ref=e509]:
+          - cell "10" [ref=e510]
+          - cell "LM-001" [ref=e511]
+          - cell "LM-001(4MM单层油砂钢化夹丝玻璃);LM-001超白双绒夹丝玻璃钢化4-4mm;LM-001超白双绒夹丝玻璃钢化-C646锁-下口1000 等 6 个SKU" [ref=e512]
+          - cell "6" [ref=e513]
+          - cell "6" [ref=e514]
+          - cell "49" [ref=e515]
+          - cell "58.76 ㎡" [ref=e516]
+          - cell "¥29,881.14" [ref=e517]
+          - cell "尚匡玻璃" [ref=e518]
+        - row "11 超白钢化 超白钢化;4mm超白钢化(单面夹丝7063) 2 5 213 125.34 ㎡ ¥28,421.00 太仓云瑞" [ref=e519]:
+          - cell "11" [ref=e520]
+          - cell "超白钢化" [ref=e521]
+          - cell "超白钢化;4mm超白钢化(单面夹丝7063)" [ref=e522]
+          - cell "2" [ref=e523]
+          - cell "5" [ref=e524]
+          - cell "213" [ref=e525]
+          - cell "125.34 ㎡" [ref=e526]
+          - cell "¥28,421.00" [ref=e527]
+          - cell "太仓云瑞" [ref=e528]
+        - row "12 LM-002 LM-002超白双绒夹丝玻璃钢化(4+4mm);LM-002(4+4MM)双绒+双绒夹丝透明钢化 2 3 27 48.29 ㎡ ¥24,856.00 吴江" [ref=e529]:
+          - cell "12" [ref=e530]
+          - cell "LM-002" [ref=e531]
+          - cell "LM-002超白双绒夹丝玻璃钢化(4+4mm);LM-002(4+4MM)双绒+双绒夹丝透明钢化" [ref=e532]
+          - cell "2" [ref=e533]
+          - cell "3" [ref=e534]
+          - cell "27" [ref=e535]
+          - cell "48.29 ㎡" [ref=e536]
+          - cell "¥24,856.00" [ref=e537]
+          - cell "吴江" [ref=e538]
+        - row "13 欧洲茶玻平四边钢化 欧洲茶玻平四边钢化 1 1 430 284.05 ㎡ ¥24,556.24 联营" [ref=e539]:
+          - cell "13" [ref=e540]
+          - cell "欧洲茶玻平四边钢化" [ref=e541]
+          - cell "欧洲茶玻平四边钢化" [ref=e542]
+          - cell "1" [ref=e543]
+          - cell "1" [ref=e544]
+          - cell "430" [ref=e545]
+          - cell "284.05 ㎡" [ref=e546]
+          - cell "¥24,556.24" [ref=e547]
+          - cell "联营" [ref=e548]
+        - row "14 欧茶钢化烤漆 欧茶钢化烤漆 1 1 233 124.78 ㎡ ¥24,308.10 太仓云瑞" [ref=e549]:
+          - cell "14" [ref=e550]
+          - cell "欧茶钢化烤漆" [ref=e551]
+          - cell "欧茶钢化烤漆" [ref=e552]
+          - cell "1" [ref=e553]
+          - cell "1" [ref=e554]
+          - cell "233" [ref=e555]
+          - cell "124.78 ㎡" [ref=e556]
+          - cell "¥24,308.10" [ref=e557]
+          - cell "太仓云瑞" [ref=e558]
+        - row "15 LM-031 LM-031超白双绒透明夹丝玻璃钢化4mm;LM-031超白双绒+超白透明夹丝玻璃钢化(4+4mm);LM-031超白双绒+超白透明夹丝钢化4+4mm 等 4 个SKU 4 5 37 39.75 ㎡ ¥23,858.00 吴江" [ref=e559]:
+          - cell "15" [ref=e560]
+          - cell "LM-031" [ref=e561]
+          - cell "LM-031超白双绒透明夹丝玻璃钢化4mm;LM-031超白双绒+超白透明夹丝玻璃钢化(4+4mm);LM-031超白双绒+超白透明夹丝钢化4+4mm 等 4 个SKU" [ref=e562]
+          - cell "4" [ref=e563]
+          - cell "5" [ref=e564]
+          - cell "37" [ref=e565]
+          - cell "39.75 ㎡" [ref=e566]
+          - cell "¥23,858.00" [ref=e567]
+          - cell "吴江" [ref=e568]
+  - generic [ref=e569]:
+    - generic [ref=e570]:
+      - heading "来源表明细" [level=2] [ref=e571]
+      - paragraph [ref=e572]: 用于看各 sheet 的有效行、数量、面积和金额总览。
+    - table [ref=e575]:
+      - rowgroup [ref=e576]:
+        - row "排名 来源表 有效行 数量 面积 金额" [ref=e577]:
+          - columnheader "排名" [ref=e578]
+          - columnheader "来源表" [ref=e579]
+          - columnheader "有效行" [ref=e580]
+          - columnheader "数量" [ref=e581]
+          - columnheader "面积" [ref=e582]
+          - columnheader "金额" [ref=e583]
+      - rowgroup [ref=e584]:
+        - row "1 透明玻璃 18 3,442 1,562.29 ㎡ ¥549,149.26" [ref=e585]:
+          - cell "1" [ref=e586]
+          - cell "透明玻璃" [ref=e587]
+          - cell "18" [ref=e588]
+          - cell "3,442" [ref=e589]
+          - cell "1,562.29 ㎡" [ref=e590]
+          - cell "¥549,149.26" [ref=e591]
+        - row "2 太仓云瑞 50 4,985 2,268.77 ㎡ ¥399,567.35" [ref=e592]:
+          - cell "2" [ref=e593]
+          - cell "太仓云瑞" [ref=e594]
+          - cell "50" [ref=e595]
+          - cell "4,985" [ref=e596]
+          - cell "2,268.77 ㎡" [ref=e597]
+          - cell "¥399,567.35" [ref=e598]
+        - row "3 夹丝玻璃 22 299 299.20 ㎡ ¥225,707.32" [ref=e599]:
+          - cell "3" [ref=e600]
+          - cell "夹丝玻璃" [ref=e601]
+          - cell "22" [ref=e602]
+          - cell "299" [ref=e603]
+          - cell "299.20 ㎡" [ref=e604]
+          - cell "¥225,707.32" [ref=e605]
+        - row "4 伟刚 4 3,059 2,048.51 ㎡ ¥140,874.79" [ref=e606]:
+          - cell "4" [ref=e607]
+          - cell "伟刚" [ref=e608]
+          - cell "4" [ref=e609]
+          - cell "3,059" [ref=e610]
+          - cell "2,048.51 ㎡" [ref=e611]
+          - cell "¥140,874.79" [ref=e612]
+        - row "5 晶博耀 24 287 278.74 ㎡ ¥118,594.28" [ref=e613]:
+          - cell "5" [ref=e614]
+          - cell "晶博耀" [ref=e615]
+          - cell "24" [ref=e616]
+          - cell "287" [ref=e617]
+          - cell "278.74 ㎡" [ref=e618]
+          - cell "¥118,594.28" [ref=e619]
+        - row "6 联营 18 1,141 606.70 ㎡ ¥75,602.80" [ref=e620]:
+          - cell "6" [ref=e621]
+          - cell "联营" [ref=e622]
+          - cell "18" [ref=e623]
+          - cell "1,141" [ref=e624]
+          - cell "606.70 ㎡" [ref=e625]
+          - cell "¥75,602.80" [ref=e626]
+        - row "7 尚匡玻璃 21 90 137.48 ㎡ ¥72,935.81" [ref=e627]:
+          - cell "7" [ref=e628]
+          - cell "尚匡玻璃" [ref=e629]
+          - cell "21" [ref=e630]
+          - cell "90" [ref=e631]
+          - cell "137.48 ㎡" [ref=e632]
+          - cell "¥72,935.81" [ref=e633]
+        - row "8 吴江 16 95 133.70 ㎡ ¥61,532.00" [ref=e634]:
+          - cell "8" [ref=e635]
+          - cell "吴江" [ref=e636]
+          - cell "16" [ref=e637]
+          - cell "95" [ref=e638]
+          - cell "133.70 ㎡" [ref=e639]
+          - cell "¥61,532.00" [ref=e640]
+        - row "9 艺术玻璃 25 358 244.31 ㎡ ¥42,027.33" [ref=e641]:
+          - cell "9" [ref=e642]
+          - cell "艺术玻璃" [ref=e643]
+          - cell "25" [ref=e644]
+          - cell "358" [ref=e645]
+          - cell "244.31 ㎡" [ref=e646]
+          - cell "¥42,027.33" [ref=e647]
+        - row "10 烤漆玻璃 8 396 148.21 ㎡ ¥18,653.12" [ref=e648]:
+          - cell "10" [ref=e649]
+          - cell "烤漆玻璃" [ref=e650]
+          - cell "8" [ref=e651]
+          - cell "396" [ref=e652]
+          - cell "148.21 ㎡" [ref=e653]
+          - cell "¥18,653.12" [ref=e654]
+  - generic [ref=e655]:
+    - generic [ref=e656]:
+      - heading "数据质量说明" [level=2] [ref=e657]
+      - paragraph [ref=e658]: 已自动剔除每个 sheet 的汇总行、空白行和 0 值行,避免把总计重复计入图表。
+    - generic [ref=e660]:
+      - generic [ref=e661]:
+        - text: 已排除的典型行
+        - list [ref=e662]:
+          - listitem [ref=e663]: 太仓云瑞 第 53 行:合计(summary_row)
+          - listitem [ref=e664]: 伟刚 第 7 行:空白(blank_name)
+          - listitem [ref=e665]: 联营 第 21 行:空白(blank_name)
+          - listitem [ref=e666]: 吴江 第 18 行:空白(blank_name)
+          - listitem [ref=e667]: 尚匡玻璃 第 23 行:总计(summary_row)
+          - listitem [ref=e668]: 晶博耀 第 26 行:总计(summary_row)
+          - listitem [ref=e669]: 晶博耀 第 38 行:空白(blank_name)
+          - listitem [ref=e670]: 夹丝玻璃 第 24 行:总计(summary_row)
+        - generic [ref=e671]: 这类行主要是 总计/合计、空白行或 0 值占位行。它们不会进入图表和总表。
+      - generic [ref=e672]:
+        - generic [ref=e674]:
+          - heading "自动摘要" [level=3] [ref=e675]
+          - paragraph [ref=e676]: 顶部最强的几条产品级记录。
+        - generic [ref=e677]: 蓝星灰钢化(¥520,787.15);蓝星灰镀膜钢化(¥149,054.35);超白玻钢化(¥144,285.72)
+        - generic [ref=e678]: 这份报表的重点,是把编码型料单和厚度型 SKU 先做产品级归并,再对四个标签分别统计。 这样既能看总盘,也能回溯每个 SKU 的原始来源。
+  - generic [ref=e679]: 生成时间:2026-05-25 12:03:49 · 数据来源:D:\edge下载文件\玻璃明细.xlsx

BIN
.playwright-cli/page-2026-05-25T04-11-40-815Z.png


BIN
.playwright-cli/page-2026-05-25T04-12-29-139Z.png


BIN
.playwright-cli/page-2026-05-25T04-12-56-823Z.png


+ 575 - 0
.playwright-cli/page-2026-05-25T04-13-46-837Z.yml

@@ -0,0 +1,575 @@
+- generic [ref=e2]:
+  - generic [ref=e4]:
+    - generic [ref=e5]:
+      - heading "玻璃明细数据报表" [level=1] [ref=e6]
+      - paragraph [ref=e7]: 以“料单编号聚合 + 厚度 SKU 归并 + 标签并行统计”为主线,整合来源表中的有效明细。 已优先合并以 LM- 开头的同编号产品,并把 超白钢化 这类厚度 SKU 统一到产品级别。
+      - generic [ref=e8]:
+        - generic [ref=e9]: 文件:玻璃明细.xlsx
+        - generic [ref=e10]: 有效明细 206 行
+        - generic [ref=e11]: 产品级 92 组
+        - generic [ref=e12]: 已剔除 11 条汇总/空白行
+    - generic [ref=e13]:
+      - generic [ref=e14]:
+        - generic [ref=e15]: 总数量
+        - strong [ref=e16]: 14,152
+      - generic [ref=e17]:
+        - generic [ref=e18]: 总面积
+        - strong [ref=e19]: 7,727.92 ㎡
+      - generic [ref=e20]:
+        - generic [ref=e21]: 总金额
+        - strong [ref=e22]: ¥1,704,644.06
+      - generic [ref=e23]:
+        - generic [ref=e24]: 来源表
+        - strong [ref=e25]: "10"
+  - generic [ref=e26]:
+    - generic [ref=e27]:
+      - heading "统计概览" [level=2] [ref=e28]
+      - paragraph [ref=e29]: 先看整体规模,再往下看产品级聚合、标签维度和来源表分布。
+    - generic [ref=e31]:
+      - generic [ref=e32]:
+        - generic [ref=e33]: 有效记录
+        - strong [ref=e34]: "206"
+        - emphasis [ref=e35]: 去掉总计/空白行后
+      - generic [ref=e36]:
+        - generic [ref=e37]: 产品级分组
+        - strong [ref=e38]: "92"
+        - emphasis [ref=e39]: 同编号、同家族归并
+      - generic [ref=e40]:
+        - generic [ref=e41]: 原始SKU数
+        - strong [ref=e42]: "126"
+        - emphasis [ref=e43]: 去重后的原始名称数
+      - generic [ref=e44]:
+        - generic [ref=e45]: 夹丝命中
+        - strong [ref=e46]: "68"
+        - emphasis [ref=e47]: 可与其他标签重叠
+      - generic [ref=e48]:
+        - generic [ref=e49]: 透明命中
+        - strong [ref=e50]: "29"
+        - emphasis [ref=e51]: 可与其他标签重叠
+      - generic [ref=e52]:
+        - generic [ref=e53]: 烤漆/艺术
+        - strong [ref=e54]: 34 / 86
+        - emphasis [ref=e55]: 双维并行统计
+  - generic [ref=e56]:
+    - generic [ref=e57]:
+      - heading "规则聚合重点" [level=2] [ref=e58]
+      - paragraph [ref=e59]: 以下两组是这次报表里最关键的归并逻辑,和你的统计规则一一对应。
+    - generic [ref=e61]:
+      - generic [ref=e62]:
+        - generic [ref=e63]:
+          - generic [ref=e64]:
+            - generic [ref=e65]: LM 编号聚合
+            - generic [ref=e66]: LM-012
+          - generic [ref=e67]: 3 个 SKU
+        - generic [ref=e68]:
+          - generic [ref=e69]:
+            - generic [ref=e70]: 数量
+            - strong [ref=e71]: "52"
+          - generic [ref=e72]:
+            - generic [ref=e73]: 面积
+            - strong [ref=e74]: 61.76 ㎡
+          - generic [ref=e75]:
+            - generic [ref=e76]: 金额
+            - strong [ref=e77]: ¥41,124.00
+          - generic [ref=e78]:
+            - generic [ref=e79]: 来源
+            - strong [ref=e80]: 吴江
+        - generic [ref=e81]:
+          - generic [ref=e82]: LM-012单面双绒+超白夹丝透明钢化(4+4mm) ×2
+          - generic [ref=e83]: LM-012单面双绒+超白透明夹丝玻璃钢化(4+4mm) ×1
+          - generic [ref=e84]: LM-012单面双绒+超白玉砂夹丝玻璃钢化(4+4mm) ×1
+      - generic [ref=e85]:
+        - generic [ref=e86]:
+          - generic [ref=e87]:
+            - generic [ref=e88]: 超白钢化家族
+            - generic [ref=e89]: 超白钢化
+          - generic [ref=e90]: 2 个 SKU
+        - generic [ref=e91]:
+          - generic [ref=e92]:
+            - generic [ref=e93]: 数量
+            - strong [ref=e94]: "213"
+          - generic [ref=e95]:
+            - generic [ref=e96]: 面积
+            - strong [ref=e97]: 125.34 ㎡
+          - generic [ref=e98]:
+            - generic [ref=e99]: 金额
+            - strong [ref=e100]: ¥28,421.00
+          - generic [ref=e101]:
+            - generic [ref=e102]: 来源
+            - strong [ref=e103]: 太仓云瑞
+        - generic [ref=e104]:
+          - generic [ref=e105]: 超白钢化 ×4
+          - generic [ref=e106]: 4mm超白钢化(单面夹丝7063) ×1
+  - generic [ref=e107]:
+    - generic [ref=e108]:
+      - heading "产品级别 Top 10" [level=2] [ref=e109]
+      - paragraph [ref=e110]: 按金额排序后的产品级统计,已把同编号和同家族 SKU 聚合到一条产品线。
+    - generic [ref=e112]:
+      - generic [ref=e113]:
+        - generic [ref=e115]:
+          - heading "产品金额排行" [level=3] [ref=e116]
+          - paragraph [ref=e117]: 看哪个产品级组合贡献了最多销售额。
+        - img "bar chart" [ref=e118]:
+          - generic [ref=e120]: ¥130,196.79
+          - generic [ref=e121]: ¥260,393.57
+          - generic [ref=e122]: ¥390,590.36
+          - generic [ref=e123]: ¥520,787.15
+          - generic [ref=e124]:
+            - generic [ref=e125]: 蓝星灰钢化
+            - generic [ref=e128]: ¥520,787.15
+          - generic [ref=e129]:
+            - generic [ref=e130]: 蓝星灰镀膜钢化
+            - generic [ref=e133]: ¥149,054.35
+          - generic [ref=e134]:
+            - generic [ref=e135]: 超白玻钢化
+            - generic [ref=e138]: ¥144,285.72
+          - generic [ref=e139]:
+            - generic [ref=e140]: 蓝星灰钢化烤漆
+            - generic [ref=e143]: ¥136,356.15
+          - generic [ref=e144]:
+            - generic [ref=e145]: LM-035
+            - generic [ref=e148]: ¥84,769.78
+          - generic [ref=e149]:
+            - generic [ref=e150]: 欧茶钢化
+            - generic [ref=e153]: ¥61,140.84
+          - generic [ref=e154]:
+            - generic [ref=e155]: LM-012
+            - generic [ref=e158]: ¥41,124.00
+          - generic [ref=e159]:
+            - generic [ref=e160]: 无铜镜
+            - generic [ref=e163]: ¥38,074.00
+          - generic [ref=e164]:
+            - generic [ref=e165]: 蓝星灰镀膜钢化烤漆
+            - generic [ref=e168]: ¥34,745.70
+          - generic [ref=e169]:
+            - generic [ref=e170]: LM-001
+            - generic [ref=e173]: ¥29,881.14
+      - generic [ref=e174]:
+        - generic [ref=e176]:
+          - heading "来源表金额排行" [level=3] [ref=e177]
+          - paragraph [ref=e178]: 不同 sheet 的金额贡献和有效行数分布。
+        - img "bar chart" [ref=e179]:
+          - generic [ref=e181]: ¥137,287.32
+          - generic [ref=e182]: ¥274,574.63
+          - generic [ref=e183]: ¥411,861.95
+          - generic [ref=e184]: ¥549,149.26
+          - generic [ref=e185]:
+            - generic [ref=e186]: 透明玻璃
+            - generic [ref=e189]: ¥549,149.26
+          - generic [ref=e190]:
+            - generic [ref=e191]: 太仓云瑞
+            - generic [ref=e194]: ¥399,567.35
+          - generic [ref=e195]:
+            - generic [ref=e196]: 夹丝玻璃
+            - generic [ref=e199]: ¥225,707.32
+          - generic [ref=e200]:
+            - generic [ref=e201]: 伟刚
+            - generic [ref=e204]: ¥140,874.79
+          - generic [ref=e205]:
+            - generic [ref=e206]: 晶博耀
+            - generic [ref=e209]: ¥118,594.28
+          - generic [ref=e210]:
+            - generic [ref=e211]: 联营
+            - generic [ref=e214]: ¥75,602.80
+          - generic [ref=e215]:
+            - generic [ref=e216]: 尚匡玻璃
+            - generic [ref=e219]: ¥72,935.81
+          - generic [ref=e220]:
+            - generic [ref=e221]: 吴江
+            - generic [ref=e224]: ¥61,532.00
+          - generic [ref=e225]:
+            - generic [ref=e226]: 艺术玻璃
+            - generic [ref=e229]: ¥42,027.33
+          - generic [ref=e230]:
+            - generic [ref=e231]: 烤漆玻璃
+            - generic [ref=e234]: ¥18,653.12
+  - generic [ref=e235]:
+    - generic [ref=e236]:
+      - heading "标签维度" [level=2] [ref=e237]
+      - paragraph [ref=e238]: 四个维度按名称关键词和专类 sheet 共同归类,标签之间允许重叠,因此合计不会等于总量。
+    - generic [ref=e239]:
+      - generic [ref=e240]:
+        - generic [ref=e242]:
+          - heading "四大标签金额对比" [level=3] [ref=e243]
+          - paragraph [ref=e244]: 从多个来源里单独看夹丝、透明、烤漆、艺术玻璃。
+        - img "bar chart" [ref=e245]:
+          - generic [ref=e247]: ¥154,521.07
+          - generic [ref=e248]: ¥309,042.13
+          - generic [ref=e249]: ¥463,563.20
+          - generic [ref=e250]: ¥618,084.26
+          - generic [ref=e251]:
+            - generic [ref=e252]: 夹丝
+            - generic [ref=e255]: ¥432,872.65
+          - generic [ref=e256]:
+            - generic [ref=e257]: 透明
+            - generic [ref=e260]: ¥618,084.26
+          - generic [ref=e261]:
+            - generic [ref=e262]: 烤漆
+            - generic [ref=e265]: ¥243,889.34
+          - generic [ref=e266]:
+            - generic [ref=e267]: 艺术玻璃
+            - generic [ref=e270]: ¥237,040.29
+      - generic [ref=e271]:
+        - generic [ref=e272]:
+          - generic [ref=e273]:
+            - generic [ref=e274]:
+              - generic [ref=e275]: 夹丝
+              - generic [ref=e276]: 68 行 · 698 件 · ¥432,872.65
+            - generic [ref=e277]: 25.4%
+          - generic [ref=e278]:
+            - generic [ref=e279]:
+              - generic [ref=e280]: 超白玻钢化
+              - generic [ref=e283]: ¥144,285.72
+            - generic [ref=e284]:
+              - generic [ref=e285]: LM-035
+              - generic [ref=e288]: ¥84,769.78
+            - generic [ref=e289]:
+              - generic [ref=e290]: LM-012
+              - generic [ref=e293]: ¥41,124.00
+            - generic [ref=e294]:
+              - generic [ref=e295]: LM-001
+              - generic [ref=e298]: ¥29,881.14
+            - generic [ref=e299]:
+              - generic [ref=e300]: LM-002
+              - generic [ref=e303]: ¥24,856.00
+        - generic [ref=e304]:
+          - generic [ref=e305]:
+            - generic [ref=e306]:
+              - generic [ref=e307]: 透明
+              - generic [ref=e308]: 29 行 · 3,538 件 · ¥618,084.26
+            - generic [ref=e309]: 36.3%
+          - generic [ref=e310]:
+            - generic [ref=e311]:
+              - generic [ref=e312]: 蓝星灰钢化
+              - generic [ref=e315]: ¥328,571.65
+            - generic [ref=e316]:
+              - generic [ref=e317]: 蓝星灰镀膜钢化
+              - generic [ref=e320]: ¥98,889.55
+            - generic [ref=e321]:
+              - generic [ref=e322]: 欧茶钢化
+              - generic [ref=e325]: ¥55,805.99
+            - generic [ref=e326]:
+              - generic [ref=e327]: LM-012
+              - generic [ref=e330]: ¥39,594.00
+            - generic [ref=e331]:
+              - generic [ref=e332]: LM-031
+              - generic [ref=e335]: ¥23,858.00
+        - generic [ref=e336]:
+          - generic [ref=e337]:
+            - generic [ref=e338]:
+              - generic [ref=e339]: 烤漆
+              - generic [ref=e340]: 34 行 · 3,599 件 · ¥243,889.34
+            - generic [ref=e341]: 14.3%
+          - generic [ref=e342]:
+            - generic [ref=e343]:
+              - generic [ref=e344]: 蓝星灰钢化烤漆
+              - generic [ref=e347]: ¥136,356.15
+            - generic [ref=e348]:
+              - generic [ref=e349]: 蓝星灰镀膜钢化烤漆
+              - generic [ref=e352]: ¥34,745.70
+            - generic [ref=e353]:
+              - generic [ref=e354]: 欧茶钢化烤漆
+              - generic [ref=e357]: ¥24,308.10
+            - generic [ref=e358]:
+              - generic [ref=e359]: 超白钢化烤漆
+              - generic [ref=e362]: ¥8,704.35
+            - generic [ref=e363]:
+              - generic [ref=e364]: 超白一号平四边
+              - generic [ref=e367]: ¥7,698.78
+        - generic [ref=e368]:
+          - generic [ref=e369]:
+            - generic [ref=e370]:
+              - generic [ref=e371]: 艺术玻璃
+              - generic [ref=e372]: 86 行 · 2,134 件 · ¥237,040.29
+            - generic [ref=e373]: 13.9%
+          - generic [ref=e374]:
+            - generic [ref=e375]:
+              - generic [ref=e376]: 欧洲茶玻平四边钢化
+              - generic [ref=e379]: ¥24,556.24
+            - generic [ref=e380]:
+              - generic [ref=e381]: 超白长虹玉砂玻璃钢化
+              - generic [ref=e384]: ¥18,064.56
+            - generic [ref=e385]:
+              - generic [ref=e386]: 5+5热弯夹丝玻璃
+              - generic [ref=e389]: ¥15,399.26
+            - generic [ref=e390]:
+              - generic [ref=e391]: 超白一号平四边
+              - generic [ref=e394]: ¥15,397.56
+            - generic [ref=e395]:
+              - generic [ref=e396]: 蓝星灰切磨烤灰漆
+              - generic [ref=e399]: ¥14,200.00
+  - generic [ref=e400]:
+    - generic [ref=e401]:
+      - heading "产品级明细表" [level=2] [ref=e402]
+      - paragraph [ref=e403]: 按产品级统计前 15 条,便于回看每个聚合组对应了哪些原始 SKU。
+    - table [ref=e406]:
+      - rowgroup [ref=e407]:
+        - row "排名 产品级名称 原始SKU SKU数 明细行 数量 面积 金额 主来源" [ref=e408]:
+          - columnheader "排名" [ref=e409]
+          - columnheader "产品级名称" [ref=e410]
+          - columnheader "原始SKU" [ref=e411]
+          - columnheader "SKU数" [ref=e412]
+          - columnheader "明细行" [ref=e413]
+          - columnheader "数量" [ref=e414]
+          - columnheader "面积" [ref=e415]
+          - columnheader "金额" [ref=e416]
+          - columnheader "主来源" [ref=e417]
+      - rowgroup [ref=e418]:
+        - row "1 蓝星灰钢化 蓝星灰钢化 1 3 6,377 3,211.87 ㎡ ¥520,787.15 太仓云瑞" [ref=e419]:
+          - cell "1" [ref=e420]
+          - cell "蓝星灰钢化" [ref=e421]
+          - cell "蓝星灰钢化" [ref=e422]
+          - cell "1" [ref=e423]
+          - cell "3" [ref=e424]
+          - cell "6,377" [ref=e425]
+          - cell "3,211.87 ㎡" [ref=e426]
+          - cell "¥520,787.15" [ref=e427]
+          - cell "太仓云瑞" [ref=e428]
+        - row "2 蓝星灰镀膜钢化 蓝星灰镀膜钢化 1 2 526 418.04 ㎡ ¥149,054.35 太仓云瑞" [ref=e429]:
+          - cell "2" [ref=e430]
+          - cell "蓝星灰镀膜钢化" [ref=e431]
+          - cell "蓝星灰镀膜钢化" [ref=e432]
+          - cell "1" [ref=e433]
+          - cell "2" [ref=e434]
+          - cell "526" [ref=e435]
+          - cell "418.04 ㎡" [ref=e436]
+          - cell "¥149,054.35" [ref=e437]
+          - cell "太仓云瑞" [ref=e438]
+        - row "3 超白玻钢化 2+2mm超白玻钢化(夹丝002);2+2mm超白玻钢化(夹丝012);2+2mm超白玻钢化(夹丝013) 等 4 个SKU 4 8 328 319.06 ㎡ ¥144,285.72 晶博耀" [ref=e439]:
+          - cell "3" [ref=e440]
+          - cell "超白玻钢化" [ref=e441]
+          - cell "2+2mm超白玻钢化(夹丝002);2+2mm超白玻钢化(夹丝012);2+2mm超白玻钢化(夹丝013) 等 4 个SKU" [ref=e442]
+          - cell "4" [ref=e443]
+          - cell "8" [ref=e444]
+          - cell "328" [ref=e445]
+          - cell "319.06 ㎡" [ref=e446]
+          - cell "¥144,285.72" [ref=e447]
+          - cell "晶博耀" [ref=e448]
+        - row "4 蓝星灰钢化烤漆 蓝星灰钢化烤漆 1 1 2,371 872.95 ㎡ ¥136,356.15 太仓云瑞" [ref=e449]:
+          - cell "4" [ref=e450]
+          - cell "蓝星灰钢化烤漆" [ref=e451]
+          - cell "蓝星灰钢化烤漆" [ref=e452]
+          - cell "1" [ref=e453]
+          - cell "1" [ref=e454]
+          - cell "2,371" [ref=e455]
+          - cell "872.95 ㎡" [ref=e456]
+          - cell "¥136,356.15" [ref=e457]
+          - cell "太仓云瑞" [ref=e458]
+        - row "5 LM-035 LM-035夹丝8MM夹丝正面超白夹丝LM-035,背面贴白色不透光膜(此玻璃贴墙上,背面不透光);LM-035夹丝(2+2超白钢化)(四周香槟烤漆2·3公分);LM-035夹丝钢化4mm超白+4mm超白夹丝钢化 等 10 个SKU 10 10 50 87.15 ㎡ ¥84,769.78 尚匡玻璃" [ref=e459]:
+          - cell "5" [ref=e460]
+          - cell "LM-035" [ref=e461]
+          - cell "LM-035夹丝8MM夹丝正面超白夹丝LM-035,背面贴白色不透光膜(此玻璃贴墙上,背面不透光);LM-035夹丝(2+2超白钢化)(四周香槟烤漆2·3公分);LM-035夹丝钢化4mm超白+4mm超白夹丝钢化 等 10 个SKU" [ref=e462]
+          - cell "10" [ref=e463]
+          - cell "10" [ref=e464]
+          - cell "50" [ref=e465]
+          - cell "87.15 ㎡" [ref=e466]
+          - cell "¥84,769.78" [ref=e467]
+          - cell "尚匡玻璃" [ref=e468]
+        - row "6 欧茶钢化 欧茶钢化 1 3 348 167.83 ㎡ ¥61,140.84 太仓云瑞" [ref=e469]:
+          - cell "6" [ref=e470]
+          - cell "欧茶钢化" [ref=e471]
+          - cell "欧茶钢化" [ref=e472]
+          - cell "1" [ref=e473]
+          - cell "3" [ref=e474]
+          - cell "348" [ref=e475]
+          - cell "167.83 ㎡" [ref=e476]
+          - cell "¥61,140.84" [ref=e477]
+          - cell "太仓云瑞" [ref=e478]
+        - row "7 LM-012 LM-012单面双绒+超白夹丝透明钢化(4+4mm);LM-012单面双绒+超白透明夹丝玻璃钢化(4+4mm);LM-012单面双绒+超白玉砂夹丝玻璃钢化(4+4mm) 3 4 52 61.76 ㎡ ¥41,124.00 吴江" [ref=e479]:
+          - cell "7" [ref=e480]
+          - cell "LM-012" [ref=e481]
+          - cell "LM-012单面双绒+超白夹丝透明钢化(4+4mm);LM-012单面双绒+超白透明夹丝玻璃钢化(4+4mm);LM-012单面双绒+超白玉砂夹丝玻璃钢化(4+4mm)" [ref=e482]
+          - cell "3" [ref=e483]
+          - cell "4" [ref=e484]
+          - cell "52" [ref=e485]
+          - cell "61.76 ㎡" [ref=e486]
+          - cell "¥41,124.00" [ref=e487]
+          - cell "吴江" [ref=e488]
+        - row "8 无铜镜 无铜镜 1 2 492 368.54 ㎡ ¥38,074.00 太仓云瑞" [ref=e489]:
+          - cell "8" [ref=e490]
+          - cell "无铜镜" [ref=e491]
+          - cell "无铜镜" [ref=e492]
+          - cell "1" [ref=e493]
+          - cell "2" [ref=e494]
+          - cell "492" [ref=e495]
+          - cell "368.54 ㎡" [ref=e496]
+          - cell "¥38,074.00" [ref=e497]
+          - cell "太仓云瑞" [ref=e498]
+        - row "9 蓝星灰镀膜钢化烤漆 蓝星灰镀膜钢化烤漆 1 1 129 109.89 ㎡ ¥34,745.70 太仓云瑞" [ref=e499]:
+          - cell "9" [ref=e500]
+          - cell "蓝星灰镀膜钢化烤漆" [ref=e501]
+          - cell "蓝星灰镀膜钢化烤漆" [ref=e502]
+          - cell "1" [ref=e503]
+          - cell "1" [ref=e504]
+          - cell "129" [ref=e505]
+          - cell "109.89 ㎡" [ref=e506]
+          - cell "¥34,745.70" [ref=e507]
+          - cell "太仓云瑞" [ref=e508]
+        - row "10 LM-001 LM-001(4MM单层油砂钢化夹丝玻璃);LM-001超白双绒夹丝玻璃钢化4-4mm;LM-001超白双绒夹丝玻璃钢化-C646锁-下口1000 等 6 个SKU 6 6 49 58.76 ㎡ ¥29,881.14 尚匡玻璃" [ref=e509]:
+          - cell "10" [ref=e510]
+          - cell "LM-001" [ref=e511]
+          - cell "LM-001(4MM单层油砂钢化夹丝玻璃);LM-001超白双绒夹丝玻璃钢化4-4mm;LM-001超白双绒夹丝玻璃钢化-C646锁-下口1000 等 6 个SKU" [ref=e512]
+          - cell "6" [ref=e513]
+          - cell "6" [ref=e514]
+          - cell "49" [ref=e515]
+          - cell "58.76 ㎡" [ref=e516]
+          - cell "¥29,881.14" [ref=e517]
+          - cell "尚匡玻璃" [ref=e518]
+        - row "11 超白钢化 超白钢化;4mm超白钢化(单面夹丝7063) 2 5 213 125.34 ㎡ ¥28,421.00 太仓云瑞" [ref=e519]:
+          - cell "11" [ref=e520]
+          - cell "超白钢化" [ref=e521]
+          - cell "超白钢化;4mm超白钢化(单面夹丝7063)" [ref=e522]
+          - cell "2" [ref=e523]
+          - cell "5" [ref=e524]
+          - cell "213" [ref=e525]
+          - cell "125.34 ㎡" [ref=e526]
+          - cell "¥28,421.00" [ref=e527]
+          - cell "太仓云瑞" [ref=e528]
+        - row "12 LM-002 LM-002超白双绒夹丝玻璃钢化(4+4mm);LM-002(4+4MM)双绒+双绒夹丝透明钢化 2 3 27 48.29 ㎡ ¥24,856.00 吴江" [ref=e529]:
+          - cell "12" [ref=e530]
+          - cell "LM-002" [ref=e531]
+          - cell "LM-002超白双绒夹丝玻璃钢化(4+4mm);LM-002(4+4MM)双绒+双绒夹丝透明钢化" [ref=e532]
+          - cell "2" [ref=e533]
+          - cell "3" [ref=e534]
+          - cell "27" [ref=e535]
+          - cell "48.29 ㎡" [ref=e536]
+          - cell "¥24,856.00" [ref=e537]
+          - cell "吴江" [ref=e538]
+        - row "13 欧洲茶玻平四边钢化 欧洲茶玻平四边钢化 1 1 430 284.05 ㎡ ¥24,556.24 联营" [ref=e539]:
+          - cell "13" [ref=e540]
+          - cell "欧洲茶玻平四边钢化" [ref=e541]
+          - cell "欧洲茶玻平四边钢化" [ref=e542]
+          - cell "1" [ref=e543]
+          - cell "1" [ref=e544]
+          - cell "430" [ref=e545]
+          - cell "284.05 ㎡" [ref=e546]
+          - cell "¥24,556.24" [ref=e547]
+          - cell "联营" [ref=e548]
+        - row "14 欧茶钢化烤漆 欧茶钢化烤漆 1 1 233 124.78 ㎡ ¥24,308.10 太仓云瑞" [ref=e549]:
+          - cell "14" [ref=e550]
+          - cell "欧茶钢化烤漆" [ref=e551]
+          - cell "欧茶钢化烤漆" [ref=e552]
+          - cell "1" [ref=e553]
+          - cell "1" [ref=e554]
+          - cell "233" [ref=e555]
+          - cell "124.78 ㎡" [ref=e556]
+          - cell "¥24,308.10" [ref=e557]
+          - cell "太仓云瑞" [ref=e558]
+        - row "15 LM-031 LM-031超白双绒透明夹丝玻璃钢化4mm;LM-031超白双绒+超白透明夹丝玻璃钢化(4+4mm);LM-031超白双绒+超白透明夹丝钢化4+4mm 等 4 个SKU 4 5 37 39.75 ㎡ ¥23,858.00 吴江" [ref=e559]:
+          - cell "15" [ref=e560]
+          - cell "LM-031" [ref=e561]
+          - cell "LM-031超白双绒透明夹丝玻璃钢化4mm;LM-031超白双绒+超白透明夹丝玻璃钢化(4+4mm);LM-031超白双绒+超白透明夹丝钢化4+4mm 等 4 个SKU" [ref=e562]
+          - cell "4" [ref=e563]
+          - cell "5" [ref=e564]
+          - cell "37" [ref=e565]
+          - cell "39.75 ㎡" [ref=e566]
+          - cell "¥23,858.00" [ref=e567]
+          - cell "吴江" [ref=e568]
+  - generic [ref=e569]:
+    - generic [ref=e570]:
+      - heading "来源表明细" [level=2] [ref=e571]
+      - paragraph [ref=e572]: 用于看各 sheet 的有效行、数量、面积和金额总览。
+    - table [ref=e575]:
+      - rowgroup [ref=e576]:
+        - row "排名 来源表 有效行 数量 面积 金额" [ref=e577]:
+          - columnheader "排名" [ref=e578]
+          - columnheader "来源表" [ref=e579]
+          - columnheader "有效行" [ref=e580]
+          - columnheader "数量" [ref=e581]
+          - columnheader "面积" [ref=e582]
+          - columnheader "金额" [ref=e583]
+      - rowgroup [ref=e584]:
+        - row "1 透明玻璃 18 3,442 1,562.29 ㎡ ¥549,149.26" [ref=e585]:
+          - cell "1" [ref=e586]
+          - cell "透明玻璃" [ref=e587]
+          - cell "18" [ref=e588]
+          - cell "3,442" [ref=e589]
+          - cell "1,562.29 ㎡" [ref=e590]
+          - cell "¥549,149.26" [ref=e591]
+        - row "2 太仓云瑞 50 4,985 2,268.77 ㎡ ¥399,567.35" [ref=e592]:
+          - cell "2" [ref=e593]
+          - cell "太仓云瑞" [ref=e594]
+          - cell "50" [ref=e595]
+          - cell "4,985" [ref=e596]
+          - cell "2,268.77 ㎡" [ref=e597]
+          - cell "¥399,567.35" [ref=e598]
+        - row "3 夹丝玻璃 22 299 299.20 ㎡ ¥225,707.32" [ref=e599]:
+          - cell "3" [ref=e600]
+          - cell "夹丝玻璃" [ref=e601]
+          - cell "22" [ref=e602]
+          - cell "299" [ref=e603]
+          - cell "299.20 ㎡" [ref=e604]
+          - cell "¥225,707.32" [ref=e605]
+        - row "4 伟刚 4 3,059 2,048.51 ㎡ ¥140,874.79" [ref=e606]:
+          - cell "4" [ref=e607]
+          - cell "伟刚" [ref=e608]
+          - cell "4" [ref=e609]
+          - cell "3,059" [ref=e610]
+          - cell "2,048.51 ㎡" [ref=e611]
+          - cell "¥140,874.79" [ref=e612]
+        - row "5 晶博耀 24 287 278.74 ㎡ ¥118,594.28" [ref=e613]:
+          - cell "5" [ref=e614]
+          - cell "晶博耀" [ref=e615]
+          - cell "24" [ref=e616]
+          - cell "287" [ref=e617]
+          - cell "278.74 ㎡" [ref=e618]
+          - cell "¥118,594.28" [ref=e619]
+        - row "6 联营 18 1,141 606.70 ㎡ ¥75,602.80" [ref=e620]:
+          - cell "6" [ref=e621]
+          - cell "联营" [ref=e622]
+          - cell "18" [ref=e623]
+          - cell "1,141" [ref=e624]
+          - cell "606.70 ㎡" [ref=e625]
+          - cell "¥75,602.80" [ref=e626]
+        - row "7 尚匡玻璃 21 90 137.48 ㎡ ¥72,935.81" [ref=e627]:
+          - cell "7" [ref=e628]
+          - cell "尚匡玻璃" [ref=e629]
+          - cell "21" [ref=e630]
+          - cell "90" [ref=e631]
+          - cell "137.48 ㎡" [ref=e632]
+          - cell "¥72,935.81" [ref=e633]
+        - row "8 吴江 16 95 133.70 ㎡ ¥61,532.00" [ref=e634]:
+          - cell "8" [ref=e635]
+          - cell "吴江" [ref=e636]
+          - cell "16" [ref=e637]
+          - cell "95" [ref=e638]
+          - cell "133.70 ㎡" [ref=e639]
+          - cell "¥61,532.00" [ref=e640]
+        - row "9 艺术玻璃 25 358 244.31 ㎡ ¥42,027.33" [ref=e641]:
+          - cell "9" [ref=e642]
+          - cell "艺术玻璃" [ref=e643]
+          - cell "25" [ref=e644]
+          - cell "358" [ref=e645]
+          - cell "244.31 ㎡" [ref=e646]
+          - cell "¥42,027.33" [ref=e647]
+        - row "10 烤漆玻璃 8 396 148.21 ㎡ ¥18,653.12" [ref=e648]:
+          - cell "10" [ref=e649]
+          - cell "烤漆玻璃" [ref=e650]
+          - cell "8" [ref=e651]
+          - cell "396" [ref=e652]
+          - cell "148.21 ㎡" [ref=e653]
+          - cell "¥18,653.12" [ref=e654]
+  - generic [ref=e655]:
+    - generic [ref=e656]:
+      - heading "数据质量说明" [level=2] [ref=e657]
+      - paragraph [ref=e658]: 已自动剔除每个 sheet 的汇总行、空白行和 0 值行,避免把总计重复计入图表。
+    - generic [ref=e660]:
+      - generic [ref=e661]:
+        - text: 已排除的典型行
+        - list [ref=e662]:
+          - listitem [ref=e663]: 太仓云瑞 第 53 行:合计(summary_row)
+          - listitem [ref=e664]: 伟刚 第 7 行:空白(blank_name)
+          - listitem [ref=e665]: 联营 第 21 行:空白(blank_name)
+          - listitem [ref=e666]: 吴江 第 18 行:空白(blank_name)
+          - listitem [ref=e667]: 尚匡玻璃 第 23 行:总计(summary_row)
+          - listitem [ref=e668]: 晶博耀 第 26 行:总计(summary_row)
+          - listitem [ref=e669]: 晶博耀 第 38 行:空白(blank_name)
+          - listitem [ref=e670]: 夹丝玻璃 第 24 行:总计(summary_row)
+        - generic [ref=e671]: 这类行主要是 总计/合计、空白行或 0 值占位行。它们不会进入图表和总表。
+      - generic [ref=e672]:
+        - generic [ref=e674]:
+          - heading "自动摘要" [level=3] [ref=e675]
+          - paragraph [ref=e676]: 顶部最强的几条产品级记录。
+        - generic [ref=e677]: 蓝星灰钢化(¥520,787.15);蓝星灰镀膜钢化(¥149,054.35);超白玻钢化(¥144,285.72)
+        - generic [ref=e678]: 这份报表的重点,是把编码型料单和厚度型 SKU 先做产品级归并,再对四个标签分别统计。 这样既能看总盘,也能回溯每个 SKU 的原始来源。
+  - generic [ref=e679]: 生成时间:2026-05-25 12:13:37 · 数据来源:D:\edge下载文件\玻璃明细.xlsx

+ 134 - 0
NPM技能包发布管理.md

@@ -0,0 +1,134 @@
+# NPM 技能包发布管理
+
+本文记录 Claude Code / OpenClaw 技能包的 npm 发布、下载、安装和验证方式。
+
+## 发布状态总览
+
+| 技能包 | npm 包名 | 当前版本 | 状态 | 本地目录 | npm 链接 |
+|---|---|---:|---|---|---|
+| Claude Code VOC Intelligence | `@gangvy/claude-code-voc-intelligence` | `0.3.0` | 已发布 | `claude-code-voc-intelligence/` | https://www.npmjs.com/package/@gangvy/claude-code-voc-intelligence |
+| 装修量尺图缺标记检查 | `@gangvy/claude-code-measurement-marker-check` | `0.1.2` | 已发布 | `claude-code-measurement-marker-check/` | https://www.npmjs.com/package/@gangvy/claude-code-measurement-marker-check |
+
+## VOC Intelligence 安装入口
+
+推荐给 VSCode Claude Code 插件使用工作区安装:
+
+```powershell
+npx --yes @gangvy/claude-code-voc-intelligence@latest workspace --smoke
+```
+
+工作区安装会写入:
+
+```text
+.\.mcp.json
+.\.claude\plugins\voc-intelligence
+.\.claude\skills\xiaohongshu-trend-intelligence\SKILL.md
+.\.claude\skills\douyin-trend-intelligence\SKILL.md
+```
+
+全局安装:
+
+```powershell
+npm install -g @gangvy/claude-code-voc-intelligence
+claude-voc install
+```
+
+npx 免全局安装:
+
+```powershell
+npx --yes @gangvy/claude-code-voc-intelligence@latest install
+```
+
+Claude Code 命令行加载:
+
+```powershell
+claude --plugin-dir "$env:USERPROFILE\.claude\plugins\voc-intelligence"
+```
+
+## 教学测试话术
+
+小红书趋势情报:
+
+```text
+帮我做一份家装全屋定制行业的小红书趋势情报。
+先用演示样例跑通流程,不要真实采集。
+在聊天里给我第一轮样本观察和待确认问题。
+```
+
+抖音趋势情报:
+
+```text
+帮我做一份家装全屋定制行业的抖音趋势情报。
+先用演示样例跑通流程,不要真实采集。
+在聊天里给我第一轮样本观察和待确认问题。
+```
+
+真实采集口径:
+
+```text
+请切到 live 模式,并先检查 token 和余额状态。
+```
+
+没有 token 或余额不足时,技能包必须返回充值/开通链接,不允许把底层 403、权限或余额接口报错直接展示给用户。
+
+## 发布流程
+
+发布前验证:
+
+```powershell
+npm --prefix claude-code-voc-intelligence run mcp:smoke
+npm --prefix claude-code-voc-intelligence run smoke:package
+npm --prefix claude-code-voc-intelligence run xhs:sample
+npm --prefix claude-code-voc-intelligence run dy:sample
+```
+
+构建套件与 npm 包:
+
+```powershell
+npm run claude-voc:build
+npm run claude-voc:npm-pack
+```
+
+发布:
+
+```powershell
+npm run claude-voc:npm-publish
+```
+
+发布后验证:
+
+```powershell
+npm view @gangvy/claude-code-voc-intelligence version
+New-Item -ItemType Directory -Force outputs\vscode-workspace-plugin-npx-smoke-0.3.0 | Out-Null
+Push-Location outputs\vscode-workspace-plugin-npx-smoke-0.3.0
+npx --yes @gangvy/claude-code-voc-intelligence@latest workspace --smoke
+Pop-Location
+```
+
+## 认证方式
+
+发布脚本支持临时读取 `NPM_TOKEN` 或 `NODE_AUTH_TOKEN`。本地建议放在:
+
+```text
+release\npm\claude-code-voc-intelligence\.env.local
+```
+
+只允许本地使用,不要提交 npm token、VOC session token、模型 API key 或任何真实用户 token。
+
+## 本次发布记录
+
+- 2026-05-25 已发布 `@gangvy/claude-code-voc-intelligence@0.3.0`,npm `latest` 已验证为 `0.3.0`。
+- `0.3.0` 新增抖音趋势情报能力:`douyin-trend-intelligence` skill、`voc_douyin_trend_run`、`voc_douyin_token_check`、`voc_douyin_preference_update`。
+- 保留小红书趋势情报能力,并让工作区安装同时生成小红书和抖音两个 skill 入口。
+- 统一 token / 充值体验:没有 token 返回开通链接,余额不足或业务 403 返回充值链接,不暴露底层接口报错。
+- 新增 `business-status recharge guard` smoke,用来防止 HTTP 200 但业务码 403 时误判为成功。
+- 已通过 `mcp:smoke`、`smoke:package`、`xhs:sample`、`dy:sample`、`claude-voc:build`、`claude-voc:npm-pack`、`claude-voc:npm-publish`。
+- 发布后已用 `npx --yes @gangvy/claude-code-voc-intelligence@latest workspace --smoke` 从 npm registry 重新安装并验证通过。
+
+## 后续更新规则
+
+- 升级功能时先改 `claude-code-voc-intelligence/package.json` 版本,再同步 `skill-package-manifest.json`、`.claude-plugin/plugin.json` 和发布管理文档。
+- 保持输入简单:用户说自然语言即可,不要求理解 MCP schema。
+- 第一轮报告只输出“初步判断 / 机会假设 / 待校准”,不写成最终结论。
+- 新增平台能力优先作为并列 skill 和并列 MCP tool,不把完整业务闭环强塞进单一 skill。
+- 每次发布前必须跑包级 smoke,并包含 sample、workspace 安装、no-token 充值提示、余额/业务码保护和 MCP tool 发现。

+ 8 - 0
claude-code-measurement-marker-check/.claude-plugin/plugin.json

@@ -0,0 +1,8 @@
+{
+  "name": "measurement-marker-check",
+  "description": "P0 renovation measurement drawing marker check skill for Claude Code. Upload a measurement image, identify missing annotations, and produce a review checklist.",
+  "version": "0.1.2",
+  "author": {
+    "name": "nkkj-BrainHack"
+  }
+}

+ 74 - 0
claude-code-measurement-marker-check/README.md

@@ -0,0 +1,74 @@
+# Claude Code Measurement Marker Check
+
+实战二独立技能包:装修量尺图缺标记检查。
+
+P0 小闭环:
+
+1. 用户上传一张量尺图、手绘量尺单、户型草图或现场测量截图。
+2. Claude Code 先观察图片,提取已标记内容、疑似缺标位置和模糊待确认区域。
+3. 本地工具把结构化观察转成稳定报告:缺标记清单、风险说明、图像依据、人工复核建议。
+
+## 用户入口话术
+
+```text
+帮我检查这张装修量尺图有没有哪里未标记。
+重点看空间名称、门窗洞口、长宽高、管道/插座/烟道和现场限制备注。
+先给我缺标记清单和人工复核建议。
+```
+
+## 本地验证
+
+```bash
+npm --prefix claude-code-measurement-marker-check run measure:sample
+npm --prefix claude-code-measurement-marker-check run smoke
+```
+
+## Claude Code 加载
+
+推荐给 VSCode Claude Code 使用工作区安装:
+
+```bash
+npx @gangvy/claude-code-measurement-marker-check workspace --smoke
+```
+
+安装后会生成:
+
+```text
+.\.claude\plugins\measurement-marker-check
+.\.claude\skills\measurement-marker-check\SKILL.md
+```
+
+如果 Claude Code 会话已经打开,安装后重启当前 VSCode Claude Code 会话。
+
+用户级安装:
+
+```bash
+npm install -g @gangvy/claude-code-measurement-marker-check
+claude-measure-marker install
+```
+
+免全局安装到用户级插件目录:
+
+```bash
+npx @gangvy/claude-code-measurement-marker-check install
+```
+
+```bash
+claude --plugin-dir ./claude-code-measurement-marker-check
+```
+
+进入 Claude Code 后可直接调用:
+
+```text
+/measurement-marker-check:measurement-marker-check
+```
+
+## 参考实现
+
+参考项目 `E:\ltc-pobingfeng` 中的图片识别调用方式:
+
+- `src/modules/owner/nav/consultation/menchuang-agent/image-analysis.service.ts`
+- `src/modules/owner/nav/consultation/menchuang-agent/window-recognition.service.ts`
+- `AI模型导入与使用文档.md`
+
+核心模式是 `completionJSON` 结构化输出,并通过 `vision: true` 和 `images: [imageUrl]` 传入图片。当前独立技能 P0 优先利用 Claude Code 的上传图片视觉能力完成观察,再用本地工具固化报告。

+ 334 - 0
claude-code-measurement-marker-check/bin/claude-measure-marker.js

@@ -0,0 +1,334 @@
+#!/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 WORKSPACE_ROOT = process.cwd();
+const DEFAULT_TARGET = path.join(os.homedir(), '.claude', 'plugins', 'measurement-marker-check');
+const WORKSPACE_TARGET = path.join(WORKSPACE_ROOT, '.claude', 'plugins', 'measurement-marker-check');
+const WORKSPACE_PLUGINS_ROOT = path.join(WORKSPACE_ROOT, '.claude', 'plugins');
+
+const COPY_ENTRIES = [
+  '.claude-plugin',
+  'README.md',
+  'scripts',
+  'skill-package-manifest.json',
+  'skills',
+  'test-fixtures',
+  'tools',
+  'package.json'
+];
+
+function expandHome(value) {
+  return String(value || '').replace(/^~(?=$|[\\/])/, os.homedir());
+}
+
+function parseArgs(argv) {
+  const first = argv[0] && !argv[0].startsWith('--') ? argv[0] : 'install';
+  const args = {
+    command: first,
+    target: DEFAULT_TARGET,
+    smoke: false,
+    force: false,
+    help: false
+  };
+
+  if (first === 'workspace' || first === 'install-workspace') {
+    args.command = 'install';
+    args.target = WORKSPACE_TARGET;
+  }
+
+  for (let i = first === argv[0] ? 1 : 0; i < argv.length; i++) {
+    const token = argv[i];
+    if (token === '--target' && argv[i + 1]) {
+      args.target = argv[++i];
+    } else if (token.startsWith('--target=')) {
+      args.target = token.slice('--target='.length);
+    } else if (token === '--workspace') {
+      args.target = WORKSPACE_TARGET;
+    } else if (token === '--smoke') {
+      args.smoke = true;
+    } else if (token === '--force') {
+      args.force = true;
+    } else if (token === '--help' || token === '-h') {
+      args.help = true;
+    }
+  }
+
+  args.target = path.resolve(expandHome(args.target));
+  return args;
+}
+
+function printHelp() {
+  console.log([
+    'Claude measurement marker skill package installer',
+    '',
+    'Usage:',
+    '  claude-measure-marker install [--target <dir>] [--smoke]',
+    '  claude-measure-marker workspace [--smoke]',
+    '  claude-measure-marker install --workspace [--smoke]',
+    '  claude-measure-marker check',
+    '  claude-measure-marker smoke',
+    '  claude-measure-marker path',
+    '',
+    'npx:',
+    '  npx @gangvy/claude-code-measurement-marker-check install',
+    '  npx @gangvy/claude-code-measurement-marker-check workspace --smoke',
+    '',
+    'Options:',
+    '  --workspace      Install into ./.claude/plugins/measurement-marker-check',
+    '  --target <dir>   Install into a custom directory',
+    '  --force          Allow overwriting an existing custom target',
+    '  --smoke          Run sample smoke checks after install',
+    '  --help, -h       Show help'
+  ].join('\n'));
+}
+
+function ensureDir(dirPath) {
+  fs.mkdirSync(dirPath, { recursive: true });
+}
+
+function isInside(parentDir, childDir) {
+  const relative = path.relative(path.resolve(parentDir), path.resolve(childDir));
+  return relative === '' || (!!relative && !relative.startsWith('..') && !path.isAbsolute(relative));
+}
+
+function isSafeDefaultTarget(targetDir) {
+  return path.resolve(targetDir) === path.resolve(DEFAULT_TARGET);
+}
+
+function isSafeWorkspaceTarget(targetDir) {
+  return isInside(WORKSPACE_PLUGINS_ROOT, targetDir);
+}
+
+function canOverwriteTarget(targetDir, force) {
+  return force || isSafeDefaultTarget(targetDir) || isSafeWorkspaceTarget(targetDir);
+}
+
+function copyDirRecursive(source, destination) {
+  const stat = fs.statSync(source);
+  if (stat.isDirectory()) {
+    ensureDir(destination);
+    for (const child of fs.readdirSync(source)) {
+      if (child === 'node_modules' || child === 'outputs' || child === '.git') continue;
+      copyDirRecursive(path.join(source, child), path.join(destination, child));
+    }
+    return;
+  }
+  ensureDir(path.dirname(destination));
+  fs.copyFileSync(source, destination);
+}
+
+function copyPlugin(target, force) {
+  if (fs.existsSync(target)) {
+    if (!canOverwriteTarget(target, force)) {
+      throw new Error(`Refusing to overwrite custom target without --force: ${target}`);
+    }
+    fs.rmSync(target, { recursive: true, force: true });
+  }
+  ensureDir(target);
+
+  for (const entry of COPY_ENTRIES) {
+    const source = path.join(SOURCE_ROOT, entry);
+    if (!fs.existsSync(source)) continue;
+    copyDirRecursive(source, path.join(target, entry));
+  }
+}
+
+function checkPlugin(target) {
+  const required = [
+    '.claude-plugin/plugin.json',
+    'skills/measurement-marker-check/SKILL.md',
+    'tools/measurement-marker-check-run.js',
+    'scripts/smoke.js',
+    'package.json'
+  ];
+  const missing = required.filter(entry => !fs.existsSync(path.join(target, entry)));
+  if (missing.length) {
+    throw new Error(`Install target is missing required files: ${missing.join(', ')}`);
+  }
+  return {
+    status: 'ok',
+    target,
+    required
+  };
+}
+
+function asForwardSlash(filePath) {
+  return path.resolve(filePath).replace(/\\/g, '/');
+}
+
+function quotedPath(filePath) {
+  return `"${asForwardSlash(filePath)}"`;
+}
+
+function workspaceSkillText(target) {
+  const runner = path.join(target, 'tools', 'measurement-marker-check-run.js');
+  return [
+    '---',
+    'name: measurement-marker-check',
+    'description: Check 装修量尺图、手绘量尺单、户型草图或现场测量图片中是否存在疑似未标记位置,输出缺标记清单、待复核区域和下一步补标建议。Use when the user asks for 实战二、装修量尺、量尺图识别、尺寸图缺标记、哪里没标、未标注检查 or measurement drawing review.',
+    'allowed-tools: Read Write Bash(node *)',
+    '---',
+    '',
+    '# 装修量尺图缺标记检查官',
+    '',
+    '你是面向设计/量尺团队的量尺图缺标记检查官。用户要的是业务复核清单,不是技术日志。',
+    '',
+    '## 工作流',
+    '',
+    '1. 如果用户没有上传图片或给出图片路径,先请用户上传量尺图、手绘量尺单、户型草图或现场测量截图。',
+    '2. 如果用户上传了图片,先直接观察图片内容,整理已标记内容、疑似缺标位置、待人工复核区域。',
+    '3. 把观察结果写成结构化 JSON,保存到 `outputs/measurement-marker-check/input.json`。',
+    '4. 调用本地工具生成稳定报告。',
+    '5. 优先把工具返回的 `assistantMessage` 正文发给用户,文件路径只放末尾作为归档信息。',
+    '',
+    '## 检查口径',
+    '',
+    '- 空间名称:客厅、卧室、厨房、卫生间、阳台等。',
+    '- 关键尺寸:长、宽、高、门洞、窗洞、墙面、柜体位置等。',
+    '- 门窗洞口:宽高、离地高度、开启方向、门套或窗台限制。',
+    '- 梁柱/墙体限制:梁高、柱位、墙厚、承重/非承重提示。',
+    '- 水电/管道/烟道:下水、地漏、插座、开关、燃气、烟道、排水管。',
+    '- 安装避让备注:需避让、不可拆、需复核、现场限制、手写备注。',
+    '',
+    '## 工具输入 JSON 结构',
+    '',
+    '```json',
+    '{',
+    '  "project": "项目或样本名称",',
+    '  "image": "图片文件名或路径",',
+    '  "detectedMarkers": [',
+    '    { "area": "厨房右侧墙面", "markerType": "关键尺寸", "value": "3200mm", "evidence": "墙面横向尺寸已标注", "confidence": 0.9 }',
+    '  ],',
+    '  "missingMarkers": [',
+    '    { "area": "窗户下沿", "missingType": "窗台高度", "risk": "可能影响台面、吊柜或窗帘盒设计", "evidence": "窗户位置已画出,但没有离地高度标注", "confidence": 0.78 }',
+    '  ],',
+    '  "uncertainAreas": [',
+    '    { "area": "厨房右上角", "reason": "疑似烟道或立管,但文字不清晰", "action": "请现场照片或原始量尺单复核", "confidence": 0.58 }',
+    '  ],',
+    '  "overallJudgement": "存在关键缺标记,建议人工复核后再进入标准尺寸表。"',
+    '}',
+    '```',
+    '',
+    '## 生成报告命令',
+    '',
+    '```bash',
+    `node ${quotedPath(runner)} --input "outputs/measurement-marker-check/input.json" --output "outputs/measurement-marker-check" --result-prefix MEASURE_MARKER_RESULT`,
+    '```',
+    '',
+    '## 演示样例命令',
+    '',
+    '```bash',
+    `node ${quotedPath(runner)} --sample --output "outputs/measurement-marker-check-sample" --result-prefix MEASURE_MARKER_RESULT`,
+    '```',
+    '',
+    '## 输出规则',
+    '',
+    '- 先给结论:是否存在疑似缺标记。',
+    '- 必须明确“区域 + 缺失类型 + 风险 + 图像依据 + 置信度”。',
+    '- 看不清的地方归入“待人工复核区域”,不要强行判断。',
+    '- 不要把 P0 结果说成最终工程结论;它是辅助复核清单。',
+    '- 不要只返回文件路径。',
+    '',
+    '## 推荐启动话术',
+    '',
+    '```text',
+    '帮我检查这张装修量尺图有没有哪里未标记。',
+    '重点看空间名称、门窗洞口、长宽高、管道、插座/烟道和现场限制备注。',
+    '先给我缺标记清单和人工复核建议。',
+    '```',
+    ''
+  ].join('\n');
+}
+
+function writeWorkspaceSkillEntry(target) {
+  if (!isSafeWorkspaceTarget(target)) return;
+  const skillDir = path.join(WORKSPACE_ROOT, '.claude', 'skills', 'measurement-marker-check');
+  ensureDir(skillDir);
+  fs.writeFileSync(path.join(skillDir, 'SKILL.md'), workspaceSkillText(target), 'utf8');
+  console.log('Wrote workspace skill entry: .\\.claude\\skills\\measurement-marker-check\\SKILL.md');
+}
+
+function runSmoke(target) {
+  const result = spawnSync(process.execPath, ['scripts/smoke.js'], {
+    cwd: target,
+    stdio: 'inherit',
+    shell: false
+  });
+  if (result.status !== 0) {
+    throw new Error('smoke failed');
+  }
+}
+
+function printNextSteps(target) {
+  const workspaceMode = isSafeWorkspaceTarget(target);
+  console.log('');
+  console.log('Install complete.');
+  console.log('');
+  if (workspaceMode) {
+    console.log('VSCode workspace mode is ready.');
+    console.log('');
+    console.log('Generated project files:');
+    console.log('  .\\.claude\\skills\\measurement-marker-check\\SKILL.md');
+    console.log('  .\\.claude\\plugins\\measurement-marker-check');
+    console.log('');
+    console.log('Restart the VSCode Claude Code session if it was already open.');
+  } else {
+    console.log('Claude Code CLI launch command:');
+    console.log(`  claude --plugin-dir "${target}"`);
+  }
+  console.log('');
+  console.log('Try this prompt in Claude Code:');
+  console.log('  帮我检查这张装修量尺图有没有哪里未标记。');
+  console.log('  重点看空间名称、门窗洞口、长宽高、管道、插座/烟道和现场限制备注。');
+  console.log('  先给我缺标记清单和人工复核建议。');
+  console.log('');
+  console.log(`Install target: ${target}`);
+}
+
+function main() {
+  const args = parseArgs(process.argv.slice(2));
+
+  if (args.help || args.command === 'help') {
+    printHelp();
+    return;
+  }
+
+  if (args.command === 'path') {
+    console.log(args.target);
+    return;
+  }
+
+  if (args.command === 'install') {
+    copyPlugin(args.target, args.force);
+    const check = checkPlugin(args.target);
+    console.log(JSON.stringify(check, null, 2));
+    writeWorkspaceSkillEntry(args.target);
+    if (args.smoke) runSmoke(args.target);
+    printNextSteps(args.target);
+    return;
+  }
+
+  if (args.command === 'check') {
+    console.log(JSON.stringify(checkPlugin(args.target), null, 2));
+    return;
+  }
+
+  if (args.command === 'smoke') {
+    runSmoke(SOURCE_ROOT);
+    return;
+  }
+
+  printHelp();
+  process.exitCode = 1;
+}
+
+try {
+  main();
+} catch (error) {
+  console.error(`claude-measure-marker failed: ${error.message}`);
+  process.exit(1);
+}

+ 23 - 0
claude-code-measurement-marker-check/package.json

@@ -0,0 +1,23 @@
+{
+  "name": "@gangvy/claude-code-measurement-marker-check",
+  "version": "0.1.2",
+  "description": "Claude Code skill for P0 renovation measurement drawing missing-marker checks.",
+  "type": "commonjs",
+  "bin": {
+    "claude-measure-marker": "bin/claude-measure-marker.js"
+  },
+  "scripts": {
+    "measure:sample": "node tools/measurement-marker-check-run.js --sample --output ../outputs/claude-code-measurement-marker-sample --result-prefix MEASURE_MARKER_RESULT",
+    "smoke": "node scripts/smoke.js"
+  },
+  "files": [
+    ".claude-plugin/",
+    "bin/",
+    "README.md",
+    "scripts/",
+    "skill-package-manifest.json",
+    "skills/",
+    "tools/"
+  ],
+  "dependencies": {}
+}

+ 49 - 0
claude-code-measurement-marker-check/scripts/smoke.js

@@ -0,0 +1,49 @@
+#!/usr/bin/env node
+const path = require('path');
+const { runMeasurementMarkerCheck } = require('../tools/measurement-marker-check-run');
+
+async function main() {
+  const output = path.resolve(__dirname, '..', 'outputs', 'measurement-marker-smoke');
+  const sample = await runMeasurementMarkerCheck({
+    sample: true,
+    output
+  });
+
+  if (sample.status !== 'ok') {
+    throw new Error(`sample failed: ${sample.status}`);
+  }
+  if (!sample.summary || sample.summary.missingCount < 1) {
+    throw new Error('sample did not report missing markers');
+  }
+  if (!Array.isArray(sample.files) || sample.files.length < 2) {
+    throw new Error('sample did not write report files');
+  }
+
+  const needsVision = await runMeasurementMarkerCheck({
+    image: 'demo-measurement.jpg'
+  });
+  if (needsVision.status !== 'needs_vision_observation') {
+    throw new Error(`expected needs_vision_observation, got: ${needsVision.status}`);
+  }
+
+  console.log(JSON.stringify({
+    status: 'ok',
+    checks: [
+      'sample report',
+      'missing marker count',
+      'report files',
+      'needs vision observation guard'
+    ],
+    missingCount: sample.summary.missingCount,
+    uncertainCount: sample.summary.uncertainCount,
+    files: sample.files
+  }, null, 2));
+}
+
+main().catch(error => {
+  console.error(JSON.stringify({
+    status: 'error',
+    message: error.message
+  }, null, 2));
+  process.exit(1);
+});

+ 13 - 0
claude-code-measurement-marker-check/skill-package-manifest.json

@@ -0,0 +1,13 @@
+{
+  "name": "claude-code-measurement-marker-check",
+  "version": "0.1.2",
+  "description": "Claude Code 独立技能包:装修量尺图缺标记检查。P0 支持上传量尺图后识别疑似未标记位置、待复核区域和补标建议。",
+  "plugin": "measurement-marker-check",
+  "skills": [
+    "measurement-marker-check"
+  ],
+  "entrySkill": "measurement-marker-check",
+  "sampleCommand": "npm run measure:sample",
+  "smokeCommand": "npm run smoke",
+  "installHint": "独立加载此目录即可:claude --plugin-dir ./claude-code-measurement-marker-check"
+}

+ 125 - 0
claude-code-measurement-marker-check/skills/measurement-marker-check/SKILL.md

@@ -0,0 +1,125 @@
+---
+name: measurement-marker-check
+description: Check装修量尺图、手绘量尺单、户型草图或现场测量图片中是否存在疑似未标记位置,输出缺标记清单、待复核区域和下一步补标建议。Use when the user asks for 实战二、装修量尺、量尺图识别、尺寸图缺标记、哪里没标、未标注检查 or measurement drawing review.
+allowed-tools: Read Write Bash(node *)
+---
+
+# 装修量尺图缺标记检查官
+
+## 角色
+
+你是面向设计/量尺团队的“量尺图缺标记检查官”。你的任务是看懂用户上传的量尺图、手绘草图、现场测量照片或截图,判断图中是否存在疑似未标记的位置,并把结果转成可复核的业务清单。
+
+P0 目标不是一次性替代人工量尺,而是跑通一个小闭环:
+
+- 用户上传一张量尺图。
+- 你识别已标记内容和疑似缺标位置。
+- 你输出“哪里可能没标、缺什么、为什么有风险、下一步怎么补”。
+- 结果要能被设计师或量尺人员现场确认。
+
+## 触发场景
+
+当用户提到以下需求时,使用本技能:
+
+- “实战二量尺图识别”
+- “上传量尺图看哪里没标”
+- “检查这张尺寸图有没有漏标”
+- “量尺图是否有未标记”
+- “手写量尺单识别缺失项”
+- “Claudecode 流程跑实战2”
+
+## 看图检查口径
+
+默认检查以下标记是否清楚:
+
+- 空间名称:客厅、卧室、厨房、卫生间、阳台等。
+- 关键尺寸:长、宽、高、门洞、窗洞、墙面、柜体位置等。
+- 门窗洞口:宽高、离地高度、开启方向、门套或窗台限制。
+- 梁柱/墙体限制:梁高、柱位、墙厚、承重/非承重提示。
+- 水电/管道/烟道:下水、地漏、插座、开关、燃气、烟道、排水管。
+- 安装避让备注:需避让、不可拆、需复核、现场限制、手写备注。
+
+## 参考识图实现
+
+参考 `E:\ltc-pobingfeng` 的图片识别功能,尤其是:
+
+- `AI模型导入与使用文档.md`
+- `src/modules/owner/nav/consultation/menchuang-agent/image-analysis.service.ts`
+- `src/modules/owner/nav/consultation/menchuang-agent/window-recognition.service.ts`
+
+该项目的关键做法是:用 `completionJSON` 定义 JSON 输出格式,并在调用参数中设置 `vision: true`、`images: [imageUrl]`。本技能 P0 在 Claude Code 里先利用上传图片的视觉能力完成观察,再调用本技能目录里的本地工具固化报告。
+
+## 工作流
+
+1. 如果用户没有给图片或图片路径,先让用户上传量尺图,不要生成空报告。
+2. 如果用户上传了图片或给了本地图片路径,先直接观察图片内容。
+3. 输出前先在内部整理结构化观察:
+
+```json
+{
+  "project": "项目或样本名称",
+  "image": "图片文件名或路径",
+  "detectedMarkers": [
+    {
+      "area": "厨房右侧墙面",
+      "markerType": "关键尺寸",
+      "value": "3200mm",
+      "evidence": "墙面横向尺寸已标注",
+      "confidence": 0.9
+    }
+  ],
+  "missingMarkers": [
+    {
+      "area": "窗户下沿",
+      "missingType": "窗台高度",
+      "risk": "窗台高度缺失,可能影响台面、吊柜或窗帘盒设计",
+      "evidence": "窗户位置已画出,但没有离地高度标注",
+      "confidence": 0.78
+    }
+  ],
+  "uncertainAreas": [
+    {
+      "area": "厨房右上角",
+      "reason": "疑似烟道或立管,但图中文字不清晰",
+      "action": "请现场照片或原始量尺单复核",
+      "confidence": 0.58
+    }
+  ],
+  "overallJudgement": "存在关键缺标记,建议人工复核后再进入标准尺寸表。"
+}
+```
+
+4. 把上述 JSON 写入工作区临时文件,例如 `outputs/measurement-marker-check/input.json`。
+5. 调用本地工具生成稳定报告:
+
+```bash
+node "${CLAUDE_SKILL_DIR}/../../tools/measurement-marker-check-run.js" --input "outputs/measurement-marker-check/input.json" --output "outputs/measurement-marker-check" --result-prefix MEASURE_MARKER_RESULT
+```
+
+6. 优先把工具返回的 `assistantMessage` 正文发给用户。文件路径只放在末尾作为归档信息。
+
+P0 sample 演示命令:
+
+```bash
+node "${CLAUDE_SKILL_DIR}/../../tools/measurement-marker-check-run.js" --sample --output "outputs/measurement-marker-check-sample" --result-prefix MEASURE_MARKER_RESULT
+```
+
+## 输出规则
+
+- 先给结论:是否存在疑似缺标记。
+- 必须明确“区域 + 缺失类型 + 风险 + 图像依据 + 置信度”。
+- 对看不清的地方要归入“待人工复核区域”,不要强行判定。
+- 如果没有发现明显缺标,也要说明检查口径和残余风险。
+- 不要只说“图片不清晰”;要指出哪里不清晰、需要补什么资料。
+- 不要把 P0 结果说成最终工程结论;它是辅助复核清单。
+- 不要暴露模型参数、接口参数或工具实现细节给业务用户。
+
+## 推荐启动话术
+
+用户可以直接说:
+
+```text
+帮我检查这张装修量尺图有没有哪里未标记。
+重点看空间名称、门窗洞口、长宽高、管道/插座/烟道和现场限制备注。
+先给我缺标记清单和人工复核建议。
+```

+ 43 - 0
claude-code-measurement-marker-check/skills/measurement-marker-check/references/fmode-vision-reference.md

@@ -0,0 +1,43 @@
+# Fmode 图片识别参考
+
+参考项目:`E:\ltc-pobingfeng`
+
+关键文件:
+
+- `AI模型导入与使用文档.md`
+- `src/modules/owner/nav/consultation/menchuang-agent/image-analysis.service.ts`
+- `src/modules/owner/nav/consultation/menchuang-agent/window-recognition.service.ts`
+
+## 核心调用方式
+
+参考项目使用 `fmode-ng/core` 的 `completionJSON` 做结构化 JSON 输出:
+
+```typescript
+import { completionJSON } from 'fmode-ng/core';
+
+const result = await completionJSON(
+  systemPrompt,
+  outputFormat,
+  onProgress,
+  3,
+  {
+    model: 'fmode-1.6-cn',
+    temperature: 0.1,
+    vision: true,
+    images: [imageUrl]
+  }
+);
+```
+
+## 对实战二的迁移方式
+
+P0 阶段在 Claude Code 技能里先不引入前端上传链路,流程拆为两段:
+
+1. Claude Code 接收用户上传的量尺图,并做视觉观察。
+2. 本地工具接收结构化观察结果,生成稳定的缺标记报告。
+
+后续如果要接入前端,可复用参考项目的上传和图片 URL 识别方式:
+
+- 图片上传到可访问 URL。
+- 用 `completionJSON` 加 `vision: true` 和 `images: [imageUrl]` 调模型。
+- 输出固定 JSON,再交给本技能的报告工具生成业务可读结果。

+ 32 - 0
claude-code-measurement-marker-check/test-fixtures/chain-test-input.json

@@ -0,0 +1,32 @@
+{
+  "project": "实战二链路测试",
+  "image": "demo-measurement.jpg",
+  "sourceType": "image",
+  "detectedMarkers": [
+    {
+      "area": "厨房右墙",
+      "markerType": "关键尺寸",
+      "value": "3200mm",
+      "evidence": "横向墙面尺寸已标注",
+      "confidence": 0.91
+    }
+  ],
+  "missingMarkers": [
+    {
+      "area": "窗户下沿",
+      "missingType": "窗台离地高度",
+      "risk": "影响台面与窗帘盒设计复核",
+      "evidence": "窗户轮廓清晰但未见离地高度",
+      "confidence": 0.83
+    }
+  ],
+  "uncertainAreas": [
+    {
+      "area": "右上角立管",
+      "reason": "文字模糊,疑似烟道或排水管",
+      "action": "请量尺人员回看原图或现场照片",
+      "confidence": 0.55
+    }
+  ],
+  "overallJudgement": "存在一处关键缺标和一处待复核区域,建议补齐后再进入标准尺寸表。"
+}

+ 414 - 0
claude-code-measurement-marker-check/tools/measurement-marker-check-run.js

@@ -0,0 +1,414 @@
+#!/usr/bin/env node
+const fs = require('fs');
+const path = require('path');
+
+const DEFAULT_REQUIRED_MARKERS = [
+  '空间名称',
+  '关键尺寸',
+  '门窗洞口',
+  '梁柱/墙体限制',
+  '水电/管道/烟道',
+  '安装避让备注'
+];
+
+const SAMPLE_ANALYSIS = {
+  project: '实战二 P0 演示样本',
+  image: 'sample-measurement-plan.jpg',
+  sourceType: 'image',
+  detectedMarkers: [
+    {
+      area: '厨房右侧墙面',
+      markerType: '关键尺寸',
+      value: '3200mm',
+      evidence: '墙面横向尺寸已标注',
+      confidence: 0.9
+    },
+    {
+      area: '厨房水槽区域',
+      markerType: '水电/管道/烟道',
+      value: '下水管',
+      evidence: '右下角有管道符号和文字',
+      confidence: 0.82
+    }
+  ],
+  missingMarkers: [
+    {
+      area: '厨房左侧门洞',
+      missingType: '门洞宽度/高度',
+      risk: '门洞尺寸缺失,后续柜体和动线复核容易出现偏差',
+      evidence: '图中能看到门洞轮廓,但附近没有宽高数值',
+      confidence: 0.86
+    },
+    {
+      area: '窗户下沿',
+      missingType: '窗台高度',
+      risk: '窗台高度缺失,可能影响台面、吊柜或窗帘盒设计',
+      evidence: '窗户位置已画出,但没有离地高度标注',
+      confidence: 0.78
+    }
+  ],
+  uncertainAreas: [
+    {
+      area: '厨房右上角',
+      reason: '疑似烟道或立管,但图中文字不清晰',
+      action: '请现场照片或原始量尺单复核',
+      confidence: 0.58
+    }
+  ],
+  overallJudgement: '存在关键缺标记,建议人工复核后再进入标准尺寸表。'
+};
+
+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 readJsonFile(filePath) {
+  if (!filePath) return {};
+  const absolute = path.resolve(filePath);
+  if (!fs.existsSync(absolute)) {
+    throw new Error(`Input JSON not found: ${absolute}`);
+  }
+  return JSON.parse(fs.readFileSync(absolute, 'utf8'));
+}
+
+function parseJsonValue(value, fallback) {
+  if (!value) return fallback;
+  if (Array.isArray(value) || typeof value === 'object') return value;
+  return JSON.parse(String(value));
+}
+
+function asArray(value) {
+  return Array.isArray(value) ? value : [];
+}
+
+function normalizeInput(input = {}) {
+  const fromFile = readJsonFile(input.input || input['input-json']);
+  const detectedMarkers = parseJsonValue(input.detectedMarkers || input['detected-markers'], fromFile.detectedMarkers || []);
+  const missingMarkers = parseJsonValue(input.missingMarkers || input['missing-markers'], fromFile.missingMarkers || []);
+  const uncertainAreas = parseJsonValue(input.uncertainAreas || input['uncertain-areas'], fromFile.uncertainAreas || []);
+
+  return {
+    ...fromFile,
+    ...input,
+    sample: Boolean(input.sample || fromFile.sample),
+    project: input.project || fromFile.project,
+    image: input.image || input.imageUrl || input.imagePath || fromFile.image || fromFile.imageUrl || fromFile.imagePath,
+    imageUrl: input.imageUrl || input['image-url'] || fromFile.imageUrl,
+    imagePath: input.imagePath || input['image-path'] || fromFile.imagePath,
+    sourceType: input.sourceType || input['source-type'] || fromFile.sourceType,
+    requiredMarkers: splitList(input.requiredMarkers || input['required-markers']).length
+      ? splitList(input.requiredMarkers || input['required-markers'])
+      : fromFile.requiredMarkers,
+    detectedMarkers: asArray(detectedMarkers),
+    missingMarkers: asArray(missingMarkers),
+    uncertainAreas: asArray(uncertainAreas),
+    overallJudgement: input.overallJudgement || input['overall-judgement'] || fromFile.overallJudgement
+  };
+}
+
+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 }]
+  };
+}
+
+function normalizeConfidence(value) {
+  if (typeof value !== 'number' || Number.isNaN(value)) return 0.7;
+  return Math.max(0, Math.min(1, value));
+}
+
+function normalizeMarker(item = {}) {
+  return {
+    area: item.area || item.location || item.space || '未指明区域',
+    markerType: item.markerType || item.type || item.label || '未分类标记',
+    value: item.value || item.text || '',
+    evidence: item.evidence || item.reason || '',
+    confidence: normalizeConfidence(item.confidence)
+  };
+}
+
+function normalizeMissing(item = {}) {
+  return {
+    area: item.area || item.location || item.space || '未指明区域',
+    missingType: item.missingType || item.markerType || item.type || '未说明缺失项',
+    risk: item.risk || item.impact || '需要人工复核,避免后续尺寸整理漏项。',
+    evidence: item.evidence || item.reason || '',
+    confidence: normalizeConfidence(item.confidence)
+  };
+}
+
+function normalizeUncertain(item = {}) {
+  return {
+    area: item.area || item.location || item.space || '未指明区域',
+    reason: item.reason || item.evidence || '图像信息不足,暂无法确认。',
+    action: item.action || '建议回看原始量尺图或现场照片后复核。',
+    confidence: normalizeConfidence(item.confidence)
+  };
+}
+
+function countByType(items) {
+  return items.reduce((acc, item) => {
+    const key = item.markerType || item.missingType || '未分类';
+    acc[key] = (acc[key] || 0) + 1;
+    return acc;
+  }, {});
+}
+
+function buildMeasurementMarkerReport(input = {}) {
+  const source = input.sample ? SAMPLE_ANALYSIS : input;
+  const detectedMarkers = asArray(source.detectedMarkers).map(normalizeMarker);
+  const missingMarkers = asArray(source.missingMarkers).map(normalizeMissing);
+  const uncertainAreas = asArray(source.uncertainAreas).map(normalizeUncertain);
+  const requiredMarkers = asArray(source.requiredMarkers).length
+    ? source.requiredMarkers.map(String)
+    : DEFAULT_REQUIRED_MARKERS;
+  const status = missingMarkers.length ? 'needs_review' : 'ok';
+  const pass = status === 'ok' && uncertainAreas.length === 0;
+
+  const summary = {
+    project: source.project || '装修量尺图缺标记检查',
+    image: source.image || source.imageUrl || source.imagePath || '',
+    sourceType: source.sourceType || 'image',
+    pass,
+    status,
+    detectedCount: detectedMarkers.length,
+    missingCount: missingMarkers.length,
+    uncertainCount: uncertainAreas.length,
+    detectedByType: countByType(detectedMarkers),
+    missingByType: countByType(missingMarkers)
+  };
+
+  const assistantMessage = renderAssistantMessage({
+    summary,
+    requiredMarkers,
+    detectedMarkers,
+    missingMarkers,
+    uncertainAreas,
+    overallJudgement: source.overallJudgement
+  });
+
+  return {
+    summary,
+    data: {
+      requiredMarkers,
+      detectedMarkers,
+      missingMarkers,
+      uncertainAreas,
+      overallJudgement: source.overallJudgement || defaultJudgement(summary)
+    },
+    assistantMessage
+  };
+}
+
+function defaultJudgement(summary) {
+  if (summary.missingCount > 0) {
+    return '当前量尺图存在疑似未标记位置,建议先人工复核缺失项,再输出标准化尺寸表。';
+  }
+  if (summary.uncertainCount > 0) {
+    return '当前未发现明确缺标,但仍有图像不清晰区域,需要人工确认。';
+  }
+  return '当前未发现明显缺标记,可进入下一步标准化整理。';
+}
+
+function renderAssistantMessage({ summary, requiredMarkers, detectedMarkers, missingMarkers, uncertainAreas, overallJudgement }) {
+  const lines = [];
+  lines.push('## 量尺图缺标记检查(P0)');
+  lines.push('');
+  lines.push(`**结论**:${overallJudgement || defaultJudgement(summary)}`);
+  lines.push('');
+  lines.push(`- 已识别标记:${summary.detectedCount} 项`);
+  lines.push(`- 疑似缺标记:${summary.missingCount} 项`);
+  lines.push(`- 待确认区域:${summary.uncertainCount} 项`);
+  lines.push('');
+  lines.push('### 本轮检查口径');
+  requiredMarkers.forEach(item => lines.push(`- ${item}`));
+  lines.push('');
+
+  lines.push('### 疑似未标记位置');
+  if (missingMarkers.length) {
+    missingMarkers.forEach((item, index) => {
+      lines.push(`${index + 1}. ${item.area}:缺少「${item.missingType}」`);
+      lines.push(`   - 风险:${item.risk}`);
+      if (item.evidence) lines.push(`   - 图像依据:${item.evidence}`);
+      lines.push(`   - 置信度:${Math.round(item.confidence * 100)}%`);
+    });
+  } else {
+    lines.push('- 暂未发现明确缺标记。');
+  }
+  lines.push('');
+
+  if (uncertainAreas.length) {
+    lines.push('### 待人工复核区域');
+    uncertainAreas.forEach((item, index) => {
+      lines.push(`${index + 1}. ${item.area}:${item.reason}`);
+      lines.push(`   - 建议动作:${item.action}`);
+      lines.push(`   - 置信度:${Math.round(item.confidence * 100)}%`);
+    });
+    lines.push('');
+  }
+
+  if (detectedMarkers.length) {
+    lines.push('### 已识别标记摘录');
+    detectedMarkers.slice(0, 8).forEach((item, index) => {
+      const value = item.value ? `:${item.value}` : '';
+      const evidence = item.evidence ? `(${item.evidence})` : '';
+      lines.push(`${index + 1}. ${item.area} / ${item.markerType}${value}${evidence}`);
+    });
+    if (detectedMarkers.length > 8) {
+      lines.push(`- 另有 ${detectedMarkers.length - 8} 项已识别标记写入结构化结果。`);
+    }
+    lines.push('');
+  }
+
+  lines.push('### 下一步');
+  if (missingMarkers.length || uncertainAreas.length) {
+    lines.push('- 先让量尺/设计同事确认上述缺标或模糊区域。');
+    lines.push('- 复核后补齐尺寸、限制条件或备注,再生成标准化尺寸对照表。');
+  } else {
+    lines.push('- 可继续输出标准化尺寸对照表和异常预警清单。');
+  }
+
+  return lines.join('\n');
+}
+
+function writeMeasurementMarkerReport(outputDir, report) {
+  const absolute = path.resolve(outputDir || path.join('outputs', 'measurement-marker-check', new Date().toISOString().slice(0, 10)));
+  fs.mkdirSync(absolute, { recursive: true });
+  const jsonPath = path.join(absolute, 'measurement-marker-check-result.json');
+  const mdPath = path.join(absolute, 'measurement-marker-check-report.md');
+  fs.writeFileSync(jsonPath, JSON.stringify(report, null, 2), 'utf8');
+  fs.writeFileSync(mdPath, report.assistantMessage, 'utf8');
+  return [jsonPath, mdPath];
+}
+
+async function runMeasurementMarkerCheck(input = {}) {
+  const normalized = normalizeInput(input);
+  const outputDir = path.resolve(normalized.output || path.join('outputs', 'measurement-marker-check', new Date().toISOString().slice(0, 10)));
+
+  if (!normalized.sample && !normalized.image && !normalized.imageUrl && !normalized.imagePath) {
+    return errorResult('请提供量尺图片路径/URL,或使用 --sample 跑 P0 演示样例。', {
+      nextActions: [
+        '上传或传入一张量尺图',
+        'Claude 先基于图片输出 detectedMarkers / missingMarkers / uncertainAreas',
+        '再调用本工具生成 P0 缺标记报告'
+      ]
+    });
+  }
+
+  if (!normalized.sample && !normalized.detectedMarkers.length && !normalized.missingMarkers.length && !normalized.uncertainAreas.length) {
+    return {
+      status: 'needs_vision_observation',
+      assistantMessage: [
+        '已收到量尺图片入口,但还缺少看图后的结构化观察。',
+        '',
+        '请先让 Claude 直接观察上传的量尺图,提取:',
+        '- detectedMarkers:已标记的空间、尺寸、门窗、管道、梁柱、备注等',
+        '- missingMarkers:疑似未标记的位置、缺失类型、风险、图像依据、置信度',
+        '- uncertainAreas:图像模糊或无法确定的位置',
+        '',
+        '随后把这些结构化结果传给本工具,即可生成 P0 缺标记报告。'
+      ].join('\n'),
+      summary: {
+        image: normalized.image || normalized.imageUrl || normalized.imagePath,
+        needsVisionObservation: true
+      },
+      data: {
+        expectedInputShape: {
+          detectedMarkers: [{ area: '厨房右侧墙面', markerType: '关键尺寸', value: '3200mm', evidence: '墙面横向尺寸已标注', confidence: 0.9 }],
+          missingMarkers: [{ area: '窗户下沿', missingType: '窗台高度', risk: '影响台面或窗帘盒设计', evidence: '窗户有轮廓但未见离地高度', confidence: 0.78 }],
+          uncertainAreas: [{ area: '右上角', reason: '文字模糊', action: '人工复核原图', confidence: 0.58 }]
+        }
+      },
+      files: [],
+      nextActions: ['先进行图片视觉识别', '再调用 measurement marker check 工具生成报告'],
+      warnings: ['P0 阶段不在本地工具内直接调用视觉模型,由 Claude Code 视觉能力或上游识图服务提供观察结果。'],
+      errors: []
+    };
+  }
+
+  const report = buildMeasurementMarkerReport(normalized);
+  const files = writeMeasurementMarkerReport(outputDir, report);
+
+  return okResult({
+    assistantMessage: report.assistantMessage,
+    summary: report.summary,
+    data: report.data,
+    files,
+    nextActions: report.summary.missingCount || report.summary.uncertainCount
+      ? ['让量尺/设计同事补充缺标或模糊区域', '补齐后继续输出标准化尺寸对照表']
+      : ['继续输出标准化尺寸对照表', '可选:与人工标准表做差异对比'],
+    warnings: normalized.sample ? ['当前使用 P0 sample 演示数据。'] : []
+  });
+}
+
+async function main() {
+  const args = parseArgs(process.argv.slice(2));
+  try {
+    const result = await runMeasurementMarkerCheck(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' || result.status === 'needs_vision_observation' ? 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 = {
+  runMeasurementMarkerCheck,
+  buildMeasurementMarkerReport,
+  SAMPLE_ANALYSIS,
+  DEFAULT_REQUIRED_MARKERS
+};

+ 2 - 2
claude-code-voc-intelligence/.claude-plugin/plugin.json

@@ -1,7 +1,7 @@
 {
   "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",
+  "description": "VOC intelligence skills for Claude Code: end-to-end VOC business workflows, Xiaohongshu and Douyin trend reports, live social VOC collection, token/recharge checks, evidence-card outputs, prioritized issue pools, single-problem deep dives, competitor maps, 7-day content plans, VOC-backed speaking script co-creation, boss/operator action plans, and multi-turn memory.",
+  "version": "0.3.10",
   "author": {
     "name": "nkkj-BrainHack"
   }

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

@@ -2,4 +2,5 @@ node_modules/
 memory/
 outputs/
 .env
+.env.local
 .tmp/

+ 4 - 4
claude-code-voc-intelligence/.mcp.json

@@ -1,11 +1,11 @@
 {
   "mcpServers": {
-    "voc-intelligence": {
+    "voc": {
       "command": "node",
       "args": [
-        "./mcp/src/server.js"
-      ]
+        "E:\\workspace\\openclaw-voc-skill\\claude-code-voc-intelligence\\mcp\\src\\server.js"
+      ],
+      "cwd": "E:\\workspace\\openclaw-voc-skill\\claude-code-voc-intelligence"
     }
   }
 }
-

+ 267 - 82
claude-code-voc-intelligence/README.md

@@ -1,148 +1,333 @@
 # Claude Code VOC Intelligence
 
-Claude Code 原生版 VOC 技能包。
+面向 Claude Code 的 VOC 用户洞察技能包。它不是单纯的报告生成器,而是一套“采集真实用户声音 -> 看市场 -> 看竞品 -> 整理问题池 -> 深挖关键问题 -> 生成内容选题 -> 沉淀偏好记忆 -> 下一轮继续优化”的工作流。
+
+当前能力覆盖:
+
+- VOC 经营闭环:适合用户只说“看顾客关心什么、先改哪里、下周发什么”时,一次串起趋势、问题池、单点深挖、内容计划和口播稿。
+- 小红书趋势情报:适合看行业趋势、用户顾虑、内容选题和销售话术。
+- 抖音趋势情报:适合看短视频平台上的用户真实反馈、评论问题、内容机会和门店转化线索。
+- VOC 竞品图谱:适合在竞品不清楚时先找候选竞品,再判断别人被夸什么、被骂什么、我方怎么错位竞争。
+- VOC 问题池:适合把评论、报告证据或用户反馈整理成可排序、可跟进、可继续深挖的问题列表。
+- 单点 VOC 深挖:适合把一个具体问题转成老板能执行的经营动作、内容选题和 7 天验证计划。
+- VOC 内容选题:适合把用户问题转成 7 天短视频选题、口播脚本、镜头建议和验证指标。
+- VOC 口播脚本共创:适合把一条 VOC 选题继续打磨成 60 秒稿、30 秒稿、提词器版,并支持多轮改稿和定稿记忆。
+
+这套技能包不覆盖创始人故事、品牌自传或老板个人 IP 长文。口播脚本要做,但必须来自 VOC 用户问题、竞品评论差异、问题池 Top 问题或单点深挖动作。
+
+## 什么时候用
+
+用户说这些话时,Claude Code 应优先使用本技能包,而不是让用户自己设计提示词:
+
+- “帮我看看这个行业用户最近在关心什么”
+- “跑一份小红书/抖音趋势情报”
+- “先用演示样例跑通流程,不要真实采集”
+- “切到 live,真实采集一下”
+- “帮我看看竞品都在做什么”
+- “我不知道竞品是谁,帮我先做一份竞品图谱”
+- “这些评论里用户到底在吐槽什么”
+- “把这些评论整理成问题池”
+- “哪些问题最影响生意,先改哪个”
+- “这个问题老板该怎么解决”
+- “继续深挖排队/价格/服务/味道这个问题”
+- “下周账号发什么内容”
+- “根据这些顾客吐槽生成 7 天口播脚本”
+- “这个动作不适合我们,下次别推荐”
+- “这个有效,后面优先按这个方向来”
+- “帮我看看顾客关心什么,告诉我先改哪里、下周发什么”
+- “跑一遍完整 VOC 经营闭环”
+
+## 能交付什么
+
+第一轮趋势情报会输出:
+
+- 第一轮初步判断
+- 数据口径
+- 样本观察
+- 机会假设
+- 用户顾虑/决策阻力
+- 证据样本
+- 可继续验证的选题方向
+- 待确认问题
+
+VOC 问题池会输出:
+
+- 问题优先级
+- 每个问题的状态、证据数、影响环节和建议动作
+- 本周建议推进
+- 下一步深挖入口
+- 问题池记忆文件
+
+VOC 竞品图谱会输出:
+
+- 竞品候选清单
+- 用户可能被竞品吸引的点
+- 用户可能吐槽竞品的点
+- 我方错位机会
+- 可转成内容的选题方向
+- 下一步问题池入口
+
+单点 VOC 深挖会输出:
+
+- 一句话判断
+- 优先级判断
+- 用户原话证据
+- 表层问题 vs 深层问题
+- 老板视角影响
+- 今天就能做、7 天内验证、长期沉淀
+- 可转化内容选题
+- 7 天验证计划
+- 下轮验证指标
+- 已应用的使用记忆
+
+VOC 内容选题会输出:
+
+- 7 天内容表
+- 每天对应的用户问题
+- 前 3 秒钩子
+- 30-60 秒口播脚本
+- 镜头建议
+- 评论区引导
+- 验证指标
+
+## 安装
+
+推荐安装到当前工作区,适合 VSCode Claude Code:
+
+```powershell
+npx --yes @gangvy/claude-code-voc-intelligence@latest workspace --smoke
+```
+
+安装后会生成:
+
+```text
+.mcp.json
+.claude/skills/xiaohongshu-trend-intelligence/SKILL.md
+.claude/skills/douyin-trend-intelligence/SKILL.md
+.claude/skills/voc-issue-pool/SKILL.md
+.claude/skills/voc-problem-deep-dive/SKILL.md
+.claude/skills/voc-content-plan/SKILL.md
+.claude/skills/voc-speaking-script/SKILL.md
+.claude/skills/voc-competitor-map/SKILL.md
+.claude/skills/voc-business-workflow/SKILL.md
+.claude/plugins/voc-intelligence
+```
+
+也可以全局安装:
+
+```powershell
+npm install -g @gangvy/claude-code-voc-intelligence
+claude-voc install --smoke
+```
 
-当前第一阶段聚焦“小红书趋势情报官”,用于从小红书内容和评论中发现行业趋势、用户顾虑、设计元素、内容选题和待验证门店话术。
+或免全局安装:
 
-## 当前范围
+```powershell
+npx --yes @gangvy/claude-code-voc-intelligence@latest install --smoke
+```
 
-- `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 双交付方式的打包经验。
+```text
+帮我做一份本地生活餐饮行业的抖音趋势情报。
+先用演示样例跑通流程,不要真实采集。
+在聊天里给我第一轮样本观察和待确认问题。
+```
 
-## 技术路线
+问题池:
 
-第一阶段:
+```text
+把这些评论整理成问题池,告诉我哪些问题最影响生意,先改哪个。
+```
 
-- Claude Code Skill 负责自然语言工作流。
-- 本地工具模块负责生成 sample 趋势报告。
-- MCP tools 负责 sample/live 采集、趋势报告、偏好更新。
-- 输出统一返回 `assistantMessage`。
+单点 VOC 深挖:
 
-当前 MCP tools:
+```text
+继续深挖“排队”这个问题,站在老板视角告诉我怎么解决,给出今天能做的动作和 7 天验证计划。
+```
 
-- `voc_xiaohongshu_token_check`
-- `voc_xiaohongshu_trend_run`
-- `voc_xiaohongshu_preference_update`
+竞品图谱:
 
-## 本地测试
+```text
+我是南昌做烧烤的,不知道竞品是谁,帮我先做一份竞品图谱和错位竞争建议。
+```
 
-在仓库根目录执行:
+7 天内容选题
 
-```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
+```text
+根据“排队久、人均贵、第一次来怕踩雷”这些顾客问题,帮我生成 7 天短视频选题和口播脚本。
 ```
 
-真实采集前先配置小红书/TikHub token:
+多轮记忆反馈:
 
-```bash
-set XIAOHONGSHU_API_TOKEN=你的token
-npm --prefix claude-code-voc-intelligence run xhs:live
+```text
+这个店不想做等位饮品,更想先改评论区回复和预点单,后面按这个方向继续优化。
 ```
 
-如果没有 token,live 模式会返回 `needs_token`,不会误用 OpenClaw VOC token。
+## Claude Code 应如何调用
 
-## 打包交付
+优先使用技能入口:
 
-在仓库根目录执行:
+- `xiaohongshu-trend-intelligence`
+- `douyin-trend-intelligence`
+- `voc-issue-pool`
+- `voc-problem-deep-dive`
+- `voc-content-plan`
+- `voc-speaking-script`
+- `voc-competitor-map`
+- `voc-business-workflow`
 
-```bash
-npm run claude-voc:build
+MCP 工具:
+
+- `voc_business_workflow_run`
+- `voc_xiaohongshu_trend_run`
+- `voc_xiaohongshu_preference_update`
+- `voc_xiaohongshu_token_check`
+- `voc_douyin_trend_run`
+- `voc_douyin_preference_update`
+- `voc_douyin_token_check`
+- `voc_issue_pool_run`
+- `voc_problem_deep_dive_run`
+- `voc_content_plan_run`
+- `voc_competitor_map_run`
+- `voc_speaking_script_run`
+
+命令行入口:
+
+```powershell
+npm --prefix claude-code-voc-intelligence run xhs:sample
+npm --prefix claude-code-voc-intelligence run dy:sample
+npm --prefix claude-code-voc-intelligence run voc:issue-pool
+npm --prefix claude-code-voc-intelligence run voc:deep-dive
+npm --prefix claude-code-voc-intelligence run voc:business-workflow
+npm --prefix claude-code-voc-intelligence run mcp:smoke
+npm --prefix claude-code-voc-intelligence run smoke:package
 ```
 
-该命令会生成:
+更完整的能力索引见
 
-```text
-dist/claude-code-voc-intelligence.zip
-dist/claude-code-voc-intelligence-suite-manifest.json
-```
+- `docs/capability-map.md`
+- `docs/deliverables/trend-insight-report.md`
+- `docs/deliverables/competitor-map-report.md`
+- `docs/deliverables/voc-issue-pool-report.md`
+- `docs/deliverables/single-problem-action-plan.md`
+- `docs/deliverables/seven-day-content-plan.md`
+- `docs/live-manual-acceptance-checklist.md`
+- `docs/payment-package-links.md`
+- `docs/context-budget-implementation-plan.md`
+- `skills/voc-content-plan/references/restaurant-content-framework.md`
+- `skills/voc-content-plan/references/restaurant-speaking-script-patterns.md`
+- `skills/voc-competitor-map/references/restaurant-competitor-framework.md`
+- `skills/voc-competitor-map/references/restaurant-differentiation-patterns.md`
+
+## 工作坊输出口径
 
-并在临时目录里解压、重新安装依赖、跑 sample / preference / MCP smoke。
+面向老板或线下工作坊时,优先按这五段表达:
 
-用户拿到 zip 后的安装方式:
+1. 你这轮能拿到什么。
+2. 我会怎么做。
+3. 第一版结果。
+4. 你需要确认什么。
+5. 下一步建议。
 
-```bash
-node install.js
+每个模块都要对应一个交付物:趋势清单、竞品图谱、问题清单、单点行动计划、7 天内容选题。不要在前台讲 MCP、schema、提示词工程。
+
+## VOC 经营闭环
+
+当用户想一次拿到“市场真实声音 -> 先改哪个问题 -> 老板能做什么 -> 下周发什么 -> 第一条口播稿”时,优先使用:
+
+```text
+voc_business_workflow_run
 ```
 
-如果要顺手验证完整 sample 链路:
+典型话术
 
-```bash
-node install.js --smoke
+```text
+帮我看一下南昌餐饮最近顾客在关心什么,并告诉我先改哪里、下周发什么。
 ```
 
-`--smoke` 会输出简洁检查结果,不会把完整报告 JSON 打满屏幕。
+这条链路会自动串起趋势情报、VOC 问题池、单点深挖、7 天内容计划和口播脚本。第一轮仍然是初步判断,必须提示用户结合门店、销售、私域反馈校准
 
-## npm 安装
+## token 和充值口径
 
-全局安装:
+sample 模式不需要 token。live 模式需要 VOC-AI 数据服务 token。
 
-```bash
-npm install -g @gangvy/claude-code-voc-intelligence
-claude-voc install
-```
+允许读取 token 的位置:
 
-免全局安装:
+- 本次请求参数
+- 当前工作区 `.env.local`
+- 系统环境变量
+- `~/.claude/voc-credentials.json`
 
-```bash
-npx @gangvy/claude-code-voc-intelligence install
-```
+Claude Code 技能包不读取 `~/.openclaw/voc-credentials.json`。
 
-两种方式都会把技能包安装到:
+如果没有 token 或余额不足,应给用户充值/开通链接,不要暴露底层接口 403、权限堆栈或上游响应体:
 
 ```text
-~/.claude/plugins/voc-intelligence
+https://app.fmode.cn/dev/apig-pay/?apigid=Vo3ROWEvDy&fun_id=HOkkX72PMF
 ```
 
-安装后按提示用 `claude --plugin-dir` 加载即可。
+## 记忆能力
+
+趋势情报支持偏好记忆,适合记录“保留哪些方向、屏蔽哪些方向、下一轮关注什么”。
+
+问题池支持分桶记忆,适合记录同一客户/门店的已发现问题、状态和历史证据。
+连续更新同一个问题池时,会输出“问题变化”,对比上轮/本轮证据数,标记新增、变多、变少、持平或本轮未再出现的问题。
+
+单点 VOC 深挖支持分桶记忆。默认按 `project/brand/store + industry + scenario + audience` 隔离,避免不同客户、不同行业串味。
+当用户反馈“这个有效/无效”后,工具会记录动作验证次数,后续建议会优先推有效动作、降低无效动作,并解释为什么这样排序。
 
-npm 本地打包验证:
+示例
 
-```bash
-npm run claude-voc:npm-pack
+```text
+这个动作不适合我们,下次别推荐等位饮品。
+这个有效,后面优先做评论区回复和预点单。
 ```
 
-上传到 CDN 需要先配置七牛环境变量:
+工具会把反馈沉淀为偏好动作、屏蔽动作、验证有效动作或验证无效动作,让后续建议逐步贴近真实业务。
+
+## 发布前验证
 
-```bash
-set QINIU_AK=...
-set QINIU_SK=...
-npm run claude-voc:upload
+```powershell
+npm --prefix claude-code-voc-intelligence run mcp:smoke
+npm --prefix claude-code-voc-intelligence run smoke:package
+npm run claude-voc:build
+npm run claude-voc:npm-publish
 ```
 
-Claude Code 插件开发态加载:
+真实采集不默认放入自动 smoke。发布前或课程现场前,按 `docs/live-manual-acceptance-checklist.md` 做一次小规模 live 验收。
+## VOC 口播脚本共创
+
+`voc-content-plan` 负责把 VOC 问题转成 7 天内容计划;`voc-speaking-script` 负责把其中一条选题继续打磨成能拍的口播稿。
 
-```bash
-claude --plugin-dir ./claude-code-voc-intelligence
+典型话术:
+
+```text
+选题 2,进入脚本共创。
 ```
 
-进入 Claude Code 后可直接调用:
+```text
+开头更狠一点,老板视角,少讲概念,多给一个真实场景。
+```
 
 ```text
-/voc-intelligence:xiaohongshu-trend-intelligence
+定稿,并把这个风格记住。以后少讲工具清单,多讲老板判断。
 ```
 
-MCP server 启动命令
+对应 MCP 工具
 
-```bash
-npm --prefix claude-code-voc-intelligence run mcp:start
+```text
+voc_speaking_script_run
 ```
 
+它会输出 60 秒稿、30 秒压缩版、提词器版、标题备选、镜头建议、评论区引导和质量门槛,并把“保留/不要/定稿”等反馈写入脚本记忆。完整的抖音日更、对标账号监控、逐字稿拆解仍属于 `douyin-speaking-daily` 的深度能力;本包提供的是 VOC 到口播共创的轻量出口。

+ 598 - 32
claude-code-voc-intelligence/bin/claude-voc.js

@@ -5,56 +5,83 @@ const path = require('path');
 const { spawnSync } = require('child_process');
 
 const SOURCE_ROOT = path.resolve(__dirname, '..');
+const WORKSPACE_ROOT = process.cwd();
 const DEFAULT_TARGET = path.join(os.homedir(), '.claude', 'plugins', 'voc-intelligence');
+const WORKSPACE_TARGET = path.join(WORKSPACE_ROOT, '.claude', 'plugins', 'voc-intelligence');
+const WORKSPACE_PLUGINS_ROOT = path.join(WORKSPACE_ROOT, '.claude', 'plugins');
 const MIN_NODE_MAJOR = 18;
+
 const EXCLUDED_DIRS = new Set([
   'node_modules',
   'memory',
   'outputs',
   '.tmp',
-  '.git'
+  '.git',
+  '.claude'
 ]);
+
 const EXCLUDED_FILES = new Set([
   '.env',
+  '.env.local',
   '.npmrc'
 ]);
 
 function usage() {
   return [
-    'Claude VOC 技能包安装工具',
+    'Claude VOC skill package installer',
     '',
-    '用法:',
+    'Usage:',
     '  claude-voc install',
-    '  claude-voc install --smoke',
+    '  claude-voc workspace',
+    '  claude-voc install --workspace --smoke',
     '  claude-voc check',
     '  claude-voc smoke',
     '  claude-voc path',
     '',
-    '也可以免全局安装:',
+    'npx:',
     '  npx @gangvy/claude-code-voc-intelligence install',
+    '  npx @gangvy/claude-code-voc-intelligence workspace --smoke',
     '',
-    '参数:',
-    '  --target <dir>    指定安装目录,默认 ~/.claude/plugins/voc-intelligence',
-    '  --skip-install    复制后跳过 npm install',
-    '  --smoke           安装后跑 sample / 偏好记忆 / MCP smoke',
-    '  --help, -h        显示帮助'
+    'Options:',
+    '  --workspace      Install into ./.claude/plugins/voc-intelligence',
+    '  --target <dir>   Install into a custom directory',
+    '  --force          Allow overwriting an existing custom target',
+    '  --skip-install   Skip npm install after copying files',
+    '  --smoke          Run sample/preference/MCP smoke checks after install',
+    '  --help, -h       Show help'
   ].join('\n');
 }
 
+function expandHome(value) {
+  return value.replace(/^~(?=$|[\\/])/, os.homedir());
+}
+
 function parseArgs(argv) {
+  const first = argv[0] && !argv[0].startsWith('--') ? argv[0] : 'install';
   const opts = {
-    command: argv[0] && !argv[0].startsWith('--') ? argv[0] : 'install',
+    command: first,
     target: DEFAULT_TARGET,
     skipInstall: false,
     smoke: false,
+    force: false,
     help: false
   };
-  for (let i = opts.command === argv[0] ? 1 : 0; i < argv.length; i++) {
+
+  if (first === 'workspace' || first === 'install-workspace') {
+    opts.command = 'install';
+    opts.target = WORKSPACE_TARGET;
+  }
+
+  for (let i = first === 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 === '--workspace') {
+      opts.target = WORKSPACE_TARGET;
+    } else if (token === '--force') {
+      opts.force = true;
     } else if (token === '--skip-install') {
       opts.skipInstall = true;
     } else if (token === '--smoke') {
@@ -63,7 +90,8 @@ function parseArgs(argv) {
       opts.help = true;
     }
   }
-  opts.target = path.resolve(opts.target.replace(/^~(?=$|[\\/])/, os.homedir()));
+
+  opts.target = path.resolve(expandHome(opts.target));
   return opts;
 }
 
@@ -86,7 +114,7 @@ function run(command, args, cwd) {
 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}+`);
+    throw new Error(`Node.js ${MIN_NODE_MAJOR}+ is required. Current version: ${process.version}`);
   }
 }
 
@@ -94,10 +122,23 @@ function ensureDir(dirPath) {
   fs.mkdirSync(dirPath, { recursive: true });
 }
 
+function isInside(parentDir, childDir) {
+  const relative = path.relative(path.resolve(parentDir), path.resolve(childDir));
+  return relative === '' || (!!relative && !relative.startsWith('..') && !path.isAbsolute(relative));
+}
+
 function isSafeDefaultTarget(targetDir) {
   return path.resolve(targetDir) === path.resolve(DEFAULT_TARGET);
 }
 
+function isSafeWorkspaceTarget(targetDir) {
+  return isInside(WORKSPACE_PLUGINS_ROOT, targetDir);
+}
+
+function canOverwriteTarget(targetDir, opts) {
+  return opts.force || isSafeDefaultTarget(targetDir) || isSafeWorkspaceTarget(targetDir);
+}
+
 function shouldCopyEntry(entry) {
   if (entry.isDirectory()) return !EXCLUDED_DIRS.has(entry.name);
   if (entry.isFile()) {
@@ -133,51 +174,573 @@ function assertInstalled(targetDir) {
     '.claude-plugin/plugin.json',
     '.mcp.json',
     'skills/xiaohongshu-trend-intelligence/SKILL.md',
+    'skills/douyin-trend-intelligence/SKILL.md',
+    'skills/voc-issue-pool/SKILL.md',
+    'skills/voc-problem-deep-dive/SKILL.md',
+    'skills/voc-content-plan/SKILL.md',
+    'skills/voc-speaking-script/SKILL.md',
+    'skills/voc-competitor-map/SKILL.md',
+    'skills/voc-business-workflow/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(', ')}`);
+    throw new Error(`Missing required files in install target: ${missing.join(', ')}`);
   }
 }
 
+function writeMcpConfig(targetDir) {
+  const serverPath = path.join(targetDir, 'mcp', 'src', 'server.js');
+  const mcpConfig = {
+    mcpServers: {
+      voc: {
+        command: 'node',
+        args: [serverPath],
+        cwd: targetDir
+      }
+    }
+  };
+  fs.writeFileSync(path.join(targetDir, '.mcp.json'), `${JSON.stringify(mcpConfig, null, 2)}\n`, 'utf8');
+}
+
+function readJsonIfExists(filePath) {
+  if (!fs.existsSync(filePath)) return {};
+  return JSON.parse(fs.readFileSync(filePath, 'utf8'));
+}
+
+function writeWorkspaceMcpConfig(targetDir) {
+  const mcpConfigPath = path.join(WORKSPACE_ROOT, '.mcp.json');
+  const existing = readJsonIfExists(mcpConfigPath);
+  const mcpConfig = {
+    ...existing,
+    mcpServers: {
+      ...(existing.mcpServers || {}),
+      voc: {
+        command: 'node',
+        args: [path.join(targetDir, 'mcp', 'src', 'server.js')],
+        cwd: targetDir
+      }
+    }
+  };
+  fs.writeFileSync(mcpConfigPath, `${JSON.stringify(mcpConfig, null, 2)}\n`, 'utf8');
+}
+
+function asForwardSlash(filePath) {
+  return path.resolve(filePath).replace(/\\/g, '/');
+}
+
+function quotedPath(filePath) {
+  return `"${asForwardSlash(filePath)}"`;
+}
+
+function workspaceTrendSkillText(options) {
+  const trendRunner = path.join(options.targetDir, 'mcp', 'src', 'tools', options.trendTool);
+  const preferenceUpdater = path.join(options.targetDir, 'mcp', 'src', 'tools', options.preferenceTool);
+  const profilePath = path.join(options.targetDir, 'memory-templates', options.profileFile);
+  const defaultMemoryPath = path.join(WORKSPACE_ROOT, 'outputs', options.memoryDir, options.memoryFile);
+  const liveLimits = options.platform === '抖音'
+    ? '--keyword-limit 1 --videos-per-keyword 1 --max-comment-pages 1'
+    : '--keyword-limit 1 --notes-per-keyword 1 --max-comment-pages 1';
+
+  return [
+    '---',
+    `name: ${options.skillName}`,
+    `description: ${options.description}`,
+    'allowed-tools: Read Write Bash(node *)',
+    '---',
+    '',
+    `# ${options.platform}趋势情报官`,
+    '',
+    `你是面向业务用户的“${options.platform}趋势情报官”。用户要的是第一轮样本观察、机会假设和待确认问题,不是技术日志。`,
+    '',
+    '## 必须执行',
+    '',
+    `- 用户要求${options.platform}趋势情报、内容趋势、评论分析、真实采集、live采集或社媒 VOC 时,必须运行本技能包的工具或 MCP,不要只凭经验回答。`,
+    '- 用户说“live / 真实采集 / 进入 live / 实时采集 / 抓真实数据”时,必须运行 live 模式工具。不要回复“我不能实时抓取外部数据”。本技能包已经提供本地 VOC 采集入口。',
+    '- live 模式返回 `needs_token`、`needs_recharge` 或 `needs_valid_token` 时,必须把工具输出里的 `assistantMessage` 原样转述给用户,尤其不能省略充值/开通链接。',
+    '- 如果工具输出里有 `nextActions`,至少转述其中带 `apig-pay` 的充值链接。',
+    '- 第一轮只说“初步判断 / 机会假设 / 待校准”,不要说成最终结论。',
+    '- 后续用户说“保留、不要、更偏、这个不准”等反馈时,运行偏好更新命令沉淀记忆。',
+    '',
+    '## MANDATORY Live Rule',
+    '',
+    '- If the user asks for live collection, real collection, or entering live mode, run the local VOC tool in live mode.',
+    '- Do not answer that you cannot fetch external data. The local VOC tool is the approved collection entry.',
+    '- If the tool returns `needs_token`, `needs_recharge`, or `needs_valid_token`, paste its `assistantMessage` to the user and keep the payment URL visible.',
+    '- The payment URL must be shown when there is no token or insufficient balance: `https://app.fmode.cn/dev/apig-pay/?apigid=Vo3ROWEvDy&fun_id=HOkkX72PMF`',
+    '',
+    '## sample 命令',
+    '',
+    '```bash',
+    `node ${quotedPath(trendRunner)} --collection-mode sample --profile ${quotedPath(profilePath)} --output ${JSON.stringify(options.sampleOutput)} --assistant-message-only`,
+    '```',
+    '',
+    '## live 命令',
+    '',
+    '```bash',
+    `node ${quotedPath(trendRunner)} --collection-mode live --profile ${quotedPath(profilePath)} --output ${JSON.stringify(options.liveOutput)} ${liveLimits} --assistant-message-only`,
+    '```',
+    '',
+    '运行后把命令输出正文直接发给用户。不要展示 JSON、summary、data、files 或调试日志。',
+    '',
+    '## 偏好更新命令',
+    '',
+    '```bash',
+    `node ${quotedPath(preferenceUpdater)} --message "<用户反馈原文>" --memory ${quotedPath(defaultMemoryPath)} --result-prefix ${options.resultPrefix}`,
+    '```',
+    '',
+    '## 充值链接口径',
+    '',
+    '没有 token 或余额不足时,用户必须看到:',
+    '',
+    '```text',
+    'https://app.fmode.cn/dev/apig-pay/?apigid=Vo3ROWEvDy&fun_id=HOkkX72PMF',
+    '```',
+    '',
+    '套餐入口可作为补充:',
+    '',
+    '```text',
+    'https://app.fmode.cn/dev/apig-pay/#/workshop/99',
+    '```',
+    ''
+  ].join('\n');
+}
+
+function workspaceSkillText(targetDir) {
+  return workspaceTrendSkillText({
+    targetDir,
+    skillName: 'xiaohongshu-trend-intelligence',
+    platform: '小红书',
+    description: 'Build Xiaohongshu trend intelligence reports. Use when the user asks for 小红书趋势情报、行业趋势、内容选题、销售话术、真实采集、live采集、用户顾虑 or social VOC analysis.',
+    trendTool: 'xiaohongshu-trend-run.js',
+    preferenceTool: 'xiaohongshu-preference-update.js',
+    profileFile: 'xiaohongshu-trend-profile.json',
+    memoryDir: 'claude-code-xhs-memory',
+    memoryFile: 'xiaohongshu-trend-memory.json',
+    sampleOutput: 'outputs/claude-code-xhs-sample',
+    liveOutput: 'outputs/claude-code-xhs-live',
+    resultPrefix: 'XHS_PREF_RESULT'
+  });
+  const trendRunner = path.join(targetDir, 'mcp', 'src', 'tools', 'xiaohongshu-trend-run.js');
+  const preferenceUpdater = path.join(targetDir, 'mcp', 'src', 'tools', 'xiaohongshu-preference-update.js');
+  const profilePath = path.join(targetDir, 'memory-templates', 'xiaohongshu-trend-profile.json');
+  const defaultMemoryPath = path.join(WORKSPACE_ROOT, 'outputs', 'claude-code-xhs-memory', 'xiaohongshu-trend-memory.json');
+
+  return [
+    '---',
+    '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 *)',
+    '---',
+    '',
+    '# 小红书趋势情报官',
+    '',
+    '你是面向业务用户的“小红书趋势情报官”。用户要的是第一轮样本观察、机会假设和待确认问题,不是技术日志。',
+    '',
+    '## 必须执行',
+    '',
+    '- 当用户要求做小红书趋势情报、行业趋势、女性客户消费决策、家装全屋定制趋势、设计元素洞察或内容选题时,优先运行下面的 sample 工具命令。',
+    '- 不要只凭经验回答,也不要只返回文件路径。',
+    '- 工具输出里会有 `assistantMessage`,请把它的正文直接发给用户。',
+    '- 第一轮只能说“初步判断 / 机会假设 / 待校准”,不要说成最终结论。',
+    '- 后续用户说“保留、不要、更偏、这个不准”等反馈时,运行偏好更新命令沉淀记忆。',
+    '',
+    '## sample 趋势情报命令',
+    '',
+    '```bash',
+    `node ${quotedPath(trendRunner)} --collection-mode sample --profile ${quotedPath(profilePath)} --output "outputs/claude-code-xhs-sample" --assistant-message-only`,
+    '```',
+    '',
+    '运行后把命令输出正文直接发给用户。不要展示 JSON、summary、data、files 或调试日志。',
+    '',
+    '## 偏好更新命令',
+    '',
+    '```bash',
+    `node ${quotedPath(preferenceUpdater)} --message "<用户反馈原文>" --memory ${quotedPath(defaultMemoryPath)} --result-prefix XHS_PREF_RESULT`,
+    '```',
+    '',
+    '## 推荐用户启动话术',
+    '',
+    '```text',
+    '帮我做一份家装全屋定制行业的小红书趋势情报。',
+    '先用演示样例跑通流程,不要真实采集。',
+    '在聊天里给我第一轮样本观察和待确认问题。',
+    '```',
+    ''
+  ].join('\n');
+}
+
+function workspaceDouyinSkillText(targetDir) {
+  return workspaceTrendSkillText({
+    targetDir,
+    skillName: 'douyin-trend-intelligence',
+    platform: '抖音',
+    description: 'Build Douyin trend intelligence reports. Use when the user asks for 抖音趋势情报、视频评论、口播选题、短视频开头、内容趋势、真实采集、live采集、用户顾虑 or social VOC analysis.',
+    trendTool: 'douyin-trend-run.js',
+    preferenceTool: 'douyin-preference-update.js',
+    profileFile: 'douyin-trend-profile.json',
+    memoryDir: 'claude-code-douyin-memory',
+    memoryFile: 'douyin-trend-memory.json',
+    sampleOutput: 'outputs/claude-code-douyin-sample',
+    liveOutput: 'outputs/claude-code-douyin-live',
+    resultPrefix: 'DOUYIN_PREF_RESULT'
+  });
+  const trendRunner = path.join(targetDir, 'mcp', 'src', 'tools', 'douyin-trend-run.js');
+  const preferenceUpdater = path.join(targetDir, 'mcp', 'src', 'tools', 'douyin-preference-update.js');
+  const profilePath = path.join(targetDir, 'memory-templates', 'douyin-trend-profile.json');
+  const defaultMemoryPath = path.join(WORKSPACE_ROOT, 'outputs', 'claude-code-douyin-memory', 'douyin-trend-memory.json');
+
+  return [
+    '---',
+    'name: douyin-trend-intelligence',
+    'description: Build Douyin trend intelligence reports from industry direction, audience, keywords, videos, and comments. Use when the user asks for 抖音趋势情报、视频评论、口播选题、短视频开头、内容趋势 or social VOC analysis.',
+    'allowed-tools: Read Write Bash(node *)',
+    '---',
+    '',
+    '# 抖音趋势情报官',
+    '',
+    '你是面向业务用户的“抖音趋势情报官”。用户要的是第一轮样本观察、机会假设和待确认问题,不是技术日志。',
+    '',
+    '## 必须执行',
+    '',
+    '- 当用户要求做抖音趋势情报、视频评论、口播选题、短视频开头或内容趋势时,优先运行下面的 sample 工具命令。',
+    '- 不要只凭经验回答,也不要只返回文件路径。',
+    '- 工具输出里会有 `assistantMessage`,请把它的正文直接发给用户。',
+    '- 第一轮只能说“初步判断 / 机会假设 / 待校准”,不要说成最终结论。',
+    '- 后续用户说“保留、不要、更偏、这个不准”等反馈时,运行偏好更新命令沉淀记忆。',
+    '',
+    '## sample 趋势情报命令',
+    '',
+    '```bash',
+    `node ${quotedPath(trendRunner)} --collection-mode sample --profile ${quotedPath(profilePath)} --output "outputs/claude-code-douyin-sample" --assistant-message-only`,
+    '```',
+    '',
+    '运行后把命令输出正文直接发给用户。不要展示 JSON、summary、data、files 或调试日志。',
+    '',
+    '## 偏好更新命令',
+    '',
+    '```bash',
+    `node ${quotedPath(preferenceUpdater)} --message "<用户反馈原文>" --memory ${quotedPath(defaultMemoryPath)} --result-prefix DOUYIN_PREF_RESULT`,
+    '```',
+    '',
+    '## 推荐用户启动话术',
+    '',
+    '```text',
+    '帮我做一份家装全屋定制行业的抖音趋势情报。',
+    '先用演示样例跑通流程,不要真实采集。',
+    '在聊天里给我第一轮样本观察和待确认问题。',
+    '```',
+    ''
+  ].join('\n');
+}
+
+function workspaceProblemDeepDiveSkillText(targetDir) {
+  const deepDiveRunner = path.join(targetDir, 'mcp', 'src', 'tools', 'voc-problem-deep-dive-run.js');
+  return [
+    '---',
+    'name: voc-problem-deep-dive',
+    'description: Deep-dive a single VOC problem into root cause, boss/operator impact, concrete actions, content ideas, and validation metrics. Use when the user says 继续挖、深入分析、老板怎么解决、这个问题怎么办、怎么改门店、怎么转成内容、单点VOC or VOC深挖.',
+    'allowed-tools: Read Write Bash(node *)',
+    '---',
+    '',
+    '# 单点 VOC 深挖官',
+    '',
+    '你是面向老板和门店经营者的“单点 VOC 深挖官”。用户不需要学习提示词。用户只要说“这个问题继续挖”“老板怎么解决”“这个差评背后是什么”,你就要把真实用户声音转成可执行经营动作。',
+    '',
+    '## 必须执行',
+    '',
+    '- 当用户说“继续挖、深入分析、老板怎么解决、这个问题怎么办、怎么改、怎么转成内容、单点 VOC”时,必须运行单点深挖工具。',
+    '- 不要输出提示词教学,不要让用户自己设计分析维度。',
+    '- 输出必须站在老板视角:这件事影响新客、复购、客单、口碑,还是现场效率。',
+    '- 每次只深挖一个问题点。如果用户给了多个问题,先选择最影响转化的一个。',
+    '- 如果上一轮趋势报告里有证据样本或高赞评论,要把相关评论作为 evidence 传入工具。',
+    '- 要使用记忆能力:用户说“这个动作不适合”“我们已经试过”“这个有效”“下次更偏内容/门店动作”时,把原话作为 `feedback` 传入;如果能明确识别,也同步传 `blockedActions`、`validatedActions`、`rejectedActions` 或 `preferredActions`。',
+    '- 后续多轮迭代时要带上 `project`/`brand`/`store`、`industry`、`scenario`、`audience`。未显式传 `memory` 时,工具会按这些字段自动隔离记忆,避免不同行业、不同客户串味。',
+    '- 如果用户没有指定记忆文件,不要强行要求用户理解路径;让工具使用默认分桶记忆即可。只有在同一个客户需要固定沉淀时,才显式传同一个 `memory` 路径。',
+    '',
+    '## 命令',
+    '',
+    '```bash',
+    `node ${quotedPath(deepDiveRunner)} --issue "排队" --industry "本地生活餐饮" --scenario "门店经营" --audience "到店顾客" --assistant-message-only`,
+    '```',
+    '',
+    '## 带记忆的多轮迭代',
+    '',
+    '```bash',
+    `node ${quotedPath(deepDiveRunner)} --issue "排队" --industry "本地生活餐饮" --scenario "门店经营" --audience "到店顾客" --memory "outputs/voc-problem-memory.json" --feedback "老板不想做等位饮品,更想先改评论区回复和预点单" --blocked-actions "等位饮品" --preferred-actions "评论区回复,预点单" --assistant-message-only`,
+    '```',
+    '',
+    '自然语言反馈也可以直接传入 `feedback`,工具会尝试自动提取偏好和屏蔽动作:',
+    '',
+    '```bash',
+    `node ${quotedPath(deepDiveRunner)} --issue "排队" --project "样板门店A" --industry "本地生活餐饮" --scenario "门店经营" --audience "到店顾客" --feedback "这个店不想做等位饮品,更想先改评论区回复和预点单" --assistant-message-only`,
+    '```',
+    '',
+    '运行后把命令输出正文直接发给用户。不要展示 JSON、summary、data、files 或调试日志。',
+    ''
+  ].join('\n');
+}
+
+function workspaceIssuePoolSkillText(targetDir) {
+  const issuePoolRunner = path.join(targetDir, 'mcp', 'src', 'tools', 'voc-issue-pool-run.js');
+  return [
+    '---',
+    'name: voc-issue-pool',
+    'description: Turn VOC comments, report evidence, or user feedback into a prioritized issue pool with status, evidence, business impact, next actions, and follow-up deep-dive suggestions. Use when the user asks 哪些问题最影响生意、问题池、问题清单、用户都在吐槽什么、先改哪个、VOC问题管理、问题优先级、把评论整理成问题.',
+    'allowed-tools: Read Write Bash(node *)',
+    '---',
+    '',
+    '# VOC 问题池官',
+    '',
+    '你是面向老板、门店经营者和营销负责人的 VOC 问题池官。用户不需要知道分类法,也不需要学习提示词。用户只要说“这些评论里哪些问题最重要”“先改哪个”“整理成问题池”,你就把真实用户声音整理成可排序、可跟进、可继续深挖的问题列表。',
+    '',
+    '## 必须执行',
+    '',
+    '- 当用户说“问题池、问题清单、用户吐槽什么、哪些问题最影响生意、先改哪个、VOC 问题管理、把评论整理成问题”时,优先运行 `voc_issue_pool_run`。',
+    '- 如果上一轮小红书/抖音报告里有证据样本或高赞评论,把相关评论作为 `evidence`、`evidenceText` 或 `reportPath` 传入。',
+    '- 输出必须站在业务视角:这个问题影响新客、复购、客单、口碑、内容信任,还是现场效率。',
+    '- 不要只做词频统计。每个问题都要包含证据样本、影响环节、建议动作和下一步。',
+    '- 如果用户说某个问题已解决、验证中、暂缓,要通过 `resolvedIssues`、`validatingIssues`、`blockedIssues` 或 `statusUpdates` 更新问题状态。',
+    '- 后续深挖时,引导用户直接说“继续深挖「问题名」”,然后转入 `voc-problem-deep-dive`。',
+    '',
+    '## 命令',
+    '',
+    '```bash',
+    `node ${quotedPath(issuePoolRunner)} --project "样板门店A" --industry "本地生活餐饮" --scenario "门店经营" --audience "到店顾客" --evidence "排队久,但是不知道值不值得等。,人均有点高,第一次来不知道怎么点不踩雷。,服务忙起来没人理。" --assistant-message-only`,
+    '```',
+    '',
+    '带报告文件:',
+    '',
+    '```bash',
+    `node ${quotedPath(issuePoolRunner)} --project "样板门店A" --industry "本地生活餐饮" --report "outputs/douyin-trend-report.md" --assistant-message-only`,
+    '```',
+    '',
+    '运行后把命令输出正文直接发给用户。不要展示 JSON、summary、data、files 或调试日志。',
+    ''
+  ].join('\n');
+}
+
+function workspaceContentPlanSkillText(targetDir) {
+  const runner = path.join(targetDir, 'mcp', 'src', 'tools', 'voc-content-plan-run.js');
+  return [
+    '---',
+    'name: voc-content-plan',
+    'description: Turn VOC issues into a 7-day content plan with short-video topics and speaking scripts. Use when the user asks 下周账号发什么、口播脚本、短视频选题、把用户问题变成内容、7天内容计划.',
+    'allowed-tools: Read Write Bash(node *)',
+    '---',
+    '',
+    '# VOC 内容选题官',
+    '',
+    '当用户说“下周发什么、7 天选题、口播脚本、短视频脚本、账号内容、把问题变成内容”时,运行本工具。',
+    '',
+    '必须把内容绑定到 VOC 用户问题,不要输出空泛爆款标题。不要覆盖创始人故事。',
+    '',
+    '## 命令',
+    '',
+    '```bash',
+    `node ${quotedPath(runner)} --brand "样板门店A" --industry "本地生活餐饮" --issues "第一次来怕踩雷;觉得人均贵;怕排队久" --assistant-message-only`,
+    '```',
+    '',
+    '运行后把正文直接发给用户,不要展示 JSON。',
+    ''
+  ].join('\n');
+}
+
+function workspaceSpeakingScriptSkillText(targetDir) {
+  const runner = path.join(targetDir, 'mcp', 'src', 'tools', 'voc-speaking-script-run.js');
+  return [
+    '---',
+    'name: voc-speaking-script',
+    'description: Turn one VOC-backed topic into a co-created speaking script with 60-second draft, 30-second compressed version, teleprompter text, revision feedback, finalization, and script memory. Use when the user says 选题2进入脚本共创、改稿、定稿、口播稿、提词器、开头更狠、老板视角、少讲概念.',
+    'allowed-tools: Read Write Bash(node *)',
+    '---',
+    '',
+    '# VOC 口播脚本共创',
+    '',
+    '当用户从 7 天内容计划里选择某条选题,或说“进入脚本共创、改稿、定稿、提词器版、开头更狠、老板视角”时,运行本工具。',
+    '',
+    '口播必须来自 VOC 用户问题、趋势证据、问题池 Top 问题、单点深挖动作或竞品评论差异。没有真实证据时要标注低证据风险。',
+    '',
+    '## 命令',
+    '',
+    '```bash',
+    `node ${quotedPath(runner)} --brand "样板门店A" --industry "本地生活餐饮" --topic "第一次来怎么点不踩雷" --user-issue "第一次来怕点错" --evidence "第一次来不知道怎么点,怕踩雷" --assistant-message-only`,
+    '```',
+    '',
+    '改稿:',
+    '',
+    '```bash',
+    `node ${quotedPath(runner)} --brand "样板门店A" --topic "第一次来怎么点不踩雷" --user-issue "第一次来怕点错" --evidence "第一次来不知道怎么点,怕踩雷" --feedback "开头更狠一点,老板视角,少讲概念,多给具体场景" --assistant-message-only`,
+    '```',
+    '',
+    '定稿时传 `--finalize true`,工具会写入脚本记忆。运行后把正文直接发给用户,不要展示 JSON。',
+    ''
+  ].join('\n');
+}
+
+function workspaceCompetitorMapSkillText(targetDir) {
+  const runner = path.join(targetDir, 'mcp', 'src', 'tools', 'voc-competitor-map-run.js');
+  return [
+    '---',
+    'name: voc-competitor-map',
+    'description: Build a competitor map and differentiated opportunity report. Use when the user asks 看竞品、竞品分析、错位竞争、别人为什么吸引顾客、我不知道竞品是谁.',
+    'allowed-tools: Read Write Bash(node *)',
+    '---',
+    '',
+    '# VOC 竞品图谱官',
+    '',
+    '当用户说“看竞品、竞品分析、错位竞争、别人怎么做、我不知道竞品是谁”时,运行本工具。',
+    '',
+    '如果用户没有明确竞品,先按同城、同品类、同价格带、同消费场景、平台声量生成候选竞品类型。不要覆盖创始人故事。',
+    '',
+    '## 命令',
+    '',
+    '```bash',
+    `node ${quotedPath(runner)} --brand "样板门店A" --city "南昌" --category "餐饮" --assistant-message-only`,
+    '```',
+    '',
+    '运行后把正文直接发给用户,不要展示 JSON。',
+    ''
+  ].join('\n');
+}
+
+function workspaceBusinessWorkflowSkillText(targetDir) {
+  const runner = path.join(targetDir, 'mcp', 'src', 'tools', 'voc-business-workflow-run.js');
+  return [
+    '---',
+    'name: voc-business-workflow',
+    'description: Run the full VOC business workflow from market voices to issue pool, top issue action, 7-day content plan, and one speaking script. Use when the user asks 看顾客关心什么、先改哪里、下周发什么、完整VOC闭环、经营诊断.',
+    'allowed-tools: Read Write Bash(node *)',
+    '---',
+    '',
+    '# VOC经营闭环',
+    '',
+    '当用户想一次拿到“看市场、看问题、找动作、做内容”的结果时,运行本工具。不要让用户理解 MCP、schema 或多个工具名。',
+    '',
+    '## 命令',
+    '',
+    '```bash',
+    `node ${quotedPath(runner)} --brand "样板门店A" --industry "本地生活餐饮" --platform douyin --collection-mode sample --keywords "南昌菜,第一次来怎么点,排队" --assistant-message-only`,
+    '```',
+    '',
+    '真实采集时把 `--collection-mode sample` 换成 `--collection-mode live`。如果工具提示没有 token、余额不足或没有样本,直接把工具正文发给用户,并保留充值/降级建议。',
+    '',
+    '## 交付物',
+    '',
+    '必须包含五段:市场和用户声音、先改哪个问题、Top问题怎么解决、下周账号发什么、第一条口播稿。',
+    ''
+  ].join('\n');
+}
+
+function writeWorkspaceSkillEntries(targetDir) {
+  const xhsSkillDir = path.join(WORKSPACE_ROOT, '.claude', 'skills', 'xiaohongshu-trend-intelligence');
+  const douyinSkillDir = path.join(WORKSPACE_ROOT, '.claude', 'skills', 'douyin-trend-intelligence');
+  const issuePoolSkillDir = path.join(WORKSPACE_ROOT, '.claude', 'skills', 'voc-issue-pool');
+  const deepDiveSkillDir = path.join(WORKSPACE_ROOT, '.claude', 'skills', 'voc-problem-deep-dive');
+  const contentPlanSkillDir = path.join(WORKSPACE_ROOT, '.claude', 'skills', 'voc-content-plan');
+  const speakingScriptSkillDir = path.join(WORKSPACE_ROOT, '.claude', 'skills', 'voc-speaking-script');
+  const competitorMapSkillDir = path.join(WORKSPACE_ROOT, '.claude', 'skills', 'voc-competitor-map');
+  const businessWorkflowSkillDir = path.join(WORKSPACE_ROOT, '.claude', 'skills', 'voc-business-workflow');
+  ensureDir(xhsSkillDir);
+  ensureDir(douyinSkillDir);
+  ensureDir(issuePoolSkillDir);
+  ensureDir(deepDiveSkillDir);
+  ensureDir(contentPlanSkillDir);
+  ensureDir(speakingScriptSkillDir);
+  ensureDir(competitorMapSkillDir);
+  ensureDir(businessWorkflowSkillDir);
+  fs.writeFileSync(path.join(xhsSkillDir, 'SKILL.md'), workspaceSkillText(targetDir), 'utf8');
+  fs.writeFileSync(path.join(douyinSkillDir, 'SKILL.md'), workspaceDouyinSkillText(targetDir), 'utf8');
+  fs.writeFileSync(path.join(issuePoolSkillDir, 'SKILL.md'), workspaceIssuePoolSkillText(targetDir), 'utf8');
+  fs.writeFileSync(path.join(deepDiveSkillDir, 'SKILL.md'), workspaceProblemDeepDiveSkillText(targetDir), 'utf8');
+  fs.writeFileSync(path.join(contentPlanSkillDir, 'SKILL.md'), workspaceContentPlanSkillText(targetDir), 'utf8');
+  fs.writeFileSync(path.join(speakingScriptSkillDir, 'SKILL.md'), workspaceSpeakingScriptSkillText(targetDir), 'utf8');
+  fs.writeFileSync(path.join(competitorMapSkillDir, 'SKILL.md'), workspaceCompetitorMapSkillText(targetDir), 'utf8');
+  fs.writeFileSync(path.join(businessWorkflowSkillDir, 'SKILL.md'), workspaceBusinessWorkflowSkillText(targetDir), 'utf8');
+}
+
+function writeWorkspaceActivation(targetDir) {
+  if (!isSafeWorkspaceTarget(targetDir)) return;
+  writeWorkspaceMcpConfig(targetDir);
+  writeWorkspaceSkillEntries(targetDir);
+  console.log('Wrote workspace MCP config: .\\.mcp.json');
+  console.log('Wrote workspace skill entry: .\\.claude\\skills\\xiaohongshu-trend-intelligence\\SKILL.md');
+  console.log('Wrote workspace skill entry: .\\.claude\\skills\\douyin-trend-intelligence\\SKILL.md');
+  console.log('Wrote workspace skill entry: .\\.claude\\skills\\voc-issue-pool\\SKILL.md');
+  console.log('Wrote workspace skill entry: .\\.claude\\skills\\voc-problem-deep-dive\\SKILL.md');
+  console.log('Wrote workspace skill entry: .\\.claude\\skills\\voc-content-plan\\SKILL.md');
+  console.log('Wrote workspace skill entry: .\\.claude\\skills\\voc-speaking-script\\SKILL.md');
+  console.log('Wrote workspace skill entry: .\\.claude\\skills\\voc-competitor-map\\SKILL.md');
+  console.log('Wrote workspace skill entry: .\\.claude\\skills\\voc-business-workflow\\SKILL.md');
+}
+
 function printNextSteps(targetDir) {
+  const workspaceMode = isSafeWorkspaceTarget(targetDir);
+  console.log('');
+  console.log('Install complete.');
+  console.log('');
+  if (workspaceMode) {
+    console.log('VSCode workspace mode is ready.');
+    console.log('');
+    console.log('Generated project files:');
+    console.log('  .\\.mcp.json');
+    console.log('  .\\.claude\\skills\\xiaohongshu-trend-intelligence\\SKILL.md');
+    console.log('  .\\.claude\\skills\\douyin-trend-intelligence\\SKILL.md');
+    console.log('  .\\.claude\\skills\\voc-issue-pool\\SKILL.md');
+    console.log('  .\\.claude\\skills\\voc-problem-deep-dive\\SKILL.md');
+    console.log('  .\\.claude\\skills\\voc-content-plan\\SKILL.md');
+    console.log('  .\\.claude\\skills\\voc-speaking-script\\SKILL.md');
+    console.log('  .\\.claude\\skills\\voc-competitor-map\\SKILL.md');
+    console.log('  .\\.claude\\skills\\voc-business-workflow\\SKILL.md');
+    console.log('  .\\.claude\\plugins\\voc-intelligence');
+    console.log('');
+    console.log('Restart the VSCode Claude Code session if it was already open.');
+  } else {
+    console.log('Claude Code CLI launch command:');
+    console.log(`  claude --plugin-dir "${targetDir}"`);
+  }
+  console.log('');
+  console.log('Try this prompt in Claude Code:');
+  console.log('  帮我看一下南昌餐饮最近顾客在关心什么,并告诉我先改哪里、下周发什么。');
   console.log('');
-  console.log('安装完成。');
+  console.log('Or try:');
+  console.log('  帮我做一份家装全屋定制行业的小红书趋势情报。');
+  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('Or try:');
+  console.log('  帮我做一份家装全屋定制行业的抖音趋势情报。');
+  console.log('  先用演示样例跑通流程,不要真实采集。');
+  console.log('  在聊天里给我第一轮样本观察和待确认问题。');
   console.log('');
-  console.log(`安装目录:${targetDir}`);
+  console.log(`Install target: ${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}`);
+      if (!canOverwriteTarget(opts.target, opts)) {
+        throw new Error(`Target already exists. Use --force to overwrite custom target: ${opts.target}`);
       }
       fs.rmSync(opts.target, { recursive: true, force: true });
     }
     ensureDir(opts.target);
     const copied = copyDirRecursive(SOURCE_ROOT, opts.target);
-    console.log(`已复制技能包文件:${copied} 个`);
+    console.log(`Copied skill package files: ${copied}`);
   }
 
   assertInstalled(opts.target);
+  writeMcpConfig(opts.target);
+  writeWorkspaceActivation(opts.target);
 
   if (!opts.skipInstall) {
-    console.log('正在安装运行依赖...');
+    console.log('Installing runtime dependencies...');
     run('npm', ['install', '--omit=dev', '--ignore-scripts'], opts.target);
   }
 
   if (opts.smoke) {
-    console.log('正在运行安装自检...');
+    console.log('Running install smoke checks...');
     run(process.execPath, ['install.js', '--smoke', '--skip-install', '--no-next'], opts.target);
   } else {
     run(process.execPath, ['install.js', '--check', '--no-next'], opts.target);
@@ -198,7 +761,11 @@ function smoke(opts) {
 
 function printPath(opts) {
   console.log(opts.target);
-  console.log(`claude --plugin-dir "${opts.target}"`);
+  if (isSafeWorkspaceTarget(opts.target)) {
+    console.log('.\\.claude\\plugins\\voc-intelligence');
+  } else {
+    console.log(`claude --plugin-dir "${opts.target}"`);
+  }
 }
 
 function main() {
@@ -211,14 +778,13 @@ function main() {
   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()}`);
+  throw new Error(`Unknown command: ${opts.command}\n\n${usage()}`);
 }
 
 try {
   main();
 } catch (error) {
   console.error('');
-  console.error(`claude-voc 执行失败:${error.message}`);
+  console.error(`claude-voc failed: ${error.message}`);
   process.exit(1);
 }
-

+ 194 - 0
claude-code-voc-intelligence/docs/capability-map.md

@@ -0,0 +1,194 @@
+# Claude Code VOC Intelligence 能力地图
+
+这份文档给 Claude Code、维护者和交付人员快速定位:这套技能包能做什么、该用哪个入口、出了问题去哪里看。
+
+## 一句话定位
+
+把小红书、抖音等平台的用户真实声音,转成企业能用的趋势情报、问题优先级、经营动作、内容选题和多轮验证记忆。
+
+## 用户任务到入口映射
+
+| 用户想做的事 | 推荐入口 | MCP 工具 | 输出重点 |
+| --- | --- | --- | --- |
+| 一次跑完整经营闭环 | `voc-business-workflow` | `voc_business_workflow_run` | 市场声音、问题池、Top 问题动作、7 天内容计划、第一条口播稿 |
+| 先演示流程,不真实采集 | 小红书/抖音趋势 skill | `voc_xiaohongshu_trend_run` / `voc_douyin_trend_run`,`collectionMode=sample` | 第一轮样本观察、机会假设、待确认问题 |
+| 真实采集行业趋势 | 小红书/抖音趋势 skill | `voc_xiaohongshu_trend_run` / `voc_douyin_trend_run`,`collectionMode=live` | 真实笔记/视频、评论证据、趋势假设 |
+| 检查 token 是否可用 | 平台趋势 skill | `voc_xiaohongshu_token_check` / `voc_douyin_token_check` | 是否已配置、无 token 时给充值链接 |
+| 用户回答校准问题 | 平台趋势 skill | `voc_xiaohongshu_preference_update` / `voc_douyin_preference_update` | 保存偏好、屏蔽方向、下一轮聚焦 |
+| 把评论整理成问题池 | `voc-issue-pool` | `voc_issue_pool_run` | 问题优先级、状态、证据、影响环节、建议动作 |
+| 判断哪些问题最影响生意 | `voc-issue-pool` | `voc_issue_pool_run` | 本周优先推进的问题和下一步深挖入口 |
+| 深挖一个具体问题 | `voc-problem-deep-dive` | `voc_problem_deep_dive_run` | 老板视角影响、动作清单、7 天验证 |
+| 用户说某动作不适合 | `voc-problem-deep-dive` | `voc_problem_deep_dive_run`,传 `feedback` | 自动解析为屏蔽动作或偏好动作 |
+| 选中一条内容继续改口播 | `voc-speaking-script` | `voc_speaking_script_run` | 60 秒稿、30 秒稿、提词器版、改稿反馈、定稿记忆 |
+| 下轮继续优化 | 同一 skill | 带同一 `memory`,或带 project/industry/scenario/audience 让工具自动分桶 | 应用历史记忆,避免从零开始 |
+
+## 技能入口
+
+### `voc-business-workflow`
+
+适合:
+
+- 用户只想说自然语言,不想选择工具
+- 一次串起市场洞察、问题优先级、单点动作、内容计划和口播稿
+- 远程给客户先跑一份第一轮经营交付
+
+用户典型话术:
+
+```text
+帮我看一下南昌餐饮最近顾客在关心什么,并告诉我先改哪里、下周发什么。
+```
+
+### `xiaohongshu-trend-intelligence`
+
+适合:
+
+- 小红书趋势情报
+- 行业用户顾虑
+- 内容选题
+- 销售话术
+- 小红书评论证据卡
+
+用户典型话术:
+
+```text
+帮我做一份家装全屋定制行业的小红书趋势情报,先用演示样例跑通流程。
+```
+
+### `douyin-trend-intelligence`
+
+适合:
+
+- 抖音趋势情报
+- 本地生活/餐饮评论洞察
+- 短视频内容机会
+- 门店转化线索
+
+用户典型话术:
+
+```text
+帮我看下南昌餐饮行业抖音上用户最近在吐槽什么,先小规模真实采集。
+```
+
+### `voc-problem-deep-dive`
+
+适合:
+
+- 单点 VOC 深挖
+- 差评背后问题拆解
+- 老板怎么解决
+- 门店动作和内容选题
+- 多轮记忆优化
+
+用户典型话术:
+
+```text
+继续深挖“排队”这个问题,告诉老板今天能改什么,7 天怎么验证。
+```
+
+### `voc-issue-pool`
+
+适合:
+
+- 评论问题池
+- 用户吐槽归类
+- 问题优先级排序
+- 问题状态管理
+- 从趋势报告进入单点深挖前的中间层
+
+用户典型话术:
+
+```text
+把这些评论整理成问题池,告诉我哪些问题最影响生意,先改哪个。
+```
+
+## 关键产品原则
+
+- 首轮报告是“初步判断”,不是最终结论。
+- 输出必须带证据,不只给抽象观点。
+- 老板用户不需要学习提示词,Claude Code 要主动选择技能和工具。
+- live 失败时给可执行下一步,不暴露原始接口错误。
+- 多轮反馈要进入记忆,让建议越用越贴近客户。
+- 不同行业、不同客户的记忆要隔离。
+
+## 记忆规则
+
+趋势情报记忆:
+
+- 记录保留方向、屏蔽方向、聚焦模式、用户备注。
+- 适合下一轮趋势报告排序和话术收敛。
+
+单点 VOC 记忆:
+
+- 记录偏好动作、屏蔽动作、偏好内容角度、验证有效动作、验证无效动作。
+- 未显式传 `memory` 时,默认按 `project/brand/store + industry + scenario + audience` 分桶。
+- 显式传 `memory` 时保持兼容,用指定文件沉淀。
+
+## 文件索引
+
+| 文件 | 用途 |
+| --- | --- |
+| `README.md` | 面向使用者和 Claude Code 的总入口说明 |
+| `skills/*/SKILL.md` | Claude Code 触发规则和操作约束 |
+| `mcp/src/server.js` | MCP 工具注册、schema、工具说明 |
+| `mcp/src/tools/*` | CLI/MCP 可调用工具入口 |
+| `mcp/src/features/*` | 趋势报告、单点深挖、记忆、信号提取 |
+| `mcp/src/core/credentials.js` | token 读取口径 |
+| `mcp/src/core/payment-links.js` | 充值/开通提示 |
+| `mcp/src/core/evidence-quality.js` | 证据质量门槛、无样本降级建议 |
+| `docs/live-manual-acceptance-checklist.md` | live 采集人工验收 |
+| `docs/payment-package-links.md` | 支付链接和套餐入口说明 |
+| `docs/context-budget-implementation-plan.md` | 上下文省 token 工作流 |
+| `docs/deliverables/trend-insight-report.md` | 行业用户洞察趋势清单模板 |
+| `docs/deliverables/competitor-map-report.md` | 竞品图谱与错位机会报告模板 |
+| `docs/deliverables/voc-issue-pool-report.md` | VOC 问题清单与优先级模板 |
+| `docs/deliverables/single-problem-action-plan.md` | 单点问题 7 天行动计划模板 |
+| `docs/deliverables/seven-day-content-plan.md` | 7 天内容选题与口播脚本模板 |
+| `scripts/smoke-package.js` | 包级 smoke,覆盖 sample、记忆、token、MCP |
+| `scripts/smoke-mcp.js` | MCP 工具发现和基本调用 smoke |
+
+## 工作坊输出口径
+
+面向餐饮老板或线下工作坊时,Claude Code 输出优先按:
+
+1. 你这轮能拿到什么。
+2. 我会怎么做。
+3. 第一版结果。
+4. 你需要确认什么。
+5. 下一步建议。
+
+每个小节都必须对应一个交付物。不要在前台讲 MCP、schema、collectionMode、提示词工程。
+
+## 维护检查点
+
+每次发布至少确认:
+
+- `package.json`、`package-lock.json`、`skill-package-manifest.json`、`.claude-plugin/plugin.json`、`mcp/src/server.js` 版本一致。
+- `npm --prefix claude-code-voc-intelligence run mcp:smoke` 通过。
+- `npm --prefix claude-code-voc-intelligence run smoke:package` 通过。
+- `npm run claude-voc:build` 通过。
+- npm 发布后 `npm view @gangvy/claude-code-voc-intelligence version` 返回新版本。
+- 测试工作区 `npx --yes @gangvy/claude-code-voc-intelligence@latest workspace --smoke` 通过。
+
+## 下一轮优化补充
+
+- `voc-issue-pool` 已支持跨轮问题变化:同一记忆范围下会对比上轮/本轮证据数,并标记新增、变多、变少、持平、本轮未再出现。
+- `voc-problem-deep-dive` 已支持动作权重记忆:用户反馈“有效/无效”后,会记录验证次数,后续优先推荐有效动作、降低无效动作。
+- 趋势情报输出后的推荐链路应优先进入问题池:先把证据整理成 VOC 问题池,判断先改哪个问题,再进入单点 VOC 深挖。
+
+## 工作坊交付型入口
+
+| 老板说法 | 推荐入口 | MCP 工具 | 交付物 |
+| --- | --- | --- | --- |
+| 帮我看这个品类最近顾客在关心什么 | 小红书/抖音趋势 skill | `voc_xiaohongshu_trend_run` / `voc_douyin_trend_run` | 行业用户洞察趋势清单 |
+| 帮我看顾客关心什么、先改哪里、下周发什么 | `voc-business-workflow` | `voc_business_workflow_run` | VOC 经营闭环报告 |
+| 我不知道竞品是谁,帮我看看别人怎么做 | `voc-competitor-map` | `voc_competitor_map_run` | 竞品图谱与错位机会报告 |
+| 顾客到底在吐槽什么,先改哪个 | `voc-issue-pool` | `voc_issue_pool_run` | VOC 问题清单与优先级 |
+| 这个问题老板怎么解决 | `voc-problem-deep-dive` | `voc_problem_deep_dive_run` | 单点问题 7 天行动计划 |
+| 下周账号发什么,帮我写口播 | `voc-content-plan` | `voc_content_plan_run` | 7 天内容选题与口播脚本 |
+| 选题 2 进入脚本共创 | `voc-speaking-script` | `voc_speaking_script_run` | 口播稿改稿与定稿记忆 |
+
+边界说明:
+
+- 口播/短视频脚本属于本技能包范围,但必须来自 VOC 用户问题、竞品评论差异、问题池 Top 问题或单点深挖动作。
+- 创始人故事、品牌自传、老板个人 IP 长文不属于 `claude-code-voc-intelligence` 主线。
+- 现场工作坊前台不要讲 MCP、schema、提示词工程;前台只讲“看市场、看竞品、看问题、找动作、做内容”。

+ 491 - 0
claude-code-voc-intelligence/docs/context-budget-implementation-plan.md

@@ -0,0 +1,491 @@
+# Claude Code 上下文省 token 工作流落地方案
+
+## 背景问题
+
+截图里的失败不是单次工具报错,而是会话已经背了过多原始资料、读取记录和中间推理。Claude Code 报出:
+
+```text
+Input tokens exceed the configured limit of 272000 tokens.
+```
+
+这说明继续在同一会话里点“继续”,大概率还是会失败。技能包需要把“长任务上下文”从聊天窗口里搬到文件和工具里,让 Claude 每轮只读取必要的轻量状态,而不是反复读取所有原始图片、Excel、长 Markdown 和完整历史对话。
+
+## 落地目标
+
+给 `claude-code-voc-intelligence` 增加一套“省 token 长任务工作流”,用于真实采集任务文档、小红书样本整理、客户素材归档、批量图片/Excel/文本分析等场景。
+
+目标效果:
+
+- 用户可以把大量资料放到工作区,Claude 不会一次读完全部文件。
+- 每轮最多处理 1 个样本单元,例如 `XHS-001_1`。
+- 每个样本单元处理完后,必须写入中间摘要文件。
+- 后续会话只读取任务状态、中间摘要和下一个样本单元,不重复读取原始大文件。
+- 上下文不足时,技能包能主动生成交接摘要,并提示新会话如何继续。
+- 最终报告由多个中间摘要汇总而来,而不是由一个超长会话硬扛到底。
+
+## 核心设计
+
+采用“原始资料 -> 文件清单 -> 样本单元摘要 -> 总控任务状态 -> 最终报告”的流水线。
+
+```mermaid
+flowchart TD
+  A["原始资料目录"] --> B["context scan 文件清单"]
+  B --> C["sample units 样本单元索引"]
+  C --> D["每轮只处理 1 个样本单元"]
+  D --> E["unit summary 中间摘要"]
+  E --> F["task-state 当前进度"]
+  F --> G["handoff 新会话交接摘要"]
+  E --> H["final report 汇总文档"]
+```
+
+关键原则:
+
+- 聊天上下文只放“当前要判断的内容”。
+- 大文件只让脚本读取,Claude 只读脚本生成的轻量摘要。
+- 任务进度写入文件,不依赖会话记忆。
+- 新会话从 `task-state` 恢复,而不是从聊天历史恢复。
+
+## 需要新增的文件结构
+
+建议先落在 `claude-code-voc-intelligence` 技能包内:
+
+```text
+claude-code-voc-intelligence/
+  docs/
+    context-budget-implementation-plan.md
+  skills/xiaohongshu-trend-intelligence/references/
+    context-budget-workflow.md
+  mcp/src/features/context-budget/
+    file-manifest.js
+    sample-units.js
+    checkpoint.js
+    handoff.js
+  mcp/src/tools/
+    context-budget-run.js
+  scripts/
+    smoke-context-budget.js
+```
+
+安装到工作区后,运行时在客户项目里生成:
+
+```text
+.claude/task-state/
+  voc-context-state.md
+  voc-context-state.json
+  handoff.md
+
+outputs/voc-context/
+  file-manifest.json
+  sample-units.json
+  unit-summaries/
+    XHS-001_1.md
+    XHS-002_1.md
+  final/
+    真实采集任务文档.md
+```
+
+## 技术实现模块
+
+### 1. 文件清单扫描器
+
+作用:先建立资料目录的轻量索引,避免 Claude 直接递归读取全部内容。
+
+建议实现:
+
+```text
+mcp/src/features/context-budget/file-manifest.js
+```
+
+输入保持简单:
+
+```json
+{
+  "rootDir": "docs/真实采集任务文档",
+  "taskName": "小红书真实采集任务文档"
+}
+```
+
+输出内容:
+
+- 文件路径
+- 文件类型
+- 文件大小
+- 修改时间
+- 所属样本单元推断,例如 `XHS-001_1`
+- 是否疑似大文件
+- 是否建议脚本预处理
+
+不在第一步读取图片正文、Excel 全量内容或长 Markdown 全文。
+
+### 2. 样本单元识别器
+
+作用:把 `01-06`、`XHS-001_1`、`XHS-008_8` 这类目录识别成可独立处理的样本单元。
+
+建议规则:
+
+- 优先识别 `XHS-\d+(_\d+)?`
+- 其次识别一级/二级目录名
+- 允许用户在状态文件里手动修正单元关系
+- 每个单元包含:截图、笔记正文、评论、Excel 行、人工备注、校验材料
+
+输出:
+
+```text
+outputs/voc-context/sample-units.json
+```
+
+示例字段:
+
+```json
+{
+  "unitId": "XHS-001_1",
+  "status": "pending",
+  "files": [],
+  "summaryPath": "outputs/voc-context/unit-summaries/XHS-001_1.md",
+  "estimatedRisk": "large-image-set"
+}
+```
+
+### 3. 单元摘要生成器
+
+作用:每轮只处理一个样本单元,并把结果写成结构化摘要。
+
+建议实现:
+
+```text
+mcp/src/features/context-budget/sample-units.js
+```
+
+摘要模板:
+
+```markdown
+# XHS-001_1 样本摘要
+
+## 已读取资料
+
+## 账号/笔记信息
+
+## 小红书趋势关键词
+
+## 用户评论与真实顾虑
+
+## 可验证话题方向
+
+## 博主真实度/商业痕迹判断
+
+## 证据片段
+
+## 待复核问题
+
+## 不要重复读取的原始文件
+```
+
+约束:
+
+- 单元摘要控制在 800-1500 字。
+- 摘要必须引用文件名,但不要复制大段原文。
+- 图片只记录观察结论、截图编号和需要复核的位置。
+- Excel 只读取表头、关键列、与当前单元相关的行。
+
+### 4. 任务状态与检查点
+
+作用:让新会话知道任务做到哪里,不靠历史聊天。
+
+建议实现:
+
+```text
+mcp/src/features/context-budget/checkpoint.js
+```
+
+生成两个文件:
+
+```text
+.claude/task-state/voc-context-state.json
+.claude/task-state/voc-context-state.md
+```
+
+状态内容:
+
+- 当前目标
+- 已完成样本单元
+- 正在处理的样本单元
+- 待处理样本单元
+- 已生成摘要文件
+- 禁止重复读取的原始目录
+- 下一步只允许读取哪些文件
+- 汇总报告缺口
+
+`voc-context-state.md` 要给人看,`voc-context-state.json` 给工具读。
+
+### 5. 新会话交接摘要
+
+作用:上下文过大时,直接停止扩读,生成新会话启动提示。
+
+建议实现:
+
+```text
+mcp/src/features/context-budget/handoff.js
+```
+
+生成:
+
+```text
+.claude/task-state/handoff.md
+```
+
+内容模板:
+
+```markdown
+# 新会话交接摘要
+
+## 当前任务
+
+## 已完成
+
+## 不要重新读取
+
+## 新会话只读取
+
+1. .claude/task-state/voc-context-state.md
+2. outputs/voc-context/sample-units.json
+3. outputs/voc-context/unit-summaries/
+
+## 下一步
+
+只处理 XHS-002_1,完成后写入对应摘要并停止。
+
+## 可直接复制给 Claude Code 的 Prompt
+```
+
+## 需要新增的 MCP 工具
+
+建议在 `mcp/src/server.js` 增加三个工具。入参保持简单,避免让用户理解技术参数。
+
+### `voc_context_prepare`
+
+用途:初始化长任务,扫描目录,生成文件清单、样本单元和任务状态。
+
+输入:
+
+```json
+{
+  "rootDir": "docs/真实采集任务文档",
+  "taskName": "小红书真实采集任务文档"
+}
+```
+
+输出:
+
+- `assistantMessage`:告诉用户识别出多少样本单元,建议先处理哪个。
+- `data.statePath`
+- `data.sampleUnitsPath`
+- `nextActions`
+
+### `voc_context_next_unit`
+
+用途:读取任务状态,返回下一轮应该处理的单元和允许读取的文件清单。
+
+输入:
+
+```json
+{
+  "statePath": ".claude/task-state/voc-context-state.json"
+}
+```
+
+输出:
+
+- 下一单元 ID
+- 允许读取的文件
+- 禁止读取的文件
+- 摘要写入路径
+
+### `voc_context_checkpoint`
+
+用途:处理完一个样本单元后,写入摘要、更新任务状态、生成必要的 handoff。
+
+输入:
+
+```json
+{
+  "unitId": "XHS-001_1",
+  "summary": "本轮摘要正文",
+  "status": "completed"
+}
+```
+
+输出:
+
+- 更新后的进度
+- 下一个单元
+- 新会话启动提示
+
+## CLI 命令设计
+
+为了兼容 Claude Code 工具不可用或客户只会终端的情况,也建议在 `claude-voc` CLI 增加同款命令:
+
+```powershell
+claude-voc context prepare --root "docs/真实采集任务文档" --task "小红书真实采集任务文档"
+claude-voc context next
+claude-voc context checkpoint --unit XHS-001_1 --summary "outputs/voc-context/unit-summaries/XHS-001_1.md"
+claude-voc context handoff
+```
+
+客户不需要主动使用这些命令,但 Claude Code 可以通过 Bash 调用,作为稳定兜底。
+
+## Skill 文档需要加的规则
+
+在 `skills/xiaohongshu-trend-intelligence/SKILL.md` 增加“长任务省 token 协议”:
+
+```markdown
+## 长任务省 token 协议
+
+- 如果用户要求处理多个样本目录、批量图片、Excel 或长文档,不要一次读取全部资料。
+- 先调用/执行 context prepare,生成文件清单、样本单元和任务状态。
+- 每轮最多处理 1 个样本单元。
+- 每处理完一个样本单元,必须写入 unit summary,并更新 task-state。
+- 后续汇总只能优先读取 unit summary,不重复读取原始大文件。
+- 当上下文明显变大、用户说继续多次、或任务超过 3 个样本单元时,先生成 handoff,再建议开新会话。
+- 新会话只读取 handoff、task-state、sample-units 和必要的 unit summary。
+```
+
+## 面向用户的话术
+
+当用户给了大批资料时,Claude 应该这样说:
+
+```text
+资料比较多,我会按省上下文方式处理:先建立文件清单和样本单元索引,然后每轮只处理一个样本单元。每个单元处理完都会写入摘要,后续汇总只读摘要,不重复读取原始大文件。
+
+我先处理 XHS-001_1,处理完会停下来给你确认。
+```
+
+当上下文不足时:
+
+```text
+当前会话已经接近上下文上限。我已经把进度写入 .claude/task-state/handoff.md。
+
+建议新开一个 Claude Code 会话,并只让它读取 handoff、task-state 和已生成的 unit summary,不要重新读取所有原始图片和 Excel。
+```
+
+新会话启动 Prompt:
+
+```text
+继续处理小红书真实采集任务文档,但请控制上下文。
+
+先只读取:
+1. .claude/task-state/handoff.md
+2. .claude/task-state/voc-context-state.md
+3. outputs/voc-context/sample-units.json
+4. 已生成的 unit summary
+
+不要重新读取所有 XHS 原始图片、Excel、长文档。
+请先告诉我当前进度、缺什么、下一步只处理哪个样本单元。
+每次最多处理一个样本单元,处理完写入对应摘要文件后停止。
+```
+
+## 分阶段实施计划
+
+### P0:先用规则和文档止血
+
+改动:
+
+- 新增 `references/context-budget-workflow.md`
+- 在 `SKILL.md` 引用省 token 协议
+- 在 `customer-quickstart.md` 或 `demo-runbook.md` 补充大资料处理话术
+
+收益:不改代码也能显著降低 Claude 一次性读取全部文件的概率。
+
+### P1:实现本地文件清单和任务状态
+
+改动:
+
+- 新增 `context-budget-run.js`
+- 支持 `prepare`、`next`、`checkpoint`、`handoff`
+- 写入 `.claude/task-state` 和 `outputs/voc-context`
+
+收益:长任务可以跨会话续跑。
+
+### P2:接入 MCP 工具
+
+改动:
+
+- 在 `server.js` 注册 `voc_context_prepare`
+- 注册 `voc_context_next_unit`
+- 注册 `voc_context_checkpoint`
+- smoke 测试覆盖工具返回的 `assistantMessage`
+
+收益:Claude Code 插件里可以直接调用工具,不必靠手写 Bash。
+
+### P3:增加文件预处理能力
+
+按优先级实现:
+
+1. Markdown/TXT 分章节摘要。
+2. Excel 只提取 sheet 名、列名、前 N 行和命中当前样本 ID 的行。
+3. 图片只提取路径、大小、尺寸、所属样本,不直接 OCR 全图。
+4. 后续如需要,再接 OCR 或视觉模型,但必须按单元处理。
+
+如果引入依赖,建议:
+
+- Excel:`xlsx`
+- 图片尺寸:`image-size`
+
+第一版也可以先不加依赖,只做路径、大小、扩展名和目录归属。
+
+### P4:打包和发布
+
+改动:
+
+- `package.json` 增加 smoke 脚本
+- `scripts/smoke-package.js` 增加 context workflow 检查
+- `npm run claude-voc:npm-pack`
+- `npm run claude-voc:build`
+- 升级 npm 版本并发布
+
+## 验收标准
+
+功能验收:
+
+- 给一个包含 6 个样本单元、图片、Excel、长文档的目录,`prepare` 后能生成 `sample-units.json`。
+- Claude 第一轮只处理 1 个样本单元。
+- 处理完会生成 `unit-summaries/<unitId>.md`。
+- `task-state.md` 能清楚显示已完成、待处理和下一步。
+- 新会话只读 handoff 和摘要,也能继续下一单元。
+
+体验验收:
+
+- 不再出现“继续”后反复超 token。
+- 不让用户理解复杂参数。
+- 不把 JSON 调试信息直接甩给用户。
+- 不要求用户手动整理 01-06 到 XHS-001 的映射,工具能先自动推断,必要时再让用户确认。
+
+安全验收:
+
+- 不把 VOC token、npm token 写入状态文件、摘要文件或报告。
+- `.claude/task-state` 可以进入工作区,但任何 `.env.local`、`.npmrc` 不进入 git。
+
+## 推荐优先落地顺序
+
+建议先做 P0 + P1。
+
+原因:
+
+- 这两个阶段能直接解决截图里的核心问题。
+- 不需要马上引入 Excel/OCR 复杂依赖。
+- 不改变小红书采集接口入参。
+- 后续再把工具挂到 MCP,风险更小。
+
+第一轮实施后,真实工作流会变成:
+
+```text
+用户给大资料目录
+-> Claude 调 context prepare
+-> 生成样本单元
+-> Claude 只处理 XHS-001_1
+-> 写摘要和 checkpoint
+-> 用户确认
+-> 下一轮处理 XHS-002_1
+-> 最后汇总所有 unit summary
+```
+
+这套方案和当前小红书趋势情报官并不冲突。它是给“资料很多、任务很长、需要跨会话完成”的场景加一层上下文管理能力。

+ 25 - 61
claude-code-voc-intelligence/docs/customer-quickstart.md

@@ -1,79 +1,43 @@
-# 小红书趋势情报官客户快速说明
+# 客户快速开始
 
-## 这个技能包能做什么
+这套技能包适合给业务用户直接看趋势,不是给技术人员看接口。
 
-“小红书趋势情报官”用于帮行业客户从小红书内容和评论里看机会:
+## 你可以这样开始
 
-- 下季度可能流行的风格、设计元素和场景。
-- 用户在评论里反复担心的问题,比如预算、环保、翻车、售后。
-- 可以继续验证的内容选题、门店话术、产品表达方向。
-- 用户反馈后的偏好记忆,下次报告会自动更贴近客户业务。
-
-当前版本优先服务“家装全屋定制/女性消费决策”场景,也可以通过行业、关键词和目标人群扩展到其他行业。
-
-## 第一次怎么说
-
-直接在 Claude Code 里说:
+如果你想一次拿到经营闭环:
 
 ```text
-帮我做一份家装全屋定制行业的小红书趋势情报。
-重点看女性客户下季度可能关注的设计元素、风格和消费顾虑。
-先在聊天里给我第一轮样本观察和待确认问题,不要只给文件路径。
+帮我看一下[行业/门店]最近顾客在关心什么,并告诉我先改哪里、下周发什么。
 ```
 
-系统应该直接返回报告正文,而不是只告诉你文件保存在哪里。
-
-## 报告看什么
-
-重点看四块:
+它会输出:市场和用户声音、先改哪个问题、Top 问题怎么解决、7 天内容计划和第一条口播稿。
 
-- “第一轮初步判断(待校准)”:先判断趋势主线。
-- “样本观察 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 加载这个技能包,再按上面的自然语言话术启动
+再切 live 模式。
 
+如果没有 token 或余额不足,系统会给充值链接,不会只丢一条冷冰冰的报错。

+ 43 - 0
claude-code-voc-intelligence/docs/deliverables/competitor-map-report.md

@@ -0,0 +1,43 @@
+# 竞品图谱与错位机会报告模板
+
+## 适用场景
+
+用户想看竞品、同城同品类别人怎么做、为什么吸引顾客,或者用户还不知道竞品是谁。
+
+老板语言入口:
+
+- 帮我看看竞品都在做什么。
+- 我不知道竞品是谁,帮我先做一份竞品图谱。
+- 看看同城同品类别人为什么吸引顾客。
+
+## 输入字段
+
+- 品牌/门店。
+- 城市/商圈。
+- 行业/品类。
+- 价格带,可选。
+- 消费场景。
+- 竞品名单,可选。
+- 竞品评论/VOC 证据,可选。
+
+## 输出结构
+
+1. 你这轮能拿到什么。
+2. 业务口径。
+3. 竞品候选图谱。
+4. 每类竞品为什么要看。
+5. 用户可能被吸引的点。
+6. 用户可能吐槽的点。
+7. 我方错位机会。
+8. VOC 信号。
+9. 证据样本。
+10. 下一步:进入问题池或 7 天内容选题。
+
+## 验收标准
+
+- 用户没给竞品时,必须先生成候选维度。
+- 输出必须带 VOC 证据样本。
+- 不能只做 SWOT。
+- 必须给出错位竞争动作和内容角度。
+- 不覆盖创始人故事。
+

+ 45 - 0
claude-code-voc-intelligence/docs/deliverables/seven-day-content-plan.md

@@ -0,0 +1,45 @@
+# 7 天内容选题与口播脚本模板
+
+## 适用场景
+
+用户想知道下周账号发什么,或想把 VOC 问题转成短视频选题、口播脚本和镜头建议。
+
+老板语言入口:
+
+- 下周账号发什么。
+- 根据这些顾客吐槽生成 7 天口播。
+- 把排队久、人均贵、怕踩雷变成视频脚本。
+
+## 输入字段
+
+- 品牌/门店。
+- 行业/品类。
+- 平台。
+- VOC 问题。
+- 趋势报告/问题池/单点深挖结果,可选。
+- 内容周期,默认 7 天。
+
+## 输出结构
+
+1. 你这轮能拿到什么。
+2. 业务口径。
+3. 7 天内容表。
+4. 每天对应的用户问题。
+5. 洞察判断。
+6. 选题标题。
+7. 前 3 秒钩子。
+8. 30-60 秒口播脚本。
+9. 镜头建议。
+10. 评论区引导。
+11. 验证指标。
+12. 拍摄提醒。
+13. 下一步建议。
+
+## 验收标准
+
+- 每条内容必须绑定一个 VOC 用户问题。
+- 口播要能被餐饮老板直接照着拍。
+- 不输出泛泛爆款标题。
+- 不覆盖创始人故事。
+- 发布后要引导把评论反馈放回 VOC 问题池。
+

+ 49 - 0
claude-code-voc-intelligence/docs/deliverables/single-problem-action-plan.md

@@ -0,0 +1,49 @@
+# 单点问题 7 天行动计划模板
+
+## 适用场景
+
+用户已经知道一个具体问题,想知道老板应该怎么解决、今天先改什么、7 天怎么验证。
+
+老板语言入口:
+
+- 这个问题老板怎么解决。
+- 继续深挖排队/价格/服务/味道。
+- 这个差评背后到底是什么问题。
+- 这个动作有效/无效,后面怎么调整。
+
+## 输入字段
+
+- 单个问题。
+- 行业/品类。
+- 场景。
+- 目标用户。
+- 证据样本。
+- 偏好动作/屏蔽动作,可选。
+- 有效/无效反馈,可选。
+- 深挖记忆,可选。
+
+## 输出结构
+
+1. 一句话判断。
+2. 业务口径。
+3. 优先级判断。
+4. 用户原话证据。
+5. 表层问题 vs 深层问题。
+6. 老板视角影响。
+7. 可执行解决动作。
+8. 为什么优先推荐这些动作,记忆命中时显示。
+9. 今天就能做。
+10. 7 天内验证。
+11. 长期沉淀。
+12. 可转化内容选题。
+13. 7 天验证计划。
+14. 下轮验证指标。
+15. 仍需确认的问题。
+
+## 验收标准
+
+- 每次只深挖一个问题。
+- 必须给出老板今天能做的动作。
+- 必须给出 7 天验证指标。
+- 用户反馈有效/无效后,后续排序必须变化。
+

+ 41 - 0
claude-code-voc-intelligence/docs/deliverables/trend-insight-report.md

@@ -0,0 +1,41 @@
+# 行业用户洞察趋势清单模板
+
+## 适用场景
+
+用户想知道某个行业、城市、品类或平台上,顾客最近在关心什么、吐槽什么、什么内容值得跟进。
+
+老板语言入口:
+
+- 帮我看这个品类最近顾客在关心什么。
+- 看看南昌餐饮最近有什么机会。
+- 跑一份小红书/抖音趋势情报。
+
+## 输入字段
+
+- 行业/品类。
+- 城市/区域,可选。
+- 目标用户。
+- 关键词。
+- 平台:小红书/抖音。
+- 采集模式:sample/live。
+- 历史偏好记忆,可选。
+
+## 输出结构
+
+1. 你这轮能拿到什么。
+2. 第一轮初步判断。
+3. 数据口径。
+4. 样本观察 Top 5。
+5. 用户顾虑/决策阻力。
+6. 证据样本。
+7. 可继续验证的选题方向。
+8. 待确认问题。
+9. 下一步:整理成 VOC 问题池。
+
+## 验收标准
+
+- 必须说明这是第一轮判断,不是最终结论。
+- 必须包含证据样本,不能只有抽象判断。
+- 必须给出下一步进入问题池的建议。
+- 不要求用户理解 MCP、profile、collectionMode。
+

+ 42 - 0
claude-code-voc-intelligence/docs/deliverables/voc-issue-pool-report.md

@@ -0,0 +1,42 @@
+# VOC 问题清单与优先级模板
+
+## 适用场景
+
+用户想知道评论里到底在吐槽什么、哪些问题最影响生意、先改哪个。
+
+老板语言入口:
+
+- 顾客到底在吐槽什么。
+- 把这些评论整理成问题池。
+- 我这个店先改哪里。
+- 哪些问题最影响生意。
+
+## 输入字段
+
+- 品牌/门店。
+- 行业/品类。
+- 使用场景。
+- 目标用户。
+- 评论/证据文本。
+- 上一轮趋势报告,可选。
+- 问题状态更新,可选。
+- 问题池记忆,可选。
+
+## 输出结构
+
+1. 一句话判断。
+2. 业务口径。
+3. 问题优先级。
+4. 每个问题的状态、证据数、影响环节、建议动作。
+5. 问题变化:新增、变多、变少、持平、本轮未再出现。
+6. 本周建议推进。
+7. 下一轮怎么用。
+8. 问题池记忆文件。
+
+## 验收标准
+
+- 必须站在经营视角说明影响新客、复购、客单、口碑或现场效率。
+- 必须包含证据样本。
+- 连续使用时必须对比问题变化。
+- 必须引导进入单点深挖。
+

+ 19 - 64
claude-code-voc-intelligence/docs/demo-runbook.md

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

+ 50 - 0
claude-code-voc-intelligence/docs/live-manual-acceptance-checklist.md

@@ -0,0 +1,50 @@
+# VOC Live 采集手动验收清单
+
+这份清单用于发布前或课程现场前确认真实采集链路可用。自动 smoke 不默认跑 live,因为 live 依赖上游余额、token 状态和平台接口稳定性。
+
+## 验收前准备
+
+- 当前工作区存在 `.env.local`,并配置 `VOC_TOKEN=<服务方提供的 session token>`。
+- 不依赖 `~/.openclaw/voc-credentials.json`。
+- 如果使用全局配置,只允许使用 `~/.claude/voc-credentials.json`。
+- 上游余额充足,且充值入口可打开:
+  `https://app.fmode.cn/dev/apig-pay/?apigid=Vo3ROWEvDy&fun_id=HOkkX72PMF`
+
+## 小红书 live 最小验收
+
+```bash
+node ".claude/plugins/voc-intelligence/mcp/src/tools/xiaohongshu-trend-run.js" --collection-mode live --industry "本地生活餐饮" --keywords "南昌美食,南昌菜" --keyword-limit 1 --notes-per-keyword 1 --max-comment-pages 1 --output "outputs/live-acceptance-xhs" --result-prefix XHS_TREND_RESULT
+```
+
+验收标准:
+
+- `status` 为 `ok`,或在无 token/余额不足时返回友好充值提示。
+- 不出现原始 `403`、权限栈、接口响应体。
+- 报告里有证据样本、原帖链接、评论证据。
+- `files` 中包含 Markdown、JSON,若有图片则包含本地 `assets`。
+
+## 抖音 live 最小验收
+
+```bash
+node ".claude/plugins/voc-intelligence/mcp/src/tools/douyin-trend-run.js" --collection-mode live --industry "本地生活餐饮" --keywords "南昌美食,南昌菜" --keyword-limit 1 --videos-per-keyword 1 --max-comment-pages 1 --output "outputs/live-acceptance-douyin" --result-prefix DOUYIN_TREND_RESULT
+```
+
+验收标准:
+
+- `status` 为 `ok`,或在无 token/余额不足时返回友好充值提示。
+- 不出现原始 `403`、权限栈、接口响应体。
+- 报告里有视频证据、原视频链接、评论证据。
+- 输出内容仍是“第一轮初步判断 / 机会假设 / 待确认问题”,不能把首轮样本写成最终结论。
+
+## 单点 VOC 记忆验收
+
+```bash
+node ".claude/plugins/voc-intelligence/mcp/src/tools/voc-problem-deep-dive-run.js" --issue "排队" --project "样板门店A" --industry "本地生活餐饮" --scenario "门店经营" --audience "到店顾客" --feedback "这个店不想做等位饮品,更想先改评论区回复和预点单" --assistant-message-only
+```
+
+验收标准:
+
+- 输出有“优先级判断、老板视角影响、可执行解决动作、7 天验证计划、下轮验证指标”。
+- “等位饮品”只能出现在“已应用的使用记忆”,不能出现在“可执行解决动作”。
+- 默认记忆文件按项目/行业/场景/人群隔离,不应污染其它行业或其它客户。
+

+ 86 - 0
claude-code-voc-intelligence/docs/payment-package-links.md

@@ -0,0 +1,86 @@
+# VOC 技能套餐与充值入口说明
+
+## 适用场景
+
+本说明用于把 `apig-pay` 的课程套餐、Claude Code 技能包和 live 真实采集串成一个闭环:
+
+```text
+安装 VS Code Claude Code
+-> 安装 VOC Intelligence 技能包
+-> 购买/开通技能套餐
+-> 复制并配置 session token
+-> 运行小红书 live 真实采集
+```
+
+## 套餐入口
+
+公开套餐总入口:
+
+```text
+https://app.fmode.cn/dev/apig-pay/#/workshop
+```
+
+常用套餐直达:
+
+| 套餐 | 入口 | 说明 |
+|---|---|---|
+| 课程特惠体验包 | https://app.fmode.cn/dev/apig-pay/#/workshop/19-9 | 隐藏直达,适合课堂试用 |
+| 课程加量体验包 | https://app.fmode.cn/dev/apig-pay/#/workshop/29-9 | 隐藏直达,适合课上多跑几轮 |
+| 纯享练习包 | https://app.fmode.cn/dev/apig-pay/#/workshop/99 | 公开入口,适合自助练习 |
+| 现场实训包 | https://app.fmode.cn/dev/apig-pay/#/workshop/299 | 公开入口,适合线下课程 |
+| 情报官实战包 | https://app.fmode.cn/dev/apig-pay/#/workshop/499 | 隐藏直达,适合企业实训 |
+| 情报官团队包 | https://app.fmode.cn/dev/apig-pay/#/workshop/699 | 公开入口,适合团队共用和复盘 |
+
+## 单 APIG 补额度入口
+
+当前小红书 live 采集走 VOC 社媒数据中台:
+
+```text
+https://app.fmode.cn/dev/apig-pay/?apigid=Vo3ROWEvDy&fun_id=HOkkX72PMF
+```
+
+如果用户只是社媒数据中台余额不足,可以使用这个入口补额度。课程和工作坊场景优先引导用户买技能套餐,因为技能套餐会同时给社媒数据中台和电商数据中台分配额度。
+
+## 技能包安装入口
+
+推荐 VS Code Claude Code 工作区安装:
+
+```powershell
+npx @gangvy/claude-code-voc-intelligence@latest workspace --smoke
+```
+
+安装后会写入:
+
+```text
+.\.claude\plugins\voc-intelligence
+.\.claude\skills\xiaohongshu-trend-intelligence\SKILL.md
+.\.mcp.json
+```
+
+安装完成后重启当前 VS Code Claude Code 会话。
+
+## Token 配置口径
+
+支付页登录后会显示“复制 OpenClaw 授权命令”。学员复制后在目标工作区终端执行,把 session token 写入本机凭证文件。
+
+技能包会按以下顺序读取 token:
+
+```text
+工具入参 vocToken / xiaohongshuToken
+workspace .env.local
+VOC_TOKEN
+VOC_SOCIAL_TOKEN
+~/.claude/voc-credentials.json
+```
+
+注意:
+
+- 不要把 token 写入 Git 仓库。
+- 不要把 token 放进讲义截图、报告或聊天输出。
+- 真实采集失败时,优先区分“没有 token”“token 失效”“余额不足”三类问题。
+
+## 课堂讲师话术
+
+可以这样讲:
+
+> 技能包负责告诉 Claude Code 怎么完成小红书趋势情报任务;套餐包负责给真实采集提供数据中台额度。先用 sample 模式让大家看懂流程,再让需要真实采集的学员购买套餐、配置 token,最后切到 live 模式。

+ 59 - 31
claude-code-voc-intelligence/install.js

@@ -30,19 +30,19 @@ function parseArgs(argv) {
 
 function usage() {
   return [
-    'Claude Code VOC Intelligence 安装器',
+    'Claude Code VOC Intelligence installer',
     '',
-    '用法:',
+    'Usage:',
     '  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       显示帮助'
+    'Options:',
+    '  --check          Check files and environment only',
+    '  --smoke          Run sample/preference/MCP smoke checks',
+    '  --skip-install   Skip npm install',
+    '  --no-next        Do not print next-step instructions',
+    '  --help, -h       Show help'
   ].join('\n');
 }
 
@@ -67,17 +67,32 @@ function readJson(relativePath) {
   return JSON.parse(fs.readFileSync(path.join(ROOT, relativePath), 'utf8'));
 }
 
+function writeMcpConfig(rootDir = ROOT) {
+  const mcpConfigPath = path.join(rootDir, '.mcp.json');
+  const serverPath = path.join(rootDir, 'mcp', 'src', 'server.js');
+  const mcpConfig = {
+    mcpServers: {
+      voc: {
+        command: 'node',
+        args: [serverPath],
+        cwd: rootDir
+      }
+    }
+  };
+  fs.writeFileSync(mcpConfigPath, `${JSON.stringify(mcpConfig, null, 2)}\n`, 'utf8');
+}
+
 function assertFile(relativePath) {
   const absolute = path.join(ROOT, relativePath);
   if (!fs.existsSync(absolute)) {
-    throw new Error(`缺少必要文件:${relativePath}`);
+    throw new Error(`Missing required file: ${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}+`);
+    throw new Error(`Node.js ${MIN_NODE_MAJOR}+ is required. Current version: ${process.version}`);
   }
 }
 
@@ -88,6 +103,13 @@ function checkFiles() {
     '.mcp.json',
     'skill-package-manifest.json',
     'skills/xiaohongshu-trend-intelligence/SKILL.md',
+    'skills/douyin-trend-intelligence/SKILL.md',
+    'skills/voc-issue-pool/SKILL.md',
+    'skills/voc-problem-deep-dive/SKILL.md',
+    'skills/voc-content-plan/SKILL.md',
+    'skills/voc-speaking-script/SKILL.md',
+    'skills/voc-competitor-map/SKILL.md',
+    'skills/voc-business-workflow/SKILL.md',
     'mcp/src/server.js'
   ].forEach(assertFile);
 
@@ -95,20 +117,19 @@ function checkFiles() {
   const pluginJson = readJson('.claude-plugin/plugin.json');
   const mcpJson = readJson('.mcp.json');
   if (!PACKAGE_NAMES.has(packageJson.name)) {
-    throw new Error('package.json name 不正确');
+    throw new Error('package.json name is invalid');
   }
   if (pluginJson.name !== 'voc-intelligence') {
-    throw new Error('.claude-plugin/plugin.json name 不正确');
+    throw new Error('.claude-plugin/plugin.json name is invalid');
   }
-  if (!mcpJson.mcpServers || !mcpJson.mcpServers['voc-intelligence']) {
-    throw new Error('.mcp.json 未配置 voc-intelligence MCP server');
+  if (!mcpJson.mcpServers || !(mcpJson.mcpServers.voc || mcpJson.mcpServers['voc-intelligence'])) {
+    throw new Error('.mcp.json does not configure the VOC MCP server');
   }
 }
 
 function checkClaudeCommand() {
   const command = process.platform === 'win32' ? 'where' : 'which';
-  const args = ['claude'];
-  const child = spawnSync(command, args, {
+  const child = spawnSync(command, ['claude'], {
     cwd: ROOT,
     encoding: 'utf8',
     stdio: 'pipe'
@@ -119,20 +140,27 @@ function checkClaudeCommand() {
 function printNextSteps({ claudeAvailable }) {
   const loadCommand = `claude --plugin-dir "${ROOT}"`;
   console.log('');
-  console.log('安装检查完成。');
+  console.log('Install check complete.');
   console.log('');
-  console.log('下一步:');
-  console.log(`  1. 启动 Claude Code:${loadCommand}`);
-  console.log('  2. 在 Claude Code 里说:');
+  console.log('Next step:');
+  console.log(`  1. Start Claude Code with: ${loadCommand}`);
+  console.log('  2. In Claude Code, say:');
+  console.log('     帮我看一下南昌餐饮最近顾客在关心什么,并告诉我先改哪里、下周发什么。');
+  console.log('  或者说:');
   console.log('     帮我做一份家装全屋定制行业的小红书趋势情报。');
-  console.log('     重点看女性客户下季度可能关注的设计元素、风格和消费顾虑。');
-  console.log('     先在聊天里给我第一轮样本观察和待确认问题,不要只给文件路径。');
+  console.log('     先用演示样例跑通流程,不要真实采集。');
+  console.log('     在聊天里给我第一轮样本观察和待确认问题。');
+  console.log('  或者说:');
+  console.log('     帮我做一份家装全屋定制行业的抖音趋势情报。');
+  console.log('     先用演示样例跑通流程,不要真实采集。');
+  console.log('     在聊天里给我第一轮样本观察和待确认问题。');
   console.log('');
   if (!claudeAvailable) {
-    console.log('提示:当前终端没有检测到 claude 命令。如果 Claude Code 已安装但不在 PATH 中,请在 Claude Code 所在终端里运行上面的加载命令。');
+    console.log('Tip: the current terminal did not detect the claude command.');
+    console.log('If Claude Code is installed elsewhere, run the command above in that terminal.');
     console.log('');
   }
-  console.log('真实采集需要配置小红书/TikHub token;未配置时仍可用 sample 模式做演示。');
+  console.log('Live collection requires a Xiaohongshu or Douyin/VOC token. Sample mode works without a token.');
 }
 
 function main() {
@@ -143,24 +171,25 @@ function main() {
   }
 
   console.log('');
-  console.log('Claude Code VOC Intelligence 安装检查');
-  console.log('====================================');
+  console.log('Claude Code VOC Intelligence install check');
+  console.log('==========================================');
   console.log('');
 
   checkNodeVersion();
   checkFiles();
+  writeMcpConfig(ROOT);
   console.log(`Node.js: ${process.version}`);
-  console.log('必要文件:OK');
+  console.log('Required files: OK');
 
   if (!opts.check && !opts.skipInstall) {
     console.log('');
-    console.log('正在安装依赖...');
+    console.log('Installing dependencies...');
     run('npm', ['install', '--omit=dev', '--ignore-scripts']);
   }
 
   if (opts.smoke) {
     console.log('');
-    console.log('正在运行 sample / 偏好记忆 / MCP smoke...');
+    console.log('Running sample/preference/MCP smoke checks...');
     run(process.execPath, ['scripts/smoke-package.js']);
   }
 
@@ -173,7 +202,6 @@ try {
   main();
 } catch (error) {
   console.error('');
-  console.error(`安装失败:${error.message}`);
+  console.error(`Install failed: ${error.message}`);
   process.exit(1);
 }
-

+ 55 - 6
claude-code-voc-intelligence/mcp/src/core/credentials.js

@@ -5,7 +5,7 @@ const os = require('os');
 function readJsonMaybe(filePath) {
   try {
     if (!filePath || !fs.existsSync(filePath)) return {};
-    return JSON.parse(fs.readFileSync(filePath, 'utf8'));
+    return JSON.parse(fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, ''));
   } catch {
     return {};
   }
@@ -15,22 +15,71 @@ function firstNonEmpty(values) {
   return values.find(value => typeof value === 'string' && value.trim()) || '';
 }
 
-function readXiaohongshuToken(input = {}) {
+function readEnvFileMaybe(filePath) {
+  try {
+    if (!filePath || !fs.existsSync(filePath)) return {};
+    const env = {};
+    const content = fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, '');
+    for (const rawLine of content.split(/\r?\n/)) {
+      const line = rawLine.trim();
+      if (!line || line.startsWith('#')) continue;
+      const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
+      if (!match) continue;
+      let value = match[2].trim();
+      if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
+        value = value.slice(1, -1);
+      }
+      env[match[1]] = value;
+    }
+    return env;
+  } catch {
+    return {};
+  }
+}
+
+function readWorkspaceEnvLocal() {
+  return readEnvFileMaybe(path.join(process.cwd(), '.env.local'));
+}
+
+function readVocToken(input = {}) {
+  const workspaceEnv = readWorkspaceEnvLocal();
   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,
+    input.douyinToken,
+    input.vocToken,
+    input['xiaohongshu-token'],
+    input['douyin-token'],
+    input['voc-token'],
+    input.token,
+    input.apiToken,
+    input['api-token'],
+    workspaceEnv.VOC_TOKEN,
+    workspaceEnv.VOC_SOCIAL_TOKEN,
+    workspaceEnv.XIAOHONGSHU_API_TOKEN,
+    workspaceEnv.DOUYIN_API_TOKEN,
+    workspaceEnv.DOUYIN_TOKEN,
+    process.env.VOC_TOKEN,
+    process.env.VOC_SOCIAL_TOKEN,
     process.env.XIAOHONGSHU_API_TOKEN,
+    process.env.DOUYIN_API_TOKEN,
+    process.env.DOUYIN_TOKEN,
     process.env.TIKHUB_API_TOKEN,
     process.env.FMODE_XIAOHONGSHU_TOKEN,
     process.env.FMODE_SOCIAL_TOKEN,
+    claudeCreds.vocToken,
+    claudeCreds.token,
     claudeCreds.xiaohongshuToken,
-    claudeCreds.tikhubToken,
-    openclawCreds.xiaohongshuToken,
-    openclawCreds.tikhubToken
+    claudeCreds.douyinToken,
+    claudeCreds.tikhubToken
   ]);
 }
 
+function readXiaohongshuToken(input = {}) {
+  return readVocToken(input);
+}
+
 module.exports = {
+  readVocToken,
   readXiaohongshuToken
 };

+ 106 - 0
claude-code-voc-intelligence/mcp/src/core/evidence-quality.js

@@ -0,0 +1,106 @@
+function asArray(value) {
+  return Array.isArray(value) ? value : [];
+}
+
+function hasSourceUrl(item) {
+  return Boolean(item && (item.sourceUrl || item.webUrl || item.url));
+}
+
+function hasImage(item) {
+  return Boolean(
+    item &&
+    (item.localCoverPath ||
+      item.coverUrl ||
+      (Array.isArray(item.imageUrls) && item.imageUrls.length))
+  );
+}
+
+function hasText(item) {
+  return Boolean(
+    item &&
+    String(item.contentExcerpt || item.rawExcerpt || item.content || item.desc || item.title || '').trim()
+  );
+}
+
+function assessEvidenceQuality({ items, comments, itemLabel = '样本', collectionMode = 'sample' }) {
+  const rows = asArray(items);
+  const commentRows = asArray(comments);
+  const sourceUrlCount = rows.filter(hasSourceUrl).length;
+  const imageCount = rows.filter(hasImage).length;
+  const textCount = rows.filter(hasText).length;
+  const highLikeCommentCount = commentRows.filter(comment => Number(comment.likeCount || 0) > 0).length;
+
+  let score = 0;
+  if (rows.length >= 8) score += 2;
+  else if (rows.length >= 3) score += 1;
+  if (commentRows.length >= 50) score += 2;
+  else if (commentRows.length >= 10) score += 1;
+  if (sourceUrlCount >= Math.min(rows.length, 3)) score += 1;
+  if (imageCount >= Math.min(rows.length, 3)) score += 1;
+  if (highLikeCommentCount >= 3) score += 1;
+  if (textCount >= Math.min(rows.length, 3)) score += 1;
+
+  const grade = score >= 7 ? 'A' : score >= 4 ? 'B' : 'C';
+  const canMakeBusinessDecision = grade === 'A' || (grade === 'B' && collectionMode === 'live');
+  const recommendation = canMakeBusinessDecision
+    ? '可以作为第一轮经营判断依据,但仍建议结合门店/销售/私域反馈校准。'
+    : '只适合作为初步机会假设,不建议直接当最终经营结论。';
+
+  return {
+    grade,
+    itemLabel,
+    itemCount: rows.length,
+    commentCount: commentRows.length,
+    sourceUrlCount,
+    imageCount,
+    textCount,
+    highLikeCommentCount,
+    canMakeBusinessDecision,
+    recommendation
+  };
+}
+
+function buildEvidenceQualitySection(quality) {
+  return [
+    '## 证据质量门槛',
+    '',
+    `- 本轮证据质量:${quality.grade}`,
+    `- ${quality.itemLabel}数量:${quality.itemCount}`,
+    `- 评论数量:${quality.commentCount}`,
+    `- 含原文/原视频链接:${quality.sourceUrlCount}`,
+    `- 含图片/封面:${quality.imageCount}`,
+    `- 含文本摘录:${quality.textCount}`,
+    `- 含点赞评论:${quality.highLikeCommentCount}`,
+    `- 是否适合直接做经营判断:${quality.canMakeBusinessDecision ? '适合第一轮判断' : '仅适合初步假设'}`,
+    `- 使用建议:${quality.recommendation}`
+  ];
+}
+
+function buildNoSampleFallbackMessage({ platformLabel, profile, examples = [] }) {
+  const keywords = asArray(profile && profile.keywords).slice(0, 5).join('、') || '当前关键词';
+  const exampleLines = examples.length ? examples.map(item => `- ${item}`) : [
+    '- 换成更具体的品类词、场景词或城市词',
+    '- 从产品词扩展到用户真实说法',
+    '- 先用 sample 模式演示完整流程,但明确标注非真实采集'
+  ];
+
+  return [
+    `本次 live 模式没有采到可用于分析的${platformLabel}样本。`,
+    '',
+    '这不代表行业没有机会,更可能是关键词过窄、平台内容表达不一样,或本轮上游没有返回可用内容。',
+    '',
+    '建议按这三步继续:',
+    `1. 先保留当前方向:${keywords}`,
+    '2. 自动换一组更贴近用户说法的关键词再试一次',
+    '3. 如果仍然没有样本,降级到相关大词做第一轮机会假设,并明确标注“非真实采集/待验证”',
+    '',
+    '可尝试的关键词方向:',
+    ...exampleLines
+  ].join('\n');
+}
+
+module.exports = {
+  assessEvidenceQuality,
+  buildEvidenceQualitySection,
+  buildNoSampleFallbackMessage
+};

+ 117 - 0
claude-code-voc-intelligence/mcp/src/core/payment-links.js

@@ -0,0 +1,117 @@
+const VOC_SOCIAL_APIG_ID = 'Vo3ROWEvDy';
+const VOC_RECHARGE_FUN_ID = 'HOkkX72PMF';
+const VOC_RECHARGE_BASE_URL = 'https://app.fmode.cn/dev/apig-pay/';
+const PARSE_USERS_ME_URL = 'https://server.fmode.cn/parse/users/me';
+const DEFAULT_WORKSHOP_PACKAGE_SLUG = '99';
+const VOC_WORKSHOP_PACKAGE_SLUGS = {
+  trial199: '19-9',
+  trial299: '29-9',
+  selfPractice: '99',
+  onsiteTraining: '299',
+  teamDelivery: '699'
+};
+
+function buildVocRechargeUrl(options = {}) {
+  const url = new URL(VOC_RECHARGE_BASE_URL);
+  if (options.userId) {
+    url.searchParams.set('user', String(options.userId));
+  }
+  url.searchParams.set('apigid', VOC_SOCIAL_APIG_ID);
+  url.searchParams.set('fun_id', VOC_RECHARGE_FUN_ID);
+  return url.toString();
+}
+
+function buildVocWorkshopUrl(options = {}) {
+  const url = new URL(VOC_RECHARGE_BASE_URL);
+  if (options.userId) {
+    url.searchParams.set('user', String(options.userId));
+  }
+  const slug = options.slug ? String(options.slug) : '';
+  url.hash = slug ? `/workshop/${slug}` : '/workshop';
+  return url.toString();
+}
+
+async function resolveVocUserId(token) {
+  if (!token) return '';
+  try {
+    const response = await fetch(PARSE_USERS_ME_URL, {
+      method: 'GET',
+      headers: {
+        'X-Parse-Application-Id': 'ncloudmaster',
+        'X-Parse-Session-Token': token,
+        Accept: 'application/json'
+      }
+    });
+    if (!response.ok) return '';
+    const json = await response.json();
+    return json && json.objectId ? String(json.objectId) : '';
+  } catch {
+    return '';
+  }
+}
+
+async function buildVocRechargeInfo(options = {}) {
+  const userId = await resolveVocUserId(options.token);
+  const workshopUrl = buildVocWorkshopUrl({ userId });
+  const recommendedPackageUrl = buildVocWorkshopUrl({
+    userId,
+    slug: options.packageSlug || DEFAULT_WORKSHOP_PACKAGE_SLUG
+  });
+  return {
+    paymentUrl: buildVocRechargeUrl({ userId }),
+    fallbackPaymentUrl: buildVocRechargeUrl(),
+    workshopUrl,
+    recommendedPackageUrl,
+    packageUrls: {
+      trial199: buildVocWorkshopUrl({ userId, slug: VOC_WORKSHOP_PACKAGE_SLUGS.trial199 }),
+      trial299: buildVocWorkshopUrl({ userId, slug: VOC_WORKSHOP_PACKAGE_SLUGS.trial299 }),
+      selfPractice: buildVocWorkshopUrl({ userId, slug: VOC_WORKSHOP_PACKAGE_SLUGS.selfPractice }),
+      onsiteTraining: buildVocWorkshopUrl({ userId, slug: VOC_WORKSHOP_PACKAGE_SLUGS.onsiteTraining }),
+      teamDelivery: buildVocWorkshopUrl({ userId, slug: VOC_WORKSHOP_PACKAGE_SLUGS.teamDelivery })
+    },
+    apigId: VOC_SOCIAL_APIG_ID,
+    funId: VOC_RECHARGE_FUN_ID,
+    personalized: Boolean(userId)
+  };
+}
+
+function buildMissingTokenMessage(paymentUrl, recharge = {}, options = {}) {
+  const platformLabel = options.platformLabel || '小红书';
+  const packageUrl = recharge.recommendedPackageUrl || recharge.workshopUrl || paymentUrl;
+  return [
+    `还没有检测到 VOC-AI 数据服务 token,暂时不能进行真实 ${platformLabel} 采集。`,
+    '',
+    `请先打开充值/开通链接:${paymentUrl}`,
+    '',
+    `课程或工作坊用户也可以打开套餐入口:${packageUrl}`,
+    '',
+    '完成充值或开通后,把服务方提供的 session token 配置到当前工作区 `.env.local` 或 `~/.claude/voc-credentials.json`,再重新运行 live 采集。',
+    '',
+    '系统只会用 token 发起请求,不会在报告、日志或聊天输出里回显 token。'
+  ].join('\n');
+}
+
+function buildRechargeRequiredMessage(paymentUrl, recharge = {}, options = {}) {
+  const platformLabel = options.platformLabel || '小红书';
+  const packageUrl = recharge.recommendedPackageUrl || recharge.workshopUrl || paymentUrl;
+  return [
+    `VOC-AI 数据服务余额不足,暂时不能继续真实 ${platformLabel} 采集。`,
+    '',
+    `请先打开充值链接补充额度:${paymentUrl}`,
+    '',
+    `课程或工作坊用户也可以打开套餐入口:${packageUrl}`,
+    '',
+    `我已经隐藏了底层接口权限/余额报错,不会把真实 ${platformLabel} 接口 403 或权限信息直接展示给用户。`
+  ].join('\n');
+}
+
+module.exports = {
+  VOC_SOCIAL_APIG_ID,
+  VOC_RECHARGE_FUN_ID,
+  VOC_WORKSHOP_PACKAGE_SLUGS,
+  buildVocRechargeInfo,
+  buildVocRechargeUrl,
+  buildVocWorkshopUrl,
+  buildMissingTokenMessage,
+  buildRechargeRequiredMessage
+};

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

@@ -0,0 +1,259 @@
+const { DouyinApi } = require('../../providers/douyin-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 videoScore(video) {
+  return normalizeCount(video.likeCount) +
+    normalizeCount(video.commentCount) * 3 +
+    normalizeCount(video.shareCount) * 2 +
+    normalizeCount(video.playCount) * 0.02;
+}
+
+function pickFirstString(...values) {
+  return values
+    .flat()
+    .filter(value => typeof value === 'string')
+    .map(value => value.trim())
+    .find(Boolean) || '';
+}
+
+function nestedValue(object, path) {
+  return path.split('.').reduce((current, key) => (
+    current && typeof current === 'object' ? current[key] : undefined
+  ), object);
+}
+
+function collectUrls(value, result = []) {
+  if (!value) return result;
+  if (typeof value === 'string') {
+    if (/^https?:\/\//i.test(value) && !result.includes(value)) result.push(value);
+    return result;
+  }
+  if (Array.isArray(value)) {
+    value.forEach(item => collectUrls(item, result));
+    return result;
+  }
+  if (typeof value !== 'object') return result;
+
+  ['url', 'url_default', 'url_pre', 'original_url', 'large', 'middle', 'thumbnail', 'image_url', 'imageUrl']
+    .forEach(key => collectUrls(value[key], result));
+
+  ['url_list', 'urlList', 'info_list', 'infoList', 'images', 'image_list', 'imageList']
+    .forEach(key => collectUrls(value[key], result));
+
+  return result;
+}
+
+function extractImageUrls(video = {}, raw = {}) {
+  const result = [];
+  [
+    video.cover,
+    video.origin_cover,
+    video.dynamic_cover,
+    video.video && video.video.cover,
+    video.video && video.video.origin_cover,
+    video.video && video.video.dynamic_cover,
+    raw.cover,
+    raw.origin_cover,
+    raw.dynamic_cover,
+    raw.video && raw.video.cover,
+    raw.video && raw.video.origin_cover,
+    raw.video && raw.video.dynamic_cover
+  ].forEach(value => collectUrls(value, result));
+  return result;
+}
+
+function pickSourceUrl(video = {}, raw = {}) {
+  const id = video.aweme_id || video.id || raw.aweme_id || raw.id || '';
+  const candidates = [
+    nestedValue(video, 'share_info.link'),
+    nestedValue(raw, 'share_info.link'),
+    nestedValue(video, 'share_info.share_url'),
+    nestedValue(raw, 'share_info.share_url'),
+    video.share_url,
+    raw.share_url,
+    video.web_url,
+    raw.web_url,
+    video.url,
+    raw.url,
+    id ? `https://www.douyin.com/video/${id}` : ''
+  ].filter(Boolean);
+  return pickFirstString(candidates);
+}
+
+function sourceUrlVerified(url = '') {
+  return /douyin\.com|v\.douyin\.com|aweme/i.test(String(url || ''));
+}
+
+function normalizeVideo(raw = {}, keyword = '', detail) {
+  const video = detail && (detail.aweme_detail || detail.aweme_info || detail.id) ? (detail.aweme_detail || detail.aweme_info || detail) : raw;
+  const item = video.aweme_info || raw.aweme_info || video;
+  const author = item.author || raw.author || {};
+  const sourceUrl = pickSourceUrl(item, raw);
+  const imageUrls = extractImageUrls(item, raw);
+  const stats = item.statistics || raw.statistics || {};
+  const desc = item.desc || item.title || raw.desc || raw.title || '';
+  const id = item.aweme_id || item.id || raw.aweme_id || raw.id || '';
+  const tags = [];
+  (Array.isArray(item.cha_list) ? item.cha_list : []).forEach(tag => {
+    const text = tag?.cha_name || tag?.desc || tag?.title || tag?.name || '';
+    if (text) tags.push(String(text));
+  });
+  const textExtra = Array.isArray(item.text_extra) ? item.text_extra : [];
+  textExtra.forEach(extra => {
+    const text = extra?.hashtag_name || extra?.name || extra?.text || '';
+    if (text) tags.push(String(text));
+  });
+
+  return {
+    id,
+    videoId: id,
+    platform: 'douyin',
+    keyword,
+    title: desc,
+    desc,
+    content: desc,
+    contentExcerpt: String(desc || '').replace(/\s+/g, ' ').trim(),
+    rawExcerpt: String(desc || '').replace(/\s+/g, ' ').trim(),
+    author: author.nickname || author.name || '',
+    authorId: author.uid || author.id || '',
+    authorSecUid: author.sec_uid || author.secUid || '',
+    likeCount: normalizeCount(stats.digg_count || item.like_count || item.digg_count),
+    collectCount: normalizeCount(stats.collect_count || item.collect_count || item.favorite_count),
+    commentCount: normalizeCount(stats.comment_count || item.comment_count),
+    shareCount: normalizeCount(stats.share_count || item.share_count),
+    playCount: normalizeCount(stats.play_count || item.play_count),
+    timestamp: item.create_time || raw.create_time || '',
+    url: sourceUrl,
+    webUrl: sourceUrl,
+    shareUrl: sourceUrl,
+    sourceUrl,
+    sourceUrlVerified: sourceUrlVerified(sourceUrl),
+    fallbackUrl: id ? `https://www.douyin.com/video/${id}` : '',
+    coverUrl: imageUrls[0] || '',
+    imageUrls,
+    tags: [...new Set(tags)].filter(Boolean)
+  };
+}
+
+function normalizeComment(raw = {}, videoId, keyword) {
+  const user = raw.user_info || raw.user || {};
+  return {
+    id: raw.cid || raw.id || raw.comment_id || `${videoId}-${Math.random().toString(36).slice(2, 8)}`,
+    cid: raw.cid || raw.comment_id || raw.id || '',
+    noteId: videoId,
+    videoId,
+    platform: 'douyin',
+    keyword,
+    text: raw.text || raw.content || '',
+    likeCount: normalizeCount(raw.digg_count || raw.like_count || raw.likeCount),
+    author: user.nickname || user.name || '',
+    authorId: user.uid || user.user_id || user.id || '',
+    createTime: raw.create_time || '',
+    ipLocation: raw.ip_label || raw.ip_location || '',
+    replyCommentTotal: normalizeCount(raw.reply_comment_total || raw.reply_count || 0)
+  };
+}
+
+function extractSearchVideos(data = {}) {
+  const cards = data.business_data || data.cards || data.items || [];
+  if (!Array.isArray(cards)) return [];
+  return cards
+    .map(card => card?.data?.aweme_info || card?.data?.aweme_detail?.aweme_info || card?.data?.aweme_info || card?.aweme_info || card)
+    .filter(item => item && (item.aweme_id || item.id));
+}
+
+function extractComments(data = {}) {
+  const comments = data.comments || data.comment_list || [];
+  return Array.isArray(comments) ? comments : [];
+}
+
+async function collectLiveDataset({ profile, token, options = {} }) {
+  const api = new DouyinApi({ token, baseUrl: options.baseUrl });
+  const keywordLimit = Math.max(1, Number(options.keywordLimit || 1));
+  const videosPerKeyword = Math.max(1, Number(options.videosPerKeyword || options.notesPerKeyword || 1));
+  const maxCommentPages = Math.max(0, Number(options.maxCommentPages ?? 1));
+  const keywords = (profile.keywords || []).slice(0, keywordLimit);
+  const videos = [];
+  const comments = [];
+  const warnings = [];
+  const errors = [];
+
+  for (const keyword of keywords) {
+    try {
+      const search = await api.searchVideos({
+        keyword,
+        cursor: 0,
+        sortType: options.sortType || options.sort || '1',
+        publishTime: options.publishTime || options.publish_time || '7',
+        filterDuration: options.filterDuration || options.filter_duration || '0',
+        contentType: options.contentType || options.content_type || '1',
+        searchId: options.searchId || options.search_id || '',
+        backtrace: options.backtrace || ''
+      });
+      const rawVideos = extractSearchVideos(search.data)
+        .map(raw => normalizeVideo(raw, keyword))
+        .sort((a, b) => videoScore(b) - videoScore(a))
+        .slice(0, videosPerKeyword);
+
+      for (const video of rawVideos) {
+        videos.push(video);
+        let cursor = 0;
+        for (let page = 0; page < maxCommentPages; page++) {
+          try {
+            const commentResult = await api.getVideoComments({ awemeId: video.id, cursor, count: 20 });
+            extractComments(commentResult.data).forEach(rawComment => {
+              comments.push(normalizeComment(rawComment, video.id, keyword));
+            });
+            cursor = Number(commentResult.data.cursor || 0);
+            if (!commentResult.data.has_more || !cursor) break;
+          } catch (error) {
+            warnings.push({
+              stage: 'video_comments',
+              keyword,
+              videoId: video.id,
+              message: error.message,
+              httpStatus: error.httpStatus
+            });
+            break;
+          }
+        }
+      }
+    } catch (error) {
+      errors.push({
+        stage: 'search_videos',
+        keyword,
+        message: error.message,
+        httpStatus: error.httpStatus
+      });
+    }
+  }
+
+  return {
+    videos,
+    comments,
+    warnings,
+    errors
+  };
+}
+
+module.exports = {
+  collectLiveDataset,
+  normalizeVideo,
+  normalizeComment,
+  extractSearchVideos,
+  extractComments,
+  pickSourceUrl,
+  sourceUrlVerified
+};

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

@@ -0,0 +1,130 @@
+const path = require('path');
+const { readJson, writeJson, defaultMemoryPath } = require('../../core/memory-store');
+
+const DEFAULT_MEMORY_FILE = 'douyin-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.3.10',
+    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
+};

+ 318 - 0
claude-code-voc-intelligence/mcp/src/features/douyin-trend/report.js

@@ -0,0 +1,318 @@
+const fs = require('fs');
+const path = require('path');
+const { rankTopicsWithMemory, cleanList } = require('./preference-memory');
+const { buildSignalAnalysis } = require('./signal-extractor');
+const {
+  firstTerm,
+  firstDistinctTerm,
+  findTopComment,
+  buildOpportunityTopics,
+  buildBusinessTalk
+} = require('../xiaohongshu-trend/topic-builder');
+const {
+  assessEvidenceQuality,
+  buildEvidenceQualitySection
+} = require('../../core/evidence-quality');
+
+function ensureDir(dirPath) {
+  fs.mkdirSync(dirPath, { recursive: true });
+}
+
+function scoreVideo(video) {
+  return Number(video.likeCount || 0) +
+    Number(video.commentCount || 0) * 3 +
+    Number(video.shareCount || 0) * 2 +
+    Number(video.playCount || 0) * 0.02;
+}
+
+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(1).replace(/\.0$/, '')}w`;
+  }
+  return String(Math.round(count));
+}
+
+function truncateText(value, limit = 120) {
+  const text = String(value || '').replace(/\s+/g, ' ').trim();
+  if (text.length <= limit) return text;
+  return `${text.slice(0, limit - 1)}...`;
+}
+
+function markdownUrl(value) {
+  const url = String(value || '').trim().replace(/[<>]/g, '');
+  return url ? `<${url}>` : '';
+}
+
+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, videos, comments, collectionMode, signals }) {
+  const audience = asList(profile.targetAudience).slice(0, 3).join('、') || '未标注';
+  const keywords = asList(profile.keywords).slice(0, 5).join('、') || '未标注';
+  const presetLabel = signals && signals.preset && signals.preset.label ? signals.preset.label : '';
+  return [
+    '## 数据口径',
+    '',
+    '- 平台:抖音',
+    `- 行业/品类:${profile.industry || presetLabel || '未标注'}`,
+    profile.businessType ? `- 业务类型:${profile.businessType}` : '',
+    `- 目标人群:${audience}`,
+    `- 关注关键词:${keywords}`,
+    `- 样本:${videos.length} 条视频 / ${comments.length} 条评论`,
+    presetLabel ? `- 行业模板:${presetLabel}` : '',
+    collectionMode === 'sample'
+      ? '- 说明:本次为演示样例,用于确认交付形式;接入 VOC 服务后可切换真实抖音采集。'
+      : ''
+  ].filter(Boolean);
+}
+
+function buildEvidenceReason(video, topComment) {
+  if (video.evidenceReason) return video.evidenceReason;
+  if (topComment && topComment.text) {
+    return '这条样本同时提供了视频表达和用户评论,可以用来校验趋势判断是否贴近真实顾虑。';
+  }
+  return '这条样本互动较高,适合用作本轮趋势判断的视频证据。';
+}
+
+function buildSourceLine(video) {
+  const url = video.sourceUrl || video.webUrl || video.url || '';
+  if (!url) return '   - 原视频:暂无可访问链接';
+  return `   - 原视频:[打开视频](${markdownUrl(url)})`;
+}
+
+function buildEvidenceSection(videos, comments) {
+  const rows = videos.slice(0, 3).map((video, index) => {
+    const topComment = findTopComment(comments, video.id);
+    const coverUrl = video.localCoverPath || video.coverUrl || (Array.isArray(video.imageUrls) ? video.imageUrls[0] : '');
+    const excerpt = truncateText(video.contentExcerpt || video.rawExcerpt || video.desc || video.content, 120);
+    const topCommentText = topComment ? truncateText(topComment.text, 120) : '';
+    return [
+      `${index + 1}. ${video.title || '未命名视频'}`,
+      `   - 作者:${video.author || '未标注'} | 互动:${formatCount(video.likeCount)}赞 / ${formatCount(video.commentCount)}评 / ${formatCount(video.shareCount)}转 / ${formatCount(video.playCount)}播`,
+      buildSourceLine(video),
+      `   - 为什么值得看:${buildEvidenceReason(video, topComment)}`,
+      coverUrl ? `   - 图片:\n\n![视频封面](${markdownUrl(coverUrl)})` : '   - 图片:暂无可展示封面,可点开视频查看。',
+      excerpt ? `   - 视频文案:${excerpt}` : '',
+      topCommentText ? `   - 高赞评论:${topCommentText}` : ''
+    ].filter(Boolean).join('\n');
+  });
+  return [
+    '## 证据样本',
+    '',
+    rows.length ? rows.join('\n') : '暂无可展示样本。'
+  ];
+}
+
+function buildConcernLines(decisionSignals, applicationLabel) {
+  if (!decisionSignals.length) {
+    return ['- 暂未出现稳定高频顾虑,建议先扩大关键词或评论样本。'];
+  }
+  return decisionSignals.slice(0, 5).map(item => (
+    `- ${item.term}:在样本中出现 ${item.count} 次,适合转成${applicationLabel || '业务验证或内容选题'}。`
+  ));
+}
+
+function buildObservationLines({ videos, comments, signals }) {
+  const preference = firstTerm(signals.preferenceSignals, '核心卖点');
+  const secondPreference = firstDistinctTerm(signals.preferenceSignals.slice(1), [preference], '');
+  const concern = firstDistinctTerm(signals.decisionSignals, [preference], '主要顾虑');
+  const secondConcern = firstDistinctTerm(signals.decisionSignals, [preference, concern], '');
+  const element = firstDistinctTerm(signals.elementSignals, [preference, concern], '具体元素');
+  const risk = firstDistinctTerm(signals.riskSignals, [preference, concern, element], '潜在风险');
+  const topVideo = videos[0] || {};
+  const topComment = topVideo.id ? findTopComment(comments, topVideo.id) : comments[0];
+  return [
+    `1. 样本里“${preference}”是一个明显入口,但它需要和“${concern}”一起看,不能直接等同于真实选择理由。`,
+    secondPreference
+      ? `2. “${secondPreference}”也在样本中出现,说明用户可能在比较不同场景、人群和使用条件。`
+      : '2. 暂时还没有第二个稳定偏好信号,建议下一轮补充更细的关键词或评论样本。',
+    secondConcern
+      ? `3. 用户顾虑里除了“${concern}”,还出现了“${secondConcern}”,这两个点需要用你的真实客户反馈来排优先级。`
+      : `3. 当前顾虑仍集中在“${concern}”,还不能判断是否足够普遍。`,
+    `4. “${element}”这类具体元素可以继续验证,因为它更容易连接到内容表达、成本、维护和转化承诺。`,
+    topComment && topComment.text
+      ? `5. 高赞评论提到“${truncateText(topComment.text, 80)}”,适合作为下一轮向客户/销售/社群核验的问题。`
+      : `5. 当前评论证据还不够强,下一轮需要补充更多真实评论原话来校准“${risk}”这类风险点。`
+  ];
+}
+
+function buildHypothesisLines({ signals }) {
+  const preference = firstTerm(signals.preferenceSignals, '核心卖点');
+  const concern = firstDistinctTerm(signals.decisionSignals, [preference], '主要顾虑');
+  const element = firstDistinctTerm(signals.elementSignals, [preference, concern], '具体元素');
+  return [
+    `1. ${preference} 可能不是单独成立的趋势,而是和“${concern}”一起影响用户选择。`,
+    `2. ${element} 更适合继续验证,因为它能连接到场景、成本、效果证据和落地方案。`,
+    '3. 第一轮样本更像是在提示“用户为什么犹豫”,下一轮需要用你的真实客户反馈校准。'
+  ];
+}
+
+function buildDynamicValidationQuestions({ videos, comments, signals, rankedTopics }) {
+  const preference = firstTerm(signals.preferenceSignals, '当前卖点');
+  const concern = firstDistinctTerm(signals.decisionSignals, [preference], '当前顾虑');
+  const element = firstDistinctTerm(signals.elementSignals, [preference, concern], '当前元素');
+  const risk = firstDistinctTerm(signals.riskSignals, [preference, concern, element], '风险点');
+  const topVideo = videos[0] || {};
+  const topComment = topVideo.id ? findTopComment(comments, topVideo.id) : comments[0];
+  const topTopic = rankedTopics[0] || {};
+  return [
+    `样本里“${preference}”和“${concern}”同时出现,你实际接触的用户是先被${preference}吸引,还是先担心${concern}?`,
+    topComment && topComment.text
+      ? `这条高赞评论“${truncateText(topComment.text, 80)}”像不像你销售、客服、社群或私域里真实听到的问题?如果不像,真实问题更接近什么?`
+      : '当前样本缺少高赞评论证据,你手上有没有最近用户反复问的一句话?',
+    `围绕“${element}”,你们现在有没有可证明的案例、价格/成本、效果证据或售后承诺?没有的话,这个方向先不要直接定稿。`,
+    topTopic.title
+      ? `如果只继续深挖一个方向,你更想验证“${topTopic.title}”,还是换成更贴近你业务的方向?`
+      : `如果只继续深挖一个方向,你更想验证“${risk}”,还是换成更贴近你业务的方向?`
+  ];
+}
+
+function buildOneLine({ profile, signals }) {
+  const industry = profile.industry || (signals.preset && signals.preset.label) || '当前行业';
+  const preference = firstTerm(signals.preferenceSignals, '核心卖点');
+  const concern = firstDistinctTerm(signals.decisionSignals, [preference], '主要顾虑');
+  const risk = firstDistinctTerm(signals.riskSignals, [preference, concern], '风险点');
+  return `本轮${industry}最值得关注的不是单一“${preference}”,而是“${preference}、${concern}、${risk}”一起进入用户决策。`;
+}
+
+function buildTrendReport({ profile, dataset, collectionMode, memory = {} }) {
+  const videos = [...(dataset.videos || [])].sort((a, b) => scoreVideo(b) - scoreVideo(a));
+  const comments = [...(dataset.comments || [])].sort((a, b) => Number(b.likeCount || 0) - Number(a.likeCount || 0));
+  const signals = buildSignalAnalysis({ profile, videos, comments });
+  const topTopics = buildOpportunityTopics({ profile, notes: videos, comments, signals });
+  const rankedTopics = rankTopicsWithMemory(topTopics, memory);
+  const evidenceQuality = assessEvidenceQuality({
+    items: videos,
+    comments,
+    itemLabel: '视频',
+    collectionMode
+  });
+  const validationQuestions = buildDynamicValidationQuestions({
+    videos,
+    comments,
+    signals,
+    rankedTopics
+  });
+  const salesTalk = buildBusinessTalk({ profile, signals });
+  const applicationLabel = signals.preset && signals.preset.applicationLabel;
+
+  const assistantMessage = [
+    `# 抖音趋势情报${collectionMode === 'sample' ? '(演示样例)' : ''}`,
+    '',
+    '## 第一轮初步判断(待校准)',
+    '',
+    `${buildOneLine({ profile, signals })} 这还不是最终结论,需要先结合你的真实客户反馈做一轮校准。`,
+    '',
+    ...buildScopeSection({ profile, videos, comments, collectionMode, signals }),
+    '',
+    ...buildEvidenceQualitySection(evidenceQuality),
+    '',
+    ...buildMemorySection(memory),
+    memoryHasSignal(memory) ? '' : '',
+    '## 视频观察 Top 5',
+    '',
+    ...buildObservationLines({ videos, comments, signals }),
+    '',
+    '## 本轮机会假设',
+    '',
+    ...buildHypothesisLines({ signals }),
+    '',
+    '## 用户顾虑/决策阻力',
+    '',
+    ...buildConcernLines(signals.decisionSignals, applicationLabel),
+    '',
+    ...buildEvidenceSection(videos, comments),
+    '',
+    '## 可继续验证的选题方向',
+    '',
+    ...rankedTopics.map((topic, index) => [
+      `${index + 1}. ${topic.title}`,
+      `   - 角度:${topic.angle}`,
+      `   - 证据:${topic.evidence}`,
+      topic.confidence ? `   - 置信度:${topic.confidence}` : ''
+    ].filter(Boolean).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',
+      platform: 'douyin',
+      industryPreset: signals.preset && signals.preset.id,
+      videoCount: videos.length,
+      commentCount: comments.length,
+      topicCount: rankedTopics.length,
+      preferenceMemoryApplied: memoryHasSignal(memory),
+      topStyleSignals: signals.preferenceSignals.slice(0, 5),
+      topPreferenceSignals: signals.preferenceSignals.slice(0, 5),
+      topDecisionSignals: signals.decisionSignals.slice(0, 5),
+      topElementSignals: signals.elementSignals.slice(0, 5),
+      topRiskSignals: signals.riskSignals.slice(0, 5),
+      evidenceVideoCount: Math.min(videos.length, 3),
+      evidenceQuality,
+      validationQuestionCount: validationQuestions.length
+    },
+    data: {
+      profile,
+      industryPreset: signals.preset,
+      signals,
+      videos,
+      comments,
+      topTopics: rankedTopics,
+      validationQuestions,
+      preferenceMemory: memory,
+      salesTalk,
+      evidenceQuality
+    }
+  };
+}
+
+function writeReport(outputDir, report) {
+  ensureDir(outputDir);
+  const mdPath = path.join(outputDir, 'douyin-trend-report.md');
+  const jsonPath = path.join(outputDir, 'douyin-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
+};

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

@@ -0,0 +1,103 @@
+const { resolveIndustryPreset, cleanList } = require('../xiaohongshu-trend/industry-presets');
+
+function sampleImage(label, accent = '#d8c4a4') {
+  const safeLabel = String(label || '抖音趋势样本').slice(0, 24);
+  const svg = [
+    '<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="760" viewBox="0 0 1200 760">',
+    '<rect width="1200" height="760" fill="#f5f0e7"/>',
+    `<rect x="88" y="76" width="1024" height="528" rx="28" fill="${accent}" opacity="0.3"/>`,
+    '<rect x="148" y="130" width="416" height="394" rx="24" fill="#ffffff" opacity="0.86"/>',
+    '<rect x="620" y="132" width="440" height="72" rx="16" fill="#ffffff" opacity="0.82"/>',
+    '<rect x="620" y="236" width="360" height="52" rx="12" fill="#ffffff" opacity="0.74"/>',
+    '<rect x="620" y="320" width="390" height="52" rx="12" fill="#ffffff" opacity="0.68"/>',
+    '<rect x="620" y="404" width="310" height="52" rx="12" fill="#ffffff" opacity="0.62"/>',
+    `<text x="120" y="668" font-size="44" font-family="Arial, sans-serif" fill="#4a3727">${safeLabel}</text>`,
+    '<text x="120" y="714" font-size="24" font-family="Arial, sans-serif" fill="#806853">Douyin sample evidence image</text>',
+    '</svg>'
+  ].join('');
+  return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
+}
+
+function metric(index, base) {
+  return Math.max(1, Math.round(base - index * base * 0.28));
+}
+
+function buildVideo({ item, preset, keyword, index }) {
+  const id = `dy-${preset.id}-${String(index + 1).padStart(3, '0')}`;
+  const coverUrl = sampleImage(item.imageLabel || item.title, ['#b7a17a', '#dfc7a2', '#a7b8aa'][index % 3]);
+  const sourceUrl = `https://www.douyin.com/video/${id}?from=sample&share_token=sample-token-${preset.id}-${index + 1}`;
+  return {
+    id,
+    videoId: id,
+    platform: 'douyin',
+    keyword,
+    title: item.title,
+    desc: item.content,
+    content: item.content,
+    contentExcerpt: item.content,
+    author: item.author || `${preset.label}观察员`,
+    authorId: `${id}-author`,
+    authorSecUid: `${id}-secuid`,
+    likeCount: metric(index, 128000),
+    commentCount: metric(index, 8600),
+    shareCount: metric(index, 5200),
+    playCount: metric(index, 1880000),
+    createTime: new Date(Date.UTC(2026, 4, 25, 8 + index, 0, 0)).toISOString(),
+    timestamp: Date.UTC(2026, 4, 25, 8 + index, 0, 0),
+    sourceUrl,
+    webUrl: sourceUrl,
+    shareUrl: sourceUrl,
+    sourceUrlVerified: true,
+    coverUrl,
+    imageUrls: [coverUrl],
+    tags: item.tags || [keyword, preset.label],
+    statistics: {
+      digg_count: metric(index, 128000),
+      comment_count: metric(index, 8600),
+      share_count: metric(index, 5200),
+      play_count: metric(index, 1880000)
+    }
+  };
+}
+
+function buildComments({ item, videoId, keyword, index }) {
+  const comments = cleanList(item.comments).length
+    ? cleanList(item.comments)
+    : [`我最关心的是${keyword}到底适不适合自己,不想只看种草结论。`];
+  return comments.map((text, commentIndex) => ({
+    id: `${videoId}-comment-${commentIndex + 1}`,
+    cid: `${videoId}-comment-${commentIndex + 1}`,
+    noteId: videoId,
+    videoId,
+    platform: 'douyin',
+    keyword,
+    text,
+    likeCount: Math.max(1, 401 - index * 88 - commentIndex * 65),
+    replyCommentTotal: commentIndex === 0 ? 3 : 0,
+    createTime: new Date(Date.UTC(2026, 4, 25, 9 + index, commentIndex * 6, 0)).toISOString(),
+    user: {
+      nickname: `评论用户${index + 1}-${commentIndex + 1}`,
+      uid: `${videoId}-user-${commentIndex + 1}`
+    }
+  }));
+}
+
+function buildSampleDataset(profile = {}) {
+  const preset = resolveIndustryPreset(profile);
+  const keywords = cleanList(profile.keywords).length ? cleanList(profile.keywords) : preset.defaultKeywords;
+  const videos = [];
+  const comments = [];
+
+  preset.sampleNotes.slice(0, 3).forEach((item, index) => {
+    const keyword = keywords[index] || keywords[0] || preset.label;
+    const video = buildVideo({ item, preset, keyword, index });
+    videos.push(video);
+    comments.push(...buildComments({ item, videoId: video.id, keyword, index }));
+  });
+
+  return { videos, comments };
+}
+
+module.exports = {
+  buildSampleDataset
+};

+ 79 - 0
claude-code-voc-intelligence/mcp/src/features/douyin-trend/signal-extractor.js

@@ -0,0 +1,79 @@
+const { buildSignalTerms, unique } = require('../xiaohongshu-trend/industry-presets');
+
+function textOfItem(item = {}) {
+  return [
+    item.title,
+    item.desc,
+    item.content,
+    item.text,
+    item.contentExcerpt,
+    item.rawExcerpt,
+    item.author && item.author.nickname,
+    ...(Array.isArray(item.tags) ? item.tags : [])
+  ].filter(Boolean).join(' ');
+}
+
+function collectTerms(items, terms) {
+  return unique(terms)
+    .map(term => {
+      const evidence = items.filter(item => textOfItem(item).includes(term));
+      return {
+        term,
+        count: evidence.length,
+        evidenceIds: evidence.map(item => item.id || item.videoId || '').filter(Boolean).slice(0, 5)
+      };
+    })
+    .filter(item => item.count > 0)
+    .sort((a, b) => b.count - a.count || a.term.localeCompare(b.term));
+}
+
+function collectTagTerms(videos = []) {
+  const counter = new Map();
+  videos.forEach(video => {
+    (Array.isArray(video.tags) ? video.tags : []).forEach(tag => {
+      const key = String(tag || '').trim();
+      if (!key) return;
+      counter.set(key, (counter.get(key) || 0) + 1);
+    });
+  });
+  return [...counter.entries()]
+    .map(([term, count]) => ({ term, count, evidenceIds: [] }))
+    .sort((a, b) => b.count - a.count || a.term.localeCompare(b.term));
+}
+
+function mergeSignals(primary, fallback) {
+  const seen = new Set();
+  return [...primary, ...fallback]
+    .filter(item => {
+      if (!item || !item.term || seen.has(item.term)) return false;
+      seen.add(item.term);
+      return true;
+    });
+}
+
+function buildSignalAnalysis({ profile = {}, videos = [], comments = [] } = {}) {
+  const { preset, preferenceTerms, decisionTerms, elementTerms, riskTerms } = buildSignalTerms(profile);
+  const allItems = [...videos, ...comments];
+  const tags = collectTagTerms(videos);
+  const keywordSignals = collectTerms(allItems, profile.keywords || []);
+
+  const preferenceSignals = mergeSignals(collectTerms(allItems, preferenceTerms), keywordSignals);
+  const decisionSignals = mergeSignals(collectTerms(allItems, decisionTerms), tags);
+  const elementSignals = mergeSignals(collectTerms(allItems, elementTerms), tags);
+  const riskSignals = collectTerms(allItems, riskTerms);
+
+  return {
+    preset,
+    preferenceSignals,
+    decisionSignals,
+    elementSignals,
+    riskSignals,
+    tagSignals: tags,
+    keywordSignals
+  };
+}
+
+module.exports = {
+  buildSignalAnalysis,
+  collectTerms
+};

+ 1 - 0
claude-code-voc-intelligence/mcp/src/features/douyin-trend/topic-builder.js

@@ -0,0 +1 @@
+module.exports = require('../xiaohongshu-trend/topic-builder');

+ 214 - 0
claude-code-voc-intelligence/mcp/src/features/voc-business-workflow/business-workflow.js

@@ -0,0 +1,214 @@
+const path = require('path');
+const { runXiaohongshuTrend } = require('../../tools/xiaohongshu-trend-run');
+const { runDouyinTrend } = require('../../tools/douyin-trend-run');
+const { runVocIssuePool } = require('../../tools/voc-issue-pool-run');
+const { runVocProblemDeepDive } = require('../../tools/voc-problem-deep-dive-run');
+const { runVocContentPlan } = require('../../tools/voc-content-plan-run');
+const { runVocSpeakingScript } = require('../../tools/voc-speaking-script-run');
+
+function asList(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 firstText(...values) {
+  for (const value of values) {
+    if (value && String(value).trim()) return String(value).trim();
+  }
+  return '';
+}
+
+function collectEvidence(trendResult) {
+  const comments = trendResult.data && Array.isArray(trendResult.data.comments)
+    ? trendResult.data.comments
+    : [];
+  const notes = trendResult.data && Array.isArray(trendResult.data.notes)
+    ? trendResult.data.notes
+    : [];
+  const videos = trendResult.data && Array.isArray(trendResult.data.videos)
+    ? trendResult.data.videos
+    : [];
+  const commentEvidence = comments
+    .slice(0, 12)
+    .map(comment => comment.text || comment.content)
+    .filter(Boolean);
+  const itemEvidence = [...notes, ...videos]
+    .slice(0, 5)
+    .map(item => firstText(item.title, item.desc, item.content, item.contentExcerpt))
+    .filter(Boolean);
+  return [...commentEvidence, ...itemEvidence].slice(0, 16);
+}
+
+function extractIssues(issuePoolResult, fallbackEvidence) {
+  const issues = issuePoolResult.data && Array.isArray(issuePoolResult.data.issues)
+    ? issuePoolResult.data.issues
+    : [];
+  const titles = issues.map(issue => issue.title || issue.issue || issue.name).filter(Boolean);
+  return titles.length ? titles : fallbackEvidence.slice(0, 3);
+}
+
+function compactSection(title, body, maxLines = 18) {
+  const lines = String(body || '').split('\n').filter(Boolean);
+  return [`## ${title}`, '', ...lines.slice(0, maxLines)].join('\n');
+}
+
+async function runBusinessWorkflow(input = {}) {
+  const platform = String(input.platform || input.sourcePlatform || 'douyin').toLowerCase().includes('xiao')
+    || String(input.platform || '').includes('小红书')
+    ? 'xiaohongshu'
+    : 'douyin';
+  const collectionMode = input.collectionMode || input['collection-mode'] || 'sample';
+  const project = firstText(input.project, input.brand, input.store, 'VOC经营闭环');
+  const industry = firstText(input.industry, input.category, '本地生活餐饮');
+  const brand = firstText(input.brand, input.store, project);
+  const scenario = firstText(input.scenario, input.scene, '经营诊断');
+  const audience = firstText(input.audience, input.targetAudience, '目标用户');
+  const keywords = asList(input.keywords).length ? asList(input.keywords) : asList(input.keyword);
+  const outputRoot = path.resolve(input.output || path.join('outputs', 'voc-business-workflow', new Date().toISOString().slice(0, 10)));
+
+  const trendInput = {
+    ...input,
+    project,
+    industry,
+    targetAudience: Array.isArray(input.targetAudience) ? input.targetAudience : [audience],
+    keywords: keywords.length ? keywords : undefined,
+    collectionMode,
+    output: path.join(outputRoot, platform === 'xiaohongshu' ? 'xiaohongshu-trend' : 'douyin-trend'),
+    keywordLimit: input.keywordLimit || input['keyword-limit'] || 2,
+    notesPerKeyword: input.notesPerKeyword || input['notes-per-keyword'] || 2,
+    videosPerKeyword: input.videosPerKeyword || input['videos-per-keyword'] || 2,
+    maxCommentPages: input.maxCommentPages || input['max-comment-pages'] || 1
+  };
+  const trendResult = platform === 'xiaohongshu'
+    ? await runXiaohongshuTrend(trendInput)
+    : await runDouyinTrend(trendInput);
+
+  if (trendResult.status !== 'ok') {
+    return {
+      ...trendResult,
+      assistantMessage: [
+        '# VOC经营闭环未完成',
+        '',
+        '第一步真实声音采集没有拿到可继续分析的样本,先按下面提示处理:',
+        '',
+        trendResult.assistantMessage || '',
+        '',
+        '处理完成后再说:继续做经营闭环。'
+      ].filter(Boolean).join('\n')
+    };
+  }
+
+  const evidence = collectEvidence(trendResult);
+  const issuePool = await runVocIssuePool({
+    project,
+    brand,
+    industry,
+    scenario,
+    audience,
+    evidence,
+    reportText: trendResult.assistantMessage
+  });
+  const issues = extractIssues(issuePool, evidence);
+  const topIssue = firstText(input.issue, input.problem, issues[0], '用户顾虑不清晰');
+  const deepDive = await runVocProblemDeepDive({
+    project,
+    brand,
+    industry,
+    scenario,
+    audience,
+    issue: topIssue,
+    evidence
+  });
+  const contentPlan = await runVocContentPlan({
+    project,
+    brand,
+    industry,
+    platform: platform === 'xiaohongshu' ? '小红书' : '抖音',
+    issues: issues.slice(0, 5),
+    reportText: deepDive.assistantMessage
+  });
+  const firstTopic = contentPlan.data && Array.isArray(contentPlan.data.plan)
+    ? contentPlan.data.plan[0]
+    : null;
+  const speakingScript = await runVocSpeakingScript({
+    project,
+    brand,
+    industry,
+    topic: firstText(input.topic, firstTopic && firstTopic.title, topIssue),
+    userIssue: firstText(firstTopic && firstTopic.userIssue, topIssue),
+    evidence: evidence.slice(0, 5),
+    feedback: input.feedback,
+    finalize: Boolean(input.finalize)
+  });
+
+  const files = [
+    ...(trendResult.files || []),
+    ...(issuePool.files || []),
+    ...(deepDive.files || []),
+    ...(contentPlan.files || []),
+    ...(speakingScript.files || [])
+  ];
+  const assistantMessage = [
+    '# VOC经营闭环(第一轮)',
+    '',
+    '你这轮能拿到:市场真实声音、问题优先级、一个单点解决动作、7天内容方向,以及一条可继续改稿的口播稿。',
+    '',
+    `- 平台:${platform === 'xiaohongshu' ? '小红书' : '抖音'}`,
+    `- 模式:${collectionMode}`,
+    `- 行业/品类:${industry}`,
+    `- 品牌/门店:${brand}`,
+    '',
+    compactSection('1. 市场和用户声音', trendResult.assistantMessage, 16),
+    '',
+    compactSection('2. 先改哪个问题', issuePool.assistantMessage, 18),
+    '',
+    compactSection('3. Top问题怎么解决', deepDive.assistantMessage, 18),
+    '',
+    compactSection('4. 下周账号发什么', contentPlan.assistantMessage, 18),
+    '',
+    compactSection('5. 第一条口播稿', speakingScript.assistantMessage, 18),
+    '',
+    '## 下一步建议',
+    '',
+    `1. 先确认 Top 问题“${topIssue}”是否真的最影响生意。`,
+    '2. 从 7 天内容计划里选 1 条进入改稿。',
+    '3. 7 天后反馈哪些动作有效/无效,让记忆继续优化。'
+  ].join('\n');
+
+  return {
+    status: 'ok',
+    assistantMessage,
+    summary: {
+      workflowStage: 'business_workflow_first_round',
+      platform,
+      collectionMode,
+      trendStatus: trendResult.status,
+      issueCount: issues.length,
+      topIssue,
+      contentTopicCount: contentPlan.summary && contentPlan.summary.topicCount,
+      speakingScriptStatus: speakingScript.status
+    },
+    data: {
+      trend: trendResult,
+      issuePool,
+      deepDive,
+      contentPlan,
+      speakingScript
+    },
+    files,
+    nextActions: [
+      `确认 Top 问题“${topIssue}”是否优先`,
+      '选择 7 天内容计划中的一条继续改成最终口播稿',
+      '执行 7 天后反馈有效/无效动作,让记忆继续收敛'
+    ],
+    warnings: [
+      collectionMode === 'sample' ? '本轮使用 sample 演示数据,不是真实采集。' : ''
+    ].filter(Boolean),
+    errors: []
+  };
+}
+
+module.exports = {
+  runBusinessWorkflow
+};

+ 198 - 0
claude-code-voc-intelligence/mcp/src/features/voc-competitor-map/competitor-map.js

@@ -0,0 +1,198 @@
+const fs = require('fs');
+const path = require('path');
+
+const DEFAULT_COMPETITOR_TYPES = [
+  {
+    name: '同城同品类强势门店',
+    why: '抢同一批顾客的正餐/聚餐选择。',
+    praised: ['招牌菜记忆点强', '排队带来热闹感', '内容里菜品视觉冲击强'],
+    complained: ['排队久', '人均不透明', '第一次来不知道怎么点'],
+    opportunity: '不要只学热闹,要把“第一次来怎么点、几点来更舒服、哪些人适合来”讲清楚。'
+  },
+  {
+    name: '同价格带高口碑门店',
+    why: '用户会拿它来判断“值不值”。',
+    praised: ['套餐清晰', '服务稳定', '出品稳定'],
+    complained: ['贵', '菜品不够惊喜', '高峰体验下降'],
+    opportunity: '用价值解释和套餐路径降低第一次决策成本。'
+  },
+  {
+    name: '内容平台声量高的网红店',
+    why: '影响用户种草预期和探店选择。',
+    praised: ['画面好看', '话题性强', '适合拍照分享'],
+    complained: ['到店落差', '滤镜重', '不好吃或不稳定'],
+    opportunity: '用真实场景和避坑表达建立信任,不和对方拼滤镜。'
+  }
+];
+
+function splitList(value) {
+  if (!value) return [];
+  if (Array.isArray(value)) return value.flatMap(splitList);
+  return String(value).split(/[;;,,、\n\r]+/).map(item => item.trim()).filter(Boolean);
+}
+
+function readTextIfExists(filePath) {
+  if (!filePath) return '';
+  const absolute = path.resolve(filePath);
+  if (!fs.existsSync(absolute)) return '';
+  return fs.readFileSync(absolute, 'utf8').replace(/^\uFEFF/, '');
+}
+
+function collectEvidence(input = {}) {
+  const explicit = splitList(input.evidence || input.comments || input.voices || input.reportText);
+  if (explicit.length) return explicit.slice(0, 50);
+  const report = readTextIfExists(input.report || input.reportPath);
+  if (report) {
+    return report
+      .split(/\r?\n/)
+      .map(line => line.trim())
+      .filter(line => /评论|吐槽|担心|排队|贵|招牌|服务|好吃|不好吃|人均|套餐/.test(line))
+      .slice(0, 50);
+  }
+  return [
+    '排队是排队,但第一次来真的不知道点什么。',
+    '人均有点高,如果没有套餐说明会犹豫。',
+    '视频里很种草,到店以后最怕落差。',
+    '招牌菜好吃,但高峰期服务有点跟不上。',
+    '如果老板能讲清楚适合几个人吃,会更容易决定。'
+  ];
+}
+
+function inferSignals(evidence = []) {
+  const text = evidence.join('\n');
+  const signals = [];
+  if (/排队|等位|等待/.test(text)) signals.push({ term: '排队/等位', risk: '热度带来期待,也带来体验压力' });
+  if (/贵|人均|性价比|值不值|套餐/.test(text)) signals.push({ term: '价格/套餐', risk: '用户需要更清晰的价值解释和点单路径' });
+  if (/招牌|特色|好吃|不好吃|味道/.test(text)) signals.push({ term: '招牌菜/味道', risk: '招牌要从菜名变成可复述的口味记忆' });
+  if (/服务|没人理|态度|卫生|环境/.test(text)) signals.push({ term: '服务/现场体验', risk: '高峰体验会影响复购和差评' });
+  if (/视频|种草|网红|滤镜|落差/.test(text)) signals.push({ term: '内容预期/到店落差', risk: '内容表达需要更真实,主动讲适合谁不适合谁' });
+  return signals.length ? signals : [
+    { term: '用户决策不清晰', risk: '需要先补充竞品评论和用户真实反馈' }
+  ];
+}
+
+function buildCompetitorCandidates(input = {}) {
+  const explicit = splitList(input.competitors || input.competitorNames);
+  if (explicit.length) {
+    return explicit.map((name, index) => ({
+      name,
+      type: DEFAULT_COMPETITOR_TYPES[index % DEFAULT_COMPETITOR_TYPES.length].name,
+      why: '用户已明确提到或业务方指定,需要优先纳入比较。'
+    }));
+  }
+  const city = String(input.city || input.region || '本地').trim();
+  const category = String(input.category || input.industry || '餐饮').trim();
+  return DEFAULT_COMPETITOR_TYPES.map(item => ({
+    name: `${city}${category}${item.name}`,
+    type: item.name,
+    why: item.why
+  }));
+}
+
+function buildCompetitorMap(input = {}) {
+  const brand = String(input.project || input.brand || input.store || '你的品牌').trim();
+  const city = String(input.city || input.region || '本地').trim();
+  const category = String(input.category || input.industry || '餐饮').trim();
+  const priceBand = String(input.priceBand || input.price || '未指定').trim();
+  const scene = String(input.scenario || input.scene || '到店消费/聚餐/日常就餐').trim();
+  const evidence = collectEvidence(input);
+  const signals = inferSignals(evidence);
+  const candidates = buildCompetitorCandidates(input);
+  const competitorCards = candidates.map((candidate, index) => {
+    const type = DEFAULT_COMPETITOR_TYPES[index % DEFAULT_COMPETITOR_TYPES.length];
+    return {
+      ...candidate,
+      praised: type.praised,
+      complained: type.complained,
+      opportunity: type.opportunity
+    };
+  });
+  const opportunities = signals.map(signal => ({
+    signal: signal.term,
+    risk: signal.risk,
+    action: `${brand} 可以围绕“${signal.term}”做错位表达:讲清楚适合谁、怎么点、什么时候来、为什么值,而不是只说好吃或热闹。`,
+    contentAngle: `从用户关心的“${signal.term}”切入,拍一条老板解释型视频。`
+  }));
+
+  const assistantMessage = [
+    '# 竞品图谱与错位竞争机会报告',
+    '',
+    '## 你这轮能拿到什么',
+    '- 一份可用于工作坊现场讨论的竞品候选图谱。',
+    '- 一组基于 VOC 的被夸点、被吐槽点和错位机会。',
+    '- 下一步可以直接转成问题池、单点深挖或 7 天内容选题。',
+    '',
+    '## 业务口径',
+    `- 品牌/门店:${brand}`,
+    `- 城市/区域:${city}`,
+    `- 品类:${category}`,
+    `- 价格带:${priceBand}`,
+    `- 消费场景:${scene}`,
+    '',
+    '## 竞品候选图谱',
+    ...competitorCards.map((item, index) => [
+      '',
+      `### ${index + 1}. ${item.name}`,
+      `- 类型:${item.type}`,
+      `- 为什么要看:${item.why}`,
+      `- 用户可能被吸引的点:${item.praised.join('、')}`,
+      `- 用户可能吐槽的点:${item.complained.join('、')}`,
+      `- 我方错位机会:${item.opportunity}`
+    ].join('\n')),
+    '',
+    '## VOC 信号',
+    ...signals.map(item => `- ${item.term}:${item.risk}`),
+    '',
+    '## 错位竞争建议',
+    ...opportunities.map((item, index) => [
+      `${index + 1}. ${item.signal}`,
+      `   - 风险:${item.risk}`,
+      `   - 经营动作:${item.action}`,
+      `   - 内容角度:${item.contentAngle}`
+    ].join('\n')),
+    '',
+    '## 证据样本',
+    ...evidence.slice(0, 6).map((item, index) => `${index + 1}. ${item}`),
+    '',
+    '## 下一步建议',
+    '1. 从候选竞品里确认 2-3 家真实竞品。',
+    '2. 把竞品评论继续放进 VOC 问题池,比较谁被夸、谁被骂。',
+    '3. 选择一个错位机会,生成 7 天内容选题和口播脚本。'
+  ].join('\n');
+
+  return {
+    status: 'ok',
+    assistantMessage,
+    summary: {
+      brand,
+      city,
+      category,
+      competitorCount: competitorCards.length,
+      signalCount: signals.length,
+      evidenceCount: evidence.length
+    },
+    data: {
+      brand,
+      city,
+      category,
+      priceBand,
+      scene,
+      competitors: competitorCards,
+      signals,
+      opportunities,
+      evidence
+    },
+    files: [],
+    nextActions: [
+      '确认 2-3 家真实竞品',
+      '把竞品评论整理成 VOC 问题池',
+      '选择一个错位机会生成 7 天内容选题'
+    ],
+    warnings: [],
+    errors: []
+  };
+}
+
+module.exports = {
+  buildCompetitorMap
+};

+ 206 - 0
claude-code-voc-intelligence/mcp/src/features/voc-content-plan/content-plan.js

@@ -0,0 +1,206 @@
+const fs = require('fs');
+const path = require('path');
+
+const DEFAULT_VOC_ISSUES = [
+  {
+    issue: '第一次来怕踩雷',
+    insight: '用户不是不想来,而是不知道怎么点才稳。',
+    title: '第一次来我们店,照这个顺序点不容易踩雷',
+    hook: '第一次来别一上来就乱点,这 3 个菜最稳。',
+    script: '第一次来我们店,如果你怕点多了浪费、点错了踩雷,可以先按这个顺序来。第一道点招牌菜,先判断口味是不是你喜欢的;第二道点一个下饭菜,适合两三个人一起分;第三道再点一个清爽解腻的。这样人均更稳,也更容易吃出这家店的特色。',
+    shots: ['菜单特写', '招牌菜出餐', '老板解释点单顺序', '顾客桌面组合'],
+    commentPrompt: '第一次来你最怕点错什么?评论区告诉我,我帮你搭配。',
+    metric: '评论里“怎么点/第一次来/套餐/人均”的提问是否增加'
+  },
+  {
+    issue: '觉得人均贵',
+    insight: '用户真正想知道的是贵在哪里、值不值、怎么点更划算。',
+    title: '为什么这顿饭人均不算低,但很多老客还会再来',
+    hook: '如果你只看人均,会觉得我们不便宜,但这几个地方才是成本。',
+    script: '很多顾客第一次看人均会犹豫,所以今天直接讲清楚。我们贵的地方不是装修噱头,而是这几类食材、这道招牌菜的处理时间,还有高峰期也要保持稳定出品。第一次来不建议乱点,可以先按两人/四人套餐吃,既能吃到招牌,也不会一下子超预算。',
+    shots: ['食材近景', '后厨处理过程', '招牌菜上桌', '套餐组合价格牌'],
+    commentPrompt: '想看两人不踩雷点单,还是四人聚餐点单?',
+    metric: '评论里“贵/值不值”下降,“套餐/怎么点”上升'
+  },
+  {
+    issue: '怕排队久',
+    insight: '排队问题可以转成预期管理和到店攻略。',
+    title: '不想排太久,建议你这几个时间来',
+    hook: '想吃但怕排队,先记住这 3 个时间点。',
+    script: '我们高峰期确实会排队,但不是任何时间都要等。一般来说,工作日早点来更舒服,周末建议避开正餐最高峰。如果你已经在路上,可以先看评论区置顶的等位提示,或者提前想好要点的招牌菜,到店后更快决定。',
+    shots: ['门口等位情况', '非高峰空桌', '等位提示牌', '招牌菜快剪'],
+    commentPrompt: '你更想知道排队时间,还是想先看点单攻略?',
+    metric: '评论里“几点去/要等多久”的咨询是否变成可引导问题'
+  },
+  {
+    issue: '不知道招牌菜是什么',
+    insight: '招牌菜要从“老板推荐”变成“不同人群怎么选”。',
+    title: '第一次来,老板不建议你把所有招牌都点一遍',
+    hook: '招牌菜不是越多越好,第一次来先点这几个就够。',
+    script: '很多人第一次来会问哪个最招牌。我的建议是不要一次点太多,先按口味选。如果你喜欢重口味,先点这一道;如果带孩子或者长辈,先点更稳的这一道;如果是朋友聚餐,再加一个适合分享的菜。招牌不是让你多花钱,而是帮你少踩雷。',
+    shots: ['三道菜并排', '不同人群标签', '老板手指菜单', '夹菜试吃'],
+    commentPrompt: '你是重口味、带家人,还是朋友聚餐?我给你推荐点法。',
+    metric: '评论里“招牌/推荐/带孩子/聚餐”的提问是否增加'
+  },
+  {
+    issue: '担心服务忙不过来',
+    insight: '服务焦虑要用流程透明和高峰期提醒来降低。',
+    title: '高峰期来吃饭,先知道这几个小提醒',
+    hook: '我们忙的时候,最怕顾客不知道该找谁。',
+    script: '高峰期服务压力确实更大,所以我们把几个常见问题提前说清楚。加菜、催菜、打包、开发票分别找哪里,桌上也会放提示。你来吃饭不用猜流程,遇到问题直接按提示找对应的人,我们也会持续优化高峰期响应。',
+    shots: ['桌面提示牌', '服务员动线', '加菜流程', '收银台/打包区'],
+    commentPrompt: '你最在意上菜速度、服务响应,还是环境卫生?',
+    metric: '评论里“没人理/服务慢”下降,“流程/提示”正向反馈增加'
+  },
+  {
+    issue: '内容看着种草,到店怕落差',
+    insight: '内容要降低夸张表达,主动讲适合谁、不适合谁。',
+    title: '这家店适合谁,不适合谁,老板直接说清楚',
+    hook: '先说缺点,再说适合什么人来。',
+    script: '我们不想只拍滤镜很好看的视频,所以今天直接说清楚。我们适合喜欢这种口味、愿意接受高峰等位、想吃本地特色的人;如果你只想安安静静坐很久,或者完全不能吃辣,可能要提前避开某些菜。讲清楚不适合谁,反而能减少到店落差。',
+    shots: ['真实门店环境', '无滤镜菜品', '高峰现场', '老板正面说明'],
+    commentPrompt: '你来之前最想先知道优点还是缺点?',
+    metric: '评论里“真实/避坑/适合谁”的互动是否增加'
+  },
+  {
+    issue: '不知道为什么值得专门来',
+    insight: '门店内容要把特色从“好吃”讲到“为什么值得来”。',
+    title: '如果你只来一次,建议把这顿饭吃明白',
+    hook: '不是所有菜都要点,但这一个理由值得你来一次。',
+    script: '很多店都说自己好吃,但顾客真正想知道为什么值得专门来。我们这家店最核心的不是菜名,而是这个做法、这个口味记忆和这类用餐场景。第一次来,你可以不用点满一桌,但一定要试这一道,因为它最能代表我们想做的味道。',
+    shots: ['核心菜品制作', '口味细节', '顾客夹菜反应', '门头和桌面氛围'],
+    commentPrompt: '你会为了哪种味道专门跑一趟?',
+    metric: '收藏、转发、评论里“想去/值得去/地址”的信号是否增加'
+  }
+];
+
+function splitList(value) {
+  if (!value) return [];
+  if (Array.isArray(value)) return value.flatMap(splitList);
+  return String(value).split(/[;;\n\r]+/).map(item => item.trim()).filter(Boolean);
+}
+
+function readTextIfExists(filePath) {
+  if (!filePath) return '';
+  const absolute = path.resolve(filePath);
+  if (!fs.existsSync(absolute)) return '';
+  return fs.readFileSync(absolute, 'utf8').replace(/^\uFEFF/, '');
+}
+
+function collectIssues(input = {}) {
+  const explicit = splitList(input.issues || input.problems || input.vocIssues || input.evidence || input.comments);
+  if (explicit.length) return explicit.slice(0, 12);
+  const text = String(input.reportText || input.evidenceText || '').trim() || readTextIfExists(input.report || input.reportPath);
+  const lines = splitList(text).filter(line => /排队|贵|踩雷|服务|招牌|套餐|人均|不好吃|落差|等位|怎么点/.test(line));
+  return lines.length ? lines.slice(0, 12) : DEFAULT_VOC_ISSUES.map(item => item.issue);
+}
+
+function inferTemplate(issue, index) {
+  const text = String(issue || '');
+  const matched = DEFAULT_VOC_ISSUES.find(item =>
+    text.includes(item.issue) ||
+    text.includes(item.issue.replace('第一次来', '')) ||
+    item.issue.includes(text) ||
+    (/排队|等位|等待/.test(text) && item.issue.includes('排队')) ||
+    (/贵|人均|性价比|值不值/.test(text) && item.issue.includes('人均')) ||
+    (/踩雷|怎么点|点单/.test(text) && item.issue.includes('踩雷')) ||
+    (/招牌|推荐菜|特色菜/.test(text) && item.issue.includes('招牌')) ||
+    (/服务|没人理|态度/.test(text) && item.issue.includes('服务')) ||
+    (/落差|网红|滤镜|种草/.test(text) && item.issue.includes('落差'))
+  );
+  return matched || DEFAULT_VOC_ISSUES[index % DEFAULT_VOC_ISSUES.length];
+}
+
+function buildContentPlan(input = {}) {
+  const brand = String(input.project || input.brand || input.store || '你的门店').trim();
+  const industry = String(input.industry || input.category || '本地生活餐饮').trim();
+  const platform = String(input.platform || '抖音/小红书').trim();
+  const days = Math.max(1, Math.min(Number(input.days || 7), 15));
+  const issues = collectIssues(input);
+  const plan = Array.from({ length: days }, (_, index) => {
+    const sourceIssue = issues[index % issues.length];
+    const template = inferTemplate(sourceIssue, index);
+    return {
+      day: index + 1,
+      userIssue: sourceIssue || template.issue,
+      insight: template.insight,
+      title: template.title,
+      hook: template.hook,
+      script: template.script.replace(/我们店/g, brand === '你的门店' ? '我们店' : brand),
+      shots: template.shots,
+      commentPrompt: template.commentPrompt,
+      validationMetric: template.metric,
+      platform
+    };
+  });
+
+  const assistantMessage = [
+    '# 7 天 VOC 内容选题与口播脚本',
+    '',
+    '## 你这轮能拿到什么',
+    `- 一份面向 ${brand} 的 7 天内容选题表。`,
+    '- 每条内容都绑定一个真实用户问题,不做空泛爆款标题。',
+    '- 每条都包含前 3 秒钩子、口播脚本、镜头建议、评论区引导和验证指标。',
+    '',
+    '## 业务口径',
+    `- 品牌/门店:${brand}`,
+    `- 行业/品类:${industry}`,
+    `- 发布平台:${platform}`,
+    `- 内容周期:${days} 天`,
+    '',
+    '## 7 天内容表',
+    ...plan.map(item => [
+      '',
+      `### Day ${item.day}:${item.title}`,
+      `- 回应的用户问题:${item.userIssue}`,
+      `- 洞察判断:${item.insight}`,
+      `- 前 3 秒钩子:${item.hook}`,
+      `- 口播脚本:${item.script}`,
+      `- 镜头建议:${item.shots.join(' -> ')}`,
+      `- 评论区引导:${item.commentPrompt}`,
+      `- 验证指标:${item.validationMetric}`
+    ].join('\n')),
+    '',
+    '## 拍摄提醒',
+    '- 先拍真实门店、真实菜品、真实服务流程,不要只拍滤镜氛围。',
+    '- 每条内容只解决一个用户问题,避免一条视频讲太多。',
+    '- 发完后把评论里的新问题继续放回 VOC 问题池,下一轮再优化。',
+    '',
+    '## 下一步建议',
+    '1. 先选 2 条最容易拍的内容今天开拍。',
+    '2. 发布 7 天后,把评论反馈告诉我,我会判断哪些选题有效、哪些要降权。',
+    '3. 如果已经有趋势报告或问题池,可以把 Top 3 问题传进来,我会重排内容顺序。'
+  ].join('\n');
+
+  return {
+    status: 'ok',
+    assistantMessage,
+    summary: {
+      brand,
+      industry,
+      platform,
+      days,
+      topicCount: plan.length,
+      sourceIssueCount: issues.length
+    },
+    data: {
+      brand,
+      industry,
+      platform,
+      issues,
+      plan
+    },
+    files: [],
+    nextActions: [
+      '选择 2 条最容易拍的内容先执行',
+      '发布后把评论反馈放回 VOC 问题池',
+      '7 天后反馈有效/无效,我会更新内容方向'
+    ],
+    warnings: [],
+    errors: []
+  };
+}
+
+module.exports = {
+  buildContentPlan
+};

+ 391 - 0
claude-code-voc-intelligence/mcp/src/features/voc-issue-pool/issue-pool.js

@@ -0,0 +1,391 @@
+const fs = require('fs');
+const path = require('path');
+const { readJson, writeJson, defaultMemoryPath } = require('../../core/memory-store');
+
+const DEFAULT_MEMORY_FILE = 'voc-issue-pool-memory.json';
+const DEFAULT_MEMORY_DIR = 'voc-issue-pool';
+
+const ISSUE_CATEGORIES = [
+  {
+    id: 'waiting',
+    title: '排队/等待',
+    terms: ['排队', '等位', '等待', '叫号', '翻台', '等太久'],
+    severity: 'high',
+    impact: ['新客转化', '现场效率', '口碑预期'],
+    action: '先统一评论区和门口等位预期,再做 7 天翻台/预点单验证。'
+  },
+  {
+    id: 'price',
+    title: '价格/性价比',
+    terms: ['贵', '价格', '人均', '性价比', '不值', '划算', '预算', '增项'],
+    severity: 'high',
+    impact: ['新客转化', '客单结构', '信任'],
+    action: '把价格解释成点单路径、价值证据和避坑说明,先降低第一次决策成本。'
+  },
+  {
+    id: 'taste_quality',
+    title: '味道/出品稳定',
+    terms: ['不好吃', '味道', '出品', '不稳定', '翻车', '口味', '辣', '咸', '淡'],
+    severity: 'high',
+    impact: ['复购', '差评风险', '推荐'],
+    action: '先盯核心单品 SOP 和高峰期出品稳定,避免内容预期高于门店交付。'
+  },
+  {
+    id: 'service',
+    title: '服务/体验',
+    terms: ['服务', '态度', '没人理', '员工', '老板', '体验', '环境', '卫生'],
+    severity: 'medium',
+    impact: ['复购', '带人到店', '现场效率'],
+    action: '把服务问题拆成高峰岗位、响应话术和现场流程,不只归因员工态度。'
+  },
+  {
+    id: 'product_function',
+    title: '产品/功能交付',
+    terms: ['不好用', '收纳', '动线', '板材', '环保', '售后', '柜门', '五金', '灯带'],
+    severity: 'high',
+    impact: ['成交信任', '售后风险', '复购转介绍'],
+    action: '把效果图卖点转成使用风险说明、材料证据和售后承诺。'
+  },
+  {
+    id: 'content_expectation',
+    title: '内容预期/到店落差',
+    terms: ['网红', '视频', '图片', '滤镜', '种草', '实际', '没有想象', '落差'],
+    severity: 'medium',
+    impact: ['内容信任', '到店转化', '差评风险'],
+    action: '降低夸张表达,改成真实场景、适合谁、不适合谁和避坑提醒。'
+  }
+];
+
+function splitList(value) {
+  if (!value) return [];
+  if (Array.isArray(value)) return value.flatMap(splitList);
+  return String(value).split(/[,,、\n\r]+/).map(item => item.trim()).filter(Boolean);
+}
+
+function cleanList(values) {
+  return [...new Set(splitList(values))].filter(Boolean);
+}
+
+function slugify(value) {
+  const raw = String(value || '').trim().toLowerCase();
+  if (!raw) return '';
+  return raw
+    .replace(/[^\p{L}\p{N}]+/gu, '-')
+    .replace(/^-+|-+$/g, '')
+    .slice(0, 80) || '';
+}
+
+function memoryScope(input = {}) {
+  return {
+    project: String(input.project || input.brand || input.store || '').trim(),
+    industry: String(input.industry || input.category || '').trim(),
+    scenario: String(input.scenario || input.scene || input.businessType || '').trim(),
+    audience: String(input.audience || input.targetAudience || '').trim()
+  };
+}
+
+function memoryScopeKey(input = {}) {
+  const scope = memoryScope(input);
+  const parts = [scope.project, scope.industry, scope.scenario, scope.audience]
+    .map(slugify)
+    .filter(Boolean);
+  return parts.length ? parts.join('__') : 'default';
+}
+
+function memoryPath(input = {}) {
+  if (input.memory || input.memoryPath) {
+    return path.resolve(input.memory || input.memoryPath);
+  }
+  const key = memoryScopeKey(input);
+  if (key && key !== 'default') {
+    return path.resolve('memory', DEFAULT_MEMORY_DIR, `${key}.json`);
+  }
+  return path.resolve(defaultMemoryPath(DEFAULT_MEMORY_FILE));
+}
+
+function defaultMemory(input = {}) {
+  return {
+    version: '0.3.10',
+    scopeKey: memoryScopeKey(input),
+    scope: memoryScope(input),
+    issues: {},
+    snapshots: [],
+    notes: [],
+    updatedAt: ''
+  };
+}
+
+function readMemory(input = {}) {
+  return {
+    ...defaultMemory(input),
+    ...readJson(memoryPath(input), {})
+  };
+}
+
+function readTextIfExists(filePath) {
+  if (!filePath) return '';
+  const absolute = path.resolve(filePath);
+  if (!fs.existsSync(absolute)) return '';
+  return fs.readFileSync(absolute, 'utf8').replace(/^\uFEFF/, '');
+}
+
+function collectEvidence(input = {}) {
+  const rawExplicit = input.evidence || input.comments || input.voices || input.issues || input.issueTexts;
+  const explicit = Array.isArray(rawExplicit)
+    ? rawExplicit.map(item => String(item).trim()).filter(Boolean)
+    : String(rawExplicit || '')
+      .split(/[;\n\r]+/)
+      .map(item => item.trim())
+      .filter(Boolean);
+  if (explicit.length) return explicit.slice(0, 80);
+  const text = String(input.evidenceText || input.reportText || '').trim() || readTextIfExists(input.report || input.reportPath);
+  if (!text) {
+    return [
+      '想吃但怕排队,到了现场才发现等位时间不确定。',
+      '排了很久以后,如果菜品普通就会觉得不值。',
+      '人均有点高,第一次来不知道怎么点不踩雷。',
+      '视频看着很种草,实际到店体验没有想象中稳定。',
+      '服务忙起来没人理,问问题不知道找谁。'
+    ];
+  }
+  return text
+    .split(/\r?\n/)
+    .map(line => line.replace(/^[-*\d.]+\s*/, '').trim())
+    .filter(line => line.length >= 6)
+    .slice(0, 80);
+}
+
+function matchCategory(text) {
+  return ISSUE_CATEGORIES.find(category => category.terms.some(term => text.includes(term))) || {
+    id: 'other',
+    title: '其它待归类问题',
+    terms: [],
+    severity: 'medium',
+    impact: ['转化', '复购', '口碑'],
+    action: '先补充更多评论证据,再判断它属于产品、服务、价格还是内容预期问题。'
+  };
+}
+
+function priorityScore(issue) {
+  const severityScore = issue.severity === 'high' ? 3 : issue.severity === 'medium' ? 2 : 1;
+  const countScore = Math.min(issue.count, 5);
+  const unresolvedScore = issue.status === '已解决' ? -2 : issue.status === '验证中' ? 0 : 1;
+  return severityScore * 10 + countScore + unresolvedScore;
+}
+
+function mergeStatusFromInput(issues, input = {}) {
+  const statusUpdates = input.statusUpdates || {};
+  const resolved = cleanList(input.resolvedIssues);
+  const validating = cleanList(input.validatingIssues);
+  const blocked = cleanList(input.blockedIssues);
+
+  for (const issue of issues) {
+    if (statusUpdates[issue.title] || statusUpdates[issue.id]) {
+      issue.status = String(statusUpdates[issue.title] || statusUpdates[issue.id]);
+    }
+    if (resolved.some(item => issue.title.includes(item) || issue.id === item)) issue.status = '已解决';
+    if (validating.some(item => issue.title.includes(item) || issue.id === item)) issue.status = '验证中';
+    if (blocked.some(item => issue.title.includes(item) || issue.id === item)) issue.status = '暂缓';
+  }
+}
+
+function buildIssues(input = {}, memory = {}) {
+  const evidence = collectEvidence(input);
+  const grouped = new Map();
+  for (const line of evidence) {
+    const category = matchCategory(line);
+    if (!grouped.has(category.id)) {
+      const previous = memory.issues?.[category.id] || {};
+      grouped.set(category.id, {
+        id: category.id,
+        title: category.title,
+        status: previous.status || '待验证',
+        severity: category.severity,
+        count: 0,
+        evidence: [],
+        impact: category.impact,
+        action: category.action,
+        lastSeenAt: ''
+      });
+    }
+    const issue = grouped.get(category.id);
+    issue.count += 1;
+    if (issue.evidence.length < 3) issue.evidence.push(line);
+  }
+  for (const [id, previous] of Object.entries(memory.issues || {})) {
+    if (!grouped.has(id) && previous.count > 0 && previous.status !== '已解决') {
+      grouped.set(id, {
+        ...previous,
+        count: 0,
+        previousCount: previous.count || 0,
+        delta: -(previous.count || 0),
+        trend: 'resolved',
+        evidence: [],
+        status: previous.status || '待验证'
+      });
+    }
+  }
+  const issues = [...grouped.values()].map(issue => {
+    const previous = memory.issues?.[issue.id] || {};
+    const previousCount = Number.isFinite(Number(issue.previousCount))
+      ? Number(issue.previousCount)
+      : Number(previous.count || 0);
+    const delta = issue.count - previousCount;
+    let trend = 'stable';
+    if (!previousCount && issue.count > 0) trend = 'new';
+    else if (issue.count === 0 && previousCount > 0) trend = 'resolved';
+    else if (delta > 0) trend = 'up';
+    else if (delta < 0) trend = 'down';
+    return {
+      ...issue,
+      previousCount,
+      delta,
+      trend
+    };
+  });
+  mergeStatusFromInput(issues, input);
+  return issues
+    .map(issue => ({ ...issue, priorityScore: priorityScore(issue) }))
+    .sort((a, b) => b.priorityScore - a.priorityScore);
+}
+
+function writeMemory(input = {}, issues = []) {
+  const current = readMemory(input);
+  const nextIssues = { ...(current.issues || {}) };
+  const now = new Date().toISOString();
+  for (const issue of issues) {
+    nextIssues[issue.id] = {
+      ...(nextIssues[issue.id] || {}),
+      ...issue,
+      totalCount: (nextIssues[issue.id]?.totalCount || 0) + issue.count,
+      lastSeenAt: now
+    };
+  }
+  const snapshot = {
+    at: now,
+    evidenceCount: issues.reduce((sum, issue) => sum + issue.count, 0),
+    issues: issues.map(issue => ({
+      id: issue.id,
+      title: issue.title,
+      count: issue.count,
+      previousCount: issue.previousCount || 0,
+      delta: issue.delta || 0,
+      trend: issue.trend || 'stable',
+      status: issue.status
+    }))
+  };
+  const feedback = String(input.feedback || input.message || '').trim();
+  const next = {
+    ...current,
+    version: '0.3.10',
+    scopeKey: memoryScopeKey(input),
+    scope: memoryScope(input),
+    issues: nextIssues,
+    snapshots: [...(current.snapshots || []), snapshot].slice(-12),
+    notes: feedback ? [...(current.notes || []), feedback].slice(-40) : (current.notes || []),
+    updatedAt: now
+  };
+  const filePath = memoryPath(input);
+  writeJson(filePath, next);
+  return { memoryPath: filePath, memory: next };
+}
+
+function formatIssue(issue, index) {
+  return [
+    `### ${index + 1}. ${issue.title}`,
+    `- 状态:${issue.status}`,
+    `- 优先级:${issue.priorityScore >= 33 ? '高' : issue.priorityScore >= 22 ? '中' : '低'}`,
+    `- 本轮证据数:${issue.count}`,
+    `- 影响环节:${issue.impact.join('、')}`,
+    `- 建议动作:${issue.action}`,
+    '- 证据样本:',
+    ...issue.evidence.map(item => `  - ${item}`),
+    `- 下一步:如果要深挖,直接说“继续深挖「${issue.title}」这个问题”。`
+  ].join('\n');
+}
+
+function formatTrendLabel(trend) {
+  return {
+    new: '新增',
+    up: '变多',
+    down: '变少',
+    stable: '持平',
+    resolved: '本轮未再出现'
+  }[trend] || '持平';
+}
+
+function formatIssueChange(issue) {
+  const deltaText = issue.delta > 0 ? `+${issue.delta}` : String(issue.delta || 0);
+  return `- ${issue.title}:${formatTrendLabel(issue.trend)}(上轮 ${issue.previousCount || 0} / 本轮 ${issue.count} / 变化 ${deltaText})`;
+}
+
+function buildIssuePool(input = {}) {
+  const memory = readMemory(input);
+  const issues = buildIssues(input, memory);
+  const scope = memoryScope(input);
+  const memoryUpdate = writeMemory(input, issues);
+  const topIssue = issues[0];
+  const assistantMessage = [
+    '# VOC 问题池',
+    '',
+    '## 一句话判断',
+    topIssue
+      ? `本轮最值得优先处理的是“${topIssue.title}”,它同时影响${topIssue.impact.join('、')}。`
+      : '本轮证据还不足,需要先补充更多真实评论或趋势报告。',
+    '',
+    '## 业务口径',
+    `- 项目/品牌:${scope.project || '未指定'}`,
+    `- 行业/品类:${scope.industry || '通用行业'}`,
+    `- 使用场景:${scope.scenario || '经营/内容/转化'}`,
+    `- 目标用户:${scope.audience || '目标用户'}`,
+    `- 问题数:${issues.length}`,
+    '',
+    '## 问题优先级',
+    ...issues.map(formatIssue),
+    '',
+    '## 问题变化',
+    ...(issues.length ? issues.map(formatIssueChange) : ['- 本轮证据不足,还不能做跨轮变化判断。']),
+    '',
+    '## 建议本周推进',
+    ...(issues.slice(0, 3).map((issue, index) => `${index + 1}. 先处理“${issue.title}”:${issue.action}`)),
+    '',
+    '## 下轮怎么用',
+    '- 想继续深入某个问题:说“继续深挖「问题名」”。',
+    '- 已经验证过动作:告诉我“这个有效/无效”,我会更新问题池状态和记忆。',
+    '- 下次再采集:把新评论或报告继续传入问题池,对比问题是否变多或变少。',
+    '',
+    '## 已保存',
+    `- 问题池记忆:${memoryUpdate.memoryPath}`
+  ].join('\n');
+
+  return {
+    status: 'ok',
+    assistantMessage,
+    summary: {
+      issueCount: issues.length,
+      topIssue: topIssue?.title || '',
+      memoryPath: memoryUpdate.memoryPath,
+      scopeKey: memoryScopeKey(input)
+    },
+    data: {
+      scope,
+      issues,
+      memory: memoryUpdate.memory
+    },
+    files: [memoryUpdate.memoryPath],
+    nextActions: [
+      topIssue ? `继续深挖「${topIssue.title}」` : '补充更多评论证据',
+      '更新已验证/已解决的问题状态',
+      '下一轮采集后继续更新问题池'
+    ],
+    warnings: [],
+    errors: []
+  };
+}
+
+module.exports = {
+  DEFAULT_MEMORY_FILE,
+  memoryPath,
+  readMemory,
+  buildIssuePool
+};

+ 662 - 0
claude-code-voc-intelligence/mcp/src/features/voc-problem-deep-dive/deep-dive.js

@@ -0,0 +1,662 @@
+const fs = require('fs');
+const path = require('path');
+const { readJson, writeJson, defaultMemoryPath } = require('../../core/memory-store');
+
+const DEFAULT_MEMORY_FILE = 'voc-problem-deep-dive-memory.json';
+const DEFAULT_MEMORY_DIR = 'voc-problem-deep-dive';
+
+const ACTION_TERMS = [
+  '评论区回复',
+  '预点单',
+  '等位饮品',
+  '等位提示牌',
+  '门口提示牌',
+  '推荐先点',
+  '招牌菜',
+  '套餐',
+  '人均',
+  '点单路径',
+  'SOP',
+  '话术',
+  '员工培训',
+  '收台',
+  '翻台',
+  '试吃',
+  '内容表达',
+  '复盘',
+  '优惠券',
+  '会员',
+  '预约'
+];
+
+const ISSUE_PRESETS = [
+  {
+    match: /排队|等位|等待|翻台|叫号/,
+    issue: '排队',
+    priority: 'high',
+    severity: '高',
+    effort: '中',
+    impactTargets: ['新客转化', '复购', '内容口碑', '现场效率'],
+    judgment: '排队不是单纯的服务瑕疵,它同时暴露了预期管理、翻台效率和等位体验三个问题。',
+    surface: '用户表面在说“排队久”。',
+    deeper: '深层问题是等待时间不透明、等待过程没有被照顾、排队抬高了用户对出品的预期。',
+    voices: [
+      '想吃但怕排队,到了现场才发现等位时间不确定。',
+      '排了很久以后,如果菜品没有超出预期,就会觉得不值。',
+      '排队可以接受,但希望知道大概要等多久、值不值得等。'
+    ],
+    actionsToday: [
+      '统一评论区和门口话术:告诉用户高峰大概要等多久、几点来更舒服。',
+      '在门口放一个简单等位提示牌:当前桌数、预计等待区间、推荐先点的招牌菜。'
+    ],
+    actions7Days: [
+      '测试预点单或排队先点招牌菜,让用户等待时已经进入消费流程。',
+      '连续记录 7 天点单、出餐、结账、收台时间,找出翻台最长卡点。'
+    ],
+    actionsLongTerm: [
+      '把等位体验设计成品牌体验的一部分,比如小份试吃、等位饮品、等位菜单。',
+      '把内容表达从“天天排队”升级为“怎么排更值、什么时候来体验最好”。'
+    ],
+    content: [
+      '别只说南昌烧烤排队,告诉用户几点来不用等太久。',
+      '这家店为什么排队还值得等?只讲 3 个超过预期的点。',
+      '老板视角:排队不是炫耀,是一道必须被管理的用户体验。'
+    ],
+    metrics: [
+      '评论里“排队久/等太久/不值”出现次数是否下降。',
+      '评论里“几点去/要等多久/能不能预约”是否变成可引导咨询。',
+      '高峰期门口放弃率、翻台时长、预点单比例是否改善。'
+    ]
+  },
+  {
+    match: /贵|价格|人均|性价比|不值|划算/,
+    issue: '价格/性价比',
+    priority: 'high',
+    severity: '高',
+    effort: '低',
+    impactTargets: ['新客转化', '客单结构', '评论信任'],
+    judgment: '价格争议通常不是用户单纯嫌贵,而是用户没有感知到“贵在哪里值得”。',
+    surface: '用户表面在说“贵、不值、超预算”。',
+    deeper: '深层问题是套餐路径不清楚、招牌菜价值没有被解释、用户不知道怎么点最稳。',
+    voices: [
+      '味道可以,但这个人均会让我犹豫要不要再来。',
+      '如果是请客可以,日常吃就有点贵。',
+      '套餐看起来便宜,但实际点下来超预算。'
+    ],
+    actionsToday: [
+      '做一个 2 人尝鲜点单路径,写清楚人均、分量和适合场景。',
+      '把招牌菜旁边加一句价值解释:贵在哪里、适合谁、怎么吃更值。'
+    ],
+    actions7Days: [
+      '测试 2 人/4 人/6 人套餐,观察用户是否从质疑价格变成询问套餐。',
+      '整理评论区高频价格问题,统一回复人均区间和不踩雷点法。'
+    ],
+    actionsLongTerm: [
+      '建立稳定的新客尝鲜套餐,用低决策成本承接第一次到店。',
+      '把价格表达从“便宜”改成“怎么吃得值”。'
+    ],
+    content: [
+      '第一次来南昌烧烤,照这个 2 人套餐点不容易踩雷。',
+      '这道招牌菜到底贵在哪?老板只讲真实成本和口味差异。',
+      '人均多少能吃舒服?别让顾客到店才算账。'
+    ],
+    metrics: [
+      '评论里“贵/不值/踩雷套餐”是否下降。',
+      '首次套餐销量和招牌菜点单率是否上升。',
+      '用户是否开始在评论里询问套餐而不是直接质疑价格。'
+    ]
+  },
+  {
+    match: /不好吃|味道|出品|稳定|咸|辣|淡|翻车/,
+    issue: '味道/出品稳定',
+    priority: 'high',
+    severity: '高',
+    effort: '中',
+    impactTargets: ['复购', '转介绍', '差评风险'],
+    judgment: '味道问题最伤复购,老板要先判断是单品问题、批次问题,还是用户预期被内容抬高。',
+    surface: '用户表面在说“不好吃、不稳定、没有视频里那么惊艳”。',
+    deeper: '深层问题是招牌菜标准化不足,内容预期和门店交付没有对齐。',
+    voices: [
+      '刷到视频很想吃,但到店感觉没有说得那么惊艳。',
+      '有时候好吃,有时候味道不稳定。',
+      '辣度、咸淡和火候不稳定,会影响整桌体验。'
+    ],
+    actionsToday: [
+      '先盯 3 个核心单品,记录咸淡、温度、出餐时间和退换反馈。',
+      '把内容里的夸张表达降下来,改成具体口味描述。'
+    ],
+    actions7Days: [
+      '连续 7 天复盘招牌菜差评,区分食材、火候、等待、预期四类原因。',
+      '高峰期暂时减少不稳定 SKU,把出品能力集中到招牌菜。'
+    ],
+    actionsLongTerm: [
+      '建立核心单品 SOP 和抽检机制。',
+      '把真实口味标签固定下来,让内容、菜单和服务员表达一致。'
+    ],
+    content: [
+      '别只说好吃,讲清楚这家烧烤到底是哪种口味。',
+      '老板复盘:一条差评背后的出品稳定问题。',
+      '招牌菜为什么要少而稳?比菜单越多越重要。'
+    ],
+    metrics: [
+      '评论里“没有想象中好吃/不稳定/翻车”是否下降。',
+      '招牌菜复点率和退换反馈是否改善。',
+      '用户是否开始用具体口味词夸菜,而不是只说网红。'
+    ]
+  },
+  {
+    match: /服务|态度|老板|员工|体验|环境|卫生/,
+    issue: '服务/体验',
+    priority: 'medium',
+    severity: '中',
+    effort: '低',
+    impactTargets: ['复购', '带人到店', '现场效率'],
+    judgment: '服务体验问题不是一句“培训员工”能解决,而是高峰压力、流程缺口和话术不统一共同造成的。',
+    surface: '用户表面在说“没人理、态度差、环境一般”。',
+    deeper: '深层问题是高峰期岗位分工不清楚,用户遇到等待、催菜、投诉时没有稳定回应。',
+    voices: [
+      '东西还可以,但服务体验让我下次犹豫。',
+      '忙的时候没人理,问题不知道问谁。',
+      '环境和卫生会影响我愿不愿意带朋友来。'
+    ],
+    actionsToday: [
+      '设置高峰期固定响应人,专门处理等位、催菜、加单和投诉。',
+      '统一 5 句高频回应话术,避免员工临场发挥。'
+    ],
+    actions7Days: [
+      '每天复盘 3 条现场问题,记录发生时段、岗位、处理结果。',
+      '把收台、明档、卫生细节做成用户看得见的信任信号。'
+    ],
+    actionsLongTerm: [
+      '建立高峰期服务动线和岗位表。',
+      '把服务问题从“员工态度”升级为“流程管理”。'
+    ],
+    content: [
+      '一家烧烤店真正拉开差距的,不只是味道,还有高峰期体验。',
+      '老板每天复盘 3 个服务问题,比喊口号更有用。',
+      '为什么有些小店环境一般,顾客还是愿意反复来?'
+    ],
+    metrics: [
+      '评论里“没人理/态度差/环境差/卫生”是否下降。',
+      '高峰催菜次数、投诉次数、收台时长是否下降。',
+      '带朋友/家庭聚餐相关正向评论是否增加。'
+    ]
+  }
+];
+
+function splitList(value) {
+  if (!value) return [];
+  if (Array.isArray(value)) return value.flatMap(splitList);
+  return String(value).split(/[,,、\n\r]+/).map(item => item.trim()).filter(Boolean);
+}
+
+function cleanList(values) {
+  return [...new Set(splitList(values))].filter(Boolean);
+}
+
+function slugify(value) {
+  const raw = String(value || '').trim().toLowerCase();
+  if (!raw) return '';
+  const slug = raw
+    .replace(/[^\p{L}\p{N}]+/gu, '-')
+    .replace(/^-+|-+$/g, '')
+    .slice(0, 80);
+  return slug || '';
+}
+
+function memoryScope(input = {}) {
+  return {
+    project: String(input.project || input.brand || input.store || '').trim(),
+    industry: String(input.industry || input.category || '').trim(),
+    scenario: String(input.scenario || input.scene || input.businessType || '').trim(),
+    audience: String(input.audience || input.targetAudience || '').trim()
+  };
+}
+
+function memoryScopeKey(input = {}) {
+  const scope = memoryScope(input);
+  const parts = [scope.project, scope.industry, scope.scenario, scope.audience]
+    .map(slugify)
+    .filter(Boolean);
+  return parts.length ? parts.join('__') : 'default';
+}
+
+function memoryPath(input = {}) {
+  if (input.memory || input.memoryPath) {
+    return path.resolve(input.memory || input.memoryPath);
+  }
+  const key = memoryScopeKey(input);
+  if (key && key !== 'default') {
+    return path.resolve('memory', DEFAULT_MEMORY_DIR, `${key}.json`);
+  }
+  return path.resolve(defaultMemoryPath(DEFAULT_MEMORY_FILE));
+}
+
+function defaultMemory() {
+  return {
+    version: '0.3.10',
+    scopeKey: '',
+    scope: {},
+    preferredActions: [],
+    blockedActions: [],
+    preferredContentAngles: [],
+    knownIssues: [],
+    validatedActions: [],
+    rejectedActions: [],
+    actionStats: {},
+    notes: [],
+    lastIssue: '',
+    updatedAt: ''
+  };
+}
+
+function readMemory(input = {}) {
+  const scope = memoryScope(input);
+  const scopeKey = memoryScopeKey(input);
+  return {
+    ...defaultMemory(),
+    scopeKey,
+    scope,
+    ...readJson(memoryPath(input), {})
+  };
+}
+
+function appendUnique(existing = [], values = [], limit = 30) {
+  return [...new Set([...(existing || []), ...cleanList(values)])].slice(-limit);
+}
+
+function findActionTerms(text) {
+  const source = String(text || '');
+  if (!source) return [];
+  return ACTION_TERMS.filter(term => source.includes(term));
+}
+
+function extractTermsNearSignals(text, signals = []) {
+  const source = String(text || '');
+  if (!source) return [];
+  const found = [];
+  for (const term of ACTION_TERMS) {
+    const index = source.indexOf(term);
+    if (index < 0) continue;
+    const nearby = source.slice(index, index + term.length + 20).split(/[,,。;;!!\n\r]/)[0];
+    if (signals.some(signal => nearby.includes(signal))) found.push(term);
+  }
+  return found;
+}
+
+function keepValidatedTerms(text, terms = []) {
+  const source = String(text || '');
+  return cleanList(terms).filter(term => {
+    const index = source.indexOf(term);
+    if (index < 0) return true;
+    const nearby = source.slice(index, index + term.length + 20).split(/[,,。;;!!\n\r]/)[0];
+    return !nearby.includes('无效');
+  });
+}
+
+function extractTermsAfterSignals(text, signals = [], stopSignals = []) {
+  const source = String(text || '');
+  const found = [];
+  for (const signal of signals) {
+    const index = source.indexOf(signal);
+    if (index < 0) continue;
+    if (signal === '有效' && source[index - 1] === '无') continue;
+    let tail = source.slice(index + signal.length, index + signal.length + 48);
+    const stopIndexes = stopSignals
+      .map(stop => tail.indexOf(stop))
+      .filter(stopIndex => stopIndex >= 0);
+    if (stopIndexes.length) {
+      tail = tail.slice(0, Math.min(...stopIndexes));
+    }
+    found.push(...findActionTerms(tail));
+  }
+  return found;
+}
+
+function parseFeedbackMemory(input = {}) {
+  const feedback = String(input.feedback || input.message || '').trim();
+  if (!feedback) {
+    return {
+      preferredActions: [],
+      blockedActions: [],
+      validatedActions: [],
+      rejectedActions: [],
+      preferredContentAngles: []
+    };
+  }
+  const blockedSignals = ['不想做', '不要做', '不适合', '别做', '不能做', '先不要', '别用', '排除'];
+  const preferredSignals = ['更想', '优先', '先改', '先做', '偏好', '希望', '重点做', '可以做'];
+  const validatedSignals = ['有效', '有用', '验证通过', '跑通', '能转化', '效果好'];
+  const rejectedSignals = ['无效', '没用', '效果不好', '不转化', '验证失败'];
+  const contentSignals = ['内容', '选题', '口播', '话术', '小红书', '抖音'];
+  return {
+    preferredActions: extractTermsAfterSignals(feedback, preferredSignals, blockedSignals),
+    blockedActions: extractTermsAfterSignals(feedback, blockedSignals, preferredSignals),
+    validatedActions: keepValidatedTerms(feedback, [
+      extractTermsAfterSignals(feedback, validatedSignals),
+      extractTermsNearSignals(feedback, validatedSignals)
+    ]),
+    rejectedActions: cleanList([
+      extractTermsAfterSignals(feedback, rejectedSignals),
+      extractTermsNearSignals(feedback, rejectedSignals)
+    ]),
+    preferredContentAngles: contentSignals.some(signal => feedback.includes(signal)) ? findActionTerms(feedback) : []
+  };
+}
+
+function bumpActionStats(currentStats = {}, actions = [], field = '') {
+  const next = { ...(currentStats || {}) };
+  for (const action of cleanList(actions)) {
+    if (!action) continue;
+    next[action] = {
+      validatedCount: Number(next[action]?.validatedCount || 0),
+      rejectedCount: Number(next[action]?.rejectedCount || 0),
+      lastUpdatedAt: next[action]?.lastUpdatedAt || ''
+    };
+    next[action][field] += 1;
+    next[action].lastUpdatedAt = new Date().toISOString();
+  }
+  return next;
+}
+
+function updateMemory(input = {}, result = {}) {
+  const filePath = memoryPath(input);
+  const current = readMemory(input);
+  const feedback = String(input.feedback || input.message || '').trim();
+  const parsed = parseFeedbackMemory(input);
+  let actionStats = current.actionStats || {};
+  actionStats = bumpActionStats(actionStats, [input.validatedActions, parsed.validatedActions], 'validatedCount');
+  actionStats = bumpActionStats(actionStats, [input.rejectedActions, parsed.rejectedActions], 'rejectedCount');
+  const next = {
+    ...current,
+    version: '0.3.10',
+    scopeKey: memoryScopeKey(input),
+    scope: memoryScope(input),
+    preferredActions: appendUnique(current.preferredActions, [input.preferredActions, parsed.preferredActions]),
+    blockedActions: appendUnique(current.blockedActions, [input.blockedActions, parsed.blockedActions]),
+    preferredContentAngles: appendUnique(current.preferredContentAngles, [input.preferredContentAngles, parsed.preferredContentAngles]),
+    knownIssues: appendUnique(current.knownIssues, [result.summary?.issue]),
+    validatedActions: appendUnique(current.validatedActions, [input.validatedActions, parsed.validatedActions]),
+    rejectedActions: appendUnique(current.rejectedActions, [input.rejectedActions, parsed.rejectedActions]),
+    actionStats,
+    notes: appendUnique(current.notes, feedback ? [feedback] : [], 40),
+    lastIssue: result.summary?.issue || current.lastIssue,
+    updatedAt: new Date().toISOString()
+  };
+  writeJson(filePath, next);
+  return { memoryPath: filePath, memory: next };
+}
+
+function readTextIfExists(filePath) {
+  if (!filePath) return '';
+  const absolute = path.resolve(filePath);
+  if (!fs.existsSync(absolute)) return '';
+  return fs.readFileSync(absolute, 'utf8').replace(/^\uFEFF/, '');
+}
+
+function inferPreset(issue, evidenceText = '') {
+  const text = `${issue || ''}\n${evidenceText || ''}`;
+  return ISSUE_PRESETS.find(preset => preset.match.test(text)) || {
+    issue: issue || '待深挖问题',
+    priority: 'medium',
+    severity: '中',
+    effort: '中',
+    impactTargets: ['新客转化', '复购', '口碑'],
+    judgment: '这个问题需要先从用户真实声音里拆清楚:用户表面在吐槽什么,背后真正影响决策的是什么。',
+    surface: '用户已经表达了犹豫或不满。',
+    deeper: '深层问题可能是产品交付、现场体验和内容预期没有对齐。',
+    voices: [
+      '用户已经表达了犹豫或不满,但还需要继续归类证据。',
+      '这类反馈通常同时包含产品、体验和预期管理问题。'
+    ],
+    actionsToday: ['先收集 10 条相关评论,按产品、服务、价格、场景四类归因。'],
+    actions7Days: ['选择一个最小动作做 7 天验证,看负面表达是否下降。'],
+    actionsLongTerm: ['把高频问题沉淀成门店 SOP、内容话术和复盘指标。'],
+    content: ['从一条真实评论看门店怎么改。'],
+    metrics: ['相关负面词出现次数是否下降。']
+  };
+}
+
+function pickEvidence(input = {}) {
+  const explicit = cleanList(input.evidence || input.evidenceText || input.comments || input.voices);
+  if (explicit.length) return explicit.slice(0, 6);
+  const reportText = readTextIfExists(input.report || input.reportPath);
+  if (!reportText) return [];
+  return reportText
+    .split(/\r?\n/)
+    .map(line => line.trim())
+    .filter(line => /高赞评论|评论|吐槽|担心|怕|不满|问题|顾虑/.test(line))
+    .slice(0, 6);
+}
+
+function filterBlocked(actions, memory) {
+  const blocked = cleanList(memory.blockedActions);
+  if (!blocked.length) return actions;
+  return actions.filter(action => !blocked.some(term => action.includes(term)));
+}
+
+function prioritizePreferred(actions, memory) {
+  const preferred = cleanList(memory.preferredActions);
+  const validated = cleanList(memory.validatedActions);
+  const rejected = cleanList(memory.rejectedActions);
+  const stats = memory.actionStats || {};
+  if (!preferred.length && !validated.length && !rejected.length && !Object.keys(stats).length) return actions;
+  return [...actions].sort((a, b) => {
+    const scoreA = actionMemoryScore(a, { preferred, validated, rejected, stats });
+    const scoreB = actionMemoryScore(b, { preferred, validated, rejected, stats });
+    return scoreB - scoreA;
+  });
+}
+
+function actionMemoryScore(action, { preferred = [], validated = [], rejected = [], stats = {} } = {}) {
+  let score = 0;
+  for (const term of preferred) {
+    if (action.includes(term)) score += 20;
+  }
+  for (const term of validated) {
+    if (action.includes(term)) score += 30;
+  }
+  for (const term of rejected) {
+    if (action.includes(term)) score -= 25;
+  }
+  for (const [term, item] of Object.entries(stats || {})) {
+    if (!action.includes(term)) continue;
+    score += Number(item.validatedCount || 0) * 12;
+    score -= Number(item.rejectedCount || 0) * 10;
+  }
+  return score;
+}
+
+function actionMemoryReasonLines(memory) {
+  const lines = [];
+  const stats = memory.actionStats || {};
+  const validatedStats = Object.entries(stats)
+    .filter(([, item]) => Number(item.validatedCount || 0) > 0)
+    .sort((a, b) => Number(b[1].validatedCount || 0) - Number(a[1].validatedCount || 0))
+    .slice(0, 3);
+  const rejectedStats = Object.entries(stats)
+    .filter(([, item]) => Number(item.rejectedCount || 0) > 0)
+    .sort((a, b) => Number(b[1].rejectedCount || 0) - Number(a[1].rejectedCount || 0))
+    .slice(0, 3);
+  if (memory.preferredActions?.length) lines.push(`- 优先贴合你之前偏好的动作:${memory.preferredActions.slice(-5).join('、')}`);
+  if (validatedStats.length) {
+    lines.push(`- 已验证有效的动作会被提前:${validatedStats.map(([term, item]) => `${term} ${item.validatedCount} 次`).join('、')}`);
+  }
+  if (rejectedStats.length) {
+    lines.push(`- 已验证无效的动作会被降权:${rejectedStats.map(([term, item]) => `${term} ${item.rejectedCount} 次`).join('、')}`);
+  }
+  if (memory.blockedActions?.length) lines.push(`- 已明确不想做的动作不会进入可执行建议:${memory.blockedActions.slice(-5).join('、')}`);
+  return lines;
+}
+
+function memoryLines(memory) {
+  const lines = [];
+  if (memory.knownIssues?.length) lines.push(`- 已多次关注的问题:${memory.knownIssues.slice(-5).join('、')}`);
+  if (memory.preferredActions?.length) lines.push(`- 用户偏好的动作:${memory.preferredActions.slice(-5).join('、')}`);
+  if (memory.blockedActions?.length) lines.push(`- 用户不想采用的动作:${memory.blockedActions.slice(-5).join('、')}`);
+  if (memory.validatedActions?.length) lines.push(`- 已验证有效动作:${memory.validatedActions.slice(-5).join('、')}`);
+  if (memory.rejectedActions?.length) lines.push(`- 已验证无效动作:${memory.rejectedActions.slice(-5).join('、')}`);
+  const stats = Object.entries(memory.actionStats || {}).slice(-5);
+  if (stats.length) {
+    lines.push(`- 动作验证次数:${stats.map(([term, item]) => `${term} 有效${item.validatedCount || 0}/无效${item.rejectedCount || 0}`).join('、')}`);
+  }
+  return lines;
+}
+
+function buildDeepDive(input = {}) {
+  const currentMemory = readMemory(input);
+  const parsedFeedback = parseFeedbackMemory(input);
+  const effectiveMemory = {
+    ...currentMemory,
+    preferredActions: appendUnique(currentMemory.preferredActions, [input.preferredActions, parsedFeedback.preferredActions]),
+    blockedActions: appendUnique(currentMemory.blockedActions, [input.blockedActions, parsedFeedback.blockedActions]),
+    preferredContentAngles: appendUnique(currentMemory.preferredContentAngles, [input.preferredContentAngles, parsedFeedback.preferredContentAngles]),
+    validatedActions: appendUnique(currentMemory.validatedActions, [input.validatedActions, parsedFeedback.validatedActions]),
+    rejectedActions: appendUnique(currentMemory.rejectedActions, [input.rejectedActions, parsedFeedback.rejectedActions]),
+    actionStats: bumpActionStats(
+      bumpActionStats(currentMemory.actionStats || {}, [input.validatedActions, parsedFeedback.validatedActions], 'validatedCount'),
+      [input.rejectedActions, parsedFeedback.rejectedActions],
+      'rejectedCount'
+    )
+  };
+  const issue = String(input.issue || input.problem || input.topic || currentMemory.lastIssue || '').trim();
+  const industry = String(input.industry || input.category || '通用行业').trim();
+  const scenario = String(input.scenario || input.scene || input.businessType || '门店/品牌/服务商').trim();
+  const audience = String(input.audience || input.targetAudience || '目标用户').trim();
+  const evidence = pickEvidence(input);
+  const preset = inferPreset(issue, evidence.join('\n'));
+  const finalIssue = issue || preset.issue;
+  const voices = evidence.length ? evidence : preset.voices;
+
+  const todayActions = prioritizePreferred(filterBlocked(preset.actionsToday, effectiveMemory), effectiveMemory);
+  const weekActions = prioritizePreferred(filterBlocked(preset.actions7Days, effectiveMemory), effectiveMemory);
+  const longTermActions = prioritizePreferred(filterBlocked(preset.actionsLongTerm, effectiveMemory), effectiveMemory);
+  const actionReasons = actionMemoryReasonLines(effectiveMemory);
+  const memoryApplied = Boolean(
+    effectiveMemory.preferredActions.length ||
+    effectiveMemory.blockedActions.length ||
+    effectiveMemory.validatedActions.length ||
+    effectiveMemory.rejectedActions.length ||
+    Object.keys(effectiveMemory.actionStats || {}).length ||
+    currentMemory.knownIssues.length
+  );
+
+  const summary = {
+    issue: finalIssue,
+    industry,
+    scenario,
+    audience,
+    priority: preset.priority,
+    severity: preset.severity,
+    effort: preset.effort,
+    impactTargets: preset.impactTargets,
+    evidenceCount: voices.length,
+    memoryApplied,
+    memoryScopeKey: memoryScopeKey(input),
+    parsedFeedback
+  };
+
+  const output = {
+    status: 'ok',
+    assistantMessage: [
+      `# 单点 VOC 深挖:${finalIssue}`,
+      '',
+      '## 一句话判断',
+      `${preset.judgment}`,
+      '',
+      '## 业务口径',
+      `- 行业/品类:${industry}`,
+      `- 使用场景:${scenario}`,
+      `- 目标用户:${audience}`,
+      '',
+      '## 优先级判断',
+      `- 严重度:${preset.severity}`,
+      `- 执行难度:${preset.effort}`,
+      `- 优先级:${preset.priority === 'high' ? '高,建议本周处理' : '中,建议进入 7 天验证'}`,
+      `- 主要影响:${preset.impactTargets.join('、')}`,
+      '',
+      '## 用户原话证据',
+      ...voices.map((voice, index) => `${index + 1}. ${voice}`),
+      '',
+      '## 表层问题 vs 深层问题',
+      `- 表层问题:${preset.surface}`,
+      `- 深层问题:${preset.deeper}`,
+      '',
+      '## 老板视角影响',
+      ...preset.impactTargets.map(item => `- ${item}`),
+      '',
+      '## 可执行解决动作',
+      ...(actionReasons.length ? ['### 为什么优先推荐这些动作', ...actionReasons, ''] : []),
+      '### 今天就能做',
+      ...todayActions.map((item, index) => `${index + 1}. ${item}`),
+      '',
+      '### 7 天内验证',
+      ...weekActions.map((item, index) => `${index + 1}. ${item}`),
+      '',
+      '### 长期沉淀',
+      ...longTermActions.map((item, index) => `${index + 1}. ${item}`),
+      '',
+      '## 可转化内容选题',
+      ...preset.content.map((item, index) => `${index + 1}. ${item}`),
+      '',
+      '## 7 天验证计划',
+      '1. 第 1 天:确定一个门店动作和一个内容动作,不要同时改太多。',
+      '2. 第 2-4 天:记录相关评论、现场反馈和员工反馈。',
+      '3. 第 5-7 天:复查负面词是否下降,咨询型评论是否增加。',
+      '',
+      '## 下轮验证指标',
+      ...preset.metrics.map(item => `- ${item}`),
+      ...(memoryApplied ? ['', '## 已应用的使用记忆', ...memoryLines(effectiveMemory)] : []),
+      '',
+      '## 仍需确认的问题',
+      `1. 这个“${finalIssue}”主要发生在新客、老客,还是高峰期用户身上?`,
+      '2. 你们现在有没有对应的现场记录或员工反馈?',
+      '3. 如果只先改一个动作,老板更愿意改产品、服务、价格表达,还是内容表达?'
+    ].join('\n'),
+    summary,
+    data: {
+      issue: finalIssue,
+      industry,
+      scenario,
+      audience,
+      evidence: voices,
+      priority: preset.priority,
+      severity: preset.severity,
+      effort: preset.effort,
+      impactTargets: preset.impactTargets,
+      surfaceProblem: preset.surface,
+      deepProblem: preset.deeper,
+      actions: {
+        today: todayActions,
+        sevenDays: weekActions,
+        longTerm: longTermActions
+      },
+      contentIdeas: preset.content,
+      validationMetrics: preset.metrics,
+      memoryApplied
+    },
+    files: [],
+    nextActions: [
+      `先确认“${finalIssue}”主要影响新客还是复购`,
+      '选择一个门店动作和一个内容动作做 7 天验证',
+      '下一轮把验证结果告诉我,我会更新记忆并调整建议'
+    ],
+    warnings: [],
+    errors: []
+  };
+
+  const memoryUpdate = updateMemory(input, output);
+  output.summary.memoryApplied = memoryApplied;
+  output.summary.memoryPath = memoryUpdate.memoryPath;
+  output.data.memory = memoryUpdate.memory;
+  output.files = [memoryUpdate.memoryPath];
+  return output;
+}
+
+module.exports = {
+  DEFAULT_MEMORY_FILE,
+  memoryPath,
+  readMemory,
+  updateMemory,
+  buildDeepDive
+};

+ 293 - 0
claude-code-voc-intelligence/mcp/src/features/voc-speaking-script/speaking-script.js

@@ -0,0 +1,293 @@
+const fs = require('fs');
+const path = require('path');
+
+function splitList(value) {
+  if (!value) return [];
+  if (Array.isArray(value)) return value.flatMap(splitList);
+  return String(value)
+    .split(/[;\n\r,,、]+/)
+    .map(item => item.trim())
+    .filter(Boolean);
+}
+
+function readJsonMaybe(filePath) {
+  try {
+    if (!filePath || !fs.existsSync(filePath)) return {};
+    return JSON.parse(fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, ''));
+  } catch {
+    return {};
+  }
+}
+
+function writeJson(filePath, data) {
+  fs.mkdirSync(path.dirname(filePath), { recursive: true });
+  fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
+}
+
+function defaultMemoryPath(input = {}) {
+  return path.resolve(
+    input.memory || input.memoryPath || path.join('memory', 'voc-speaking-script-memory.json')
+  );
+}
+
+function inferTopic(input = {}) {
+  const topic = String(input.topic || input.title || input.selectedTopic || input.userIssue || input.issue || '').trim();
+  if (topic) return topic;
+  const issues = splitList(input.issues || input.problems || input.vocIssues);
+  return issues[0] || '把一个真实顾客顾虑讲清楚';
+}
+
+function parseFeedback(feedback = '') {
+  const text = String(feedback || '').trim();
+  const preferred = [];
+  const blocked = [];
+  const styleHints = [];
+
+  if (/老板视角|经营者|创始人视角/.test(text)) preferred.push('老板视角');
+  if (/更狠|更犀利|更直接|开头.*狠/.test(text)) preferred.push('强开头');
+  if (/具体场景|少讲概念|别讲概念|多给案例|真实场景/.test(text)) preferred.push('具体场景');
+  if (/转化|私信|到店|成交|咨询/.test(text)) preferred.push('转化导向');
+  if (/温和|真诚|克制|别太硬/.test(text)) preferred.push('真诚克制');
+
+  if (/不要.*工具|别.*工具|不讲工具|少讲工具/.test(text)) blocked.push('工具清单式表达');
+  if (/不要.*概念|少讲概念|别.*概念/.test(text)) blocked.push('抽象概念');
+  if (/太泛|空泛|大词/.test(text)) blocked.push('空泛表达');
+  if (/不适合|不要|别再|降权/.test(text)) styleHints.push('本轮有降权反馈,需要避免重复推荐相同表达');
+
+  return { preferred, blocked, styleHints, raw: text };
+}
+
+function mergeUnique(...lists) {
+  return [...new Set(lists.flat().filter(Boolean))];
+}
+
+function buildEvidence(input = {}) {
+  return splitList(input.evidence || input.comments || input.voices || input.vocEvidence || input.evidenceText)
+    .slice(0, 8);
+}
+
+function buildScriptDraft({ brand, industry, topic, userIssue, evidence, preferred, blocked, feedback }) {
+  const isBoss = preferred.includes('老板视角');
+  const strong = preferred.includes('强开头');
+  const concrete = preferred.includes('具体场景');
+  const conversion = preferred.includes('转化导向');
+  const sincere = preferred.includes('真诚克制');
+  const avoidConcept = blocked.includes('抽象概念') || blocked.includes('空泛表达');
+
+  const hook = strong
+    ? `先说一句实话:${userIssue} 这个问题不讲清楚,再会拍视频也留不住人。`
+    : `很多人看到 ${brand} 会先问一个问题:${userIssue}。`;
+
+  const opening = isBoss
+    ? `我是 ${brand} 的经营者,今天不讲漂亮话,直接把这个问题拆开说。`
+    : `今天这条内容只回答一个真实顾客问题,不绕弯。`;
+
+  const evidenceLine = evidence.length
+    ? `我们看到的顾客原话里,有人提到:“${evidence[0]}”。这说明用户不是没兴趣,而是在做决定前还缺一个确定答案。`
+    : `这一版先按 VOC 问题做结构推演,证据还不够满,正式发布前最好再补 3-5 条真实评论或私域反馈。`;
+
+  const middle = concrete
+    ? `放到真实场景里,就是顾客准备来之前,会担心自己会不会踩雷、值不值得、到店后能不能马上做决定。所以这条视频要讲清楚:适合谁、不适合谁、第一次怎么选。`
+    : `所以我们不只说“我们很好”,而是要把用户最担心的条件讲清楚:适合谁、解决什么问题、怎么判断值不值。`;
+
+  const proof = avoidConcept
+    ? `具体做法是:给一个真实案例,讲一个选择标准,再给一个可以立刻照做的小动作。`
+    : `这里的关键不是堆卖点,而是把能力翻译成用户听得懂的证明。`;
+
+  const close = conversion
+    ? `如果你也有这个顾虑,可以在评论区留一句“我担心的是哪一点”,我按你的情况给你一个更具体的建议。`
+    : `你来之前先把这个问题想清楚,选择会更稳,也更不容易被单一卖点带偏。`;
+
+  const tone = sincere ? '语气要真诚克制,少夸自己,多解释判断依据。' : '语气要直接、清楚、有老板判断。';
+
+  return {
+    hook,
+    script60: [hook, opening, evidenceLine, middle, proof, close].join('\n'),
+    script30: [hook, evidenceLine, proof, close].join('\n'),
+    teleprompter: [hook, opening, evidenceLine, middle, proof, close]
+      .join('\n\n')
+      .replace(/。/g, '。\n'),
+    titles: [
+      `别急着问好不好,先看 ${userIssue}`,
+      `${industry} 顾客真正担心的不是表面卖点`,
+      `把 ${topic} 讲清楚,用户才敢做决定`
+    ],
+    shots: [
+      '开头正面对镜,直接抛出顾客顾虑',
+      '切真实场景或证据截图,不用过度包装',
+      '展示一个具体案例或操作细节',
+      '结尾回到评论区问题,引导用户补充自己的情况'
+    ],
+    commentPrompt: `你最担心的是 ${userIssue},还是另一个问题?评论区告诉我。`,
+    qualityNotes: [tone]
+  };
+}
+
+function updateMemory(memory, input, draft, feedbackSignals, finalized) {
+  const now = new Date().toISOString();
+  const next = {
+    version: memory.version || '0.1.0',
+    preferredStyles: mergeUnique(memory.preferredStyles || [], feedbackSignals.preferred),
+    blockedStyles: mergeUnique(memory.blockedStyles || [], feedbackSignals.blocked),
+    notes: [...(memory.notes || [])],
+    finalizedScripts: [...(memory.finalizedScripts || [])],
+    updatedAt: now
+  };
+
+  if (feedbackSignals.raw) {
+    next.notes.push({
+      at: now,
+      feedback: feedbackSignals.raw,
+      preferred: feedbackSignals.preferred,
+      blocked: feedbackSignals.blocked
+    });
+  }
+
+  if (finalized) {
+    next.finalizedScripts.push({
+      at: now,
+      brand: input.brand || input.project || input.store || '',
+      topic: inferTopic(input),
+      script60: draft.script60,
+      script30: draft.script30,
+      titles: draft.titles
+    });
+  }
+
+  return next;
+}
+
+function buildQualityGate({ evidence, input }) {
+  const warnings = [];
+  if (!evidence.length) warnings.push('缺少真实用户原话,这版只能作为结构推演稿。');
+  if (!input.topic && !input.title && !input.userIssue && !input.issue) warnings.push('缺少明确选题,建议从 7 天内容计划里选择一条再共创。');
+  const level = warnings.length >= 2 ? 'fail' : warnings.length ? 'warn' : 'pass';
+  return { level, warnings };
+}
+
+function buildVocSpeakingScript(input = {}) {
+  const brand = String(input.brand || input.project || input.store || '你的品牌/门店').trim();
+  const industry = String(input.industry || input.category || '你的行业').trim();
+  const topic = inferTopic(input);
+  const userIssue = String(input.userIssue || input.issue || input.problem || topic).trim();
+  const evidence = buildEvidence(input);
+  const feedbackText = String(input.feedback || input.message || '').trim();
+  const memoryPath = defaultMemoryPath(input);
+  const memory = readJsonMaybe(memoryPath);
+  const feedbackSignals = parseFeedback(feedbackText);
+  const preferred = mergeUnique(memory.preferredStyles || [], feedbackSignals.preferred, splitList(input.preferredStyles));
+  const blocked = mergeUnique(memory.blockedStyles || [], feedbackSignals.blocked, splitList(input.blockedStyles));
+  const finalized = input.finalize === true || input.finalize === 'true' || /定稿|确认这版/.test(feedbackText);
+  const draft = buildScriptDraft({ brand, industry, topic, userIssue, evidence, preferred, blocked, feedback: feedbackText });
+  const qualityGate = buildQualityGate({ evidence, input });
+  const nextMemory = updateMemory(memory, input, draft, feedbackSignals, finalized);
+  writeJson(memoryPath, nextMemory);
+
+  const outputDir = input.output ? path.resolve(input.output) : '';
+  const files = [];
+  const assistantMessage = [
+    '# VOC 口播脚本共创',
+    '',
+    '## 你这轮能拿到什么',
+    '- 一条从 VOC 问题出发的 60 秒口播稿。',
+    '- 一条 30 秒压缩版。',
+    '- 一版提词器文本、标题备选、镜头建议和评论区引导。',
+    '- 本轮反馈会写入脚本记忆,下次会优先沿用有效风格、避开不适合方向。',
+    '',
+    '## 业务口径',
+    `- 品牌/门店:${brand}`,
+    `- 行业/品类:${industry}`,
+    `- 选题:${topic}`,
+    `- 回应的用户问题:${userIssue}`,
+    '',
+    '## 证据与质量门槛',
+    `- 质量状态:${qualityGate.level}`,
+    evidence.length ? evidence.map((item, index) => `- 证据 ${index + 1}:${item}`).join('\n') : '- 暂无用户原话证据,建议补充评论/私域反馈后再定稿。',
+    ...qualityGate.warnings.map(item => `- 风险提醒:${item}`),
+    '',
+    '## 历史偏好已应用',
+    preferred.length ? `- 优先风格:${preferred.join('、')}` : '- 暂无历史优先风格。',
+    blocked.length ? `- 避免方向:${blocked.join('、')}` : '- 暂无历史禁区。',
+    '',
+    '## 60 秒口播稿',
+    draft.script60,
+    '',
+    '## 30 秒压缩版',
+    draft.script30,
+    '',
+    '## 提词器版',
+    draft.teleprompter,
+    '',
+    '## 标题备选',
+    ...draft.titles.map((item, index) => `${index + 1}. ${item}`),
+    '',
+    '## 镜头建议',
+    ...draft.shots.map((item, index) => `${index + 1}. ${item}`),
+    '',
+    '## 评论区引导',
+    draft.commentPrompt,
+    '',
+    '## 下一步',
+    finalized
+      ? '- 已按定稿处理,并写入脚本记忆。下一轮生成会参考这次的风格和禁区。'
+      : '- 你可以回复“开头更狠一点”“少讲概念,多给场景”“定稿”,我会继续改稿或写入记忆。'
+  ].join('\n');
+
+  if (outputDir) {
+    fs.mkdirSync(outputDir, { recursive: true });
+    const mdPath = path.join(outputDir, 'voc-speaking-script.md');
+    const jsonPath = path.join(outputDir, 'voc-speaking-script.json');
+    fs.writeFileSync(mdPath, assistantMessage, 'utf8');
+    writeJson(jsonPath, {
+      brand,
+      industry,
+      topic,
+      userIssue,
+      evidence,
+      preferred,
+      blocked,
+      qualityGate,
+      draft,
+      memoryPath
+    });
+    files.push(mdPath, jsonPath);
+  }
+
+  return {
+    status: 'ok',
+    assistantMessage,
+    summary: {
+      brand,
+      industry,
+      topic,
+      userIssue,
+      evidenceCount: evidence.length,
+      qualityGate: qualityGate.level,
+      finalized,
+      memoryPath
+    },
+    data: {
+      brand,
+      industry,
+      topic,
+      userIssue,
+      evidence,
+      preferred,
+      blocked,
+      qualityGate,
+      draft,
+      memory: nextMemory
+    },
+    files,
+    nextActions: finalized
+      ? ['用这版拍摄', '发布后把评论反馈放回 VOC 问题池', '下一轮沿用脚本记忆继续优化']
+      : ['回复修改要求继续改稿', '确认后回复“定稿”写入记忆', '发布后把有效/无效反馈告诉我'],
+    warnings: qualityGate.warnings,
+    errors: []
+  };
+}
+
+module.exports = {
+  buildVocSpeakingScript,
+  parseFeedback
+};

+ 156 - 0
claude-code-voc-intelligence/mcp/src/features/xiaohongshu-trend/assets.js

@@ -0,0 +1,156 @@
+const fs = require('fs');
+const path = require('path');
+
+const DEFAULT_TIMEOUT_MS = 8000;
+const DEFAULT_LIMIT = 10;
+
+function ensureDir(dirPath) {
+  fs.mkdirSync(dirPath, { recursive: true });
+}
+
+function safeName(value, fallback = 'asset') {
+  const cleaned = String(value || '')
+    .trim()
+    .replace(/[^a-zA-Z0-9_-]+/g, '-')
+    .replace(/^-+|-+$/g, '')
+    .slice(0, 80);
+  return cleaned || fallback;
+}
+
+function extFromContentType(contentType = '') {
+  const value = String(contentType || '').split(';')[0].trim().toLowerCase();
+  if (value === 'image/jpeg' || value === 'image/jpg') return '.jpg';
+  if (value === 'image/png') return '.png';
+  if (value === 'image/webp') return '.webp';
+  if (value === 'image/gif') return '.gif';
+  if (value === 'image/svg+xml') return '.svg';
+  return '';
+}
+
+function extFromUrl(url = '') {
+  try {
+    const parsed = new URL(String(url));
+    const ext = path.extname(parsed.pathname).toLowerCase();
+    if (['.jpg', '.jpeg', '.png', '.webp', '.gif', '.svg'].includes(ext)) return ext === '.jpeg' ? '.jpg' : ext;
+  } catch {
+    // Ignore invalid URLs; caller will decide whether to skip.
+  }
+  return '';
+}
+
+function parseDataUrl(value = '') {
+  const match = String(value).match(/^data:([^,]*),(.*)$/s);
+  if (!match) return null;
+  const meta = match[1] || '';
+  const [rawContentType] = meta.split(';');
+  const contentType = rawContentType || 'application/octet-stream';
+  const isBase64 = meta.split(';').some(part => part.toLowerCase() === 'base64');
+  const body = match[2] || '';
+  return {
+    contentType,
+    buffer: isBase64 ? Buffer.from(body, 'base64') : Buffer.from(decodeURIComponent(body), 'utf8')
+  };
+}
+
+async function fetchWithTimeout(url, timeoutMs = DEFAULT_TIMEOUT_MS) {
+  const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
+  const timeout = controller ? setTimeout(() => controller.abort(), timeoutMs) : null;
+  try {
+    const response = await fetch(url, {
+      signal: controller ? controller.signal : undefined,
+      headers: {
+        Accept: 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
+        'User-Agent': 'claude-code-voc-intelligence/asset-cache'
+      }
+    });
+    const arrayBuffer = await response.arrayBuffer();
+    return {
+      ok: response.ok,
+      status: response.status,
+      contentType: response.headers.get('content-type') || '',
+      buffer: Buffer.from(arrayBuffer)
+    };
+  } finally {
+    if (timeout) clearTimeout(timeout);
+  }
+}
+
+async function readImageSource(url, timeoutMs) {
+  if (!url) return null;
+  const data = parseDataUrl(url);
+  if (data) return data;
+  if (!/^https?:\/\//i.test(String(url))) return null;
+  const fetched = await fetchWithTimeout(url, timeoutMs);
+  if (!fetched.ok) {
+    const error = new Error(`image request failed with HTTP ${fetched.status}`);
+    error.httpStatus = fetched.status;
+    throw error;
+  }
+  return {
+    contentType: fetched.contentType,
+    buffer: fetched.buffer
+  };
+}
+
+function firstImageUrl(note = {}) {
+  return note.coverUrl || (Array.isArray(note.imageUrls) ? note.imageUrls[0] : '') || '';
+}
+
+function normalizeLimit(value) {
+  if (value === undefined || value === null || value === '') return DEFAULT_LIMIT;
+  const parsed = Number(value);
+  if (!Number.isFinite(parsed)) return DEFAULT_LIMIT;
+  return Math.max(0, parsed);
+}
+
+async function cacheEvidenceAssets({ notes = [], outputDir, limit = DEFAULT_LIMIT, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
+  const warnings = [];
+  const files = [];
+  if (!outputDir || !Array.isArray(notes) || !notes.length) {
+    return { warnings, files };
+  }
+
+  const assetsDir = path.join(outputDir, 'assets');
+  ensureDir(assetsDir);
+  const maxItems = normalizeLimit(limit);
+
+  for (const note of notes.slice(0, maxItems)) {
+    const imageUrl = firstImageUrl(note);
+    if (!imageUrl) continue;
+    const noteId = safeName(note.id || note.noteId || note.title, 'note');
+    try {
+      const source = await readImageSource(imageUrl, timeoutMs);
+      if (!source || !source.buffer || !source.buffer.length) {
+        warnings.push({
+          stage: 'asset_cache',
+          noteId: note.id || '',
+          message: '未识别到可缓存的图片地址'
+        });
+        continue;
+      }
+      const ext = extFromContentType(source.contentType) || extFromUrl(imageUrl) || '.jpg';
+      const filename = `${noteId}-cover${ext}`;
+      const absolutePath = path.join(assetsDir, filename);
+      fs.writeFileSync(absolutePath, source.buffer);
+      const relativePath = path.posix.join('assets', filename);
+      note.localCoverPath = relativePath;
+      note.localCoverAbsolutePath = absolutePath;
+      note.coverOriginalUrl = note.coverOriginalUrl || imageUrl;
+      files.push(absolutePath);
+    } catch (error) {
+      warnings.push({
+        stage: 'asset_cache',
+        noteId: note.id || '',
+        message: error.message || String(error)
+      });
+    }
+  }
+
+  return { warnings, files };
+}
+
+module.exports = {
+  cacheEvidenceAssets,
+  parseDataUrl,
+  safeName
+};

+ 335 - 0
claude-code-voc-intelligence/mcp/src/features/xiaohongshu-trend/industry-presets.js

@@ -0,0 +1,335 @@
+function cleanList(value) {
+  if (!value) return [];
+  const items = Array.isArray(value) ? value : [value];
+  return items
+    .flatMap(item => String(item || '').split(/[,,、\n\r]+/))
+    .map(item => item.trim())
+    .filter(Boolean);
+}
+
+function unique(items) {
+  return [...new Set(cleanList(items))];
+}
+
+function textOfProfile(profile = {}) {
+  return [
+    profile.industry,
+    profile.businessType,
+    ...(cleanList(profile.keywords)),
+    ...(cleanList(profile.trendQuestions)),
+    ...(cleanList(profile.mustTrackSignals))
+  ].join(' ');
+}
+
+const PRESETS = [
+  {
+    id: 'home-custom',
+    label: '家装全屋定制',
+    aliases: ['家装', '装修', '全屋定制', '橱柜', '衣柜', '软装', '收纳'],
+    defaultKeywords: ['全屋定制', '装修避坑', '小户型收纳', '衣柜设计', '厨房橱柜'],
+    preferenceSignals: ['奶油风', '原木', '弧形', '无主灯', '低饱和', '显大', '耐看'],
+    decisionSignals: ['预算', '增项', '环保', '板材', '不好打理', '落灰', '翻车', '动线', '收纳'],
+    elementSignals: ['开放格', '柜门', '灯带', '抽屉', '五金', '餐边柜', '玄关', '衣柜'],
+    riskSignals: ['显旧', '压抑', '落灰', '增项', '甲醛', '尺寸翻车'],
+    applicationLabel: '门店追问或内容选题',
+    talkTarget: '客户',
+    sampleNotes: [
+      {
+        title: '全屋定制最容易翻车的不是价格,是柜子做完不好住',
+        content: '很多女生装修前只看奶油风效果图,入住后才发现开放格落灰、柜门不好打理、收纳不够。全屋定制要先想生活动线,再看风格。',
+        author: '住进理想家',
+        imageLabel: '全屋定制翻车避坑',
+        tags: ['全屋定制', '装修避坑', '收纳'],
+        comments: [
+          '我就是开放格翻车,擦灰擦到怀疑人生,现在只想做能关起来的柜子。',
+          '预算最怕增项,前期说得很好,后面五金、灯带、抽屉全加钱。'
+        ]
+      },
+      {
+        title: '奶油风下半年还会流行吗?关键看材质和灯光',
+        content: '奶油风不是越白越好看,真正高级的是低饱和颜色、木纹、弧形和无主灯配合。评论更关心耐看、好打理、显大。',
+        author: '软装设计阿晴',
+        imageLabel: '奶油风耐看与打理',
+        tags: ['奶油风', '装修风格', '设计元素'],
+        comments: [
+          '奶油风好看是好看,就怕两年后显旧,而且有孩子真的怕不好打理。',
+          '我更想知道敏感鼻炎家庭怎么选板材,环保比风格重要。'
+        ]
+      },
+      {
+        title: '小户型别再硬塞满墙柜,真的会显压抑',
+        content: '小户型收纳不是柜子越多越好,要留出视觉呼吸感。评论区很多人问玄关、餐边柜、卧室衣柜怎么做才不显堵。',
+        author: '小家改造研究所',
+        imageLabel: '小户型收纳动线',
+        tags: ['小户型', '收纳', '柜体设计'],
+        comments: ['小户型真的不能只看收纳图,住进去以后动线堵才是最崩溃的。']
+      }
+    ]
+  },
+  {
+    id: 'pet-food',
+    label: '宠物食品',
+    aliases: ['宠物', '猫粮', '狗粮', '宠物食品', '宠物零食', '养猫', '养狗'],
+    defaultKeywords: ['猫粮推荐', '狗粮避坑', '宠物零食', '肠胃敏感猫粮', '冻干主粮'],
+    preferenceSignals: ['高蛋白', '无谷', '冻干', '鲜肉', '低敏', '适口性', '小包装'],
+    decisionSignals: ['成分', '价格', '软便', '泪痕', '过敏', '复购', '试吃装', '品牌口碑'],
+    elementSignals: ['主粮', '罐头', '冻干', '零食', '益生菌', '配料表', '颗粒大小'],
+    riskSignals: ['拉肚子', '不吃', '诱食剂', '虚标', '过敏', '油腻'],
+    applicationLabel: '产品卖点或种草选题',
+    talkTarget: '养宠用户',
+    sampleNotes: [
+      {
+        title: '新手选猫粮别只看蛋白高,先看肠胃能不能适应',
+        content: '很多新手一上来就买高蛋白猫粮,但评论里更关心软便、适口性和配料表。肠胃敏感猫要先小包装试吃,再看复购。',
+        author: '养猫配方研究员',
+        imageLabel: '猫粮配方与软便',
+        tags: ['猫粮推荐', '配料表', '肠胃敏感'],
+        comments: ['我家猫换粮最怕软便,贵不贵反而是第二位。', '希望品牌能直接出试吃装,不然一大袋不吃太浪费。']
+      },
+      {
+        title: '宠物零食越来越卷,铲屎官最怕的其实是诱食剂',
+        content: '冻干、肉条、罐头都在讲高肉含量,但高赞评论反复问有没有诱食剂、盐分高不高、会不会挑食。',
+        author: '狗狗零食测评',
+        imageLabel: '宠物零食避坑',
+        tags: ['宠物零食', '诱食剂', '测评'],
+        comments: ['我不怕零食贵,就怕吃完主粮不吃了。', '能不能把盐分和添加剂讲清楚,比拍得好看重要。']
+      },
+      {
+        title: '狗粮避坑:别只看进口国产,先看狗狗便便和毛发状态',
+        content: '不少用户评论说换粮后便便、泪痕、掉毛变化更能说明问题。真实喂养反馈比单纯参数更容易打动人。',
+        author: '养狗日记本',
+        imageLabel: '狗粮真实反馈',
+        tags: ['狗粮避坑', '复购', '喂养反馈'],
+        comments: ['我更想看连续喂一个月的反馈,不想只看开箱。']
+      }
+    ]
+  },
+  {
+    id: 'beauty-skincare',
+    label: '美妆护肤',
+    aliases: ['美妆', '护肤', '彩妆', '精华', '面霜', '防晒', '敏感肌'],
+    defaultKeywords: ['敏感肌护肤', '防晒推荐', '粉底液测评', '抗老精华', '油皮护肤'],
+    preferenceSignals: ['抗老', '修护', '提亮', '控油', '持妆', '轻薄', '温和'],
+    decisionSignals: ['成分', '肤质', '刺激', '闷痘', '价格', '功效', '妆感', '复购'],
+    elementSignals: ['精华', '面霜', '防晒', '粉底液', '散粉', '屏障', '配方'],
+    riskSignals: ['烂脸', '搓泥', '假白', '拔干', '过敏', '暗沉'],
+    applicationLabel: '功效表达或种草选题',
+    talkTarget: '目标消费者',
+    sampleNotes: [
+      {
+        title: '敏感肌别盲目追猛药,先看修护和刺激风险',
+        content: '评论区很多人不是不想抗老,而是怕泛红、刺痛和屏障受损。温和修护正在变成敏感肌护肤的第一决策点。',
+        author: '成分党小禾',
+        imageLabel: '敏感肌修护',
+        tags: ['敏感肌', '修护', '抗老'],
+        comments: ['我最怕用了第二天泛红,功效可以慢一点。', '能不能按肤质讲,不要所有人都推同一瓶。']
+      },
+      {
+        title: '防晒推荐别只讲 SPF,油皮更在意闷不闷痘',
+        content: '防晒内容里,清爽、成膜、泛白、搓泥是评论高频词。用户想知道的是通勤场景能不能长期用。',
+        author: '油皮防晒实验室',
+        imageLabel: '防晒肤感测评',
+        tags: ['防晒', '油皮', '肤感'],
+        comments: ['防晒指数都差不多,我更关心会不会闷痘和搓泥。']
+      },
+      {
+        title: '粉底液测评开始从遮瑕转向真实持妆',
+        content: '用户不只看上脸瞬间,更关心四小时后暗沉、卡粉和出油状态。真实带妆记录比棚拍更有说服力。',
+        author: '底妆记录员',
+        imageLabel: '粉底液持妆',
+        tags: ['粉底液', '持妆', '暗沉'],
+        comments: ['求别只开美颜,我想看下午真实斑驳情况。']
+      }
+    ]
+  },
+  {
+    id: 'baby-maternal',
+    label: '母婴用品',
+    aliases: ['母婴', '婴儿', '宝宝', '纸尿裤', '奶粉', '儿童用品', '孕妈'],
+    defaultKeywords: ['纸尿裤测评', '宝宝辅食', '婴儿湿巾', '奶粉选择', '新手妈妈'],
+    preferenceSignals: ['温和', '安全', '透气', '省心', '便携', '无添加', '分龄'],
+    decisionSignals: ['安全', '过敏', '红屁屁', '价格', '成分', '吸收', '口碑', '售后'],
+    elementSignals: ['纸尿裤', '湿巾', '奶瓶', '辅食', '奶粉', '推车', '洗护'],
+    riskSignals: ['红屁屁', '漏尿', '过敏', '呛奶', '添加剂', '踩雷'],
+    applicationLabel: '妈妈决策内容或产品表达',
+    talkTarget: '家长',
+    sampleNotes: [
+      {
+        title: '纸尿裤测评别只讲薄,妈妈更怕红屁屁和漏尿',
+        content: '高赞评论集中在夜用漏尿、红屁屁、尺码偏小。真实使用场景比单纯参数更影响购买。',
+        author: '新手妈妈测评',
+        imageLabel: '纸尿裤夜用测评',
+        tags: ['纸尿裤', '红屁屁', '漏尿'],
+        comments: ['我最怕半夜漏尿,薄不薄真的排第二。', '能不能按宝宝体重讲尺码,老买错。']
+      },
+      {
+        title: '宝宝辅食火的不是花样,是省心和不过敏',
+        content: '用户会收藏辅食清单,但评论更关心过敏、添加剂和制作时间。省心但安全是下一轮核心机会。',
+        author: '辅食日记',
+        imageLabel: '宝宝辅食安全',
+        tags: ['辅食', '过敏', '无添加'],
+        comments: ['我想要简单一点的,别每天像做大餐。']
+      },
+      {
+        title: '婴儿湿巾到底怎么选?成分表比香味重要',
+        content: '很多家长不再只看抽数和价格,而是看成分、厚度和会不会刺激皮肤。',
+        author: '母婴用品避坑',
+        imageLabel: '婴儿湿巾成分',
+        tags: ['湿巾', '成分', '温和'],
+        comments: ['便宜没用,宝宝一红就不敢用了。']
+      }
+    ]
+  },
+  {
+    id: 'education-training',
+    label: '教育培训',
+    aliases: ['教育', '培训', '课程', '教培', '升学', '考研', '英语', '职业培训'],
+    defaultKeywords: ['课程避坑', '英语启蒙', '考研规划', '职业技能培训', '学习方法'],
+    preferenceSignals: ['提分', '陪跑', '规划', '试听', '体系化', '实战', '反馈'],
+    decisionSignals: ['价格', '师资', '效果', '时间', '试听', '退费', '作业反馈', '适合程度'],
+    elementSignals: ['课程', '老师', '班型', '资料', '题库', '社群', '作业'],
+    riskSignals: ['没效果', '退费难', '跟不上', '销售焦虑', '虚假承诺'],
+    applicationLabel: '课程转化或内容选题',
+    talkTarget: '学习者/家长',
+    sampleNotes: [
+      {
+        title: '报课前别先问价格,先看孩子能不能跟上节奏',
+        content: '评论里很多家长担心不是贵,而是课程节奏太快、作业反馈不及时、试听和正课差距大。',
+        author: '学习规划老师',
+        imageLabel: '课程节奏与反馈',
+        tags: ['课程避坑', '试听', '作业反馈'],
+        comments: ['试听课很好,正课没人管才是最崩溃的。', '我更想知道跟不上有没有补救机制。']
+      },
+      {
+        title: '英语启蒙内容变了:家长不只要资源,要可执行路径',
+        content: '收藏高的内容通常有清单,但评论里反复问每天怎么安排、多久能看到变化。',
+        author: '英语启蒙路线图',
+        imageLabel: '英语启蒙路径',
+        tags: ['英语启蒙', '学习路径', '家长陪跑'],
+        comments: ['资源太多了,我缺的是每天照着做的计划。']
+      },
+      {
+        title: '职业技能培训最该讲清楚的是学完能做什么',
+        content: '用户关心案例、作品、就业和真实项目,不再满足于只看课程大纲。',
+        author: '职业技能观察',
+        imageLabel: '技能培训结果',
+        tags: ['职业培训', '就业', '实战'],
+        comments: ['能不能看往期学员作品,比老师头衔更重要。']
+      }
+    ]
+  },
+  {
+    id: 'local-dining',
+    label: '本地生活餐饮',
+    aliases: ['餐饮', '咖啡', '本地生活', '探店', '火锅', '烘焙', '小吃', '酒馆'],
+    defaultKeywords: ['本地探店', '餐厅推荐', '咖啡店打卡', '火锅避坑', '团购套餐'],
+    preferenceSignals: ['性价比', '氛围感', '出片', '服务', '口味稳定', '排队', '套餐'],
+    decisionSignals: ['价格', '口味', '分量', '环境', '排队', '服务', '停车', '复购'],
+    elementSignals: ['套餐', '招牌菜', '咖啡', '甜品', '包间', '团购', '位置'],
+    riskSignals: ['踩雷', '照骗', '排队久', '服务差', '分量少', '不值'],
+    applicationLabel: '探店内容或到店转化',
+    talkTarget: '到店用户',
+    sampleNotes: [
+      {
+        title: '探店内容别只拍氛围,评论区最关心值不值',
+        content: '用户会被氛围图吸引,但评论里反复问人均、分量、排队和团购能不能用。',
+        author: '城市吃喝指南',
+        imageLabel: '探店性价比',
+        tags: ['本地探店', '性价比', '团购'],
+        comments: ['图很好看,我就想知道两个人吃完到底多少钱。', '排队一小时就劝退了。']
+      },
+      {
+        title: '咖啡店打卡从出片转向长期复购',
+        content: '评论里除了装修,还在问豆子、甜品、座位和工作日体验。复购理由比一次性打卡更重要。',
+        author: '咖啡店观察',
+        imageLabel: '咖啡店复购',
+        tags: ['咖啡', '氛围', '复购'],
+        comments: ['能坐下来办公吗?如果只能拍照就不会再去了。']
+      },
+      {
+        title: '火锅团购套餐最容易翻车的是隐藏限制',
+        content: '用户对团购套餐很敏感,关注锅底、菜量、节假日能不能用和服务态度。',
+        author: '团购避坑局',
+        imageLabel: '火锅团购避坑',
+        tags: ['火锅', '团购', '套餐'],
+        comments: ['套餐看着便宜,结果锅底和小料另算就不香了。']
+      }
+    ]
+  }
+];
+
+function buildFallbackPreset(profile = {}) {
+  const industry = profile.industry || '通用行业';
+  const keywords = unique(profile.keywords).length ? unique(profile.keywords) : ['用户需求', '产品体验', '消费顾虑'];
+  const mustTrack = unique(profile.mustTrackSignals);
+  return {
+    id: 'generic',
+    label: industry,
+    aliases: [],
+    defaultKeywords: keywords,
+    preferenceSignals: unique([...mustTrack, ...keywords, '口碑', '体验', '效果', '品质', '服务', '场景']),
+    decisionSignals: unique([...mustTrack, '价格', '效果', '安全', '服务', '口碑', '复购', '踩坑', '性价比']),
+    elementSignals: unique([...keywords, '产品', '服务', '方案', '套餐', '功能', '案例', '体验']),
+    riskSignals: unique(['踩坑', '不值', '没效果', '售后', '虚假宣传', '价格不透明']),
+    applicationLabel: '业务验证或内容选题',
+    talkTarget: '目标用户',
+    sampleNotes: buildGenericSampleNotes(industry, keywords)
+  };
+}
+
+function buildGenericSampleNotes(industry, keywords) {
+  const [first, second, third] = keywords;
+  return [
+    {
+      title: `${first}最近为什么被反复讨论?`,
+      content: `${industry}用户不只看表面卖点,更关心真实体验、价格透明和长期使用是否稳定。评论里常见的问题是有没有对比、会不会踩坑、适不适合自己。`,
+      author: `${industry}观察员`,
+      imageLabel: `${first}趋势观察`,
+      tags: [first, '趋势观察', '用户顾虑'],
+      comments: ['我最想看真实使用反馈,别只给种草图。', '价格和后续成本讲清楚,比单纯说好更有用。']
+    },
+    {
+      title: `别只讲${second || first},用户更想知道适不适合自己`,
+      content: `高互动内容往往会把场景、人群和风险点讲清楚。用户不是没有兴趣,而是不确定这个选择是否适合自己的预算、时间和使用场景。`,
+      author: `${industry}内容研究所`,
+      imageLabel: `${second || first}场景验证`,
+      tags: [second || first, '场景', '避坑'],
+      comments: ['有没有不同人群的对比?我怕买了不适合。']
+    },
+    {
+      title: `${third || first}内容要从卖点走向证据`,
+      content: `用户更愿意相信可验证证据,比如真实案例、评论反馈、前后对比和使用周期,而不是单纯的结论式推荐。`,
+      author: `${industry}证据样本库`,
+      imageLabel: `${third || first}证据样本`,
+      tags: [third || first, '案例', '口碑'],
+      comments: ['能不能给具体案例?只说推荐我没法判断。']
+    }
+  ];
+}
+
+function resolveIndustryPreset(profile = {}) {
+  const haystack = textOfProfile(profile).toLowerCase();
+  const matched = PRESETS.find(preset => preset.aliases.some(alias => haystack.includes(alias.toLowerCase())));
+  return matched || buildFallbackPreset(profile);
+}
+
+function buildSignalTerms(profile = {}) {
+  const preset = resolveIndustryPreset(profile);
+  const mustTrack = unique(profile.mustTrackSignals);
+  const keywords = unique(profile.keywords).length ? unique(profile.keywords) : preset.defaultKeywords;
+  return {
+    preset,
+    preferenceTerms: unique([...mustTrack, ...preset.preferenceSignals, ...keywords.slice(0, 3)]),
+    decisionTerms: unique([...mustTrack, ...preset.decisionSignals]),
+    elementTerms: unique([...preset.elementSignals, ...keywords]),
+    riskTerms: unique([...preset.riskSignals, '避坑', '翻车', '顾虑'])
+  };
+}
+
+module.exports = {
+  PRESETS,
+  cleanList,
+  unique,
+  resolveIndustryPreset,
+  buildSignalTerms
+};

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

@@ -29,16 +29,123 @@ function pickTags(note) {
     .map(String);
 }
 
+function pickFirstString(...values) {
+  return values
+    .flat()
+    .filter(value => typeof value === 'string')
+    .map(value => value.trim())
+    .find(Boolean) || '';
+}
+
+function nestedValue(object, path) {
+  return path.split('.').reduce((current, key) => (
+    current && typeof current === 'object' ? current[key] : undefined
+  ), object);
+}
+
+function hasXsecToken(url) {
+  return /[?&]xsec_token=/i.test(String(url || ''));
+}
+
+function pickWebUrl(note = {}, raw = {}) {
+  const candidates = [
+    'mini_program_info.webpage_url',
+    'note_card.mini_program_info.webpage_url',
+    'share_info.link',
+    'note_card.share_info.link',
+    'webpage_url',
+    'webUrl',
+    'url'
+  ].flatMap(field => [nestedValue(note, field), nestedValue(raw, field)]);
+  const withXsecToken = candidates.find(hasXsecToken);
+  return withXsecToken || pickFirstString(candidates);
+}
+
+function looksLikeImageUrl(value) {
+  return /^https?:\/\//i.test(String(value || ''));
+}
+
+function collectImageUrlsFromValue(value, result = []) {
+  if (!value) return result;
+  if (typeof value === 'string') {
+    if (looksLikeImageUrl(value) && !result.includes(value)) result.push(value);
+    return result;
+  }
+  if (Array.isArray(value)) {
+    value.forEach(item => collectImageUrlsFromValue(item, result));
+    return result;
+  }
+  if (typeof value !== 'object') return result;
+
+  [
+    'url',
+    'url_default',
+    'url_pre',
+    'original_url',
+    'original',
+    'large',
+    'large_url',
+    'middle',
+    'middle_url',
+    'thumbnail',
+    'image_url',
+    'imageUrl'
+  ].forEach(key => collectImageUrlsFromValue(value[key], result));
+
+  [
+    'url_list',
+    'urlList',
+    'info_list',
+    'infoList',
+    'images',
+    'image_list',
+    'imageList'
+  ].forEach(key => collectImageUrlsFromValue(value[key], result));
+
+  return result;
+}
+
+function extractImageUrls(note = {}, raw = {}) {
+  const result = [];
+  [
+    note.cover,
+    raw.cover,
+    note.note_card && note.note_card.cover,
+    raw.note_card && raw.note_card.cover,
+    note.image_list,
+    raw.image_list,
+    note.images_list,
+    raw.images_list,
+    note.imageList,
+    raw.imageList,
+    note.note_card && note.note_card.image_list,
+    raw.note_card && raw.note_card.image_list
+  ].forEach(value => collectImageUrlsFromValue(value, result));
+  return result;
+}
+
+function contentExcerpt(text, limit = 120) {
+  const value = String(text || '').replace(/\s+/g, ' ').trim();
+  if (value.length <= limit) return value;
+  return `${value.slice(0, limit - 1)}...`;
+}
+
 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 || {};
+  const accessibleUrl = pickWebUrl(note, raw);
+  const fallbackUrl = id ? `https://www.xiaohongshu.com/discovery/item/${id}` : '';
+  const imageUrls = extractImageUrls(note, raw);
+  const content = note.desc || raw.desc || note.content || raw.content || '';
   return {
     id,
     platform: 'xiaohongshu',
     keyword,
     title: note.title || raw.title || '',
-    content: note.desc || raw.desc || note.content || raw.content || '',
+    content,
+    contentExcerpt: contentExcerpt(content),
+    rawExcerpt: contentExcerpt(content),
     author: user.nickname || user.name || '',
     authorId: user.userid || user.user_id || '',
     type: note.type || raw.type || '',
@@ -47,7 +154,19 @@ function normalizeNote(raw, keyword, detail) {
     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}` : '',
+    url: accessibleUrl || fallbackUrl,
+    webUrl: accessibleUrl,
+    shareUrl: pickFirstString(
+      nestedValue(note, 'share_info.link'),
+      nestedValue(raw, 'share_info.link'),
+      nestedValue(note, 'note_card.share_info.link'),
+      nestedValue(raw, 'note_card.share_info.link')
+    ),
+    sourceUrl: accessibleUrl || fallbackUrl,
+    sourceUrlVerified: hasXsecToken(accessibleUrl),
+    fallbackUrl,
+    coverUrl: imageUrls[0] || '',
+    imageUrls,
     tags: pickTags(note).length ? pickTags(note) : pickTags(raw)
   };
 }
@@ -88,9 +207,9 @@ function extractComments(data) {
 }
 
 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 api = new XiaohongshuApi({ token, baseUrl: options.baseUrl });
+  const keywordLimit = Math.max(1, Number(options.keywordLimit || 1));
+  const notesPerKeyword = Math.max(1, Number(options.notesPerKeyword || 1));
   const maxCommentPages = Math.max(0, Number(options.maxCommentPages ?? 1));
   const keywords = (profile.keywords || []).slice(0, keywordLimit);
   const notes = [];
@@ -167,6 +286,9 @@ async function collectLiveDataset({ profile, token, options = {} }) {
 }
 
 module.exports = {
-  collectLiveDataset
+  collectLiveDataset,
+  normalizeNote,
+  pickWebUrl,
+  extractImageUrls,
+  hasXsecToken
 };
-

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

@@ -19,7 +19,7 @@ function appendNote(notes, note) {
 
 function defaultTrendMemory() {
   return {
-    version: '0.1.2',
+  version: '0.1.14',
     preferredDirections: [],
     blockedDirections: [],
     focusModes: [],

+ 125 - 101
claude-code-voc-intelligence/mcp/src/features/xiaohongshu-trend/report.js

@@ -1,6 +1,18 @@
 const fs = require('fs');
 const path = require('path');
 const { rankTopicsWithMemory, cleanList } = require('./preference-memory');
+const { buildSignalAnalysis } = require('./signal-extractor');
+const {
+  firstTerm,
+  firstDistinctTerm,
+  findTopComment,
+  buildOpportunityTopics,
+  buildBusinessTalk
+} = require('./topic-builder');
+const {
+  assessEvidenceQuality,
+  buildEvidenceQualitySection
+} = require('../../core/evidence-quality');
 
 function ensureDir(dirPath) {
   fs.mkdirSync(dirPath, { recursive: true });
@@ -22,19 +34,20 @@ 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 `${(count / 10000).toFixed(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 truncateText(value, limit = 120) {
+  const text = String(value || '').replace(/\s+/g, ' ').trim();
+  if (text.length <= limit) return text;
+  return `${text.slice(0, limit - 1)}...`;
+}
+
+function markdownUrl(value) {
+  const url = String(value || '').trim().replace(/[<>]/g, '');
+  return url ? `<${url}>` : '';
 }
 
 function memoryHasSignal(memory = {}) {
@@ -59,35 +72,54 @@ function buildMemorySection(memory = {}) {
   ].filter(Boolean);
 }
 
-function buildScopeSection({ profile, notes, comments, collectionMode }) {
+function buildScopeSection({ profile, notes, comments, collectionMode, signals }) {
   const audience = asList(profile.targetAudience).slice(0, 3).join('、') || '未标注';
   const keywords = asList(profile.keywords).slice(0, 5).join('、') || '未标注';
+  const presetLabel = signals && signals.preset && signals.preset.label ? signals.preset.label : '';
   return [
     '## 数据口径',
     '',
-    `- 行业/品类:${profile.industry || '未标注'}`,
+    `- 行业/品类:${profile.industry || presetLabel || '未标注'}`,
+    profile.businessType ? `- 业务类型:${profile.businessType}` : '',
     `- 目标人群:${audience}`,
     `- 关注关键词:${keywords}`,
     `- 样本:${notes.length} 条笔记 / ${comments.length} 条评论`,
+    presetLabel ? `- 行业模板:${presetLabel}` : '',
     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 buildEvidenceReason(note, topComment) {
+  if (note.evidenceReason) return note.evidenceReason;
+  if (topComment && topComment.text) {
+    return '这条样本同时提供了原帖表达和用户评论,可以用来校验趋势判断是否贴近真实顾虑。';
+  }
+  return '这条样本互动较高,适合用作本轮趋势判断的原帖证据。';
+}
+
+function buildSourceLine(note) {
+  const url = note.sourceUrl || note.webUrl || note.url || '';
+  if (!url) return '   - 原帖:暂无可访问链接';
+  const label = note.sourceUrlVerified === false ? '打开原帖(链接待核验)' : '打开原帖';
+  return `   - 原帖:[${label}](${markdownUrl(url)})`;
 }
 
 function buildEvidenceSection(notes, comments) {
   const rows = notes.slice(0, 3).map((note, index) => {
     const topComment = findTopComment(comments, note.id);
+    const coverUrl = note.localCoverPath || note.coverUrl || (Array.isArray(note.imageUrls) ? note.imageUrls[0] : '');
+    const excerpt = truncateText(note.contentExcerpt || note.rawExcerpt || note.content, 120);
+    const topCommentText = topComment ? truncateText(topComment.text, 120) : '';
     return [
       `${index + 1}. ${note.title || '未命名笔记'}`,
       `   - 作者:${note.author || '未标注'} | 互动:${formatCount(note.likeCount)}赞 / ${formatCount(note.collectCount)}藏 / ${formatCount(note.commentCount)}评`,
-      topComment ? `   - 高赞评论:${topComment.text}` : ''
+      buildSourceLine(note),
+      `   - 为什么值得看:${buildEvidenceReason(note, topComment)}`,
+      coverUrl ? `   - 图片:\n\n![原帖首图](${markdownUrl(coverUrl)})` : '   - 图片:暂无可展示首图,可点开原帖查看。',
+      excerpt ? `   - 原文摘录:${excerpt}` : '',
+      topCommentText ? `   - 高赞评论:${topCommentText}` : ''
     ].filter(Boolean).join('\n');
   });
   return [
@@ -97,139 +129,123 @@ function buildEvidenceSection(notes, comments) {
   ];
 }
 
-function buildConcernLines(decisionSignals) {
+function buildConcernLines(decisionSignals, applicationLabel) {
   if (!decisionSignals.length) {
     return ['- 暂未出现稳定高频顾虑,建议先扩大关键词或评论样本。'];
   }
   return decisionSignals.slice(0, 5).map(item => (
-    `- ${item.term}:在样本中出现 ${item.count} 次,适合转成选题钩子或门店追问。`
+    `- ${item.term}:在样本中出现 ${item.count} 次,适合转成${applicationLabel || '业务验证或内容选题'}。`
   ));
 }
 
-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, '设计元素');
+function buildObservationLines({ notes, comments, signals }) {
+  const preference = firstTerm(signals.preferenceSignals, '核心卖点');
+  const secondPreference = firstDistinctTerm(signals.preferenceSignals.slice(1), [preference], '');
+  const concern = firstDistinctTerm(signals.decisionSignals, [preference], '主要顾虑');
+  const secondConcern = firstDistinctTerm(signals.decisionSignals, [preference, concern], '');
+  const element = firstDistinctTerm(signals.elementSignals, [preference, concern], '具体元素');
+  const risk = firstDistinctTerm(signals.riskSignals, [preference, concern, element], '潜在风险');
   const topNote = notes[0] || {};
   const topComment = topNote.id ? findTopComment(comments, topNote.id) : comments[0];
-  const lines = [
-    `1. 样本里“${topStyle}”出现更靠前,但它需要和“${topConcern}”一起看,不能直接等同于真实需求。`,
-    secondStyle
-      ? `2. “${secondStyle}”也在样本中出现,说明风格判断要继续比较不同人群和户型场景。`
-      : `2. 暂时没有第二个稳定风格信号,建议下一轮扩大风格类样本。`,
+  return [
+    `1. 样本里“${preference}”是一个明显入口,但它需要和“${concern}”一起看,不能直接等同于真实购买/选择理由。`,
+    secondPreference
+      ? `2. “${secondPreference}”也在样本中出现,说明用户可能在比较不同场景、人群和使用条件。`
+      : '2. 暂时还没有第二个稳定偏好信号,建议下一轮补充更细的关键词或评论样本。',
     secondConcern
-      ? `3. 用户顾虑里除了“${topConcern}”,还出现了“${secondConcern}”,这两个点需要用户真实反馈来排优先级。`
-      : `3. 当前主要顾虑集中在“${topConcern}”,还不能判断是否足够普遍。`,
-    `4. “${topElement}”这类具体元素可以继续验证,因为它更容易连接到方案、价格、维护和售后承诺。`,
+      ? `3. 用户顾虑里除了“${concern}”,还出现了“${secondConcern}”,这两个点需要用你的真实客户反馈来排优先级。`
+      : `3. 当前顾虑仍集中在“${concern}”,还不能判断是否足够普遍。`,
+    `4. “${element}”这类具体元素可以继续验证,因为它更容易连接到方案、价格、维护和售后承诺。`,
     topComment && topComment.text
-      ? `5. 高赞评论提到“${topComment.text}”,适合作为下一轮向客户/销售核验的问题。`
-      : `5. 当前评论证据不足,下一轮需要补充一条真实客户原话来校准。`
+      ? `5. 高赞评论提到“${truncateText(topComment.text, 80)}”,适合作为下一轮向客户/销售/社群核验的问题。`
+      : `5. 当前评论证据还不够强,下一轮需要补充更多真实客户原话来校准“${risk}”这类风险点。`
   ];
-  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, '设计元素');
+function buildHypothesisLines({ signals }) {
+  const preference = firstTerm(signals.preferenceSignals, '核心卖点');
+  const concern = firstDistinctTerm(signals.decisionSignals, [preference], '主要顾虑');
+  const element = firstDistinctTerm(signals.elementSignals, [preference, concern], '具体元素');
   return [
-    `1. ${topStyle} 可能不是单独流行,而是和“${topConcern}”一起影响客户选择。`,
-    `2. ${topElement} 这类具体元素更适合继续验证,因为它能连接到预算、维护和落地方案。`,
-    `3. 第一轮样本更像是在提示“客户怕什么”,下一轮需要用你的真实客户反馈校准。`
+    `1. ${preference} 可能不是单独成立的趋势,而是和“${concern}”一起影响用户选择。`,
+    `2. ${element} 更适合继续验证,因为它能连接到场景、成本、效果证据和落地方案。`,
+    '3. 第一轮样本更像是在提示“用户为什么犹豫”,下一轮需要用你的真实客户反馈校准。'
   ];
 }
 
-function buildDynamicValidationQuestions({ notes, comments, styleSignals, decisionSignals, elementSignals, rankedTopics }) {
-  const topStyle = firstTerm(styleSignals, '当前风格');
-  const topConcern = firstTerm(decisionSignals, '当前顾虑');
-  const topElement = firstTerm(elementSignals, '当前设计元素');
+function buildDynamicValidationQuestions({ notes, comments, signals, rankedTopics }) {
+  const preference = firstTerm(signals.preferenceSignals, '当前卖点');
+  const concern = firstDistinctTerm(signals.decisionSignals, [preference], '当前顾虑');
+  const element = firstDistinctTerm(signals.elementSignals, [preference, concern], '当前元素');
+  const risk = firstDistinctTerm(signals.riskSignals, [preference, concern, element], '风险点');
   const topNote = notes[0] || {};
   const topComment = topNote.id ? findTopComment(comments, topNote.id) : comments[0];
   const topTopic = rankedTopics[0] || {};
   return [
-    `样本里“${topStyle}”和“${topConcern}”同时出现,你实际接触的客户是先被风格吸引,还是先担心${topConcern}?`,
+    `样本里“${preference}”和“${concern}”同时出现,你实际接触的用户是先被${preference}吸引,还是先担心${concern}?`,
     topComment && topComment.text
-      ? `这条高赞评论“${topComment.text}”像不像你门店/销售/私域里真实听到的问题?如果不像,真实问题更接近什么?`
-      : `当前样本缺少高赞评论证据,你手上有没有客户最近反复问的一句话?`,
-    `围绕“${topElement}”,你们现在有没有可证明的案例、材料、价格或售后承诺?没有的话,这个方向先不要直接定稿。`,
+      ? `这条高赞评论“${truncateText(topComment.text, 80)}”像不像你销售、客服、社群或私域里真实听到的问题?如果不像,真实问题更接近什么?`
+      : '当前样本缺少高赞评论证据,你手上有没有最近客户反复问的一句话?',
+    `围绕“${element}”,你们现在有没有可证明的案例、价格/成本、效果证据或售后承诺?没有的话,这个方向先不要直接定稿。`,
     topTopic.title
       ? `如果只继续深挖一个方向,你更想验证“${topTopic.title}”,还是换成更贴近你业务的方向?`
-      : `如果只继续深挖一个方向,你更想看风格趋势、用户顾虑、设计元素,还是门店转化话术?`
+      : `如果只继续深挖一个方向,你更想验证“${risk}”,还是换成更贴近你业务的方向?`
   ];
 }
 
+function buildOneLine({ profile, signals }) {
+  const industry = profile.industry || (signals.preset && signals.preset.label) || '当前行业';
+  const preference = firstTerm(signals.preferenceSignals, '核心卖点');
+  const concern = firstDistinctTerm(signals.decisionSignals, [preference], '主要顾虑');
+  const risk = firstDistinctTerm(signals.riskSignals, [preference, concern], '风险点');
+  return `本轮${industry}最值得关注的不是单一“${preference}”,而是“${preference}、${concern}、${risk}”一起进入用户决策。`;
+}
+
 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 signals = buildSignalAnalysis({ profile, notes, comments });
+  const topTopics = buildOpportunityTopics({ profile, notes, comments, signals });
   const rankedTopics = rankTopicsWithMemory(topTopics, memory);
+  const evidenceQuality = assessEvidenceQuality({
+    items: notes,
+    comments,
+    itemLabel: '笔记',
+    collectionMode
+  });
   const validationQuestions = buildDynamicValidationQuestions({
     notes,
     comments,
-    styleSignals,
-    decisionSignals,
-    elementSignals,
+    signals,
     rankedTopics
   });
-
-  const salesTalk = [
-    '如果客户只说想要奶油风,可以追问:你更担心不好打理、显旧,还是预算超支?',
-    '讲全屋定制时,不要先推柜体套餐,先帮客户列出最容易翻车的 3 个位置。',
-    '对小户型客户,可以用“收纳够用但不压抑”替代“柜子越多越好”。'
-  ];
+  const salesTalk = buildBusinessTalk({ profile, signals });
+  const applicationLabel = signals.preset && signals.preset.applicationLabel;
 
   const assistantMessage = [
     `# 小红书趋势情报${collectionMode === 'sample' ? '(演示样例)' : ''}`,
     '',
     '## 第一轮初步判断(待校准)',
     '',
-    `${oneLine} 这还不是最终结论,需要先结合你的真实客户反馈做一轮校准。`,
+    `${buildOneLine({ profile, signals })} 这还不是最终结论,需要先结合你的真实客户反馈做一轮校准。`,
+    '',
+    ...buildScopeSection({ profile, notes, comments, collectionMode, signals }),
     '',
-    ...buildScopeSection({ profile, notes, comments, collectionMode }),
+    ...buildEvidenceQualitySection(evidenceQuality),
     '',
     ...buildMemorySection(memory),
     memoryHasSignal(memory) ? '' : '',
     '## 样本观察 Top 5',
     '',
-    ...buildObservationLines({ notes, comments, styleSignals, decisionSignals, elementSignals }),
+    ...buildObservationLines({ notes, comments, signals }),
     '',
     '## 本轮机会假设',
     '',
-    ...buildHypothesisLines({ styleSignals, decisionSignals, elementSignals }),
+    ...buildHypothesisLines({ signals }),
     '',
     '## 用户顾虑/决策阻力',
     '',
-    ...buildConcernLines(decisionSignals),
+    ...buildConcernLines(signals.decisionSignals, applicationLabel),
     '',
     ...buildEvidenceSection(notes, comments),
     '',
@@ -238,10 +254,11 @@ function buildTrendReport({ profile, dataset, collectionMode, memory = {} }) {
     ...rankedTopics.map((topic, index) => [
       `${index + 1}. ${topic.title}`,
       `   - 角度:${topic.angle}`,
-      `   - 证据:${topic.evidence}`
-    ].join('\n')),
+      `   - 证据:${topic.evidence}`,
+      topic.confidence ? `   - 置信度:${topic.confidence}` : ''
+    ].filter(Boolean).join('\n')),
     '',
-    '## 暂可参考话术(待确认)',
+    '## 暂可参考业务话术/内容表达(待确认)',
     '',
     ...salesTalk.map(item => `- ${item}`),
     '',
@@ -256,24 +273,31 @@ function buildTrendReport({ profile, dataset, collectionMode, memory = {} }) {
     assistantMessage,
     summary: {
       workflowStage: 'initial_hypothesis_needs_calibration',
+      industryPreset: signals.preset && signals.preset.id,
       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),
+      topStyleSignals: signals.preferenceSignals.slice(0, 5),
+      topPreferenceSignals: signals.preferenceSignals.slice(0, 5),
+      topDecisionSignals: signals.decisionSignals.slice(0, 5),
+      topElementSignals: signals.elementSignals.slice(0, 5),
+      topRiskSignals: signals.riskSignals.slice(0, 5),
       evidenceNoteCount: Math.min(notes.length, 3),
+      evidenceQuality,
       validationQuestionCount: validationQuestions.length
     },
     data: {
       profile,
+      industryPreset: signals.preset,
+      signals,
       notes,
       comments,
       topTopics: rankedTopics,
       validationQuestions,
       preferenceMemory: memory,
-      salesTalk
+      salesTalk,
+      evidenceQuality
     }
   };
 }

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

@@ -1,74 +1,75 @@
-function buildSampleDataset(profile = {}) {
-  const keywords = Array.isArray(profile.keywords) && profile.keywords.length
-    ? profile.keywords
-    : ['全屋定制', '奶油风装修', '小户型收纳'];
+const { resolveIndustryPreset, cleanList } = require('./industry-presets');
+
+function sampleImage(label, accent = '#d7b98e') {
+  const safeLabel = String(label || '行业趋势样本').slice(0, 24);
+  const svg = [
+    '<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="760" viewBox="0 0 1200 760">',
+    '<rect width="1200" height="760" fill="#f7f2ea"/>',
+    `<rect x="88" y="80" width="1024" height="520" rx="24" fill="${accent}" opacity="0.32"/>`,
+    '<rect x="144" y="132" width="420" height="392" rx="18" fill="#ffffff" opacity="0.86"/>',
+    '<rect x="620" y="132" width="436" height="72" rx="14" fill="#ffffff" opacity="0.82"/>',
+    '<rect x="620" y="236" width="360" height="52" rx="12" fill="#ffffff" opacity="0.74"/>',
+    '<rect x="620" y="320" width="390" height="52" rx="12" fill="#ffffff" opacity="0.68"/>',
+    '<rect x="620" y="404" width="300" height="52" rx="12" fill="#ffffff" opacity="0.62"/>',
+    `<text x="120" y="668" font-size="44" font-family="Arial, sans-serif" fill="#4b3b2b">${safeLabel}</text>`,
+    '<text x="120" y="714" font-size="24" font-family="Arial, sans-serif" fill="#806f5d">Xiaohongshu sample evidence image</text>',
+    '</svg>'
+  ].join('');
+  return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
+}
 
-  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: ['小户型', '收纳', '柜体设计']
-    }
-  ];
+function metric(index, base) {
+  return Math.max(1, Math.round(base - index * base * 0.28));
+}
 
-  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
-    }
-  ];
+function buildNote({ item, preset, keyword, index }) {
+  const id = `xhs-${preset.id}-${String(index + 1).padStart(3, '0')}`;
+  const coverUrl = sampleImage(item.imageLabel || item.title, ['#b7a17a', '#dfc7a2', '#a7b8aa'][index % 3]);
+  return {
+    id,
+    keyword,
+    title: item.title,
+    content: item.content,
+    author: item.author || `${preset.label}观察员`,
+    likeCount: metric(index, 6920),
+    collectCount: metric(index, 4100),
+    commentCount: metric(index, 612),
+    shareCount: metric(index, 488),
+    sourceUrl: `https://www.xiaohongshu.com/discovery/item/${id}?xsec_source=app_share&xsec_token=sample-token-${preset.id}-${index + 1}`,
+    webUrl: `https://www.xiaohongshu.com/discovery/item/${id}?xsec_source=app_share&xsec_token=sample-token-${preset.id}-${index + 1}`,
+    sourceUrlVerified: true,
+    coverUrl,
+    imageUrls: [coverUrl],
+    contentExcerpt: item.content,
+    tags: item.tags || [keyword, preset.label]
+  };
+}
+
+function buildComments({ item, noteId, keyword, index }) {
+  const comments = cleanList(item.comments).length
+    ? cleanList(item.comments)
+    : [`我最关心的是${keyword}到底适不适合自己,不想只看种草结论。`];
+  return comments.map((text, commentIndex) => ({
+    id: `${noteId}-comment-${commentIndex + 1}`,
+    noteId,
+    keyword,
+    text,
+    likeCount: Math.max(1, 401 - index * 88 - commentIndex * 65)
+  }));
+}
+
+function buildSampleDataset(profile = {}) {
+  const preset = resolveIndustryPreset(profile);
+  const keywords = cleanList(profile.keywords).length ? cleanList(profile.keywords) : preset.defaultKeywords;
+  const notes = [];
+  const comments = [];
+
+  preset.sampleNotes.slice(0, 3).forEach((item, index) => {
+    const keyword = keywords[index] || keywords[0] || preset.label;
+    const note = buildNote({ item, preset, keyword, index });
+    notes.push(note);
+    comments.push(...buildComments({ item, noteId: note.id, keyword, index }));
+  });
 
   return { notes, comments };
 }
@@ -76,4 +77,3 @@ function buildSampleDataset(profile = {}) {
 module.exports = {
   buildSampleDataset
 };
-

+ 77 - 0
claude-code-voc-intelligence/mcp/src/features/xiaohongshu-trend/signal-extractor.js

@@ -0,0 +1,77 @@
+const { buildSignalTerms, unique } = require('./industry-presets');
+
+function textOfItem(item = {}) {
+  return [
+    item.title,
+    item.content,
+    item.text,
+    item.contentExcerpt,
+    item.rawExcerpt,
+    ...(Array.isArray(item.tags) ? item.tags : [])
+  ].filter(Boolean).join(' ');
+}
+
+function collectTerms(items, terms) {
+  return unique(terms)
+    .map(term => {
+      const evidence = items.filter(item => textOfItem(item).includes(term));
+      return {
+        term,
+        count: evidence.length,
+        evidenceIds: evidence.map(item => item.id || item.noteId || '').filter(Boolean).slice(0, 5)
+      };
+    })
+    .filter(item => item.count > 0)
+    .sort((a, b) => b.count - a.count || a.term.localeCompare(b.term));
+}
+
+function collectTagTerms(notes = []) {
+  const counter = new Map();
+  notes.forEach(note => {
+    (Array.isArray(note.tags) ? note.tags : []).forEach(tag => {
+      const key = String(tag || '').trim();
+      if (!key) return;
+      counter.set(key, (counter.get(key) || 0) + 1);
+    });
+  });
+  return [...counter.entries()]
+    .map(([term, count]) => ({ term, count, evidenceIds: [] }))
+    .sort((a, b) => b.count - a.count || a.term.localeCompare(b.term));
+}
+
+function mergeSignals(primary, fallback) {
+  const seen = new Set();
+  return [...primary, ...fallback]
+    .filter(item => {
+      if (!item || !item.term || seen.has(item.term)) return false;
+      seen.add(item.term);
+      return true;
+    });
+}
+
+function buildSignalAnalysis({ profile = {}, notes = [], comments = [] } = {}) {
+  const { preset, preferenceTerms, decisionTerms, elementTerms, riskTerms } = buildSignalTerms(profile);
+  const allItems = [...notes, ...comments];
+  const tags = collectTagTerms(notes);
+  const keywordSignals = collectTerms(allItems, profile.keywords || []);
+
+  const preferenceSignals = mergeSignals(collectTerms(allItems, preferenceTerms), keywordSignals);
+  const decisionSignals = mergeSignals(collectTerms(allItems, decisionTerms), tags);
+  const elementSignals = mergeSignals(collectTerms(allItems, elementTerms), tags);
+  const riskSignals = collectTerms(allItems, riskTerms);
+
+  return {
+    preset,
+    preferenceSignals,
+    decisionSignals,
+    elementSignals,
+    riskSignals,
+    tagSignals: tags,
+    keywordSignals
+  };
+}
+
+module.exports = {
+  buildSignalAnalysis,
+  collectTerms
+};

+ 84 - 0
claude-code-voc-intelligence/mcp/src/features/xiaohongshu-trend/topic-builder.js

@@ -0,0 +1,84 @@
+function firstTerm(items, fallback = '') {
+  return items && items[0] && items[0].term ? items[0].term : fallback;
+}
+
+function firstDistinctTerm(items, excluded = [], fallback = '') {
+  const blocked = new Set([].concat(excluded).filter(Boolean).map(String));
+  const match = (items || []).find(item => item && item.term && !blocked.has(item.term));
+  return match ? match.term : fallback;
+}
+
+function findTopComment(comments, noteId) {
+  return comments
+    .filter(comment => comment.noteId === noteId)
+    .sort((a, b) => Number(b.likeCount || 0) - Number(a.likeCount || 0))[0];
+}
+
+function truncateText(value, limit = 80) {
+  const text = String(value || '').replace(/\s+/g, ' ').trim();
+  if (text.length <= limit) return text;
+  return `${text.slice(0, limit - 1)}...`;
+}
+
+function buildOpportunityTopics({ profile = {}, notes = [], comments = [], signals = {}, memoryRanker } = {}) {
+  const preset = signals.preset || {};
+  const industry = profile.industry || preset.label || '当前行业';
+  const preference = firstTerm(signals.preferenceSignals, firstTerm(signals.keywordSignals, '核心卖点'));
+  const concern = firstDistinctTerm(signals.decisionSignals, [preference], firstTerm(signals.riskSignals, '主要顾虑'));
+  const element = firstDistinctTerm(signals.elementSignals, [preference, concern], firstTerm(signals.keywordSignals, '具体元素'));
+  const risk = firstDistinctTerm(signals.riskSignals, [preference, concern, element], concern);
+  const topNote = notes[0] || {};
+  const topComment = topNote.id ? findTopComment(comments, topNote.id) : comments[0];
+  const commentEvidence = topComment && topComment.text ? truncateText(topComment.text, 70) : `${concern} 在样本中反复出现`;
+
+  const topics = [
+    {
+      title: `别只讲${preference},要讲清楚用户担心的${concern}`,
+      angle: '把显性卖点转成决策阻力的解释',
+      evidence: commentEvidence,
+      relatedNotes: topNote.id ? [topNote.id] : [],
+      confidence: topComment ? 'medium' : 'low'
+    },
+    {
+      title: `${industry}内容要从“好不好”走向“适不适合我”`,
+      angle: '用人群、场景和使用条件降低用户判断成本',
+      evidence: `${element}、${concern} 等信号共同出现,说明用户需要更具体的选择依据`,
+      relatedNotes: notes.slice(0, 2).map(note => note.id).filter(Boolean),
+      confidence: notes.length >= 2 ? 'medium' : 'low'
+    },
+    {
+      title: `围绕${element}做一轮真实案例验证`,
+      angle: '用评论原话和案例证据验证内容/产品机会',
+      evidence: risk ? `样本里出现 ${risk} 这类风险词,适合继续核验` : '当前样本还需要补充更多评论原话',
+      relatedNotes: notes.slice(0, 3).map(note => note.id).filter(Boolean),
+      confidence: signals.riskSignals && signals.riskSignals.length ? 'medium' : 'low'
+    }
+  ];
+
+  return typeof memoryRanker === 'function' ? memoryRanker(topics) : topics;
+}
+
+function buildBusinessTalk({ profile = {}, signals = {} } = {}) {
+  const preset = signals.preset || {};
+  const target = preset.talkTarget || '目标用户';
+  const application = preset.applicationLabel || '业务验证或内容选题';
+  const preference = firstTerm(signals.preferenceSignals, '核心卖点');
+  const concern = firstDistinctTerm(signals.decisionSignals, [preference], '主要顾虑');
+  const element = firstDistinctTerm(signals.elementSignals, [preference, concern], '具体元素');
+  const risk = firstDistinctTerm(signals.riskSignals, [preference, concern, element], '风险点');
+  const businessType = profile.businessType || '业务团队';
+
+  return [
+    `面对${target}沟通时,不要只说“${preference}好”,先追问:你最担心${concern},还是${risk}?`,
+    `${businessType}可以把“${element}”拆成真实案例、价格/成本、适用人群和售后承诺四个部分来讲。`,
+    `下一轮${application}建议优先确认“${concern}”是否真的是成交/转化前的最大阻力。`
+  ];
+}
+
+module.exports = {
+  firstTerm,
+  firstDistinctTerm,
+  findTopComment,
+  buildOpportunityTopics,
+  buildBusinessTalk
+};

+ 140 - 0
claude-code-voc-intelligence/mcp/src/providers/douyin-api.js

@@ -0,0 +1,140 @@
+const DOUYIN_API_ROOT = 'https://server.fmode.cn/api/voc-social/douyin';
+const DOUYIN_APP_BASE_URL = `${DOUYIN_API_ROOT}/app`;
+const DOUYIN_SEARCH_PATH = 'search/fetch_general_search_v2';
+const DOUYIN_VIDEO_COMMENTS_PATH = 'app/v3/fetch_video_comments';
+const DOUYIN_VIDEO_REPLIES_PATH = 'app/v3/fetch_video_comment_replies';
+
+function buildEndpointUrl(baseUrl, endpointPath) {
+  return `${String(baseUrl || '').replace(/\/$/, '')}/${String(endpointPath || '').replace(/^\//, '')}`;
+}
+
+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 pickBusinessStatus(json) {
+  const topCode = Number(json?.code);
+  if (Number.isFinite(topCode) && topCode !== 0 && topCode !== 200) {
+    return topCode;
+  }
+  const nestedCode = Number(json?.data?.status_code);
+  if (Number.isFinite(nestedCode) && nestedCode !== 0 && nestedCode !== 200) {
+    return nestedCode;
+  }
+  return 0;
+}
+
+function redactSecret(value) {
+  return String(value || '')
+    .replace(/Bearer\s+[^"'\s,}]+/gi, 'Bearer [REDACTED]')
+    .replace(/\bsk-[A-Za-z0-9_-]{6,}\b/g, 'sk-[REDACTED]')
+    .replace(/\br:[A-Za-z0-9]{4,}\b/g, 'r:[REDACTED]')
+    .replace(/("Authorization"\s*:\s*")([^"]+)(")/gi, '$1[REDACTED]$3')
+    .replace(/("api[_-]?token"\s*:\s*")([^"]+)(")/gi, '$1[REDACTED]$3');
+}
+
+function isSuccessCode(json) {
+  const topCode = Number(json?.code);
+  if (Number.isFinite(topCode) && ![0, 200].includes(topCode)) {
+    return false;
+  }
+  const statusCode = Number(json?.data?.status_code);
+  if (Number.isFinite(statusCode) && ![0, 200].includes(statusCode)) {
+    return false;
+  }
+  return true;
+}
+
+async function requestJson(url, token, { method = 'GET', body } = {}) {
+  const response = await fetch(url, {
+    method,
+    headers: {
+      Authorization: `Bearer ${token}`,
+      Accept: 'application/json',
+      ...(method === 'POST' ? { 'Content-Type': 'application/json' } : {})
+    },
+    body: body ? JSON.stringify(body) : undefined
+  });
+  const text = await response.text();
+  let json;
+  try {
+    json = JSON.parse(text);
+  } catch {
+    json = undefined;
+  }
+  if (!response.ok || (json && !isSuccessCode(json))) {
+    const message = json?.message || json?.msg || json?.data?.msg || json?.data?.message || text || `HTTP ${response.status}`;
+    const error = new Error(redactSecret(message));
+    error.httpStatus = pickBusinessStatus(json) || response.status || 500;
+    error.responseBody = redactSecret(text);
+    throw error;
+  }
+  return { json, data: unwrapData(json), rawText: text };
+}
+
+class DouyinApi {
+  constructor(options = {}) {
+    this.baseUrl = options.baseUrl || process.env.DOUYIN_API_BASE_URL || process.env.VOC_DOUYIN_API_BASE_URL || DOUYIN_API_ROOT;
+    this.token = options.token;
+  }
+
+  async searchVideos({
+    keyword,
+    cursor = 0,
+    sortType = '0',
+    publishTime = '0',
+    filterDuration = '0',
+    contentType = '0',
+    searchId = '',
+    backtrace = ''
+  }) {
+    const url = buildEndpointUrl(this.baseUrl, DOUYIN_SEARCH_PATH);
+    return requestJson(url, this.token, {
+      method: 'POST',
+      body: {
+        keyword,
+        cursor,
+        sort_type: sortType,
+        publish_time: publishTime,
+        filter_duration: filterDuration,
+        content_type: contentType,
+        search_id: searchId,
+        backtrace
+      }
+    });
+  }
+
+  async getVideoComments({ awemeId, cursor = 0, count = 20 }) {
+    const url = new URL(buildEndpointUrl(this.baseUrl, DOUYIN_VIDEO_COMMENTS_PATH));
+    url.searchParams.set('aweme_id', String(awemeId));
+    url.searchParams.set('cursor', String(cursor));
+    url.searchParams.set('count', String(count));
+    return requestJson(url, this.token);
+  }
+
+  async getCommentReplies({ itemId, commentId, cursor = 0, count = 20 }) {
+    const url = new URL(buildEndpointUrl(this.baseUrl, DOUYIN_VIDEO_REPLIES_PATH));
+    url.searchParams.set('item_id', String(itemId));
+    url.searchParams.set('comment_id', String(commentId));
+    url.searchParams.set('cursor', String(cursor));
+    url.searchParams.set('count', String(count));
+    return requestJson(url, this.token);
+  }
+}
+
+module.exports = {
+  DOUYIN_API_ROOT,
+  DOUYIN_APP_BASE_URL,
+  DouyinApi
+};

+ 22 - 6
claude-code-voc-intelligence/mcp/src/providers/xiaohongshu-api.js

@@ -1,4 +1,5 @@
-const DEFAULT_BASE_URL = 'https://server.fmode.cn/thapi/v1/xiaohongshu/app';
+const VOC_SOCIAL_API_ROOT = 'https://server.fmode.cn/api/voc-social';
+const XIAOHONGSHU_APP_BASE_URL = `${VOC_SOCIAL_API_ROOT}/xiaohongshu/app`;
 
 function buildUrl(baseUrl, path, query = {}) {
   const url = new URL(`${baseUrl.replace(/\/$/, '')}/${path.replace(/^\//, '')}`);
@@ -15,11 +16,23 @@ function unwrapData(json) {
   return json.data?.data || json.data || json;
 }
 
+function pickBusinessStatus(json) {
+  const topCode = Number(json?.code);
+  if (Number.isFinite(topCode) && topCode !== 0 && topCode !== 200) {
+    return topCode;
+  }
+  const nestedCode = Number(json?.data?.status_code);
+  if (Number.isFinite(nestedCode) && nestedCode !== 0 && nestedCode !== 200) {
+    return nestedCode;
+  }
+  return 0;
+}
+
 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(/\bsk-[A-Za-z0-9_-]{6,}\b/g, 'sk-[REDACTED]')
+    .replace(/\br:[A-Za-z0-9]{4,}\b/g, 'r:[REDACTED]')
     .replace(/("Authorization"\s*:\s*")([^"]+)(")/gi, '$1[REDACTED]$3')
     .replace(/("api[_-]?token"\s*:\s*")([^"]+)(")/gi, '$1[REDACTED]$3');
 }
@@ -39,10 +52,11 @@ async function requestJson(url, token) {
   } catch {
     json = undefined;
   }
-  if (!response.ok) {
+  const businessStatus = pickBusinessStatus(json);
+  if (!response.ok || businessStatus) {
     const message = json?.message || json?.msg || json?.data?.msg || text || `HTTP ${response.status}`;
     const error = new Error(redactSecret(message));
-    error.httpStatus = response.status;
+    error.httpStatus = businessStatus || response.status || 500;
     error.responseBody = redactSecret(text);
     throw error;
   }
@@ -51,7 +65,7 @@ async function requestJson(url, token) {
 
 class XiaohongshuApi {
   constructor(options = {}) {
-    this.baseUrl = options.baseUrl || DEFAULT_BASE_URL;
+    this.baseUrl = options.baseUrl || process.env.XIAOHONGSHU_API_BASE_URL || XIAOHONGSHU_APP_BASE_URL;
     this.token = options.token;
   }
 
@@ -82,5 +96,7 @@ class XiaohongshuApi {
 }
 
 module.exports = {
+  VOC_SOCIAL_API_ROOT,
+  XIAOHONGSHU_APP_BASE_URL,
   XiaohongshuApi
 };

+ 435 - 10
claude-code-voc-intelligence/mcp/src/server.js

@@ -2,9 +2,18 @@
 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 { readXiaohongshuToken, readVocToken } = require('./core/credentials');
 const { runXiaohongshuTrend } = require('./tools/xiaohongshu-trend-run');
 const { updateXiaohongshuPreference } = require('./tools/xiaohongshu-preference-update');
+const { runDouyinTrend } = require('./tools/douyin-trend-run');
+const { updateDouyinPreference } = require('./tools/douyin-preference-update');
+const { runVocProblemDeepDive } = require('./tools/voc-problem-deep-dive-run');
+const { runVocIssuePool } = require('./tools/voc-issue-pool-run');
+const { runVocContentPlan } = require('./tools/voc-content-plan-run');
+const { runVocCompetitorMap } = require('./tools/voc-competitor-map-run');
+const { runVocSpeakingScript } = require('./tools/voc-speaking-script-run');
+const { runBusinessWorkflow } = require('./tools/voc-business-workflow-run');
+const { buildVocRechargeInfo, buildMissingTokenMessage } = require('./core/payment-links');
 
 function asToolResult(result) {
   return {
@@ -22,35 +31,360 @@ function asToolResult(result) {
 function createServer() {
   const server = new McpServer({
     name: 'voc-intelligence',
-    version: '0.1.2'
+    version: '0.3.10'
   });
 
+  server.registerTool(
+    'voc_business_workflow_run',
+    {
+      title: 'Run VOC Business Workflow',
+      description: [
+        'Run the full VOC business workflow from market voices to issue pool, top issue deep dive, 7-day content plan, and one speaking script.',
+        'Use when the user asks to see what customers care about, decide what to fix first, and know what to post next week in one end-to-end workflow.'
+      ].join(' '),
+      inputSchema: {
+        project: z.string().optional(),
+        brand: z.string().optional(),
+        store: z.string().optional(),
+        industry: z.string().optional(),
+        category: z.string().optional(),
+        platform: z.enum(['douyin', 'xiaohongshu', '抖音', '小红书']).optional(),
+        collectionMode: z.enum(['sample', 'live']).optional(),
+        scenario: z.string().optional(),
+        scene: z.string().optional(),
+        audience: z.string().optional(),
+        targetAudience: z.string().optional(),
+        keywords: z.array(z.string()).optional(),
+        keyword: z.string().optional(),
+        issue: z.string().optional(),
+        problem: z.string().optional(),
+        topic: z.string().optional(),
+        feedback: z.string().optional(),
+        finalize: z.boolean().optional(),
+        output: z.string().optional(),
+        keywordLimit: z.number().int().min(1).max(10).optional(),
+        notesPerKeyword: z.number().int().min(1).max(10).optional(),
+        videosPerKeyword: z.number().int().min(1).max(10).optional(),
+        maxCommentPages: z.number().int().min(0).max(5).optional(),
+        xiaohongshuToken: z.string().optional(),
+        douyinToken: z.string().optional(),
+        vocToken: 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.any()).optional(),
+        errors: z.array(z.any()).optional()
+      }
+    },
+    async input => asToolResult(await runBusinessWorkflow(input))
+  );
+
+  server.registerTool(
+    'voc_speaking_script_run',
+    {
+      title: 'Run VOC Speaking Script Co-Creation',
+      description: [
+        'Turn one VOC-backed topic into a co-created speaking script.',
+        'Use after a content plan when the user selects a topic, asks to enter script co-creation, revise a speaking script, finalize a draft, or save script preferences.'
+      ].join(' '),
+      inputSchema: {
+        project: z.string().optional(),
+        brand: z.string().optional(),
+        store: z.string().optional(),
+        industry: z.string().optional(),
+        category: z.string().optional(),
+        topic: z.string().optional(),
+        title: z.string().optional(),
+        selectedTopic: z.string().optional(),
+        userIssue: z.string().optional(),
+        issue: z.string().optional(),
+        problem: z.string().optional(),
+        issues: z.array(z.string()).optional(),
+        problems: z.array(z.string()).optional(),
+        vocIssues: z.array(z.string()).optional(),
+        evidence: z.array(z.string()).optional(),
+        comments: z.array(z.string()).optional(),
+        voices: z.array(z.string()).optional(),
+        vocEvidence: z.array(z.string()).optional(),
+        evidenceText: z.string().optional(),
+        feedback: z.string().optional(),
+        message: z.string().optional(),
+        preferredStyles: z.array(z.string()).optional(),
+        blockedStyles: z.array(z.string()).optional(),
+        finalize: z.boolean().optional(),
+        memory: z.string().optional(),
+        memoryPath: z.string().optional(),
+        output: 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.any()).optional(),
+        errors: z.array(z.any()).optional()
+      }
+    },
+    async input => asToolResult(await runVocSpeakingScript(input))
+  );
+
+  server.registerTool(
+    'voc_content_plan_run',
+    {
+      title: 'Run VOC Content Plan',
+      description: [
+        'Turn VOC issues, comments, trend reports, or issue-pool findings into a 7-day content plan and speaking scripts.',
+        'Use when the user asks what to post next week, wants口播脚本, video topics, account content ideas, or wants to turn customer problems into marketing content.'
+      ].join(' '),
+      inputSchema: {
+        project: z.string().optional(),
+        brand: z.string().optional(),
+        store: z.string().optional(),
+        industry: z.string().optional(),
+        category: z.string().optional(),
+        platform: z.string().optional(),
+        days: z.number().int().min(1).max(15).optional(),
+        issues: z.array(z.string()).optional(),
+        problems: z.array(z.string()).optional(),
+        vocIssues: z.array(z.string()).optional(),
+        evidence: z.array(z.string()).optional(),
+        comments: z.array(z.string()).optional(),
+        evidenceText: z.string().optional(),
+        reportText: z.string().optional(),
+        report: z.string().optional(),
+        reportPath: 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.any()).optional(),
+        errors: z.array(z.any()).optional()
+      }
+    },
+    async input => asToolResult(await runVocContentPlan(input))
+  );
+
+  server.registerTool(
+    'voc_competitor_map_run',
+    {
+      title: 'Run VOC Competitor Map',
+      description: [
+        'Build a competitor map and differentiated opportunity report from category, city, competitors, and VOC evidence.',
+        'Use when the user asks to look at competitors, competitor analysis, who else is doing well, or how the brand should compete differently.'
+      ].join(' '),
+      inputSchema: {
+        project: z.string().optional(),
+        brand: z.string().optional(),
+        store: z.string().optional(),
+        industry: z.string().optional(),
+        category: z.string().optional(),
+        city: z.string().optional(),
+        region: z.string().optional(),
+        priceBand: z.string().optional(),
+        price: z.string().optional(),
+        scenario: z.string().optional(),
+        scene: z.string().optional(),
+        competitors: z.array(z.string()).optional(),
+        competitorNames: z.array(z.string()).optional(),
+        evidence: z.array(z.string()).optional(),
+        comments: z.array(z.string()).optional(),
+        voices: z.array(z.string()).optional(),
+        reportText: z.string().optional(),
+        report: z.string().optional(),
+        reportPath: 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.any()).optional(),
+        errors: z.array(z.any()).optional()
+      }
+    },
+    async input => asToolResult(await runVocCompetitorMap(input))
+  );
+
+  server.registerTool(
+    'voc_issue_pool_run',
+    {
+      title: 'Run VOC Issue Pool',
+      description: [
+        'Turn user comments, report text, or VOC snippets into a prioritized issue pool.',
+        'Use when the user asks which real problems matter most, asks to manage VOC issues, or wants a problem list before deep-diving.'
+      ].join(' '),
+      inputSchema: {
+        project: z.string().optional().describe('Optional project, brand, account, or store name used to isolate issue-pool memory.'),
+        brand: z.string().optional(),
+        store: z.string().optional(),
+        industry: z.string().optional(),
+        category: z.string().optional(),
+        scenario: z.string().optional(),
+        scene: z.string().optional(),
+        businessType: z.string().optional(),
+        audience: z.string().optional(),
+        targetAudience: z.string().optional(),
+        evidence: z.array(z.string()).optional().describe('User comments or VOC snippets.'),
+        comments: z.array(z.string()).optional().describe('User comment snippets.'),
+        voices: z.array(z.string()).optional().describe('User voice snippets.'),
+        issues: z.array(z.string()).optional().describe('Known issue snippets.'),
+        issueTexts: z.array(z.string()).optional().describe('Known issue snippets.'),
+        evidenceText: z.string().optional().describe('Raw comments or evidence text.'),
+        reportText: z.string().optional().describe('Previous report markdown text.'),
+        report: z.string().optional().describe('Optional previous report markdown path.'),
+        reportPath: z.string().optional().describe('Optional previous report markdown path.'),
+        memory: z.string().optional().describe('Optional issue-pool memory JSON path.'),
+        memoryPath: z.string().optional().describe('Optional issue-pool memory JSON path.'),
+        feedback: z.string().optional().describe('User feedback or status notes.'),
+        message: z.string().optional().describe('User feedback or status notes.'),
+        resolvedIssues: z.array(z.string()).optional().describe('Issue names or ids already resolved.'),
+        validatingIssues: z.array(z.string()).optional().describe('Issue names or ids currently validating.'),
+        blockedIssues: z.array(z.string()).optional().describe('Issue names or ids to pause.'),
+        statusUpdates: z.record(z.string()).optional().describe('Map of issue title/id to status.')
+      },
+      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.any()).optional(),
+        errors: z.array(z.any()).optional()
+      }
+    },
+    async input => {
+      const result = await runVocIssuePool(input);
+      return asToolResult(result);
+    }
+  );
+
+  server.registerTool(
+    'voc_problem_deep_dive_run',
+    {
+      title: 'Run VOC Problem Deep Dive',
+      description: [
+        'Deep-dive a single VOC problem from a boss/operator perspective.',
+        'Use when the user says continue digging, what should the boss do, how to solve this problem, or turn this VOC into actions.'
+      ].join(' '),
+      inputSchema: {
+        issue: z.string().optional().describe('Single VOC issue to deep dive, e.g. 排队, 贵, 不好吃, 服务差.'),
+        problem: z.string().optional().describe('Alias for issue.'),
+        topic: z.string().optional().describe('Alias for issue.'),
+        project: z.string().optional().describe('Optional project, brand, or account name used to isolate deep-dive memory.'),
+        brand: z.string().optional().describe('Optional brand name used to isolate deep-dive memory.'),
+        store: z.string().optional().describe('Optional store name used to isolate deep-dive memory.'),
+        industry: z.string().optional(),
+        category: z.string().optional(),
+        scenario: z.string().optional(),
+        scene: z.string().optional(),
+        businessType: z.string().optional(),
+        audience: z.string().optional(),
+        targetAudience: z.string().optional(),
+        evidence: z.array(z.string()).optional().describe('Optional user comments or VOC snippets.'),
+        evidenceText: z.string().optional().describe('Optional text with comments or evidence.'),
+        comments: z.array(z.string()).optional().describe('Optional comment snippets.'),
+        voices: z.array(z.string()).optional().describe('Optional user voice snippets.'),
+        report: z.string().optional().describe('Optional previous report markdown path.'),
+        reportPath: z.string().optional().describe('Optional previous report markdown path.'),
+        memory: z.string().optional().describe('Optional deep-dive memory JSON path.'),
+        memoryPath: z.string().optional().describe('Optional deep-dive memory JSON path.'),
+        feedback: z.string().optional().describe('User feedback from previous iteration.'),
+        message: z.string().optional().describe('User feedback from previous iteration.'),
+        preferredActions: z.array(z.string()).optional().describe('Actions the user prefers.'),
+        blockedActions: z.array(z.string()).optional().describe('Actions the user does not want.'),
+        preferredContentAngles: z.array(z.string()).optional().describe('Content angles the user prefers.'),
+        validatedActions: z.array(z.string()).optional().describe('Actions proven useful in practice.'),
+        rejectedActions: z.array(z.string()).optional().describe('Actions proven ineffective in practice.')
+      },
+      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.any()).optional(),
+        errors: z.array(z.any()).optional()
+      }
+    },
+    async input => {
+      const result = await runVocProblemDeepDive(input);
+      return asToolResult(result);
+    }
+  );
+
   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()
+        xiaohongshuToken: z.string().optional().describe('Optional request-scoped collection token. It is never echoed back.'),
+        vocToken: z.string().optional().describe('Optional VOC social token, same convention as douyin-speaking-daily. It is never echoed back.')
       },
       outputSchema: {
         status: z.string(),
         assistantMessage: z.string(),
-        configured: z.boolean()
+        configured: z.boolean(),
+        data: z.record(z.any()).optional()
       }
     },
     async input => {
       const token = readXiaohongshuToken(input);
+      const recharge = token ? null : await buildVocRechargeInfo();
       const result = {
         status: token ? 'ok' : 'needs_token',
         configured: Boolean(token),
+        data: recharge ? { recharge } : {},
         assistantMessage: token
           ? '小红书 live 采集 Token 已配置,可以尝试小规模真实采集。'
-          : [
-              '还没有检测到小红书 live 采集 Token。',
-              '',
-              '可以先使用 sample 模式演示;如需真实采集,请设置 `XIAOHONGSHU_API_TOKEN` 或写入 `~/.claude/voc-credentials.json`。'
-            ].join('\n')
+          : buildMissingTokenMessage(recharge.paymentUrl)
+      };
+      return asToolResult(result);
+    }
+  );
+
+  server.registerTool(
+    'voc_douyin_token_check',
+    {
+      title: 'Check Douyin Token',
+      description: 'Check whether a Douyin/VOC API token is configured for live collection.',
+      inputSchema: {
+        douyinToken: z.string().optional().describe('Optional request-scoped Douyin token. It is never echoed back.'),
+        vocToken: z.string().optional().describe('Optional VOC social token, same convention as xiaohongshu trend.'),
+        xiaohongshuToken: z.string().optional().describe('Optional platform token alias, never echoed back.')
+      },
+      outputSchema: {
+        status: z.string(),
+        assistantMessage: z.string(),
+        configured: z.boolean(),
+        data: z.record(z.any()).optional()
+      }
+    },
+    async input => {
+      const token = readVocToken(input);
+      const recharge = token ? null : await buildVocRechargeInfo();
+      const result = {
+        status: token ? 'ok' : 'needs_token',
+        configured: Boolean(token),
+        data: recharge ? { recharge } : {},
+        assistantMessage: token
+          ? '抖音 live 采集 Token 已配置,可以开始小规模真实采集。'
+          : buildMissingTokenMessage(recharge.paymentUrl, recharge, { platformLabel: '抖音' })
       };
       return asToolResult(result);
     }
@@ -83,7 +417,10 @@ function createServer() {
         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()
+        cacheAssets: z.boolean().optional().describe('Whether to cache evidence images into the output assets directory. Defaults to true.'),
+        assetLimit: z.number().int().min(0).max(30).optional().describe('Maximum number of note cover images to cache.'),
+        xiaohongshuToken: z.string().optional().describe('Optional request-scoped collection token. It is used only for this run and never echoed back.'),
+        vocToken: z.string().optional().describe('Optional VOC social token, same convention as douyin-speaking-daily. It is used only for this run and never echoed back.')
       },
       outputSchema: {
         status: z.string(),
@@ -105,6 +442,60 @@ function createServer() {
     }
   );
 
+  server.registerTool(
+    'voc_douyin_trend_run',
+    {
+      title: 'Run Douyin Trend Intelligence',
+      description: [
+        'Generate first-round Douyin 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(),
+        videosPerKeyword: z.number().int().min(1).max(10).optional(),
+        maxCommentPages: z.number().int().min(0).max(5).optional(),
+        sortType: z.enum(['0', '1', '2']).optional(),
+        publishTime: z.enum(['0', '1', '7', '180']).optional(),
+        filterDuration: z.enum(['0', '0-1', '1-5', '5-10000']).optional(),
+        contentType: z.enum(['0', '1', '2', '3']).optional(),
+        cacheAssets: z.boolean().optional().describe('Whether to cache evidence images into the output assets directory. Defaults to true.'),
+        assetLimit: z.number().int().min(0).max(30).optional().describe('Maximum number of video cover images to cache.'),
+        douyinToken: z.string().optional().describe('Optional request-scoped collection token. It is used only for this run and never echoed back.'),
+        vocToken: z.string().optional().describe('Optional VOC social token, same convention as xiaohongshu trend. It is used only for this run and never echoed back.')
+      },
+      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.any()).optional(),
+        errors: z.array(z.any()).optional()
+      }
+    },
+    async input => {
+      const result = await runDouyinTrend({
+        ...input,
+        collectionMode: input.collectionMode || 'sample'
+      });
+      return asToolResult(result);
+    }
+  );
+
   server.registerTool(
     'voc_xiaohongshu_preference_update',
     {
@@ -139,6 +530,40 @@ function createServer() {
     }
   );
 
+  server.registerTool(
+    'voc_douyin_preference_update',
+    {
+      title: 'Update Douyin Trend Preference',
+      description: [
+        'Save user feedback for the next Douyin 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.any()).optional(),
+        errors: z.array(z.any()).optional()
+      }
+    },
+    async input => {
+      const result = await updateDouyinPreference(input);
+      return asToolResult(result);
+    }
+  );
+
   return server;
 }
 

+ 70 - 0
claude-code-voc-intelligence/mcp/src/tools/douyin-preference-update.js

@@ -0,0 +1,70 @@
+#!/usr/bin/env node
+const path = require('path');
+const { okResult } = require('../core/result-envelope');
+const { updateTrendMemory } = require('../features/douyin-trend/preference-memory');
+
+function parseArgs(argv) {
+  const args = {};
+  const setArg = (key, value) => {
+    args[key] = value;
+    if (key.includes('-')) {
+      args[key.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())] = value;
+    }
+  };
+  for (let i = 0; i < argv.length; i++) {
+    const token = argv[i];
+    if (!token.startsWith('--')) continue;
+    const eq = token.indexOf('=');
+    if (eq >= 0) {
+      setArg(token.slice(2, eq), token.slice(eq + 1));
+      continue;
+    }
+    const key = token.slice(2);
+    const next = argv[i + 1];
+    if (next && !next.startsWith('--')) {
+      setArg(key, next);
+      i++;
+    } else {
+      setArg(key, true);
+    }
+  }
+  return args;
+}
+
+async function updateDouyinPreference(input = {}) {
+  const result = updateTrendMemory(input);
+  return okResult({
+    status: 'ok',
+    assistantMessage: [
+      '# 抖音偏好记忆已更新',
+      '',
+      '已把你这次的反馈写入偏好记忆,下一轮抖音趋势情报会自动参考。',
+      '',
+      `memory: ${path.relative(process.cwd(), result.memoryPath)}`
+    ].join('\n'),
+    summary: {
+      memoryPath: result.memoryPath,
+      preferredDirections: result.memory.preferredDirections,
+      blockedDirections: result.memory.blockedDirections,
+      focusModes: result.memory.focusModes
+    },
+    data: result
+  });
+}
+
+async function main() {
+  const args = parseArgs(process.argv.slice(2));
+  const result = await updateDouyinPreference(args);
+  console.log(JSON.stringify(result, null, 2));
+}
+
+if (require.main === module) {
+  main().catch(error => {
+    console.error(JSON.stringify({ status: 'error', message: error.message }, null, 2));
+    process.exit(1);
+  });
+}
+
+module.exports = {
+  updateDouyinPreference
+};

+ 354 - 0
claude-code-voc-intelligence/mcp/src/tools/douyin-trend-run.js

@@ -0,0 +1,354 @@
+#!/usr/bin/env node
+const fs = require('fs');
+const path = require('path');
+const { okResult, errorResult } = require('../core/result-envelope');
+const { readVocToken } = require('../core/credentials');
+const { buildSampleDataset } = require('../features/douyin-trend/sample-data');
+const { collectLiveDataset } = require('../features/douyin-trend/live-collector');
+const { cacheEvidenceAssets } = require('../features/xiaohongshu-trend/assets');
+const { buildTrendReport, writeReport } = require('../features/douyin-trend/report');
+const { trendMemoryPath, readTrendMemory } = require('../features/douyin-trend/preference-memory');
+const { buildNoSampleFallbackMessage } = require('../core/evidence-quality');
+const {
+  buildVocRechargeInfo,
+  buildMissingTokenMessage,
+  buildRechargeRequiredMessage
+} = require('../core/payment-links');
+
+function parseArgs(argv) {
+  const args = {};
+  const setArg = (key, value) => {
+    args[key] = value;
+    if (key.includes('-')) {
+      args[key.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())] = value;
+    }
+  };
+  for (let i = 0; i < argv.length; i++) {
+    const token = argv[i];
+    if (!token.startsWith('--')) continue;
+    const eq = token.indexOf('=');
+    if (eq >= 0) {
+      setArg(token.slice(2, eq), token.slice(eq + 1));
+      continue;
+    }
+    const key = token.slice(2);
+    const next = argv[i + 1];
+    if (next && !next.startsWith('--')) {
+      setArg(key, next);
+      i++;
+    } else {
+      setArg(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 defaultProfile() {
+  return {
+    project: '抖音趋势情报官',
+    industry: '通用行业',
+    businessType: '品牌方/门店/服务商/内容账号',
+    targetAudience: ['目标用户'],
+    keywords: ['行业关键词', '用户痛点', '产品体验', '价格', '口碑'],
+    trendQuestions: [
+      '用户最近在关注什么表达和卖点?',
+      '哪些顾虑影响点击、停留或评论?',
+      '什么内容开头最容易带来转化?'
+    ],
+    mustTrackSignals: ['价格', '口碑', '效果', '安全', '服务', '体验']
+  };
+}
+
+function readJsonIfExists(filePath) {
+  if (!filePath) return {};
+  const absolute = path.resolve(filePath);
+  if (!fs.existsSync(absolute)) return {};
+  return JSON.parse(fs.readFileSync(absolute, 'utf8'));
+}
+
+function findAuthError(errors = []) {
+  return errors.find(error => {
+    const message = String(error && error.message || '');
+    return Number(error && error.httpStatus) === 401 ||
+      /Invalid API token|无效的API令牌|invalid token/i.test(message);
+  });
+}
+
+function findGatewayError(errors = []) {
+  return errors.find(error => {
+    const status = Number(error && error.httpStatus);
+    const message = String(error && error.message || '');
+    return status === 402 ||
+      status === 403 ||
+      /社交平台接口请求错误|没有开通|余额不足|无权限|权限或余额|permission|quota|payment|insufficient|balance/i.test(message);
+  });
+}
+
+function loadProfile(args) {
+  const fromFile = readJsonIfExists(args.profile);
+  const defaults = defaultProfile();
+  return {
+    ...fromFile,
+    project: args.project || fromFile.project || defaults.project,
+    industry: args.industry || fromFile.industry || defaults.industry,
+    businessType: args.businessType || args['business-type'] || fromFile.businessType || defaults.businessType,
+    targetAudience: splitList(args.targetAudience || args['target-audience']).length
+      ? splitList(args.targetAudience || args['target-audience'])
+      : (fromFile.targetAudience || defaults.targetAudience),
+    keywords: splitList(args.keywords).length
+      ? splitList(args.keywords)
+      : (fromFile.keywords || defaults.keywords),
+    trendQuestions: splitList(args.trendQuestions || args['trend-questions']).length
+      ? splitList(args.trendQuestions || args['trend-questions'])
+      : (fromFile.trendQuestions || defaults.trendQuestions),
+    mustTrackSignals: splitList(args.mustTrackSignals || args['must-track-signals']).length
+      ? splitList(args.mustTrackSignals || args['must-track-signals'])
+      : (fromFile.mustTrackSignals || defaults.mustTrackSignals)
+  };
+}
+
+async function runDouyinTrend(input = {}) {
+  const collectionMode = input.collectionMode || input['collection-mode'] || 'sample';
+  const outputDir = path.resolve(input.output || path.join('outputs', 'claude-code-douyin-trend', new Date().toISOString().slice(0, 10)));
+  const profile = loadProfile(input);
+  const memoryPath = trendMemoryPath(input);
+  const memory = readTrendMemory(memoryPath);
+  const warnings = [];
+  const errors = [];
+  const assetFiles = [];
+  let dataset;
+
+  if (collectionMode === 'sample') {
+    dataset = buildSampleDataset(profile);
+    warnings.push('P0 阶段使用 sample 演示数据,真实采集需要接入 VOC 服务。');
+  } else if (collectionMode === 'live') {
+    const token = readVocToken(input);
+    if (!token) {
+      const recharge = await buildVocRechargeInfo();
+      return {
+        status: 'needs_token',
+        assistantMessage: buildMissingTokenMessage(recharge.paymentUrl, recharge, { platformLabel: '抖音' }),
+        summary: { requestedCollectionMode: collectionMode },
+        data: {
+          requestedCollectionMode: collectionMode,
+          recharge
+        },
+        files: [],
+        nextActions: [
+          `打开充值/开通链接:${recharge.paymentUrl}`,
+          '充值完成后配置 session token,再重新运行 live 模式',
+          '如果只是录制教学,可以先用 sample 模式演示完整流程'
+        ],
+        warnings: [],
+        errors: []
+      };
+    }
+
+    const liveDataset = await collectLiveDataset({
+      profile,
+      token,
+      options: {
+        keywordLimit: input.keywordLimit || input['keyword-limit'],
+        videosPerKeyword: input.videosPerKeyword || input['videos-per-keyword'],
+        maxCommentPages: input.maxCommentPages || input['max-comment-pages'],
+        sortType: input.sortType || input.sort || input['sort-type'],
+        publishTime: input.publishTime || input['publish-time'],
+        filterDuration: input.filterDuration || input['filter-duration'],
+        contentType: input.contentType || input['content-type'],
+        baseUrl: input.baseUrl || input['base-url'] || process.env.DOUYIN_API_BASE_URL || process.env.VOC_DOUYIN_API_BASE_URL
+      }
+    });
+    dataset = {
+      videos: liveDataset.videos,
+      comments: liveDataset.comments
+    };
+    warnings.push(...liveDataset.warnings);
+    errors.push(...liveDataset.errors);
+
+    const collectionIssue = findGatewayError([...errors, ...warnings]);
+    if (collectionIssue) {
+      const recharge = await buildVocRechargeInfo({ token });
+      return {
+        status: 'needs_recharge',
+        assistantMessage: buildRechargeRequiredMessage(recharge.paymentUrl, recharge, { platformLabel: '抖音' }),
+        summary: {
+          videoCount: dataset.videos.length,
+          commentCount: dataset.comments.length,
+          failedStage: collectionIssue.stage || '',
+          failedKeyword: collectionIssue.keyword || ''
+        },
+        data: {
+          profile,
+          recharge
+        },
+        files: [],
+        nextActions: [
+          `打开充值链接:${recharge.paymentUrl}`,
+          '支付完成后重新运行本次抖音 live 采集',
+          '如果需要先给客户演示,可以切换 sample 模式继续'
+        ],
+        warnings: [],
+        errors: []
+      };
+    }
+
+    if (!dataset.videos.length) {
+      const authError = findAuthError(errors);
+      if (authError) {
+        const recharge = await buildVocRechargeInfo({ token });
+        return {
+          status: 'needs_valid_token',
+          assistantMessage: [
+            '当前 VOC-AI 数据服务 token 没有通过校验,暂时不能进行真实抖音采集。',
+            '',
+            '请确认已经完成 VOC-AI 数据服务开通/充值,并重新配置服务方提供的 session token。',
+            '',
+            `充值/开通链接:${recharge.paymentUrl}`
+          ].join('\n'),
+          summary: {
+            videoCount: 0,
+            commentCount: dataset.comments.length,
+            errorCount: errors.length,
+            failedStage: authError.stage || 'search_videos',
+            failedKeyword: authError.keyword || ''
+          },
+          data: { profile, recharge },
+          files: [],
+          nextActions: [
+            `打开充值/开通链接:${recharge.paymentUrl}`,
+            '重新配置有效 session token 后重试 live 模式',
+            '需要录教学视频时,可先用 sample 模式演示完整多轮校准流程'
+          ],
+          warnings: [],
+          errors: []
+        };
+      }
+
+      const gatewayError = findGatewayError(errors);
+      if (gatewayError) {
+        const recharge = await buildVocRechargeInfo({ token });
+        return {
+          status: 'needs_recharge',
+          assistantMessage: buildRechargeRequiredMessage(recharge.paymentUrl, recharge, { platformLabel: '抖音' }),
+          summary: {
+            videoCount: 0,
+            commentCount: dataset.comments.length,
+            failedStage: gatewayError.stage || 'search_videos',
+            failedKeyword: gatewayError.keyword || ''
+          },
+          data: { profile, recharge },
+          files: [],
+          nextActions: [
+            `打开充值链接:${recharge.paymentUrl}`,
+            '支付完成后重新运行本次抖音 live 采集',
+            '如果需要先给客户演示,可以切换 sample 模式继续'
+          ],
+          warnings: [],
+          errors: []
+        };
+      }
+
+      return {
+        status: 'no_live_samples',
+        assistantMessage: buildNoSampleFallbackMessage({
+          platformLabel: '抖音',
+          profile,
+          examples: [
+            '把行业词换成视频表达词,例如“第一次来怎么点”“老板推荐”“避坑”',
+            '加入城市/商圈/品类,例如“南昌 烧烤 第一次来”“南昌菜 排队”',
+            '从窄服务词扩展到用户情绪词,例如“父母礼物、家庭故事、人生感悟”'
+          ]
+        }),
+        summary: {
+          videoCount: 0,
+          commentCount: dataset.comments.length,
+          errorCount: errors.length
+        },
+        data: { profile },
+        files: [],
+        nextActions: [
+          '换一组更像视频标题/评论表达的关键词后重试 live 模式',
+          '扩大到相关大词做第一轮真实样本尝试',
+          '如果用于教学或远程演示,先切换 sample 模式,并明确标注非真实采集'
+        ],
+        warnings,
+        errors
+      };
+    }
+  } else {
+    return errorResult(`Unsupported collectionMode: ${collectionMode}`);
+  }
+
+  const cacheAssetsEnabled = input.cacheAssets !== false &&
+    input['cache-assets'] !== false &&
+    String(input.cacheAssets || input['cache-assets'] || '').toLowerCase() !== 'false';
+  if (cacheAssetsEnabled) {
+    const assetResult = await cacheEvidenceAssets({
+      notes: dataset.videos,
+      outputDir,
+      limit: input.assetLimit ?? input['asset-limit'] ?? 10
+    });
+    warnings.push(...assetResult.warnings);
+    assetFiles.push(...assetResult.files);
+  }
+
+  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]}` : '',
+      '把本轮证据整理成 VOC 问题池,判断先改哪个问题。',
+      '也可以直接补充一条你最近听到的真实用户反馈,我会据此收敛下一轮判断。'
+    ].filter(Boolean)
+    : [
+      '先补充一条你最近听到的真实用户反馈',
+      '把本轮证据整理成 VOC 问题池,判断先改哪个问题。',
+      '再基于用户反馈收敛下一轮趋势判断'
+    ];
+
+  return okResult({
+    assistantMessage: report.assistantMessage,
+    summary: report.summary,
+    data: {
+      ...report.data,
+      memoryPath
+    },
+    files: [...files, ...assetFiles],
+    nextActions,
+    warnings,
+    errors
+  });
+}
+
+async function main() {
+  const args = parseArgs(process.argv.slice(2));
+  const result = await runDouyinTrend(args);
+  if (args.assistantMessageOnly || args['assistant-message-only']) {
+    console.log(result.assistantMessage || JSON.stringify(result, null, 2));
+  } else {
+    console.log(JSON.stringify(result, null, 2));
+  }
+  if (!args.assistantMessageOnly && !args['assistant-message-only'] && (args.resultPrefix || args['result-prefix'])) {
+    const prefix = args.resultPrefix || args['result-prefix'];
+    console.log(`${prefix}=${JSON.stringify(result)}`);
+  }
+}
+
+if (require.main === module) {
+  main().catch(error => {
+    console.error(JSON.stringify({ status: 'error', message: error.message }, null, 2));
+    process.exit(1);
+  });
+}
+
+module.exports = {
+  runDouyinTrend
+};

+ 52 - 0
claude-code-voc-intelligence/mcp/src/tools/voc-business-workflow-run.js

@@ -0,0 +1,52 @@
+#!/usr/bin/env node
+const { runBusinessWorkflow } = require('../features/voc-business-workflow/business-workflow');
+
+function parseArgs(argv) {
+  const args = {};
+  const setArg = (key, value) => {
+    args[key] = value;
+    if (key.includes('-')) {
+      args[key.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())] = value;
+    }
+  };
+  for (let i = 0; i < argv.length; i++) {
+    const token = argv[i];
+    if (!token.startsWith('--')) continue;
+    const eq = token.indexOf('=');
+    if (eq >= 0) {
+      setArg(token.slice(2, eq), token.slice(eq + 1));
+      continue;
+    }
+    const key = token.slice(2);
+    const next = argv[i + 1];
+    if (next && !next.startsWith('--')) {
+      setArg(key, next);
+      i++;
+    } else {
+      setArg(key, true);
+    }
+  }
+  return args;
+}
+
+async function main() {
+  const args = parseArgs(process.argv.slice(2));
+  const result = await runBusinessWorkflow(args);
+  if (args.assistantMessageOnly || args['assistant-message-only']) {
+    console.log(result.assistantMessage || JSON.stringify(result, null, 2));
+  } else {
+    console.log(JSON.stringify(result, null, 2));
+  }
+  process.exitCode = result.status === 'ok' ? 0 : 1;
+}
+
+if (require.main === module) {
+  main().catch(error => {
+    console.error(JSON.stringify({ status: 'error', message: error.message }, null, 2));
+    process.exit(1);
+  });
+}
+
+module.exports = {
+  runBusinessWorkflow
+};

+ 59 - 0
claude-code-voc-intelligence/mcp/src/tools/voc-competitor-map-run.js

@@ -0,0 +1,59 @@
+#!/usr/bin/env node
+const { buildCompetitorMap } = require('../features/voc-competitor-map/competitor-map');
+
+function parseArgs(argv) {
+  const args = {};
+  const setArg = (key, value) => {
+    args[key] = value;
+    if (key.includes('-')) {
+      args[key.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())] = value;
+    }
+  };
+  for (let i = 0; i < argv.length; i++) {
+    const token = argv[i];
+    if (!token.startsWith('--')) continue;
+    const eq = token.indexOf('=');
+    if (eq >= 0) {
+      setArg(token.slice(2, eq), token.slice(eq + 1));
+      continue;
+    }
+    const key = token.slice(2);
+    const next = argv[i + 1];
+    if (next && !next.startsWith('--')) {
+      setArg(key, next);
+      i++;
+    } else {
+      setArg(key, true);
+    }
+  }
+  return args;
+}
+
+async function runVocCompetitorMap(input = {}) {
+  return buildCompetitorMap(input);
+}
+
+async function main() {
+  const args = parseArgs(process.argv.slice(2));
+  const result = await runVocCompetitorMap(args);
+  if (args.assistantMessageOnly || args['assistant-message-only']) {
+    console.log(result.assistantMessage || JSON.stringify(result, null, 2));
+  } else {
+    console.log(JSON.stringify(result, null, 2));
+  }
+  if (!args.assistantMessageOnly && !args['assistant-message-only'] && (args.resultPrefix || args['result-prefix'])) {
+    const prefix = args.resultPrefix || args['result-prefix'];
+    console.log(`${prefix}=${JSON.stringify(result)}`);
+  }
+}
+
+if (require.main === module) {
+  main().catch(error => {
+    console.error(JSON.stringify({ status: 'error', message: error.message }, null, 2));
+    process.exit(1);
+  });
+}
+
+module.exports = {
+  runVocCompetitorMap
+};

+ 59 - 0
claude-code-voc-intelligence/mcp/src/tools/voc-content-plan-run.js

@@ -0,0 +1,59 @@
+#!/usr/bin/env node
+const { buildContentPlan } = require('../features/voc-content-plan/content-plan');
+
+function parseArgs(argv) {
+  const args = {};
+  const setArg = (key, value) => {
+    args[key] = value;
+    if (key.includes('-')) {
+      args[key.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())] = value;
+    }
+  };
+  for (let i = 0; i < argv.length; i++) {
+    const token = argv[i];
+    if (!token.startsWith('--')) continue;
+    const eq = token.indexOf('=');
+    if (eq >= 0) {
+      setArg(token.slice(2, eq), token.slice(eq + 1));
+      continue;
+    }
+    const key = token.slice(2);
+    const next = argv[i + 1];
+    if (next && !next.startsWith('--')) {
+      setArg(key, next);
+      i++;
+    } else {
+      setArg(key, true);
+    }
+  }
+  return args;
+}
+
+async function runVocContentPlan(input = {}) {
+  return buildContentPlan(input);
+}
+
+async function main() {
+  const args = parseArgs(process.argv.slice(2));
+  const result = await runVocContentPlan(args);
+  if (args.assistantMessageOnly || args['assistant-message-only']) {
+    console.log(result.assistantMessage || JSON.stringify(result, null, 2));
+  } else {
+    console.log(JSON.stringify(result, null, 2));
+  }
+  if (!args.assistantMessageOnly && !args['assistant-message-only'] && (args.resultPrefix || args['result-prefix'])) {
+    const prefix = args.resultPrefix || args['result-prefix'];
+    console.log(`${prefix}=${JSON.stringify(result)}`);
+  }
+}
+
+if (require.main === module) {
+  main().catch(error => {
+    console.error(JSON.stringify({ status: 'error', message: error.message }, null, 2));
+    process.exit(1);
+  });
+}
+
+module.exports = {
+  runVocContentPlan
+};

+ 59 - 0
claude-code-voc-intelligence/mcp/src/tools/voc-issue-pool-run.js

@@ -0,0 +1,59 @@
+#!/usr/bin/env node
+const { buildIssuePool } = require('../features/voc-issue-pool/issue-pool');
+
+function parseArgs(argv) {
+  const args = {};
+  const setArg = (key, value) => {
+    args[key] = value;
+    if (key.includes('-')) {
+      args[key.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())] = value;
+    }
+  };
+  for (let i = 0; i < argv.length; i++) {
+    const token = argv[i];
+    if (!token.startsWith('--')) continue;
+    const eq = token.indexOf('=');
+    if (eq >= 0) {
+      setArg(token.slice(2, eq), token.slice(eq + 1));
+      continue;
+    }
+    const key = token.slice(2);
+    const next = argv[i + 1];
+    if (next && !next.startsWith('--')) {
+      setArg(key, next);
+      i++;
+    } else {
+      setArg(key, true);
+    }
+  }
+  return args;
+}
+
+async function runVocIssuePool(input = {}) {
+  return buildIssuePool(input);
+}
+
+async function main() {
+  const args = parseArgs(process.argv.slice(2));
+  const result = await runVocIssuePool(args);
+  if (args.assistantMessageOnly || args['assistant-message-only']) {
+    console.log(result.assistantMessage || JSON.stringify(result, null, 2));
+  } else {
+    console.log(JSON.stringify(result, null, 2));
+  }
+  if (!args.assistantMessageOnly && !args['assistant-message-only'] && (args.resultPrefix || args['result-prefix'])) {
+    const prefix = args.resultPrefix || args['result-prefix'];
+    console.log(`${prefix}=${JSON.stringify(result)}`);
+  }
+}
+
+if (require.main === module) {
+  main().catch(error => {
+    console.error(JSON.stringify({ status: 'error', message: error.message }, null, 2));
+    process.exit(1);
+  });
+}
+
+module.exports = {
+  runVocIssuePool
+};

+ 59 - 0
claude-code-voc-intelligence/mcp/src/tools/voc-problem-deep-dive-run.js

@@ -0,0 +1,59 @@
+#!/usr/bin/env node
+const { buildDeepDive } = require('../features/voc-problem-deep-dive/deep-dive');
+
+function parseArgs(argv) {
+  const args = {};
+  const setArg = (key, value) => {
+    args[key] = value;
+    if (key.includes('-')) {
+      args[key.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())] = value;
+    }
+  };
+  for (let i = 0; i < argv.length; i++) {
+    const token = argv[i];
+    if (!token.startsWith('--')) continue;
+    const eq = token.indexOf('=');
+    if (eq >= 0) {
+      setArg(token.slice(2, eq), token.slice(eq + 1));
+      continue;
+    }
+    const key = token.slice(2);
+    const next = argv[i + 1];
+    if (next && !next.startsWith('--')) {
+      setArg(key, next);
+      i++;
+    } else {
+      setArg(key, true);
+    }
+  }
+  return args;
+}
+
+async function runVocProblemDeepDive(input = {}) {
+  return buildDeepDive(input);
+}
+
+async function main() {
+  const args = parseArgs(process.argv.slice(2));
+  const result = await runVocProblemDeepDive(args);
+  if (args.assistantMessageOnly || args['assistant-message-only']) {
+    console.log(result.assistantMessage || JSON.stringify(result, null, 2));
+  } else {
+    console.log(JSON.stringify(result, null, 2));
+  }
+  if (!args.assistantMessageOnly && !args['assistant-message-only'] && (args.resultPrefix || args['result-prefix'])) {
+    const prefix = args.resultPrefix || args['result-prefix'];
+    console.log(`${prefix}=${JSON.stringify(result)}`);
+  }
+}
+
+if (require.main === module) {
+  main().catch(error => {
+    console.error(JSON.stringify({ status: 'error', message: error.message }, null, 2));
+    process.exit(1);
+  });
+}
+
+module.exports = {
+  runVocProblemDeepDive
+};

+ 59 - 0
claude-code-voc-intelligence/mcp/src/tools/voc-speaking-script-run.js

@@ -0,0 +1,59 @@
+#!/usr/bin/env node
+const { buildVocSpeakingScript } = require('../features/voc-speaking-script/speaking-script');
+
+function parseArgs(argv) {
+  const args = {};
+  const setArg = (key, value) => {
+    args[key] = value;
+    if (key.includes('-')) {
+      args[key.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())] = value;
+    }
+  };
+  for (let i = 0; i < argv.length; i++) {
+    const token = argv[i];
+    if (!token.startsWith('--')) continue;
+    const eq = token.indexOf('=');
+    if (eq >= 0) {
+      setArg(token.slice(2, eq), token.slice(eq + 1));
+      continue;
+    }
+    const key = token.slice(2);
+    const next = argv[i + 1];
+    if (next && !next.startsWith('--')) {
+      setArg(key, next);
+      i++;
+    } else {
+      setArg(key, true);
+    }
+  }
+  return args;
+}
+
+async function runVocSpeakingScript(input = {}) {
+  return buildVocSpeakingScript(input);
+}
+
+async function main() {
+  const args = parseArgs(process.argv.slice(2));
+  const result = await runVocSpeakingScript(args);
+  if (args.assistantMessageOnly || args['assistant-message-only']) {
+    console.log(result.assistantMessage || JSON.stringify(result, null, 2));
+  } else {
+    console.log(JSON.stringify(result, null, 2));
+  }
+  if (!args.assistantMessageOnly && !args['assistant-message-only'] && (args.resultPrefix || args['result-prefix'])) {
+    const prefix = args.resultPrefix || args['result-prefix'];
+    console.log(`${prefix}=${JSON.stringify(result)}`);
+  }
+}
+
+if (require.main === module) {
+  main().catch(error => {
+    console.error(JSON.stringify({ status: 'error', message: error.message }, null, 2));
+    process.exit(1);
+  });
+}
+
+module.exports = {
+  runVocSpeakingScript
+};

+ 193 - 45
claude-code-voc-intelligence/mcp/src/tools/xiaohongshu-trend-run.js

@@ -5,26 +5,39 @@ 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 { cacheEvidenceAssets } = require('../features/xiaohongshu-trend/assets');
 const { buildTrendReport, writeReport } = require('../features/xiaohongshu-trend/report');
 const { trendMemoryPath, readTrendMemory } = require('../features/xiaohongshu-trend/preference-memory');
+const { buildNoSampleFallbackMessage } = require('../core/evidence-quality');
+const {
+  buildVocRechargeInfo,
+  buildMissingTokenMessage,
+  buildRechargeRequiredMessage
+} = require('../core/payment-links');
 
 function parseArgs(argv) {
   const args = {};
+  const setArg = (key, value) => {
+    args[key] = value;
+    if (key.includes('-')) {
+      args[key.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())] = value;
+    }
+  };
   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);
+      setArg(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;
+      setArg(key, next);
       i++;
     } else {
-      args[key] = true;
+      setArg(key, true);
     }
   }
   return args;
@@ -36,6 +49,22 @@ function splitList(value) {
   return String(value).split(/[,,、\n\r]+/).map(item => item.trim()).filter(Boolean);
 }
 
+function defaultProfile() {
+  return {
+    project: '行业趋势情报官',
+    industry: '通用行业',
+    businessType: '品牌方/门店/服务商/内容账号',
+    targetAudience: ['目标用户'],
+    keywords: ['行业关键词', '用户痛点', '产品体验', '价格', '口碑'],
+    trendQuestions: [
+      '用户最近在关注什么卖点?',
+      '哪些顾虑影响选择或购买?',
+      '什么内容表达最容易带来转化?'
+    ],
+    mustTrackSignals: ['价格', '口碑', '效果', '安全', '服务', '体验']
+  };
+}
+
 function readJsonIfExists(filePath) {
   if (!filePath) return {};
   const absolute = path.resolve(filePath);
@@ -43,25 +72,44 @@ function readJsonIfExists(filePath) {
   return JSON.parse(fs.readFileSync(absolute, 'utf8'));
 }
 
+function findAuthError(errors = []) {
+  return errors.find(error => {
+    const message = String(error && error.message || '');
+    return Number(error && error.httpStatus) === 401 ||
+      /Invalid API token|无效的API令牌|invalid token/i.test(message);
+  });
+}
+
+function findGatewayError(errors = []) {
+  return errors.find(error => {
+    const status = Number(error && error.httpStatus);
+    const message = String(error && error.message || '');
+    return status === 402 ||
+      status === 403 ||
+      /社交平台接口请求错误|没有开通|余额不足|无权限|权限或余额|permission|quota|payment|insufficient|balance/i.test(message);
+  });
+}
+
 function loadProfile(args) {
   const fromFile = readJsonIfExists(args.profile);
+  const defaults = defaultProfile();
   return {
     ...fromFile,
-    project: args.project || fromFile.project || '小红书趋势情报官',
-    industry: args.industry || fromFile.industry || '家装全屋定制',
-    businessType: args.businessType || args['business-type'] || fromFile.businessType || '门店/设计服务/全屋定制服务商',
+    project: args.project || fromFile.project || defaults.project,
+    industry: args.industry || fromFile.industry || defaults.industry,
+    businessType: args.businessType || args['business-type'] || fromFile.businessType || defaults.businessType,
     targetAudience: splitList(args.targetAudience || args['target-audience']).length
       ? splitList(args.targetAudience || args['target-audience'])
-      : (fromFile.targetAudience || ['准备装修的女性客户']),
+      : (fromFile.targetAudience || defaults.targetAudience),
     keywords: splitList(args.keywords).length
       ? splitList(args.keywords)
-      : (fromFile.keywords || ['全屋定制', '奶油风装修', '小户型收纳']),
+      : (fromFile.keywords || defaults.keywords),
     trendQuestions: splitList(args.trendQuestions || args['trend-questions']).length
       ? splitList(args.trendQuestions || args['trend-questions'])
-      : (fromFile.trendQuestions || []),
+      : (fromFile.trendQuestions || defaults.trendQuestions),
     mustTrackSignals: splitList(args.mustTrackSignals || args['must-track-signals']).length
       ? splitList(args.mustTrackSignals || args['must-track-signals'])
-      : (fromFile.mustTrackSignals || [])
+      : (fromFile.mustTrackSignals || defaults.mustTrackSignals)
   };
 }
 
@@ -73,6 +121,7 @@ async function runXiaohongshuTrend(input = {}) {
   const memory = readTrendMemory(memoryPath);
   const warnings = [];
   const errors = [];
+  const assetFiles = [];
   let dataset;
 
   if (collectionMode === 'sample') {
@@ -81,27 +130,21 @@ async function runXiaohongshuTrend(input = {}) {
   } else if (collectionMode === 'live') {
     const token = readXiaohongshuToken(input);
     if (!token) {
+      const recharge = await buildVocRechargeInfo();
       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 },
+        assistantMessage: buildMissingTokenMessage(recharge.paymentUrl, recharge),
+        summary: { requestedCollectionMode: collectionMode },
+        data: {
+          requestedCollectionMode: collectionMode,
+          recharge
+        },
         files: [],
-        nextActions: ['配置 token 后重新运行 live 模式', '先用 sample 模式完成教学演示'],
+        nextActions: [
+          `打开充值/开通链接:${recharge.paymentUrl}`,
+          '充值完成后配置 session token,再重新运行 live 模式',
+          '如果只是录制教学,可以先用 sample 模式演示完整流程'
+        ],
         warnings: [],
         errors: []
       };
@@ -115,7 +158,8 @@ async function runXiaohongshuTrend(input = {}) {
         notesPerKeyword: input.notesPerKeyword || input['notes-per-keyword'],
         maxCommentPages: input.maxCommentPages || input['max-comment-pages'],
         sort: input.sort,
-        noteType: input.noteType || input['note-type']
+        noteType: input.noteType || input['note-type'],
+        baseUrl: input.baseUrl || input['base-url'] || process.env.XIAOHONGSHU_API_BASE_URL
       }
     });
     dataset = {
@@ -125,19 +169,100 @@ async function runXiaohongshuTrend(input = {}) {
     warnings.push(...liveDataset.warnings);
     errors.push(...liveDataset.errors);
 
+    const collectionIssue = findGatewayError([...errors, ...warnings]);
+    if (collectionIssue) {
+      const recharge = await buildVocRechargeInfo({ token });
+      return {
+        status: 'needs_recharge',
+        assistantMessage: buildRechargeRequiredMessage(recharge.paymentUrl, recharge),
+        summary: {
+          noteCount: dataset.notes.length,
+          commentCount: dataset.comments.length,
+          failedStage: collectionIssue.stage || '',
+          failedKeyword: collectionIssue.keyword || ''
+        },
+        data: {
+          profile,
+          recharge
+        },
+        files: [],
+        nextActions: [
+          `打开充值链接:${recharge.paymentUrl}`,
+          '支付完成后重新运行本次小红书 live 采集',
+          '如果需要先给客户演示,可以切换 sample 模式继续'
+        ],
+        warnings: [],
+        errors: []
+      };
+    }
+
     if (!dataset.notes.length) {
+      const authError = findAuthError(errors);
+      if (authError) {
+        const recharge = await buildVocRechargeInfo({ token });
+        return {
+          status: 'needs_valid_token',
+          assistantMessage: [
+            '当前 VOC-AI 数据服务 token 没有通过校验,暂时不能进行真实小红书采集。',
+            '',
+            '请确认已经完成 VOC-AI 数据服务开通/充值,并重新配置服务方提供的 session token。',
+            '',
+            `充值/开通链接:${recharge.paymentUrl}`
+          ].join('\n'),
+          summary: {
+            noteCount: 0,
+            commentCount: dataset.comments.length,
+            errorCount: errors.length,
+            failedStage: authError.stage || 'search_notes',
+            failedKeyword: authError.keyword || ''
+          },
+          data: { profile, recharge },
+          files: [],
+          nextActions: [
+            `打开充值/开通链接:${recharge.paymentUrl}`,
+            '重新配置有效 session token 后重试 live 模式',
+            '需要录教学视频时,可先用 sample 模式演示完整多轮校准流程'
+          ],
+          warnings: [],
+          errors: []
+        };
+      }
+
+      const gatewayError = findGatewayError(errors);
+      if (gatewayError) {
+        const recharge = await buildVocRechargeInfo({ token });
+        return {
+          status: 'needs_recharge',
+          assistantMessage: buildRechargeRequiredMessage(recharge.paymentUrl, recharge),
+          summary: {
+            noteCount: 0,
+            commentCount: dataset.comments.length,
+            failedStage: gatewayError.stage || 'search_notes',
+            failedKeyword: gatewayError.keyword || ''
+          },
+          data: { profile, recharge },
+          files: [],
+          nextActions: [
+            `打开充值链接:${recharge.paymentUrl}`,
+            '支付完成后重新运行本次小红书 live 采集',
+            '如果需要先给客户演示,可以切换 sample 模式继续'
+          ],
+          warnings: [],
+          errors: []
+        };
+      }
+
       return {
         status: 'no_live_samples',
-        assistantMessage: [
-          '本次 live 模式没有采到可用于分析的小红书样本。',
-          '',
-          '建议先缩小关键词或改用更具体的行业词,例如:',
-          '- 全屋定制',
-          '- 奶油风装修',
-          '- 衣柜设计避坑',
-          '',
-          '也可以先用 sample 模式完成流程演示。'
-        ].join('\n'),
+        assistantMessage: buildNoSampleFallbackMessage({
+          platformLabel: '小红书',
+          profile,
+          examples: [
+            '把品牌/服务词换成用户场景词,例如“第一次装修怕踩坑”“父母礼物”“家庭故事”',
+            '加入城市/品类/人群,例如“南昌 拌粉 第一次来”“小户型 衣柜 避坑”',
+            '用相关大词做降级验证,例如从“老人自传”扩展到“父母礼物、家庭故事、人生故事”'
+          ]
+        }),
         summary: {
           noteCount: 0,
           commentCount: dataset.comments.length,
@@ -145,7 +270,11 @@ async function runXiaohongshuTrend(input = {}) {
         },
         data: { profile },
         files: [],
-        nextActions: ['换更具体关键词后重试 live 模式', '先用 sample 模式演示'],
+        nextActions: [
+          '换一组更贴近用户说法的关键词后重试 live 模式',
+          '扩大到相关大词做第一轮真实样本尝试',
+          '如果用于教学或远程演示,先切换 sample 模式,并明确标注非真实采集'
+        ],
         warnings,
         errors
       };
@@ -154,6 +283,19 @@ async function runXiaohongshuTrend(input = {}) {
     return errorResult(`Unsupported collectionMode: ${collectionMode}`);
   }
 
+  const cacheAssetsEnabled = input.cacheAssets !== false &&
+    input['cache-assets'] !== false &&
+    String(input.cacheAssets || input['cache-assets'] || '').toLowerCase() !== 'false';
+  if (cacheAssetsEnabled) {
+    const assetResult = await cacheEvidenceAssets({
+      notes: dataset.notes,
+      outputDir,
+      limit: input.assetLimit ?? input['asset-limit'] ?? 10
+    });
+    warnings.push(...assetResult.warnings);
+    assetFiles.push(...assetResult.files);
+  }
+
   const report = buildTrendReport({ profile, dataset, collectionMode, memory });
   const files = writeReport(outputDir, report);
   const validationQuestions = report.data.validationQuestions || [];
@@ -161,10 +303,12 @@ async function runXiaohongshuTrend(input = {}) {
     ? [
       `先回答校准问题 1:${validationQuestions[0]}`,
       validationQuestions[1] ? `再确认问题 2:${validationQuestions[1]}` : '',
+      '把本轮证据整理成 VOC 问题池,判断先改哪个问题。',
       '也可以直接补充一条你最近听到的真实客户反馈,我会据此收敛下一轮判断。'
     ].filter(Boolean)
     : [
       '先补充一条你最近听到的真实客户反馈',
+      '把本轮证据整理成 VOC 问题池,判断先改哪个问题。',
       '再基于客户反馈收敛下一轮趋势判断'
     ];
 
@@ -175,7 +319,7 @@ async function runXiaohongshuTrend(input = {}) {
       ...report.data,
       memoryPath
     },
-    files,
+    files: [...files, ...assetFiles],
     nextActions,
     warnings,
     errors
@@ -186,16 +330,20 @@ 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']) {
+    if (args.assistantMessageOnly || args['assistant-message-only']) {
+      console.log(result.assistantMessage || JSON.stringify(result, null, 2));
+    } else {
+      console.log(JSON.stringify(result, null, 2));
+    }
+    if (!args.assistantMessageOnly && !args['assistant-message-only'] && (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);
+    process.exitCode = result.status === 'ok' ? 0 : 1;
   } catch (error) {
     const result = errorResult(error.message || String(error));
     console.log(JSON.stringify(result, null, 2));
-    process.exit(1);
+    process.exitCode = 1;
   }
 }
 

+ 33 - 0
claude-code-voc-intelligence/memory-templates/douyin-trend-profile.json

@@ -0,0 +1,33 @@
+{
+  "project": "抖音趋势情报官",
+  "industry": "通用行业",
+  "businessType": "品牌方/门店/服务商/内容账号",
+  "targetAudience": [
+    "目标用户"
+  ],
+  "platforms": [
+    "douyin"
+  ],
+  "keywords": [
+    "行业关键词",
+    "用户痛点",
+    "产品体验",
+    "价格",
+    "口碑"
+  ],
+  "trendQuestions": [
+    "用户最近在关注什么表达和卖点?",
+    "哪些顾虑影响点击、停留或评论?",
+    "什么内容开头最容易带来转化?"
+  ],
+  "mustTrackSignals": [
+    "价格",
+    "口碑",
+    "效果",
+    "安全",
+    "服务",
+    "体验"
+  ],
+  "blockedDirections": [],
+  "updatedAt": ""
+}

+ 18 - 22
claude-code-voc-intelligence/memory-templates/xiaohongshu-trend-profile.json

@@ -1,37 +1,33 @@
 {
-  "project": "小红书趋势情报官",
-  "industry": "家装全屋定制",
-  "businessType": "门店/设计服务/全屋定制服务商",
+  "project": "行业趋势情报官",
+  "industry": "通用行业",
+  "businessType": "品牌方/门店/服务商/内容账号",
   "targetAudience": [
-    "准备装修的女性客户",
-    "重视颜值和收纳的家庭决策者",
-    "关注预算、环保、售后和翻车风险的客户"
+    "目标用户"
   ],
   "platforms": [
     "xiaohongshu"
   ],
   "keywords": [
-    "全屋定制",
-    "奶油风装修",
-    "小户型收纳",
-    "衣柜设计避坑",
-    "厨房橱柜设计",
-    "装修翻车"
+    "行业关键词",
+    "用户痛点",
+    "产品体验",
+    "价格",
+    "口碑"
   ],
   "trendQuestions": [
-    "下季度女性客户更关注哪些装修风格和设计元素?",
-    "用户在评论里反复担心哪些翻车点?",
-    "哪些内容角度可以转成门店销售话术?"
+    "用户最近在关注什么卖点?",
+    "哪些顾虑影响选择或购买?",
+    "什么内容表达最容易带来转化?"
   ],
   "mustTrackSignals": [
-    "装修风格",
-    "设计元素",
-    "收纳需求",
-    "环保顾虑",
-    "预算焦虑",
-    "翻车风险"
+    "格",
+    "口碑",
+    "效果",
+    "安全",
+    "服务",
+    "体验"
   ],
   "blockedDirections": [],
   "updatedAt": ""
 }
-

+ 2 - 2
claude-code-voc-intelligence/package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "@gangvy/claude-code-voc-intelligence",
-  "version": "0.1.2",
+  "version": "0.3.10",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "@gangvy/claude-code-voc-intelligence",
-      "version": "0.1.2",
+      "version": "0.3.10",
       "dependencies": {
         "@modelcontextprotocol/sdk": "^1.29.0",
         "zod": "^4.4.3"

+ 27 - 4
claude-code-voc-intelligence/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@gangvy/claude-code-voc-intelligence",
-  "version": "0.1.2",
-  "description": "Claude Code native VOC intelligence skills and tools.",
+  "version": "0.3.10",
+  "description": "Claude Code VOC intelligence skills for Xiaohongshu and Douyin trend reports, live social VOC collection, single-issue deep dives, boss/operator action plans, and multi-turn memory.",
   "type": "commonjs",
   "bin": {
     "claude-voc": "bin/claude-voc.js"
@@ -10,10 +10,21 @@
     "install:check": "node install.js --check",
     "install:smoke": "node install.js --smoke",
     "smoke:package": "node scripts/smoke-package.js",
+    "smoke:cross-industry": "node scripts/smoke-cross-industry.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: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",
+    "dy:sample": "node mcp/src/tools/douyin-trend-run.js --collection-mode sample --profile memory-templates/douyin-trend-profile.json --output ../outputs/claude-code-douyin-sample --result-prefix DOUYIN_TREND_RESULT",
+    "dy:preference-smoke": "node mcp/src/tools/douyin-preference-update.js --message \"保留口播开头和评论区问题,不要只做纯素材剪辑,下一个版本更偏门店转化话术。\" --memory ../outputs/claude-code-douyin-memory-smoke/douyin-trend-memory.json --result-prefix DOUYIN_PREF_RESULT",
+    "dy:sample-with-memory": "node mcp/src/tools/douyin-trend-run.js --collection-mode sample --profile memory-templates/douyin-trend-profile.json --memory ../outputs/claude-code-douyin-memory-smoke/douyin-trend-memory.json --output ../outputs/claude-code-douyin-memory-smoke/report --result-prefix DOUYIN_TREND_RESULT",
+    "dy:live": "node mcp/src/tools/douyin-trend-run.js --collection-mode live --profile memory-templates/douyin-trend-profile.json --output ../outputs/claude-code-douyin-live --keyword-limit 1 --videos-per-keyword 1 --max-comment-pages 1 --result-prefix DOUYIN_TREND_RESULT",
+    "voc:issue-pool": "node mcp/src/tools/voc-issue-pool-run.js --project 样板门店A --industry 本地生活餐饮 --scenario 门店经营 --audience 到店顾客 --evidence \"排队久,但是不知道值不值得等。;人均有点高,第一次来不知道怎么点不踩雷。;服务忙起来没人理。\" --assistant-message-only",
+    "voc:deep-dive": "node mcp/src/tools/voc-problem-deep-dive-run.js --issue 排队 --industry 本地生活餐饮 --scenario 门店经营 --audience 到店顾客 --assistant-message-only",
+    "voc:content-plan": "node mcp/src/tools/voc-content-plan-run.js --brand 样板门店A --industry 本地生活餐饮 --issues \"第一次来怕踩雷;觉得人均贵;怕排队久\" --assistant-message-only",
+    "voc:speaking-script": "node mcp/src/tools/voc-speaking-script-run.js --brand 样板门店A --industry 本地生活餐饮 --topic \"第一次来怎么点不踩雷\" --user-issue \"第一次来怕点错\" --evidence \"第一次来不知道怎么点,怕踩雷\" --assistant-message-only",
+    "voc:competitor-map": "node mcp/src/tools/voc-competitor-map-run.js --brand 样板门店A --city 南昌 --category 餐饮 --assistant-message-only",
+    "voc:business-workflow": "node mcp/src/tools/voc-business-workflow-run.js --brand 样板门店A --industry 本地生活餐饮 --platform douyin --collection-mode sample --keywords \"南昌菜,第一次来怎么点,排队\" --assistant-message-only",
     "mcp:start": "node mcp/src/server.js",
     "mcp:smoke": "node scripts/smoke-mcp.js"
   },
@@ -34,9 +45,21 @@
   "publishConfig": {
     "access": "public"
   },
+  "keywords": [
+    "claude-code",
+    "voc",
+    "voice-of-customer",
+    "xiaohongshu",
+    "douyin",
+    "trend-intelligence",
+    "customer-insights",
+    "mcp",
+    "openclaw",
+    "social-listening"
+  ],
+  "homepage": "https://www.npmjs.com/package/@gangvy/claude-code-voc-intelligence",
   "dependencies": {
     "@modelcontextprotocol/sdk": "^1.29.0",
     "zod": "^4.4.3"
   }
 }
-

+ 126 - 0
claude-code-voc-intelligence/scripts/smoke-cross-industry.js

@@ -0,0 +1,126 @@
+#!/usr/bin/env node
+const fs = require('fs');
+const path = require('path');
+const { runXiaohongshuTrend } = require('../mcp/src/tools/xiaohongshu-trend-run');
+
+const LEAK_TERMS = [
+  '家装',
+  '全屋定制',
+  '奶油风',
+  '开放格',
+  '厨房橱柜',
+  '衣柜',
+  '小户型'
+];
+
+const CASES = [
+  {
+    name: 'pet-food',
+    profile: {
+      project: '行业趋势情报官',
+      industry: '宠物食品',
+      businessType: '品牌方/电商/门店',
+      targetAudience: ['养宠用户', '第一次养猫养狗的新手'],
+      keywords: ['猫粮', '狗粮', '配料表', '试吃', '复购'],
+      trendQuestions: ['用户最近在意哪些成分和适口性?', '什么话术最容易带来复购?', '哪些风险点会影响下单?'],
+      mustTrackSignals: ['成分', '适口性', '复购', '价格', '口碑']
+    }
+  },
+  {
+    name: 'beauty-skincare',
+    profile: {
+      project: '行业趋势情报官',
+      industry: '美妆护肤',
+      businessType: '品牌方/测评账号/门店',
+      targetAudience: ['护肤消费者', '敏感肌用户'],
+      keywords: ['面霜', '防晒', '粉底液', '成分党', '测评'],
+      trendQuestions: ['哪些护肤成分被反复讨论?', '用户最担心哪些翻车点?', '什么测评角度更容易转化?'],
+      mustTrackSignals: ['成分', '肤感', '持妆', '价格', '复购']
+    }
+  },
+  {
+    name: 'baby-maternal',
+    profile: {
+      project: '行业趋势情报官',
+      industry: '母婴用品',
+      businessType: '品牌方/门店/内容账号',
+      targetAudience: ['宝妈', '新生儿家庭'],
+      keywords: ['纸尿裤', '辅食', '奶瓶', '婴儿湿巾', '新手妈妈'],
+      trendQuestions: ['家长更在意什么安全和使用细节?', '哪些场景会触发购买?', '什么内容最能降低顾虑?'],
+      mustTrackSignals: ['安全', '舒适', '分龄', '口碑', '售后']
+    }
+  },
+  {
+    name: 'education-training',
+    profile: {
+      project: '行业趋势情报官',
+      industry: '教育培训',
+      businessType: '机构/老师/内容账号',
+      targetAudience: ['学生家长', '学习者'],
+      keywords: ['课程', '试听', '提分', '学习方法', '规划'],
+      trendQuestions: ['家长更担心哪些效果问题?', '什么样的课程表达更容易被信任?', '用户最在意的决策阻力是什么?'],
+      mustTrackSignals: ['效果', '师资', '时间', '价格', '退费']
+    }
+  },
+  {
+    name: 'local-dining',
+    profile: {
+      project: '行业趋势情报官',
+      industry: '本地生活餐饮',
+      businessType: '门店/团购/探店账号',
+      targetAudience: ['到店消费者', '团购用户'],
+      keywords: ['探店', '套餐', '人均', '排队', '氛围'],
+      trendQuestions: ['用户更在意口味还是性价比?', '什么门店体验会影响复购?', '哪些负面点最容易被评论放大?'],
+      mustTrackSignals: ['口味', '服务', '排队', '环境', '复购']
+    }
+  }
+];
+
+async function smokeCrossIndustry({ root = path.resolve(__dirname, '..') } = {}) {
+  const outputRoot = path.join(root, 'outputs', 'claude-code-cross-industry-smoke');
+  fs.mkdirSync(outputRoot, { recursive: true });
+  const results = [];
+
+  for (const item of CASES) {
+    const result = await runXiaohongshuTrend({
+      collectionMode: 'sample',
+      output: path.join(outputRoot, item.name),
+      cacheAssets: false,
+      ...item.profile
+    });
+    if (result.status !== 'ok') {
+      throw new Error(`${item.name} sample failed: ${result.status}`);
+    }
+    const body = String(result.assistantMessage || '');
+    const leakHits = LEAK_TERMS.filter(term => body.includes(term));
+    if (leakHits.length) {
+      throw new Error(`${item.name} leaked house-specific terms: ${leakHits.join(', ')}`);
+    }
+    results.push({
+      industry: item.profile.industry,
+      preset: result.summary && result.summary.industryPreset,
+      preview: body.split('\n').slice(0, 5).join('\n')
+    });
+  }
+
+  return {
+    status: 'ok',
+    cases: results
+  };
+}
+
+async function main() {
+  const result = await smokeCrossIndustry();
+  console.log(JSON.stringify(result, null, 2));
+}
+
+if (require.main === module) {
+  main().catch(error => {
+    console.error(JSON.stringify({ status: 'error', message: error.message }, null, 2));
+    process.exit(1);
+  });
+}
+
+module.exports = {
+  smokeCrossIndustry
+};

+ 194 - 3
claude-code-voc-intelligence/scripts/smoke-mcp.js

@@ -2,12 +2,13 @@
 const path = require('path');
 const { Client } = require('@modelcontextprotocol/sdk/client/index.js');
 const { StdioClientTransport } = require('@modelcontextprotocol/sdk/client/stdio.js');
+const { version } = require('../package.json');
 
 async function main() {
   const cwd = path.resolve(__dirname, '..');
   const client = new Client({
     name: 'voc-intelligence-smoke',
-    version: '0.1.2'
+    version
   });
 
   const transport = new StdioClientTransport({
@@ -28,17 +29,50 @@ async function main() {
   try {
     const tools = await client.listTools();
     const toolNames = tools.tools.map(tool => tool.name);
+    const trendTool = tools.tools.find(tool => tool.name === 'voc_xiaohongshu_trend_run');
+    const trendSchema = JSON.stringify(trendTool?.inputSchema || {});
+    if (!trendTool || !trendSchema.includes('xiaohongshuToken') || !trendSchema.includes('vocToken')) {
+      throw new Error('Expected voc_xiaohongshu_trend_run to accept simple request token inputs xiaohongshuToken and vocToken');
+    }
     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(', ')}`);
     }
+    if (!toolNames.includes('voc_problem_deep_dive_run')) {
+      throw new Error(`Expected tool voc_problem_deep_dive_run, got: ${toolNames.join(', ')}`);
+    }
+    if (!toolNames.includes('voc_issue_pool_run')) {
+      throw new Error(`Expected tool voc_issue_pool_run, got: ${toolNames.join(', ')}`);
+    }
+    if (!toolNames.includes('voc_content_plan_run')) {
+      throw new Error(`Expected tool voc_content_plan_run, got: ${toolNames.join(', ')}`);
+    }
+    if (!toolNames.includes('voc_speaking_script_run')) {
+      throw new Error(`Expected tool voc_speaking_script_run, got: ${toolNames.join(', ')}`);
+    }
+    if (!toolNames.includes('voc_competitor_map_run')) {
+      throw new Error(`Expected tool voc_competitor_map_run, got: ${toolNames.join(', ')}`);
+    }
+    if (!toolNames.includes('voc_business_workflow_run')) {
+      throw new Error(`Expected tool voc_business_workflow_run, got: ${toolNames.join(', ')}`);
+    }
+    const issuePoolTool = tools.tools.find(tool => tool.name === 'voc_issue_pool_run');
+    const issuePoolSchema = JSON.stringify(issuePoolTool?.inputSchema || {});
+    if (!issuePoolSchema.includes('evidence') || !issuePoolSchema.includes('resolvedIssues')) {
+      throw new Error('Expected voc_issue_pool_run to expose evidence and status inputs');
+    }
+    const deepDiveTool = tools.tools.find(tool => tool.name === 'voc_problem_deep_dive_run');
+    const deepDiveSchema = JSON.stringify(deepDiveTool?.inputSchema || {});
+    if (!deepDiveSchema.includes('project') || !deepDiveSchema.includes('feedback') || !deepDiveSchema.includes('blockedActions')) {
+      throw new Error('Expected voc_problem_deep_dive_run to expose scoped memory and feedback inputs');
+    }
 
     const preferenceResult = await client.callTool({
       name: 'voc_xiaohongshu_preference_update',
       arguments: {
-        message: '保留奶油风和全屋定制翻车方向,不要纯风格美图,下一版更偏门店转化话术',
+        message: '保留奶油风和全屋定制翻车方向,不要纯风格美图,下一更偏门店转化话术',
         memory: '../outputs/claude-code-xhs-mcp-smoke/xiaohongshu-trend-memory.json'
       }
     });
@@ -48,6 +82,22 @@ async function main() {
       throw new Error(`Expected ok preference result, got: ${JSON.stringify(preferenceStructured)}`);
     }
 
+    const sentinelToken = 'SMOKE_REQUEST_TOKEN_VALUE';
+    const tokenCheckResult = await client.callTool({
+      name: 'voc_xiaohongshu_token_check',
+      arguments: {
+        vocToken: sentinelToken
+      }
+    });
+    const tokenStructured = tokenCheckResult.structuredContent || {};
+    const tokenText = JSON.stringify(tokenStructured);
+    if (tokenStructured.status !== 'ok' || tokenStructured.configured !== true) {
+      throw new Error(`Expected request-scoped token to be accepted, got: ${tokenText}`);
+    }
+    if (tokenText.includes(sentinelToken)) {
+      throw new Error('Request-scoped token leaked in MCP token check result');
+    }
+
     const result = await client.callTool({
       name: 'voc_xiaohongshu_trend_run',
       arguments: {
@@ -63,10 +113,151 @@ async function main() {
       throw new Error(`Expected ok result, got: ${JSON.stringify(structured)}`);
     }
 
+    const assistantMessage = String(structured.assistantMessage || '');
+    const evidenceFields = ['## 证据样本', '原帖', '原文摘录', '高赞评论'];
+    const missingEvidenceFields = evidenceFields.filter(field => !assistantMessage.includes(field));
+    if (missingEvidenceFields.length) {
+      throw new Error(`Expected evidence card fields in sample report, missing: ${missingEvidenceFields.join(', ')}`);
+    }
+    if (/xiaohongshu\.com\/explore\//.test(assistantMessage)) {
+      throw new Error('Sample report should not contain /explore/ Xiaohongshu links');
+    }
+    const assetFiles = (structured.files || []).filter(file => /[\\/]assets[\\/]/.test(file));
+    if (!assetFiles.length) {
+      throw new Error('Expected cached asset files in sample report result');
+    }
+
+    const issuePoolResult = await client.callTool({
+      name: 'voc_issue_pool_run',
+      arguments: {
+        project: 'MCP样板门店',
+        industry: '本地生活餐饮',
+        scenario: '门店经营',
+        audience: '到店顾客',
+        evidence: [
+          '排队久,但是不知道值不值得等。',
+          '人均有点高,第一次来不知道怎么点不踩雷。',
+          '服务忙起来没人理。'
+        ]
+      }
+    });
+    const issuePoolStructured = issuePoolResult.structuredContent || {};
+    if (issuePoolStructured.status !== 'ok' ||
+      !String(issuePoolStructured.assistantMessage || '').includes('## 问题优先级') ||
+      !issuePoolStructured.data?.issues?.length) {
+      throw new Error(`Expected issue pool result, got: ${JSON.stringify(issuePoolStructured)}`);
+    }
+
+    const deepDiveResult = await client.callTool({
+      name: 'voc_problem_deep_dive_run',
+      arguments: {
+        issue: '排队',
+        project: 'MCP样板门店',
+        industry: '本地生活餐饮',
+        scenario: '门店经营',
+        audience: '到店顾客',
+        evidence: ['排队久,但是不知道值不值得等。'],
+        feedback: '老板不想做等位饮品,更想先改评论区回复和预点单'
+      }
+    });
+    const deepDiveStructured = deepDiveResult.structuredContent || {};
+    const deepDiveMessage = String(deepDiveStructured.assistantMessage || '');
+    if (deepDiveStructured.status !== 'ok' ||
+      !deepDiveMessage.includes('## 可执行解决动作') ||
+      !deepDiveMessage.includes('## 优先级判断') ||
+      !deepDiveStructured.summary?.memoryPath) {
+      throw new Error(`Expected boss-oriented deep dive result, got: ${JSON.stringify(deepDiveStructured)}`);
+    }
+    if (!deepDiveStructured.data?.memory?.blockedActions?.includes('等位饮品') ||
+      !deepDiveStructured.data?.memory?.preferredActions?.includes('预点单')) {
+      throw new Error('Expected deep dive MCP call to parse feedback into scoped memory');
+    }
+
+    const contentPlanResult = await client.callTool({
+      name: 'voc_content_plan_run',
+      arguments: {
+        brand: 'MCP样板门店',
+        industry: '本地生活餐饮',
+        issues: ['第一次来怕踩雷', '觉得人均贵', '怕排队久']
+      }
+    });
+    const contentPlanStructured = contentPlanResult.structuredContent || {};
+    const contentPlanMessage = String(contentPlanStructured.assistantMessage || '');
+    if (contentPlanStructured.status !== 'ok' ||
+      !contentPlanMessage.includes('7 天 VOC 内容选题') ||
+      !contentPlanMessage.includes('前 3 秒钩子') ||
+      !contentPlanStructured.data?.plan?.length) {
+      throw new Error(`Expected VOC content plan result, got: ${JSON.stringify(contentPlanStructured)}`);
+    }
+
+    const speakingScriptResult = await client.callTool({
+      name: 'voc_speaking_script_run',
+      arguments: {
+        brand: 'MCP样板门店',
+        industry: '本地生活餐饮',
+        topic: '第一次来怎么点不踩雷',
+        userIssue: '第一次来怕点错',
+        evidence: ['第一次来不知道怎么点,怕踩雷'],
+        feedback: '开头更狠一点,老板视角,少讲概念,多给具体场景',
+        memory: '../outputs/claude-code-xhs-mcp-smoke/voc-speaking-script-memory.json',
+        finalize: true
+      }
+    });
+    const speakingScriptStructured = speakingScriptResult.structuredContent || {};
+    const speakingScriptMessage = String(speakingScriptStructured.assistantMessage || '');
+    if (speakingScriptStructured.status !== 'ok' ||
+      !speakingScriptMessage.includes('VOC 口播脚本共创') ||
+      !speakingScriptMessage.includes('60 秒口播稿') ||
+      !speakingScriptMessage.includes('30 秒压缩版') ||
+      !speakingScriptStructured.data?.memory?.finalizedScripts?.length ||
+      !speakingScriptStructured.data?.memory?.preferredStyles?.includes('老板视角')) {
+      throw new Error(`Expected VOC speaking script result, got: ${JSON.stringify(speakingScriptStructured)}`);
+    }
+
+    const competitorMapResult = await client.callTool({
+      name: 'voc_competitor_map_run',
+      arguments: {
+        brand: 'MCP样板门店',
+        city: '南昌',
+        category: '餐饮'
+      }
+    });
+    const competitorMapStructured = competitorMapResult.structuredContent || {};
+    const competitorMapMessage = String(competitorMapStructured.assistantMessage || '');
+    if (competitorMapStructured.status !== 'ok' ||
+      !competitorMapMessage.includes('竞品图谱') ||
+      !competitorMapMessage.includes('错位竞争') ||
+      !competitorMapStructured.data?.competitors?.length) {
+      throw new Error(`Expected VOC competitor map result, got: ${JSON.stringify(competitorMapStructured)}`);
+    }
+
+    const businessWorkflowResult = await client.callTool({
+      name: 'voc_business_workflow_run',
+      arguments: {
+        brand: 'MCP样板门店',
+        industry: '本地生活餐饮',
+        platform: 'douyin',
+        collectionMode: 'sample',
+        keywords: ['南昌菜', '第一次来怎么点', '排队']
+      }
+    });
+    const businessWorkflowStructured = businessWorkflowResult.structuredContent || {};
+    const businessWorkflowMessage = String(businessWorkflowStructured.assistantMessage || '');
+    if (businessWorkflowStructured.status !== 'ok' ||
+      !businessWorkflowMessage.includes('VOC经营闭环') ||
+      businessWorkflowStructured.summary?.workflowStage !== 'business_workflow_first_round') {
+      throw new Error(`Expected VOC business workflow result, got: ${JSON.stringify(businessWorkflowStructured)}`);
+    }
+
     console.log(JSON.stringify({
       status: 'ok',
       tools: toolNames,
-      assistantMessagePreview: String(structured.assistantMessage || '').slice(0, 120),
+      assistantMessagePreview: assistantMessage.slice(0, 120),
+      deepDivePreview: deepDiveMessage.slice(0, 120),
+      contentPlanPreview: contentPlanMessage.slice(0, 120),
+      speakingScriptPreview: speakingScriptMessage.slice(0, 120),
+      competitorMapPreview: competitorMapMessage.slice(0, 120),
+      businessWorkflowPreview: businessWorkflowMessage.slice(0, 120),
       preferenceMemoryApplied: Boolean(structured.summary && structured.summary.preferenceMemoryApplied),
       files: structured.files || []
     }, null, 2));

+ 651 - 4
claude-code-voc-intelligence/scripts/smoke-package.js

@@ -1,18 +1,36 @@
 #!/usr/bin/env node
 const path = require('path');
+const os = require('os');
+const fs = require('fs');
 const { runXiaohongshuTrend } = require('../mcp/src/tools/xiaohongshu-trend-run');
 const { updateXiaohongshuPreference } = require('../mcp/src/tools/xiaohongshu-preference-update');
+const { normalizeNote } = require('../mcp/src/features/xiaohongshu-trend/live-collector');
+const { XiaohongshuApi } = require('../mcp/src/providers/xiaohongshu-api');
+const { runDouyinTrend } = require('../mcp/src/tools/douyin-trend-run');
+const { updateDouyinPreference } = require('../mcp/src/tools/douyin-preference-update');
+const { runVocIssuePool } = require('../mcp/src/tools/voc-issue-pool-run');
+const { runVocProblemDeepDive } = require('../mcp/src/tools/voc-problem-deep-dive-run');
+const { runVocContentPlan } = require('../mcp/src/tools/voc-content-plan-run');
+const { runVocSpeakingScript } = require('../mcp/src/tools/voc-speaking-script-run');
+const { runVocCompetitorMap } = require('../mcp/src/tools/voc-competitor-map-run');
+const { runBusinessWorkflow } = require('../mcp/src/tools/voc-business-workflow-run');
+const { normalizeVideo } = require('../mcp/src/features/douyin-trend/live-collector');
+const { DouyinApi } = require('../mcp/src/providers/douyin-api');
+const { readVocToken } = require('../mcp/src/core/credentials');
+const { smokeCrossIndustry } = require('./smoke-cross-industry');
 const { Client } = require('@modelcontextprotocol/sdk/client/index.js');
 const { StdioClientTransport } = require('@modelcontextprotocol/sdk/client/stdio.js');
+const { version } = require('../package.json');
 
 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');
+const DOUYIN_MEMORY_PATH = path.join(OUTPUT_ROOT, 'douyin-trend-memory.json');
 
 async function smokeMcpTools() {
   const client = new Client({
     name: 'voc-intelligence-package-smoke',
-    version: '0.1.2'
+    version
   });
   const transport = new StdioClientTransport({
     command: process.execPath,
@@ -23,12 +41,386 @@ async function smokeMcpTools() {
   await client.connect(transport);
   try {
     const tools = await client.listTools();
-    return tools.tools.map(tool => tool.name);
+    const toolNames = tools.tools.map(tool => tool.name);
+
+    const xhsTrendTool = tools.tools.find(tool => tool.name === 'voc_xiaohongshu_trend_run');
+    const xhsTrendSchema = JSON.stringify(xhsTrendTool?.inputSchema || {});
+    if (!xhsTrendTool || !xhsTrendSchema.includes('xiaohongshuToken') || !xhsTrendSchema.includes('vocToken')) {
+      throw new Error('Expected voc_xiaohongshu_trend_run to accept simple request token inputs xiaohongshuToken and vocToken');
+    }
+
+    const douyinTrendTool = tools.tools.find(tool => tool.name === 'voc_douyin_trend_run');
+    const douyinTrendSchema = JSON.stringify(douyinTrendTool?.inputSchema || {});
+    if (!douyinTrendTool || !douyinTrendSchema.includes('douyinToken') || !douyinTrendSchema.includes('vocToken')) {
+      throw new Error('Expected voc_douyin_trend_run to accept simple request token inputs douyinToken and vocToken');
+    }
+
+    const deepDiveTool = tools.tools.find(tool => tool.name === 'voc_problem_deep_dive_run');
+    const deepDiveSchema = JSON.stringify(deepDiveTool?.inputSchema || {});
+    if (!deepDiveTool || !deepDiveSchema.includes('issue') || !deepDiveSchema.includes('evidence')) {
+      throw new Error('Expected voc_problem_deep_dive_run to accept issue and evidence inputs');
+    }
+
+    const issuePoolTool = tools.tools.find(tool => tool.name === 'voc_issue_pool_run');
+    const issuePoolSchema = JSON.stringify(issuePoolTool?.inputSchema || {});
+    if (!issuePoolTool || !issuePoolSchema.includes('evidence') || !issuePoolSchema.includes('resolvedIssues')) {
+      throw new Error('Expected voc_issue_pool_run to accept evidence and status inputs');
+    }
+
+    return toolNames;
   } finally {
     await client.close();
   }
 }
 
+async function smokeNoTokenRechargePrompt() {
+  const originalCwd = process.cwd();
+  const originalEnv = {
+    USERPROFILE: process.env.USERPROFILE,
+    HOME: process.env.HOME,
+    VOC_TOKEN: process.env.VOC_TOKEN,
+    VOC_SOCIAL_TOKEN: process.env.VOC_SOCIAL_TOKEN,
+    XIAOHONGSHU_API_TOKEN: process.env.XIAOHONGSHU_API_TOKEN,
+    TIKHUB_API_TOKEN: process.env.TIKHUB_API_TOKEN,
+    FMODE_XIAOHONGSHU_TOKEN: process.env.FMODE_XIAOHONGSHU_TOKEN,
+    FMODE_SOCIAL_TOKEN: process.env.FMODE_SOCIAL_TOKEN,
+    DOUYIN_API_TOKEN: process.env.DOUYIN_API_TOKEN,
+    DOUYIN_TOKEN: process.env.DOUYIN_TOKEN,
+    VOC_DOUYIN_TOKEN: process.env.VOC_DOUYIN_TOKEN
+  };
+  const tempHome = path.join(os.tmpdir(), 'claude-voc-no-token-smoke');
+  const tempWorkspace = path.join(os.tmpdir(), 'claude-voc-no-token-workspace-smoke');
+  fs.mkdirSync(tempHome, { recursive: true });
+  fs.mkdirSync(tempWorkspace, { recursive: true });
+  try {
+    process.chdir(tempWorkspace);
+    process.env.USERPROFILE = tempHome;
+    process.env.HOME = tempHome;
+    [
+      'VOC_TOKEN',
+      'VOC_SOCIAL_TOKEN',
+      'XIAOHONGSHU_API_TOKEN',
+      'TIKHUB_API_TOKEN',
+      'FMODE_XIAOHONGSHU_TOKEN',
+      'FMODE_SOCIAL_TOKEN',
+      'DOUYIN_API_TOKEN',
+      'DOUYIN_TOKEN',
+      'VOC_DOUYIN_TOKEN'
+    ].forEach(key => delete process.env[key]);
+    const result = await runXiaohongshuTrend({
+      collectionMode: 'live',
+      keywords: '全屋定制',
+      output: path.join(OUTPUT_ROOT, 'no-token')
+    });
+    const body = String(result.assistantMessage || '');
+    const hasPaymentUrl = /apig-pay/.test(body) && /Vo3ROWEvDy/.test(body);
+    const hasEarlyPaymentUrl = /apig-pay/.test(body.slice(0, 180));
+    const hasNextActionUrl = (result.nextActions || []).some(action => /apig-pay/.test(String(action)));
+    if (result.status !== 'needs_token' || !hasPaymentUrl || !hasEarlyPaymentUrl || !hasNextActionUrl || (result.errors || []).length) {
+      throw new Error('no-token live path did not return a clean recharge prompt');
+    }
+  } finally {
+    Object.entries(originalEnv).forEach(([key, value]) => {
+      if (value === undefined) {
+        delete process.env[key];
+      } else {
+        process.env[key] = value;
+      }
+    });
+    process.chdir(originalCwd);
+  }
+}
+
+async function smokeDouyinNoTokenRechargePrompt() {
+  const originalCwd = process.cwd();
+  const originalEnv = {
+    USERPROFILE: process.env.USERPROFILE,
+    HOME: process.env.HOME,
+    VOC_TOKEN: process.env.VOC_TOKEN,
+    VOC_SOCIAL_TOKEN: process.env.VOC_SOCIAL_TOKEN,
+    XIAOHONGSHU_API_TOKEN: process.env.XIAOHONGSHU_API_TOKEN,
+    TIKHUB_API_TOKEN: process.env.TIKHUB_API_TOKEN,
+    FMODE_XIAOHONGSHU_TOKEN: process.env.FMODE_XIAOHONGSHU_TOKEN,
+    FMODE_SOCIAL_TOKEN: process.env.FMODE_SOCIAL_TOKEN,
+    DOUYIN_API_TOKEN: process.env.DOUYIN_API_TOKEN,
+    DOUYIN_TOKEN: process.env.DOUYIN_TOKEN,
+    VOC_DOUYIN_TOKEN: process.env.VOC_DOUYIN_TOKEN
+  };
+  const tempHome = path.join(os.tmpdir(), 'claude-voc-douyin-no-token-smoke');
+  const tempWorkspace = path.join(os.tmpdir(), 'claude-voc-douyin-no-token-workspace-smoke');
+  fs.mkdirSync(tempHome, { recursive: true });
+  fs.mkdirSync(tempWorkspace, { recursive: true });
+  try {
+    process.chdir(tempWorkspace);
+    process.env.USERPROFILE = tempHome;
+    process.env.HOME = tempHome;
+    [
+      'VOC_TOKEN',
+      'VOC_SOCIAL_TOKEN',
+      'XIAOHONGSHU_API_TOKEN',
+      'TIKHUB_API_TOKEN',
+      'FMODE_XIAOHONGSHU_TOKEN',
+      'FMODE_SOCIAL_TOKEN',
+      'DOUYIN_API_TOKEN',
+      'DOUYIN_TOKEN',
+      'VOC_DOUYIN_TOKEN'
+    ].forEach(key => delete process.env[key]);
+    const result = await runDouyinTrend({
+      collectionMode: 'live',
+      keywords: '全屋定制',
+      output: path.join(OUTPUT_ROOT, 'douyin-no-token')
+    });
+    const body = String(result.assistantMessage || '');
+    const hasPaymentUrl = /apig-pay/.test(body) && /Vo3ROWEvDy/.test(body);
+    const hasEarlyPaymentUrl = /apig-pay/.test(body.slice(0, 180));
+    const hasNextActionUrl = (result.nextActions || []).some(action => /apig-pay/.test(String(action)));
+    if (result.status !== 'needs_token' || !hasPaymentUrl || !hasEarlyPaymentUrl || !hasNextActionUrl || (result.errors || []).length) {
+      throw new Error('douyin no-token live path did not return a clean recharge prompt');
+    }
+  } finally {
+    Object.entries(originalEnv).forEach(([key, value]) => {
+      if (value === undefined) {
+        delete process.env[key];
+      } else {
+        process.env[key] = value;
+      }
+    });
+    process.chdir(originalCwd);
+  }
+}
+
+function smokeWorkspaceEnvLocalToken() {
+  const originalCwd = process.cwd();
+  const originalEnv = {
+    USERPROFILE: process.env.USERPROFILE,
+    HOME: process.env.HOME,
+    VOC_TOKEN: process.env.VOC_TOKEN,
+    VOC_SOCIAL_TOKEN: process.env.VOC_SOCIAL_TOKEN,
+    XIAOHONGSHU_API_TOKEN: process.env.XIAOHONGSHU_API_TOKEN,
+    TIKHUB_API_TOKEN: process.env.TIKHUB_API_TOKEN,
+    FMODE_XIAOHONGSHU_TOKEN: process.env.FMODE_XIAOHONGSHU_TOKEN,
+    FMODE_SOCIAL_TOKEN: process.env.FMODE_SOCIAL_TOKEN,
+    DOUYIN_API_TOKEN: process.env.DOUYIN_API_TOKEN,
+    DOUYIN_TOKEN: process.env.DOUYIN_TOKEN,
+    VOC_DOUYIN_TOKEN: process.env.VOC_DOUYIN_TOKEN
+  };
+  const tempHome = path.join(os.tmpdir(), 'claude-voc-env-local-home-smoke');
+  const tempWorkspace = path.join(os.tmpdir(), 'claude-voc-env-local-workspace-smoke');
+  fs.mkdirSync(tempHome, { recursive: true });
+  fs.mkdirSync(tempWorkspace, { recursive: true });
+  try {
+    process.chdir(tempWorkspace);
+    process.env.USERPROFILE = tempHome;
+    process.env.HOME = tempHome;
+    [
+      'VOC_TOKEN',
+      'VOC_SOCIAL_TOKEN',
+      'XIAOHONGSHU_API_TOKEN',
+      'TIKHUB_API_TOKEN',
+      'FMODE_XIAOHONGSHU_TOKEN',
+      'FMODE_SOCIAL_TOKEN',
+      'DOUYIN_API_TOKEN',
+      'DOUYIN_TOKEN',
+      'VOC_DOUYIN_TOKEN'
+    ].forEach(key => delete process.env[key]);
+    fs.writeFileSync(path.join(tempWorkspace, '.env.local'), 'VOC_TOKEN=workspace-smoke-token\n');
+    const token = readVocToken({});
+    if (token !== 'workspace-smoke-token') {
+      throw new Error('workspace .env.local token was not used');
+    }
+  } finally {
+    Object.entries(originalEnv).forEach(([key, value]) => {
+      if (value === undefined) {
+        delete process.env[key];
+      } else {
+        process.env[key] = value;
+      }
+    });
+    process.chdir(originalCwd);
+  }
+}
+
+function smokeAccessibleUrlNormalization() {
+  const note = normalizeNote({
+    id: '69f72f860000000035038a78',
+    title: '自制多功能收纳抽屉课题',
+    desc: '把柜内空间重新分区,适合小户型收纳。',
+    user: { nickname: '小熊居家' },
+    liked_count: '16.3w',
+    collected_count: '15.9w',
+    comments_count: '1.8k',
+    mini_program_info: {
+      webpage_url: 'https://www.xiaohongshu.com/discovery/item/69f72f860000000035038a78?xsec_source=app_share&xsec_token=sample-xsec-token'
+    },
+    share_info: {
+      link: 'https://www.xiaohongshu.com/discovery/item/69f72f860000000035038a78?xsec_source=app_share&xsec_token=sample-share-token'
+    },
+    image_list: [
+      { url: 'https://img.example.com/xhs-cover.jpg' }
+    ]
+  }, '全屋定制');
+  if (!note.sourceUrlVerified || !/xsec_token=sample-xsec-token/.test(note.sourceUrl || '')) {
+    throw new Error(`expected xsec_token sourceUrl, got ${note.sourceUrl}`);
+  }
+  if (/\/explore\//.test(note.sourceUrl || '') || /\/explore\//.test(note.url || '')) {
+    throw new Error('normalizeNote should not use /explore/ as the primary source URL');
+  }
+  if (note.coverUrl !== 'https://img.example.com/xhs-cover.jpg') {
+    throw new Error(`expected coverUrl from image_list, got ${note.coverUrl}`);
+  }
+}
+
+function smokeDouyinUrlNormalization() {
+  const video = normalizeVideo({
+    aweme_id: '7448118827402972455',
+    desc: '全屋定制视频开头怎么讲更容易留人?',
+    author: { nickname: '抖音家装观察员', uid: 'u001', sec_uid: 'sec001' },
+    statistics: {
+      digg_count: '1.2w',
+      comment_count: '860',
+      share_count: '520',
+      play_count: '23.5w'
+    },
+    video: {
+      cover: { url_list: ['https://img.example.com/dy-cover.jpg'] }
+    },
+    share_info: {
+      link: 'https://www.douyin.com/video/7448118827402972455?from=sample'
+    }
+  }, '全屋定制');
+  if (!video.sourceUrlVerified || !/douyin\.com\/video\//.test(video.sourceUrl || '')) {
+    throw new Error(`expected douyin sourceUrl, got ${video.sourceUrl}`);
+  }
+  if (video.coverUrl !== 'https://img.example.com/dy-cover.jpg') {
+    throw new Error(`expected coverUrl from video.cover, got ${video.coverUrl}`);
+  }
+}
+
+function smokeEvidenceCardReport(result) {
+  const body = String(result.assistantMessage || '');
+  const required = ['## 证据样本', '原帖', '原文摘录', '高赞评论'];
+  const missing = required.filter(item => !body.includes(item));
+  if (missing.length) {
+    throw new Error(`sample report missing evidence card fields: ${missing.join(', ')}`);
+  }
+  if (/xiaohongshu\.com\/explore\//.test(body)) {
+    throw new Error('sample report should not contain /explore/ Xiaohongshu links');
+  }
+  if (!body.includes('## 证据质量门槛') || !result.summary?.evidenceQuality?.grade) {
+    throw new Error('sample report did not include evidence quality gate');
+  }
+  const assetFiles = (result.files || []).filter(file => /[\\/]assets[\\/]/.test(file));
+  if (!assetFiles.length) {
+    throw new Error('sample report did not return cached asset files');
+  }
+  const missingAsset = assetFiles.find(file => !fs.existsSync(file));
+  if (missingAsset) {
+    throw new Error(`cached asset file missing: ${missingAsset}`);
+  }
+}
+
+function smokeDouyinEvidenceCardReport(result) {
+  const body = String(result.assistantMessage || '');
+  const required = ['## 证据样本', '原视频', '视频文案', '高赞评论'];
+  const missing = required.filter(item => !body.includes(item));
+  if (missing.length) {
+    throw new Error(`douyin sample report missing evidence card fields: ${missing.join(', ')}`);
+  }
+  if (!/douyin\.com\/video\//.test(body)) {
+    throw new Error('douyin sample report should contain a Douyin video link');
+  }
+  if (!body.includes('## 证据质量门槛') || !result.summary?.evidenceQuality?.grade) {
+    throw new Error('douyin sample report did not include evidence quality gate');
+  }
+  const assetFiles = (result.files || []).filter(file => /[\\/]assets[\\/]/.test(file));
+  if (!assetFiles.length) {
+    throw new Error('douyin sample report did not return cached asset files');
+  }
+  const missingAsset = assetFiles.find(file => !fs.existsSync(file));
+  if (missingAsset) {
+    throw new Error(`cached douyin asset file missing: ${missingAsset}`);
+  }
+}
+
+function sectionBetween(text, startTitle, endTitle) {
+  const body = String(text || '');
+  const start = body.indexOf(startTitle);
+  if (start < 0) return '';
+  const end = body.indexOf(endTitle, start + startTitle.length);
+  return end >= 0 ? body.slice(start, end) : body.slice(start);
+}
+
+function smokeDeepDiveMemoryBehavior(result, blockedTerm) {
+  const body = String(result.assistantMessage || '');
+  const actionSection = sectionBetween(
+    body,
+    '### 今天就能做',
+    '## \u53ef\u8f6c\u5316\u5185\u5bb9\u9009\u9898'
+  );
+  const memorySection = sectionBetween(
+    body,
+    '## \u5df2\u5e94\u7528\u7684\u4f7f\u7528\u8bb0\u5fc6',
+    '## \u4ecd\u9700\u786e\u8ba4\u7684\u95ee\u9898'
+  );
+  if (!actionSection) {
+    throw new Error('VOC problem deep dive action section was not found');
+  }
+  if (actionSection.includes(blockedTerm)) {
+    throw new Error('VOC problem deep dive leaked a blocked action into executable actions');
+  }
+  if (!memorySection.includes(blockedTerm)) {
+    throw new Error('VOC problem deep dive did not show blocked action in applied memory');
+  }
+}
+
+async function smokeBusinessStatusErrors() {
+  const originalFetch = global.fetch;
+  const makeBusinessErrorFetch = () => async () => ({
+    ok: true,
+    status: 200,
+    text: async () => JSON.stringify({
+      code: 403,
+      message: 'insufficient balance'
+    })
+  });
+
+  try {
+    global.fetch = makeBusinessErrorFetch();
+    await new XiaohongshuApi({
+      token: 'r:smoke-token',
+      baseUrl: 'https://example.test/xiaohongshu/app'
+    }).searchNotes({ keyword: 'smoke' });
+    throw new Error('xiaohongshu business status 403 did not throw');
+  } catch (error) {
+    if (error.message === 'xiaohongshu business status 403 did not throw') {
+      throw error;
+    }
+    if (Number(error.httpStatus) !== 403) {
+      throw new Error(`expected xiaohongshu business httpStatus 403, got ${error.httpStatus}`);
+    }
+  } finally {
+    global.fetch = originalFetch;
+  }
+
+  try {
+    global.fetch = makeBusinessErrorFetch();
+    await new DouyinApi({
+      token: 'r:smoke-token',
+      baseUrl: 'https://example.test/douyin'
+    }).searchVideos({ keyword: 'smoke' });
+    throw new Error('douyin business status 403 did not throw');
+  } catch (error) {
+    if (error.message === 'douyin business status 403 did not throw') {
+      throw error;
+    }
+    if (Number(error.httpStatus) !== 403) {
+      throw new Error(`expected douyin business httpStatus 403, got ${error.httpStatus}`);
+    }
+  } finally {
+    global.fetch = originalFetch;
+  }
+}
+
 async function main() {
   const sample = await runXiaohongshuTrend({
     collectionMode: 'sample',
@@ -38,9 +430,22 @@ async function main() {
   if (sample.status !== 'ok') {
     throw new Error(`sample failed: ${sample.status}`);
   }
+  smokeEvidenceCardReport(sample);
+  smokeAccessibleUrlNormalization();
+
+  const douyinSample = await runDouyinTrend({
+    collectionMode: 'sample',
+    profile: path.join(ROOT, 'memory-templates', 'douyin-trend-profile.json'),
+    output: path.join(OUTPUT_ROOT, 'douyin-sample')
+  });
+  if (douyinSample.status !== 'ok') {
+    throw new Error(`douyin sample failed: ${douyinSample.status}`);
+  }
+  smokeDouyinEvidenceCardReport(douyinSample);
+  smokeDouyinUrlNormalization();
 
   const preference = await updateXiaohongshuPreference({
-    message: '保留奶油风和全屋定制翻车方向,不要纯风格美图,下一版更偏门店转化话术',
+    message: '保留奶油风和全屋定制翻车方向,不要纯风格美图,下一更偏门店转化话术',
     memory: MEMORY_PATH
   });
   if (preference.status !== 'ok') {
@@ -57,11 +462,231 @@ async function main() {
     throw new Error('sample-with-memory did not apply preference memory');
   }
 
+  const douyinPreference = await updateDouyinPreference({
+    message: '保留口播开头和评论区问题,不要只做纯素材剪辑,下一个版本更偏门店转化话术。',
+    memory: DOUYIN_MEMORY_PATH
+  });
+  if (douyinPreference.status !== 'ok') {
+    throw new Error(`douyin preference failed: ${douyinPreference.status}`);
+  }
+
+  const douyinWithMemory = await runDouyinTrend({
+    collectionMode: 'sample',
+    profile: path.join(ROOT, 'memory-templates', 'douyin-trend-profile.json'),
+    memory: DOUYIN_MEMORY_PATH,
+    output: path.join(OUTPUT_ROOT, 'douyin-with-memory')
+  });
+  if (douyinWithMemory.status !== 'ok' || !douyinWithMemory.summary.preferenceMemoryApplied) {
+    throw new Error('douyin sample-with-memory did not apply preference memory');
+  }
+
+  const deepDive = await runVocProblemDeepDive({
+    issue: '排队',
+    industry: '本地生活餐饮',
+    scenario: '门店经营',
+    audience: '到店顾客',
+    memory: path.join(OUTPUT_ROOT, 'voc-problem-memory.json'),
+    evidence: [
+      '想吃就得排队。',
+      '排了很久以后,如果菜品普通就会觉得不值。'
+    ]
+  });
+  const deepDiveBody = String(deepDive.assistantMessage || '');
+  if (deepDive.status !== 'ok' ||
+    !deepDiveBody.includes('## 老板视角影响') ||
+    !deepDiveBody.includes('## 可执行解决动作') ||
+    !deepDiveBody.includes('## 下轮验证指标')) {
+    throw new Error('VOC problem deep dive did not return boss-oriented action output');
+  }
+  if (!deepDiveBody.includes('## 优先级判断') || !deepDiveBody.includes('## 7 天验证计划')) {
+    throw new Error('VOC problem deep dive did not return priority and validation plan');
+  }
+  if (!deepDive.summary.memoryPath || !(deepDive.files || []).includes(deepDive.summary.memoryPath)) {
+    throw new Error('VOC problem deep dive did not persist memory');
+  }
+  const deepDiveWithMemory = await runVocProblemDeepDive({
+    issue: '排队',
+    industry: '本地生活餐饮',
+    scenario: '门店经营',
+    audience: '到店顾客',
+    memory: deepDive.summary.memoryPath,
+    feedback: '老板不想做等位饮品,更想先改评论区回复和预点单',
+    blockedActions: ['等位饮品'],
+    preferredActions: ['评论区回复', '预点单']
+  });
+  const memoryBody = String(deepDiveWithMemory.assistantMessage || '');
+  if (!deepDiveWithMemory.summary.memoryApplied || !memoryBody.includes('## 已应用的使用记忆')) {
+    throw new Error('VOC problem deep dive did not apply iterative memory');
+  }
+  smokeDeepDiveMemoryBehavior(deepDiveWithMemory, '等位饮品');
+
+  const issuePool = await runVocIssuePool({
+    project: '样板门店A',
+    industry: '本地生活餐饮',
+    scenario: '门店经营',
+    audience: '到店顾客',
+    evidence: [
+      '排队久,但是不知道值不值得等。',
+      '等位时间不透明,到了现场才知道要等多久。',
+      '人均有点高,第一次来不知道怎么点不踩雷。',
+      '服务忙起来没人理。',
+      '视频很种草,实际到店体验有落差。'
+    ]
+  });
+  const issuePoolBody = String(issuePool.assistantMessage || '');
+  if (issuePool.status !== 'ok' ||
+    !issuePoolBody.includes('# VOC 问题池') ||
+    !issuePoolBody.includes('## 问题优先级') ||
+    !issuePool.summary.memoryPath ||
+    !issuePool.data?.issues?.length) {
+    throw new Error('VOC issue pool did not return prioritized issue output');
+  }
+  if (!issuePool.data.issues.some(issue => issue.title === '排队/等待')) {
+    throw new Error('VOC issue pool did not classify waiting issue');
+  }
+  const issuePoolUpdated = await runVocIssuePool({
+    project: '样板门店A',
+    industry: '本地生活餐饮',
+    scenario: '门店经营',
+    audience: '到店顾客',
+    memory: issuePool.summary.memoryPath,
+    evidence: ['排队问题已经开始做预点单验证。'],
+    validatingIssues: ['排队/等待']
+  });
+  if (!issuePoolUpdated.data?.issues?.some(issue => issue.title === '排队/等待' && issue.status === '验证中')) {
+    throw new Error('VOC issue pool did not apply issue status update');
+  }
+  const waitingTrend = issuePoolUpdated.data?.issues?.find(issue => issue.title === '排队/等待');
+  if (!waitingTrend || waitingTrend.previousCount < 1 || waitingTrend.delta >= 0 || waitingTrend.trend !== 'down') {
+    throw new Error('VOC issue pool did not compare issue trend with previous run');
+  }
+  if (!String(issuePoolUpdated.assistantMessage || '').includes('## 问题变化')) {
+    throw new Error('VOC issue pool did not output issue change section');
+  }
+
+  const parsedFeedbackDeepDive = await runVocProblemDeepDive({
+    issue: '排队',
+    project: '样板门店A',
+    industry: '本地生活餐饮',
+    scenario: '门店经营',
+    audience: '到店顾客',
+    feedback: '这个店不想做等位饮品,更想先改评论区回复和预点单'
+  });
+  smokeDeepDiveMemoryBehavior(parsedFeedbackDeepDive, '等位饮品');
+  if (!String(parsedFeedbackDeepDive.summary.memoryPath || '').includes('样板门店a')) {
+    throw new Error('VOC problem deep dive did not use scoped default memory path');
+  }
+  const parsedMemory = parsedFeedbackDeepDive.data?.memory || {};
+  if (!parsedMemory.blockedActions?.includes('等位饮品') ||
+    !parsedMemory.preferredActions?.includes('评论区回复') ||
+    !parsedMemory.preferredActions?.includes('预点单')) {
+    throw new Error('VOC problem deep dive did not parse natural-language feedback into memory');
+  }
+
+  const actionWeightedDeepDive = await runVocProblemDeepDive({
+    issue: '排队',
+    project: '样板门店A',
+    industry: '本地生活餐饮',
+    scenario: '门店经营',
+    audience: '到店顾客',
+    memory: parsedFeedbackDeepDive.summary.memoryPath,
+    validatedActions: ['预点单'],
+    rejectedActions: ['等位饮品'],
+    feedback: '预点单有效,等位饮品无效'
+  });
+  const actionWeightedBody = String(actionWeightedDeepDive.assistantMessage || '');
+  if (!actionWeightedBody.includes('为什么优先推荐这些动作')) {
+    throw new Error('VOC problem deep dive did not explain action-weight memory');
+  }
+  const actionStats = actionWeightedDeepDive.data?.memory?.actionStats || {};
+  if (!actionStats['预点单']?.validatedCount || !actionStats['等位饮品']?.rejectedCount) {
+    throw new Error('VOC problem deep dive did not persist action validation stats');
+  }
+
+  const contentPlan = await runVocContentPlan({
+    brand: '样板门店A',
+    industry: '本地生活餐饮',
+    issues: ['第一次来怕踩雷', '觉得人均贵', '怕排队久']
+  });
+  const contentPlanBody = String(contentPlan.assistantMessage || '');
+  if (contentPlan.status !== 'ok' ||
+    !contentPlanBody.includes('7 天 VOC 内容选题') ||
+    !contentPlanBody.includes('前 3 秒钩子') ||
+    !contentPlanBody.includes('口播脚本') ||
+    contentPlan.data?.plan?.length !== 7) {
+    throw new Error('VOC content plan did not return 7-day script output');
+  }
+
+  const speakingScript = await runVocSpeakingScript({
+    brand: '样板门店A',
+    industry: '本地生活餐饮',
+    topic: '第一次来怎么点不踩雷',
+    userIssue: '第一次来怕点错',
+    evidence: ['第一次来不知道怎么点,怕踩雷'],
+    feedback: '开头更狠一点,老板视角,少讲概念,多给具体场景',
+    memory: path.join(OUTPUT_ROOT, 'voc-speaking-script-memory.json'),
+    finalize: true
+  });
+  const speakingScriptBody = String(speakingScript.assistantMessage || '');
+  if (speakingScript.status !== 'ok' ||
+    !speakingScriptBody.includes('VOC 口播脚本共创') ||
+    !speakingScriptBody.includes('60 秒口播稿') ||
+    !speakingScriptBody.includes('30 秒压缩版') ||
+    !speakingScript.data?.memory?.finalizedScripts?.length ||
+    !speakingScript.data?.memory?.preferredStyles?.includes('老板视角')) {
+    throw new Error('VOC speaking script did not return co-creation output with memory');
+  }
+
+  const competitorMap = await runVocCompetitorMap({
+    brand: '样板门店A',
+    city: '南昌',
+    category: '餐饮'
+  });
+  const competitorMapBody = String(competitorMap.assistantMessage || '');
+  if (competitorMap.status !== 'ok' ||
+    !competitorMapBody.includes('竞品图谱') ||
+    !competitorMapBody.includes('错位竞争') ||
+    !competitorMap.data?.competitors?.length) {
+    throw new Error('VOC competitor map did not return competitor opportunity output');
+  }
+
+  const businessWorkflow = await runBusinessWorkflow({
+    brand: '样板门店A',
+    industry: '本地生活餐饮',
+    platform: 'douyin',
+    collectionMode: 'sample',
+    keywords: ['南昌菜', '第一次来怎么点', '排队']
+  });
+  const businessWorkflowBody = String(businessWorkflow.assistantMessage || '');
+  if (businessWorkflow.status !== 'ok' ||
+    !businessWorkflowBody.includes('VOC经营闭环') ||
+    !businessWorkflowBody.includes('市场和用户声音') ||
+    !businessWorkflowBody.includes('先改哪个问题') ||
+    !businessWorkflowBody.includes('第一条口播稿') ||
+    businessWorkflow.summary?.workflowStage !== 'business_workflow_first_round') {
+    throw new Error('VOC business workflow did not return end-to-end workflow output');
+  }
+
+  await smokeNoTokenRechargePrompt();
+  await smokeDouyinNoTokenRechargePrompt();
+  smokeWorkspaceEnvLocalToken();
+  await smokeBusinessStatusErrors();
+  await smokeCrossIndustry({ root: ROOT });
+
   const toolNames = await smokeMcpTools();
   const requiredTools = [
+    'voc_business_workflow_run',
     'voc_xiaohongshu_token_check',
     'voc_xiaohongshu_trend_run',
-    'voc_xiaohongshu_preference_update'
+    'voc_xiaohongshu_preference_update',
+    'voc_douyin_token_check',
+    'voc_douyin_trend_run',
+    'voc_douyin_preference_update',
+    'voc_issue_pool_run',
+    'voc_problem_deep_dive_run',
+    'voc_content_plan_run',
+    'voc_speaking_script_run',
+    'voc_competitor_map_run'
   ];
   for (const tool of requiredTools) {
     if (!toolNames.includes(tool)) {
@@ -73,8 +698,30 @@ async function main() {
     status: 'ok',
     checks: [
       'sample report',
+      'evidence cards',
+      'local asset cache',
+      'xsec URL normalization',
       'preference memory',
       'sample report with memory',
+      'no-token recharge prompt',
+      'workspace .env.local token source',
+      'douyin sample report',
+      'douyin sample report with memory',
+      'VOC issue pool',
+      'VOC issue pool status memory',
+      'VOC issue pool trend comparison',
+      'VOC problem deep dive',
+      'VOC problem deep dive scoped memory',
+      'VOC problem deep dive feedback parser',
+      'VOC problem deep dive blocked-action guard',
+      'VOC problem deep dive action-weight memory',
+      'VOC content plan',
+      'VOC speaking script co-creation',
+      'VOC competitor map',
+      'VOC business workflow',
+      'douyin no-token recharge prompt',
+      'business-status recharge guard',
+      'cross-industry sample smoke',
       'MCP tools'
     ],
     mcpTools: toolNames,

+ 44 - 6
claude-code-voc-intelligence/skill-package-manifest.json

@@ -1,22 +1,60 @@
 {
   "name": "claude-code-voc-intelligence",
-  "version": "0.1.2",
-  "description": "Claude Code 原生 VOC 情报技能包。当前包含小红书趋势情报官,支持 sample 演示、MCP 工具、live token 检查、动态校准问题和偏好记忆。",
+  "version": "0.3.10",
+  "description": "Claude Code native VOC intelligence package for cross-industry social VOC workflows. Includes Xiaohongshu and Douyin trend intelligence, live token checks, evidence-card reports, single-issue VOC deep dives, boss/operator action plans, and scoped multi-turn memory.",
   "plugin": "voc-intelligence",
   "skills": [
-    "xiaohongshu-trend-intelligence"
+    "xiaohongshu-trend-intelligence",
+    "douyin-trend-intelligence",
+    "voc-issue-pool",
+    "voc-problem-deep-dive",
+    "voc-content-plan",
+    "voc-speaking-script",
+    "voc-competitor-map",
+    "voc-business-workflow"
   ],
   "mcpTools": [
+    "voc_business_workflow_run",
+    "voc_speaking_script_run",
+    "voc_issue_pool_run",
+    "voc_problem_deep_dive_run",
+    "voc_content_plan_run",
+    "voc_competitor_map_run",
     "voc_xiaohongshu_token_check",
     "voc_xiaohongshu_trend_run",
-    "voc_xiaohongshu_preference_update"
+    "voc_xiaohongshu_preference_update",
+    "voc_douyin_token_check",
+    "voc_douyin_trend_run",
+    "voc_douyin_preference_update"
   ],
   "entrySkill": "xiaohongshu-trend-intelligence",
+  "capabilityMap": "docs/capability-map.md",
+  "liveAcceptanceChecklist": "docs/live-manual-acceptance-checklist.md",
+  "paymentLinksDoc": "docs/payment-package-links.md",
+  "contextBudgetDoc": "docs/context-budget-implementation-plan.md",
+  "primaryUseCases": [
+    "First-round Xiaohongshu or Douyin trend intelligence from sample or live collection",
+    "Evidence-first customer concern and opportunity reports",
+    "Prioritized VOC issue pools with status, evidence, impact, and next actions",
+    "Single VOC problem deep dive into boss/operator actions and 7-day validation plans",
+    "7-day VOC content plans with speaking scripts and validation metrics",
+    "VOC-backed speaking script co-creation with revision, finalization, and script memory",
+    "End-to-end VOC business workflow from market voices to issue, action, content, and script",
+    "Competitor maps and differentiated opportunity reports from VOC evidence",
+    "Multi-turn preference and scoped memory so repeated use gets more tailored",
+    "Friendly token and recharge handling for live collection"
+  ],
+  "recommendedUserPrompts": [
+    "帮我做一份家装全屋定制行业的小红书趋势情报。先用演示样例跑通流程,不要真实采集。",
+    "帮我看下南昌餐饮行业抖音上用户最近在吐槽什么,先小规模真实采集。",
+    "继续深挖“排队”这个问题,站在老板视角告诉我怎么解决。"
+  ],
   "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 加载。"
+  "workspaceInstallCommand": "npx @gangvy/claude-code-voc-intelligence workspace --smoke",
+  "workspaceInstallTarget": ".claude/plugins/voc-intelligence",
+  "installHint": "Supports three delivery modes: unzip and run node install.js, install globally into ~/.claude/plugins/voc-intelligence, or install into the current workspace. Workspace mode writes .mcp.json plus .claude/skills/* entries for trend intelligence, issue pool, deep dive, competitor map, content plan, speaking script, and VOC business workflow so VSCode Claude Code can discover the workflows without relying only on --plugin-dir."
 }
-

+ 61 - 0
claude-code-voc-intelligence/skills/douyin-trend-intelligence/SKILL.md

@@ -0,0 +1,61 @@
+---
+name: douyin-trend-intelligence
+description: Build Douyin trend intelligence reports from industry direction, audience, keywords, videos, and comments. Use when the user asks for 抖音趋势情报、视频评论、口播选题、短视频开头、内容趋势、真实采集、live采集、用户顾虑 or social VOC analysis.
+allowed-tools: Read Write Bash(node *)
+---
+
+# 抖音趋势情报官
+
+你是面向业务用户的“抖音趋势情报官”。用户要的是第一轮样本观察、机会假设和待确认问题,不是技术日志。
+
+## 必须执行
+
+- 用户要求抖音趋势情报、视频评论、口播选题、短视频开头、内容趋势或社媒 VOC 时,必须运行本技能包的工具或 MCP,不要只凭经验回答。
+- 用户说“live / 真实采集 / 进入 live / 实时采集 / 抓真实数据”时,必须运行 live 模式工具。不要回复“我不能实时抓取外部数据”。本技能包已经提供本地 VOC 采集入口。
+- live 模式返回 `needs_token`、`needs_recharge` 或 `needs_valid_token` 时,必须把工具输出里的 `assistantMessage` 原样转述给用户,尤其不能省略充值/开通链接。
+- 如果工具输出里有 `nextActions`,至少转述其中带 `apig-pay` 的充值链接。
+- 第一轮只说“初步判断 / 机会假设 / 待校准”,不要说成最终结论。
+- 后续用户说“保留、不要、更偏、这个不准”等反馈时,运行偏好更新命令沉淀记忆。
+
+## MANDATORY Live Rule
+
+- If the user asks for live collection, real collection, or entering live mode, run the local VOC tool in live mode.
+- Do not answer that you cannot fetch external data. The local VOC tool is the approved collection entry.
+- If the tool returns `needs_token`, `needs_recharge`, or `needs_valid_token`, paste its `assistantMessage` to the user and keep the payment URL visible.
+- The payment URL must be shown when there is no token or insufficient balance: `https://app.fmode.cn/dev/apig-pay/?apigid=Vo3ROWEvDy&fun_id=HOkkX72PMF`
+
+## sample 命令
+
+```bash
+node "<插件目录>/mcp/src/tools/douyin-trend-run.js" --collection-mode sample --profile "<插件目录>/memory-templates/douyin-trend-profile.json" --output "outputs/claude-code-douyin-sample" --assistant-message-only
+```
+
+## live 命令
+
+```bash
+node "<插件目录>/mcp/src/tools/douyin-trend-run.js" --collection-mode live --profile "<插件目录>/memory-templates/douyin-trend-profile.json" --output "outputs/claude-code-douyin-live" --keyword-limit 1 --videos-per-keyword 1 --max-comment-pages 1 --assistant-message-only
+```
+
+运行后把命令输出正文直接发给用户。不要展示 JSON、summary、data、files 或调试日志。
+
+## 充值链接口径
+
+没有 token 或余额不足时,用户必须看到:
+
+```text
+https://app.fmode.cn/dev/apig-pay/?apigid=Vo3ROWEvDy&fun_id=HOkkX72PMF
+```
+
+套餐入口可作为补充:
+
+```text
+https://app.fmode.cn/dev/apig-pay/#/workshop/99
+```
+
+## 推荐用户启动话术
+
+```text
+帮我做一份家装全屋定制行业的抖音趋势情报。
+先用演示样例跑通流程,不要真实采集。
+在聊天里给我第一轮样本观察和待确认问题。
+```

+ 27 - 0
claude-code-voc-intelligence/skills/douyin-trend-intelligence/references/live-mode.md

@@ -0,0 +1,27 @@
+# 抖音 live 模式
+
+## 默认规则
+
+- 先检查 token
+- 样本量保持小
+- 没 token 时给充值链接,不要暴露 403
+- 余额不足时也给充值链接,不要暴露底层错误
+- 用户只需要看到“怎么继续”
+
+## 推荐默认参数
+
+- `keywordLimit = 1`
+- `videosPerKeyword = 1`
+- `maxCommentPages = 1`
+- `cacheAssets = true`
+
+## 失败提示
+
+当 live 失败时,优先区分:
+
+1. 没有 token
+2. token 无效
+3. 额度不足
+4. 没有采到有效样本
+
+不要把底层接口报错直接丢给用户。

+ 32 - 0
claude-code-voc-intelligence/skills/douyin-trend-intelligence/references/output-format.md

@@ -0,0 +1,32 @@
+# 抖音趋势输出格式
+
+第一轮建议固定成这些段落:
+
+1. 第一轮初步判断(待校准)
+2. 数据口径
+3. 视频观察 Top 5
+4. 本轮机会假设
+5. 用户顾虑 / 决策阻力
+6. 证据样本
+7. 可继续验证的选题方向
+8. 暂可参考业务话术 / 内容表达
+9. 下一轮请先校准这些问题
+
+## 证据样本
+
+每条证据尽量包含:
+
+- 标题
+- 作者
+- 互动数据
+- 原视频链接
+- 视频封面
+- 视频文案
+- 高赞评论
+- 为什么值得看
+
+## 语气要求
+
+- 先判断,不要直接下结论
+- 证据优先,判断其次
+- 要能被业务人员直接拿去二次沟通

+ 24 - 0
claude-code-voc-intelligence/skills/douyin-trend-intelligence/references/user-workflow.md

@@ -0,0 +1,24 @@
+# 抖音趋势工作流
+
+## 推荐流程
+
+1. 先判断用户是不是要“样例演示”还是“真实采集”
+2. 样例演示直接跑 `voc_douyin_trend_run` 的 `sample` 模式
+3. 真实采集先跑 `voc_douyin_token_check`
+4. token 可用后,再跑 `voc_douyin_trend_run` 的 `live` 模式
+5. 用户反馈“保留、不要、更偏、这个不准”时,跑 `voc_douyin_preference_update`
+
+## 用户常见输入
+
+- 抖音趋势情报
+- 抖音视频评论分析
+- 口播开头和选题
+- 某个行业在抖音上的趋势机会
+- 用户顾虑、评论区问题、爆款表达
+
+## 交互原则
+
+- 用户不需要知道字段名、接口名、token 名称
+- 先给第一轮观察,再让用户校准
+- 不要把样本写成最终结论
+- 报告正文要能直接发到聊天里

+ 61 - 0
claude-code-voc-intelligence/skills/voc-business-workflow/SKILL.md

@@ -0,0 +1,61 @@
+---
+name: voc-business-workflow
+description: Run the full VOC business workflow from natural boss language: collect or sample market voices, summarize trends, build a VOC issue pool, deep-dive the top issue, create a 7-day content plan, and draft one VOC-backed speaking script. Use when the user asks 看顾客关心什么、先改哪里、下周发什么、跑完整VOC闭环、做一份经营诊断 or wants one end-to-end VOC workflow instead of separate tools.
+---
+
+# VOC经营闭环
+
+你是面向老板和经营者的 VOC 经营工作台。用户不需要知道 MCP、schema、collectionMode 或提示词技术。用户只要说“帮我看看顾客在关心什么,先改哪里,下周发什么”,你就要把完整链路跑起来。
+
+## 触发场景
+
+优先使用本技能:
+
+- 用户说“跑完整 VOC 闭环”
+- 用户说“帮我看看这个品类顾客关心什么,并告诉我先改哪里”
+- 用户说“看市场、看问题、给动作、给内容一起做”
+- 用户说“我明天给客户远程出一份报告,先跑一版”
+- 用户不想分步骤选择技能,只想拿到一份经营交付物
+
+## 必须执行
+
+运行 `voc_business_workflow_run`,不要只给分析建议。
+
+第一轮默认小规模运行。没有明确要求真实采集时用 `sample`;用户说“真实采集 / live / 抓真实数据”时用 `live`。
+
+如果 live 采集返回 `needs_token`、`needs_recharge`、`needs_valid_token` 或 `no_live_samples`,把工具返回的 `assistantMessage` 清楚发给用户,不要暴露底层 403 或上游报错。
+
+## 交付物口径
+
+输出必须让用户看到五段:
+
+1. 市场和用户声音
+2. 先改哪个问题
+3. Top 问题怎么解决
+4. 下周账号发什么
+5. 第一条口播稿
+
+第一轮是“初步判断”,不是最终结论。要提醒用户校准真实业务反馈。
+
+## 用户可说的话
+
+```text
+帮我看一下南昌餐饮最近顾客在关心什么,并告诉我先改哪里、下周发什么。
+```
+
+```text
+给这个门店跑一遍 VOC 经营闭环,先用演示样例。
+```
+
+```text
+进入真实采集,帮我做一版市场问题、单点动作和7天内容计划。
+```
+
+## 继续迭代
+
+用户回答“这个问题不准 / 这个动作不适合 / 这个有效 / 换一个选题”时,要把反馈继续交给对应工具:
+
+- 问题排序不准:转 `voc_issue_pool_run`
+- 单点动作不适合:转 `voc_problem_deep_dive_run`
+- 内容计划要改:转 `voc_content_plan_run`
+- 口播稿要改或定稿:转 `voc_speaking_script_run`

+ 60 - 0
claude-code-voc-intelligence/skills/voc-competitor-map/SKILL.md

@@ -0,0 +1,60 @@
+---
+name: voc-competitor-map
+description: Build a competitor map and differentiated opportunity report from category, city, own-brand notes, competitor names, and VOC evidence. Use when the user asks 看竞品、竞品分析、错位竞争、别人为什么吸引顾客、我不知道竞品是谁、帮我找竞品、品牌怎么跟进、竞争策略.
+allowed-tools: Read Write Bash(node *)
+---
+
+# VOC 竞品图谱官
+
+你是面向老板、门店经营者和营销负责人的 VOC 竞品图谱官。用户不需要先给完整竞品名单。用户只要说“帮我看看竞品”“我不知道竞品是谁”“我们这个品类怎么错位竞争”,你就要先给竞品候选维度,再产出基于用户声音的错位机会。
+
+## 必须执行
+
+- 当用户说“看竞品、竞品分析、错位竞争、别人怎么做、谁做得好、我不知道竞品是谁、竞争策略”时,优先运行 `voc_competitor_map_run`。
+- 需要判断竞品候选维度、错位竞争表达或餐饮场景时,优先参考 `references/restaurant-competitor-framework.md` 和 `references/restaurant-differentiation-patterns.md`。
+- 如果用户没有明确竞品,先按同城、同品类、同价格带、同消费场景、平台声量生成候选竞品类型。
+- 输出必须站在经营视角:别人被夸什么、被骂什么、我方该避开什么、突出什么。
+- 不能只做品牌 SWOT。必须保留用户原话或 VOC 证据样本。
+- 下一步必须引导进入问题池或内容计划:把竞品评论整理成问题池,或把错位机会生成 7 天内容选题。
+- 不覆盖创始人故事。创始人故事不是本 VOC 技能包主线。
+
+## CLI 命令
+
+```bash
+node "<插件目录>/mcp/src/tools/voc-competitor-map-run.js" --brand "样板门店A" --city "南昌" --category "餐饮" --assistant-message-only
+```
+
+带竞品名单:
+
+```bash
+node "<插件目录>/mcp/src/tools/voc-competitor-map-run.js" --brand "样板门店A" --city "南昌" --category "烧烤" --competitors "竞品A,竞品B,竞品C" --assistant-message-only
+```
+
+## 输出结构
+
+工具输出的 `assistantMessage` 已经是面向用户的正文,直接转述即可。它包含:
+
+- 你这轮能拿到什么
+- 业务口径
+- 竞品候选图谱
+- 用户可能被吸引的点
+- 用户可能吐槽的点
+- 我方错位机会
+- VOC 信号
+- 错位竞争建议
+- 证据样本
+- 下一步建议
+
+## 典型用户话术
+
+```text
+我是南昌做烧烤的,不知道竞品是谁,帮我先做一份竞品图谱。
+```
+
+```text
+帮我看看同城同品类别人为什么吸引顾客,我应该怎么错位竞争。
+```
+
+```text
+根据这些竞品评论,帮我整理他们被夸什么、被骂什么,我们该怎么跟进。
+```

+ 25 - 0
claude-code-voc-intelligence/skills/voc-competitor-map/references/restaurant-competitor-framework.md

@@ -0,0 +1,25 @@
+# 餐饮竞品图谱框架
+
+## 竞品不明确时的候选维度
+
+1. 同城同品类:抢同一批用户的直接选择。
+2. 同价格带:用户用来判断值不值。
+3. 同消费场景:聚餐、家庭餐、夜宵、工作餐、宴请。
+4. 内容声量竞品:平台上更容易被刷到的门店。
+5. 替代选择:用户不吃你时会去吃什么。
+
+## VOC 对比维度
+
+- 被夸点:味道、招牌、分量、价格、服务、环境、排队热闹、内容好看。
+- 被吐槽点:排队久、人均贵、服务慢、出品不稳、到店落差、不知道怎么点。
+- 用户决策理由:为什么想去、为什么犹豫、为什么复购、为什么不推荐。
+- 内容打法:拍菜品、拍老板、拍门店氛围、拍攻略、拍避坑。
+- 我方机会:避开对手强项,承接对手未解决的顾虑。
+
+## 输出原则
+
+- 不做空泛 SWOT。
+- 不只列门店名,要说为什么要看它。
+- 每个错位机会都要能转成经营动作或内容选题。
+- 没有真实竞品名单时,先给候选类型和补充问题。
+

+ 44 - 0
claude-code-voc-intelligence/skills/voc-competitor-map/references/restaurant-differentiation-patterns.md

@@ -0,0 +1,44 @@
+# 餐饮错位竞争表达模板
+
+## 常见错位机会
+
+### 对手热闹但排队久
+
+我方不要只学排队热闹,要讲:
+
+- 几点来更舒服。
+- 等位时怎么先点。
+- 不适合排队的人怎么选择。
+
+### 对手内容好看但到店落差
+
+我方要讲:
+
+- 真实环境。
+- 适合谁、不适合谁。
+- 无滤镜菜品。
+
+### 对手招牌强但点单复杂
+
+我方要讲:
+
+- 第一次来怎么点。
+- 两人/四人/家庭怎么点。
+- 哪些菜不建议第一次点太多。
+
+### 对手价格低
+
+我方不要硬打便宜,要讲:
+
+- 贵在哪里。
+- 怎么点更值。
+- 适合什么场景。
+
+### 对手服务强
+
+我方要补:
+
+- 高峰期流程。
+- 加菜/催菜/打包找谁。
+- 门店提示牌和员工响应。
+

+ 56 - 0
claude-code-voc-intelligence/skills/voc-content-plan/SKILL.md

@@ -0,0 +1,56 @@
+---
+name: voc-content-plan
+description: Turn VOC issues, comments, trend evidence, issue-pool output, or single-problem deep dives into a 7-day content plan with short-video topics, opening hooks, speaking scripts, shot suggestions, comment prompts, and validation metrics. Use when the user asks 下周账号发什么、口播脚本、短视频选题、爆款视频、把用户问题变成内容、7天内容计划、餐饮老板怎么拍视频.
+allowed-tools: Read Write Bash(node *)
+---
+
+# VOC 内容选题官
+
+你是面向老板、门店经营者和营销负责人的 VOC 内容选题官。用户不需要学习提示词,也不需要懂内容方法论。用户只要说“下周账号发什么”“把这些问题变成口播”“根据顾客吐槽生成 7 天选题”,你就要把 VOC 问题转成可拍、可说、可验证的内容计划。
+
+## 必须执行
+
+- 当用户说“下周发什么、7 天选题、口播脚本、短视频脚本、爆款视频、账号内容、把问题变成内容”时,优先运行 `voc_content_plan_run`。
+- 需要判断内容结构、口播结构或餐饮拍摄角度时,优先参考 `references/restaurant-content-framework.md` 和 `references/restaurant-speaking-script-patterns.md`。
+- 口播脚本必须来自 VOC:用户真实吐槽、竞品评论差异、问题池 Top 问题或单点深挖动作。
+- 不要输出泛泛的爆款标题,不要只给“高级感、烟火气、探店感”这类空词。
+- 每条内容都要包含:用户问题、选题标题、前 3 秒钩子、口播脚本、镜头建议、评论区引导、验证指标。
+- 输出默认 7 天计划;用户明确要求 3 天、15 天时再调整。
+- 不覆盖创始人故事。用户要创始人故事时,说明这不属于 VOC 技能包主线,可另起品牌故事工作流。
+
+## CLI 命令
+
+```bash
+node "<插件目录>/mcp/src/tools/voc-content-plan-run.js" --brand "样板门店A" --industry "本地生活餐饮" --issues "第一次来怕踩雷;觉得人均贵;怕排队久" --assistant-message-only
+```
+
+带报告文件:
+
+```bash
+node "<插件目录>/mcp/src/tools/voc-content-plan-run.js" --brand "样板门店A" --report "outputs/douyin-trend-report.md" --assistant-message-only
+```
+
+## 输出结构
+
+工具输出的 `assistantMessage` 已经是面向用户的正文,直接转述即可。它包含:
+
+- 你这轮能拿到什么
+- 业务口径
+- 7 天内容表
+- 每天的用户问题、洞察判断、标题、前 3 秒钩子、口播脚本、镜头建议、评论区引导、验证指标
+- 拍摄提醒
+- 下一步建议
+
+## 典型用户话术
+
+```text
+根据这些顾客吐槽,帮我生成下周 7 天抖音选题和口播脚本。
+```
+
+```text
+把“排队久、人均贵、第一次来怕踩雷”这些问题变成老板能拍的视频。
+```
+
+```text
+这份问题池里 Top 3 问题,帮我转成 7 天账号内容计划。
+```

+ 41 - 0
claude-code-voc-intelligence/skills/voc-content-plan/references/restaurant-content-framework.md

@@ -0,0 +1,41 @@
+# 餐饮 VOC 内容选题框架
+
+## 使用原则
+
+内容不是从“今天想拍什么”开始,而是从“顾客正在犹豫什么”开始。
+
+每条内容必须回答一个 VOC 问题:
+
+- 怕踩雷:第一次来怎么点。
+- 嫌贵:为什么值、怎么点更稳。
+- 怕排队:什么时候来、等位怎么安排。
+- 不知道招牌:不同人群怎么选。
+- 担心服务:高峰期怎么减少不确定。
+- 担心落差:适合谁、不适合谁。
+
+## 结构
+
+1. 用户问题:从评论、问题池、竞品吐槽中提取。
+2. 内容承诺:这条视频帮用户解决什么。
+3. 前 3 秒钩子:用用户原话或老板反常识表达。
+4. 主体口播:先承认问题,再给具体方案。
+5. 镜头证据:菜品、菜单、门店流程、真实环境。
+6. 评论区引导:让用户继续暴露顾虑。
+7. 验证指标:看评论关键词是否变化。
+
+## 常用钩子
+
+- 第一次来别乱点,先按这个顺序。
+- 觉得我们人均不低,我直接讲贵在哪里。
+- 想吃但怕排队,先记住这几个时间。
+- 这道招牌菜不是所有人都适合点。
+- 先说缺点,再说适合什么人来。
+- 高峰期最容易不舒服的地方,我们直接讲清楚。
+
+## 禁止
+
+- 不要只写“烟火气”“高级感”“宝藏小店”。
+- 不要每条都夸自己。
+- 不要把创始人故事混进 VOC 内容计划。
+- 不要脱离用户问题写泛泛爆款脚本。
+

+ 34 - 0
claude-code-voc-intelligence/skills/voc-content-plan/references/restaurant-speaking-script-patterns.md

@@ -0,0 +1,34 @@
+# 餐饮口播脚本结构
+
+## 30 秒结构
+
+1. 0-3 秒:用户问题钩子。
+2. 3-8 秒:承认顾虑,不急着自夸。
+3. 8-22 秒:给一个具体建议。
+4. 22-30 秒:评论区引导或到店动作。
+
+示例:
+
+```text
+第一次来别一上来就乱点。
+如果你怕点错、怕人均超预算,可以先按这个顺序来。
+先点招牌,再点一个下饭菜,最后加一个解腻的。
+你是两个人来还是四个人来?评论区告诉我,我给你配一版。
+```
+
+## 60 秒结构
+
+1. 用户原话。
+2. 老板解释真实原因。
+3. 给出方案。
+4. 展示门店证据。
+5. 引导用户继续提问。
+
+## 镜头搭配
+
+- 菜品问题:菜品近景、出餐过程、菜单价格、桌面组合。
+- 排队问题:门口等位、非高峰空桌、等位提示牌。
+- 价格问题:食材、分量、套餐、适合人数。
+- 服务问题:提示牌、动线、员工响应、收银/打包区。
+- 落差问题:无滤镜环境、真实高峰、适合谁不适合谁。
+

+ 74 - 0
claude-code-voc-intelligence/skills/voc-issue-pool/SKILL.md

@@ -0,0 +1,74 @@
+---
+name: voc-issue-pool
+description: Turn VOC comments, report evidence, or user feedback into a prioritized issue pool with status, evidence, business impact, next actions, and follow-up deep-dive suggestions. Use when the user asks 哪些问题最影响生意、问题池、问题清单、用户都在吐槽什么、先改哪个、VOC问题管理、问题优先级、把评论整理成问题.
+allowed-tools: Read Write Bash(node *)
+---
+
+# VOC 问题池官
+
+你是面向老板、门店经营者和营销负责人的 VOC 问题池官。用户不需要知道分类法,也不需要学习提示词。用户只要说“这些评论里哪些问题最重要”“先改哪个”“整理成问题池”,你就把真实用户声音整理成可排序、可跟进、可继续深挖的问题列表。
+
+## 必须执行
+
+- 当用户说“问题池、问题清单、用户吐槽什么、哪些问题最影响生意、先改哪个、VOC 问题管理、把评论整理成问题”时,优先运行 `voc_issue_pool_run`。
+- 如果上一轮小红书/抖音报告里有证据样本或高赞评论,把相关评论作为 `evidence`、`evidenceText` 或 `reportPath` 传入。
+- 输出必须站在业务视角:这个问题影响新客、复购、客单、口碑、内容信任,还是现场效率。
+- 不要只做词频统计。每个问题都要包含证据样本、影响环节、建议动作和下一步。
+- 如果用户说某个问题已解决、验证中、暂缓,要通过 `resolvedIssues`、`validatingIssues`、`blockedIssues` 或 `statusUpdates` 更新问题状态。
+- 后续深挖时,引导用户直接说“继续深挖「问题名」”,然后转入 `voc-problem-deep-dive`。
+
+## CLI 命令
+
+```bash
+node "<插件目录>/mcp/src/tools/voc-issue-pool-run.js" --project "样板门店A" --industry "本地生活餐饮" --scenario "门店经营" --audience "到店顾客" --evidence "排队久,但是不知道值不值得等。,人均有点高,第一次来不知道怎么点不踩雷。,服务忙起来没人理。" --assistant-message-only
+```
+
+带报告文件:
+
+```bash
+node "<插件目录>/mcp/src/tools/voc-issue-pool-run.js" --project "样板门店A" --industry "本地生活餐饮" --report "outputs/douyin-trend-report.md" --assistant-message-only
+```
+
+更新状态:
+
+```bash
+node "<插件目录>/mcp/src/tools/voc-issue-pool-run.js" --project "样板门店A" --industry "本地生活餐饮" --resolved-issues "排队/等待" --assistant-message-only
+```
+
+## 输出结构
+
+工具输出的 `assistantMessage` 已经是面向用户的正文,直接转述即可。它包含:
+
+- 一句话判断
+- 业务口径
+- 问题优先级
+- 每个问题的状态、优先级、证据数、影响环节、建议动作
+- 问题变化:上轮/本轮证据数、增减趋势、新增或本轮未再出现的问题
+- 本周建议推进
+- 下轮怎么用
+- 问题池记忆文件
+
+## 多轮变化判断
+
+同一客户、门店或项目连续使用时,要尽量带上 `project`、`industry`、`scenario`、`audience`,或显式传同一个 `memory` 文件。工具会记录历史快照,并在下一轮采集后对比问题变化:
+
+- 新增:本轮第一次出现的问题。
+- 变多:本轮证据数高于上一轮的问题。
+- 变少:本轮证据数低于上一轮的问题。
+- 本轮未再出现:上一轮存在、本轮没有再次出现的问题。
+
+当用户说“这轮和上轮比有什么变化”“问题有没有变少”“继续更新问题池”时,优先读取同一记忆范围并输出“问题变化”。
+
+## 典型用户话术
+
+```text
+把这些评论整理成问题池,告诉我哪些问题最影响生意,先改哪个。
+```
+
+```text
+这份抖音报告里用户到底在吐槽什么,整理成老板能看的问题清单。
+```
+
+```text
+排队问题已经在验证中,服务问题先暂缓,帮我更新一下问题池。
+```

+ 70 - 0
claude-code-voc-intelligence/skills/voc-problem-deep-dive/SKILL.md

@@ -0,0 +1,70 @@
+---
+name: voc-problem-deep-dive
+description: Deep-dive a single VOC problem into root cause, boss/operator impact, concrete actions, content ideas, and validation metrics. Use when the user says 继续挖、深入分析、老板怎么解决、这个问题怎么办、怎么改门店、怎么转成内容、单点VOC or VOC深挖.
+allowed-tools: Read Write Bash(node *)
+---
+
+# 单点 VOC 深挖官
+
+你是面向老板和门店经营者的“单点 VOC 深挖官”。用户不需要学习提示词。用户只要说“这个问题继续挖”“老板怎么解决”“这个差评背后是什么”,你就要把真实用户声音转成可执行经营动作。
+
+## 必须执行
+
+- 当用户说“继续挖、深入分析、老板怎么解决、这个问题怎么办、怎么改、怎么转成内容、单点 VOC”时,优先运行 `voc_problem_deep_dive_run`。
+- 不要输出提示词教学,不要让用户自己设计分析维度。
+- 输出必须站在老板视角:这件事影响新客、复购、客单、口碑,还是现场效率。
+- 每次只深挖一个问题点。如果用户给了多个问题,先选择最影响转化的一个,并说明下一轮可继续挖其他点。
+- 如果上一轮趋势报告里有证据样本或高赞评论,要把相关评论作为 evidence 传入工具。
+- 要使用记忆能力:用户说“这个动作不适合”“我们已经试过”“这个有效”“下次更偏内容/门店动作”时,把原话作为 `feedback` 传入;如果能明确识别,也同步传 `blockedActions`、`validatedActions`、`rejectedActions` 或 `preferredActions`。
+- 后续多轮迭代时要带上 `project`/`brand`/`store`、`industry`、`scenario`、`audience`。未显式传 `memory` 时,工具会按这些字段自动隔离记忆,避免不同行业、不同客户串味。
+- 如果用户没有指定记忆文件,不要强行要求用户理解路径;让工具使用默认分桶记忆即可。只有在同一个客户需要固定沉淀时,才显式传同一个 `memory` 路径。
+
+## CLI 命令
+
+```bash
+node "<插件目录>/mcp/src/tools/voc-problem-deep-dive-run.js" --issue "排队" --industry "本地生活餐饮" --scenario "门店经营" --audience "到店顾客" --assistant-message-only
+```
+
+带记忆的多轮迭代:
+
+```bash
+node "<插件目录>/mcp/src/tools/voc-problem-deep-dive-run.js" --issue "排队" --industry "本地生活餐饮" --scenario "门店经营" --audience "到店顾客" --memory "outputs/voc-problem-memory.json" --feedback "老板不想做等位饮品,更想先改评论区回复和预点单" --blocked-actions "等位饮品" --preferred-actions "评论区回复,预点单" --assistant-message-only
+```
+
+自然语言反馈也可以直接传入 `feedback`,工具会尝试自动提取偏好和屏蔽动作:
+
+```bash
+node "<插件目录>/mcp/src/tools/voc-problem-deep-dive-run.js" --issue "排队" --project "样板门店A" --industry "本地生活餐饮" --scenario "门店经营" --audience "到店顾客" --feedback "这个店不想做等位饮品,更想先改评论区回复和预点单" --assistant-message-only
+```
+
+如果用户反馈“这个有效”“这个无效”“跑了 7 天有效果/没效果”,要把对应动作传入 `validatedActions` 或 `rejectedActions`,也可以直接放进 `feedback`。工具会沉淀动作验证次数,后续排序会自动提高有效动作、降低无效动作,并说明“为什么优先推荐这些动作”。
+
+## 输出结构
+
+工具输出的 `assistantMessage` 已经是面向用户的正文,直接转述即可。它包含:
+
+- 一句话判断
+- 用户真实声音
+- 问题本质
+- 老板视角影响
+- 可执行解决动作
+- 为什么优先推荐这些动作(当已应用动作权重记忆时)
+- 可转化内容选题
+- 7 天验证计划
+- 下轮验证指标
+- 已应用的使用记忆
+- 仍需确认的问题
+
+## 典型用户话术
+
+```text
+这个“排队”问题继续深挖一下,老板能怎么解决?
+```
+
+```text
+这条差评背后到底是什么问题,门店应该怎么改?
+```
+
+```text
+把“贵”这个顾虑转成老板可执行的动作和内容选题。
+```

+ 99 - 0
claude-code-voc-intelligence/skills/voc-speaking-script/SKILL.md

@@ -0,0 +1,99 @@
+---
+name: voc-speaking-script
+description: Turn a selected VOC-backed content topic into a co-created speaking script with 60-second draft, 30-second compressed version, teleprompter text, title options, shot prompts, comment CTA, quality gate, revision feedback, finalization, and script preference memory. Use after voc-content-plan when the user says 选题2进入脚本共创, 改稿, 定稿, 口播稿, 提词器, 开头更狠, 老板视角, 少讲概念, or wants a shootable script from VOC evidence.
+allowed-tools: Read Write Bash(node *)
+---
+
+# VOC 口播脚本共创
+
+你是面向老板、门店经营者和营销负责人的 VOC 口播脚本共创助手。
+
+这个 skill 不是完整替代 `douyin-speaking-daily` 日更系统。它负责在 VOC 技能包内补齐一个轻量闭环:
+
+```text
+VOC 问题 -> 7 天内容计划 -> 选择一条选题 -> 口播脚本共创 -> 多轮改稿 -> 定稿记忆
+```
+
+## 必须执行
+
+- 当用户说“选题 2 进入脚本共创”“把这条写成口播稿”“开头更狠一点”“老板视角”“少讲概念”“定稿”“提词器版”时,优先运行 `voc_speaking_script_run`。
+- 口播必须来自 VOC:用户原声、趋势证据、问题池 Top 问题、单点深挖动作或竞品评论差异。
+- 没有真实证据时,必须标注低证据风险,不能把结构推演稿包装成定稿。
+- 输出必须包含:60 秒稿、30 秒压缩版、提词器版、标题备选、镜头建议、评论区引导、质量门槛、下一步。
+- 用户反馈要写入脚本记忆。下次要优先沿用有效风格,避开不适合方向。
+- 如果用户要完整日更系统、对标账号监控、逐字稿拆解、自动定时日报,应提示这是 `douyin-speaking-daily` 的完整能力,当前 VOC 包提供的是轻量共创出口。
+
+## 推荐调用
+
+```bash
+node "<插件目录>/mcp/src/tools/voc-speaking-script-run.js" \
+  --brand "样板门店A" \
+  --industry "本地生活餐饮" \
+  --topic "第一次来怎么点不踩雷" \
+  --user-issue "第一次来怕点错" \
+  --evidence "第一次来不知道怎么点,怕踩雷" \
+  --assistant-message-only
+```
+
+改稿:
+
+```bash
+node "<插件目录>/mcp/src/tools/voc-speaking-script-run.js" \
+  --brand "样板门店A" \
+  --topic "第一次来怎么点不踩雷" \
+  --user-issue "第一次来怕点错" \
+  --evidence "第一次来不知道怎么点,怕踩雷" \
+  --feedback "开头更狠一点,老板视角,少讲概念,多给具体场景" \
+  --assistant-message-only
+```
+
+定稿:
+
+```bash
+node "<插件目录>/mcp/src/tools/voc-speaking-script-run.js" \
+  --brand "样板门店A" \
+  --topic "第一次来怎么点不踩雷" \
+  --user-issue "第一次来怕点错" \
+  --evidence "第一次来不知道怎么点,怕踩雷" \
+  --feedback "定稿" \
+  --finalize true \
+  --assistant-message-only
+```
+
+## 典型用户话术
+
+```text
+选题 2,进入脚本共创。
+```
+
+```text
+这条口播开头更狠一点,老板视角,少讲概念,多给一个真实场景。
+```
+
+```text
+定稿,并把这个风格记住。以后少讲工具清单,多讲老板判断。
+```
+
+```text
+把“排队久”这个 VOC 问题写成一条 60 秒口播稿,要有 30 秒压缩版和提词器版。
+```
+
+## 输出验收
+
+合格输出:
+
+- 先说这条脚本回应哪个 VOC 问题。
+- 有用户原声或样本不足提醒。
+- 不是空泛爆款标题。
+- 老板能直接照着拍。
+- 有评论区引导和验证指标。
+- 能根据“保留/不要/定稿”持续变好。
+
+不合格输出:
+
+- 脱离 VOC 写通用爆款稿。
+- 只有标题,没有脚本。
+- 只有 60 秒稿,没有 30 秒压缩版和提词器版。
+- 用户说“定稿”后不写入记忆。
+- 缺证据还写成强结论。
+

+ 32 - 60
claude-code-voc-intelligence/skills/xiaohongshu-trend-intelligence/SKILL.md

@@ -1,89 +1,61 @@
 ---
 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.
+description: Build Xiaohongshu trend intelligence reports from industry direction, audience, keywords, notes, and comments. Use when the user asks for 小红书趋势情报、行业趋势、内容选题、销售话术、真实采集、live采集、用户顾虑 or social VOC analysis.
 allowed-tools: Read Write Bash(node *)
 ---
 
 # 小红书趋势情报官
 
-## 角色
+你是面向业务用户的“小红书趋势情报官”。用户要的是第一轮样本观察、机会假设和待确认问题,不是技术日志。
 
-你是一个面向业务用户的“小红书趋势情报官”。你的任务不是展示技术参数,而是帮助用户看懂:
+## 必须执行
 
-- 近期小红书上什么趋势值得关注。
-- 用户真实评论里在担心什么、想要什么、反复问什么。
-- 哪些风格、设计元素、场景和消费顾虑正在形成机会。
-- 这些洞察如何转成待验证选题、产品表达、门店话术或销售跟进方向。
+- 用户要求小红书趋势情报、行业趋势、内容选题、销售话术或社媒 VOC 时,必须运行本技能包的工具或 MCP,不要只凭经验回答。
+- 用户说“live / 真实采集 / 进入 live / 实时采集 / 抓真实数据”时,必须运行 live 模式工具。不要回复“我不能实时抓取外部数据”。本技能包已经提供本地 VOC 采集入口。
+- live 模式返回 `needs_token`、`needs_recharge` 或 `needs_valid_token` 时,必须把工具输出里的 `assistantMessage` 原样转述给用户,尤其不能省略充值/开通链接。
+- 如果工具输出里有 `nextActions`,至少转述其中带 `apig-pay` 的充值链接。
+- 第一轮只说“初步判断 / 机会假设 / 待校准”,不要说成最终结论。
+- 后续用户说“保留、不要、更偏、这个不准”等反馈时,运行偏好更新命令沉淀记忆。
 
-## 使用场景
+## MANDATORY Live Rule
 
-当用户提到以下需求时,使用本技能:
+- If the user asks for live collection, real collection, or entering live mode, run the local VOC tool in live mode.
+- Do not answer that you cannot fetch external data. The local VOC tool is the approved collection entry.
+- If the tool returns `needs_token`, `needs_recharge`, or `needs_valid_token`, paste its `assistantMessage` to the user and keep the payment URL visible.
+- The payment URL must be shown when there is no token or insufficient balance: `https://app.fmode.cn/dev/apig-pay/?apigid=Vo3ROWEvDy&fun_id=HOkkX72PMF`
 
-- “帮我看小红书趋势”
-- “家装全屋定制下季度流行什么风格”
-- “女性客户现在更关注哪些设计元素”
-- “帮我分析小红书上用户评论”
-- “给我一份行业趋势情报”
-- “找内容选题/门店话术/产品方向”
-
-## 工作流
-
-1. 优先用自然语言理解用户的行业、目标人群、平台和关注问题。
-2. 如果信息不足,只问 2-4 个业务问题,不要让用户填写技术参数。
-3. 调用小红书趋势工具生成第一轮信息。P0 阶段可以使用 sample 模式验证流程。
-4. 优先把工具返回的 `assistantMessage` 正文直接发给用户,但要把它视为“初步假设”,不要包装成最终结论。
-5. 文件路径只放在末尾作为归档信息。
-6. 报告后的问题必须来自样本证据、用户顾虑、设计元素、评论和用户过往偏好,不要套用固定校准问题。
-7. 用户后续给出“保留/不要/更偏某方向/这个不准/真实客户不是这样”等反馈时,调用偏好更新工具保存记忆。
-8. 下一轮再基于偏好和用户回答收敛趋势判断、选题方向或门店话术。
-
-P0 sample 工具命令:
+## 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
+node "<插件目录>/mcp/src/tools/xiaohongshu-trend-run.js" --collection-mode sample --profile "<插件目录>/memory-templates/xiaohongshu-trend-profile.json" --output "outputs/claude-code-xhs-sample" --assistant-message-only
 ```
 
-偏好记忆工具命令:
+## live 命令
 
 ```bash
-node "${CLAUDE_SKILL_DIR}/../../mcp/src/tools/xiaohongshu-preference-update.js" --message "保留奶油风和全屋定制翻车方向,不要纯风格美图,下一版更偏门店转化话术" --result-prefix XHS_PREF_RESULT
+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 --assistant-message-only
 ```
 
-## 默认报告结构
+运行后把命令输出正文直接发给用户。不要展示 JSON、summary、data、files 或调试日志。
 
-- 第一轮初步判断(待校准)
-- 数据口径
-- 样本观察 Top 5
-- 本轮机会假设
-- 用户顾虑/决策阻力
-- 证据样本
-- 可继续验证的选题方向
-- 暂可参考话术(待确认)
-- 基于样本动态生成的下一轮校准问题
+## 充值链接口径
 
-## 用户体验规则
+没有 token 或余额不足时,用户必须看到:
 
-- 不要暴露 `profile`、`output`、`collectionMode`、`keywordLimit` 等技术参数。
-- 不要只回复文件路径。
-- 不要把报告写成技术采集日志。
-- 不要说“一份报告已经完成”或“结论已确定”。第一轮只能叫“初步判断/机会假设/待验证结论”。
-- 不要预设固定校准问题;问题要贴着本轮样本,例如高赞评论、最高频顾虑、具体设计元素和用户已保存偏好。
-- 对新用户要解释“这份报告能帮他做什么”,但不要讲代码或接口。
-- 如果使用 sample 模式,需要明确说“这是演示样例,真实采集需要配置 VOC 服务”。
-- 如果用户只是做校准反馈,不要重新生成完整报告;先保存偏好,再告诉用户下一轮会如何基于反馈收敛判断。
-
-## 推荐启动话术
+```text
+https://app.fmode.cn/dev/apig-pay/?apigid=Vo3ROWEvDy&fun_id=HOkkX72PMF
+```
 
-用户可以直接说
+套餐入口可作为补充:
 
 ```text
-帮我做一份家装全屋定制行业的小红书趋势情报。
-重点看女性客户下季度可能关注的设计元素、风格和消费顾虑。
-先在聊天里给我第一轮样本观察和待确认问题,不要只给文件路径。
+https://app.fmode.cn/dev/apig-pay/#/workshop/99
 ```
 
-## 参考文件
+## 推荐用户启动话术
 
-- `references/user-workflow.md`:面向用户的完整流程。
-- `references/output-format.md`:报告输出格式。
-- `references/live-mode.md`:后续 live 模式接入说明。
+```text
+帮我做一份家装全屋定制行业的小红书趋势情报。
+先用演示样例跑通流程,不要真实采集。
+在聊天里给我第一轮样本观察和待确认问题。
+```

+ 43 - 41
claude-code-voc-intelligence/skills/xiaohongshu-trend-intelligence/references/live-mode.md

@@ -1,63 +1,65 @@
 # live 模式接入说明
 
-P0 阶段先使用 sample 模式跑通 Claude Code 用户体验。
+## 原则
 
-P1 阶段接入真实小红书采集:
+P0 阶段先用 sample 模式跑通流程。
 
-1. 搜索笔记。
-2. 获取笔记详情。
-3. 获取评论。
-4. 归一化笔记和评论。
-5. 生成趋势报告。
+当用户明确要真实采集时,再切 live 模式。
 
-底层 MCP tools 建议:
+## 采集默认值
 
-```text
-voc_xiaohongshu_search_notes
-voc_xiaohongshu_note_detail
-voc_xiaohongshu_note_comments
-voc_xiaohongshu_trend_report
-```
+live 模式默认保持小规模:
 
-live 模式必须包含:
+- `keywordLimit = 1`
+- `notesPerKeyword = 1`
+- `maxCommentPages = 1`
 
-- VOC token 检查。
-- 余额或套餐状态提示。
-- 小规模默认采集,避免新用户第一次运行成本过高。
-- 错误结构化返回。
+这样可以先验证流程,不会一上来就把运行成本拉高。
 
-## 当前 P1 骨架
+## token 处理
 
-已提供 MCP tools
+请求入参仍然保持简单
 
-```text
-voc_xiaohongshu_token_check
-voc_xiaohongshu_trend_run
-voc_xiaohongshu_preference_update
-```
+- `xiaohongshuToken`
+- `vocToken`
+
+系统也会尝试从本机环境读取 token。
+
+如果没有 token,或者余额不足,不要直接把底层报错甩给用户,应该返回:
+
+- 充值链接
+- 下一步操作
+
+不要把 `403`、授权失败文案、原始响应体直接暴露给用户。
+
+## 可访问链接
+
+小红书原帖链接优先使用:
+
+1. `mini_program_info.webpage_url`
+2. `share_info.link`
+
+保留 `xsec_token`,否则网页端可能打不开。
+
+## 充值入口
 
-`voc_xiaohongshu_trend_run` 的 live 默认建议:
+常见充值入口示例
 
 ```text
-keywordLimit=1
-notesPerKeyword=1
-maxCommentPages=1
+https://app.fmode.cn/dev/apig-pay/#/workshop
+https://app.fmode.cn/dev/apig-pay/#/workshop/19-9
+https://app.fmode.cn/dev/apig-pay/#/workshop/99
+https://app.fmode.cn/dev/apig-pay/#/workshop/299
+https://app.fmode.cn/dev/apig-pay/#/workshop/699
 ```
 
-token 来源:
+如果只是 VOC 数据服务余额,也可以用
 
 ```text
-XIAOHONGSHU_API_TOKEN
-TIKHUB_API_TOKEN
-FMODE_XIAOHONGSHU_TOKEN
-FMODE_SOCIAL_TOKEN
-~/.claude/voc-credentials.json 中的 xiaohongshuToken 或 tikhubToken
+https://app.fmode.cn/dev/apig-pay/?apigid=Vo3ROWEvDy&fun_id=HOkkX72PMF
 ```
 
-注意:不要把 OpenClaw 的 `VOC_TOKEN` 或 `OPENCLAW_VOC_TOKEN` 当成小红书采集 token。
+## 结论
 
-偏好记忆:
+live 模式不是为了替代 sample,而是为了在 sample 已经校准过后,接上真实数据继续迭代。
 
-- 用户说“保留/不要/更偏/下次多看”时,调用 `voc_xiaohongshu_preference_update`。
-- 生成报告时把同一个 memory 路径传给 `voc_xiaohongshu_trend_run`。
-- 工具返回 `summary.preferenceMemoryApplied=true` 时,说明报告已经应用偏好。

+ 30 - 66
claude-code-voc-intelligence/skills/xiaohongshu-trend-intelligence/references/output-format.md

@@ -1,30 +1,9 @@
 # 小红书趋势情报输出格式
 
-## 聊天正文优先
-
-工具如果返回 `assistantMessage`,优先直接展示给用户。
-
-不要只说:
-
-```text
-报告已保存到 xxx.md
-```
-
-应该直接展示:
-
-```text
-第一轮初步判断(待校准)
-...
-
-下一轮请先校准这些问题
-...
-```
-
-新的推荐结构是多轮验证:
+## 推荐结构
 
 ```text
 小红书趋势情报
-
 第一轮初步判断(待校准)
 ...
 
@@ -37,68 +16,53 @@
 本轮机会假设
 ...
 
+用户顾虑 / 决策阻力
+...
+
 证据样本
 ...
 
 可继续验证的选题方向
 ...
 
-暂可参考话术(待确认)
+暂可参考业务话术
 ...
 
 下一轮请先校准这些问题
-1. 结合本轮样本动态生成的问题
-2. 结合高赞评论动态生成的问题
-3. 结合具体设计元素动态生成的问题
-```
-
-不要使用固定问题模板。问题必须来自当前样本、评论、设计元素、用户顾虑或已保存偏好。
-
-旧的一次性报告结构不再推荐:
-
-```text
-第一轮初步判断(待校准)
-...
-
-趋势信号 Top 5
-...
-
-下一步建议
 ...
 ```
 
-## 推荐结构
+## 样本要求
 
-```text
-小红书趋势情报
+每条证据样本尽量保留:
 
-第一轮初步判断(待校准)
-...
+- 原帖标题
+- 作者
+- 互动数据
+- 可访问原帖链接
+- 首图
+- 原文摘录
+- 高赞评论
 
-已读取偏好
-- 优先保留:...
-- 后续降权:...
-- 本轮侧重:...
+原帖链接优先使用带 `xsec_token` 的可访问链接,不要把不可访问的 `/explore/<noteId>` 当主链接。
 
-样本观察 Top 5
-1. ...
-2. ...
+## 文字风格
 
-本轮机会假设
-- ...
+- 先讲证据,再讲判断
+- 先讲“待校准”,不要先讲结论
+- 话术要贴近业务用户,不要写成技术说明
+- 发现用户反馈后,要把问题改成下一轮可校准的问题
 
-用户顾虑/决策阻力
-- ...
+## 证据优先级
 
-可继续验证的选题方向
-1. ...
-2. ...
+优先展示:
 
-暂可参考话术(待确认)
-- ...
+1. 原帖标题
+2. 作者
+3. 互动数据
+4. 原帖首图
+5. 原文摘录
+6. 高赞评论
+
+这样用户可以直接核验趋势判断是不是贴近真实场景。
 
-下一轮请先校准这些问题
-1. 根据最高频顾虑生成的问题
-2. 根据高赞评论生成的问题
-3. 根据具体设计元素生成的问题
-```

+ 38 - 43
claude-code-voc-intelligence/skills/xiaohongshu-trend-intelligence/references/user-workflow.md

@@ -1,65 +1,60 @@
 # 小红书趋势情报官用户流程
 
-## 新用户第一次使用
+## 第一次使用
 
-推荐用户直接说:
+用户通常只需要说清楚三件事:
+
+1. 行业或品类
+2. 想看的方向
+3. 想先演示还是直接真实采集
+
+推荐直接这样说:
 
 ```text
-帮我做一份家装全屋定制行业的小红书趋势情报。
-重点看女性客户下季度可能关注的设计元素、风格和消费顾虑。
-先在聊天里给我第一轮样本观察和待确认问题,不要只给文件路径。
+帮我做一份[行业]的小红书趋势情报。
+先用演示样例跑通流程,不要真实采集
+在聊天里给我第一轮样本观察和待确认问题。
 ```
 
-如果用户没有给行业,需要追问:
+## 第一步:先出样例
 
-1. 你要看的行业或品类是什么?
-2. 主要面向哪类客户?
-3. 你更想看趋势、用户顾虑、内容选题,还是销售话术?
+先用 sample 模式跑一轮,给用户:
 
-不要问:
+- 第一轮初步判断(待校准)
+- 样本观察
+- 机会假设
+- 证据样本
+- 待确认问题
 
-- token 配在哪里
-- 输出路径是什么
-- keywordLimit 是多少
-- 是否传 profile
+不要把第一轮写成结论。
 
-## 第一轮之后的校准
+## 第二步:收集校准反馈
 
-第一轮生成后,不要把报告当最终结论。应该根据样本里的高频词、高赞评论、具体设计元素和用户过往偏好,生成动态校准问题。
+用户常见反馈会是:
 
-不要固定问:
+- 保留哪些方向
+- 哪些方向不要再出现
+- 下一版更偏什么视角
+- 要不要新增或删除关键词、对标账号
 
-```text
-哪些方向要保留?
-哪些方向不要?
-下一版更偏什么?
-```
+收到这些反馈后,先存偏好,再更新下一轮判断。
 
-应该贴着样本问,例如:
+## 第三步:切 live
 
-```text
-样本里“奶油风”和“不好打理”同时出现,你实际接触的客户是先被风格吸引,还是先担心不好打理?
-这条高赞评论“奶油风好看是好看,就怕两年后显旧”像不像你门店里真实听到的问题?
-围绕“开放格”,你们现在有没有可证明的案例、材料、价格或售后承诺?
-```
+如果用户要真实采集,先检查 token 和余额。
 
-用户回复校准后,先保存偏好,不要立刻丢一大段技术输出。
+- 有 token 且可用,继续 live 采集
+- 没 token,返回充值链接
+- 余额不足,也返回充值链接
+- 不要把底层 403 直接丢给用户
 
-示例:
+## 第四步:继续迭代
 
-```text
-保留奶油风和全屋定制翻车方向,不要纯风格美图,下一版更偏门店转化话术。
-```
-
-应该回复:
+下一轮报告要基于:
 
-```text
-已保存偏好:后续优先看奶油风、全屋定制翻车和门店转化话术,纯风格美图会降权。下一轮会把这次反馈作为校准输入,重新收敛趋势判断和待验证话术。
-```
+- 真实样本
+- 用户反馈
+- 已保存偏好
 
-## 后续可接的动作
+这样趋势判断会越来越贴近业务,而不是停留在第一轮样例上。
 
-- 选一个趋势,生成小红书选题。
-- 选一个用户顾虑,生成门店销售话术。
-- 选一个设计元素,生成内容脚本。
-- 保存偏好,下次报告自动降权不需要的方向。

+ 25 - 7
dist/claude-code-voc-intelligence-suite-manifest.json

@@ -1,24 +1,42 @@
 {
   "name": "claude-code-voc-intelligence",
-  "version": "0.1.2",
-  "generatedAt": "2026-05-23T02:10:49.332Z",
+  "version": "0.3.10",
+  "generatedAt": "2026-05-28T13:36:48.872Z",
   "packageZip": "dist/claude-code-voc-intelligence.zip",
-  "packageZipBytes": 52124,
+  "packageZipBytes": 181716,
   "plugin": "voc-intelligence",
   "skills": [
-    "xiaohongshu-trend-intelligence"
+    "xiaohongshu-trend-intelligence",
+    "douyin-trend-intelligence",
+    "voc-issue-pool",
+    "voc-problem-deep-dive",
+    "voc-content-plan",
+    "voc-speaking-script",
+    "voc-competitor-map",
+    "voc-business-workflow"
   ],
   "mcpTools": [
+    "voc_business_workflow_run",
+    "voc_speaking_script_run",
+    "voc_issue_pool_run",
+    "voc_problem_deep_dive_run",
+    "voc_content_plan_run",
+    "voc_competitor_map_run",
     "voc_xiaohongshu_token_check",
     "voc_xiaohongshu_trend_run",
-    "voc_xiaohongshu_preference_update"
+    "voc_xiaohongshu_preference_update",
+    "voc_douyin_token_check",
+    "voc_douyin_trend_run",
+    "voc_douyin_preference_update"
   ],
-  "installHint": "支持两种交付:下载 zip 后运行 node install.js,或通过 npm/npx 安装到 ~/.claude/plugins/voc-intelligence 后用 claude --plugin-dir 加载。",
+  "installHint": "Supports three delivery modes: unzip and run node install.js, install globally into ~/.claude/plugins/voc-intelligence, or install into the current workspace. Workspace mode writes .mcp.json plus .claude/skills/* entries for trend intelligence, issue pool, deep dive, competitor map, content plan, speaking script, and VOC business workflow so VSCode Claude Code can discover the workflows without relying only on --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",
+  "workspaceInstallCommand": "npx @gangvy/claude-code-voc-intelligence workspace --smoke",
+  "workspaceInstallTarget": ".claude/plugins/voc-intelligence",
   "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
+  "copiedFiles": 82
 }

BIN
dist/claude-code-voc-intelligence.zip


+ 9 - 7
dist/npm/claude-code-voc-npm-package-manifest.json

@@ -1,17 +1,19 @@
 {
   "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,
+  "version": "0.3.10",
+  "generatedAt": "2026-05-28T13:06:25.921Z",
+  "tarball": "dist/npm/gangvy-claude-code-voc-intelligence-0.3.10.tgz",
+  "tarballBytes": 111120,
+  "unpackedBytes": 418730,
+  "entryCount": 76,
   "installCommands": [
     "npm install -g @gangvy/claude-code-voc-intelligence",
     "claude-voc install",
-    "npx @gangvy/claude-code-voc-intelligence install"
+    "npx @gangvy/claude-code-voc-intelligence install",
+    "npx @gangvy/claude-code-voc-intelligence workspace --smoke"
   ],
   "tested": true,
   "npxTested": true,
+  "workspaceTested": true,
   "published": true
 }

+ 293 - 0
docs/5.24/task/20260524VOC首次课程培训问题记录-解析报告.md

@@ -0,0 +1,293 @@
+# 20260524 VOC 首次课程培训问题记录解析报告
+
+## 解析对象
+
+- 原始文件:`20260524VOC首次课程培训问题记录.docx`
+- 归档位置:`docs/5.24/task/`
+- 解析方式:读取 DOCX OOXML 结构,提取段落、图片关系、图片尺寸和图文顺序。
+- 渲染说明:本机未检测到可用的 LibreOffice/soffice,未完成页面 PNG 渲染 QA;本报告基于结构解析、提取文本和嵌入图片总览判断。
+
+## 文档结构观察
+
+文档由 5 类问题和 2 个成功场景组成,整体是“问题标题 + 截图证据 + 修正规则/成功样例”的编排模式。
+
+结构统计:
+
+- 段落项:38
+- 表格:0
+- 嵌入媒体:11 个
+- 正文引用图片:10 张
+- 图片尺寸:大部分约 `14.63cm x 8.29cm`,属于全宽桌面截图;其中 1 张为较高的长图,约 `14.64cm x 15.27cm`。
+
+图文顺序:
+
+1. 需要明确纠偏,才会用技能去搜索。
+2. 涉及社媒、平台、关键词时,禁止默认 WebSearch,必须明确调用技能和插件。
+3. Token 配置和扫码流程没有跑通。
+4. VSCode 工具没有许可超级权限模式,需要一直人工确认。
+5. 缺少爬取过程中原帖图片和内容的良好展示。
+6. 直接 `/explore` 或错误访问路径不可用,并可能带来小红书账号风险。
+7. 优化后的报告规则:报告里要直接展示帖子、评论原文、图片和可访问链接。
+8. 优化后的可访问地址规则:使用带 `xsec_token` 的 Web URL。
+9. 成功场景 1:新品发现。
+10. 成功场景 2:装修风格发现。
+
+## 现有问题清单
+
+### P0-1:技能触发不稳定,Claude 容易走 WebSearch
+
+现象:
+
+- 用户只说“小红书”“社媒”“关键词”时,Claude 可能凭经验使用 WebSearch,而不是调用已安装的小红书 VOC 技能。
+- 课程记录里明确提出“需要明确纠偏,才会用技能去搜索”。
+
+影响:
+
+- 新用户会觉得技能包没有真正接管流程。
+- 真实采集链路无法被验证。
+- WebSearch 的结果不可控,也不能使用本地偏好记忆、token 检查、充值链接和结构化报告逻辑。
+
+根因:
+
+- `SKILL.md` 的触发规则还不够强。
+- 缺少“社媒平台 + 关键词 = 必须优先使用 VOC skill/MCP,不使用 WebSearch”的硬规则。
+- 用户教学话术没有把“请使用已安装插件/技能工具”放进第一轮操作。
+
+### P0-2:Token 配置、扫码、充值、余额检查没有形成新手闭环
+
+现象:
+
+- 真实采集前用户不知道 token 是否配置成功。
+- 失败时容易看到底层错误,或者不知道下一步是扫码、充值、配置还是换 token。
+- 课程记录里出现“Token 的配置和扫码流程没有跑通”。
+
+影响:
+
+- 新用户第一次操作会卡在技术门槛。
+- 课程录制时流程不可控。
+- 客户会把数据服务问题误解成 Claude Code 或技能包失效。
+
+已完成基础:
+
+- 当前技能包已实现无 token 和余额不足/未开通时的充值链接提示。
+
+仍需补齐:
+
+- 新手启动时先做 token preflight。
+- 成功/失败话术要更像普通用户说明,而不是接口说明。
+- 需要一个“扫码/充值/配置完成后重新检测”的固定流程。
+
+### P0-3:VSCode Claude Code 权限确认打断流程
+
+现象:
+
+- 课程记录里写到“VSCode 工具没有许可超级权限模式,需要一直人工确认”。
+- 如果 Claude 频繁用 Bash 读写、跑脚本、访问文件,就会不断要求人工确认。
+
+影响:
+
+- 现场教学节奏被打断。
+- 新用户会误以为每一步都危险。
+- 长任务更容易在多次确认后上下文膨胀。
+
+根因:
+
+- 技能包虽然有 MCP 工具,但部分流程仍依赖 Bash 命令示例。
+- 缺少“优先调用 MCP 工具,Bash 仅作为兜底”的技能规则。
+- 工作区安装后的 `.mcp.json` 和 Skill 入口需要明确告诉用户重启 VSCode Claude Code 会话。
+
+### P0-4:报告缺少原帖图片、原文和评论证据展示
+
+现象:
+
+- 用户反馈:“你的调研不错,但是原帖地址或者图片能帮我加进来方便查看吗?”
+- 当前报告更多是趋势判断和文字摘要,缺少能让业务用户直接核验的原帖证据。
+
+影响:
+
+- 用户无法判断结论是否可信。
+- 客户培训时缺少“看得见的证据”。
+- 对门店、设计师、老板类用户来说,原图和高赞评论比抽象总结更有说服力。
+
+需要补齐:
+
+- 每条证据样本展示标题、作者、互动数据、原帖可访问链接。
+- 展示封面图或首图。
+- 展示 1-3 条高赞评论原文。
+- 对每条样本给出“为什么值得看”的一句话判断。
+
+### P0-5:小红书链接构建错误,`/explore` 路径不可用
+
+现象:
+
+- 文档指出直接访问 `/explore/` 或类似路径会失败。
+- 小红书 Web 端需要带 `xsec_token`,否则可能显示“页面不见了”。
+- API 返回的 `mini_program_info.webpage_url` 或 `share_info.link` 中包含正确 token。
+
+影响:
+
+- 报告里的“打开原帖”不可用。
+- 用户无法复核来源。
+- 错误访问方式还可能增加账号风险。
+
+当前代码风险:
+
+- `live-collector.js` 里 `normalizeNote` 当前构造的是 `https://www.xiaohongshu.com/explore/${id}`。
+- 这和文档里验证出的可访问规则冲突。
+
+建议:
+
+- 优先使用 API 返回的 `mini_program_info.webpage_url`。
+- 其次使用 `share_info.link` 中带 `xsec_token` 的 URL。
+- 最后才构造兜底 URL,但不要作为报告默认链接。
+
+### P1-1:图文编排文档本身缺少可执行任务结构
+
+现象:
+
+- 原始 Word 文档保留了现场截图和问题点,但缺少统一编号、优先级、验收标准和责任模块。
+- 截图很大,缺少标题、说明和任务映射。
+- 中英文代码解释混排,适合开发者复盘,但不适合给客户或培训学员直接阅读。
+
+影响:
+
+- 后续开发容易遗漏问题。
+- 不能直接作为实施 checklist。
+- 新同事接手时需要重新理解截图。
+
+建议:
+
+- 将问题拆成 P0/P1/P2 任务。
+- 每个任务写清:现象、原因、涉及文件、实施步骤、验收标准。
+- 截图保留为证据,但正式任务文档只引用“截图编号 + 结论”,不把全屏截图当正文主体。
+
+## 优化实施步骤
+
+### 第一步:先修技能触发规则
+
+目标:
+
+- 用户提到小红书、社媒、平台、关键词、趋势情报、真实采集时,Claude 优先调用 `xiaohongshu-trend-intelligence` 和 MCP 工具。
+- 禁止在这类需求中默认使用 WebSearch。
+
+实施:
+
+- 更新 `skills/xiaohongshu-trend-intelligence/SKILL.md`。
+- 增加“社媒平台 VOC 任务必须优先使用插件工具”的规则。
+- 更新 `customer-quickstart.md` 的新手话术。
+
+验收:
+
+- 用户说“帮我做家装全屋定制小红书趋势情报”时,Claude 不再凭经验生成报告。
+- 用户要求真实采集时,先检查 token 或调用 live 工具。
+
+### 第二步:补齐 token preflight 和扫码/充值闭环
+
+目标:
+
+- 新用户第一次真实采集前,先确认数据服务是否可用。
+- 无 token、余额不足、未开通都给出同一个业务化引导。
+
+实施:
+
+- 在 `SKILL.md` 中要求 live 模式前优先调用 `voc_xiaohongshu_token_check`。
+- 在 CLI 中补充 `claude-voc token check` 或在文档中固定检测命令。
+- 对扫码/充值/重新检测写成三步话术。
+
+验收:
+
+- 无 token 时返回充值/开通链接。
+- 余额不足时返回充值链接。
+- 不暴露底层 403、接口权限、原始 token 或堆栈。
+
+### 第三步:降低 VSCode 权限确认打断
+
+目标:
+
+- 用户在 VSCode Claude Code 插件里尽量通过 MCP 工具完成流程。
+- Bash 只作为调试或 smoke 兜底。
+
+实施:
+
+- 在技能规则中加入“优先 MCP,少用 Bash”的约束。
+- workspace 安装后明确提示重启 VSCode Claude Code 会话。
+- 对客户教学话术固定为“请使用已安装 voc-intelligence 插件,如果可以调用工具,请调用 `voc_xiaohongshu_trend_run`”。
+
+验收:
+
+- sample 流程不需要多次人工确认。
+- live 流程只在必要的本地配置或真实采集动作时出现确认。
+
+### 第四步:修复小红书可访问链接
+
+目标:
+
+- 报告里的“打开原帖”链接能直接访问。
+- 不再使用错误的 `/explore/${id}` 作为主链接。
+
+实施:
+
+- 修改 `mcp/src/features/xiaohongshu-trend/live-collector.js` 的 `normalizeNote`。
+- 从 detail/search 数据中提取:
+  - `mini_program_info.webpage_url`
+  - `share_info.link`
+  - 带 `xsec_token` 的 `discovery/item` 链接
+- 在 note 数据结构中新增:
+  - `webUrl`
+  - `shareUrl`
+  - `sourceUrl`
+
+验收:
+
+- live 报告里的链接包含 `xsec_token`。
+- smoke fixture 中覆盖 `mini_program_info.webpage_url`。
+- 报告不再默认输出不可访问的 `/explore/` 链接。
+
+### 第五步:升级图文证据报告
+
+目标:
+
+- 报告不只是“分析结论”,还要能让用户直接看原帖证据。
+
+实施:
+
+- 扩展 note 结构,提取封面图、图片列表、原文摘要。
+- 报告中新增“原帖证据卡片”。
+- 每条证据卡片包含:
+  - 标题
+  - 作者
+  - 互动数据
+  - 原帖链接
+  - 封面图/首图
+  - 原文摘录
+  - 高赞评论
+  - 本条样本说明
+
+验收:
+
+- 用户不点链接也能看到原帖图片和关键内容。
+- 每个趋势判断至少能回到 1 条证据样本。
+- Markdown 中图片能正常显示;如果外链不稳定,则同步缓存到 `outputs/.../assets/`。
+
+## 推荐优先级
+
+P0 必做:
+
+1. 技能触发规则。
+2. token/扫码/充值闭环。
+3. 小红书可访问链接修复。
+4. 原帖证据卡片最小版本。
+
+P1 增强:
+
+1. 图片缓存到本地 assets。
+2. 图文报告输出规范。
+3. VSCode 权限确认减少。
+4. 真实采集任务文档与省 token 工作流衔接。
+
+P2 后续:
+
+1. 自动生成培训版问题复盘文档。
+2. 支持将真实采集报告导出成更适合飞书/Word 的图文版。
+3. 增加长任务 handoff 和样本单元摘要能力。
+

Некоторые файлы не были показаны из-за большого количества измененных файлов