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

feat(voc): publish cost planning and calibration update

gangvy 1 месяц назад
Родитель
Сommit
a5d56cef87
23 измененных файлов с 622 добавлено и 36 удалено
  1. 1 1
      claude-code/claude-code-voc-intelligence/.claude-plugin/plugin.json
  2. 16 0
      claude-code/claude-code-voc-intelligence/README.md
  3. 5 1
      claude-code/claude-code-voc-intelligence/docs/capability-map.md
  4. 73 5
      claude-code/claude-code-voc-intelligence/docs/voc-cost-metering-and-balance-implementation-spec.md
  5. 135 0
      claude-code/claude-code-voc-intelligence/mcp/src/features/voc-cost/cost-calibration.js
  6. 184 0
      claude-code/claude-code-voc-intelligence/mcp/src/features/voc-cost/skill-planner-adapter.js
  7. 9 0
      claude-code/claude-code-voc-intelligence/mcp/src/features/voc-cost/workflow-planner.js
  8. 8 1
      claude-code/claude-code-voc-intelligence/mcp/src/server.js
  9. 15 1
      claude-code/claude-code-voc-intelligence/mcp/src/tools/voc-cost-estimate-run.js
  10. 24 2
      claude-code/claude-code-voc-intelligence/mcp/src/tools/voc-usage-report-run.js
  11. 2 2
      claude-code/claude-code-voc-intelligence/package-lock.json
  12. 2 1
      claude-code/claude-code-voc-intelligence/package.json
  13. 1 0
      claude-code/claude-code-voc-intelligence/scripts/acceptance.js
  14. 76 0
      claude-code/claude-code-voc-intelligence/scripts/smoke-cost-calibration.js
  15. 1 1
      claude-code/claude-code-voc-intelligence/skill-package-manifest.json
  16. 4 0
      claude-code/claude-code-voc-intelligence/skills/voc-cost-controller/SKILL.md
  17. 22 0
      claude-code/claude-code-voc-intelligence/skills/voc-cost-controller/references/workflow-cost-models.md
  18. 4 4
      dist/claude-code-voc-intelligence-suite-manifest.json
  19. BIN
      dist/claude-code-voc-intelligence.zip
  20. 7 7
      dist/npm/claude-code-voc-npm-package-manifest.json
  21. 6 6
      release/npm/claude-code-voc-intelligence/README.md
  22. 4 4
      release/npm发布管理文档/NPM技能包发布管理.md
  23. 23 0
      release/npm发布管理文档/变更记录/voc-skill.md

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

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

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

@@ -14,6 +14,7 @@
 - VOC 口播脚本共创:适合把一条 VOC 选题继续打磨成 60 秒稿、30 秒稿、提词器版,并支持多轮改稿和定稿记忆。
 - VOC 接口目录:适合按平台、关键词和通道查找 1,344 个已登记接口,并读取参数后完成通用调用。
 - VOC 成本控制:适合执行前比较经济版/标准版/完整版,检查余额与预算,执行后查看接口频次和额度消耗。
+- 历史 Skill 成本适配:可直接读取 OpenClaw `api-config.json` 的批量 pipeline 生成报价,并用实际账单校准下一轮估算。
 
 这套技能包不覆盖创始人故事、品牌自传或老板个人 IP 长文。口播脚本要做,但必须来自 VOC 用户问题、竞品评论差异、问题池 Top 问题或单点深挖动作。
 
@@ -40,6 +41,7 @@
 - “跑一遍完整 VOC 经营闭环”
 - “这个复杂任务预计要花多少钱,经济版和完整版差多少”
 - “查一下这份报告每个接口调用了多少次、消耗了多少额度”
+- “读取这个历史 Skill 的 api-config.json,先估算整批任务要花多少钱”
 - “查商品详情或评论,先从接口清单里找合适的接口”
 
 ## 能交付什么
@@ -220,6 +222,20 @@ MCP 工具:
 - `voc_cost_estimate`
 - `voc_usage_report`
 
+历史批处理 Skill 报价示例:
+
+```json
+{
+  "skillConfigPath": "E:/workspace/openclaw-voc-skill/openclaw-skills/xiaohongshu/xiaohongshu-mini-voc/api-config.json",
+  "keywords": ["全屋定制", "橱柜"],
+  "notesPerKeyword": 5,
+  "maxCommentPages": 2,
+  "mode": "standard"
+}
+```
+
+报价执行后会在 `outputDir/cost-estimate.json` 保存快照。把该文件和实际用量一起传给 `voc_usage_report` 的 `estimatePath`,并指定本地 `calibrationPath`,即可看到预计/实际偏差和接口级差异。
+
 命令行入口:
 
 ```powershell

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

@@ -24,6 +24,8 @@
 | 查找商品、评论、创作者或选品接口 | `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、用户扣费和成本拆分 |
+| 读取历史 OpenClaw Skill 的批处理定义后报价 | `voc-cost-controller` | `voc_cost_estimate`(`skillConfigPath`) | 从 `api-config.json` 的 pipeline 自动推导批量、分页和可选步骤 |
+| 用实际账单校准下一轮报价 | `voc-cost-controller` | `voc_usage_report`(`estimatePath` + `calibrationPath`) | 预计/实际金额、偏差方向和接口级差异 |
 
 ## Fmode Image Analysis Add-on
 
@@ -167,7 +169,7 @@ voc_api_search -> voc_api_doc -> voc_api_call
 | `skills/*/SKILL.md` | Claude Code 触发规则和操作约束 |
 | `mcp/src/server.js` | MCP 工具注册、schema、工具说明 |
 | `mcp/src/tools/*` | CLI/MCP 可调用工具入口 |
-| `mcp/src/features/*` | 趋势报告、单点深挖、记忆、信号提取 |
+| `mcp/src/features/*` | 趋势报告、单点深挖、记忆、信号提取、历史 Skill 规划和成本校准 |
 | `mcp/catalog/voc-social-endpoints.json` | 1,344 个接口的稳定 ID、通道、参数索引和版本建议 |
 | `mcp/catalog/params/*.json` | 按平台懒加载的详细参数文档 |
 | `mcp/src/core/credentials.js` | token 读取口径 |
@@ -175,6 +177,8 @@ voc_api_search -> voc_api_doc -> voc_api_call
 | `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/features/voc-cost/skill-planner-adapter.js` | 将历史 Skill `api-config.json` 转为报价 operations,不执行历史任务 |
+| `mcp/src/features/voc-cost/cost-calibration.js` | 比较预计/实际金额并保存本地校准样本 |
 | `mcp/src/core/evidence-quality.js` | 证据质量门槛、无样本降级建议 |
 | `docs/live-manual-acceptance-checklist.md` | live 采集人工验收 |
 | `docs/claude-code-troubleshooting-playbook.md` | Claude Code 故障定位、误报修复、关键词修复和发散排查 |

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

@@ -1,6 +1,6 @@
 # VOC 用量核算、成本预估与余额看板实现方案
 
-> 文档状态:P0 计费、用量展示、现有支付回调链路和工具级续跑已完成;此前为旧钱包负余额增加的 NewAPI 直入账补丁已撤销,生产源代码已包含撤销提交。2026-09-05 无扣费探针显示公网 `nova-server` 仍加载旧运行包,需发布撤销版本并复核原有云函数划拨到账。历史工作流适配和跨会话唤醒仍列为 P1。
+> 文档状态:P0 计费、用量展示、现有支付回调链路和工具级续跑已完成;此前为旧钱包负余额增加的 NewAPI 直入账补丁已撤销,生产源代码已包含撤销提交。历史 Skill 规划适配和估算偏差校准已在技能包完成并纳入下一版发布;跨会话唤醒仍属于 Agent runtime/任务队列范围。2026-09-05 无扣费探针显示公网 `nova-server` 仍加载旧运行包,生产支付回调到账复核仍需在实际宿主完成。
 > 编写日期:2026-09-01;最近复核:2026-09-05
 > 涉及仓库:`claude-code-voc-intelligence`、`future-server`、`fmode-studio`、`voc-profit-web`
 
