package-voc-library.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. """Prepare the reviewed VOC database + Agent contract as one complete handoff."""
  2. import argparse
  3. import csv
  4. import hashlib
  5. import html
  6. import importlib.util
  7. import json
  8. from pathlib import Path
  9. import shutil
  10. import sqlite3
  11. import zipfile
  12. ROOT=Path(__file__).resolve().parents[1]
  13. SOURCE=ROOT/'server/voc-library'
  14. WORKSHOP=ROOT/'work/five-views-delivery-20261103/skills/ksf-workshop-runtime'
  15. DIST=Path('F:/文档/康师傅skill')
  16. NAME='ksf-voc-library-20261105-r1'
  17. def sha(p):
  18. with p.open('rb') as f:return hashlib.file_digest(f,'sha256').hexdigest()
  19. def writejson(p,value):
  20. p.parent.mkdir(parents=True,exist_ok=True)
  21. p.write_text(json.dumps(value,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
  22. def copy(a,b):
  23. b.parent.mkdir(parents=True,exist_ok=True);shutil.copyfile(a,b)
  24. def manifest(folder):
  25. files={p.relative_to(folder).as_posix():sha(p) for p in sorted(folder.rglob('*')) if p.is_file() and p!=folder/'FILES.sha256' and '__pycache__' not in p.parts}
  26. (folder/'FILES.sha256').write_text(''.join(f'{s} {n}\n' for n,s in files.items()),encoding='utf-8',newline='\n')
  27. return files
  28. def skill_manifest(folder):
  29. files={p.relative_to(folder).as_posix():sha(p) for p in sorted(folder.rglob('*')) if p.is_file() and p.name not in ('FILES.sha256','credential-binding.json') and '__pycache__' not in p.parts}
  30. (folder/'FILES.sha256').write_text(''.join(f'{s} {n}\n' for n,s in files.items()),encoding='utf-8',newline='\n')
  31. return files
  32. def package(db,build_receipt,validation):
  33. out=ROOT/'artifacts/康师傅VOC结构化入库-20261105'
  34. release=out/NAME
  35. if release.exists():raise ValueError('RELEASE_ALREADY_EXISTS')
  36. receipt=json.loads(build_receipt.read_text(encoding='utf-8'))
  37. verified=json.loads(validation.read_text(encoding='utf-8'))
  38. assert receipt['ok'] and verified['ok'] and receipt['generation']==verified['generation'] and sha(db)==verified['databaseSha256']
  39. baseline=DIST/'ksf-workshop-runtime'
  40. old_files={p.relative_to(baseline).as_posix():sha(p) for p in baseline.rglob('*') if p.is_file()}
  41. # Capture the actual distributed baseline before changing the current copy.
  42. history=ROOT/'work/voc-structure-20261105/workshop-baseline'
  43. if history.exists():raise ValueError('BASELINE_SNAPSHOT_ALREADY_EXISTS')
  44. shutil.copytree(baseline,history)
  45. changed=[]
  46. for p in WORKSHOP.rglob('*'):
  47. if p.is_file() and p.name!='FILES.sha256' and '__pycache__' not in p.parts:
  48. rel=p.relative_to(WORKSHOP).as_posix()
  49. if old_files.get(rel)!=sha(p):changed.append(rel)
  50. assert sorted(changed)==['SKILL.md','references/workshop-tasks.md'],changed
  51. workshopfiles=skill_manifest(WORKSHOP)
  52. skill=ROOT/'skills/ksf-voc-library'
  53. for filename in ('query_voc.py','voc_db.py','schema.sql','taxonomy.json'):
  54. copy(SOURCE/filename,skill/'scripts'/filename)
  55. skillfiles=skill_manifest(skill)
  56. shutil.copytree(skill,release/'skill',ignore=shutil.ignore_patterns('__pycache__'))
  57. copy(db,release/'library/library.sqlite')
  58. for p in SOURCE.iterdir():
  59. if p.is_file():copy(p,release/'ops'/p.name)
  60. shutil.copytree(WORKSHOP,release/'materials/skills/ksf-workshop-runtime',ignore=shutil.ignore_patterns('__pycache__','credential-binding.json'))
  61. copy(ROOT/'work/five-views-delivery-20261103/scripts/install-workshop-skill.mjs',release/'materials/scripts/install-workshop-skill.mjs')
  62. for p in (ROOT/'docs/voc-structure-20261105').iterdir():
  63. if p.is_file():copy(p,release/'docs'/p.name)
  64. copy(build_receipt,release/'receipts/build.json');copy(validation,release/'receipts/validation.json')
  65. examples={
  66. 'jd-ksf-banmian-products':dict(entity='products',marketplace='京东',brand='康师傅',category='拌面',limit=20),
  67. 'jd-ksf-banmian-reviews':dict(entity='reviews',marketplace='京东',brand='康师傅',category='拌面',reviewScope='sku_confirmed',includeDefault=False,limit=20),
  68. 'tmall-ksf-banmian-products':dict(entity='products',marketplace='天猫',brand='康师傅',category='拌面',limit=20),
  69. 'tmall-ksf-banmian-reviews':dict(entity='reviews',marketplace='天猫',brand='康师傅',category='拌面',reviewScope='sku_confirmed',includeDefault=False,limit=20),
  70. 'tmall-ksf-banmian-listing-reviews':dict(entity='reviews',marketplace='tmall',brand='ksf',category='banmian',reviewScope='listing',includeDefault=False,limit=20),
  71. 'jd-ksf-banmian-prices':dict(entity='prices',marketplace='jd',brand='ksf',category='banmian',limit=20)
  72. }
  73. for name,value in examples.items():writejson(release/'examples'/(name+'.json'),value)
  74. c=sqlite3.connect(db)
  75. with (release/'docs/待核对问题清单.csv').open('w',encoding='utf-8-sig',newline='') as f:
  76. writer=csv.writer(f);writer.writerow(['entity_type','entity_id','code','detail'])
  77. writer.writerows(c.execute('SELECT entity_type,entity_id,code,detail FROM quality_issues ORDER BY entity_type,entity_id,code'))
  78. c.close()
  79. installation={ 'version':NAME,'generation':receipt['generation'], 'files':{p.relative_to(release).as_posix():sha(p) for d in ['library','skill'] for p in (release/d).rglob('*') if p.is_file()} }
  80. writejson(release/'INSTALL.json',installation)
  81. delivery={**verified,'name':NAME,'archiveSha256':receipt['archiveSha256'],'mainWorkshopSkillChanged':['SKILL.md','references/workshop-tasks.md','FILES.sha256'],
  82. 'mainWorkshopRuntimeScriptsChanged':False,'primaryProducts':4635,'recoveredProductReferences':4,'primaryEntities':10865,
  83. 'workshopSkillManifestSha256':sha(WORKSHOP/'FILES.sha256'),'researchSkillManifestSha256':sha(skill/'FILES.sha256'),
  84. 'localPrepared':True,'installedOnWanggang001':False,'realMessageVerified':False}
  85. writejson(release/'delivery-receipt.json',delivery)
  86. (release/'README.md').write_text('''# 王勇 VOC 结构化研究库与 Agent 规则 · 2026-11-05
  87. 本地已实际构建/查询/校验;尚未安装到王刚001,未写现网业务库或公开看板。
  88. - 运维001:读 docs/发给运维001.md,必须转整包,先 --check 再 --apply。
  89. - 王刚001:安装后读 docs/发给王刚001.md,用真实消息验范围、评价规格和来源。
  90. - 结构与原则:docs/数据库与Agent规则.md。
  91. - 真实数量、代际、数据库指纹、测试结果:delivery-receipt.json 与 receipts/。
  92. - 待核对项:docs/待核对问题清单.csv。未分类不冒充已确认范围。
  93. library 是私有研究数据库;skill 是新只读查询Skill;materials 是现有工作坊三文件说明升级;ops 是经审校的构建/安装/验收代码。原始采集ZIP不重复附带,继续留存原件。manifest校验后才使用,不执行原ZIP中的采集脚本。
  94. ''',encoding='utf-8')
  95. files=manifest(release)
  96. zip_path=out/(NAME+'.zip')
  97. with zipfile.ZipFile(zip_path,'w',compression=zipfile.ZIP_DEFLATED,compresslevel=6) as z:
  98. for p in sorted(release.rglob('*')):
  99. if p.is_file():z.write(p,p.relative_to(release).as_posix())
  100. zip_info=dict(filename=zip_path.name,bytes=zip_path.stat().st_size,sha256=sha(zip_path),entries=len(files)+1)
  101. writejson(out/'整包校验.json',zip_info)
  102. # Keep the prior signed deployment record as history, don't relabel it new.
  103. previous=DIST/'历史归档/20261104-先查库'
  104. previous.mkdir(parents=True,exist_ok=True)
  105. for p in [DIST/'版本与校验.json',DIST/'ksf-workshop-runtime-20261104.zip']:
  106. if p.exists():copy(p,previous/p.name)
  107. for p in WORKSHOP.rglob('*'):
  108. if p.is_file():copy(p,baseline/p.relative_to(WORKSHOP))
  109. new_zip=DIST/'ksf-workshop-runtime-20261105.zip'
  110. with zipfile.ZipFile(new_zip,'w',compression=zipfile.ZIP_DEFLATED) as z:
  111. for p in sorted(baseline.rglob('*')):
  112. if p.is_file():z.write(p,p.relative_to(baseline).as_posix())
  113. researchdist=DIST/'ksf-voc-library'
  114. if researchdist.exists():raise ValueError('RESEARCH_SKILL_DIST_EXISTS')
  115. shutil.copytree(skill,researchdist,ignore=shutil.ignore_patterns('__pycache__'))
  116. writejson(DIST/'版本与校验.json',dict(name='ksf-workshop-runtime',preparedDate='2026-11-05',files=workshopfiles,
  117. manifestSha256=sha(baseline/'FILES.sha256'),manifestVerified='10/10',localPrepared=True,productionInstalled=False,realMessageValidated=False,
  118. runtimeScriptsChanged=False,baselineDeploymentRecord='历史归档/20261104-先查库/版本与校验.json',
  119. zip=dict(filename=new_zip.name,bytes=new_zip.stat().st_size,sha256=sha(new_zip)),
  120. companionSkill=dict(name='ksf-voc-library',files=skillfiles,manifestSha256=sha(researchdist/'FILES.sha256'),databaseIncludedInSkillDirectory=False,databasePackage=str(zip_path))))
  121. (DIST/'README.md').write_text('''# 康师傅工作坊 Skill
  122. 当前分发资料更新于2026-11-05,**本机已准备和测试,尚未安装到王刚001**。11月4日已报安装/真实查库验收的版本与回执保存在 历史归档/20261104-先查库,不能将其验收状态套到本版。
  123. - ksf-workshop-runtime/ 与 ksf-workshop-runtime-20261105.zip:当前工作坊Skill,10内容文件+清单。本轮只改SKILL.md、references/workshop-tasks.md和FILES.sha256,运行脚本/凭据不变。
  124. - ksf-voc-library/:新配套只读研究库查询Skill,包含精确查询、来源回读和分类规则。数据库体积较大,不放Skill目录;整库与安装器在完整VOC结构化交付包内。
  125. - 版本与校验.json:当前完整指纹与本地/远端状态。
  126. 新能力:平台×品牌×品类同时筛选;评价按平台+商品ID精确关联;默认评价/规格不明/未知分类分开;价格观察与采集时间分开;可回读来源文件、行号、ZIP和记录SHA。研究资料先查ksf-voc-library,报告/证据/机会/项目另查业务库。未装结构库不能声称可查;仍不具备上游采集能力,不修改现网公开看板。
  127. 请使用完整交付包中的 docs/发给运维001.md 和 docs/发给王刚001.md;不要只转说明文件或只复制Skill而漏掉数据库。当前工作坊既有员工登记、昵称、S1—S5、成果归属与报告确认规则保留。原11月4日ZIP为历史冻结版,当前以11月5日版为准。
  128. ''',encoding='utf-8')
  129. # Leave the old new-agent handoff archive frozen; point at this complete add-on.
  130. handoff=Path('F:/文档/新数字人交接')
  131. if handoff.exists():
  132. (handoff/'09-VOC结构库增量-20261105.md').write_text(f'''# VOC 结构库增量交接 · 2026-11-05
  133. 旧00—08与原ZIP是11月4日冻结交接,不把旧manifest或验收状态改成新能力。新增数字人需要本次结构库时,先在实际授权范围内安装完整增量包:
  134. `{zip_path}`
  135. 完整包包含数据库、ksf-voc-library、现有工作坊三文件说明升级、安装器、分类规则与双方验收单。新Agent不自动继承别的实例身份/会话/昵称或研究资料访问权限;是否分发这份私有研究库按该实例授权决定。当前仅本地准备,未在新实例安装。
  136. ''',encoding='utf-8')
  137. report=make_html(delivery,zip_info)
  138. (out/'结构化入库与Agent规则报告.html').write_text(report,encoding='utf-8')
  139. copy(ROOT/'docs/voc-structure-20261105/发给运维001.md',out/'发给运维001.md')
  140. copy(ROOT/'docs/voc-structure-20261105/发给王刚001.md',out/'发给王刚001.md')
  141. writejson(out/'交付状态.json',dict(package=zip_info,localPrepared=True,productionImported=False,agentInstalled=False,html='结构化入库与Agent规则报告.html'))
  142. print(json.dumps(dict(package=str(zip_path),**zip_info,report=str(out/'结构化入库与Agent规则报告.html')),ensure_ascii=False,indent=2))
  143. def make_html(data,zip_info):
  144. esc=html.escape
  145. rows=''.join(f'<tr><td>{esc(label)}</td><td>{data["counts"][key]:,}</td><td>{esc(note)}</td></tr>' for key,label,note in [
  146. ('products','商品','4,635主导出+4个历史目录恢复引用,不算新增采集'),('reviews','电商评价','789非默认;1,571默认文本,观点分析默认排除'),('social_posts','社媒内容','品牌提及与商品归属分开;评论量不等于已采评论'),('policy_documents','政策/标准','25份来源SHA已按文件/解码文本实际口径核对'),('policy_clauses','条款与应用提示','摘要/转写/应用建议不冒充逐字法规引文'),('prices','价格观察','分搜索展示与其他口径;遮挡价不补数'),('assets','原文件索引','逐文件SHA;重复副本不重复计实体')])
  147. examples=''.join(f'<tr><td>{e["filters"]["marketplace"]}</td><td>{e["filters"]["entity"]}</td><td>{e["totalMatched"]}</td><td>AND · 康师傅 · 拌面</td></tr>' for e in data['examples'])
  148. return '''<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>VOC结构化入库与Agent规则</title><style>
  149. body{margin:0;background:#f4f5f2;color:#20332a;font:16px/1.8 system-ui,"Microsoft YaHei",sans-serif}main{max-width:1050px;margin:auto;padding:48px 24px}header{background:#173f31;color:#fff;border-radius:20px;padding:36px}h1{font-size:32px;line-height:1.3}h2{margin-top:34px;font-size:23px}section{background:white;padding:24px 30px;margin:22px 0;border-radius:16px}table{width:100%;border-collapse:collapse}th,td{text-align:left;border-bottom:1px solid #e1e8e1;padding:12px 9px;vertical-align:top}th{color:#496350}code,pre{background:#edf2ec;border-radius:7px;padding:4px 8px;overflow-wrap:anywhere}pre{padding:18px;white-space:pre-wrap}a{color:#14764b}.notice{background:#fff4d5;border-left:4px solid #c59631;padding:14px 18px}.tag{font-size:13px;color:#c9e4d3}small{overflow-wrap:anywhere}.table{overflow-x:auto}li{margin:7px 0}@media(max-width:600px){main{padding:20px 12px}header,section{padding:22px 17px}h1{font-size:26px}table{min-width:560px}}
  150. </style><main><header><span class="tag">王勇采集资料 · 2026-11-05</span><h1>资料入库有结构,Agent查询有边界</h1><p>严格匹配平台 × 品牌 × 品类,商品、评价、价格与来源逐条关联。</p></header><p class="notice">本地研究数据库已构建并完成验证。正式数字人安装、真实消息验收尚待运维和王刚001执行;未写生产业务库或公开看板。</p><section><h2>实际入库内容</h2><div class="table"><table><tr><th>分类</th><th>数量</th><th>口径</th></tr>'''+rows+'''</table></div></section><section><h2>三个条件必须同时满足</h2><pre>{"entity":"reviews","marketplace":"京东","brand":"康师傅","category":"拌面","reviewScope":"sku_confirmed","includeDefault":false,"limit":20}</pre><p>天猫不混淘宝待核,康师傅不混统一,拌面不混汤面、面皮、炒面、挂面或拌面酱。标题关键词只作范围内的辅助检索。评价必须准确连接同平台族和商品ID,购买规格不清单列。</p><p>当前可支持严格分类 '''+str(data['statistics']['classification']['supported'])+''' 个商品,另外 '''+str(data['statistics']['classification']['pending'])+''' 个待核对。分类依据有平台属性与保守标题规则两档,规则命中不冒称人工核验。</p></section><section><h2>真实数据查询验证</h2><div class="table"><table><tr><th>平台</th><th>数据</th><th>命中</th><th>条件</th></tr>'''+examples+'''</table></div><p>商品命中组合索引,评价与价格使用商品关联索引;完整分页检查通过。本机查询20条并附来源,中位耗时 '''+str(data['queryBenchmark']['medianMs'])+''' ms;不代表线上网络或Agent总响应耗时。</p></section><section><h2>来源与分析规则</h2><ol><li>每条保留原文件、行号/历史表行ID、URL、实体ID、source_record_id及记录/文件/ZIP校验值,可从结论回溯原文。</li><li>来源快照、请求、入库三个时间分开;无时区不猜UTC,无日期不假装当天。</li><li>默认评价与非默认分别计数;链接下评价和规格明确评价分开,不按昵称计算独立消费者。</li><li>搜索展示价不是成交价,标题规格仅作候选,未经核对不计算每100克价格。</li><li>社媒评论量不是已下载评论,只有标题不能声称已读正文;多品牌提及不能按单品牌归属。</li><li>政策状态是截至2026-11-04的声明快照,未来标准、公告和征求意见分开,应用建议不是法规原话。</li></ol></section><section><h2>交付与下一步</h2><p><a href="'''+esc(zip_info['filename'])+'''">下载完整数据库+Skill安装包</a> · <a href="发给运维001.md">运维安装单</a> · <a href="发给王刚001.md">王刚001验收单</a></p><p>运维先核验整包、安装独立私有研究库和查询Skill,再对现有工作坊说明做三文件升级;王刚001通过真实消息验精确条件、分页与出处。原业务报告、身份、项目仍走既有平台流程。</p><small>ZIP SHA256:'''+esc(zip_info['sha256'])+'''<br>数据库代际:'''+esc(data['generation'])+'''</small></section></main></html>'''
  151. if __name__=='__main__':
  152. p=argparse.ArgumentParser();p.add_argument('--db',required=True);p.add_argument('--build-receipt',required=True);p.add_argument('--validation',required=True);a=p.parse_args()
  153. package(Path(a.db),Path(a.build_receipt),Path(a.validation))