audit-voc-skill-source.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. """Inspect the installed VOC package as text/JSON only; no package code executes."""
  2. import argparse
  3. import collections
  4. import datetime as dt
  5. import hashlib
  6. import json
  7. from pathlib import Path
  8. import re
  9. FILES = ['package.json', 'SKILL.md', 'mcp/src/server.js',
  10. 'mcp/src/core/api-catalog.js', 'mcp/src/core/budget-guard.js',
  11. 'mcp/src/tools/voc-api-catalog-run.js', 'mcp/src/providers/voc-gateway.js',
  12. 'mcp/src/providers/ecommerce-gateway.js', 'mcp/src/providers/overseas-gateway.js',
  13. 'mcp/src/features/voc-competitor-map/competitor-map.js',
  14. 'mcp/src/features/media/media-pipeline.js', 'mcp/catalog/voc-social-endpoints.json',
  15. 'mcp/catalog/params/jd.json', 'mcp/catalog/params/taobao.json']
  16. def inspect(root):
  17. root = Path(root).resolve()
  18. sources = {name: (root / name).read_bytes() for name in FILES}
  19. text = {name: value.decode('utf-8-sig') for name, value in sources.items()}
  20. package = json.loads(text['package.json'])
  21. if package.get('name') != '@vocmarket/voc-skill':
  22. raise ValueError('NOT_VOC_SKILL_PACKAGE')
  23. catalog = json.loads(text['mcp/catalog/voc-social-endpoints.json'])
  24. endpoints = catalog['endpoints']
  25. tools = re.findall(r"server\.registerTool\(\s*['\"]([^'\"]+)", text['mcp/src/server.js'])
  26. selected = []
  27. for endpoint in endpoints:
  28. if endpoint.get('platform') in ('jd', 'taobao'):
  29. store = json.loads(text['mcp/catalog/params/' + endpoint['platform'] + '.json'])
  30. selected.append({**{k: endpoint.get(k) for k in ('id','platform','title','method','proxyPath','channel',
  31. 'deprecated','responseHint','recommendedFor','freshness')},
  32. 'params': endpoint.get('params') or store.get(endpoint['id'], [])})
  33. root_skill_dirs = root / 'skills'
  34. subskills = sorted(p.parent.name for p in root_skill_dirs.glob('*/SKILL.md'))
  35. # A catalog match is discovery evidence only, never an online test result.
  36. rank_candidates = [{k:e.get(k) for k in ('id','platform','title','channel')} for e in endpoints
  37. if e.get('platform') in ('jd','taobao') and
  38. re.search(r'rank|ranking|榜单|排行|top.?100|top.?400',
  39. json.dumps(e, ensure_ascii=False), flags=re.I)]
  40. return {
  41. 'schema': 'ksf.voc-skill-source-audit.v1', 'generatedAt': dt.datetime.now(dt.timezone.utc).isoformat(),
  42. 'package': package['name'], 'version': package['version'], 'sourceRoot': str(root),
  43. 'inspectionMode': 'static-text-and-json', 'packageCodeExecuted': False,
  44. 'networkRequests': 0, 'collectionCalls': 0, 'credentialsRead': False,
  45. 'files': [{'path': name, 'bytes': len(data), 'sha256': hashlib.sha256(data).hexdigest()}
  46. for name, data in sources.items()],
  47. 'toolCount': len(tools), 'tools': tools, 'subskills': subskills,
  48. 'catalogEndpointCount': len(endpoints),
  49. 'platformCounts': dict(collections.Counter(e['platform'] for e in endpoints)),
  50. 'gatewayBaseUrls': {k:v.get('baseUrl') for k,v in catalog.get('gateway',{}).items()},
  51. 'domesticEcommerceEndpoints': selected, 'jdTaobaoRankingCandidates': rank_candidates,
  52. 'liveToolRegistrationVerified': False, 'accountPermissionVerified': False,
  53. 'budgetVerified': False, 'liveCollectionVerified': False,
  54. 'integration': {
  55. 'discovery': 'voc_api_search → voc_api_doc(只读本地目录/参数)',
  56. 'collection': 'voc_api_call;已登记京东/淘宝接口channel=ecommerce,自动路由',
  57. 'cost': 'voc_cost_estimate / voc_billing_query / voc_usage_report;目录billing非人民币价',
  58. 'storage': '返回结构化JSON、用量跟踪与本地媒体;不自动写入本项目研究库',
  59. 'intake': '采集结果保留来源 → 九类batch映射校验 → 暂存 → 受限SSH dry/apply/status → 新代际回读',
  60. 'limitation': '通用单接口有默认重试,不能据成本子技能文档声称每次调用都已硬限预算;试采须固定范围并设retries=0',
  61. 'rankings': '当前本地京东/淘宝目录未据此证明完整Top100/400能力;需另核真实榜单端点',
  62. 'analysis': '竞品图谱含通用候选及兜底示例;无真实证据不能当企业、店铺或商品事实',
  63. },
  64. }
  65. def markdown(report):
  66. lines = ['# 本地VOC Skill源码能力核对', '',
  67. f"来源:{report['sourceRoot']};{report['package']} {report['version']}。", '',
  68. f"本轮仅静态读取源码与目录:{report['toolCount']}个注册工具、{len(report['subskills'])}个子技能、{report['catalogEndpointCount']}条目录接口。未执行源码、未读凭据、未请求采集。", '',
  69. '## 可复用能力', '', *['- '+v for v in report['integration'].values()], '',
  70. '## 京东与淘宝/天猫的已登记操作', '',
  71. '|接口|通道|参数名(*为必填)|说明|', '|---|---|---|---|']
  72. for e in report['domesticEcommerceEndpoints']:
  73. params = '、'.join(p['name']+('*' if p.get('required') else '') for p in e['params'])
  74. note = ('已弃用;' if e.get('deprecated') else '') + (e.get('responseHint') or e['title'])
  75. lines.append('|'+e['id']+'|'+str(e['channel'])+'|'+params+'|'+note.replace('|','/').replace('\n',' ')+'|')
  76. lines += ['', '## 真实运行仍需核实', '',
  77. '- 目标Agent是否实际注册同版MCP工具、可用入口、账号权限与费用额度。源码存在不等于目标实例可调用。',
  78. '- 店铺ID/sellerId/userId是不同接口参数,不猜填、不互换;主店与平台稳定性待真实分操作实测。',
  79. '- 商品字段映射按真实响应核定;同平台不同详情版本的价格、销量和属性不能相互冒用。',
  80. '- 当前复用0.3.38,不自动升级、安装或触发付费接口。完整源文件指纹与参数见source-audit.json。', '']
  81. return '\n'.join(lines)
  82. def main():
  83. parser = argparse.ArgumentParser(description=__doc__)
  84. parser.add_argument('--source-root', required=True)
  85. parser.add_argument('--out', required=True)
  86. args = parser.parse_args()
  87. out = Path(args.out).resolve()
  88. if out.exists():
  89. parser.error('OUTPUT_DIRECTORY_ALREADY_EXISTS')
  90. root = Path(args.source_root).resolve()
  91. if out.is_relative_to(root):
  92. parser.error('OUTPUT_INSIDE_SOURCE_PACKAGE')
  93. report = inspect(root)
  94. out.mkdir(parents=True, mode=0o700)
  95. files = {'source-audit.json': json.dumps(report, ensure_ascii=False, indent=2)+'\n',
  96. 'VOC能力核对.md': markdown(report)}
  97. for name, value in files.items():
  98. with (out/name).open('x',encoding='utf-8',newline='\n') as stream:
  99. stream.write(value)
  100. (out/name).chmod(0o600)
  101. with (out/'FILES.sha256').open('x',encoding='utf-8',newline='\n') as stream:
  102. for name in sorted(files):
  103. stream.write(hashlib.sha256((out/name).read_bytes()).hexdigest()+' '+name+'\n')
  104. print(json.dumps({'ok': True,'version':report['version'],'tools':report['toolCount'],
  105. 'catalogEndpoints':report['catalogEndpointCount'],
  106. 'domesticEcommerceEndpoints':len(report['domesticEcommerceEndpoints']),
  107. 'report':str(out/'VOC能力核对.md'),'collectionCalls':0},ensure_ascii=False))
  108. if __name__ == '__main__':
  109. main()