@@ -2359,9 +2359,9 @@ Studio 已有 `/balance`、Token 免登录、NewAPI 余额、支付二维码、
 3. 使用已有检查点续跑,确认已完成业务键的 `fetchCalls=0`;完成后将 P0 账户级支付门禁标记为通过。
 4. P1 仍为历史批处理 Skill 规划适配、跨会话自动唤醒、更多媒体入口覆盖和报价偏差校准。
 
-### 21.33 2026-09-05 技能包 0.3.34 发布候选验收
+### 21.33 2026-09-05 技能包 0.3.34 发布与线上验收
 
-本轮只推进技能包交付,不执行真实 npm 发布。版本字段已在 `package.json`、`package-lock.json`、`.claude-plugin/plugin.json`、`skill-package-manifest.json` 和 MCP server 中统一为 `0.3.34`。
+本轮完成技能包交付和 npm 正式发布。版本字段已在 `package.json`、`package-lock.json`、`.claude-plugin/plugin.json`、`skill-package-manifest.json` 和 MCP server 中统一为 `0.3.34`。
 
 | 检查项 | 结果 | 证据/结论 |
 | --- | --- | --- |
@@ -2370,11 +2370,79 @@ Studio 已有 `/balance`、Token 免登录、NewAPI 余额、支付二维码、
 | 包级全量验收 | 通过 | `claude-voc:acceptance` 覆盖样例、证据卡、本地媒体、偏好记忆、MCP、安装器和 workspace。 |
 | 计费与成本 smoke | 通过 | billing、cost estimate、budget、trend/checkpoint、request shaping、media、cross-industry、MCP 全部通过。 |
 | npm tarball | 通过 | `dist/npm/vocmarket-voc-skill-0.3.34.tgz`,166 个文件,npm 安装、npx 安装和 workspace 安装 smoke 全通过。 |
-| 发布元数据 | 通过 | `npm publish --dry-run --access public --ignore-scripts` 成功;公网查询确认 `0.3.34` 尚未占用。 |
+| 发布元数据 | 通过 | `npm publish --dry-run --access public --ignore-scripts` 与正式 `npm publish` 均成功;公网查询返回 `0.3.34`。 |
 | 旧路径与媒体策略 | 通过 | VOC 402 只返回 Tokenized Balance/显式二维码;媒体只写本地 `assets/`,不调用线上 Storage 上传链路。 |
+| 发布后线上安装 | 通过 | 直接从 npm registry 执行 `npx --yes @vocmarket/voc-skill@0.3.34 workspace --smoke`,样例、MCP 和 workspace 文件检查均通过。 |
 
 发布时使用 `Get-FileHash -Algorithm SHA256` 计算 ZIP 和 npm tarball 的校验值;由于本规格文档会随包发布,不在包内固化自身哈希,最终值以构建完成后的命令输出为准。
 
 #### 发布门禁
 
-技能包已达到可发布状态。真实发布仍需具备 npm 登录或 `NPM_TOKEN`,执行根目录 `npm run claude-voc:npm-publish`;发布后运行 `npm view @vocmarket/voc-skill@0.3.34 version --registry=https://registry.npmjs.org/` 和 `npx --yes @vocmarket/voc-skill@0.3.34 workspace --smoke` 完成线上验证。fmode-studio、future-server 的生产发布不属于本轮 npm 包发布动作。
+技能包已完成正式发布,线上版本为 `@vocmarket/voc-skill@0.3.34`。fmode-studio、future-server 的生产支付门禁仍按 21.32 的独立清单推进,不属于 npm 包发布动作。
+
+### 21.34 2026-09-06 P1 历史批处理报价适配与偏差校准
+
+本轮继续推进技能包侧 P1,新增内容已经在源码和测试中闭环;基础适配已随 `0.3.35` 正式发布,通道模型映射修复随 `0.3.36` 发布。
+
+| 工作项 | 状态 | 落地点 |
+| --- | --- | --- |
+| 历史批处理 Skill 规划适配 | 已完成 | `mcp/src/features/voc-cost/skill-planner-adapter.js` 读取本地目录或 `api-config.json`,解析 `parameters` 默认值、`pipeline[].api`、`forEach` 集合和分页上限,转换成 `voc_cost_estimate` operations。 |
+| 经济/标准/完整版策略 | 已完成 | 经济版剔除带 `condition` 的可选步骤;标准版和完整版沿用历史配置声明的范围,不虚构额外调用、不执行历史 Skill。 |
+| 报价快照 | 已完成 | `voc_cost_estimate` 指定 `outputDir` 时写入 `cost-estimate.json`,记录选定方案、接口步骤和当前服务端价目结果。 |
+| 预计/实际偏差校准 | 已完成 | `mcp/src/features/voc-cost/cost-calibration.js` 比较 `estimatePath` 与 `voc_usage_report` 实际账单,返回总额和接口级偏差;`calibrationPath` 追加本地样本并提供按工作流汇总。 |
+| 旧支付、旧钱包和媒体上传逻辑 | 保持不变 | 本轮未新增支付接口、任务级预付款、微信回调改动或线上 Storage 上传;实际调用仍逐次扣 NewAPI,媒体仍只写本地任务目录。 |
+
+#### Agent 使用方式
+
+历史 Skill 只需要把配置路径作为报价入参,Agent 仍负责向用户解释假设和选择方案:
+
+```json
+{
+  "skillConfigPath": "E:/workspace/openclaw-voc-skill/openclaw-skills/xiaohongshu/xiaohongshu-mini-voc/api-config.json",
+  "keywords": ["全屋定制", "橱柜"],
+  "notesPerKeyword": 5,
+  "maxCommentPages": 2,
+  "mode": "standard"
+}
+```
+
+执行后将估算输出目录中的 `cost-estimate.json` 和实际用量查询结果传给 `voc_usage_report`:
+
+```json
+{
+  "usageTraceId": "usage_xxx",
+  "estimatePath": "outputs/usage_xxx/cost-estimate.json",
+  "calibrationPath": "outputs/cost-calibration.json"
+}
+```
+
+`calibrationPath` 只保存预计金额、实际金额、接口标识和偏差,不保存 Token、原始请求或媒体内容。校准结果只影响下一轮 Agent 规划,不改变后端价目和 NewAPI 实际扣费。
+
+#### 本轮验证
+
+- `npm run smoke:cost-calibration`:通过,覆盖小红书历史 Skill 配置、关键词/笔记/评论分页计数、可选作者画像剔除和偏差汇总。
+- `npm run smoke:cost`:通过,既有报价和用量报告回归保持通过。
+- `npm run audit:index`、`npm run acceptance`:通过,版本 `0.3.35` 的 ZIP/npm/npx/workspace 产物包含新测试和文档。
+- npm 正式发布:通过,`@vocmarket/voc-skill@0.3.35` registry 可见,指定版本线上 workspace smoke 通过。
+
+#### 尚未由技能包单独完成的事项
+
+1. `nova-server` 实际宿主加载撤销版本后的生产支付到账、Balance 刷新和 checkpoint 续跑,仍需后端发布与账户级小额支付回归。
+2. 跨会话自动唤醒仍需 Agent runtime/callback/持久任务队列提供执行租约和支付事件唤醒;技能包继续提供 checkpoint、`usageTraceId` 和恢复参数,不在本包伪造后台执行能力。
+3. 更多采集入口的媒体覆盖依赖各入口把原始响应交给统一本地媒体流水线;当前通用 `voc_api_call`、小红书、抖音入口已覆盖,新增入口按同一契约接入。
+
+### 21.35 2026-09-06 技能包 0.3.36 发布闭环
+
+`@vocmarket/voc-skill@0.3.36` 已正式发布并完成线上指定版本验收。本版本包含历史批处理报价适配、预计/实际成本校准以及显式通道计费模型映射修复;充值仍沿用 Token 免登录 Balance 链接或 Agent 生成二维码,任务执行仍按每次实际 NewAPI 调用扣费,媒体仍只写本地任务目录。
+
+| 检查项 | 结果 | 证据/结论 |
+| --- | --- | --- |
+| npm registry 版本 | 通过 | `npm view @vocmarket/voc-skill@0.3.36 version --registry=https://registry.npmjs.org/` 返回 `0.3.36`。 |
+| 指定版本线上安装 | 通过 | `npm exec --yes --package @vocmarket/voc-skill@0.3.36 -- claude-voc workspace --smoke` 完成临时目录安装、样例、MCP 和 workspace 检查。 |
+| 当前源码验收 | 通过 | `npm --prefix claude-code/claude-code-voc-intelligence run acceptance` 通过,覆盖包级 smoke、MCP、安装器和成本校准。 |
+| 索引结构审计 | 通过 | `npm --prefix claude-code/claude-code-voc-intelligence run audit:index` 返回 `issues=[]`,11 个 Skill、19 个 MCP 工具;仅提示需由发布构建排除的本地 catalog 备份文件。 |
+| 发布产物元数据 | 通过 | `dist/npm/claude-code-voc-npm-package-manifest.json` 的版本为 `0.3.36`,`published=true`、`tested=true`、`npxTested=true`、`workspaceTested=true`。 |
+
+#### 技能包交付结论
+
+技能包已达到可发布和可安装使用的状态。P1 的报价、用量报告、接口频次/额度拆分、成本校准、余额不足充值引导、本地媒体缓存和索引审计均已在包内闭环。生产侧仍需由后端团队独立完成实际应用宿主的版本发布,以及使用余额正常测试账户完成原有支付回调、NewAPI 到账、Balance 刷新和 checkpoint 续跑回归;这些不阻塞 npm 技能包发布,但属于整体生产闭环的外部门禁。

