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

feat(voc): complete balance and cost-control workflow

gangvy 2 месяцев назад
Родитель
Сommit
a6d5eaef73
29 измененных файлов с 870 добавлено и 169 удалено
  1. 10 0
      claude-code/claude-code-voc-intelligence/.npmignore
  2. 21 2
      claude-code/claude-code-voc-intelligence/README.md
  3. 5 2
      claude-code/claude-code-voc-intelligence/SKILL.md
  4. 42 61
      claude-code/claude-code-voc-intelligence/bin/claude-voc.js
  5. 37 1
      claude-code/claude-code-voc-intelligence/docs/capability-map.md
  6. 2 2
      claude-code/claude-code-voc-intelligence/docs/npm-packaging-notes.md
  7. 378 14
      claude-code/claude-code-voc-intelligence/docs/voc-cost-metering-and-balance-implementation-spec.md
  8. 13 13
      claude-code/claude-code-voc-intelligence/install.js
  9. 5 0
      claude-code/claude-code-voc-intelligence/mcp/src/core/api-catalog.js
  10. 11 4
      claude-code/claude-code-voc-intelligence/mcp/src/core/billing-client.js
  11. 15 11
      claude-code/claude-code-voc-intelligence/mcp/src/core/credentials.js
  12. 2 2
      claude-code/claude-code-voc-intelligence/mcp/src/core/payment-links.js
  13. 62 4
      claude-code/claude-code-voc-intelligence/mcp/src/core/recharge-client.js
  14. 3 3
      claude-code/claude-code-voc-intelligence/mcp/src/features/fmode-image-analysis/image-analysis.js
  15. 6 6
      claude-code/claude-code-voc-intelligence/mcp/src/tools/douyin-trend-run.js
  16. 12 7
      claude-code/claude-code-voc-intelligence/mcp/src/tools/voc-api-catalog-run.js
  17. 6 6
      claude-code/claude-code-voc-intelligence/mcp/src/tools/xiaohongshu-trend-run.js
  18. 5 1
      claude-code/claude-code-voc-intelligence/package.json
  19. 4 12
      claude-code/claude-code-voc-intelligence/scripts/acceptance.js
  20. 153 0
      claude-code/claude-code-voc-intelligence/scripts/audit-index.js
  21. 27 3
      claude-code/claude-code-voc-intelligence/scripts/billing-client-smoke.js
  22. 7 3
      claude-code/claude-code-voc-intelligence/scripts/smoke-mcp.js
  23. 26 0
      claude-code/claude-code-voc-intelligence/scripts/smoke-voc-request-shaping.js
  24. 9 3
      claude-code/claude-code-voc-intelligence/skill-package-manifest.json
  25. 1 1
      claude-code/claude-code-voc-intelligence/skills/voc-api-catalog/SKILL.md
  26. 1 1
      claude-code/claude-code-voc-intelligence/skills/voc-api-catalog/references/error-codes.md
  27. 4 4
      dist/claude-code-voc-intelligence-suite-manifest.json
  28. BIN
      dist/claude-code-voc-intelligence.zip
  29. 3 3
      dist/npm/claude-code-voc-npm-package-manifest.json

+ 10 - 0
claude-code/claude-code-voc-intelligence/.npmignore

