Kaynağa Gözat

更新企微技能包中dofire和音频上游接口为fmode

gangvy 1 ay önce
ebeveyn
işleme
d045a05da2
23 değiştirilmiş dosya ile 390 ekleme ve 84 silme
  1. 4 6
      claude-code/claude-code-qiwe-assistant/.env.example
  2. 3 3
      claude-code/claude-code-qiwe-assistant/README.md
  3. 4 6
      claude-code/claude-code-qiwe-assistant/docs/specs/qiwei-voice-clone.md
  4. 15 12
      claude-code/claude-code-qiwe-assistant/mcp/src/core/voice-clone-service.js
  5. 3 3
      claude-code/claude-code-qiwe-assistant/mcp/src/dashboard/agent-service.js
  6. 3 3
      claude-code/claude-code-qiwe-assistant/mcp/src/tools/qiwei-voice-run.js
  7. 39 6
      claude-code/claude-code-qiwe-assistant/scripts/voice-clone-smoke-test.js
  8. 4 4
      claude-code/claude-code-qiwe-assistant/skills/qiwei-voice/SKILL.md
  9. 1 1
      claude-code/claude-code-voc-intelligence/.claude-plugin/plugin.json
  10. 36 1
      claude-code/claude-code-voc-intelligence/mcp/catalog/params/taobao.json
  11. 152 10
      claude-code/claude-code-voc-intelligence/mcp/catalog/voc-social-endpoints.json
  12. 6 2
      claude-code/claude-code-voc-intelligence/mcp/src/core/api-catalog.js
  13. 2 2
      claude-code/claude-code-voc-intelligence/package-lock.json
  14. 1 1
      claude-code/claude-code-voc-intelligence/package.json
  15. 49 0
      claude-code/claude-code-voc-intelligence/scripts/smoke-mcp.js
  16. 3 3
      claude-code/claude-code-voc-intelligence/skill-package-manifest.json
  17. 12 1
      claude-code/claude-code-voc-intelligence/skills/voc-api-catalog/SKILL.md
  18. 3 3
      dist/claude-code-voc-intelligence-suite-manifest.json
  19. BIN
      dist/claude-code-voc-intelligence.zip
  20. 5 5
      dist/npm/claude-code-voc-npm-package-manifest.json
  21. 1 1
      release/npm发布管理文档/NPM技能包发布管理.md
  22. 9 1
      release/npm发布管理文档/变更记录/voc-skill.md
  23. 35 10
      scripts/deploy/claude-code-voc-intelligence-suite.js

+ 4 - 6
claude-code/claude-code-qiwe-assistant/.env.example

@@ -57,14 +57,12 @@ QIWEI_AGENT_STARTUP_GRACE_SECONDS=10
 QIWEI_RESPONSE_REMINDER_MINUTES=15
 QIWEI_RESPONSE_URGENT_MINUTES=60
 
-# ---- 云端声音克隆与企微语音 ----
-# IndexTTS2 服务密钥只写入 .env.local,禁止提交到仓库。
-QIWEI_TTS_API_KEY=
-QIWEI_TTS_ENDPOINT=https://www.yuntts.com/api/v1/indextts2_infer
-QIWEI_TTS_MODEL=IndexTeam/IndexTTS-2
+# ---- Fmode 声音克隆与企微语音 ----
+# 默认复用上面的 Fmode Token,无需单独配置语音服务密钥。
+QIWEI_VOICE_ENDPOINT=https://server.fmode.cn/api/voice/indextts2
 QIWEI_TTS_TIMEOUT_MS=180000
 
-# 企微语音固定通过 Fmode /doFileApi 直传,无公网 URL 回源模式
+# 合成走 /api/voice/indextts2;企微语音媒体通过 /api/qiwei/doFileApi 直传
 
 # Agent 模型。密钥只放 .env.local 或用户级模型配置,禁止提交到仓库。
 AGENT_PROVIDER=claude-code

+ 3 - 3
claude-code/claude-code-qiwe-assistant/README.md

@@ -349,8 +349,8 @@ Dashboard 的「智能会话」页把回调消息、意图识别、需求画像
 - 已确认的客户群会进入独立的「群聊智能回复」列表,Agent 自动生成草稿,但必须人工编辑或审核后才能发送到群;
 - 企业版通过全局 `paused` 或单会话人工接管停止 Agent 处理,但消息仍持续接收并保存;
 - 真实发送只允许命中 `QIWEI_AUTO_REPLY_ALLOWED_SENDERS` 白名单的联系人;
-- 客服输入文本后可自动识别「自然、友好、真诚致歉、温和关怀、明确提醒」,人工确认后直接生成并发送本人音色的原生企微语音;试听默认禁用,避免额外产生一次云端合成费用;
-- 语音媒体只通过已验证的 Fmode `/doFileApi` multipart 路由直接上传,路由不可用时直接报错,不暴露公网音频回源地址;
+- 客服输入文本后可自动识别「自然、友好、真诚致歉、温和关怀、明确提醒」,人工确认后通过 Fmode `/api/voice/indextts2` 生成并发送本人音色的原生企微语音;试听默认禁用,避免额外产生一次合成费用;
+- 语音合成与媒体上传复用同一个 Fmode Token;媒体只通过已验证的 Fmode `/api/qiwei/doFileApi` multipart 路由直接上传,路由不可用时直接报错,不暴露公网音频回源地址;
 - 「本人声音」支持浏览器录音或音频上传,参考录音按企微账号隔离,注销后删除;
 - 已发送语音在消息流中显示为语音气泡,点击气泡播放或暂停;左侧「转文字」直接展开合成时保存的准确原文,不会再次调用语音识别服务;
 - 使用待审核草稿发送语音成功后,该草稿会同步结算为已发送并关联语音消息,文字操作区恢复为「让 Agent 处理」,避免同一回复再次以文字发送;
@@ -452,7 +452,7 @@ Agent 可按业务场景直接定位到对应 Skill 文档,每个 Skill 内部
 
 ## 限制
 
-- `cloud` 模块的 multipart 文件直传尚未由 Fmode 网关支持,`qiwei_api_call` 会拒绝这类接口
+- 通用 `qiwei_api_call` 仍拒绝 multipart;语音媒体由专用 `/api/qiwei/doFileApi` 路由上传
 - `/login/*`、`/client/*` 不允许通过 `qiwei_api_call` 透传,必须使用登录专用工具。
 - 服务端未挂载前,默认生产地址会返回不可用;可通过 `QIWEI_API_BASE` 指向测试环境。
 - 官方 CLI 首次下载需要 npm 网络访问,首次业务调用前需要独立完成企业微信机器人扫码授权。