+ 135 - 0
claude-code/claude-code-voc-intelligence/mcp/src/features/voc-cost/cost-calibration.js

@@ -0,0 +1,135 @@
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+
+function number(value, fallback = 0) {
+  const parsed = Number(value);
+  return Number.isFinite(parsed) ? parsed : fallback;
+}
+
+function round(value, digits = 6) {
+  const factor = 10 ** digits;
+  return Math.round(number(value) * factor) / factor;
+}
+
+function rowKey(row = {}) {
+  return String(row.endpointId || row.endpoint || row.path || row.apigId || row.model || '').trim();
+}
+
+function actualRows(actual = {}) {
+  const usage = actual.usage || actual.data?.usage || actual.data || actual;
+  return Array.isArray(usage.groups) ? usage.groups
+    : Array.isArray(usage.items) ? usage.items
+      : Array.isArray(usage.rows) ? usage.rows : [];
+}
+
+function estimateRows(estimate = {}) {
+  const source = estimate.data || estimate;
+  const plans = Array.isArray(source.plans) ? source.plans : [];
+  if (plans.length) {
+    const selected = source.selectedMode ? plans.filter(plan => plan.mode === source.selectedMode) : plans;
+    return selected.flatMap(plan => (plan.operations || []).map(operation => ({ ...operation, mode: plan.mode })));
+  }
+  return Array.isArray(source.operations) ? source.operations : [];
+}
+
+function compareEstimateToActual(estimate = {}, actual = {}) {
+  const estimateSource = estimate.data || estimate;
+  const actualSource = actual.data || actual;
+  const expectedCny = number(estimateSource.selectedMode
+    ? (estimateSource.plans || []).find(plan => plan.mode === estimateSource.selectedMode)?.expectedCny
+    : estimateSource.expectedCny ?? estimateSource.amountCny);
+  const actualTotals = actualSource.totals || actualSource.usage?.totals || {};
+  const actualCny = number(actualTotals.costCny ?? actualTotals.amountCny ?? actualTotals.actualCostCny);
+  const deltaCny = round(actualCny - expectedCny);
+  const deltaPct = expectedCny > 0 ? round((deltaCny / expectedCny) * 100, 2) : null;
+  const expectedByPath = new Map();
+  for (const row of estimateRows(estimate)) {
+    const key = rowKey(row);
+    if (!key) continue;
+    const amount = number(row.costCny ?? row.amountCny ?? number(row.priceCnyPerCall) * number(row.count));
+    expectedByPath.set(key, (expectedByPath.get(key) || 0) + amount);
+  }
+  const actualByPath = new Map();
+  for (const row of actualRows(actual)) {
+    const key = rowKey(row);
+    if (!key) continue;
+    const amount = number(row.costCny ?? row.amountCny ?? row.actualCostCny);
+    actualByPath.set(key, (actualByPath.get(key) || 0) + amount);
+  }
+  const keys = new Set([...expectedByPath.keys(), ...actualByPath.keys()]);
+  const operations = [...keys].map(endpoint => {
+    const expected = round(expectedByPath.get(endpoint) || 0);
+    const actualValue = round(actualByPath.get(endpoint) || 0);
+    return {
+      endpoint,
+      expectedCny: expected,
+      actualCny: actualValue,
+      deltaCny: round(actualValue - expected),
+      deltaPct: expected > 0 ? round(((actualValue - expected) / expected) * 100, 2) : null
+    };
+  }).sort((a, b) => Math.abs(b.deltaCny) - Math.abs(a.deltaCny));
+  return {
+    status: expectedCny > 0 || actualCny > 0 ? 'calibrated' : 'insufficient_data',
+    workflowType: estimateSource.workflowType || actualSource.workflowType || null,
+    mode: estimateSource.selectedMode || null,
+    expectedCny: round(expectedCny),
+    actualCny: round(actualCny),
+    deltaCny,
+    deltaPct,
+    direction: deltaCny > 0 ? 'underestimated' : deltaCny < 0 ? 'overestimated' : 'aligned',
+    operations,
+    observedAt: new Date().toISOString()
+  };
+}
+
+function readHistory(filePath) {
+  if (!filePath || !fs.existsSync(filePath)) return [];
+  const value = JSON.parse(fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, ''));
+  return Array.isArray(value) ? value : Array.isArray(value.samples) ? value.samples : [];
+}
+
+function appendCalibrationSample(filePath, sample, options = {}) {
+  if (!filePath) return null;
+  const target = path.resolve(filePath);
+  fs.mkdirSync(path.dirname(target), { recursive: true });
+  const history = readHistory(target);
+  const maxSamples = Math.max(1, Number(options.maxSamples) || 500);
+  history.push(sample);
+  fs.writeFileSync(target, JSON.stringify(history.slice(-maxSamples), null, 2));
+  return target;
+}
+
+function summarizeCalibration(history = []) {
+  const samples = history.filter(item => item && item.status === 'calibrated');
+  const totalExpected = samples.reduce((sum, item) => sum + number(item.expectedCny), 0);
+  const totalActual = samples.reduce((sum, item) => sum + number(item.actualCny), 0);
+  const deltaCny = round(totalActual - totalExpected);
+  return {
+    sampleCount: samples.length,
+    expectedCny: round(totalExpected),
+    actualCny: round(totalActual),
+    deltaCny,
+    deltaPct: totalExpected > 0 ? round((deltaCny / totalExpected) * 100, 2) : null,
+    direction: deltaCny > 0 ? 'underestimated' : deltaCny < 0 ? 'overestimated' : 'aligned',
+    byWorkflowType: samples.reduce((result, item) => {
+      const key = item.workflowType || 'unknown';
+      const bucket = result[key] || { sampleCount: 0, expectedCny: 0, actualCny: 0 };
+      bucket.sampleCount += 1;
+      bucket.expectedCny = round(bucket.expectedCny + number(item.expectedCny));
+      bucket.actualCny = round(bucket.actualCny + number(item.actualCny));
+      bucket.deltaCny = round(bucket.actualCny - bucket.expectedCny);
+      bucket.deltaPct = bucket.expectedCny > 0 ? round((bucket.deltaCny / bucket.expectedCny) * 100, 2) : null;
+      result[key] = bucket;
+      return result;
+    }, {})
+  };
+}
+
+module.exports = {
+  compareEstimateToActual,
+  readHistory,
+  appendCalibrationSample,
+  summarizeCalibration
+};