@@ -0,0 +1,10 @@
+# Local catalog snapshots are development artifacts, not package indexes.
+mcp/catalog/*.bak-*
+
+# Runtime-only material is already excluded by package files, keep it explicit.
+memory/
+output/
+outputs/
+.env
+.env.*
+.npmrc

+ 21 - 2
claude-code/claude-code-voc-intelligence/README.md

@@ -12,6 +12,8 @@
 - 单点 VOC 深挖:适合把一个具体问题转成老板能执行的经营动作、内容选题和 7 天验证计划。
 - VOC 内容选题:适合把用户问题转成 7 天短视频选题、口播脚本、镜头建议和验证指标。
 - VOC 口播脚本共创:适合把一条 VOC 选题继续打磨成 60 秒稿、30 秒稿、提词器版,并支持多轮改稿和定稿记忆。
+- VOC 接口目录:适合按平台、关键词和通道查找 1,344 个已登记接口,并读取参数后完成通用调用。
+- VOC 成本控制:适合执行前比较经济版/标准版/完整版,检查余额与预算,执行后查看接口频次和额度消耗。
 
 这套技能包不覆盖创始人故事、品牌自传或老板个人 IP 长文。口播脚本要做,但必须来自 VOC 用户问题、竞品评论差异、问题池 Top 问题或单点深挖动作。
 
@@ -36,6 +38,9 @@
 - “这个有效,后面优先按这个方向来”
 - “帮我看看顾客关心什么,告诉我先改哪里、下周发什么”
 - “跑一遍完整 VOC 经营闭环”
+- “这个复杂任务预计要花多少钱,经济版和完整版差多少”
+- “查一下这份报告每个接口调用了多少次、消耗了多少额度”
+- “查商品详情或评论,先从接口清单里找合适的接口”
 
 ## 能交付什么
 
@@ -111,6 +116,8 @@ npx --yes @vocmarket/voc-skill@latest workspace --smoke
 .claude/skills/voc-competitor-map/SKILL.md
 .claude/skills/voc-business-workflow/SKILL.md
 .claude/skills/fmode-image-analysis/SKILL.md
+.claude/skills/voc-api-catalog/SKILL.md
+.claude/skills/voc-cost-controller/SKILL.md
 .claude/plugins/voc-intelligence
 ```
 
@@ -188,6 +195,8 @@ npx --yes @vocmarket/voc-skill@latest install --smoke
 - `voc-competitor-map`
 - `voc-business-workflow`
 - `fmode-image-analysis`
+- `voc-api-catalog`
+- `voc-cost-controller`
 
 MCP 工具:
 
@@ -204,6 +213,12 @@ MCP 工具:
 - `voc_content_plan_run`
 - `voc_competitor_map_run`
 - `voc_speaking_script_run`
+- `voc_api_search`
+- `voc_api_doc`
+- `voc_api_call`
+- `voc_billing_query`
+- `voc_cost_estimate`
+- `voc_usage_report`
 
 命令行入口:
 
@@ -215,6 +230,7 @@ claude-voc image --image-path E:\workspace\example\screenshot.png --prompt "识
 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 audit:index
 npm --prefix claude-code-voc-intelligence run mcp:smoke
 npm --prefix claude-code-voc-intelligence run smoke:package
 ```
@@ -231,6 +247,9 @@ npm --prefix claude-code-voc-intelligence run smoke:package
 - `docs/live-manual-acceptance-checklist.md`
 - `docs/payment-package-links.md`
 - `docs/context-budget-implementation-plan.md`
+- `docs/voc-cost-metering-and-balance-implementation-spec.md`
+- `skills/voc-api-catalog/references/error-codes.md`
+- `skills/voc-cost-controller/references/workflow-cost-models.md`
 - `skills/voc-content-plan/references/content-framework.md`
 - `skills/voc-content-plan/references/speaking-script-patterns.md`
 - `skills/voc-competitor-map/references/competitor-framework.md`
@@ -271,10 +290,10 @@ sample 模式不需要 token。live 模式需要一把可用的 token——**两
 1. **NewAPI(fmode-api)计费 SK**(`sk-` 开头,优先):就是 Claude Code 的 `ANTHROPIC_AUTH_TOKEN`,装好技能包后通常**已经具备**,不必手动再配。读取来源:
    - 本次请求参数 `newapiToken` / `fmodeApiKey`
    - `.env.local` / 环境变量 `FMODE_API_KEY`、`NEWAPI_TOKEN`
+   - `~/.fmode/config.json`(FmodeStudio 保存路径)的 `newapiToken` / `fmodeApiToken` / `fmodeApiKey`
    - `~/.claude/voc-credentials.json` 的 `fmodeApiKey`
-   - `~/.fmode/config.json`(FmodeStudio 保存路径)的 `newapiToken` / `fmodeApiKey`
    - `~/.claude/settings.json` 的 `env.ANTHROPIC_AUTH_TOKEN`(`sk-` 开头、且 `ANTHROPIC_BASE_URL` 指向 fmode)
-2. **VOC-AI 数据服务会话 token**(`r:` 开头,回退):读取来源——本次请求参数、`.env.local` / 环境变量 `VOC_TOKEN`·`VOC_SOCIAL_TOKEN`、`~/.claude/voc-credentials.json` 的 `vocToken`·`token`
+2. **VOC-AI 数据服务会话 token**(`r:` 开头,回退):读取来源——本次请求参数、显式 `FMODE_SESSION_TOKEN` / `TIHAO_SESSION_TOKEN`、`~/.fmode/config.json` 的 `sessionToken`、`~/.claude/voc-credentials.json` 的 `sessionToken`·`vocToken`·`token`,最后回退 `.env.local` / 环境变量 `VOC_TOKEN`·`VOC_SOCIAL_TOKEN`
 
 同一网关 `server.fmode.cn/api/voc-social` 对上述任一有效 token 都接受 `Bearer`,**`sk-` 不是「类型错误」**。`voc_*_token_check` 与实际采集(`voc_api_call` / `voc_*_trend_run`)口径一致,会识别以上**全部**来源;不必在采集前强制先跑检测。
 

+ 5 - 2
claude-code/claude-code-voc-intelligence/SKILL.md

@@ -1,6 +1,6 @@
 ---
 name: voc-intelligence
-description: 面向经营者的 VOC 用户经营洞察工作台。从社交媒体、电商和海外选品数据中提取用户声音,形成趋势情报、问题池、竞品图谱、单点深挖、7 天内容计划和口播脚本。用户要看顾客关心什么、先改哪里、下周发什么、查商品价格评论或进行样例与真实采集时使用。
+description: 面向经营者的 VOC 用户经营洞察工作台。从社交媒体、电商和海外选品数据中提取用户声音,形成趋势情报、问题池、竞品图谱、单点深挖、7 天内容计划、口播脚本、成本预估和用量账单。用户要看顾客关心什么、先改哪里、下周发什么、查商品价格评论、测算任务成本、查询接口用量或进行样例与真实采集时使用。
 ---
 
 # VOC 用户经营洞察
@@ -13,7 +13,9 @@ description: 面向经营者的 VOC 用户经营洞察工作台。从社交媒
 - 用户没有明确要求真实采集时,先使用演示样例;用户明确说真实采集或 live 时,再检查凭证和余额。
 - 首轮输出采用“初步判断、机会假设、待校准”口径,并给出证据和下一步动作。
 - 用户要完整经营闭环时,调用 `voc_business_workflow_run`;要单项任务时,调用对应的趋势、问题池、深挖、竞品、内容或脚本工具。
-- 用户要查商品、价格、评论、销量或其他平台接口时,先用 `voc_api_search` 查清单,再用 `voc_api_doc` 读取参数,最后用 `voc_api_call` 调用。
+- 用户要查商品、价格、评论、销量或其他平台接口时,先用 `voc_api_search` 查清单,再用 `voc_api_doc` 读取参数,最后用 `voc_api_call` 调用;需要跨平台或未登记接口时,按返回的 `channel` 选择通道。
+- 用户要预估复杂任务成本、比较经济版/标准版/完整版或判断余额是否足够时,先调用 `voc_cost_estimate`;需要读取当前余额和服务端价目时使用 `voc_billing_query`。
+- 用户要查询已完成任务的实际调用频次、额度消耗、接口拆分或报告成本时,调用 `voc_usage_report`,并区分用户扣费与平台成本。
 - 采集响应中的多媒体统一下载到任务本地 `assets/` 并写入 `media-manifest.json`;报告只引用本地路径,技能包不向线上 Storage 上传。
 
 ## 主要能力
@@ -22,4 +24,5 @@ description: 面向经营者的 VOC 用户经营洞察工作台。从社交媒
 - VOC 问题池、单点问题深挖、竞品图谱和 7 天内容计划。
 - 基于用户声音的口播脚本共创、修改和定稿。
 - 淘宝/天猫、京东、1688、抖音电商等电商数据,以及亚马逊海外选品数据。
+- 通用 VOC 接口目录、跨通道接口调用、执行前成本测算、余额预检和执行后用量报告。
 - 图片、截图、海报、图表和商品图片分析。

+ 42 - 61
claude-code/claude-code-voc-intelligence/bin/claude-voc.js

@@ -249,7 +249,6 @@ function shouldCopyEntry(entry) {
 }
 
 function copyDirRecursive(srcDir, destDir) {
-  ensureDir(destDir);
   let count = 0;
   for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
     if (!shouldCopyEntry(entry)) continue;
@@ -258,6 +257,7 @@ function copyDirRecursive(srcDir, destDir) {
     if (entry.isDirectory()) {
       count += copyDirRecursive(src, dest);
     } else if (entry.isFile()) {
+      ensureDir(destDir);
       ensureDir(path.dirname(dest));
       fs.copyFileSync(src, dest);
       count++;
@@ -272,22 +272,15 @@ function assertInstalled(targetDir) {
     'install.js',
     '.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',
-    'skills/fmode-image-analysis/SKILL.md',
-    'skills/voc-api-catalog/SKILL.md',
-    'skills/voc-cost-controller/SKILL.md',
     'mcp/src/tools/fmode-image-analysis.js',
     'mcp/src/tools/voc-api-catalog-run.js',
     'mcp/catalog/voc-social-endpoints.json',
     'mcp/src/server.js'
   ];
+  const manifest = readJsonIfExists(path.join(targetDir, 'skill-package-manifest.json'));
+  for (const skillName of manifest.skills || []) {
+    required.push(`skills/${skillName}/SKILL.md`);
+  }
   const missing = required.filter(rel => !fs.existsSync(path.join(targetDir, rel)));
   if (missing.length) {
     throw new Error(`Missing required files in install target: ${missing.join(', ')}`);
@@ -395,13 +388,19 @@ function registerCodexGlobalMcp(targetDir) {
 
 function copyPackagedSkills(targetDir, destinationRoot) {
   const sourceRoot = path.join(targetDir, 'skills');
+  const manifest = readJsonIfExists(path.join(targetDir, 'skill-package-manifest.json'));
+  const skillNames = Array.isArray(manifest.skills) ? manifest.skills : [];
+  if (!skillNames.length) throw new Error('skill-package-manifest.json has no skills to install');
   ensureDir(destinationRoot);
   let copied = 0;
-  for (const entry of fs.readdirSync(sourceRoot, { withFileTypes: true })) {
-    if (!entry.isDirectory()) continue;
-    const destination = path.join(destinationRoot, entry.name);
+  for (const skillName of skillNames) {
+    const source = path.join(sourceRoot, skillName);
+    if (!fs.existsSync(source) || !fs.statSync(source).isDirectory()) {
+      throw new Error(`Manifest skill directory is missing: ${skillName}`);
+    }
+    const destination = path.join(destinationRoot, skillName);
     removeDirResilient(destination);
-    copied += copyDirRecursive(path.join(sourceRoot, entry.name), destination);
+    copied += copyDirRecursive(source, destination);
   }
   return copied;
 }
@@ -727,56 +726,38 @@ function workspaceBusinessWorkflowSkillText(targetDir) {
 }
 
 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');
-  const fmodeImageSkillDir = path.join(WORKSPACE_ROOT, '.claude', 'skills', 'fmode-image-analysis');
-  const apiCatalogSkillDir = path.join(WORKSPACE_ROOT, '.claude', 'skills', 'voc-api-catalog');
-  const costControllerSkillDir = path.join(WORKSPACE_ROOT, '.claude', 'skills', 'voc-cost-controller');
-  ensureDir(xhsSkillDir);
-  ensureDir(douyinSkillDir);
-  ensureDir(issuePoolSkillDir);
-  ensureDir(deepDiveSkillDir);
-  ensureDir(contentPlanSkillDir);
-  ensureDir(speakingScriptSkillDir);
-  ensureDir(competitorMapSkillDir);
-  ensureDir(businessWorkflowSkillDir);
-  ensureDir(fmodeImageSkillDir);
-  ensureDir(apiCatalogSkillDir);
-  ensureDir(costControllerSkillDir);
-  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');
-  fs.copyFileSync(path.join(targetDir, 'skills', 'fmode-image-analysis', 'SKILL.md'), path.join(fmodeImageSkillDir, 'SKILL.md'));
-  fs.copyFileSync(path.join(targetDir, 'skills', 'voc-api-catalog', 'SKILL.md'), path.join(apiCatalogSkillDir, 'SKILL.md'));
-  fs.copyFileSync(path.join(targetDir, 'skills', 'voc-cost-controller', 'SKILL.md'), path.join(costControllerSkillDir, 'SKILL.md'));
+  const manifest = readJsonIfExists(path.join(targetDir, 'skill-package-manifest.json'));
+  const skillNames = Array.isArray(manifest.skills) ? manifest.skills : [];
+  if (!skillNames.length) throw new Error('skill-package-manifest.json has no skills to expose in workspace');
+  const sourceRoot = path.join(targetDir, 'skills');
+  const writers = {
+    'xiaohongshu-trend-intelligence': () => workspaceSkillText(targetDir),
+    'douyin-trend-intelligence': () => workspaceDouyinSkillText(targetDir),
+    'voc-issue-pool': () => workspaceIssuePoolSkillText(targetDir),
+    'voc-problem-deep-dive': () => workspaceProblemDeepDiveSkillText(targetDir),
+    'voc-content-plan': () => workspaceContentPlanSkillText(targetDir),
+    'voc-speaking-script': () => workspaceSpeakingScriptSkillText(targetDir),
+    'voc-competitor-map': () => workspaceCompetitorMapSkillText(targetDir),
+    'voc-business-workflow': () => workspaceBusinessWorkflowSkillText(targetDir)
+  };
+  for (const skillName of skillNames) {
+    const sourceSkill = path.join(sourceRoot, skillName, 'SKILL.md');
+    if (!fs.existsSync(sourceSkill)) throw new Error(`Manifest skill entry is missing: ${skillName}`);
+    const skillDir = path.join(WORKSPACE_ROOT, '.claude', 'skills', skillName);
+    ensureDir(skillDir);
+    const content = writers[skillName] ? writers[skillName]() : fs.readFileSync(sourceSkill, 'utf8');
+    fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content, 'utf8');
+  }
+  return skillNames;
 }
 
 function writeClaudeWorkspaceActivation(targetDir) {
   writeWorkspaceMcpConfig(targetDir);
-  writeWorkspaceSkillEntries(targetDir);
+  const skillNames = writeWorkspaceSkillEntries(targetDir);
   console.log('Wrote Claude/Fmode 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');
-  console.log('Wrote workspace skill entry: .\\.claude\\skills\\fmode-image-analysis\\SKILL.md');
-  console.log('Wrote workspace skill entry: .\\.claude\\skills\\voc-api-catalog\\SKILL.md');
-  console.log('Wrote workspace skill entry: .\\.claude\\skills\\voc-cost-controller\\SKILL.md');
+  for (const skillName of skillNames) {
+    console.log(`Wrote workspace skill entry: .\\.claude\\skills\\${skillName}\\SKILL.md`);
+  }
 }
 
 function writeWorkspaceActivation(targetDir, opts) {

+ 37 - 1
claude-code/claude-code-voc-intelligence/docs/capability-map.md

@@ -4,7 +4,7 @@
 
 ## 一句话定位
 
-把小红书、抖音等平台的用户真实声音,转成企业能用的趋势情报、问题优先级、经营动作、内容选题和多轮验证记忆。
+把社媒、国内电商/创作者和海外选品接口中的用户与市场数据,转成企业能用的趋势情报、问题优先级、经营动作、内容选题、成本测算和多轮验证记忆。
 
 ## 用户任务到入口映射
 
@@ -21,6 +21,9 @@
 | 用户说某动作不适合 | `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-api-catalog` | `voc_api_search` → `voc_api_doc` → `voc_api_call` | 接口、通道、参数、返回数据和本地媒体证据 |
+| 复杂任务执行前做成本和余额测算 | `voc-cost-controller` | `voc_cost_estimate` / `voc_billing_query` | 经济版/标准版/完整版、调用计划、预计额度、差额和建议充值 |
+| 查询已完成任务的真实消耗 | `voc-cost-controller` | `voc_usage_report` | 报告/模块/接口维度的频次、quota、用户扣费和成本拆分 |
 
 ## Fmode Image Analysis Add-on
 
@@ -107,6 +110,31 @@
 把这些评论整理成问题池,告诉我哪些问题最影响生意,先改哪个。
 ```
 
+### `voc-api-catalog`
+
+适合:
+
+- 查找社媒、电商、创作者和海外选品接口。
+- 读取接口参数、通道和版本建议。
+- 调用清单内接口或用 `rawPath` 调用未登记接口。
+
+标准链路:
+
+```text
+voc_api_search -> voc_api_doc -> voc_api_call
+```
+
+### `voc-cost-controller`
+
+适合:
+
+- 大规模账号、商品、关键词任务的执行前报价。
+- 经济版、标准版、完整版的采集范围比较。
+- 当前余额、预算上限和余额不足后的充值入口。
+- 执行完成后的报告、模块、步骤和接口用量账单。
+
+成本价格以服务端 billing query 返回的当前价目为准;接口目录中的 `billing` 只表示调用单位,不作为人民币价格。
+
 ## 关键产品原则
 
 - 首轮报告是“初步判断”,不是最终结论。
@@ -115,6 +143,8 @@
 - live 失败时给可执行下一步,不暴露原始接口错误。
 - 多轮反馈要进入记忆,让建议越用越贴近客户。
 - 不同行业、不同客户的记忆要隔离。
+- 成本预估是执行前计划,实际扣费以 NewAPI 每次成功调用的计费日志为准。
+- 充值只复用现有 Tokenized Balance 链接或支付二维码;充值完成后重新查询余额并从检查点继续。
 
 ## 记忆规则
 
@@ -138,9 +168,13 @@
 | `mcp/src/server.js` | MCP 工具注册、schema、工具说明 |
 | `mcp/src/tools/*` | CLI/MCP 可调用工具入口 |
 | `mcp/src/features/*` | 趋势报告、单点深挖、记忆、信号提取 |
+| `mcp/catalog/voc-social-endpoints.json` | 1,344 个接口的稳定 ID、通道、参数索引和版本建议 |
+| `mcp/catalog/params/*.json` | 按平台懒加载的详细参数文档 |
 | `mcp/src/core/credentials.js` | token 读取口径 |
 | `mcp/src/core/payment-links.js` | 充值/开通提示 |
 | `mcp/src/core/billing-client.js` / `mcp/src/tools/voc-billing-query-run.js` | 按 Agent 操作计划读取服务端价目、余额、quota 差额和建议充值金额 |
+| `mcp/src/tools/voc-cost-estimate-run.js` | 生成执行前的多档采集计划和成本区间 |
+| `mcp/src/tools/voc-usage-report-run.js` | 查询实际计费日志并按报告、模块、接口拆分 |
 | `mcp/src/core/evidence-quality.js` | 证据质量门槛、无样本降级建议 |
 | `docs/live-manual-acceptance-checklist.md` | live 采集人工验收 |
 | `docs/claude-code-troubleshooting-playbook.md` | Claude Code 故障定位、误报修复、关键词修复和发散排查 |
@@ -154,6 +188,7 @@
 | `docs/deliverables/seven-day-content-plan.md` | 7 天内容选题与口播脚本模板 |
 | `scripts/smoke-package.js` | 包级 smoke,覆盖 sample、记忆、token、MCP |
 | `scripts/smoke-mcp.js` | MCP 工具发现和基本调用 smoke |
+| `scripts/audit-index.js` | 对比 manifest、技能目录、MCP 注册、版本和文档引用 |
 
 ## 工作坊输出口径
 
@@ -174,6 +209,7 @@
 - `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 --prefix claude-code-voc-intelligence run audit:index` 通过。
 - `npm run claude-voc:build` 通过。
 - npm 发布后 `npm view @vocmarket/voc-skill version` 返回新版本。
 - 测试工作区 `npx --yes @vocmarket/voc-skill@latest workspace --smoke` 通过。

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

@@ -117,7 +117,7 @@ npm run claude-voc:npm-pack
 `npm run claude-voc:acceptance` 是更完整的发布前验收,会额外断言 workspace 安装后的:
 
 - `.mcp.json` 已生成
-- 8 个 `.claude/skills/*/SKILL.md` 入口已生成
+- manifest 中列出的全部 `.claude/skills/*/SKILL.md` 入口已生成(当前 11 个)
 - MCP server 路径是安装后插件目录里的绝对路径
 - `npm pack --dry-run` 能看到关键文件
 
@@ -203,7 +203,7 @@ npm view @vocmarket/voc-skill@0.3.15 version --registry=https://registry.npmjs.o
 - 线上 `npm exec --yes --package @vocmarket/voc-skill@latest -- claude-voc workspace --smoke` 已通过。
 - 旧包 `@gangvy/claude-code-voc-intelligence@*` 已标记为 deprecated,安装旧包时会提示改用 `@vocmarket/voc-skill`。
 - 已新增 `npm run claude-voc:acceptance` 一键发布前验收,覆盖包级 smoke、MCP smoke、npm pack dry-run 和 workspace 安装断言。
-- 包内包含 8 个 skill、12 个 MCP tools。
+- 当前包内包含 11 个 skill、19 个 MCP tools;数量以 `skill-package-manifest.json` 和 `npm run audit:index` 为准。
 - smoke 覆盖 sample report、证据卡、图片缓存、xsec URL 规范化、偏好记忆、no-token 充值提示、祖先 `.env.local` token 读取、抖音搜索入参错误保护、跨行业样例和 MCP tool discovery。
 
 ## 后续版本同步

+ 378 - 14
claude-code/claude-code-voc-intelligence/docs/voc-cost-metering-and-balance-implementation-spec.md

@@ -1,7 +1,7 @@
 # VOC 用量核算、成本预估与余额看板实现方案
 
-> 文档状态:技能包改动已落实,待后端重启后的端到端联调
-> 编写日期:2026-09-01;最近复核:2026-09-02
+> 文档状态:P0 计费、用量展示、支付回调和工具级续跑已完成生产复测;已支付小额订单的 NewAPI 直入账代码、回调契约和本地构建已完成。2026-09-05 生产探针确认公网 `nova-server` 尚未加载该路由版本,需按第 21.28 节发布后再执行一次账户级余额刷新与幂等复测。历史工作流适配和跨会话唤醒仍列为 P1。
+> 编写日期:2026-09-01;最近复核:2026-09-05
 > 涉及仓库:`claude-code-voc-intelligence`、`future-server`、`fmode-studio`、`voc-profit-web`
 
 ## 1. 背景与目标
@@ -1227,20 +1227,43 @@ npm run smoke:package
 
 | 完成定义 | 状态 | 复核结论 |
 | --- | --- | --- |
-| AI 读取当前有效计费规则 | 已完成 | 后端统一 `POST /api/fmode/billing/query` 返回余额、价目和版本;`voc_billing_query`/`voc_cost_estimate` 已注册。 |
+| AI 读取当前有效计费规则 | Node 生产入口已上线,账户字段待有效凭证回归 | `fmode-server/api/routes.ts` 保留 Deno 入口实现;传统 Node 生产入口在 `api/api-ncloud/fmode/routes.js` 增加同一路径,并通过 `.modules/newapi-metering.min.cjs` 复用共享计费实现。远程无凭证和合成 API Key 已分别返回 401,路由已不再是 404。 |
 | 所有已接入工作流执行前三档预估 | 部分完成 | 通用规划器覆盖小红书账号监听、天猫 Listing 和自定义 operations;其他新工作流需提供规划适配器。 |
 | 每次调用关联 usageTraceId/report/skill/step/endpoint | 已完成(已接入入口) | `voc_api_call`、小红书/抖音 live 网关请求发送 `X-Fmode-*` Header,后端写入 `logs.other`;历史日志标记 mixed/reconstructed。 |
-| Balance 按模块、追踪、报告、接口展示 | 已完成(前端已接入) | Studio Balance 用量页调用统一接口,展示调用、quota、实际消费、模块/接口明细和计费规则。 |
+| Balance 按模块、追踪、报告、接口展示 | 前端完成,生产接口已挂载,账户字段待回归 | Studio Balance 用量页已接入统一接口和展示字段;生产 billing query 已可达,需使用当前有效账户凭证核对真实余额和用量。 |
 | 大模型与 VOC 费用分列 | 部分完成 | 用户用量账与平台成本报告已拆分;模型侧只有具备同一 trace 的日志才能精确合并。 |
 | 预算上限阻止超支并保留部分成果 | 已完成(已接入批处理入口) | `batch-budget-guard` 在小红书/抖音 live 每个关键词批次前读取当前余额与追踪消费,支持 80% 预警、硬预算/余额阻断、差额和建议充值;首批前阻断也会写入 `checkpoint.json`,`resume` 会跳过已完成关键词。经营闭环复用同一趋势入口;跨会话自动唤醒仍由宿主运行时负责。 |
 | 高成本低价值项优化建议 | 已完成(首批任务) | Agent 估算报告输出 valueScore、可删项和节省额;管理员看板 `/usage-diagnostics` 已按任务和接口展示成本排行、缓存命中和预计节省。 |
 | 充值后从检查点继续且不重复调用 | 已完成(工具级续跑) | VOC 402 分支保留部分结果并写检查点;支付后以同一 `usageTraceId` + `resume` 续跑,已完成业务键和分页不会重复调用。跨会话自动唤醒仍由宿主运行时负责。 |
-| Agent 返回免登录链接或支付二维码 | 已完成 | VOC 402 由统一 recharge client 复用现有 `pay_code2/order_status2`,二维码失败保留 Token Balance URL;Studio/宿主已对模型 402 直接打开现有充值入口。 |
+| Agent 返回免登录链接或支付二维码 | 技能端完成,生产 query 已挂载,账户字段待回归 | VOC 402 和统一 recharge client 已复用现有 `pay_code2/order_status2`,二维码失败保留 Token Balance URL;有效账户查询后即可验证完整生产链路。 |
 | 采集多媒体并固定本地缓存 | 已完成(所有当前 VOC 采集入口) | 小红书/抖音 sample 与 live、通用 `voc_api_call` 均经过 discovery/manifest/pipeline;`effectiveUrl` 固定为本地路径,线上 Storage 上传已关闭。 |
 | 凭证不写入代码、日志、报告 | 已完成(静态与 smoke 范围) | 请求凭证只作运行时输入,输出经过脱敏;发布包扫描和安装 smoke 通过。 |
 
 当前可以验收的 P0 交付是:统一报价、真实扣费归集、充值入口、Balance 用量页、管理员任务级成本诊断、两类首批工作流、live 媒体持久化和本地续跑检查点。后续工作集中在“为尚未接入的历史批处理 Skill 增加规划适配器、跨会话自动唤醒、更多采集入口接入媒体流水线、基于真实账号的端到端回放和历史预估校准”,不影响本轮已交付闭环。
 
+### 18.2 当前验收状态(2026-09-03)
+
+本节覆盖重启后生产复测结果。第 18.1 节保留为 9 月 2 日历史快照,不再代表当前账户状态。
+
+| 完成定义 | 当前状态 | 最新证据或剩余范围 |
+| --- | --- | --- |
+| AI 读取当前有效计费规则 | 已通过生产复测 | `billing/query` 返回 HTTP 200、当前余额和 7 条价目规则;Agent 继续在本地根据价目和计划计算报价。 |
+| 所有已接入工作流执行前三档预估 | 首批已通过,扩展项待适配 | 小红书账号监听、天猫 Listing 和自定义 `operations` 已覆盖;历史批处理 Skill 仍需逐一接入规划适配器。 |
+| 每次调用关联 usageTraceId/report/skill/step/endpoint | 已通过生产复测 | 重启后小规模 VOC 调用可按 `usageTraceId` 聚合,生产账单为 1 次调用、1 次计费、7353 quota、¥0.10;运行包写入追踪上下文和计费日志字段。 |
+| Balance 按模块、追踪、报告、接口展示 | 已通过生产复测 | Studio Balance 页面和生产 query 均可用;修正凭证优先级后,Session 与 NewAPI key 指向同一账户,已验证余额、用量汇总、模块/接口明细和规则展示。 |
+| 大模型与 VOC 费用分列 | 首批已落地,完整模型归集待扩展 | 用户账单与平台成本报告已拆分;模型日志只有携带同一追踪标识时才能精确合并,未带标识的历史数据继续标记为待归集。 |
+| 预算上限阻止超支并保留部分成果 | 已通过 | 预算守卫、80% 提示、硬上限、差额提示、检查点和工具级续跑 smoke 全部通过。 |
+| 高成本低价值项优化建议 | 已通过首批验收 | Agent 估算报告和管理员任务诊断均输出接口成本占比、可删项、预计节省和信息损失提示。 |
+| 充值后从检查点继续且不重复调用 | 工具级续跑已通过,账户级余额恢复待后端修正 | 使用原 checkpoint 和原参数续跑返回 `status=ok`、`fetchCalls=0`,已完成关键词不再触发 VOC 网关;本次支付回调已成功,但自动划拨失败,尚未形成“到账后余额增加再续跑”的同一订单闭环。 |
+| Agent 返回免登录链接或支付二维码 | 支付回调已通过,NewAPI 自动划拨待后端修正 | Token Balance URL、`recharge/order + pay_code2`、`order_status2` 在有效 Session 上实测;`VOCQA20260903P01` 返回 `SUCCESS`,Parse `AccountLog.isVerified=true`,但 `fmodeApiAutoTransfer.status=failed`(旧钱包余额为负),NewAPI quota 未增加。 |
+| 采集多媒体并固定本地缓存 | 已通过生产复测 | 重启后产物生成 `outputs/prod-restart-20260903/assets/` 本地媒体目录;运行时代码没有线上 Storage 上传、预签名或 complete 调用。 |
+| 凭证不写入代码、日志、报告 | 已通过静态、smoke 和发布包验收 | 凭证只作运行时输入,输出脱敏;zip、npm、npx 和 workspace 安装 smoke 均通过。 |
+
+#### 当前剩余工作
+
+1. **账户交易回归收尾**:支付订单 `VOCQA20260903P01` 已返回 `SUCCESS` 且 Parse 流水已核验;需修正/重新部署自动划拨回调,使已支付金额不依赖旧钱包正余额,随后复查 NewAPI 余额增加、Balance 页面刷新和同一 checkpoint 续跑。
+2. **扩展覆盖**:为尚未接入的历史批处理 Skill 增加规划适配器;完善跨会话自动唤醒、更多采集入口的媒体接入,以及基于真实报告的预估偏差校准。这些属于 P1 扩展,不阻塞当前 P0 计费闭环。
+
 ## 19. 建议首个迭代范围
 
 首个可上线迭代建议只做以下闭环:
@@ -1285,9 +1308,13 @@ npm run smoke:package
 | 修改 | `fmode-server/modules/voc-social-api/src/routes.ts` | 接收白名单任务上下文、按路径解析价格、写日志、返回 usage 响应头 | 高 |
 | 修改 | `fmode-server/modules/voc-e-commerce/src/routes.ts` | 与社媒网关保持相同计费和追踪行为 | 高 |
 | 修改 | `fmode-server/modules/voc-ecom-api/src/routes.ts` | 与社媒网关保持相同计费和追踪行为 | 高 |
-| 修改 | `fmode-server/api/routes.ts` | 在现有主路由中提供统一 `POST /api/fmode/billing/query`;媒体 Storage 路由不由本技能包调用 | 高 |
+| 修改 | `fmode-server/api/routes.ts` | 在现有 Deno 主路由中提供统一 `POST /api/fmode/billing/query`;媒体 Storage 路由不由本技能包调用 | 高 |
+| 修改 | `api/api-ncloud/fmode/routes.js` | 为实际运行的 Node `server.js` 增加 `/api/fmode/billing/query`,兼容 Bearer、`x-api-key` 和 body token,并将认证错误固定为 401 | 高 |
+| 新增产物 | `.modules/newapi-metering.min.cjs` | 将共享计费查询实现编译为 Node 可加载产物,随传统入口一起发布;不改变逐次 NewAPI 扣费 | 高 |
+| 新增构建脚本 | `fmode-server/scripts/build-newapi-metering.cjs` | 使用仓库内 esbuild 从 `fmode-server/modules/shared/newapi-metering.ts` 可重复生成上述 Node 产物 | 中 |
 | 修改 | 三个模块的构建脚本或 `scripts/build.mjs` | 确保共享计费代码进入产物,并复制到根目录 `.modules` | 中 |
-| 生成 | `.modules/voc-social-api.min.cjs`、`.modules/voc-e-commerce.min.cjs`、`.modules/voc-ecom-api.min.cjs` | 三个 VOC 网关的生产实际加载产物;由各模块构建生成,不手工编辑。billing 路由随 `fmode-server/api/routes.ts` 加载,不单独生成 CJS | 高 |
+| 生成 | `.modules/voc-social-api.min.cjs`、`.modules/voc-e-commerce.min.cjs`、`.modules/voc-ecom-api.min.cjs` | 三个 VOC 网关的生产实际加载产物;由各模块构建生成,不手工编辑 | 高 |
+| 生成 | `.modules/newapi-metering.min.cjs` | 共享计费查询 Node 产物;执行 `node fmode-server/scripts/build-newapi-metering.cjs` 生成,供传统 `server.js` 的 `/api/fmode/billing/query` 加载 | 高 |
 | 保持并复用 | `api/api-ncloud/fmode/routes.js` 的 `/recharge/order` | Agent 和 Balance 页创建或复用充值订单及 `AccountLog` | 中 |
 | 修改并复用 | `api/api-ncloud/fmode/sync-fmode-api.js`、`newapi-user-resolution.js` | 抽取可被计费模块复用的 Session 用户到 NewAPI 用户映射;保持 `syncUser()` 和 Token 创建幂等 | 高 |
 | 保持并回归 | `api/api-ncloud/fmode/serv-user.js`、`routes.js`、`cloud/user/auth-route.js` | 继续承担现有钱包、NewAPI 划拨、`pay_code2` 二维码和 `order_status2` 状态,不引入任务支付绑定 | 高 |
@@ -1559,6 +1586,8 @@ https://app.fmode.cn/dev/studio/balance/?token=USER_TOKEN
 
 ## 21. 现有技能包计费链路审计与完整修改清单
 
+> 口径说明:本节 21.1-21.9 保留的是实施前的审计基线和改造清单,其中的“当前仍……”描述用于解释为什么要改,并不代表本次验收后的运行状态。已完成项和当前真实状态以 18.1、21.10-21.12 以及 `npm run audit:index` 为准。
+
 本节以当前代码实际执行路径为依据,补全“改哪些位置才能让新模式真正跑通”。结论是:计费和充值逻辑目前分散在通用接口目录、两个平台趋势工具、业务闭环、图片分析、Token 检查和激活流程中。只修改 `voc_api_call` 或只替换一处充值 URL,无法覆盖实际报告任务。
 
 ### 21.1 当前端到端链路
@@ -1589,13 +1618,13 @@ readNewApiToken(input) || readVocToken(input)
 - `sk-` 鉴权失败后不能可靠用 Session 身份继续。
 - 专用 API 类和通用 gateway 的错误分类可能不同。
 - 402、403 和包含“权限/余额”的历史返回会被 trend runner 二次猜测。
-- 全失败和部分成功后的充值分支多次调用 `buildVocRechargeInfo()`,继续返回旧 APIG/workshop 入口。
+- 全失败和部分成功后的充值分支多次调用旧版充值构造器,继续返回旧 APIG/workshop 入口。
 - 部分报告只保存采集数据,没有统一 `usageTraceId`、实付金额、检查点和可续跑状态。
 
 #### 21.1.3 业务闭环和图片分析
 
 - `voc-business-workflow` 调用 trend runner;非 `ok` 时会包一层业务提示。它会保留底层文本,但没有稳定透传 `recharge/usage/estimate/checkpoint`,以后很容易在包装时丢链接或二维码。
-- `fmode-image-analysis` 直接调用模型网关,并把 HTTP 402 和 403 都当成 `needs_recharge`;同时使用旧 `buildVocRechargeInfo()`。这会把权限问题错误引导到充值,并且无法和 VOC 数据费用使用同一结果协议。
+- `fmode-image-analysis` 直接调用模型网关,并把 HTTP 402 和 403 都当成 `needs_recharge`;同时使用旧版充值构造器。这会把权限问题错误引导到充值,并且无法和 VOC 数据费用使用同一结果协议。
 - Agent 自身的大模型请求在余额耗尽时,模型可能已经不能生成下一条回复。这个场景必须由 Studio/Agent runtime 拦截 402 并打开现有 Balance 页面,不能只依赖 Skill 返回链接。
 
 #### 21.1.4 Token 检查和激活
@@ -1806,7 +1835,7 @@ type ResolvedBillingRule = {
 | 文件 | 必须修改的行为 |
 | --- | --- |
 | `mcp/src/tools/voc-api-catalog-run.js` | 删除 `endpoint.billing || 1` 作为报价依据;查询服务端价格;返回实际 auth/billing mode;统一生成充值结果和媒体流水线输出 |
-| `mcp/src/tools/xiaohongshu-trend-run.js` | 删除所有分散的 `buildVocRechargeInfo()` 分支;使用统一预算、检查点和 recharge;部分结果也返回同一链接/二维码协议 |
+| `mcp/src/tools/xiaohongshu-trend-run.js` | 删除旧版充值构造器分支;使用统一预算、检查点和 `buildRechargeInfo`;部分结果也返回同一链接/二维码协议 |
 | `mcp/src/tools/douyin-trend-run.js` | 与小红书完全同源,不再重复猜测 402/403 |
 | `mcp/src/features/voc-business-workflow/business-workflow.js` | 非 `ok` 时原样透传 `recharge/usage/estimate/checkpoint/files`;成功时合并各步骤实际费用,不只包装文字 |
 | `mcp/src/tools/voc-business-workflow-run.js` | schema 和输出契约增加预算、估算、账单与续跑字段 |
@@ -1844,7 +1873,7 @@ type ResolvedBillingRule = {
 | `fmode-server/modules/voc-ecom-api/src/routes.ts` | 与前两者一致;保留海外 `/forward` 业务封装 |
 | `api/api-ncloud/fmode/sync-fmode-api.js` | 保持 `syncUser()` 幂等,提供 Session 用户到 NewAPI 账户映射能力;不得在普通日志输出 Token |
 | `api/api-ncloud/fmode/newapi-user-resolution.js` | 继续处理 `parse_id/username` 冲突,增加计费身份映射测试 |
-| `fmode-server/api/routes.ts` + `fmode-server/modules/shared/newapi-metering.ts` | 实现唯一新增用户计费查询接口;同一认证解析 balance、pricing、usage;强制当前 user 条件和筛选白名单。Storage 路由不由技能包调用 |
+| `fmode-server/api/routes.ts` + `fmode-server/modules/shared/newapi-metering.ts` + `api/api-ncloud/fmode/routes.js` | 实现唯一新增用户计费查询接口;Deno 和实际 Node 入口复用同一计费实现,同一认证解析 balance、pricing、usage;强制当前 user 条件和筛选白名单。Storage 路由不由技能包调用 |
 | `api/api-ncloud/fmode/routes.js` | 保持 `/recharge/order` 契约;回归 `tradeNo` 幂等、金额校验和 Session 身份 |
 | `api/api-ncloud/fmode/serv-user.js`、`sync-fmode-api.js`、`cloud/user/auth-route.js` | 回归 `AccountLog -> pay_code2 -> order_status2 -> topUpNewApiQuota`;重复回调不重复到账 |
 | `api/api-ncloud/profit/*` | 管理员看板读取新实际金额和追踪字段,同时兼容历史日志 |
@@ -1948,9 +1977,9 @@ Studio 已有 `/balance`、Token 免登录、NewAPI 余额、支付二维码、
 | 充值路径 | 保持现状 | 402 只返回当前 Session 的 Tokenized Balance 链接;显式 `createPaymentQr=true` 才调用现有支付码链路。缺 token、401、403 不生成充值入口。 |
 | 任务支付语义 | 保持现状 | `usageTraceId` 仅为执行追踪标签,所有规模任务仍按 NewAPI 实际成功调用逐次扣费,不建立任务预付款。 |
 
-本次代码变更只涉及技能包的媒体入口、兼容参数说明和通用 API 响应落盘,不改变后端计费、充值中心或 Studio 的支付主体逻辑。后端模块产物由后端发布流程负责同步。
+本次改动包含技能包媒体入口、兼容参数说明、通用 API 响应落盘,以及后端统一 billing query 的 Node 生产挂载;不改变现有充值中心或 Studio 的支付主体逻辑。后端模块产物由后端发布流程负责同步。
 
-### 21.12 2026-09-02 发布前复核结果
+### 21.12 2026-09-02 发布包复核结果
 
 技能包发布前回归已完成,结果如下:
 
@@ -1959,8 +1988,343 @@ Studio 已有 `/balance`、Token 免登录、NewAPI 余额、支付二维码、
 | `npm run claude-voc:build` | 通过 | 安装 smoke 覆盖 sample、偏好、MCP、媒体、充值提示和跨行业流程 |
 | `npm run claude-voc:npm-pack` | 通过 | tarball 安装、npx 安装和 workspace 安装三种模式均通过 |
 | `npm run claude-voc:acceptance` | 通过 | 全包验收通过,MCP 工具清单包含 billing、cost、usage 工具 |
+| manifest/index audit | 通过 | manifest、11 个技能目录、19 个 MCP 工具、文档引用和版本号一致;workspace/npm/zip 安装均按 manifest 驱动 |
 | 媒体 pipeline 回归 | 通过 | 所有媒体写入本地 `assets/`,manifest 的 `cloudUrl=null`,报告使用本地路径 |
 | 旧逻辑扫描 | 通过 | 源码、zip 和 npm 产物均未发现线上 Storage 上传、预签名或上传完成调用 |
 | 空白差异检查 | 通过 | `git diff --check` 无错误;仅有 Git 的换行符提示 |
 
-当前仍需真实环境联调的项目只剩后端三个模块重启后的账单查询、生产 NewAPI 用量、Tokenized Balance 充值到账刷新和检查点续跑;这些由后端发布完成后按第 21.8 节执行,不阻塞技能包本地打包验收。
+当前仍需真实环境联调的项目只剩生产账户级 NewAPI 用量、Tokenized Balance 充值到账刷新和检查点续跑;路由可达性、认证状态、模块计费逻辑和技能包本地验收均已完成。账户级验证需要当前有效测试凭证,不使用真实大任务作为首次探针。
+
+### 21.13 2026-09-02 后端更新后联调结果
+
+本轮使用合成请求和公开健康端点验证,不读取或输出任何真实账号凭证,也不触发真实扣费或充值:
+
+| 检查项 | 结果 | 证据/结论 |
+| --- | --- | --- |
+| 生产 VOC 路由存活 | 通过 | `GET /api/voc-social/health`、`/api/voc-ecom/health`、`/api/voc-e-commerce/health` 均返回 200。 |
+| 生产三模块产物 | 通过 | `future-server/.modules` 中三个产物均包含 `balanceUrl`、`usage_trace_id`、`actual_cost_cny`;Node Router 工厂加载 smoke 通过。 |
+| 生产统一计费查询 | 路由已上线,账户字段待回归 | 根目录 `server.js` 通过自动加载 `/api/fmode` 传统路由;远程无凭证请求(2026-09-02 16:06,Asia/Shanghai)返回 401,合成 `x-api-key` 返回 `Invalid NewAPI token`,确认新路由和产物已进入线上进程。使用本机旧 VOC 会话变量查询时返回“当前用户不存在”,未得到真实账户数据。 |
+| 生产 Storage 路由 | 不发布(按设计) | 技能包继续只写本地媒体,不依赖 `/api/fmode/storage/*`;线上 Storage 上传、预签名和 complete 均不进入本次 Node 挂载。 |
+| 主路由源码认证错误 | 已修正 | `fmode-server/api/routes.ts` 的 billing、Storage upload/complete 先单独处理认证,缺凭证统一返回 401,查询/存储内部错误继续保留自身状态码。 |
+| 本地主路由探针 | 通过 | 编译 `api/routes.ts` 后,billing 和三个 Storage 入口无凭证均返回 401,根路由列出新接口。 |
+| 技能包回归 | 通过 | `claude-voc:acceptance`、`build`、`npm-pack`、billing/cost/media smoke 和索引审计均通过。 |
+
+#### 账户级闭环仍需补齐
+
+1. 使用当前有效测试账户验证 `balance`、`pricing`、`usage` 和 `balanceUrl`;本机旧 VOC 会话变量已过期或不属于生产账户。
+2. 用测试账户执行一次可控的 VOC 调用,核对 `usage_trace_id`、`actual_cost_cny`、NewAPI 余额减少和 Balance 用量明细四者一致;随后验证充值到账刷新和检查点续跑。
+
+### 21.14 2026-09-02 代码与回归测试复核
+
+| 测试项 | 结果 | 证据 |
+| --- | --- | --- |
+| 技能包索引审计 | 通过 | `node scripts/audit-index.js`:11 个 Skill、19 个 MCP 工具,issues 数量为 0。 |
+| 技能包全量 acceptance | 通过 | `npm run acceptance`:sample、memory、预算、断点续跑、跨行业、MCP、安装 smoke 全部通过。 |
+| 计费/报价/媒体 smoke | 通过 | `npm run smoke:billing`、`npm run smoke:cost`、`npm run smoke:media` 全部通过。 |
+| `voc-ecom-api` 路由测试 | 通过 | 6 项缓存、上游失败和扣费行为测试全部通过。 |
+| `voc-e-commerce` 路由测试 | 通过 | 构建测试包并完成路由回归。 |
+| `fmode-voice-api` 测试 | 通过 | 19 项认证、报价、余额、指纹和音频行为测试全部通过。 |
+| `fmode-qiwei-api` 测试 | 通过 | 76 项登录、订阅、计费、充值和 NewAPI metering 测试全部通过。 |
+| 生产公开端点 | 通过 | VOC 三个 health 接口和 Balance 页面均返回 200;billing 无凭证返回 401,合成 API Key 返回 401。 |
+| 线上账户级数据 | 待有效凭证 | 本机旧 VOC 会话返回“当前用户不存在”,未触发真实扣费,需有效测试账户完成最后一段闭环。 |
+
+### 21.15 2026-09-02 账户级闭环复测与计费锁修复
+
+本轮使用本机已配置的有效测试凭证完成生产查询;凭证值不写入报告、日志或测试输出。
+
+| 检查项 | 结果 | 证据 |
+| --- | --- | --- |
+| NewAPI 与 Session 身份映射 | 通过 | 两种凭证均解析到同一 NewAPI 用户;`billing/query` 返回余额和 7 条价目规则,Session 请求返回免登录 Balance URL。 |
+| 账户历史用量归集 | 通过 | 生产查询返回 17,388 条日志、128,283,092 quota、约 ¥1,649.649221 实际费用;可按模块、接口和追踪字段筛选。 |
+| 原始 VOC 成功调用扣费 | 通过 | 抖音综合搜索单调用前后余额减少 7,353 quota(约 ¥0.10),`usage` 返回 1 次、1 个上游调用、同一 `usageTraceId`。 |
+| 技能层带追踪调用 | 已修复,线上待重启 | 发现共享计费事务以 `pg-promise.none()` 执行返回 void 行的 advisory lock,带 `X-Fmode-Request-Id` 时扣费阶段报 `No return data was expected`;已改为 `one()` 并抽出可单测函数,重建 `.modules/newapi-metering.min.cjs`。 |
+| 修复回归测试 | 通过 | `fmode-qiwei-api` 77 项测试全通过,新增 advisory-lock 行为测试;VOC request shaping、billing、cost、media smoke 全通过。 |
+| 线上修复后复测 | 待重启/发布 | 当前远程进程仍返回旧错误,说明新 `.modules/newapi-metering.min.cjs` 尚未被线上 `server.js` 进程加载;重启 `fmode-server` 后需重跑一次带 `X-Fmode-Request-Id` 的小规模调用。 |
+
+#### 本轮结论
+
+账户查询、价目读取、Session 免登录链接、真实 NewAPI 扣费和用量归集已经有生产证据;“技能调用带追踪头后成功返回并写入同一账单”被发现一个后端事务锁兼容问题,代码和本地构建已修复,但必须在服务器加载新产物后才能把 P0 账户级闭环标记为完成。充值到账刷新和检查点续跑仍需在该修复上线后进行一次真实支付回归,避免使用大任务作为首次探针。
+
+### 21.16 2026-09-02 继续联调与旧充值分支收口
+
+| 检查项 | 结果 | 证据/结论 |
+| --- | --- | --- |
+| fmode-studio 构建 | 通过 | Angular 生产构建完成,生成 Balance 用量视图及其懒加载资源。 |
+| fmode-studio OBS 发布 | 通过 | 使用仓库既有部署脚本和本机 `obsutil.exe` 同步 151 个文件,成功 151、失败 0;未清理旧 hash 资源。 |
+| fmode-studio CDN 刷新 | 通过 | 目录刷新接口返回 HTTP 200;线上 `chunk-MT7JRXAQ.js` 与本地构建 SHA-256 一致,确认用量代码已部署。 |
+| 技能包旧充值构造器 | 已收口 | 运行时代码统一使用 `buildRechargeInfo`;旧 APIG/workshop 入口不再由 VOC 402 分支生成,历史审计说明保留在文档中。 |
+| 二维码充值链路 | 已补齐 | `requestPaymentQr` 先调用 `/api/fmode/recharge/order` 创建幂等预订单,再带 Parse 应用头调用 `pay_code2`;原有 `order_status2` 轮询信息继续返回。 |
+| 技能包回归 | 通过 | `npm run acceptance`、`npm run smoke:billing`、`smoke:cost`、`smoke:media`、`smoke:request`、根目录 `claude-voc:build --validate --build --test-package` 全部通过;索引为 11 Skill、19 MCP、issues=0。 |
+| 后端模块回归 | 通过 | `fmode-qiwei-api` 77 项、`voc-social-api`、`voc-ecom-api`、`voc-e-commerce` 路由测试全部通过。 |
+| 线上后端复测 | 待线上入口恢复 | 本轮访问 `server.fmode.cn` 的 80/443 端口可建立 TCP,但 HTTPS 握手由对端关闭,health、billing 和带请求幂等头的真实调用尚未取得新响应;未据此声称远程计费修复已生效。 |
+
+#### 当前剩余门禁
+
+1. 恢复 `server.fmode.cn` 对应的 fmode-server/反向代理进程后,重新执行三个 health、带 `X-Fmode-Request-Id` 的小规模 VOC 调用,并确认同一 requestId 重放只扣一次。
+2. 使用有效测试 Session 完成一次充值二维码建单、支付状态轮询、余额刷新和 checkpoint 续跑;不使用大规模报告作为首次探针。
+3. 上述两项完成后,才能将 P0 账户级闭环从“代码已完成”更新为“生产已验收”。
+
+### 21.17 2026-09-02 发布后最终回归记录
+
+本轮完成 Studio 发布、线上页面回归、技能包重建和后端生产入口复测。所有测试均使用本机已有测试会话或合成请求,凭证值未写入报告、日志和产物。
+
+| 检查项 | 结果 | 证据/结论 |
+| --- | --- | --- |
+| fmode-studio 发布 | 通过 | 使用 `deploy.ps1`、`E:\\obsutil_windows_amd64_5.7.9\\obsutil.exe` 完成构建和 OBS 增量同步;151 个文件成功、0 失败;CDN 目录刷新 HTTP 200。 |
+| 线上 Balance 页面 | 通过 | Token 免登录进入账户页;余额、已用金额、充值入口和账户明细可见;URL 加载后清理地址栏中的 Token。 |
+| Balance 用量抽屉 | 通过 | “用量分析”展示日期筛选、调用次数、计费次数、quota、实际消耗、模块汇总、接口明细和当前计费规则;线上指定日期筛选后数据更新。 |
+| 移动端布局 | 通过 | 390×844 视口截图确认抽屉、日期控件、汇总卡片和模块列表无重叠;截图产物位于 `fmode-studio/output/playwright/balance-mobile.png`。 |
+| Balance 控制台 | 通过 | 线上回归读取到 0 个 error、0 个 warning。 |
+| 生产公开入口 | 通过 | 三个 VOC health 接口 HTTP 200;billing query 无凭证 POST 返回 401;生产路由可达。 |
+| 生产带追踪 VOC 调用 | 待重启 | 同一 `X-Fmode-Request-Id` 的两次最小请求均返回 `No return data was expected.`,账单前后保持不变,表明线上进程仍加载旧计费模块;本地源码、构建产物及单元测试已包含 advisory-lock 修复。 |
+| 技能包全量验收 | 通过 | `acceptance`、`smoke:billing`、`smoke:cost`、`smoke:media`、`smoke:request`、`audit:index` 均通过;索引为 11 个 Skill、19 个 MCP 工具。 |
+| 发布包验收 | 通过 | `claude-voc:build` 和 `claude-voc:npm-pack` 完成 zip、npm tarball、npx、workspace 三种安装 smoke;最新 tarball 为 `dist/npm/vocmarket-voc-skill-0.3.33.tgz`。 |
+| 后端模块回归 | 通过 | `fmode-qiwei-api` 77 项、`voc-social-api`、`voc-ecom-api`、`voc-e-commerce` 路由测试全部通过。 |
+| 旧逻辑扫描 | 通过 | 运行时代码未发现线上 Storage 上传、预签名或 complete 调用;媒体统一写本地 `assets/`;402 运行时统一使用 Tokenized Balance 链接,二维码为显式请求才走现有支付链。 |
+
+#### 当前唯一生产门禁
+
+服务器重启并加载最新 `.modules/newapi-metering.min.cjs` 后,需再次执行以下三项:
+
+1. 三个 VOC health + billing query 认证探针。
+2. 带 `X-Fmode-Request-Id` 的最小 VOC 请求及同标识重放,确认两次成功且 NewAPI 只新增一条日志、只扣一笔 quota。
+3. 在测试账户上完成一次二维码建单/状态轮询/余额刷新;支付完成后用同一 checkpoint 继续,确认已完成步骤不重复采集。
+
+在这三项通过前,P0 账户级闭环保持“代码和页面已完成、生产幂等等待进程重启复测”;不使用大规模报告作为首次探针。
+
+### 21.18 2026-09-02 共享计费修复后的 VOC 产物重建与复测
+
+本轮进一步核对了生产 Node 入口的加载方式:`future-server/server.js` 直接 `require` 根目录 `.modules/voc-*.min.cjs`,三个 VOC 模块在构建时会把 `shared/newapi-metering.ts` 内联到各自产物。因此只替换独立的 `newapi-metering.min.cjs` 不足以让已运行的社交、电商和聚合路由获得锁修复。
+
+| 检查项 | 结果 | 证据/结论 |
+| --- | --- | --- |
+| 三个 VOC 模块重建 | 通过 | 使用同一份共享计费源码重建 `voc-social-api`、`voc-ecom-api`、`voc-e-commerce` 的 CJS/ESM 和压缩产物,并将最新压缩 CJS 复制到 `future-server/.modules/`。 |
+| 产物锁实现 | 通过 | 四个运行包均包含 `SELECT pg_advisory_xact_lock(hashtext($1))` 且使用 `db.one()`,未再使用会触发 `No return data was expected` 的 `none()`。 |
+| 模块路由回归 | 通过 | `voc-social-api`、`voc-ecom-api`、`voc-e-commerce` 三套测试全部通过;压缩 CJS 可由 Node 正常加载并导出路由工厂。 |
+| 技能包回归 | 通过 | `audit:index`、`smoke:billing`、`smoke:cost`、`smoke:media`、`smoke:request` 全部通过;索引仍为 11 个 Skill、19 个 MCP 工具。 |
+| 生产公开入口 | 通过 | 三个 VOC health 接口继续返回 HTTP 200;使用有效测试凭证的 `billing/query` 返回余额、7 条价目和用量结构。 |
+| 生产带追踪调用 | 待同步/重启 | 远程同一 `X-Fmode-Request-Id` 的两次最小调用仍返回旧错误且账单无变化,表明远程主机尚未加载本轮重建的三个 VOC 压缩产物(或对应进程尚未重启)。 |
+
+#### 远程发布门禁
+
+远程发布需同时同步以下四个文件,然后只重启承载 `server.js` 的 `nova-server` 进程:
+
+```text
+.modules/newapi-metering.min.cjs
+.modules/voc-social-api.min.cjs
+.modules/voc-ecom-api.min.cjs
+.modules/voc-e-commerce.min.cjs
+```
+
+进程重启后按顺序执行:三个 health 探针、`billing/query` 认证探针、同一 `X-Fmode-Request-Id` 的两次最小社交调用,再按 `usageTraceId` 查询 Balance 用量。两次调用都成功时应只出现一条 NewAPI 日志、一次 quota 扣减,并同时存在 `usage_trace_id`、`request_id` 和 `actual_cost_cny`。本轮尚未执行真实支付,充值二维码/到账刷新和 checkpoint 续跑仍保持为最后的账户级验收项。
+
+### 21.19 2026-09-03 重启后生产计费复测
+
+服务器重启后重新执行生产入口探针。探针使用同一 `usageTraceId=prod-restart-20260903`、`reportId=prod-restart-report-20260903` 和 `X-Fmode-Request-Id=prod-restart-request-20260903`,请求 `douyin.search_general`;凭证值不写入文档或输出。
+
+| 检查项 | 结果 | 证据/结论 |
+| --- | --- | --- |
+| 三个 VOC health | 通过 | `GET /api/voc-social/health`、`/api/voc-ecom/health`、`/api/voc-e-commerce/health` 均返回 HTTP 200。 |
+| 生产 billing/query | 通过 | HTTP 200;返回当前余额、7 条价目规则以及余额/用量/报价结构。 |
+| 生产 VOC 成功调用 | 通过 | 追踪目录生成 `usage-context.json`,并生成 65 个本地媒体文件;请求已进入重启后的 VOC 运行包和本地媒体管线。 |
+| NewAPI 实际扣费 | 通过 | 按该追踪标识查询:`calls=1`、`billedCalls=1`、`quota=7353`、`costCny=0.1`、`cacheHits=0`、`upstreamCalls=1`;接口为 `voc-social-api / douyin/search/fetch_general_search_v2`。 |
+| 同请求标识幂等 | 通过(账单口径) | 同一追踪筛选只出现 1 条成功计费记录,没有第二笔 quota 或费用;证明锁修复后的重复执行不会重复扣费。探针的第二次调用在本地媒体下载阶段未及时回收标准输出,因此不把未回收的响应文本当作额外证据。 |
+| 追踪与余额快照 | 通过 | 查询返回可用余额 `781326033 quota`(约 `¥10626.034049`),并能按 `usageTraceId` 精确筛选该调用。 |
+| 本地媒体策略 | 通过 | 产物位于 `outputs/prod-restart-20260903/assets/`;运行时未调用线上 Storage 上传、预签名或 complete。 |
+
+#### 本轮结论
+
+重启后的生产进程已经加载最新四个 `.modules` 产物,health、统一计费查询、VOC 实际调用、NewAPI 逐次扣费、追踪归集和本地媒体缓存形成闭环。`usageTraceId` 仍只是账单归集标签,所有调用继续按 NewAPI 实际成功调用逐次扣费,不创建任务级支付或预付款。
+
+账户级最后一项仍是支付系统的真实交易回归:在测试账户上完成一次小额二维码建单、状态轮询、余额刷新,并用同一 checkpoint 续跑验证已完成步骤不重复采集。该项不影响本轮计费与媒体闭环结论,也不使用大规模报告作为首次支付探针。
+
+### 21.20 2026-09-03 支付响应兼容修复与小额订单复测
+
+本轮继续执行账户级充值验收。有效 Session 的余额查询返回 HTTP 200;使用固定验收订单号 `VOCQA20260903001`,金额为 `¥1.00`,避免重复创建新订单。
+
+| 检查项 | 结果 | 证据/结论 |
+| --- | --- | --- |
+| `recharge/order` 预订单 | 通过 | HTTP 200,服务端返回同一 `tradeNo`;预订单与 AccountLog 已建立,订单尚未验证到账。 |
+| `pay_code2` 响应解析 | 已修复并通过 | 线上 Parse Cloud 返回 `{ result: { prepay_id, code_url, nonce_str } }`;`recharge-client` 现在兼容 `result`、`data` 和旧版顶层字段,成功得到 `weixin://` 支付码。 |
+| `order_status2` 轮询 | 通过 | HTTP 200,订单状态为 `NOTPAY`;轮询请求带上本次二维码对应的 `nonce_str`。 |
+| 充值前后余额 | 符合预期 | 当前余额保持 `781318680 quota`(约 `¥10625.934048`),因为订单尚未扫码支付,NewAPI 余额没有增加。 |
+| 本地回归 | 通过 | `smoke:billing` 已增加 Parse `result` 包装响应用例并通过;原有充值请求、余额估算和 Token 脱敏断言继续通过。 |
+
+#### 本轮结论
+
+二维码建单、线上响应解析和状态轮询已形成可工作的链路;当前订单停留在 `NOTPAY`,实际扫码支付、到账回调、余额刷新和 checkpoint 续跑仍需支付动作完成后再验收。此次修复不改变充值金额、NewAPI 扣费或任务级账单语义。
+
+### 21.21 2026-09-03 发布包复核与全局旧路径审计
+
+本轮在支付订单保持 `NOTPAY` 的前提下,完成最新代码、zip/npm 交付物和全局运行路径复核。审计范围覆盖技能包源码、构建产物、安装器、`fmode-studio` 余额页、`future-server` VOC 路由及 `voc-profit-web` 看板引用;未再次创建充值订单,也未触发新的真实采集扣费。
+
+| 检查项 | 结果 | 证据/结论 |
+| --- | --- | --- |
+| 最新 npm 包包含二维码修复 | 通过 | `dist/npm/vocmarket-voc-skill-0.3.33.tgz` 已包含 `recharge-client.js` 的 `result`、`data` 和顶层字段兼容解析。 |
+| zip/npm/npx/workspace 交付 | 通过 | `claude-voc:build`、`claude-voc:npm-pack` 均返回成功;安装 smoke 覆盖 sample、MCP、凭证脱敏、本地媒体和 workspace 配置。 |
+| 重启后生产入口复测 | 通过 | 三个 VOC health 均 HTTP 200;认证 `billing/query` 返回 HTTP 200、余额结构和 7 条当前价目项。 |
+| 技能包回归 | 通过 | `audit:index`、`acceptance`、`smoke:billing`、`smoke:cost`、`smoke:media`、`smoke:request` 全部通过;索引为 11 个 Skill、19 个 MCP 工具。 |
+| 旧充值运行路径 | 已收口 | 运行时代码只由 Tokenized Balance URL 和显式二维码链路处理 VOC 402;`apig-pay/workshop` 仅保留在课程套餐说明与历史审计记录,不进入 VOC 余额不足分支。 |
+| 线上 Storage 上传路径 | 已关闭 | VOC 媒体 pipeline 只写任务目录 `assets/`,`effectiveUrl` 使用本地路径,`cloudUrl=null`;未调用 `/api/fmode/storage/*`、预签名或 complete。后端通用 Storage 路由保留给其他产品能力,VOC 技能不触发。 |
+| 任务级支付语义 | 符合设计 | `usageTraceId` 只作本地账单追踪标签;实际费用继续按每次成功 NewAPI 调用扣除,不创建任务预付款或一次性任务支付。 |
+| 订单状态 | 待用户支付 | 固定订单 `VOCQA20260903001` 轮询仍为 HTTP 200 / `NOTPAY`;余额刷新和 checkpoint 续跑需在该订单实际到账后执行。 |
+
+#### 当前闭环结论
+
+代码、生产重启后的计费归集、余额查询、Agent 报价、充值链接/二维码、预算守卫、本地媒体缓存、发布包安装和旧路径审计均已完成。剩余门禁仅是一次真实支付动作:订单到账后复查余额变化,使用原 checkpoint 续跑并确认已完成业务键不重复调用;跨会话自动唤醒、历史批处理适配和历史报价校准继续作为 P1 扩展。
+
+### 21.22 2026-09-03 固定订单二维码恢复
+
+为推进账户级交易回归,本轮继续复用 `VOCQA20260903001`,没有创建新订单。先查询该订单的 `AccountLog.detail.title`,确认首次建单描述为“VOC 充值链路验收”。支付码请求若使用不同 `body`,微信侧按商户订单幂等规则返回 HTTP 400/code 141;改回原描述后成功出码。
+
+| 检查项 | 结果 | 证据/结论 |
+| --- | --- | --- |
+| 原订单详情读取 | 通过 | Parse `AccountLog` 查询到固定订单、金额 ¥1.00、原始 detail 标题和 `isVerified=false`。 |
+| 参数一致性重试 | 通过 | 使用原 `body`、金额、账套和 `tradeNo` 调用 `pay_code2`,HTTP 200,兼容解析得到 `code_url`。 |
+| 二维码交付物 | 已生成 | [VOCQA20260903001.png](/E:/workspace/openclaw-voc-skill/outputs/recharge-qa/VOCQA20260903001.png),本地 QR 图像已检查可读。 |
+| 订单状态 | 待支付 | 最新 `order_status2` 返回 HTTP 200 / `NOTPAY`;支付完成后继续余额刷新和 checkpoint 续跑。 |
+
+该结果将“同一 `tradeNo` 重试必须保持支付描述、金额和账套一致”纳入充值客户端联调约束,避免把幂等订单误判为二维码服务故障。
+
+### 21.23 2026-09-03 ¥0.01 充值验收订单
+
+根据测试要求,使用同一有效 Session 创建新的 ¥0.01 验收订单 `VOCQA20260903P01`,不复用 ¥1.00 订单,避免金额和描述不一致触发微信幂等校验。
+
+| 检查项 | 结果 | 证据/结论 |
+| --- | --- | --- |
+| `recharge/order` | 通过 | HTTP 200,返回订单号 `VOCQA20260903P01`,AccountLog 已建立。 |
+| `pay_code2` | 通过 | HTTP 200,返回二维码支付码;响应兼容 `result` 包装。 |
+| 本地二维码 | 已生成 | [VOCQA20260903P01.png](/E:/workspace/openclaw-voc-skill/outputs/recharge-qa/VOCQA20260903P01.png),图像检查通过。 |
+| `order_status2` | 待支付 | HTTP 200,当前状态 `NOTPAY`;扫码后继续轮询到账。 |
+
+该订单专用于 ¥0.01 支付回归,后续只复用此 `tradeNo` 轮询,不再创建新的探针订单。
+
+### 21.24 2026-09-03 支付完成后的账户闭环复核
+
+用户完成 `VOCQA20260903P01` 的 ¥0.01 支付后,使用同一订单和当前有效 Session 复查支付回调、账户流水、NewAPI 余额以及检查点续跑。21.20-21.23 中的 `NOTPAY` 是支付前的历史快照,当前状态以本节为准。
+
+| 检查项 | 结果 | 证据/结论 |
+| --- | --- | --- |
+| `order_status2` | 通过 | HTTP 200,订单状态 `SUCCESS`。不创建新订单,复用原 `tradeNo` 完成查询。 |
+| Parse `AccountLog` | 通过 | 订单金额 ¥0.01,`isVerified=true`,支付回调已确认到账;流水 `detail.fmodeApiAutoTransfer.status=failed`。 |
+| 自动划拨失败原因 | 已定位 | 回调复用 `/api/fmode/newapi/topup`,该接口先检查旧钱包余额;测试账户钱包余额为 `-35.086`,因此返回“余额不足”,没有进入 NewAPI top-up。 |
+| NewAPI 用户余额 | 已复核 | `/api/fmode/newapi/user` HTTP 200;当前账户可用 `606973963 quota`,约 ¥8254.85;top-up 列表未出现本次订单,说明 ¥0.01 尚未增加 NewAPI quota。 |
+| 统一 billing query | 通过 | 使用与当前 Session 对应的 NewAPI key 请求 HTTP 200;返回同一账户的余额和用量,当前快照 `calls=2`、`billedCalls=2`、`quota=14706`、`costCny=0.2`、`cacheHits=1`、`upstreamCalls=1`。 |
+| 凭证账户一致性 | 已修复 | `credentials.js` 优先读取 `~/.fmode/config.json` 的 Session 和 NewAPI key,再回退 `~/.vocmarket`;复测 `billingUserId` 与 `sessionUserId` 一致,避免看板读取到另一账户。 |
+| checkpoint 续跑 | 通过工具级验收 | 使用现有 checkpoint 和原关键词参数续跑,`status=ok`、`fetchCalls=0`、保留 1 条已完成样本、`pendingKeywords=[]`;完成业务键未再次触发 VOC 网关。 |
+
+#### 本轮结论与后续门禁
+
+本轮已经证明“支付下单 → 微信支付回调 → Parse 流水核验 → 订单状态查询”和“原 checkpoint 去重续跑”可工作;同时暴露出测试账户旧钱包负余额会阻断“支付金额 → NewAPI quota”自动划拨。当前不应把 Parse `isVerified=true` 直接等同于 NewAPI 余额已增加。
+
+账户级 P0 收尾需要后端将自动划拨回调改为已验证支付订单的直接 NewAPI 入账,或明确使用独立的充值钱包且不受历史负余额影响;修复后复用 `VOCQA20260903P01` 做余额前后快照、Balance 页面自动刷新和续跑复测。技能包侧已补齐有效 Session 与 NewAPI key 的同账户解析,后续无需增加新的支付接口。
+
+### 21.25 2026-09-03 已支付后的再次回调复核
+
+用户确认已完成 `VOCQA20260903P01` 的 ¥0.01 支付后,复用原订单再次调用 `WbyPrzMI3A`,验证后端重启后的回调行为;本轮没有创建新订单,也没有再次发起支付。
+
+| 检查项 | 结果 | 证据/结论 |
+| --- | --- | --- |
+| `order_status2` | 通过 | HTTP 200,仍返回 `SUCCESS`;微信支付状态稳定。 |
+| `WbyPrzMI3A` 回调重试 | 未通过 | HTTP 200 但返回“自动划拨到 fmodeAPI 失败”,`transferError=余额不足,请先预约充值`。 |
+| Parse `AccountLog` | 已核验 | `VOCQA20260903P01` 金额 ¥0.01、`isVerified=true`;`detail.fmodeApiAutoTransfer.status` 仍为 `failed`。 |
+| 本订单 NewAPI 入账 | 未完成 | `/api/fmode/newapi/user` HTTP 200;当前余额 `606973963 quota`(约 ¥8254.85),top-up 列表没有与 `VOCQA20260903P01` 对应的记录。列表中的 ¥0.01/735 quota 属于另一笔历史 `FAPI` 订单。 |
+| billing 余额复核 | 通过 | `/api/fmode/billing/query` HTTP 200,返回的用户、余额与 Session 对应账户一致;本订单没有引起余额变化。 |
+
+#### 最新门禁结论
+
+支付成功和 Parse 流水确认已闭环,但“已验证支付订单 → NewAPI quota”仍被生产回调中的旧钱包余额检查拦截。当前验收不能标记为完成;后端需要部署直接按已验证订单入账(或独立充值钱包)逻辑后,再复用 `VOCQA20260903P01` 做一次余额前后快照、Balance 自动刷新与原 checkpoint 续跑验证。
+### 21.26 2026-09-05 已验证支付直入 NewAPI 修复
+
+为收尾账户级闭环,保留现有 `POST /api/fmode/newapi/topup` 地址,仅增加严格的 `verifiedPayment` 分支,避免扩展支付接口:
+
+```json
+{
+  "sessionToken": "<当前用户 Session>",
+  "amount": 0.01,
+  "verifiedPayment": {
+    "accountLogId": "<已支付 AccountLog.objectId>",
+    "orderNumber": "<微信 out_trade_no>",
+    "amount": 0.01
+  }
+}
+```
+
+服务端要求 Session 对应用户与 AccountLog 所有权一致,同时校验 `orderType=recharge-wxpay`、`isVerified=true`、订单号和金额精确匹配。通过后跳过旧钱包余额检查和钱包 debit 日志,使用 `WXPAY:<AccountLog.objectId>` 作为稳定 `top_ups.trade_no` 直接调用 `topUpNewApiQuota`。同一交易号通过事务级锁返回原 quota(`idempotent=true`),不会重复增加 NewAPI 余额;普通请求不带 `verifiedPayment` 时仍保持原钱包扣减和失败退款语义。
+
+云函数 `WbyPrzMI3A` 已同步传入 `accountLogId`、`orderNumber`、`amount` 和 Session,支付回调重试复用同一订单。部署后需使用已支付小额订单复核 NewAPI quota 前后值、Balance 自动刷新和原 checkpoint 续跑,确认已完成步骤的采集调用数为 0。
+
+### 21.27 2026-09-05 支付回调签名门禁与交付收尾
+
+`fmode-wxpay-api` 的旧 `/wxreturn` 实现存在回调副作用顺序问题:它会在比对微信 V2 签名前写入 `PayReturn`、修改 `AccountLog`/订单并调用云函数。现已调整为先从注入的 `payConfigByAppId[appid].mch_key` 计算全部非空通知字段的 MD5 签名,再使用常量时间比较;签名或商户配置不匹配时立即返回微信失败 XML,不写任何业务数据、不触发自动划拨。
+
+| 检查项 | 结果 | 证据/结论 |
+| --- | --- | --- |
+| 已验证支付入账契约 | 通过 | `verifiedPayment` 同时传递 `accountLogId`、`orderNumber`、`amount`;服务端继续以 AccountLog 的用户、订单类型、核验状态和金额作为入账前提。 |
+| 微信回调签名门禁 | 通过本地源码审计 | 有效签名才可写支付流水、更新订单及调用 `WbyPrzMI3A`;移除了无模块来源的旧 `test.updatemsg` 调用。 |
+| 支付模块构建 | 通过 | `fmode-wxpay-api` 的 `npm run build` 成功生成 `dist/0.0.5/` 产物。 |
+| 后端语法检查 | 通过 | `fmode-wxpay-api/src/routes.js`、`api/api-ncloud/fmode/routes.js`、`sync-fmode-api.js` 均通过 `node --check`。 |
+| 技能包回归 | 通过 | `acceptance` 与 `audit:index` 成功;索引为 11 个 Skill、19 个 MCP 工具,媒体仍只缓存到本地 `assets/`。 |
+
+#### 最后生产验收
+
+服务器加载最新 `future-server` 路由和 `fmode-wxpay-api/dist/0.0.5` 后,只复用既有的已支付 ¥0.01 订单,不创建新订单:
+
+1. 调用 `WbyPrzMI3A`,确认 `fmodeApiAutoTransfer.status=success`,且返回 `WXPAY:<AccountLogId>` 交易号。
+2. 查询 NewAPI 余额和 `top_ups`,确认本订单仅新增一次对应 quota;再次调用同一回调,确认 `idempotent=true` 且余额不再增加。
+3. 刷新 Balance 页面,确认该余额立即可见;使用原 checkpoint 续跑,确认已完成步骤的 `fetchCalls=0`。
+
+这三项通过后,P0 的“报价 -> 余额不足链接/二维码 -> 支付 -> NewAPI 按次扣费 -> 余额刷新 -> 检查点续跑”闭环即可标记完成。不会引入任务预付款或一次性任务支付,`usageTraceId` 仍只用于用量归集。
+
+### 21.28 2026-09-05 生产运行版本复核与最终发布门禁
+
+本轮使用当前本机已配置的有效 Session 做无金额预检:`billing/query`、`newapi/user` 和价目/用量查询均返回 HTTP 200。为了不向错误账户或未核验流水入账,没有创建支付二维码、没有发起新支付,也没有执行成功入账。复用历史测试订单号仅用于验证生产分支是否已加载。
+
+| 检查项 | 结果 | 证据/结论 |
+| --- | --- | --- |
+| 当前账户计费查询 | 通过 | `billing/query` 返回当前余额、7 条价目规则和 51 个用量分组;技能包当前凭证解析仍保持 Session 与 NewAPI Key 同账户。 |
+| 已验证支付路由加载 | 未通过 | 向 `newapi/topup` 发送带 `verifiedPayment` 的未核验预订单后,公网仍返回旧钱包分支的“余额不足”响应,而不是新分支应返回的“支付订单尚未核验完成”。这证明实际 `nova-server` 未加载本地修复。 |
+| 真实金额变更 | 未发生 | 本轮没有可用的历史支付账户 Session;请求被旧钱包余额校验阻断,未新增 NewAPI quota 或 `top_ups` 记录。 |
+| 技能包回归 | 通过 | `smoke:billing`、`smoke:budget`、`smoke:request`、`audit:index`、`acceptance` 均通过;索引仍为 11 个 Skill、19 个 MCP 工具。 |
+
+#### 发布对象与顺序
+
+1. 将 `api/api-ncloud/fmode/routes.js` 和 `api/api-ncloud/fmode/sync-fmode-api.js` 发布到公网 `server.fmode.cn` 实际运行目录,并重启或滚动重载该机器上的 **`nova-server`** 进程。`/api/fmode/*` 路由在该进程中注册,只重启 `fmode-server` 动态模块不会加载这两个文件。
+2. 构建并上传 `fmode-server/modules/fmode-wxpay-api/dist/0.0.5/` 到模块仓库,更新实际宿主的 `loadModule` 地址到 `0.0.5`,并确认 `payConfigByAppId[appid].mch_key` 仍由运行环境注入;随后重载该动态模块的宿主进程。该步骤负责回调签名门禁,不替代第 1 步。
+3. 使用**原支付账户的有效 Session**复用已支付的 `VOCQA20260903P01`,调用 `WbyPrzMI3A` 或等价的 `verifiedPayment` 契约。首次结果必须新增 `WXPAY:<AccountLogId>`,再次结果必须为 `idempotent=true`,且余额不再增加。
+4. 在同一账户刷新 Balance 页面,确认 NewAPI 余额变化可见;再用原 checkpoint 续跑并确认 `fetchCalls=0`。完成后更新本节状态为“生产验收通过”。
+
+#### 当前结论
+
+技能包、Studio 用量页、报价/预算、本地媒体缓存、支付客户端兼容和后端源码均已完成;生产 P0 尚缺一次正确的 `nova-server` 发布与账户级入账幂等验收。该门禁不能通过重启错误进程替代,也不能用未核验预订单或不同账户的 Session 代替已支付订单验证。
+
+### 21.29 2026-09-05 技能包最终回归与无扣费生产预检
+
+本轮补齐 NewAPI Key 鉴权时服务端未返回 `balanceUrl` 的兼容逻辑:技能包以当前已解析的 Session 在本地生成同一账户的免登录 Balance 链接。该逻辑只构造链接,不创建支付订单,也不会把 Session 写入 MCP 返回的二维码请求参数。
+
+| 检查项 | 结果 | 证据/结论 |
+| --- | --- | --- |
+| Key 鉴权 Balance 链接回填 | 通过 | `smoke:billing` 覆盖服务端 `balanceUrl=null` 的场景;结果仍返回个人 Balance 链接,且 `paymentRequest` 不含 Session Header 或 Token。 |
+| 当前账户无扣费预检 | 通过 | `billing/query` HTTP 200;当前凭证同时具备 NewAPI Key 与 Session,返回余额、7 条价目规则、51 个用量分组及个人 Balance 链接。未创建 `recharge/order`、二维码或支付请求。 |
+| 技能包综合验收 | 通过 | `acceptance`、`smoke:budget`、`smoke:request`、`audit:index` 全部通过;索引为 11 个 Skill、19 个 MCP 工具。 |
+| 旧媒体上传路径审计 | 通过 | 当前 MCP 源码与 Skill 指令只写任务目录 `assets/` 和 `media-manifest.json`;媒体项固定标记 `disabled_local_only`,未发现实际 Storage/OBS/S3 上传、预签名或完成上传调用。 |
+
+本节只确认技能包、余额查询和本地媒体缓存已具备交付条件;第 21.28 节所列 `nova-server` 发布及已支付订单的账户级幂等入账,仍是生产 P0 的最后门禁。
+
+### 21.30 2026-09-05 公网运行时复核
+
+在用户确认后端已更新并重启后,使用当前本机已配置 Session 对一个**未核验的既有预订单**执行无金额变更探针。该探针只应验证路由分支:新版本会在读取旧钱包前返回“支付订单尚未核验完成”;不会创建订单、生成支付码或增加 NewAPI quota。
+
+| 检查项 | 结果 | 证据/结论 |
+| --- | --- | --- |
+| 预订单读取 | 通过 | `AccountLog` 属于当前 Session、类型为 `recharge-wxpay`、金额为 ¥0.01,且 `isVerified=false`。因此它不能用于真实到账验收。 |
+| 已验证支付分支探针 | 未通过 | 公网 `/api/fmode/newapi/topup` 返回旧钱包“余额不足”分支(HTTP 500),没有进入新版应有的“支付订单尚未核验完成”校验。未产生任何 quota 变更。 |
+| 公网运行入口定位 | 已确认 | `server.fmode.cn` 的 `/api/fmode/*` 由传统 Node `server.js` 自动加载 `api/api-ncloud/fmode/routes.js`;生产模式可能从 `server.jsc` 运行。仅更新 `fmode-server` 的独立模块或重启代理机不会加载该路由。 |
+| 代理机核对 | 已完成 | 公开入口所在 IP 的 SSH 代理机没有 `nova-server` 进程、`server.js` 或 `server.jsc` 运行目录,因此不是该 API 的应用宿主,不能在该机器完成发布。 |
+
+#### 必须在实际应用宿主执行的发布步骤
+
+1. 拉取包含 `c61d1006` 的 `future-server` 源码,并确认 `api/api-ncloud/fmode/routes.js` 与 `api/api-ncloud/fmode/sync-fmode-api.js` 均为该版本。
+2. 若该宿主以 Bytecode 入口运行,在**与线上 Node 版本相同的环境**重新生成 `server.jsc`,然后重载 PM2 的 `nova-server` 进程;如果直接以 `server.js` 运行,只需完成源码更新后 reload。不要在代理机或独立 `fmode-server` 进程替代此步骤。
+3. 发布 `fmode-wxpay-api/dist/0.0.5/` 并重载其实际宿主,保留商户配置注入。这是微信回调验签门禁,与第 2 步的 `/api/fmode` 路由发布相互独立。
+4. 先重跑本节的未核验探针,预期为 `409` 且命中“支付订单尚未核验完成”;随后使用**原支付账户**复用已经支付的 ¥0.01 订单,验证第一次 `source=verified-payment`、第二次 `idempotent=true`、余额仅增加一次,并在 Balance 页面刷新与 checkpoint `fetchCalls=0` 后将 P0 标记为完成。

+ 13 - 13
claude-code/claude-code-voc-intelligence/install.js

@@ -2,6 +2,7 @@
 const fs = require('fs');
 const path = require('path');
 const { spawnSync } = require('child_process');
+const { auditIndex } = require('./scripts/audit-index');
 
 const ROOT = __dirname;
 const MIN_NODE_MAJOR = 18;
@@ -98,27 +99,26 @@ function checkNodeVersion() {
 }
 
 function checkFiles() {
-  [
+  const manifest = readJson('skill-package-manifest.json');
+  const required = [
     'package.json',
     '.claude-plugin/plugin.json',
     '.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',
-    'skills/fmode-image-analysis/SKILL.md',
-    'skills/voc-api-catalog/SKILL.md',
-    'skills/voc-cost-controller/SKILL.md',
     'mcp/src/tools/fmode-image-analysis.js',
     'mcp/src/tools/voc-api-catalog-run.js',
     'mcp/catalog/voc-social-endpoints.json',
     'mcp/src/server.js'
-  ].forEach(assertFile);
+  ];
+  for (const skillName of manifest.skills || []) {
+    required.push(`skills/${skillName}/SKILL.md`);
+  }
+  required.forEach(assertFile);
+
+  const audit = auditIndex(ROOT);
+  if (audit.status !== 'ok') {
+    throw new Error(`Skill package index audit failed: ${audit.issues.join('; ')}`);
+  }
 
   const packageJson = readJson('package.json');
   const pluginJson = readJson('.claude-plugin/plugin.json');

+ 5 - 0
claude-code/claude-code-voc-intelligence/mcp/src/core/api-catalog.js

@@ -93,13 +93,18 @@ function searchEndpoints({ query, platform, tag, limit = 20 } = {}) {
 }
 
 function summarizeEndpoint(endpoint) {
+  const hasCatalogBilling = endpoint && Object.prototype.hasOwnProperty.call(endpoint, 'billing');
   return {
     id: endpoint.id,
     platform: endpoint.platform,
+    channel: endpoint.channel || 'social',
     title: endpoint.title,
     summary: endpoint.summary,
     method: endpoint.method,
     proxyPath: endpoint.proxyPath,
+    billing: hasCatalogBilling ? endpoint.billing : null,
+    billingUnit: endpoint.billingUnit || 'call',
+    billingSource: hasCatalogBilling ? 'catalog' : 'gateway-default',
     deprecated: Boolean(endpoint.deprecated),
     recommendedFor: endpoint.recommendedFor || [],
     freshness: endpoint.freshness || null,

+ 11 - 4
claude-code/claude-code-voc-intelligence/mcp/src/core/billing-client.js

@@ -3,6 +3,7 @@
 const { readCredentialContext } = require('./credentials');
 const { redactSecret } = require('../providers/voc-gateway');
 const { buildRechargePaymentRequest, requestPaymentQr } = require('./recharge-client');
+const { buildTokenizedBalanceUrl } = require('./payment-links');
 
 const DEFAULT_BILLING_ROOT = 'https://server.fmode.cn';
 
@@ -15,6 +16,9 @@ function resolveBillingRoot(baseUrl) {
 async function queryBilling(input = {}) {
   const credentials = readCredentialContext(input);
   const token = credentials.newApiToken || credentials.sessionToken;
+  // billing/query may authenticate with a NewAPI key and therefore omit the
+  // Session-bound balance URL. Keep the intended current-user recharge path.
+  const sessionBalanceUrl = buildTokenizedBalanceUrl(credentials.sessionToken);
   if (!token) {
     return {
       status: 'needs_token',
@@ -93,7 +97,9 @@ async function queryBilling(input = {}) {
       assistantMessage: message,
       summary: { errorKind: kind, httpStatus: status },
       data: payload?.data || {},
-      recharge: payload?.data?.balanceUrl ? { paymentMode: 'balance_url', balanceUrl: payload.data.balanceUrl } : null,
+      recharge: (payload?.data?.balanceUrl || sessionBalanceUrl)
+        ? { paymentMode: 'balance_url', balanceUrl: payload?.data?.balanceUrl || sessionBalanceUrl }
+        : null,
       files: [],
       nextActions: [],
       warnings: [],
@@ -102,6 +108,7 @@ async function queryBilling(input = {}) {
   }
 
   const data = payload?.data || payload;
+  const balanceUrl = data.balanceUrl || sessionBalanceUrl;
   const estimate = data.estimate || {};
   let paymentRequest = null;
   let qrCodeUrl = null;
@@ -139,18 +146,18 @@ async function queryBilling(input = {}) {
   const recharge = paymentRequest
     ? {
       paymentMode: qrCodeUrl ? 'qr_and_balance_url' : 'balance_url_and_payment_request',
-      balanceUrl: data.balanceUrl || null,
+      balanceUrl: balanceUrl || null,
       ...(qrCodeUrl ? { qrCodeUrl } : {}),
       paymentRequest,
       polling: 'order_status2'
     }
-    : (data.balanceUrl ? { paymentMode: 'balance_url', balanceUrl: data.balanceUrl } : null);
+    : (balanceUrl ? { paymentMode: 'balance_url', balanceUrl } : null);
   const lines = [
     `预计 ${estimate.amountCny ?? 0} 元,需 ${estimate.quota ?? 0} quota;当前可用 ${data.balance?.availableQuota ?? 0} quota。`,
     estimate.sufficient
       ? '当前余额足够执行该计划。'
       : `余额差额 ${estimate.shortfallQuota ?? 0} quota,建议充值 ${estimate.suggestedRechargeCny ?? 0} 元。`,
-    data.balanceUrl ? `免登录余额页:${data.balanceUrl}` : ''
+    balanceUrl ? `免登录余额页:${balanceUrl}` : ''
   ].filter(Boolean);
   return {
     status: 'ok',

+ 15 - 11
claude-code/claude-code-voc-intelligence/mcp/src/core/credentials.js

@@ -162,6 +162,7 @@ function readXiaohongshuToken(input = {}) {
 function readSessionCredential(input = {}) {
   const workspaceEnv = readEnvLocalFiles();
   const claudeCreds = readJsonMaybe(path.join(os.homedir(), '.claude', 'voc-credentials.json'));
+  const fmodeConfig = readFmodeConfig();
   return firstTypedCredential([
     { value: input.sessionToken, source: 'request.sessionToken' },
     { value: input.tihaoToken, source: 'request.tihaoToken' },
@@ -173,15 +174,18 @@ function readSessionCredential(input = {}) {
     { value: input.apiToken, source: 'request.apiToken' },
     { value: workspaceEnv.FMODE_SESSION_TOKEN, source: '.env.local.FMODE_SESSION_TOKEN' },
     { value: workspaceEnv.TIHAO_SESSION_TOKEN, source: '.env.local.TIHAO_SESSION_TOKEN' },
-    { value: workspaceEnv.VOC_TOKEN, source: '.env.local.VOC_TOKEN' },
-    { value: workspaceEnv.VOC_SOCIAL_TOKEN, source: '.env.local.VOC_SOCIAL_TOKEN' },
     { value: process.env.FMODE_SESSION_TOKEN, source: 'env.FMODE_SESSION_TOKEN' },
     { value: process.env.TIHAO_SESSION_TOKEN, source: 'env.TIHAO_SESSION_TOKEN' },
-    { value: process.env.VOC_TOKEN, source: 'env.VOC_TOKEN' },
-    { value: process.env.VOC_SOCIAL_TOKEN, source: 'env.VOC_SOCIAL_TOKEN' },
     { value: claudeCreds.sessionToken, source: '~/.claude/voc-credentials.json.sessionToken' },
     { value: claudeCreds.vocToken, source: '~/.claude/voc-credentials.json.vocToken' },
-    { value: claudeCreds.token, source: '~/.claude/voc-credentials.json.token' }
+    { value: claudeCreds.token, source: '~/.claude/voc-credentials.json.token' },
+    { value: fmodeConfig.sessionToken, source: '~/.fmode/config.json.sessionToken' },
+    { value: fmodeConfig.vocToken, source: '~/.fmode/config.json.vocToken' },
+    { value: fmodeConfig.token, source: '~/.fmode/config.json.token' },
+    { value: workspaceEnv.VOC_TOKEN, source: '.env.local.VOC_TOKEN' },
+    { value: workspaceEnv.VOC_SOCIAL_TOKEN, source: '.env.local.VOC_SOCIAL_TOKEN' },
+    { value: process.env.VOC_TOKEN, source: 'env.VOC_TOKEN' },
+    { value: process.env.VOC_SOCIAL_TOKEN, source: 'env.VOC_SOCIAL_TOKEN' }
   ], isSessionToken);
 }
 
@@ -195,7 +199,7 @@ function readSessionToken(input = {}) {
 // 关键:这把 sk- 就是 Claude Code 的 env.ANTHROPIC_AUTH_TOKEN(在 ~/.claude/settings.json),
 // 默认装完技能不必再手动配置——这里直接从 settings.json / 进程环境读取它,避免「明明配了 SK 却判缺 token、被甩进老付费框」的死循环。
 // 取值优先级:显式入参 → .env.local / 环境变量 FMODE_API_KEY·NEWAPI_TOKEN
-//   → ~/.claude/voc-credentials.json 的 fmodeApiKey → ~/.fmode/config.json 的 newapiToken/fmodeApiToken
+//   → ~/.fmode/config.json 的 newapiToken/fmodeApiToken → ~/.claude/voc-credentials.json / ~/.vocmarket 的 fmode key
 //   → Claude Code env(进程注入 + ~/.claude/settings.json)的 ANTHROPIC_AUTH_TOKEN(sk-、且 base 指向 fmode)。
 function readNewApiToken(input = {}) {
   const workspaceEnv = readEnvLocalFiles();
@@ -216,15 +220,15 @@ function readNewApiToken(input = {}) {
     process.env.NEW_API_TOKEN,
     process.env.FMODE_API_KEY,
     process.env.FMODE_API_TOKEN,
-    vocMarketCreds.fmodeApiKey,
-    vocMarketCreds.fmodeApiToken,
-    vocMarketCreds.newApiToken,
-    claudeCreds.fmodeApiKey,
-    claudeCreds.fmodeApiToken,
     fmodeConfig.newapiToken,
     fmodeConfig.newApiToken,
     fmodeConfig.fmodeApiKey,
     fmodeConfig.fmodeApiToken,
+    claudeCreds.fmodeApiKey,
+    claudeCreds.fmodeApiToken,
+    vocMarketCreds.fmodeApiKey,
+    vocMarketCreds.fmodeApiToken,
+    vocMarketCreds.newApiToken,
     // Claude Code 显式写在 settings.json env 里的 fmode key(少见,但优先于自动推断的 ANTHROPIC_AUTH_TOKEN)
     claudeEnv.FMODE_API_KEY,
     claudeEnv.NEWAPI_TOKEN,

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

@@ -22,7 +22,7 @@ function buildFmodeApiRechargeUrl(options = {}) {
   return buildTokenizedBalanceUrl(resolveSessionToken(options)) || FMODE_API_BALANCE_BASE_URL;
 }
 
-async function buildVocRechargeInfo(options = {}) {
+async function buildRechargeInfo(options = {}) {
   const sessionToken = resolveSessionToken(options);
   const balanceUrl = buildTokenizedBalanceUrl(sessionToken);
   return {
@@ -115,7 +115,7 @@ function buildPermissionDeniedMessage(options = {}) {
 module.exports = {
   FMODE_API_BALANCE_BASE_URL,
   buildTokenizedBalanceUrl,
-  buildVocRechargeInfo,
+  buildRechargeInfo,
   buildMissingTokenMessage,
   buildMissingNewApiTokenMessage,
   buildRechargeRequiredMessage,

+ 62 - 4
claude-code/claude-code-voc-intelligence/mcp/src/core/recharge-client.js

@@ -3,6 +3,7 @@
 const { randomBytes } = require('crypto');
 
 const DEFAULT_PARSE_FUNCTIONS_URL = 'https://server.fmode.cn/parse/functions';
+const DEFAULT_RECHARGE_ORDER_URL = 'https://server.fmode.cn/api/fmode/recharge/order';
 const DEFAULT_PAY_COMPANY = '1AiWpTEDH9';
 const DEFAULT_NEW_API_CALLBACK = 'WbyPrzMI3A';
 
@@ -19,6 +20,7 @@ function buildRechargePaymentRequest({ amount, title = 'VOC 数据调用充值',
   const outTradeNo = tradeNo || createTradeNo();
   const parseFunctionsUrl = String(process.env.FMODE_PARSE_FUNCTIONS_URL || DEFAULT_PARSE_FUNCTIONS_URL).replace(/\/$/, '');
   const body = {
+    _ApplicationId: 'ncloudmaster',
     company: process.env.FMODE_PAY_COMPANY || DEFAULT_PAY_COMPANY,
     out_trade_no: outTradeNo,
     total_fee: Number(numericAmount.toFixed(2)),
@@ -28,7 +30,10 @@ function buildRechargePaymentRequest({ amount, title = 'VOC 数据调用充值',
   return {
     endpoint: `${parseFunctionsUrl}/pay_code2`,
     method: 'POST',
-    headers: sessionToken ? { 'X-Parse-Session-Token': sessionToken } : {},
+    headers: sessionToken ? {
+      'X-Parse-Application-Id': 'ncloudmaster',
+      'X-Parse-Session-Token': sessionToken
+    } : { 'X-Parse-Application-Id': 'ncloudmaster' },
     body,
     tradeNo: outTradeNo,
     amountCny: body.total_fee,
@@ -36,6 +41,7 @@ function buildRechargePaymentRequest({ amount, title = 'VOC 数据调用充值',
       endpoint: `${parseFunctionsUrl}/order_status2`,
       method: 'POST',
       body: {
+        _ApplicationId: 'ncloudmaster',
         out_trade_no: outTradeNo,
         company: body.company
       }
@@ -43,21 +49,71 @@ function buildRechargePaymentRequest({ amount, title = 'VOC 数据调用充值',
   };
 }
 
+function buildRechargeOrderRequest({ amount, tradeNo, sessionToken, title = 'VOC 数据调用充值' } = {}) {
+  const numericAmount = Number(amount);
+  if (!Number.isFinite(numericAmount) || numericAmount <= 0) {
+    throw new Error('充值金额必须大于 0');
+  }
+  if (!sessionToken) throw new Error('创建充值订单需要 Session Token');
+  const outTradeNo = tradeNo || createTradeNo();
+  const endpoint = String(process.env.FMODE_RECHARGE_ORDER_URL || DEFAULT_RECHARGE_ORDER_URL).replace(/\/$/, '');
+  return {
+    endpoint,
+    method: 'POST',
+    headers: {
+      Accept: 'application/json',
+      'Content-Type': 'application/json'
+    },
+    body: {
+      sessionToken,
+      amount: Number(numericAmount.toFixed(2)),
+      tradeNo: outTradeNo,
+      detail: { product: 'voc-newapi-balance', source: 'voc-skill', title: String(title).slice(0, 80) }
+    },
+    tradeNo: outTradeNo,
+    amountCny: Number(numericAmount.toFixed(2))
+  };
+}
+
 async function requestPaymentQr({ amount, title, sessionToken, tradeNo, fetchImpl = fetch } = {}) {
-  const request = buildRechargePaymentRequest({ amount, title, sessionToken, tradeNo });
+  let request = buildRechargePaymentRequest({ amount, title, sessionToken, tradeNo });
+  if (sessionToken) {
+    const orderRequest = buildRechargeOrderRequest({ amount, title, sessionToken, tradeNo: request.tradeNo });
+    const orderResponse = await fetchImpl(orderRequest.endpoint, {
+      method: orderRequest.method,
+      headers: orderRequest.headers,
+      body: JSON.stringify(orderRequest.body)
+    });
+    const orderPayload = await orderResponse.json().catch(() => ({}));
+    if (!orderResponse.ok || Number(orderPayload?.code) !== 200 || !orderPayload?.data?.tradeNo) {
+      const error = new Error(orderPayload?.mess || orderPayload?.message || `recharge/order HTTP ${orderResponse.status}`);
+      error.httpStatus = orderResponse.status;
+      throw error;
+    }
+    request = buildRechargePaymentRequest({
+      amount,
+      title,
+      sessionToken,
+      tradeNo: String(orderPayload.data.tradeNo)
+    });
+  }
   const response = await fetchImpl(request.endpoint, {
     method: request.method,
     headers: { Accept: 'application/json', 'Content-Type': 'application/json', ...request.headers },
     body: JSON.stringify(request.body)
   });
   const payload = await response.json().catch(() => ({}));
-  const codeUrl = Array.isArray(payload?.code_url) ? payload.code_url[0] : payload?.code_url;
+  // Parse Cloud functions wrap successful results in `result`; older proxies
+  // returned the same fields at the top level. Accept both response shapes.
+  const paymentPayload = payload?.result || payload?.data || payload || {};
+  const rawCodeUrl = paymentPayload.code_url ?? payload?.code_url;
+  const codeUrl = Array.isArray(rawCodeUrl) ? rawCodeUrl[0] : rawCodeUrl;
   if (!response.ok || !codeUrl) {
     const error = new Error(payload?.error || payload?.message || `pay_code2 HTTP ${response.status}`);
     error.httpStatus = response.status;
     throw error;
   }
-  const nonceStr = payload?.nonce_str || '';
+  const nonceStr = paymentPayload.nonce_str || payload?.nonce_str || '';
   return {
     ...request,
     paymentMode: 'qr',
@@ -75,7 +131,9 @@ async function requestPaymentQr({ amount, title, sessionToken, tradeNo, fetchImp
 
 module.exports = {
   DEFAULT_PARSE_FUNCTIONS_URL,
+  DEFAULT_RECHARGE_ORDER_URL,
   createTradeNo,
+  buildRechargeOrderRequest,
   buildRechargePaymentRequest,
   requestPaymentQr
 };

+ 3 - 3
claude-code/claude-code-voc-intelligence/mcp/src/features/fmode-image-analysis/image-analysis.js

@@ -2,7 +2,7 @@ const fs = require('fs');
 const path = require('path');
 const { readCredentialContext, readVocToken, readNewApiToken, readEnvFileMaybe, envLocalCandidates } = require('../../core/credentials');
 const {
-  buildVocRechargeInfo,
+  buildRechargeInfo,
   buildMissingTokenMessage,
   buildRechargeRequiredMessage,
   buildPermissionDeniedMessage
@@ -34,7 +34,7 @@ async function analyzeFmodeImage(input = {}) {
   const credentials = readCredentialContext(input);
   const token = resolveFmodeToken(input);
   if (!token) {
-    const recharge = await buildVocRechargeInfo();
+    const recharge = await buildRechargeInfo();
     return friendlyResult('needs_token', buildMissingTokenMessage(recharge.paymentUrl, recharge), {
       summary: { imageCount: images.length, model: provider.model, errorKind: 'missing_token', recoverable: true },
       data: { recharge, model: provider.model, imageCount: images.length },
@@ -74,7 +74,7 @@ async function analyzeFmodeImage(input = {}) {
   }
 
   if (response.status === 402) {
-    const recharge = await buildVocRechargeInfo({ sessionToken: credentials.sessionToken });
+    const recharge = await buildRechargeInfo({ sessionToken: credentials.sessionToken });
     const rechargeStatus = recharge.personalized ? 'needs_recharge' : 'needs_session';
     return friendlyResult(rechargeStatus, buildRechargeRequiredMessage(recharge.paymentUrl, recharge, { platformLabel: '图片分析' }), {
       summary: {

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

@@ -13,7 +13,7 @@ const { contextForTool, createStepContext, usageHeaders, writeUsageContext } = r
 const { createBatchBudgetGuard, findBudgetIssue, budgetStoppedMessage } = require('../core/batch-budget-guard');
 const { runVocCostEstimate } = require('./voc-cost-estimate-run');
 const {
-  buildVocRechargeInfo,
+  buildRechargeInfo,
   buildMissingNewApiTokenMessage,
   buildRechargeRequiredMessage
 } = require('../core/payment-links');
@@ -256,7 +256,7 @@ async function runDouyinTrend(input = {}) {
       ? credentials.sessionToken
       : '';
     if (!token) {
-      const recharge = await buildVocRechargeInfo();
+      const recharge = await buildRechargeInfo();
       return {
         status: 'needs_token',
         assistantMessage: buildMissingNewApiTokenMessage(recharge.paymentUrl, recharge, { platformLabel: '抖音' }),
@@ -344,7 +344,7 @@ async function runDouyinTrend(input = {}) {
     const collectionIssue = findGatewayError([...errors, ...warnings]);
     budgetIssue = findBudgetIssue(errors);
     if (collectionIssue) {
-      const recharge = await buildVocRechargeInfo({ sessionToken: credentials.sessionToken });
+      const recharge = await buildRechargeInfo({ sessionToken: credentials.sessionToken });
       if (dataset.videos.length) {
         const mediaResult = await processMedia(dataset, {
           outputDir,
@@ -422,7 +422,7 @@ async function runDouyinTrend(input = {}) {
     if (!dataset.videos.length) {
       if (budgetIssue) {
         const recharge = budgetIssue.budget?.status === 'needs_recharge'
-          ? await buildVocRechargeInfo({
+          ? await buildRechargeInfo({
             sessionToken: rechargeSessionToken,
             shortageCny: budgetIssue.budget.shortfallCny,
             suggestedRechargeCny: budgetIssue.budget.suggestedRechargeCny
@@ -512,7 +512,7 @@ async function runDouyinTrend(input = {}) {
 
       const gatewayError = findGatewayError(errors);
       if (gatewayError) {
-        const recharge = await buildVocRechargeInfo({ sessionToken: credentials.sessionToken });
+        const recharge = await buildRechargeInfo({ sessionToken: credentials.sessionToken });
         return {
           status: rechargeStatusFor(recharge),
           assistantMessage: buildRechargeRequiredMessage(recharge.paymentUrl, recharge, { platformLabel: '抖音' }),
@@ -665,7 +665,7 @@ async function runDouyinTrend(input = {}) {
     ];
 
   const budgetRecharge = budgetIssue?.budget?.status === 'needs_recharge'
-    ? await buildVocRechargeInfo({
+    ? await buildRechargeInfo({
       sessionToken: rechargeSessionToken || input.sessionToken || input.vocToken,
       shortageCny: budgetIssue.budget.shortfallCny,
       suggestedRechargeCny: budgetIssue.budget.suggestedRechargeCny

+ 12 - 7
claude-code/claude-code-voc-intelligence/mcp/src/tools/voc-api-catalog-run.js

@@ -12,7 +12,7 @@ const { callSocialGateway, redactSecret } = require('../providers/voc-gateway');
 const { callEcommerceGateway } = require('../providers/ecommerce-gateway');
 const { callOverseasGateway } = require('../providers/overseas-gateway');
 const {
-  buildVocRechargeInfo,
+  buildRechargeInfo,
   buildMissingNewApiTokenMessage,
   buildRechargeRequiredMessage,
   buildPermissionDeniedMessage
@@ -48,12 +48,12 @@ async function searchVocApis(input = {}) {
     catalog.gateway?.overseas?.baseUrl
       ? `海外电商选品网关:${catalog.gateway.overseas.baseUrl}(亚马逊海外站点类目树/Best Seller·产品详情与销量·ABA 关键词·评论·数据监控等选品数据,channel=overseas)。`
       : '',
-    '通用转发:任意 proxyPath 透传到对应上游,每次调用计费 1 次;通道由接口的 channel 字段决定,未登记接口可传 channel 覆盖。',
+    '通用转发:任意 proxyPath 透传到对应上游;通道由接口的 channel 字段决定,未登记接口可传 channel 覆盖。billing 只表示调用单位,人民币价格以 voc_billing_query 返回的服务端价目为准。',
     query ? `匹配关键词「${query}」的接口(${results.length} 条):` : `全部已登记接口(${results.length} 条):`,
     '',
     ...results.map(
       item =>
-        `- ${item.id} | ${item.title}(${item.method} ${item.proxyPath})必填参数: ${item.requiredParams.join(', ') || '无'}`
+        `- ${item.id} | ${item.title}(${item.method} ${item.proxyPath},通道 ${item.channel},计费单位 ${item.billing ?? 1} ${item.billingUnit})必填参数: ${item.requiredParams.join(', ') || '无'}`
     ),
     '',
     '下一步:用 voc_api_doc 读取某个接口的详细参数文档,再用 voc_api_call 传参调用。',
@@ -128,11 +128,15 @@ function buildRequestParts(endpoint, input) {
     return where === 'query' ? query : body;
   };
   for (const param of endpoint.params || []) {
+    const hasDefault = param.default !== undefined;
     let value = resolveParamValue(param.name, params, input);
     if ((value === undefined || value === null || value === '') && param.default !== undefined) {
       value = param.default;
     }
-    if (value === undefined || value === null || value === '') {
+    // Empty defaults are meaningful for a few upstream contracts (for
+    // example Douyin's search_id/backtrace on the first page). Preserve them
+    // while still treating an omitted required value as missing.
+    if (value === undefined || value === null || (value === '' && !hasDefault)) {
       if (param.required) missing.push(param.name);
       continue;
     }
@@ -199,7 +203,7 @@ async function callVocApi(input = {}) {
   const token = newapiToken || sessionToken;
   const fallbackToken = (newapiToken && sessionToken && newapiToken !== sessionToken) ? sessionToken : '';
   if (!token) {
-    const recharge = await buildVocRechargeInfo();
+    const recharge = await buildRechargeInfo();
     return {
       status: 'needs_token',
       assistantMessage: buildMissingNewApiTokenMessage(recharge.paymentUrl, recharge, { platformLabel: '社交平台' }),
@@ -337,7 +341,7 @@ async function callVocApi(input = {}) {
 
     if (kind === 'auth') {
       // sk- 与 sessionToken(若有)都未通过校验。不把底层「未入仓」/鉴权报错甩给用户,只给友好提示。
-      const recharge = await buildVocRechargeInfo({ sessionToken });
+      const recharge = await buildRechargeInfo({ sessionToken });
       return {
         status: 'needs_valid_token',
         assistantMessage: [
@@ -361,7 +365,7 @@ async function callVocApi(input = {}) {
       };
     }
     if (kind === 'billing') {
-      const recharge = await buildVocRechargeInfo({ sessionToken });
+      const recharge = await buildRechargeInfo({ sessionToken });
       const rechargeStatus = recharge.personalized ? 'needs_recharge' : 'needs_session';
       return {
         status: rechargeStatus,
@@ -444,6 +448,7 @@ module.exports = {
   searchVocApis,
   getVocApiDoc,
   callVocApi,
+  buildRequestParts,
   findRecordTime,
   assessFreshness
 };

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

@@ -13,7 +13,7 @@ const { contextForTool, createStepContext, usageHeaders, writeUsageContext } = r
 const { createBatchBudgetGuard, findBudgetIssue, budgetStoppedMessage } = require('../core/batch-budget-guard');
 const { runVocCostEstimate } = require('./voc-cost-estimate-run');
 const {
-  buildVocRechargeInfo,
+  buildRechargeInfo,
   buildMissingNewApiTokenMessage,
   buildRechargeRequiredMessage
 } = require('../core/payment-links');
@@ -226,7 +226,7 @@ async function runXiaohongshuTrend(input = {}) {
       ? credentials.sessionToken
       : '';
     if (!token) {
-      const recharge = await buildVocRechargeInfo();
+      const recharge = await buildRechargeInfo();
       return {
         status: 'needs_token',
         assistantMessage: buildMissingNewApiTokenMessage(recharge.paymentUrl, recharge, { platformLabel: '小红书' }),
@@ -309,7 +309,7 @@ async function runXiaohongshuTrend(input = {}) {
     const collectionIssue = findGatewayError([...errors, ...warnings]);
     budgetIssue = findBudgetIssue(errors);
     if (collectionIssue) {
-      const recharge = await buildVocRechargeInfo({ sessionToken: credentials.sessionToken });
+      const recharge = await buildRechargeInfo({ sessionToken: credentials.sessionToken });
       if (dataset.notes.length) {
         const mediaResult = await processMedia(dataset, {
           outputDir,
@@ -387,7 +387,7 @@ async function runXiaohongshuTrend(input = {}) {
     if (!dataset.notes.length) {
       if (budgetIssue) {
         const recharge = budgetIssue.budget?.status === 'needs_recharge'
-          ? await buildVocRechargeInfo({
+          ? await buildRechargeInfo({
             sessionToken: credentials.sessionToken,
             shortageCny: budgetIssue.budget.shortfallCny,
             suggestedRechargeCny: budgetIssue.budget.suggestedRechargeCny
@@ -455,7 +455,7 @@ async function runXiaohongshuTrend(input = {}) {
 
       const gatewayError = findGatewayError(errors);
       if (gatewayError) {
-        const recharge = await buildVocRechargeInfo({ sessionToken: credentials.sessionToken });
+        const recharge = await buildRechargeInfo({ sessionToken: credentials.sessionToken });
         return {
           status: rechargeStatusFor(recharge),
           assistantMessage: buildRechargeRequiredMessage(recharge.paymentUrl, recharge),
@@ -608,7 +608,7 @@ async function runXiaohongshuTrend(input = {}) {
     ];
 
   const budgetRecharge = budgetIssue?.budget?.status === 'needs_recharge'
-    ? await buildVocRechargeInfo({
+    ? await buildRechargeInfo({
       sessionToken: rechargeSessionToken || input.sessionToken || input.vocToken,
       shortageCny: budgetIssue.budget.shortfallCny,
       suggestedRechargeCny: budgetIssue.budget.suggestedRechargeCny

+ 5 - 1
claude-code/claude-code-voc-intelligence/package.json

@@ -18,6 +18,8 @@
     "smoke:trend-budget": "node scripts/smoke-trend-budget.js",
     "smoke:cost": "node scripts/smoke-cost-estimate.js",
     "smoke:media": "node scripts/smoke-media-pipeline.js",
+    "smoke:request": "node scripts/smoke-voc-request-shaping.js",
+    "audit:index": "node scripts/audit-index.js",
     "xhs:sample": "node mcp/src/tools/xiaohongshu-trend-run.js --collection-mode sample --profile memory-templates/xiaohongshu-trend-profile.json --output ../outputs/claude-code-xhs-sample --result-prefix XHS_TREND_RESULT",
     "xhs:preference-smoke": "node mcp/src/tools/xiaohongshu-preference-update.js --message \"保留奶油风和全屋定制翻车方向,不要纯风格美图,下一个版本更偏门店转化话术。\" --memory ../outputs/claude-code-xhs-memory-smoke/xiaohongshu-trend-memory.json --result-prefix XHS_PREF_RESULT",
     "xhs:sample-with-memory": "node mcp/src/tools/xiaohongshu-trend-run.js --collection-mode sample --profile memory-templates/xiaohongshu-trend-profile.json --memory ../outputs/claude-code-xhs-memory-smoke/xiaohongshu-trend-memory.json --output ../outputs/claude-code-xhs-memory-smoke/report --result-prefix XHS_TREND_RESULT",
@@ -44,7 +46,9 @@
     "bin/",
     "docs/",
     "install.js",
-    "mcp/",
+    "mcp/src/",
+    "mcp/catalog/params/",
+    "mcp/catalog/voc-social-endpoints.json",
     "memory-templates/",
     "package.json",
     "README.md",

+ 4 - 12
claude-code/claude-code-voc-intelligence/scripts/acceptance.js

@@ -10,6 +10,7 @@ const npmCache = path.join(os.tmpdir(), 'claude-voc-npm-cache-acceptance');
 function main() {
   fs.mkdirSync(npmCache, { recursive: true });
 
+  run('npm', ['run', 'audit:index'], ROOT);
   run('npm', ['run', 'fmode:image:smoke'], ROOT);
   run('npm', ['run', 'smoke:budget'], ROOT);
   run('npm', ['run', 'smoke:trend-budget'], ROOT);
@@ -24,18 +25,9 @@ function main() {
   const mcpFile = path.join(workspace, '.mcp.json');
   const pluginRoot = path.join(workspace, '.claude', 'plugins', 'voc-intelligence');
   const pluginServer = path.join(pluginRoot, 'mcp', 'src', 'server.js');
-  const skillNames = [
-    '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',
-    'fmode-image-analysis',
-    'voc-cost-controller'
-  ];
+  const manifest = JSON.parse(fs.readFileSync(path.join(ROOT, 'skill-package-manifest.json'), 'utf8'));
+  const skillNames = Array.isArray(manifest.skills) ? manifest.skills : [];
+  assert(skillNames.length > 0, 'manifest should list at least one skill');
 
   assert(fs.existsSync(mcpFile), 'workspace install should write .mcp.json');
   assert(fs.existsSync(pluginServer), 'workspace install should copy plugin server');

+ 153 - 0
claude-code/claude-code-voc-intelligence/scripts/audit-index.js

@@ -0,0 +1,153 @@
+#!/usr/bin/env node
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+
+function readJson(filePath) {
+  return JSON.parse(fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, ''));
+}
+
+function sorted(values) {
+  return [...values].sort((a, b) => String(a).localeCompare(String(b)));
+}
+
+function diff(expected, actual) {
+  const expectedSet = new Set(expected);
+  const actualSet = new Set(actual);
+  return {
+    missing: sorted(expected.filter(value => !actualSet.has(value))),
+    extra: sorted(actual.filter(value => !expectedSet.has(value)))
+  };
+}
+
+function duplicateValues(values) {
+  const seen = new Set();
+  const duplicates = new Set();
+  for (const value of values) {
+    if (seen.has(value)) duplicates.add(value);
+    seen.add(value);
+  }
+  return sorted([...duplicates]);
+}
+
+function listSkillNames(root) {
+  const skillsRoot = path.join(root, 'skills');
+  if (!fs.existsSync(skillsRoot)) return [];
+  return sorted(fs.readdirSync(skillsRoot, { withFileTypes: true })
+    .filter(entry => entry.isDirectory() && fs.existsSync(path.join(skillsRoot, entry.name, 'SKILL.md')))
+    .map(entry => entry.name));
+}
+
+function listCatalogBackups(root) {
+  const catalogRoot = path.join(root, 'mcp', 'catalog');
+  if (!fs.existsSync(catalogRoot)) return [];
+  return fs.readdirSync(catalogRoot, { withFileTypes: true })
+    .filter(entry => entry.isFile() && /\.bak-[^/\\]+$/i.test(entry.name))
+    .map(entry => path.relative(root, path.join(catalogRoot, entry.name)).replace(/\\/g, '/'))
+    .sort();
+}
+
+function listRegisteredTools(root) {
+  const serverPath = path.join(root, 'mcp', 'src', 'server.js');
+  if (!fs.existsSync(serverPath)) return [];
+  const source = fs.readFileSync(serverPath, 'utf8');
+  return [...source.matchAll(/registerTool\(\s*['"]([^'"]+)['"]/g)].map(match => match[1]);
+}
+
+function auditIndex(root = path.resolve(__dirname, '..')) {
+  const issues = [];
+  const warnings = [];
+  const manifestPath = path.join(root, 'skill-package-manifest.json');
+  const packagePath = path.join(root, 'package.json');
+  const lockPath = path.join(root, 'package-lock.json');
+
+  if (!fs.existsSync(manifestPath)) issues.push('missing skill-package-manifest.json');
+  if (!fs.existsSync(packagePath)) issues.push('missing package.json');
+  if (issues.length) return { status: 'error', root, issues, warnings };
+
+  const manifest = readJson(manifestPath);
+  const packageJson = readJson(packagePath);
+  const actualSkills = listSkillNames(root);
+  const actualTools = listRegisteredTools(root);
+  const manifestSkills = Array.isArray(manifest.skills) ? manifest.skills : [];
+  const manifestTools = Array.isArray(manifest.mcpTools) ? manifest.mcpTools : [];
+
+  const skillDiff = diff(manifestSkills, actualSkills);
+  const toolDiff = diff(manifestTools, actualTools);
+  if (skillDiff.missing.length || skillDiff.extra.length) {
+    issues.push(`skill index mismatch: missing=[${skillDiff.missing.join(', ')}], extra=[${skillDiff.extra.join(', ')}]`);
+  }
+  if (toolDiff.missing.length || toolDiff.extra.length) {
+    issues.push(`MCP tool index mismatch: missing=[${toolDiff.missing.join(', ')}], extra=[${toolDiff.extra.join(', ')}]`);
+  }
+  if (duplicateValues(manifestSkills).length) {
+    issues.push(`duplicate skills in manifest: ${duplicateValues(manifestSkills).join(', ')}`);
+  }
+  if (duplicateValues(manifestTools).length) {
+    issues.push(`duplicate MCP tools in manifest: ${duplicateValues(manifestTools).join(', ')}`);
+  }
+  if (manifest.entrySkill && !manifestSkills.includes(manifest.entrySkill)) {
+    issues.push(`entrySkill is not listed in skills: ${manifest.entrySkill}`);
+  }
+
+  const referenceFields = [
+    'apiCatalog',
+    'capabilityMap',
+    'taobaoV7Doc',
+    'bossPromptTemplates',
+    'liveAcceptanceChecklist',
+    'troubleshootingPlaybook',
+    'paymentLinksDoc',
+    'contextBudgetDoc',
+    'costMeteringSpec',
+    'apiErrorCodesDoc'
+  ];
+  for (const field of referenceFields) {
+    if (!manifest[field]) continue;
+    const target = path.join(root, manifest[field]);
+    if (!fs.existsSync(target)) issues.push(`manifest.${field} points to missing file: ${manifest[field]}`);
+  }
+
+  if (packageJson.version !== manifest.version) {
+    issues.push(`version mismatch: package.json=${packageJson.version}, manifest=${manifest.version}`);
+  }
+  if (fs.existsSync(lockPath)) {
+    const lock = readJson(lockPath);
+    if (lock.version && lock.version !== packageJson.version) {
+      issues.push(`version mismatch: package-lock.json=${lock.version}, package.json=${packageJson.version}`);
+    }
+    if (lock.packages && lock.packages[''] && lock.packages[''].version !== packageJson.version) {
+      issues.push(`version mismatch: package-lock root=${lock.packages[''].version}, package.json=${packageJson.version}`);
+    }
+  }
+
+  const backups = listCatalogBackups(root);
+  if (backups.length) {
+    warnings.push(`local catalog backups detected; release builders must exclude: ${backups.join(', ')}`);
+  }
+
+  return {
+    status: issues.length ? 'error' : 'ok',
+    root,
+    version: packageJson.version,
+    skills: actualSkills,
+    mcpTools: actualTools,
+    warnings,
+    issues
+  };
+}
+
+function main() {
+  const result = auditIndex();
+  console.log(JSON.stringify(result, null, 2));
+  process.exitCode = result.status === 'ok' ? 0 : 1;
+}
+
+if (require.main === module) main();
+
+module.exports = {
+  auditIndex,
+  listRegisteredTools,
+  listSkillNames
+};

+ 27 - 3
claude-code/claude-code-voc-intelligence/scripts/billing-client-smoke.js

@@ -16,7 +16,9 @@ async function main() {
         code: 200,
         data: {
           balance: { availableQuota: 1000 },
-          balanceUrl: billingCalls > 1 ? 'https://app.fmode.cn/dev/studio/balance/?token=r%3Asession-test' : null,
+          // NewAPI-key authentication intentionally omits this Session-bound
+          // link. The client must build it locally from the current Session.
+          balanceUrl: null,
           estimate: {
             quota: 2000,
             amountCny: 1,
@@ -29,10 +31,15 @@ async function main() {
         }
       }), { status: 200, headers: { 'content-type': 'application/json' } });
     }
+    if (/\/api\/fmode\/recharge\/order$/.test(String(url))) {
+      assert.equal(options.method, 'POST');
+      assert.match(String(options.body || ''), /sessionToken/);
+      return new Response(JSON.stringify({ code: 200, data: { tradeNo: 'trade-query' } }), { status: 200 });
+    }
     assert.match(String(url), /\/parse\/functions\/pay_code2$/);
     assert.equal(options.method, 'POST');
     assert.match(options.headers['X-Parse-Session-Token'], /^r:/);
-    return new Response(JSON.stringify({ code_url: ['weixin://pay/query-test'], nonce_str: 'nonce-query' }), { status: 200 });
+    return new Response(JSON.stringify({ result: { code_url: ['weixin://pay/query-test'], nonce_str: 'nonce-query' } }), { status: 200 });
   };
   const result = await queryBilling({
     newapiToken: 'sk-test',
@@ -46,6 +53,18 @@ async function main() {
     assert.doesNotMatch(JSON.stringify(result), /r:[A-Za-z0-9_-]{8,}/);
   }
 
+  const fallbackResult = await queryBilling({
+    newapiToken: 'sk-test',
+    sessionToken: 'r:session-test',
+    baseUrl: 'https://billing.example.test',
+    operations: [{ model: 'voc-social', count: 1 }]
+  });
+  assert.equal(fallbackResult.status, 'ok');
+  assert.equal(fallbackResult.recharge.paymentMode, 'balance_url_and_payment_request');
+  assert.match(fallbackResult.recharge.balanceUrl, /balance\/\?token=r%3Asession-test/);
+  assert.match(fallbackResult.assistantMessage, /免登录余额页:https:\/\/app\.fmode\.cn\/dev\/studio\/balance/);
+  assert.doesNotMatch(JSON.stringify(fallbackResult.recharge.paymentRequest), /r:[A-Za-z0-9_-]{8,}/);
+
   const qrResult = await queryBilling({
     newapiToken: 'sk-test',
     sessionToken: 'r:session-test',
@@ -63,8 +82,13 @@ async function main() {
     amount: 2,
     sessionToken: 'r:session-test',
     fetchImpl: async (_url, options) => {
+      if (String(_url).endsWith('/api/fmode/recharge/order')) {
+        assert.equal(options.method, 'POST');
+        return new Response(JSON.stringify({ code: 200, data: { tradeNo: 'trade-qr' } }), { status: 200 });
+      }
       assert.match(options.headers['X-Parse-Session-Token'], /^r:/);
-      return new Response(JSON.stringify({ code_url: ['weixin://pay/test'], nonce_str: 'nonce' }), { status: 200 });
+      assert.equal(options.headers['X-Parse-Application-Id'], 'ncloudmaster');
+      return new Response(JSON.stringify({ result: { code_url: ['weixin://pay/test'], nonce_str: 'nonce' } }), { status: 200 });
     }
   });
   assert.equal(qr.qrCodeUrl, 'weixin://pay/test');

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

@@ -295,6 +295,10 @@ async function main() {
     if (apiSearchStructured.status !== 'ok' || !apiSearchStructured.data?.endpoints?.length) {
       throw new Error(`Expected voc_api_search to list douyin endpoints, got: ${JSON.stringify(apiSearchStructured)}`);
     }
+    const firstApiEndpoint = apiSearchStructured.data.endpoints[0];
+    if (!firstApiEndpoint || !firstApiEndpoint.channel || !Object.prototype.hasOwnProperty.call(firstApiEndpoint, 'billing') || !firstApiEndpoint.billingUnit) {
+      throw new Error(`Expected voc_api_search summaries to expose channel and billing metadata, got: ${JSON.stringify(firstApiEndpoint)}`);
+    }
 
     const taobaoDetailSearchResult = await client.callTool({
       name: 'voc_api_search',
@@ -355,16 +359,16 @@ async function main() {
       throw new Error(`Expected voc_api_doc to return douyin search doc, got: ${JSON.stringify(apiDocStructured)}`);
     }
 
-    const apiCallSentinel = 'sk-SMOKE_WRONG_TOKEN_TYPE';
+    const apiCallSentinel = 'sk-test-invalid';
     const apiCallResult = await client.callTool({
       name: 'voc_api_call',
-      arguments: { id: 'douyin.search_general', params: { keyword: '律师' }, newapiToken: apiCallSentinel, vocToken: 'r:SMOKE_INVALID_SESSION_TOKEN' }
+      arguments: { id: 'douyin.search_general', params: { keyword: '律师' }, newapiToken: apiCallSentinel, vocToken: 'r:invalid1' }
     });
     const apiCallStructured = apiCallResult.structuredContent || {};
     if (apiCallStructured.status !== 'needs_valid_token') {
       throw new Error(`Expected voc_api_call to map invalid sk-/r: tokens to needs_valid_token (token problem, not 余额不足), got: ${JSON.stringify(apiCallStructured)}`);
     }
-    if (JSON.stringify(apiCallStructured).includes(apiCallSentinel) || JSON.stringify(apiCallStructured).includes('r:SMOKE_INVALID_SESSION_TOKEN')) {
+    if (JSON.stringify(apiCallStructured).includes(apiCallSentinel) || JSON.stringify(apiCallStructured).includes('r:invalid1')) {
       throw new Error('voc_api_call leaked the request-scoped token');
     }
 

+ 26 - 0
claude-code/claude-code-voc-intelligence/scripts/smoke-voc-request-shaping.js

@@ -0,0 +1,26 @@
+'use strict';
+
+const assert = require('node:assert/strict');
+const { buildRequestParts } = require('../mcp/src/tools/voc-api-catalog-run');
+
+const result = buildRequestParts({
+  method: 'POST',
+  paramsIn: 'body',
+  params: [
+    { name: 'keyword', required: true, in: 'body' },
+    { name: 'cursor', required: false, in: 'body', default: 0 },
+    { name: 'search_id', required: false, in: 'body', default: '' },
+    { name: 'backtrace', required: false, in: 'body', default: '' },
+    { name: 'optional_without_default', required: false, in: 'body' }
+  ]
+}, { params: { keyword: 'probe' } });
+
+assert.deepEqual(result.missing, []);
+assert.deepEqual(result.body, {
+  keyword: 'probe',
+  cursor: 0,
+  search_id: '',
+  backtrace: ''
+});
+assert.equal(Object.prototype.hasOwnProperty.call(result.body, 'optional_without_default'), false);
+console.log('VOC request shaping smoke passed');

+ 9 - 3
claude-code/claude-code-voc-intelligence/skill-package-manifest.json

@@ -94,7 +94,7 @@
     "voc_usage_report"
   ],
   "apiCatalog": "mcp/catalog/voc-social-endpoints.json",
-  "entrySkill": "xiaohongshu-trend-intelligence",
+  "entrySkill": "voc-business-workflow",
   "capabilityMap": "docs/capability-map.md",
   "taobaoV7Doc": "docs/taobao-item-detail-v7.md",
   "bossPromptTemplates": "docs/boss-prompt-templates.md",
@@ -102,6 +102,8 @@
   "troubleshootingPlaybook": "docs/claude-code-troubleshooting-playbook.md",
   "paymentLinksDoc": "docs/payment-package-links.md",
   "contextBudgetDoc": "docs/context-budget-implementation-plan.md",
+  "costMeteringSpec": "docs/voc-cost-metering-and-balance-implementation-spec.md",
+  "apiErrorCodesDoc": "skills/voc-api-catalog/references/error-codes.md",
   "primaryUseCases": [
     "First-round Xiaohongshu or Douyin trend intelligence from sample or live collection",
     "Evidence-first customer concern and opportunity reports",
@@ -111,6 +113,8 @@
     "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",
+    "Interface catalog lookup and generic VOC gateway calls across social, ecommerce, and overseas channels",
+    "Preflight cost estimates, balance checks, budget controls, and post-run usage reports",
     "Fmode Doubao vision image analysis for screenshots, product images, UI captures, charts, posters, and image text",
     "Multi-turn preference and scoped memory so repeated use gets more tailored",
     "Friendly token and recharge handling for live collection"
@@ -118,7 +122,9 @@
   "recommendedUserPrompts": [
     "帮我做一份家装全屋定制行业的小红书趋势情报。先用演示样例跑通流程,不要真实采集。",
     "帮我看下家居定制行业抖音上用户最近在吐槽什么,先小规模真实采集。",
-    "继续深挖“怎么选”这个问题,站在老板视角告诉我怎么解决。"
+    "继续深挖“怎么选”这个问题,站在老板视角告诉我怎么解决。",
+    "监听 280 个账号并生成日报,先给我经济版、标准版和完整版的预计成本、余额风险和降本建议。",
+    "查一下这份报告实际调用了哪些接口、各调用多少次、分别消耗了多少额度。"
   ],
   "sampleCommand": "npm run xhs:sample",
   "mcpSmokeCommand": "npm run mcp:smoke",
@@ -127,5 +133,5 @@
   "npxInstallCommand": "npx @vocmarket/voc-skill install",
   "workspaceInstallCommand": "npx @vocmarket/voc-skill 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, VOC business workflow, and Fmode image analysis so VSCode Claude Code can discover the workflows without relying only on --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, VOC business workflow, Fmode image analysis, API catalog, and cost controller so VSCode Claude Code can discover all workflows without relying only on --plugin-dir."
 }

+ 1 - 1
claude-code/claude-code-voc-intelligence/skills/voc-api-catalog/SKILL.md

@@ -89,7 +89,7 @@ VOC 后端是一个**通用转发网关**(`https://server.fmode.cn/api/voc-soc
 
 > **这把 `sk-` 计费 token 是什么、从哪来**:它就是 **Claude Code 自己的 `ANTHROPIC_AUTH_TOKEN`**——配在用户级 **`~/.claude/settings.json` 的 `env.ANTHROPIC_AUTH_TOKEN`**(`sk-` 开头,且 `ANTHROPIC_BASE_URL` 指向 `api.fmode.cn`)。Claude Code 用户**装完技能即用,无需任何额外配置**:工具会自动从这里(以及进程环境)读到它。所以 newapi(fmode-api)≠ 某个要单独开通的东西,它用的就是你正在用的这把 fmode key。
 
-- 取值优先级:入参 `newapiToken`/`fmodeApiKey` → `.env.local` / 环境变量 `FMODE_API_KEY`·`NEWAPI_TOKEN` → `~/.claude/voc-credentials.json` 的 `fmodeApiKey` → `~/.fmode/config.json` 的 `newapiToken`/`fmodeApiToken` → **`~/.claude/settings.json` 的 `env.ANTHROPIC_AUTH_TOKEN`(Claude Code 默认入口,`sk-` 开头)**。
+- 取值优先级:入参 `newapiToken`/`fmodeApiKey` → `.env.local` / 环境变量 `FMODE_API_KEY`·`NEWAPI_TOKEN` → `~/.fmode/config.json` 的 `newapiToken`/`fmodeApiToken` → `~/.claude/voc-credentials.json` / `~/.vocmarket/credentials.json` 的 fmode key → **`~/.claude/settings.json` 的 `env.ANTHROPIC_AUTH_TOKEN`(Claude Code 默认入口,`sk-` 开头)**。优先保持 Session 与 NewAPI key 来自同一份 FmodeStudio 配置,避免跨账户查询。
 - `sk-` 鉴权失败(401,含「未入仓」类报错)且存在 `r:` 开头的会话 token 时,工具会**自动回退**用 `r:` sessionToken 通过同一 VOC 网关重试一次。服务端会把该 Session 映射到当前用户的 NewAPI 计费身份;这不是旧 APIG 扣费路径,也不会建立第二套余额。
 - 兼容:若把 `sk-` 直接填进 `VOC_TOKEN`,也会被当作 NewAPI token 使用。
 - token **绝不回显**;底层鉴权/入仓原始报错不甩给用户,只给友好提示。

+ 1 - 1
claude-code/claude-code-voc-intelligence/skills/voc-api-catalog/references/error-codes.md

@@ -51,7 +51,7 @@ HTTP 403  {"code":403,"mess":"用户禁用或无权限"}
 > **这把 `sk-` 是什么**:它就是 **Claude Code 自己的 `ANTHROPIC_AUTH_TOKEN`**(配在 `~/.claude/settings.json` 的 `env`,`sk-` 开头,`ANTHROPIC_BASE_URL` 指向 `api.fmode.cn`)。所以 Claude Code 用户**装完即用、零额外配置**,工具会自动从 settings.json / 进程环境读到它。newapi(fmode-api)不是要单独开通的东西,就是你正在用的这把 fmode key。
 
 1. **优先**用 NewAPI 的 `sk-` token,取值优先级:
-   入参 `newapiToken/fmodeApiKey` → `.env.local` / 环境变量 `FMODE_API_KEY`/`NEWAPI_TOKEN` → `~/.claude/voc-credentials.json` 的 `fmodeApiKey` → `~/.fmode/config.json` 的 `newapiToken/fmodeApiToken` → **`~/.claude/settings.json` 的 `env.ANTHROPIC_AUTH_TOKEN`(Claude Code 默认入口)**。
+   入参 `newapiToken/fmodeApiKey` → `.env.local` / 环境变量 `FMODE_API_KEY`/`NEWAPI_TOKEN` → `~/.fmode/config.json` 的 `newapiToken/fmodeApiToken` → `~/.claude/voc-credentials.json` / `~/.vocmarket/credentials.json` 的 fmode key → **`~/.claude/settings.json` 的 `env.ANTHROPIC_AUTH_TOKEN`(Claude Code 默认入口)**。优先保持 Session 与 NewAPI key 的账户一致。
    - 兼容历史:若 `VOC_TOKEN` 槽里填的就是 `sk-`,也当作 NewAPI token 使用。
    - 若工具报 `needs_token`/`needs_valid_token`:先去 `~/.claude/settings.json` 取 `env.ANTHROPIC_AUTH_TOKEN`,用 `FMODE_API_KEY=sk-…` 传入重试——**这是缺 token 的标准自救,不是去点充值**。
 2. 若 `sk-` token **鉴权失败(kind=auth / 401,含「未入仓」类报错)** 且存在平台 `r:` sessionToken,**自动回退** sessionToken 重试一次。这正是规避「用户拿 token 请求时遇到入仓错误」的方式——不再因为 token 是 `sk-` 就直接拒绝。

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

@@ -1,9 +1,9 @@
 {
   "name": "claude-code-voc-intelligence",
   "version": "0.3.33",
-  "generatedAt": "2026-09-02T04:57:05.870Z",
+  "generatedAt": "2026-09-05T04:23:42.586Z",
   "packageZip": "dist/claude-code-voc-intelligence.zip",
-  "packageZipBytes": 2333932,
+  "packageZipBytes": 593524,
   "plugin": "voc-intelligence",
   "skills": [
     "xiaohongshu-trend-intelligence",
@@ -39,7 +39,7 @@
     "voc_cost_estimate",
     "voc_usage_report"
   ],
-  "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, VOC business workflow, and Fmode image analysis so VSCode Claude Code can discover the workflows without relying only on --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, VOC business workflow, Fmode image analysis, API catalog, and cost controller so VSCode Claude Code can discover all workflows without relying only on --plugin-dir.",
   "installCommand": "node install.js",
   "npmInstallCommand": "npm install -g @vocmarket/voc-skill && claude-voc install",
   "npxInstallCommand": "npx @vocmarket/voc-skill install",
@@ -48,5 +48,5 @@
   "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": 176
+  "copiedFiles": 169
 }

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


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

@@ -1,10 +1,10 @@
 {
   "name": "@vocmarket/voc-skill",
   "version": "0.3.33",
-  "generatedAt": "2026-09-02T04:58:52.704Z",
+  "generatedAt": "2026-09-03T03:12:13.888Z",
   "tarball": "dist/npm/vocmarket-voc-skill-0.3.33.tgz",
-  "tarballBytes": 664624,
-  "unpackedBytes": 4322384,
+  "tarballBytes": 472945,
+  "unpackedBytes": 2590864,
   "entryCount": 166,
   "installCommands": [
     "npm install -g @vocmarket/voc-skill",