+ 4 - 6
claude-code/claude-code-qiwe-assistant/docs/specs/qiwei-voice-clone.md

@@ -9,9 +9,9 @@
 ```text
 本人参考录音
   -> 16kHz 单声道 WAV 档案
-  -> 云端 IndexTTS2
+  -> Fmode /api/voice/indextts2
   -> 24kHz 单声道 SILK
-  -> Fmode /doFileApi 直传
+  -> Fmode /api/qiwei/doFileApi 直传
   -> /msg/sendVoice
 ```
 
@@ -84,13 +84,11 @@ Dashboard 从待审核草稿发送语音时,在 `voice-send` 请求中携带
 ## 配置
 
 ```dotenv
-QIWEI_TTS_API_KEY=
-QIWEI_TTS_ENDPOINT=https://www.yuntts.com/api/v1/indextts2_infer
-QIWEI_TTS_MODEL=IndexTeam/IndexTTS-2
+QIWEI_VOICE_ENDPOINT=https://server.fmode.cn/api/voice/indextts2
 QIWEI_TTS_TIMEOUT_MS=180000
 ```
 
-语音媒体只使用 Fmode 网关的 `/doFileApi` multipart 代理。该路由已于 2026-07-28 完成真实 SILK 上传验证,可以直接返回企微发送所需的 `fileId`、`fileAesKey` 和 `fileSize`。路由不可用或上传失败时直接报错,不使用公网 URL 回源。系统会在合成前检查 Fmode 鉴权和企微账号配置,避免已知无法上传时产生合成费用。
+语音合成使用 Fmode `/api/voice/indextts2`,自动复用技能包的 Fmode Token。语音媒体只使用 Fmode 网关的 `/api/qiwei/doFileApi` multipart 代理;该路由已完成真实 SILK 上传验证,可以直接返回企微发送所需的 `fileId`、`fileAesKey` 和 `fileSize`。路由不可用或上传失败时直接报错,不使用公网 URL 回源。系统会在合成前检查 Fmode 鉴权和企微账号配置,避免已知无法上传时产生合成费用。
 
 ## 安全
 

+ 15 - 12
claude-code/claude-code-qiwe-assistant/mcp/src/core/voice-clone-service.js