+ 184 - 0
claude-code/claude-code-voc-intelligence/mcp/src/features/voc-cost/skill-planner-adapter.js

@@ -0,0 +1,184 @@
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+const { cleanLabel } = require('../../core/usage-context');
+
+function readJson(filePath) {
+  return JSON.parse(fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, ''));
+}
+
+function resolveConfigPath(input = {}) {
+  const value = input.skillConfigPath || input.legacySkillPath || input.skillPath || input.apiConfigPath;
+  if (!value) return null;
+  const candidate = path.resolve(String(value));
+  if (!fs.existsSync(candidate)) throw new Error(`历史 Skill 配置不存在:${candidate}`);
+  const stat = fs.statSync(candidate);
+  if (stat.isDirectory()) {
+    const nested = path.join(candidate, 'api-config.json');
+    if (!fs.existsSync(nested)) throw new Error(`历史 Skill 目录缺少 api-config.json:${candidate}`);
+    return nested;
+  }
+  return candidate;
+}
+
+function loadSkillConfig(input = {}) {
+  if (input.skillConfig && typeof input.skillConfig === 'object') {
+    return { config: input.skillConfig, path: null };
+  }
+  const configPath = resolveConfigPath(input);
+  if (!configPath) return null;
+  return { config: readJson(configPath), path: configPath };
+}
+
+function defaultsFromConfig(config = {}) {
+  const defaults = {};
+  for (const [name, definition] of Object.entries(config.parameters || {})) {
+    if (definition && definition.default !== undefined) defaults[name] = definition.default;
+  }
+  return defaults;
+}
+
+function valueFromTemplate(value, input, fallback = 0) {
+  if (typeof value === 'number') return Number.isFinite(value) ? value : fallback;
+  const text = String(value == null ? '' : value).trim();
+  const match = text.match(/^\$\{([^}]+)\}$/);
+  const key = match ? match[1].split(/[.[\]]/)[0] : text;
+  const raw = key && input[key] !== undefined ? input[key] : undefined;
+  const number = Number(raw);
+  return Number.isFinite(number) ? number : fallback;
+}
+
+function arrayLength(value) {
+  return Array.isArray(value) ? value.length : 0;
+}
+
+function keywordCount(input) {
+  return arrayLength(input.keywords) || arrayLength(input.keywordMatrix) || Number(input.keywordCount) || 1;
+}
+
+function accountCount(input) {
+  return arrayLength(input.accounts) || Number(input.accountCount) || Number(input.asinCount) || 1;
+}
+
+function selectedItemCount(input) {
+  const keywords = keywordCount(input);
+  const perKeyword = Number(input.notesPerKeyword ?? input.itemsPerKeyword ?? input.videosPerKeyword ?? 1);
+  return Math.max(1, keywords * (Number.isFinite(perKeyword) && perKeyword > 0 ? perKeyword : 1));
+}
+
+function countForEach(forEach, input) {
+  const text = String(forEach || '').toLowerCase();
+  if (!text) return 1;
+  if (text.includes('asin') && (text.includes('page') || text.includes('×') || text.includes('x'))) {
+    const pages = Number(input.maxPagesPerAsin ?? input.pagesPerAsin ?? 1);
+    return accountCount(input) * Math.max(1, Number.isFinite(pages) ? pages : 1);
+  }
+  if (text.includes('asin')) return accountCount(input);
+  if (text.includes('keyword') || text.includes('关键词')) return keywordCount(input);
+  if (text.includes('selectednote') || text.includes('selectednotes') || text.includes('入选笔记')) return selectedItemCount(input);
+  if (text.includes('selectedvideo') || text.includes('selectedvideos') || text.includes('入选视频')) return selectedItemCount(input);
+  if (text.includes('note') || text.includes('笔记')) return selectedItemCount(input);
+  if (text.includes('video') || text.includes('视频')) return selectedItemCount(input);
+  if (text.includes('product') || text.includes('商品') || text.includes('sku')) {
+    return Math.max(1, Number(input.productCount ?? input.skuCount ?? input.itemCount ?? 1));
+  }
+  if (text.includes('user') || text.includes('作者') || text.includes('账号')) return accountCount(input);
+  return 1;
+}
+
+function stepCount(step, input) {
+  let count = countForEach(step.forEach, input);
+  const pagination = step.pagination;
+  if (pagination && !String(step.forEach || '').toLowerCase().includes('page')) {
+    const maxPages = valueFromTemplate(pagination.maxPages, input, 1);
+    if (maxPages > 1) count *= maxPages;
+  }
+  return Math.max(1, Math.floor(count));
+}
+
+function modelForConfig(config, api = {}) {
+  const explicit = api.model || api.channel;
+  if (explicit) {
+    const normalized = String(explicit).toLowerCase();
+    if (normalized === 'ecom' || normalized === 'ecommerce') return 'voc-ecom';
+    if (normalized === 'overseas' || normalized === 'overseas-ecom') return 'voc-overseas';
+    if (normalized === 'social') return 'voc-social';
+    return cleanLabel(explicit);
+  }
+  const source = `${config.category || ''} ${api.forwardUrl || ''} ${api.service || ''} ${api.skill || ''}`.toLowerCase();
+  if (source.includes('overseas') || source.includes('amazon') || source.includes('跨境')) return 'voc-overseas';
+  if (source.includes('ecom') || source.includes('taobao') || source.includes('amazon') || source.includes('电商')) return 'voc-ecom';
+  return 'voc-social';
+}
+
+function pathForApi(api = {}, step = {}) {
+  const value = api.proxyPath || api.path || api.endpoint || api.forwardPath || api.service || api.skill || step.name || 'legacy_skill_step';
+  return cleanLabel(value, 'legacy_skill_step');
+}
+
+function operationFromStep(config, step, input, index) {
+  if (!step || step.type === 'compute' || !step.api) return null;
+  const api = Array.isArray(step.api) ? step.api[0] : step.api;
+  const condition = String(step.condition || '').trim();
+  return {
+    model: modelForConfig(config, api),
+    apigId: cleanLabel(api.apigId || api.apig_id),
+    path: pathForApi(api, step),
+    count: stepCount(step, input),
+    label: cleanLabel(step.name, `历史 Skill 步骤 ${index + 1}`),
+    valueScore: condition ? 2 : 4,
+    required: !condition,
+    sourceSkill: cleanLabel(api.skill),
+    method: cleanLabel(api.method || 'GET').toUpperCase(),
+    ...(condition ? { condition } : {})
+  };
+}
+
+function buildLegacySkillPlans(input = {}) {
+  const loaded = loadSkillConfig(input);
+  if (!loaded) return [];
+  const config = loaded.config || {};
+  const values = { ...defaultsFromConfig(config), ...input };
+  const steps = Array.isArray(config.pipeline) ? config.pipeline : [];
+  const operations = steps.map((step, index) => operationFromStep(config, step, values, index)).filter(Boolean);
+  if (!operations.length) return [];
+  const name = cleanLabel(config.name || (loaded.path && path.basename(path.dirname(loaded.path))) || 'legacy_skill');
+  const workflowType = `legacy_skill:${name}`;
+  const build = (mode) => {
+    const selected = mode === 'economy' ? operations.filter(operation => operation.required) : operations;
+    return {
+      id: `${name}:${mode}`,
+      mode,
+      label: ({ economy: '经济版', standard: '标准版', complete: '完整版' })[mode],
+      workflowType,
+      assumptions: {
+        skillName: name,
+        skillConfigPath: loaded.path,
+        parameterDefaults: defaultsFromConfig(config),
+        modePolicy: mode === 'economy' ? '仅保留 pipeline 中无 condition 的必需接口步骤。' : '沿用历史 Skill pipeline 声明的接口步骤和分页上限。'
+      },
+      operations: selected.map(operation => ({ ...operation })),
+      strategy: mode === 'economy'
+        ? '沿用历史 Skill 的主链路,跳过可选画像、补充评论等步骤。'
+        : mode === 'standard'
+          ? '按历史 Skill 默认参数执行全部声明步骤。'
+          : '按历史 Skill 声明的分页和批量范围完整覆盖,不虚构额外调用。'
+    };
+  };
+  const modes = input.mode && input.mode !== 'all'
+    ? [String(input.mode).toLowerCase()]
+    : ['economy', 'standard', 'complete'];
+  const normalized = modes.map(mode => mode === 'low' ? 'economy' : mode === 'full' ? 'complete' : mode)
+    .filter(mode => ['economy', 'standard', 'complete'].includes(mode));
+  return (normalized.length ? normalized : ['standard']).map(build);
+}
+
+module.exports = {
+  resolveConfigPath,
+  loadSkillConfig,
+  defaultsFromConfig,
+  countForEach,
+  stepCount,
+  buildLegacySkillPlans
+};

