'use strict'; const assert = require('assert/strict'); const fs = require('fs'); const path = require('path'); const { spawnSync } = require('child_process'); const ROOT = path.resolve(__dirname, '..'); function readJson(relativePath) { return JSON.parse(fs.readFileSync(path.join(ROOT, relativePath), 'utf8').replace(/^\uFEFF/, '')); } function runNpmPackDryRun() { const command = process.platform === 'win32' ? 'cmd.exe' : 'npm'; const args = process.platform === 'win32' ? ['/d', '/s', '/c', 'npm', 'pack', '--dry-run', '--json'] : ['pack', '--dry-run', '--json']; const result = spawnSync(command, args, { cwd: ROOT, encoding: 'utf8' }); if (result.status !== 0) throw new Error('npm pack --dry-run 执行失败'); return JSON.parse(result.stdout)[0]; } function main() { const pkg = readJson('package.json'); const lock = readJson('package-lock.json'); const manifest = readJson('skill-package-manifest.json'); const plugin = readJson('.claude-plugin/plugin.json'); const catalog = readJson('mcp/catalog/qiwei-endpoints.json'); const catalogSource = fs.readFileSync(path.join(ROOT, 'mcp', 'catalog', 'qiwei-endpoints.json'), 'utf8'); const serverSource = fs.readFileSync(path.join(ROOT, 'mcp', 'src', 'server.js'), 'utf8'); const dashboardSource = fs.readFileSync(path.join(ROOT, 'mcp', 'src', 'dashboard', 'app.js'), 'utf8'); const serverVersion = serverSource.match(/new McpServer\([\s\S]*?version:\s*['"]([^'"]+)/)?.[1]; const toolNames = [...serverSource.matchAll(/registerTool\(\s*['"]([^'"]+)['"]/g)].map(match => match[1]); const skillDirs = fs.readdirSync(path.join(ROOT, 'skills'), { withFileTypes: true }) .filter(entry => entry.isDirectory() && fs.existsSync(path.join(ROOT, 'skills', entry.name, 'SKILL.md'))) .map(entry => entry.name) .sort(); assert.equal(pkg.name, 'fmode-qiwei'); assert.equal(pkg.bin?.['fmode-qiwei'], 'install.js'); assert.equal(pkg.version, lock.version); assert.equal(pkg.version, lock.packages?.['']?.version); assert.equal(pkg.version, manifest.version); assert.equal(pkg.version, plugin.version); assert.equal(pkg.version, serverVersion); assert.equal(manifest.name, pkg.name); assert.equal(manifest.npmPackage, pkg.name); assert.equal(manifest.status, 'published'); assert.match(manifest.installCommand, /fmode-qiwei@latest/); assert.equal(pkg.engines?.node, '>=22.5.0'); assert.equal(new Set(toolNames).size, toolNames.length, 'MCP 工具名不能重复'); assert.equal(toolNames.length, manifest.mcpToolCount, 'MCP 工具数量与 manifest 不一致'); assert.deepEqual(skillDirs, [...manifest.skills].sort(), 'Skill 目录与 manifest 不一致'); assert.equal( (catalogSource.match(/q-[a-z-]+=/gi) || []).length, 0, '公开接口清单不能包含外部签名参数' ); const createClient = catalog.endpoints.find(endpoint => endpoint.id === 'client.createClient'); assert(createClient, '接口清单缺少 client.createClient'); const hiddenNetworkParam = ['a', 'i', 'd'].join(''); assert( !createClient.params.some(param => String(param.name).toLowerCase() === hiddenNetworkParam), '公开接口清单不能暴露内部网络组件参数' ); assert.equal( createClient.params.find(param => param.name === 'clientVersion')?.desc, '客户端版本由 Fmode 服务自动选择,通常无需填写。' ); assert.doesNotMatch( dashboardSource, /(?:window\s*\.\s*)?(?:alert|confirm|prompt)\s*\(/, 'Dashboard 不能调用浏览器原生 alert、confirm 或 prompt' ); const pack = runNpmPackDryRun(); const packedPaths = new Set(pack.files.map(item => item.path.replace(/\\/g, '/'))); for (const required of [ '.claude-plugin/plugin.json', '.env.example', 'install.js', 'mcp/src/core/runtime-context.js', 'mcp/src/core/startup-summary.js', 'mcp/src/core/relay-daemon.js', 'mcp/src/core/agent-memory.js', 'mcp/src/core/agent-context-builder.js', 'mcp/src/core/agent-memory-worker.js', 'mcp/src/core/voice-clone-service.js', 'mcp/src/server.js', 'mcp/src/tools/qiwei-agent-control-run.js', 'qiwei.runtime.config.example.mjs', 'runtime/callback-service/package.json', 'runtime/callback-service/src/index.mjs', 'runtime/callback-service/src/personal-polling.mjs', 'runtime/callback-service/src/enterprise-relay-client.mjs', 'scripts/start-callback-runtime.mjs', 'scripts/callback-runtime-smoke-test.mjs', 'scripts/callback-relay-smoke-test.js', 'scripts/voice-clone-smoke-test.js', 'scripts/agent-console-smoke-test.js', 'scripts/preview-dashboard.js', 'scripts/startup-preview-smoke-test.js', 'skill-package-manifest.json', 'THIRD_PARTY_NOTICES.md', ]) { assert(packedPaths.has(required), `npm 包缺少 ${required}`); } const forbidden = [...packedPaths].filter(item => /(^|\/)(?:node_modules|outputs?|\.playwright-cli|coverage|dist)(\/|$)/i.test(item) || /(^|\/)\.env\.local$/i.test(item) || /\.(?:db|sqlite|sqlite3|log|tgz|zip)$/i.test(item) ); assert.deepEqual(forbidden, [], `npm 包包含本地运行文件:${forbidden.join(', ')}`); process.stdout.write(`${JSON.stringify({ status: 'ok', version: pkg.version, skillCount: skillDirs.length, mcpToolCount: toolNames.length, packageEntries: pack.entryCount, packageSize: pack.size, unpackedSize: pack.unpackedSize, forbiddenFiles: forbidden.length, }, null, 2)}\n`); } try { main(); } catch (error) { process.stderr.write(`${error.message}\n`); process.exit(1); }