@@ -156,9 +156,9 @@ function extensionFor(name, mime = '') {
 class VoiceCloneService {
   constructor({ config = {}, qiwei }) {
     this.config = {
-      endpoint: String(config.endpoint || 'https://www.yuntts.com/api/v1/indextts2_infer').trim(),
-      apiKey: String(config.apiKey || '').trim(),
-      model: String(config.model || 'IndexTeam/IndexTTS-2').trim(),
+      endpoint: String(config.endpoint || 'https://server.fmode.cn/api/voice/indextts2').trim(),
+      authToken: String(config.authToken || '').trim().replace(/^Bearer\s+/i, ''),
+      model: String(config.model || 'fmode-voice').trim(),
       requestTimeoutMs: Math.max(15000, Math.min(300000, Number(config.requestTimeoutMs) || 180000)),
     };
     this.qiwei = qiwei;
@@ -190,9 +190,10 @@ class VoiceCloneService {
 
   status() {
     const metadata = this.readMetadata();
+    const authToken = this.config.authToken || String(this.qiwei?.context?.()?.token || '').trim();
     return {
-      configured: Boolean(this.config.apiKey && this.config.endpoint),
-      provider: 'yuntts-indextts2',
+      configured: Boolean(authToken && this.config.endpoint),
+      provider: 'fmode-voice',
       model: this.config.model,
       enrolled: Boolean(metadata && fs.existsSync(this.profilePath())),
       profile: metadata ? {
@@ -259,12 +260,14 @@ class VoiceCloneService {
   }
 
   requireReady() {
-    if (!this.config.apiKey) throw new Error('缺少 QIWEI_TTS_API_KEY,请先配置云端语音服务');
+    const authToken = this.config.authToken || String(this.qiwei?.context?.()?.token || '').trim();
+    if (!authToken) throw new Error('缺少 Fmode 鉴权,请先在 Fmode Studio 中完成登录');
     if (!fs.existsSync(this.profilePath())) throw new Error('尚未初始化本人声音,请先录制或上传参考音频');
+    return authToken;
   }
 
   async synthesize({ text, context = '', tone = 'auto' }) {
-    this.requireReady();
+    const authToken = this.requireReady();
     const input = String(text || '').trim();
     if (!input) throw new Error('语音文本不能为空');
     if (input.length > 600) throw new Error('语音文本不能超过 600 字');
@@ -290,13 +293,13 @@ class VoiceCloneService {
     try {
       response = await fetch(this.config.endpoint, {
         method: 'POST',
-        headers: { Authorization: `Bearer ${this.config.apiKey}` },
+        headers: { Authorization: `Bearer ${authToken}` },
         body: form,
         signal: controller.signal,
       });
     } catch (error) {
       if (error.name === 'AbortError') throw new Error('声音合成超时,请稍后重试');
-      throw new Error(`声音合成服务不可用:${error.message}`);
+      throw new Error(`Fmode 语音服务不可用:${error.message}`);
     } finally {
       clearTimeout(timeout);
     }
@@ -305,7 +308,7 @@ class VoiceCloneService {
       const raw = (await readResponseBuffer(response, MAX_ERROR_BODY_BYTES)).toString('utf8');
       let parsed;
       try { parsed = JSON.parse(raw); } catch { parsed = {}; }
-      throw new Error(parsed.message || parsed.error || `声音合成失败(HTTP ${response.status})`);
+      throw new Error(parsed.mess || parsed.message || parsed.error || `声音合成失败(HTTP ${response.status})`);
     }
     const runDir = createRunDir('voice', `clone-${crypto.randomBytes(4).toString('hex')}`);
     const wavPath = path.join(runDir, 'speech.wav');
@@ -314,7 +317,7 @@ class VoiceCloneService {
     if (!wavInfo.duration || wavInfo.duration > 180) throw new Error('声音合成结果无效');
     const manifestPath = writeRunManifest(runDir, {
       type: 'voice-clone',
-      provider: 'yuntts-indextts2',
+      provider: 'fmode-voice',
       model: this.config.model,
       textLength: input.length,
       textSha256: crypto.createHash('sha256').update(input).digest('hex'),
@@ -376,7 +379,7 @@ class VoiceCloneService {
       if (outcome !== 'sent') removeFile(result.wavPath);
       writeRunManifest(result.runDir, {
         type: 'voice-clone',
-        provider: 'yuntts-indextts2',
+        provider: 'fmode-voice',
         model: this.config.model,
         textLength: result.text.length,
         textSha256: crypto.createHash('sha256').update(result.text).digest('hex'),

+ 3 - 3
claude-code/claude-code-qiwe-assistant/mcp/src/dashboard/agent-service.js

@@ -180,9 +180,9 @@ function loadAgentConfig(overrides = {}) {
       responseUrgentMinutes: number('QIWEI_RESPONSE_URGENT_MINUTES', 60, 1, 10080),
     },
     voice: {
-      endpoint: value('QIWEI_TTS_ENDPOINT', 'https://www.yuntts.com/api/v1/indextts2_infer'),
-      apiKey: value('QIWEI_TTS_API_KEY'),
-      model: value('QIWEI_TTS_MODEL', 'IndexTeam/IndexTTS-2'),
+      endpoint: value('QIWEI_VOICE_ENDPOINT', 'https://server.fmode.cn/api/voice/indextts2'),
+      authToken: value('QIWEI_VOICE_AUTH_TOKEN'),
+      model: 'fmode-voice',
       requestTimeoutMs: number('QIWEI_TTS_TIMEOUT_MS', 180000, 15000, 300000),
     },
   };

+ 3 - 3
claude-code/claude-code-qiwe-assistant/mcp/src/tools/qiwei-voice-run.js

@@ -257,9 +257,9 @@ function voiceRuntime(input = {}) {
   return new VoiceCloneService({
     qiwei,
     config: {
-      endpoint: process.env.QIWEI_TTS_ENDPOINT || env.QIWEI_TTS_ENDPOINT,
-      apiKey: process.env.QIWEI_TTS_API_KEY || env.QIWEI_TTS_API_KEY,
-      model: process.env.QIWEI_TTS_MODEL || env.QIWEI_TTS_MODEL,
+      endpoint: process.env.QIWEI_VOICE_ENDPOINT || env.QIWEI_VOICE_ENDPOINT,
+      authToken: input.authToken,
+      model: 'fmode-voice',
       requestTimeoutMs: process.env.QIWEI_TTS_TIMEOUT_MS || env.QIWEI_TTS_TIMEOUT_MS,
     },
   });

+ 39 - 6
claude-code/claude-code-qiwe-assistant/scripts/voice-clone-smoke-test.js

@@ -15,6 +15,7 @@ const {
   readResponseBuffer,
 } = require('../mcp/src/core/voice-clone-service');
 const { __testing: voiceToolTesting } = require('../mcp/src/tools/qiwei-voice-run');
+const { FmodeQiweiClient } = require('../mcp/src/providers/fmode-agent-transport');
 
 function wavBuffer({ duration = 1, sampleRate = 16000, frequency = 220 } = {}) {
   const samples = Math.floor(duration * sampleRate);
@@ -61,7 +62,7 @@ async function main() {
   const calls = [];
   let sendSuccess = true;
   const qiwei = {
-    context: () => ({ uid: 'account-smoke', guid: 'guid-smoke' }),
+    context: () => ({ token: 'fmode-smoke-token', uid: 'account-smoke', guid: 'guid-smoke' }),
     isConfigured: () => true,
     async uploadVoiceFile(filePath) {
       calls.push({ type: 'upload', filePath });
@@ -76,9 +77,8 @@ async function main() {
   const service = new VoiceCloneService({
     qiwei,
     config: {
-      endpoint: 'https://tts.example/indextts2',
-      apiKey: 'test-key',
-      model: 'IndexTeam/IndexTTS-2',
+      endpoint: 'https://server.fmode.cn/api/voice/indextts2',
+      model: 'fmode-voice',
     },
   });
 
@@ -93,7 +93,9 @@ async function main() {
 
   const originalFetch = global.fetch;
   const generated = wavBuffer({ duration: 1.2, sampleRate: 22050, frequency: 330 });
-  global.fetch = async (_url, options = {}) => {
+  global.fetch = async (url, options = {}) => {
+    assert.equal(url, 'https://server.fmode.cn/api/voice/indextts2');
+    assert.equal(options.headers.Authorization, 'Bearer fmode-smoke-token');
     const payload = JSON.parse(options.body.get('payload'));
     assert.equal(payload.use_random, false);
     assert.equal([0, 3].includes(payload.emo_control_method), true);
@@ -141,9 +143,40 @@ async function main() {
     global.fetch = originalFetch;
   }
 
+  const uploadClient = new FmodeQiweiClient({
+    authToken: 'fmode-upload-token',
+    uid: 'upload-account',
+    guid: 'upload-guid',
+    apiBase: 'https://server.fmode.cn/api/qiwei',
+  });
+  const silkPath = path.join(root, 'upload.silk');
+  fs.writeFileSync(silkPath, Buffer.from('silk-smoke'));
+  global.fetch = async (url, options = {}) => {
+    assert.equal(url, 'https://server.fmode.cn/api/qiwei/doFileApi');
+    assert.equal(options.headers.Authorization, 'Bearer fmode-upload-token');
+    assert.equal(options.body.get('uid'), 'upload-account');
+    assert.equal(options.body.get('guid'), 'upload-guid');
+    assert.equal(options.body.get('method'), '/cloud/cdnBigUpload');
+    assert.equal(options.body.get('fileType'), '5');
+    assert.equal(options.body.get('file').name, 'upload.silk');
+    return new Response(JSON.stringify({
+      code: 200,
+      data: { data: { fileId: 'file-upload', fileAesKey: 'aes-upload', fileSize: 10 } },
+    }), { status: 200, headers: { 'Content-Type': 'application/json' } });
+  };
+  try {
+    assert.deepEqual(await uploadClient.uploadVoiceFile(silkPath), {
+      fileId: 'file-upload',
+      fileAesKey: 'aes-upload',
+      fileSize: 10,
+    });
+  } finally {
+    global.fetch = originalFetch;
+  }
+
   service.revoke();
   assert.equal(service.status().enrolled, false);
-  console.log('[ok] voice clone tone, enrollment, retained sent WAV, SILK and send orchestration');
+  console.log('[ok] Fmode voice synthesis, doFileApi upload, tone, enrollment, SILK and send orchestration');
 }
 
 main()

+ 4 - 4
claude-code/claude-code-qiwe-assistant/skills/qiwei-voice/SKILL.md

@@ -52,13 +52,13 @@ description: 管理企微语音转写与本人声音克隆。用于接收、解
 
 ## 上传策略
 
-只通过 Fmode `/doFileApi` 上传本地 SILK。网关未开放或上传失败时直接报错,不使用公网 URL 回源。语音发送请求发生网络结果不确定时不自动重试,避免重复发送。
+声音合成通过 Fmode `/api/voice/indextts2`,并复用技能包的 Fmode Token。只通过 Fmode `/api/qiwei/doFileApi` 上传本地 SILK;上传失败时直接报错,不使用公网 URL 回源。语音发送请求发生网络结果不确定时不自动重试,避免重复发送。
 
 ## 配置与依赖
 
-- `QIWEI_TTS_API_KEY`:云端 IndexTTS2 密钥
-- `QIWEI_TTS_ENDPOINT`:默认 `https://www.yuntts.com/api/v1/indextts2_infer`
-- `QIWEI_TTS_MODEL`:默认 `IndexTeam/IndexTTS-2`
+- `QIWEI_VOICE_ENDPOINT`:默认 `https://server.fmode.cn/api/voice/indextts2`
+- Fmode 鉴权:自动复用 `QIWEI_AUTH_TOKEN`、`FMODE_API_KEY` 或当前 Fmode Studio 的 NewAPI Token
+- 语音合成按 Fmode 服务端计费规则扣除 Token 额度
 - `@binsee/wx-voice`:SILK 编解码;
 - `@ffmpeg-installer/ffmpeg`、`@ffprobe-installer/ffprobe`:音频标准化和校验。
 

+ 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.31",
+  "version": "0.3.32",
   "author": {
     "name": "nkkj-BrainHack"
   }

+ 36 - 1
claude-code/claude-code-voc-intelligence/mcp/catalog/params/taobao.json

@@ -17,6 +17,15 @@
       "desc": "淘宝/天猫上的唯一商品标识符(商品ID)。"
     }
   ],
+  "taobao.get_item_detail_v3": [
+    {
+      "name": "itemId",
+      "in": "query",
+      "type": "string",
+      "required": true,
+      "desc": "淘宝/天猫上的唯一商品标识符(商品ID)。"
+    }
+  ],
   "taobao.get_item_detail_v4": [
     {
       "name": "itemId",
@@ -35,6 +44,15 @@
       "desc": "淘宝/天猫上的唯一商品标识符(商品ID)。"
     }
   ],
+  "taobao.get_item_detail_v6": [
+    {
+      "name": "itemId",
+      "in": "query",
+      "type": "string",
+      "required": true,
+      "desc": "淘宝/天猫上的唯一商品标识符(商品ID)。"
+    }
+  ],
   "taobao.get_item_detail_v9": [
     {
       "name": "itemId",
@@ -167,6 +185,23 @@
       "default": 1
     }
   ],
+  "taobao.get_shop_item_list_v4": [
+    {
+      "name": "sellerId",
+      "in": "query",
+      "type": "string",
+      "required": true,
+      "desc": "用于标识淘宝/天猫店铺的卖家用户ID。"
+    },
+    {
+      "name": "page",
+      "in": "query",
+      "type": "integer",
+      "required": false,
+      "desc": "用于分页的页码。",
+      "default": 1
+    }
+  ],
   "taobao.get_item_sale_v1": [
     {
       "name": "itemId",
@@ -223,4 +258,4 @@
       "default": 1
     }
   ]
-}
+}

+ 152 - 10
claude-code/claude-code-voc-intelligence/mcp/catalog/voc-social-endpoints.json

@@ -22542,7 +22542,11 @@
       "platform": "taobao",
       "channel": "ecommerce",
       "title": "淘宝天猫·商品详情",
-      "summary": "获取淘宝和天猫商品详情数据,包括价格、图片和店铺详情,用于商品研究、目录监控和电商分析。",
+      "summary": "淘宝天猫商品详情兼容版本,可能返回上游快照。查价格与完整详情优先使用 v6,查页面展示销量使用 v3。",
+      "recommendedFor": [
+        "legacy_detail_compatibility"
+      ],
+      "freshness": "recordTime 是上游数据采集时间,不代表本次请求时间;调用方应根据 recordTime 计算滞后时长。",
       "method": "GET",
       "paramsIn": "query",
       "proxyPath": "taobao/get-item-detail/v1",
@@ -22570,7 +22574,8 @@
       "platform": "taobao",
       "channel": "ecommerce",
       "title": "淘宝天猫·商品详情",
-      "summary": "通过任务驱动的v2流程获取淘宝和天猫商品详情数据。如果数据在短暂等待后仍未就绪,响应将返回待处理任务状态。",
+      "summary": "商品详情 V2 任务结果读取接口;直接调用不会发起商品数据采集。普通详情查询优先使用 v6。",
+      "recommendationPriority": -100,
       "method": "GET",
       "paramsIn": "query",
       "proxyPath": "taobao/get-item-detail/v2",
@@ -22593,12 +22598,52 @@
       ],
       "billing": 1
     },
+    {
+      "id": "taobao.get_item_detail_v3",
+      "platform": "taobao",
+      "channel": "ecommerce",
+      "title": "淘宝天猫·页面展示销量(V3)",
+      "summary": "通过商品 ID 获取淘宝或天猫商品页当前展示的销量,适合核对“已售/付款人数”;金额与 SKU 明细请使用 v6。",
+      "method": "GET",
+      "paramsIn": "query",
+      "proxyPath": "taobao/get-item-detail/v3",
+      "params": [],
+      "requiredParams": [
+        "itemId"
+      ],
+      "recommendationPriority": 90,
+      "recommendedFor": [
+        "page_sales",
+        "displayed_sales",
+        "buyer_count"
+      ],
+      "freshness": "recordTime 是上游数据采集时间;返回前必须与当前时间比较,不把请求时间当作采集时间。",
+      "responseHint": "页面销量读取 data.sellCount;该版本不返回商品价格。",
+      "tags": [
+        "taobao",
+        "淘宝天猫",
+        "淘宝",
+        "天猫",
+        "tmall",
+        "Taobao and Tmall",
+        "ecommerce",
+        "电商",
+        "商品",
+        "product",
+        "页面销量",
+        "已售",
+        "付款人数",
+        "sellCount",
+        "page sales"
+      ],
+      "billing": 1
+    },
     {
       "id": "taobao.get_item_detail_v4",
       "platform": "taobao",
       "channel": "ecommerce",
-      "title": "淘宝天猫·商品详情",
-      "summary": "获取淘宝和天猫商品详情数据,包括价格、图片和店铺详情,用于商品研究、目录监控和电商分析。",
+      "title": "淘宝天猫·券后价与商品详情(V4)",
+      "summary": "获取淘宝或天猫商品的标价、SKU 优惠后价格与商品详情;需要券后价/优惠后价口径时优先使用。",
       "method": "GET",
       "paramsIn": "query",
       "proxyPath": "taobao/get-item-detail/v4",
@@ -22606,6 +22651,13 @@
       "requiredParams": [
         "itemId"
       ],
+      "recommendationPriority": 95,
+      "recommendedFor": [
+        "coupon_price",
+        "discount_price"
+      ],
+      "freshness": "recordTime 是上游数据采集时间;优惠后价仍可因账号、SKU、地区、优惠券和活动时间与结算页不同。",
+      "responseHint": "价格单位通常为分:itemPrice 是标价,DiscountPrice 是优惠后价,SKU 读取 sku2info.finalPrice/finalSkuPrice。",
       "tags": [
         "taobao",
         "淘宝天猫",
@@ -22617,7 +22669,13 @@
         "ecommerce",
         "电商",
         "商品",
-        "product"
+        "product",
+        "商品详情",
+        "券后价",
+        "优惠后价",
+        "DiscountPrice",
+        "coupon price",
+        "discount price"
       ],
       "billing": 1
     },
@@ -22649,12 +22707,54 @@
       ],
       "billing": 1
     },
+    {
+      "id": "taobao.get_item_detail_v6",
+      "platform": "taobao",
+      "channel": "ecommerce",
+      "title": "淘宝天猫·商品详情与价格(V6)",
+      "summary": "通过商品 ID 获取淘宝或天猫商品详情、SKU 价格、促销价、图片和店铺信息;查价格与完整详情时优先使用。页面展示销量请另用 v3 核对。",
+      "method": "GET",
+      "paramsIn": "query",
+      "proxyPath": "taobao/get-item-detail/v6",
+      "params": [],
+      "requiredParams": [
+        "itemId"
+      ],
+      "recommendationPriority": 100,
+      "recommendedFor": [
+        "product_detail",
+        "price",
+        "sku_price",
+        "promotion_price"
+      ],
+      "freshness": "recordTime 是上游数据采集时间;返回前必须与当前时间比较,不把请求时间当作采集时间。",
+      "responseHint": "价格优先读取 itemPrice.price / promotionPrice 及 SKU 价格;页面展示销量以 v3 的 sellCount 为准。",
+      "tags": [
+        "taobao",
+        "淘宝天猫",
+        "淘宝",
+        "天猫",
+        "tmall",
+        "Taobao and Tmall",
+        "ecommerce",
+        "电商",
+        "商品",
+        "product",
+        "商品详情",
+        "价格",
+        "促销价",
+        "SKU",
+        "product detail",
+        "price"
+      ],
+      "billing": 1
+    },
     {
       "id": "taobao.get_item_detail_v9",
       "platform": "taobao",
       "channel": "ecommerce",
-      "title": "淘宝天猫·商品详情",
-      "summary": "获取淘宝和天猫商品详情数据,包括价格、图片和店铺详情,用于商品研究、目录监控和电商分析。",
+      "title": "淘宝天猫·商品详情兼容版(V9)",
+      "summary": "淘宝天猫商品详情 V9 兼容数据源。num 是该版本的销售量级信号,totalCount 是评价总量,两者均不等于商品页当前展示付款人数。价格/关键属性优先 v6,页面销量优先 v3。",
       "method": "GET",
       "paramsIn": "query",
       "proxyPath": "taobao/get-item-detail/v9",
@@ -22662,6 +22762,12 @@
       "requiredParams": [
         "itemId"
       ],
+      "recommendationPriority": -50,
+      "recommendedFor": [
+        "legacy_v9_compatibility"
+      ],
+      "freshness": "recordTime 只表示 V9 响应快照的采集时间;嵌套 attribute 可能来自更旧的属性源,不能仅根据 recordTime 判定每个属性都已刷新。",
+      "responseHint": "num=V9 销售量级信号;totalCount=评价总量;attribute=V9 属性快照。不要把 num/totalCount 当作当前页面付款人数,也不要用 V9 attribute 覆盖 V6 的更新关键属性。",
       "tags": [
         "taobao",
         "淘宝天猫",
@@ -22795,7 +22901,8 @@
       "platform": "taobao",
       "channel": "ecommerce",
       "title": "淘宝天猫·店铺商品列表",
-      "summary": "获取淘宝和天猫店铺商品列表数据,包括商品标题、价格和图片,用于卖家研究和目录跟踪。",
+      "summary": "已弃用的淘宝天猫店铺商品列表 V3;新调用使用 v4,只需 sellerId 并支持分页。",
+      "deprecated": true,
       "method": "GET",
       "paramsIn": "query",
       "proxyPath": "taobao/get-shop-item-list/v3",
@@ -22819,12 +22926,47 @@
       ],
       "billing": 1
     },
+    {
+      "id": "taobao.get_shop_item_list_v4",
+      "platform": "taobao",
+      "channel": "ecommerce",
+      "title": "淘宝天猫·店铺商品列表(V4)",
+      "summary": "通过 sellerId 分页获取淘宝或天猫店铺商品列表,支持全部淘宝和天猫店铺;新调用优先使用。",
+      "method": "GET",
+      "paramsIn": "query",
+      "proxyPath": "taobao/get-shop-item-list/v4",
+      "params": [],
+      "requiredParams": [
+        "sellerId"
+      ],
+      "recommendationPriority": 100,
+      "recommendedFor": [
+        "shop_product_list",
+        "seller_catalog"
+      ],
+      "tags": [
+        "taobao",
+        "淘宝天猫",
+        "淘宝",
+        "天猫",
+        "tmall",
+        "Taobao and Tmall",
+        "ecommerce",
+        "电商",
+        "店铺商品",
+        "店铺商品列表",
+        "sellerId",
+        "shop product list"
+      ],
+      "billing": 1
+    },
     {
       "id": "taobao.get_item_sale_v1",
       "platform": "taobao",
       "channel": "ecommerce",
       "title": "淘宝天猫·商品销量",
-      "summary": "获取淘宝和天猫商品销量数据,用于销售追踪、商品研究和电商监控。",
+      "summary": "已弃用的 30 天商品销量接口,上游可能返回空数据。核对商品页“已售/付款人数”使用 get_item_detail_v3。",
+      "deprecated": true,
       "method": "GET",
       "paramsIn": "query",
       "proxyPath": "taobao/get-item-sale/v1",
@@ -28379,4 +28521,4 @@
       "billing": 1
     }
   ]
-}
+}

+ 6 - 2
claude-code/claude-code-voc-intelligence/mcp/src/core/api-catalog.js

@@ -78,7 +78,8 @@ function searchEndpoints({ query, platform, tag, limit = 20 } = {}) {
   const tagKey = normalize(tag);
   let results = listEndpoints().map(endpoint => ({
     endpoint,
-    score: scoreEndpoint(endpoint, terms)
+    score: scoreEndpoint(endpoint, terms),
+    preference: endpoint.deprecated ? -1000 : Number(endpoint.recommendationPriority || 0)
   }));
   if (platformKey) {
     results = results.filter(item => normalize(item.endpoint.platform) === platformKey);
@@ -87,7 +88,7 @@ function searchEndpoints({ query, platform, tag, limit = 20 } = {}) {
     results = results.filter(item => (item.endpoint.tags || []).some(t => normalize(t) === tagKey));
   }
   results = results.filter(item => item.score > 0);
-  results.sort((a, b) => b.score - a.score);
+  results.sort((a, b) => (b.score - a.score) || (b.preference - a.preference));
   return results.slice(0, Math.max(1, limit)).map(item => item.endpoint);
 }
 
@@ -99,6 +100,9 @@ function summarizeEndpoint(endpoint) {
     summary: endpoint.summary,
     method: endpoint.method,
     proxyPath: endpoint.proxyPath,
+    deprecated: Boolean(endpoint.deprecated),
+    recommendedFor: endpoint.recommendedFor || [],
+    freshness: endpoint.freshness || null,
     requiredParams: (endpoint.params && endpoint.params.length)
       ? endpoint.params.filter(p => p.required).map(p => p.name)
       : (endpoint.requiredParams || []),

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

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

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

@@ -1,6 +1,6 @@
 {
   "name": "@vocmarket/voc-skill",
-  "version": "0.3.31",
+  "version": "0.3.32",
   "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": {

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

@@ -291,6 +291,55 @@ async function main() {
       throw new Error(`Expected voc_api_search to list douyin endpoints, got: ${JSON.stringify(apiSearchStructured)}`);
     }
 
+    const taobaoDetailSearchResult = await client.callTool({
+      name: 'voc_api_search',
+      arguments: { platform: 'taobao', query: '淘宝 商品详情' }
+    });
+    const taobaoDetailEndpoints = taobaoDetailSearchResult.structuredContent?.data?.endpoints || [];
+    const taobaoDetailIds = taobaoDetailEndpoints.map(endpoint => endpoint.id);
+    if (taobaoDetailIds[0] !== 'taobao.get_item_detail_v6') {
+      throw new Error(`Expected current Taobao endpoints with v6 preferred for product details, got: ${taobaoDetailIds.join(', ')}`);
+    }
+
+    const taobaoCatalogResult = await client.callTool({
+      name: 'voc_api_search',
+      arguments: { platform: 'taobao' }
+    });
+    const taobaoCatalogIds = (taobaoCatalogResult.structuredContent?.data?.endpoints || []).map(endpoint => endpoint.id);
+    for (const id of ['taobao.get_item_detail_v3', 'taobao.get_item_detail_v6', 'taobao.get_shop_item_list_v4']) {
+      if (!taobaoCatalogIds.includes(id)) {
+        throw new Error(`Expected current Taobao catalog to include ${id}, got: ${taobaoCatalogIds.join(', ')}`);
+      }
+    }
+
+    const taobaoSalesSearchResult = await client.callTool({
+      name: 'voc_api_search',
+      arguments: { platform: 'taobao', query: '淘宝 页面销量' }
+    });
+    const taobaoSalesEndpoints = taobaoSalesSearchResult.structuredContent?.data?.endpoints || [];
+    if (taobaoSalesEndpoints[0]?.id !== 'taobao.get_item_detail_v3' || taobaoSalesEndpoints[0]?.deprecated) {
+      throw new Error(`Expected Taobao v3 to be preferred for page sales, got: ${JSON.stringify(taobaoSalesEndpoints)}`);
+    }
+
+    const taobaoCouponPriceSearchResult = await client.callTool({
+      name: 'voc_api_search',
+      arguments: { platform: 'taobao', query: '淘宝 券后价' }
+    });
+    const taobaoCouponPriceEndpoints = taobaoCouponPriceSearchResult.structuredContent?.data?.endpoints || [];
+    if (taobaoCouponPriceEndpoints[0]?.id !== 'taobao.get_item_detail_v4') {
+      throw new Error(`Expected Taobao v4 to be preferred for coupon prices, got: ${JSON.stringify(taobaoCouponPriceEndpoints)}`);
+    }
+
+    const taobaoV9DocResult = await client.callTool({
+      name: 'voc_api_doc',
+      arguments: { id: 'taobao.get_item_detail_v9' }
+    });
+    const taobaoV9Endpoint = taobaoV9DocResult.structuredContent?.data?.endpoint || {};
+    if (!String(taobaoV9Endpoint.responseHint || '').includes('totalCount=') ||
+      Number(taobaoV9Endpoint.recommendationPriority) >= 0) {
+      throw new Error(`Expected Taobao v9 field semantics and compatibility priority, got: ${JSON.stringify(taobaoV9Endpoint)}`);
+    }
+
     const apiDocResult = await client.callTool({
       name: 'voc_api_doc',
       arguments: { id: 'douyin.search_general' }

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

@@ -1,6 +1,6 @@
 {
   "name": "claude-code-voc-intelligence",
-  "version": "0.3.31",
+  "version": "0.3.32",
   "capabilities": {
     "summary": "三类情报源同源接入:①社媒内容 ②国内电商+创作者情报 ③海外选品。统一 search→doc→call、鉴权计费一致。",
     "channels": [
@@ -28,7 +28,7 @@
         "key": "ecommerce",
         "label": "国内电商 + 创作者情报",
         "platforms": 32,
-        "apis": 285,
+        "apis": 288,
         "examples": [
           "京东",
           "淘宝天猫",
@@ -53,7 +53,7 @@
     ],
     "totals": {
       "platforms": 51,
-      "apis": 1340
+      "apis": 1343
     }
   },
   "description": "Claude Code native VOC intelligence package for cross-industry social VOC workflows. Includes Xiaohongshu and Douyin trend intelligence, live token checks, evidence-card reports, single-issue VOC deep dives, boss/operator action plans, and scoped multi-turn memory.",

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

@@ -66,7 +66,18 @@ VOC 后端是一个**通用转发网关**(`https://server.fmode.cn/api/voc-soc
    - 能力与性价比的客观评估:抖音 `douyin.xingtu.cost_performance_analysis` / `conversion_analysis`;小红书 `xiaohongshu.pgy.cost_effectiveness_analysis`。
 3. 把这些创作者侧事实**喂给下游 VOC 技能**(voc-competitor-map 做竞对达人布局、voc-content-plan / voc-speaking-script 做选题与脚本、voc-issue-pool 补充创作者 / 受众视角),形成洞察结论,而非「该投谁」的采买清单。
 
-> 提示:`voc_api_search` 用「星图 创作者」「蒲公英 画像」「受众分布」「性价比」等词即可定位创作者接口,用「京东 商品」「淘宝 评论」「1688 搜索」「亚马逊 选品」「ABA 关键词」等词定位电商/选品接口;`voc_api_doc` 会在请求行标注通道(ecommerce / overseas)并列出参数。当前清单 ecommerce 通道已登记约 285 个接口(电商商品 + 创作者 + 内容平台)、overseas 通道 37 个(亚马逊选品),未登记的可直接用 `voc_api_call` 传 `rawPath` + 对应 `channel` 调用。
+> 提示:`voc_api_search` 用「星图 创作者」「蒲公英 画像」「受众分布」「性价比」等词即可定位创作者接口,用「京东 商品」「淘宝 评论」「1688 搜索」「亚马逊 选品」「ABA 关键词」等词定位电商/选品接口;`voc_api_doc` 会在请求行标注通道(ecommerce / overseas)并列出参数。当前清单 ecommerce 通道已登记约 288 个接口(电商商品 + 创作者 + 内容平台)、overseas 通道 37 个(亚马逊选品),未登记的可直接用 `voc_api_call` 传 `rawPath` + 对应 `channel` 调用。
+
+## 淘宝/天猫版本选择与时效
+
+- 查商品详情、SKU 标价与促销价:优先 `taobao.get_item_detail_v6`;查券后价/优惠后价使用 `taobao.get_item_detail_v4`。价格必须标注是标价、促销价、活动价还是券后价,不要压成单一「真实价格」。
+- 查商品页当前展示的「已售/付款人数」:使用 `taobao.get_item_detail_v3` 的 `data.sellCount`。V3 不返回价格,价格与页面销量需分别调用 V6/V3。
+- `taobao.get_item_detail_v1` 仅用于兼容旧流程;`v2` 直接调用不会发起商品采集。`taobao.get_item_sale_v1` 和 `taobao.get_shop_item_list_v3` 已被上游标记弃用。
+- V9 的 `num` 是该版本销售量级信号,`totalCount` 是评价总量,两者都不是商品页当前展示付款人数。V9 `attribute` 可能是独立旧属性快照;价格和“上市时间”等关键属性优先 V6,页面销量优先 V3。
+- 返回里的 `recordTime` 是上游数据采集时间,不是本次 API 请求时间。每个版本有独立数据源/快照,必须用「当前时间 - recordTime」计算滞后并在结果中明示。
+- 上游文档未给出固定刷新 SLA,不要将返回值表述为「请求时实时数据」,也不要将旧版 `orderPayUV` 与商品页当前展示口径直接等同。需要页面口径时以 V3 `sellCount` 为准。
+- 实际结算价还会受 SKU、账号会员价、地区、店铺券、平台补贴和活动时间影响。API 返回的是对应口径快照,不等于任意账号在结算页的最终应付价。
+- 店铺商品列表的新调用优先 `taobao.get_shop_item_list_v4`,入参为 `sellerId` + 可选 `page`。
 
 ## Token(计费已迁移 NewAPI)
 

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

@@ -1,9 +1,9 @@
 {
   "name": "claude-code-voc-intelligence",
-  "version": "0.3.31",
-  "generatedAt": "2026-07-23T02:40:15.266Z",
+  "version": "0.3.32",
+  "generatedAt": "2026-07-30T09:56:01.468Z",
   "packageZip": "dist/claude-code-voc-intelligence.zip",
-  "packageZipBytes": 666693,
+  "packageZipBytes": 687866,
   "plugin": "voc-intelligence",
   "skills": [
     "xiaohongshu-trend-intelligence",

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


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

@@ -1,10 +1,10 @@
 {
   "name": "@vocmarket/voc-skill",
-  "version": "0.3.31",
-  "generatedAt": "2026-07-23T02:43:00.912Z",
-  "tarball": "dist/npm/vocmarket-voc-skill-0.3.31.tgz",
-  "tarballBytes": 579738,
-  "unpackedBytes": 4052076,
+  "version": "0.3.32",
+  "generatedAt": "2026-07-30T10:00:09.679Z",
+  "tarball": "dist/npm/vocmarket-voc-skill-0.3.32.tgz",
+  "tarballBytes": 582658,
+  "unpackedBytes": 4062488,
   "entryCount": 140,
   "installCommands": [
     "npm install -g @vocmarket/voc-skill",

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

@@ -25,7 +25,7 @@
 
 | 技能包 | npm 包名 | 当前版本 | 状态 | 本地目录 | npm 链接 |
 |---|---|---:|---|---|---|
-| Claude Code VOC Intelligence | `@vocmarket/voc-skill` | `0.3.31` | 已发布 | `claude-code/claude-code-voc-intelligence/` | https://www.npmjs.com/package/@vocmarket/voc-skill |
+| 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 |
 | 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 |

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

@@ -3,6 +3,15 @@
 > 当前版本号见 [`../NPM技能包发布管理.md`](../NPM技能包发布管理.md) 的「发布状态总览」。本文按版本号倒序记录每次更新的简要内容。
 > 本地目录:`claude-code/claude-code-voc-intelligence/`。
 
+## 0.3.32(2026-07-30)
+
+- 补齐上游淘宝/天猫 3 个有效接口:商品详情 V3、商品详情 V6、店铺商品列表 V4;ecommerce 通道从 285 增至 288,全包接口从 1340 增至 1343。
+- 修正淘宝接口选择:商品详情/SKU 价格/关键属性优先 V6,商品页展示销量优先 V3 `sellCount`,券后价优先 V4,店铺列表优先 V4。
+- 明确 V9 字段口径:`num` 是 V9 销售量级信号,`totalCount` 是评价总量,均不是页面当前付款人数;V9 `attribute` 可能来自旧属性快照,不再用于覆盖 V6 关键属性。
+- 时效规则:`recordTime` 是上游版本快照的采集时间,不是 API 请求时间,也不代表所有嵌套属性都在该时刻刷新;上游未公布固定刷新 SLA。
+- 标记上游弃用的商品销量 V1 与店铺商品列表 V3,搜索时降序但保留兼容调用。
+- 验证:MCP smoke、package smoke、指定商品 V3/V4/V6/V9 真实调用、ZIP 解包安装及 npm pack 三种安装路径均已通过;已发布并验证 npm `latest=0.3.32`(140 文件,package 582.7 kB,unpacked 4,062,488 bytes,shasum `64583ab98eb7aae6fa372431162610415c3638be`),全新目录线上 `@latest workspace --smoke` 通过。
+
 ## 0.3.31(2026-07-23)
 
 - 已发布并验证 npm `latest=0.3.31`(140 文件,package 579.7 kB,shasum `cf9be1d8…`)。
@@ -84,4 +93,3 @@
 
 - 已发布并验证 `latest=0.3.0`。新增抖音趋势情报:`douyin-trend-intelligence` skill、`voc_douyin_trend_run`、`voc_douyin_token_check`、`voc_douyin_preference_update`;工作区安装同时生成小红书+抖音两个 skill 入口。
 - 统一 token/充值体验:无 token 返回开通链接,余额不足/业务 403 返回充值链接,不暴露底层报错;新增 `business-status recharge guard` smoke(防 HTTP 200 但业务码 403 误判成功)。
-

+ 35 - 10
scripts/deploy/claude-code-voc-intelligence-suite.js

@@ -163,6 +163,33 @@ function run(command, args, cwd) {
     const detail = child.error ? `: ${child.error.message}` : '';
     throw new Error(`${command} ${args.join(' ')} failed with exit ${child.status}${detail}`);
   }
+  return child;
+}
+
+function createZip(sourceDir, destinationPath) {
+  if (process.platform === 'win32') {
+    run('tar.exe', ['-a', '-c', '-f', destinationPath, '-C', sourceDir, '.'], PROJECT_ROOT);
+    return;
+  }
+  run('zip', ['-q', '-r', destinationPath, '.'], sourceDir);
+}
+
+function listZipEntries(zipPath) {
+  const result = process.platform === 'win32'
+    ? run('tar.exe', ['-t', '-f', zipPath], PROJECT_ROOT)
+    : run('unzip', ['-Z1', zipPath], PROJECT_ROOT);
+  return result.stdout
+    .split(/\r?\n/)
+    .map(entry => entry.replace(/^\.\//, '').replace(/\\/g, '/'))
+    .filter(Boolean);
+}
+
+function extractZip(zipPath, destinationDir) {
+  if (process.platform === 'win32') {
+    run('tar.exe', ['-x', '-f', zipPath, '-C', destinationDir], PROJECT_ROOT);
+    return;
+  }
+  run('unzip', ['-q', zipPath, '-d', destinationDir], PROJECT_ROOT);
 }
 
 function validateSuite() {
@@ -194,11 +221,13 @@ function buildSuite() {
   ensureDir(TEMP_ROOT);
   const copied = copyDirRecursive(SUITE_DIR, TEMP_ROOT);
   if (fs.existsSync(PACKAGE_ZIP)) fs.unlinkSync(PACKAGE_ZIP);
-  run('powershell', [
-    '-NoProfile',
-    '-Command',
-    `Compress-Archive -Path '${TEMP_ROOT}\\*' -DestinationPath '${PACKAGE_ZIP}' -Force`
-  ], PROJECT_ROOT);
+  createZip(TEMP_ROOT, PACKAGE_ZIP);
+  const entries = new Set(listZipEntries(PACKAGE_ZIP));
+  for (const required of ['install.js', '.claude-plugin/plugin.json']) {
+    if (!entries.has(required)) {
+      throw new Error(`Built zip is incomplete: missing ${required}`);
+    }
+  }
   writeDistManifest({ uploaded: false, copiedFiles: copied });
   const size = fs.statSync(PACKAGE_ZIP).size;
   console.log(`  ok: ${PACKAGE_ZIP} (${(size / 1024).toFixed(0)} KB, ${copied} files)`);
@@ -209,11 +238,7 @@ function testPackage() {
   if (!fs.existsSync(PACKAGE_ZIP)) throw new Error(`Package zip not found: ${PACKAGE_ZIP}`);
   if (fs.existsSync(TEST_ROOT)) fs.rmSync(TEST_ROOT, { recursive: true, force: true });
   ensureDir(TEST_ROOT);
-  run('powershell', [
-    '-NoProfile',
-    '-Command',
-    `Expand-Archive -Path '${PACKAGE_ZIP}' -DestinationPath '${TEST_ROOT}' -Force`
-  ], PROJECT_ROOT);
+  extractZip(PACKAGE_ZIP, TEST_ROOT);
   run(process.execPath, ['install.js', '--smoke'], TEST_ROOT);
   console.log('  ok: extracted package can run install.js and sample/preference/MCP smoke');
 }