+ 9 - 0
claude-code/claude-code-voc-intelligence/mcp/src/features/voc-cost/workflow-planner.js

@@ -1,6 +1,7 @@
 'use strict';
 
 const { cleanLabel } = require('../../core/usage-context');
+const { buildLegacySkillPlans } = require('./skill-planner-adapter');
 
 const MODES = ['economy', 'standard', 'complete'];
 
@@ -120,6 +121,14 @@ function planCustom(input, mode) {
 }
 
 function buildWorkflowPlans(input = {}) {
+  // Historical OpenClaw orchestration Skills describe their real batch shape
+  // in api-config.json. Reuse that declaration when the caller supplies it,
+  // so the Agent does not have to hand-copy every pipeline step into an
+  // operations array before asking for a quote.
+  if (input.skillConfigPath || input.legacySkillPath || input.skillPath || input.apiConfigPath || input.skillConfig) {
+    const legacyPlans = buildLegacySkillPlans(input);
+    if (legacyPlans.length) return legacyPlans;
+  }
   const type = String(input.workflowType || '').toLowerCase();
   const xhs = type.includes('xhs') || type.includes('xiaohongshu');
   const douyin = type.includes('douyin');

+ 8 - 1
claude-code/claude-code-voc-intelligence/mcp/src/server.js

@@ -66,7 +66,7 @@ function normalizeToolResult(result = {}) {
 function createServer() {
   const server = new McpServer({
     name: 'voc-intelligence',
-    version: '0.3.34'
+    version: '0.3.36'
   });
 
   server.registerTool(
@@ -907,6 +907,10 @@ function createServer() {
       inputSchema: {
         task: z.string().max(500).optional(),
         workflowType: z.string().max(100).optional(),
+        skillConfigPath: z.string().max(1000).optional().describe('本地历史 OpenClaw Skill 目录或 api-config.json 路径;只读取规划声明,不执行 Skill。'),
+        legacySkillPath: z.string().max(1000).optional().describe('skillConfigPath 的兼容别名。'),
+        apiConfigPath: z.string().max(1000).optional().describe('api-config.json 的兼容别名。'),
+        skillConfig: z.object({}).passthrough().optional().describe('已读取的历史 Skill 配置对象;不包含凭证。'),
         mode: z.enum(['economy', 'standard', 'complete', 'low', 'full', 'all']).optional(),
         accountCount: z.number().int().min(1).max(1000000).optional(),
         keywordCount: z.number().int().min(1).max(1000000).optional(),
@@ -984,6 +988,9 @@ function createServer() {
         operations: z.array(z.object({ model: z.string().min(1), apigId: z.string().optional(), path: z.string().optional(), count: z.number().int().min(1).max(1000000).optional() }).passthrough()).max(100).optional(),
         reportPath: z.string().optional(),
         usagePath: z.string().optional(),
+        estimatePath: z.string().optional().describe('执行前 voc_cost_estimate 输出的 cost-estimate.json,用于偏差校准。'),
+        calibrationPath: z.string().optional().describe('本地估算校准历史 JSON 路径;只写入估算与实际金额汇总。'),
+        maxCalibrationSamples: z.number().int().min(1).max(5000).optional(),
         refresh: z.boolean().optional(),
         baseUrl: z.string().url().optional(),
         newapiToken: z.string().optional(),

+ 15 - 1
claude-code/claude-code-voc-intelligence/mcp/src/tools/voc-cost-estimate-run.js

@@ -1,5 +1,7 @@
 'use strict';
 
+const fs = require('fs');
+const path = require('path');
 const { queryBilling } = require('../core/billing-client');
 const { contextForTool, writeUsageContext } = require('../core/usage-context');
 const { buildWorkflowPlans, flattenPlans } = require('../features/voc-cost/workflow-planner');
@@ -8,7 +10,15 @@ const { errorResult } = require('../core/result-envelope');
 
 async function runVocCostEstimate(input = {}) {
   const context = contextForTool({ ...input, skillId: input.skillId || 'voc-cost-controller' });
-  const plans = buildWorkflowPlans(input);
+  let plans;
+  try {
+    plans = buildWorkflowPlans(input);
+  } catch (error) {
+    return errorResult(`历史 Skill 规划配置无效:${error.message || String(error)}`, {
+      summary: { errorKind: 'invalid_skill_config' },
+      data: { context }
+    });
+  }
   const flat = flattenPlans(plans);
   if (!flat.length) return errorResult('请提供 operations 或可识别的 workflowType 与任务规模。', { summary: { errorKind: 'missing_plan' } });
   const query = await queryBilling({
@@ -55,6 +65,10 @@ async function runVocCostEstimate(input = {}) {
   if (input.outputDir) {
     const filePath = writeUsageContext(context, input.outputDir);
     if (filePath) files.push(filePath);
+    const estimatePath = path.join(path.resolve(String(input.outputDir)), 'cost-estimate.json');
+    fs.mkdirSync(path.dirname(estimatePath), { recursive: true });
+    fs.writeFileSync(estimatePath, JSON.stringify({ ...report, status: 'ok' }, null, 2));
+    files.push(estimatePath);
   }
   return {
     status: 'ok',

+ 24 - 2
claude-code/claude-code-voc-intelligence/mcp/src/tools/voc-usage-report-run.js

@@ -5,6 +5,7 @@ const path = require('path');
 const { queryBilling } = require('../core/billing-client');
 const { contextForTool } = require('../core/usage-context');
 const { errorResult } = require('../core/result-envelope');
+const { compareEstimateToActual, appendCalibrationSample, readHistory, summarizeCalibration } = require('../features/voc-cost/cost-calibration');
 
 function readJson(filePath) {
   try {
@@ -14,6 +15,10 @@ function readJson(filePath) {
   }
 }
 
+function readOptionalJson(filePath) {
+  return filePath ? readJson(filePath) : null;
+}
+
 function extractUsage(source = {}) {
   const usage = source.usage || source.data?.usage || source.data?.billing || source.billing || source;
   const rows = Array.isArray(usage.groups) ? usage.groups : Array.isArray(usage.items) ? usage.items : Array.isArray(usage.rows) ? usage.rows : Array.isArray(usage.operations) ? usage.operations : [];
@@ -68,13 +73,30 @@ async function runVocUsageReport(input = {}) {
     balance: source.balance || null,
     pricing: source.catalog || source.pricing || []
   };
+  const estimate = readOptionalJson(input.estimatePath);
+  let calibration = null;
+  let calibrationFile = null;
+  if (estimate) {
+    calibration = compareEstimateToActual(estimate, report);
+    if (input.calibrationPath) {
+      calibrationFile = appendCalibrationSample(input.calibrationPath, calibration, { maxSamples: input.maxCalibrationSamples });
+      calibration = { ...calibration, history: summarizeCalibration(readHistory(calibrationFile)), file: calibrationFile };
+    }
+  }
+  if (calibration) report.calibration = calibration;
+  const calibrationLine = calibration
+    ? `估算校准:预计 ¥${calibration.expectedCny.toFixed(2)},实际 ¥${calibration.actualCny.toFixed(2)},偏差 ${calibration.deltaPct == null ? '暂无' : `${calibration.deltaPct}%`}(${calibration.direction})。`
+    : '';
   return {
     status: 'ok',
-    assistantMessage: formatMessage(report),
+    assistantMessage: [formatMessage(report), calibrationLine].filter(Boolean).join('\n'),
     summary: { usageTraceId: report.usageTraceId, reportId: report.reportId, calls: extracted.calls, billedCalls: extracted.billedCalls, quota: extracted.quota, costCny: extracted.costCny, quality: report.quality },
     data: report,
     recharge: null,
-    files: input.reportPath || input.usagePath ? [path.resolve(input.reportPath || input.usagePath)] : [],
+    files: [
+      ...(input.reportPath || input.usagePath ? [path.resolve(input.reportPath || input.usagePath)] : []),
+      ...(calibrationFile ? [calibrationFile] : [])
+    ],
     nextActions: [],
     warnings: query?.warnings || [],
     errors: []

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

@@ -1,12 +1,12 @@
 {
   "name": "@vocmarket/voc-skill",
-  "version": "0.3.34",
+  "version": "0.3.36",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "@vocmarket/voc-skill",
-      "version": "0.3.34",
+      "version": "0.3.36",
       "dependencies": {
         "@modelcontextprotocol/sdk": "^1.29.0",
         "zod": "^4.4.3"

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

@@ -1,6 +1,6 @@
 {
   "name": "@vocmarket/voc-skill",
-  "version": "0.3.34",
+  "version": "0.3.36",
   "description": "Claude Code VOC intelligence skills for Xiaohongshu and Douyin trend reports, live social VOC collection, Fmode image analysis, single-issue deep dives, boss/operator action plans, and multi-turn memory.",
   "type": "commonjs",
   "bin": {
@@ -17,6 +17,7 @@
     "smoke:budget": "node scripts/smoke-budget-guard.js",
     "smoke:trend-budget": "node scripts/smoke-trend-budget.js",
     "smoke:cost": "node scripts/smoke-cost-estimate.js",
+    "smoke:cost-calibration": "node scripts/smoke-cost-calibration.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",

+ 1 - 0
claude-code/claude-code-voc-intelligence/scripts/acceptance.js

@@ -13,6 +13,7 @@ function main() {
   run('npm', ['run', 'audit:index'], ROOT);
   run('npm', ['run', 'fmode:image:smoke'], ROOT);
   run('npm', ['run', 'smoke:budget'], ROOT);
+  run('npm', ['run', 'smoke:cost-calibration'], ROOT);
   run('npm', ['run', 'smoke:trend-budget'], ROOT);
   run('npm', ['run', 'smoke:package'], ROOT);
   run('npm', ['run', 'mcp:smoke'], ROOT);

+ 76 - 0
claude-code/claude-code-voc-intelligence/scripts/smoke-cost-calibration.js

@@ -0,0 +1,76 @@
+'use strict';
+
+const assert = require('node:assert/strict');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const { buildLegacySkillPlans } = require('../mcp/src/features/voc-cost/skill-planner-adapter');
+const {
+  compareEstimateToActual,
+  appendCalibrationSample,
+  readHistory,
+  summarizeCalibration
+} = require('../mcp/src/features/voc-cost/cost-calibration');
+
+function main() {
+  const root = path.resolve(__dirname, '..', '..', '..');
+  const xhsConfig = path.join(root, 'openclaw-skills', 'xiaohongshu', 'xiaohongshu-mini-voc', 'api-config.json');
+  const plans = buildLegacySkillPlans({
+    skillConfigPath: xhsConfig,
+    keywords: ['甲', '乙', '丙'],
+    notesPerKeyword: 2,
+    maxCommentPages: 2,
+    includeUserInfo: true
+  });
+  assert.equal(plans.length, 3);
+  assert.equal(plans[0].operations.find(operation => operation.path.includes('search-notes')).count, 3);
+  assert.equal(plans[1].operations.find(operation => operation.path.includes('note-detail')).count, 6);
+  assert.equal(plans[1].operations.find(operation => operation.path.includes('note-comments')).count, 12);
+  assert.equal(plans[0].operations.some(operation => operation.path.includes('user-info')), false);
+  assert.equal(plans[1].operations.some(operation => operation.path.includes('user-info')), true);
+
+  const ecomPlans = buildLegacySkillPlans({
+    skillConfig: {
+      name: 'legacy-ecom-fixture',
+      category: 'ecommerce',
+      pipeline: [{ step: 1, name: '商品详情', forEach: 'products', api: { channel: 'ecommerce', endpoint: 'product.detail', method: 'GET' } }]
+    },
+    productCount: 2,
+    mode: 'standard'
+  });
+  assert.equal(ecomPlans[0].operations[0].model, 'voc-ecom');
+
+  const estimate = {
+    workflowType: 'legacy_skill:xiaohongshu-mini-voc',
+    selectedMode: 'standard',
+    plans: [{
+      mode: 'standard',
+      expectedCny: 10,
+      operations: [{ path: 'xhs.search', costCny: 4 }, { path: 'xhs.detail', costCny: 6 }]
+    }]
+  };
+  const actual = {
+    workflowType: estimate.workflowType,
+    totals: { costCny: 12 },
+    rows: [{ path: 'xhs.search', costCny: 4 }, { path: 'xhs.detail', costCny: 8 }]
+  };
+  const sample = compareEstimateToActual(estimate, actual);
+  assert.equal(sample.status, 'calibrated');
+  assert.equal(sample.deltaCny, 2);
+  assert.equal(sample.deltaPct, 20);
+
+  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'voc-cost-calibration-'));
+  const historyPath = path.join(tmp, 'calibration.json');
+  appendCalibrationSample(historyPath, sample);
+  const summary = summarizeCalibration(readHistory(historyPath));
+  assert.equal(summary.sampleCount, 1);
+  assert.equal(summary.actualCny, 12);
+  console.log('legacy skill planner and cost calibration smoke passed');
+}
+
+try {
+  main();
+} catch (error) {
+  console.error(error.stack || error);
+  process.exitCode = 1;
+}

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

@@ -1,6 +1,6 @@
 {
   "name": "claude-code-voc-intelligence",
-  "version": "0.3.34",
+  "version": "0.3.36",
   "capabilities": {
     "summary": "三类情报源同源接入:①社媒内容 ②国内电商+创作者情报 ③海外选品。统一 search→doc→call、鉴权计费一致。",
     "channels": [

+ 4 - 0
claude-code/claude-code-voc-intelligence/skills/voc-cost-controller/SKILL.md

@@ -7,10 +7,14 @@ description: VOC 复杂采集任务的执行前成本估算、预算检查和执
 
 对账号、商品或关键词规模较大的任务,先调用 `voc_cost_estimate`。Agent 根据返回的接口计划、当前生效价目和余额向用户说明经济版、标准版、完整版的预期成本区间,不把估算当作已消费。
 
+历史 OpenClaw 编排 Skill 也可以直接报价:将该 Skill 的目录或 `api-config.json` 作为 `skillConfigPath` 传给 `voc_cost_estimate`,适配器只读取 `parameters` 默认值和 `pipeline` 中的接口步骤,自动估算 `forEach`、分页和可选步骤的调用次数。经济版会跳过带 `condition` 的可选步骤;标准版和完整版沿用配置声明的批量范围,不执行历史 Skill,也不读取其凭证。
+
 执行过程中使用同一 `usageTraceId`,每批次前调用预算守卫:本地预算达到 80% 发出提示,超过硬上限停止新增采集并保存检查点。NewAPI 仍按每次实际调用实时扣费,不创建任务级支付或冻结余额。
 
 VOC 数据余额不足时,保留已完成结果和检查点,返回差额、建议充值金额、Token 免登录 Balance 链接或现有二维码支付结果。到账后重新查询余额并从检查点继续;Agent 模型自身 402 由宿主运行时处理。
 
 任务完成后调用 `voc_usage_report`,按追踪标识或日期范围展示调用次数、计费次数、quota、用户扣费和接口拆分。大模型费用与 VOC 数据费用分开;没有精确追踪时标记为 `reconstructed` 或 `mixed`。
 
+将执行前输出目录中的 `cost-estimate.json` 通过 `estimatePath` 传给 `voc_usage_report`,可同时得到预计与实际金额的偏差、方向和接口级差异。设置本地 `calibrationPath` 后会追加校准样本,供后续由 Agent 调整任务规模、分页和可选接口的报价假设;该文件只保存汇总金额和接口标识,不保存凭证或原始请求。
+
 价格唯一来源是服务端 billing query。目录中的 `billing` 字段不能被当作人民币单价。

+ 22 - 0
claude-code/claude-code-voc-intelligence/skills/voc-cost-controller/references/workflow-cost-models.md

@@ -17,3 +17,25 @@
 - 评价:`S * K * P(taobao.get_item_comment_v3)`
 
 接口单价、quota 和版本始终从 billing query 返回值读取。每个步骤可附 `valueScore`、`required` 与 `fallback`,用于成本价值排序;是否删除字段必须说明信息损失。
+
+## 历史 Skill 规划适配
+
+历史 OpenClaw 编排 Skill 的 `api-config.json` 可以作为 `voc_cost_estimate.skillConfigPath` 输入。适配器读取:
+
+- `parameters.*.default`:补齐未显式传入的任务规模;
+- `pipeline[].api`:生成模型/通道、方法、接口标识和标签;
+- `pipeline[].forEach`:识别关键词、账号、ASIN、商品、入选笔记/视频等集合规模;
+- `pipeline[].pagination.maxPages`:将分页上限乘入调用次数;
+- `pipeline[].condition`:标记可选步骤,经济版默认剔除。
+
+`type=compute` 的清洗、去重和统计步骤不计为接口调用。无法从自然语言 `forEach` 安全推断时只按 1 次估算,并在计划中保留原步骤名,Agent 应在报价中说明该假设并建议用一次小规模实际运行校准。
+
+## 估算偏差校准
+
+执行前 `voc_cost_estimate` 在 `outputDir/cost-estimate.json` 保存计划快照;执行后 `voc_usage_report` 传入 `estimatePath`,会输出:
+
+- `expectedCny`、`actualCny`、`deltaCny` 和 `deltaPct`;
+- `direction`:`underestimated`、`overestimated` 或 `aligned`;
+- 按接口的预计/实际差异。
+
+传入 `calibrationPath` 会追加本地 JSON 样本,建议按客户、工作流和日期隔离。校准只改变下一轮 Agent 的计划假设,不改变服务端实际计费规则,也不创建任务预付款。

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

@@ -1,9 +1,9 @@
 {
   "name": "claude-code-voc-intelligence",
-  "version": "0.3.34",
-  "generatedAt": "2026-09-05T12:19:51.689Z",
+  "version": "0.3.36",
+  "generatedAt": "2026-09-05T16:33:50.365Z",
   "packageZip": "dist/claude-code-voc-intelligence.zip",
-  "packageZipBytes": 596082,
+  "packageZipBytes": 607320,
   "plugin": "voc-intelligence",
   "skills": [
     "xiaohongshu-trend-intelligence",
@@ -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": 169
+  "copiedFiles": 172
 }

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


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

@@ -1,11 +1,11 @@
 {
   "name": "@vocmarket/voc-skill",
-  "version": "0.3.34",
-  "generatedAt": "2026-09-05T12:21:45.324Z",
-  "tarball": "dist/npm/vocmarket-voc-skill-0.3.34.tgz",
-  "tarballBytes": 478342,
-  "unpackedBytes": 2607507,
-  "entryCount": 166,
+  "version": "0.3.36",
+  "generatedAt": "2026-09-05T16:35:11.430Z",
+  "tarball": "dist/npm/vocmarket-voc-skill-0.3.36.tgz",
+  "tarballBytes": 487148,
+  "unpackedBytes": 2636540,
+  "entryCount": 169,
   "installCommands": [
     "npm install -g @vocmarket/voc-skill",
     "claude-voc install",
@@ -15,5 +15,5 @@
   "tested": true,
   "npxTested": true,
   "workspaceTested": true,
-  "published": false
+  "published": true
 }

+ 6 - 6
release/npm/claude-code-voc-intelligence/README.md

@@ -106,18 +106,18 @@ npm publish <tgz> --access public
 
 ## 当前发布状态
 
-- 当前 npm latest:`@vocmarket/voc-skill@0.3.33`
-- 本地发布候选:`@vocmarket/voc-skill@0.3.34`(尚未执行真实 publish)
-- 本地 dist zip/tgz manifest 已重建到 `0.3.34`
-- 已验证:`npm view @vocmarket/voc-skill version dist-tags.latest` 返回 `0.3.33`;`0.3.34` 当前未占用
-- 已验证:`npm publish --dry-run --access public --ignore-scripts` 成功,包内 166 个文件
-- 已验证:`npm exec --yes --package @vocmarket/voc-skill@latest -- claude-voc workspace --smoke`
+- 当前 npm latest:`@vocmarket/voc-skill@0.3.36`
+- 本地发布产物:`@vocmarket/voc-skill@0.3.36`
+- 本地 dist zip/tgz manifest 已重建到 `0.3.36`,并标记 `published=true`
+- 已验证:`npm run claude-voc:build`、`npm run claude-voc:npm-pack`、`npm run claude-voc:acceptance`
+- 已验证:`0.3.36` 的 npm tarball、npx、workspace 安装 smoke,以及历史 Skill 规划/成本校准 smoke
 - 已迁移提示:旧包 `@gangvy/claude-code-voc-intelligence@*` 已标记为 deprecated,提示改用 `@vocmarket/voc-skill`。
 - 已新增:`npm run claude-voc:acceptance` 一键发布前验收,借鉴 `tihao-sop` 的 workspace 安装断言方式。
 - 已新增:图文报告资产缓存,证据样本首图默认缓存到 `outputs/<run>/assets/`,Markdown 报告优先引用本地 `assets/...`,JSON 保留原图 URL 与本地路径。
 - 已修复:无 VOC token、余额不足或未开通时,不再暴露底层 403/权限/余额报错,统一返回充值链接引导。
 - 已优化:小红书/社媒任务优先调用 VOC Skill/MCP,不用 WebSearch 替代;报告证据样本包含原帖链接、首图、原文摘录、高赞评论和“为什么值得看”。
 - 已修复:小红书原帖链接优先使用 `mini_program_info.webpage_url` 或带 `xsec_token` 的 `share_info.link`,不再把 `/explore/<noteId>` 作为主链接。
+- 已新增:`voc_cost_estimate` 可读取历史 Skill `api-config.json` 生成批处理报价;`voc_usage_report` 支持预计/实际偏差校准和本地样本沉淀。
 
 ## 发布成功但校验 404 的处理
 

+ 4 - 4
release/npm发布管理文档/NPM技能包发布管理.md

@@ -25,8 +25,8 @@
 
 | 技能包 | npm 包名 | 当前版本 | 状态 | 本地目录 | npm 链接 |
 |---|---|---:|---|---|---|
-| Claude Code VOC Intelligence | `@vocmarket/voc-skill` | `0.3.32` | 已发布 | `claude-code/claude-code-voc-intelligence/` | https://www.npmjs.com/package/@vocmarket/voc-skill |
-| 提号 SOP 博主名单 | `@vocmarket/tihao` | `1.0.18` | 已发布 | `E:\workspace\tihao-ai\claude-code-tihao-sourcing\` | https://www.npmjs.com/package/@vocmarket/tihao |
+| Claude Code VOC Intelligence | `@vocmarket/voc-skill` | `0.3.36` | 已发布;registry 与指定版本线上 workspace smoke 已通过 | `claude-code/claude-code-voc-intelligence/` | https://www.npmjs.com/package/@vocmarket/voc-skill |
+| 提号 SOP 博主名单 | `@vocmarket/tihao` | `1.0.19` | 已发布 | `E:\workspace\tihao-ai\claude-code-tihao-sourcing\` | https://www.npmjs.com/package/@vocmarket/tihao |
 | fmode 视觉分析 | `fmode-vision` | `0.1.1` | 已发布 | `claude-code/fmode-vision/`(如有) | https://www.npmjs.com/package/fmode-vision |
 | fmode 音视频处理 | `fmode-ffmpeg` | `0.1.1` | 已发布 | `claude-code/fmode-ffmpeg/` | https://www.npmjs.com/package/fmode-ffmpeg |
 | fmode 录音转写 | `fmode-listen` | `0.1.1` | 已发布 | `claude-code/fmode-listen/`(如有) | https://www.npmjs.com/package/fmode-listen |
@@ -95,7 +95,7 @@ npx --yes @vocmarket/tihao@latest workspace --smoke
 | `displayName` | `提号` |
 | `status` | `published` |
 | `npmPackage` | `@vocmarket/tihao` |
-| `npmVersion` | `1.0.18` |
+| `npmVersion` | `1.0.19` |
 | `installCommand` | `npx --yes @vocmarket/tihao@latest workspace --smoke` |
 | `globalInstallCommand` | `npm install -g @vocmarket/tihao && tihao install --smoke` |
 | `workspaceSkillPath` | `.claude/skills/tihao/SKILL.md` |
@@ -231,4 +231,4 @@ Pop-Location
 请切到 live 模式,并先检查 token 和余额状态。
 ```
 
-> 缺 token(`needs_token`)时**先引导自救**(读 `~/.claude/settings.json` 等来源的 `sk-` token),不直接甩充值链接;只有真正 `402 余额不足` 才返回充值/开通链接(`sk-` 计费链 → `studio/?balance=fmodeapi`,`r:` 会话 token 链 → `apig-pay/?apigid=Vo3ROWEvDy&fun_id=HOkkX72PMF`)。任何情况下都不允许把底层 403、权限或余额接口报错直接展示给用户。
+> 缺 token(`needs_token`)时先引导恢复凭证;只有确认 `402` 余额不足才由技能返回当前用户 Tokenized Balance 链接或显式二维码。任何情况下都不允许把底层 403、权限或余额接口报错直接展示给用户。

+ 23 - 0
release/npm发布管理文档/变更记录/voc-skill.md

@@ -3,6 +3,29 @@
 > 当前版本号见 [`../NPM技能包发布管理.md`](../NPM技能包发布管理.md) 的「发布状态总览」。本文按版本号倒序记录每次更新的简要内容。
 > 本地目录:`claude-code/claude-code-voc-intelligence/`。
 
+## 0.3.35(2026-09-06)
+
+- 新增历史 OpenClaw Skill 规划适配器:`voc_cost_estimate` 可读取本地 `api-config.json`,从参数默认值、pipeline、集合批量和分页上限生成经济/标准/完整版报价;经济版自动跳过带 condition 的可选步骤。
+- 新增预计/实际成本校准:报价目录写入 `cost-estimate.json`,`voc_usage_report` 支持 `estimatePath`、`calibrationPath`,输出总额和接口级偏差并保存本地样本。
+- 保持现有产品口径:NewAPI 仍按成功调用逐次扣费,充值仍复用 Tokenized Balance/现有二维码,媒体仍只缓存本地。
+- 验证通过:`smoke:cost-calibration`、`smoke:cost`、`acceptance`、`audit:index`、ZIP/npm/npx/workspace 安装 smoke;已正式发布并通过 registry 和线上 workspace smoke。
+
+## 0.3.36(2026-09-06)
+
+- 修复历史 Skill 规划适配器的显式通道映射:`channel=ecommerce` / `overseas` / `social` 分别归一到 `voc-ecom` / `voc-overseas` / `voc-social` 计费模型,避免把通道名误作为服务端模型名。
+- 增加通道映射回归 smoke;其他报价、校准、充值和本地媒体策略保持不变。
+- 正式发布:`npm view @vocmarket/voc-skill@0.3.36 version` 返回 `0.3.36`;指定版本 `npx --yes --package @vocmarket/voc-skill@0.3.36 -- claude-voc workspace --smoke` 通过。
+- 当前源码验收:`acceptance` 与 `audit:index` 通过,版本字段统一为 `0.3.36`。
+
+## 0.3.33(2026-09-01)
+
+- 新增并默认优先使用 `taobao.get_item_detail_v7`,用于实时发起商品标题、基础价格、SKU、库存和店铺信息采集; ecommerce 通道增至 289 个接口,全包增至 1344 个接口。
+- 新增 `docs/taobao-item-detail-v7.md`,明确 V7 的请求参数、核心字段、错误处理、实测结果,以及 V3/V4/V6/V7 的分工;不再将单一版本描述为覆盖页面全部字段。
+- V3/V4/V6/V7 统一加入 48 小时时效门禁:根据 `recordTime` 计算快照滞后时长,超时返回 `stale_data`,结果只作历史参考。
+- 修正 HTTP 200 中嵌套业务码的误判:`301` 会自动重试,连续失败后返回 `upstream_unstable`;`202` 返回 `not_supported`,两者都不再作为成功商品详情。
+- 根目录新增标准 `SKILL.md` 入口,保证上传 ZIP 或 npm 包到外部 Agent Skills 平台时可直接识别。
+- 发布验证:MCP smoke、package smoke、XHS/Douyin sample、主 acceptance、ZIP 解包安装、npm tarball/global/npx/workspace 安装均通过;已发布并匿名验证 npm `latest=0.3.33`(143 文件,package 616,325 bytes,unpacked 4,155,129 bytes,shasum `2fd579f81015215804fe6e0eb7f6860b81822e84`),全新目录线上 `@0.3.33 workspace --smoke` 通过。
+
 ## 0.3.32(2026-07-30)
 
 - 补齐上游淘宝/天猫 3 个有效接口:商品详情 V3、商品详情 V6、店铺商品列表 V4;ecommerce 通道从 285 增至 288,全包接口从 1340 增至 1343。