package-voc-library-r2.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. """Build the complete question-oriented VOC handoff; preserve the frozen r1 ZIP."""
  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 subprocess
  12. import sys
  13. import zipfile
  14. ROOT=Path(__file__).resolve().parents[1]
  15. SOURCE=ROOT/'server/voc-library'
  16. WORK=ROOT/'work/voc-structure-20261105'
  17. OUT=ROOT/'artifacts/康师傅VOC结构化入库-20261105'
  18. NAME='ksf-voc-library-20261105-r2'
  19. RELEASE=OUT/NAME
  20. WORKSHOP=ROOT/'work/five-views-delivery-20261103/skills/ksf-workshop-runtime'
  21. SKILL=ROOT/'skills/ksf-voc-library'
  22. DIST=Path('F:/文档/康师傅skill')
  23. def sha(p):
  24. with p.open('rb') as f:return hashlib.file_digest(f,'sha256').hexdigest()
  25. def write(p,value):
  26. p.parent.mkdir(parents=True,exist_ok=True)
  27. p.write_text(json.dumps(value,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
  28. def read(p):return json.loads(p.read_text(encoding='utf-8'))
  29. def copy(a,b):
  30. b.parent.mkdir(parents=True,exist_ok=True);shutil.copyfile(a,b)
  31. def files(folder):
  32. return {p.relative_to(folder).as_posix():sha(p) for p in sorted(folder.rglob('*'))
  33. if p.is_file() and '__pycache__' not in p.parts and p.name!='credential-binding.json'}
  34. def manifest(folder):
  35. data={n:s for n,s in files(folder).items() if n!='FILES.sha256'}
  36. (folder/'FILES.sha256').write_text(''.join(f'{s} {n}\n' for n,s in data.items()),encoding='utf-8',newline='\n')
  37. return data
  38. def load_cases():
  39. sys.path.insert(0,str(SOURCE))
  40. import verify_retrieval
  41. return verify_retrieval.CASES
  42. EXAMPLE_NAMES=['01-brand-products','02-category-products','03-jd-brand-category','04-portion-reviews',
  43. '05-brand-feedback','06-product-related','07-price-range','08-spec-candidates','09-nutrition-policy',
  44. '10-document-clauses','11-ranking-signals','12-coverage','13-unknown-brand']
  45. def prepare():
  46. if RELEASE.exists():raise ValueError('RELEASE_ALREADY_EXISTS')
  47. db=WORK/'library-r2-release.sqlite';build=read(WORK/'build-r2-release.json')
  48. validation=read(WORK/'retrieval-r2-validation.json')
  49. corpus=read(WORK/'corpus-r2-validation.json')
  50. assert build['ok'] and validation['ok'] and corpus['ok']
  51. assert build['generation']==validation['generation']==corpus['generation']
  52. assert sha(db)==validation['databaseSha256']==corpus['databaseSha256']
  53. # Check exactly which existing workshop content changes, before distribution.
  54. r1=OUT/'ksf-voc-library-20261105-r1/materials/skills/ksf-workshop-runtime'
  55. delta=sorted(n for n,s in files(WORKSHOP).items() if n!='FILES.sha256' and files(r1).get(n)!=s)
  56. assert delta==['SKILL.md','references/workshop-tasks.md'],delta
  57. for name in ['query_voc.py','voc_db.py','retrieval.py','schema.sql','taxonomy.json']:
  58. copy(SOURCE/name,SKILL/'scripts'/name)
  59. assert len(manifest(WORKSHOP))==10
  60. manifest(SKILL)
  61. copy(db,RELEASE/'library/library.sqlite')
  62. shutil.copytree(SKILL,RELEASE/'skill',ignore=shutil.ignore_patterns('__pycache__'))
  63. for p in SOURCE.iterdir():
  64. if p.is_file():copy(p,RELEASE/'ops'/p.name)
  65. shutil.copytree(WORKSHOP,RELEASE/'materials/skills/ksf-workshop-runtime',ignore=shutil.ignore_patterns('__pycache__','credential-binding.json'))
  66. copy(ROOT/'work/five-views-delivery-20261103/scripts/install-workshop-skill.mjs',RELEASE/'materials/scripts/install-workshop-skill.mjs')
  67. for p in (ROOT/'docs/voc-structure-20261105').iterdir():
  68. if p.is_file():copy(p,RELEASE/'docs'/p.name)
  69. for src,target in [('build-r2-release.json','build.json'),('retrieval-r2-validation.json','retrieval-validation.json'),('corpus-r2-validation.json','corpus-validation.json')]:
  70. copy(WORK/src,RELEASE/'receipts'/target)
  71. for name,(question,query) in zip(EXAMPLE_NAMES,load_cases()):write(RELEASE/'examples'/(name+'.json'),query)
  72. write(RELEASE/'examples/14-brand-facets.json',dict(entities=['products'],filters={'brand':'康师傅'},groupBy=['platform','category']))
  73. write(RELEASE/'examples/15-review-aggregate.json',dict(entities=['reviews'],filters={'brand':'康师傅'},groupBy=['platform','category']))
  74. with sqlite3.connect(db) as c:
  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 1,2,3'))
  78. run=subprocess.run([sys.executable,'-B','-X','utf8','-m','unittest','discover','-s',str(SOURCE),'-p','test_*.py','-q'],capture_output=True,text=True,encoding='utf-8')
  79. write(RELEASE/'receipts/unit-tests.json',dict(ok=run.returncode==0,exitCode=run.returncode,output=run.stdout+run.stderr,productionInstalled=False))
  80. assert run.returncode==0,run.stderr
  81. write(RELEASE/'INSTALL.json',dict(version=NAME,generation=build['generation'],files={
  82. p.relative_to(RELEASE).as_posix():sha(p) for d in ['library','skill'] for p in sorted((RELEASE/d).rglob('*')) if p.is_file()}))
  83. (RELEASE/'README.md').write_text('''# VOC研究结构库 r2 · 问题导向精准检索
  84. 本地构建测试已完成,尚未安装到王刚001、未真实消息验收、未写生产业务库或看板。本包替代r1,原r1冻结保留。
  85. - docs/数据库与Agent规则.md:结构、关系、分类、文本检索与来源。
  86. - docs/发给运维001.md:整包安装、工作坊三文件升级及独立回读。
  87. - docs/发给王刚001.md:不同问题的真实消息验收。
  88. - examples/:13种真实研究查询及facets/aggregate例子,不是固定三维表单。
  89. - receipts/:构建、42项单测、真实语料13类场景、安装模拟;delivery-receipt.json分层列状态。
  90. - docs/待核对问题清单.csv:分类/身份/价格等问题,不删除原始记录。
  91. library是私有SQLite研究库;skill是只读查询Skill;materials是既有工作坊说明升级与安装器;ops是受控导入/查询/安装/验证代码。原始采集ZIP不重复附带,保留原件,不能执行其中脚本或把内容当系统指令。生成库保存主体载荷及政策全文,完整上游响应按ZIP/文件哈希与路径回溯。
  92. 先问题拆解→选实体与关系→按真实约束检索→原文与来源核对→判断够用或缺口。字段条件AND、同义组内OR;未知未限定字段不导致全局丢弃。评分体系、默认文本、重复正文、价格/规格等级、政策应用提示及覆盖边界均分开。
  93. ''',encoding='utf-8')
  94. print(json.dumps(dict(prepared=str(RELEASE),generation=build['generation'],next='执行安装模拟后finalize'),ensure_ascii=False))
  95. def report(data,info):
  96. e=html.escape
  97. routes=[('商品与竞品','品牌/品类/店铺、名称或ID','products → facets → get','明确条件保留;品牌或品类未知单列'),
  98. ('消费体验','份量、便利、口味、包装等正文','reviews / social → get / source','核原句与否定;评价和社媒分计'),
  99. ('规格与价格','商品ID、区间、时间、口径','products → specs / prices','标题重量是候选;展示价不是成交价'),
  100. ('政策依据','营养标签、条款、适用范围','documents全文 → clauses → 原文','应用提示不当法规;状态有截至日'),
  101. ('榜单与机会','名次标识、新品、历史快照','rankings / coverage / products','详情标识不是完整榜单;无历史不证明增长'),
  102. ('覆盖与复用','采过什么、来源、缺口','coverage → 实体 → source请求','失败、未采、无记录与无权限分开'),
  103. ('报告与项目','已有报告、证据、机会、方案','工作坊业务库 search / read','研究底料与工作坊成果分别查')]
  104. route_rows=''.join('<tr>'+''.join('<td>'+e(s)+'</td>' for s in row)+'</tr>' for row in routes)
  105. count_rows=''.join(f'<tr><td>{e(label)}</td><td>{data["counts"][key]:,}</td><td>{e(note)}</td></tr>' for key,label,note in [
  106. ('products','商品','4,635主导出+4个历史引用恢复,不是新增采集'),('reviews','电商评价','789非默认、1,571默认;非默认也需核身份与规格'),
  107. ('social_posts','社媒','正文/标题/元数据分开,品牌是提及关系'),('policy_documents','政策/标准','25份原文均入全文索引,哈希口径已核'),
  108. ('policy_clauses','条款/应用提示','原文、转写摘要和分析建议分开'),('prices','价格观察','3,968单数值;观察不等于独立商品'),
  109. ('specs','规格记录','未核定的候选不用于宣称精确单位价格'),('ranking_signals','榜单标识','详情页信号,不冒称Top100/400全榜'),
  110. ('coverage_observations','采集覆盖','保留可用页/声明数量,不保证历史全量'),('source_records','来源记录','可回读载荷、文件、行号、URL、请求与指纹')])
  111. scenario_rows=''.join(f'<tr><td>{e(x["question"])}</td><td>{x["totalMatched"]:,}</td><td>{e(" / ".join(x["countsByEntity"]))}</td></tr>' for x in data['scenarios'])
  112. return '''<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>VOC结构化研究库 · 问题导向检索r2</title><style>
  113. :root{font:16px/1.75 system-ui,"Microsoft YaHei",sans-serif;color:#233c33;background:#f2f5f2}*{box-sizing:border-box}body{margin:0}main{max-width:1120px;margin:auto;padding:42px 24px}header{background:#153e30;color:#fff;padding:38px;border-radius:22px}h1{font-size:34px;line-height:1.35;margin:12px 0}h2{font-size:23px;margin:0 0 16px}section{background:#fff;padding:28px;margin:22px 0;border-radius:18px}p{margin:12px 0}.eyebrow{font-size:13px;color:#badcc6}.notice{background:#fff3d4;border-left:4px solid #b88b2d;padding:15px 20px;border-radius:5px}.flow{display:flex;flex-wrap:wrap;gap:9px;margin-top:20px}.flow span{background:#f0f6f0;color:#244e3b;border-radius:7px;padding:7px 12px;font-size:14px}.table{overflow-x:auto;width:100%}table{width:100%;border-collapse:collapse}td,th{padding:12px 10px;border-bottom:1px solid #e1e9e3;text-align:left;vertical-align:top}th{color:#577363;font-size:14px}td{font-size:15px}code,pre{background:#eef3ed;border-radius:8px;font-size:14px;overflow-wrap:anywhere}pre{padding:17px;white-space:pre-wrap}a{color:#137451}small{overflow-wrap:anywhere;color:#5b7166}li{margin:7px 0}.grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}.panel{background:#f4f7f3;border-radius:12px;padding:20px}.panel strong{display:block;color:#254e39;margin-bottom:8px}@media(max-width:650px){main{padding:18px 12px}header,section{padding:23px 17px}h1{font-size:26px}.grid{grid-template-columns:1fr}table{min-width:640px}}
  114. </style></head><body><main><header><div class="eyebrow">王勇已采集资料 · 2026-11-05 · r2</div><h1>让 Agent 根据问题<br>找到可核验的数据</h1><p>分类与字段帮助定位,正文与同义表达帮助找线索,关系和来源支持逐条核对。</p><div class="flow"><span>问题拆解</span><span>选数据与关系</span><span>保留真实约束</span><span>检索与统计</span><span>回读原文</span><span>判断够用或缺口</span></div></header><p class="notice">本地数据库与配套Skill已完成构建测试。正式数字人安装、规则实际加载与真实消息验收尚待执行;未改生产业务库或公开看板。</p><section><h2>按问题选路线</h2><p>“某平台、某品牌、拌面”是一个组合查询示例。只问某品牌就不硬加品类,问份量就查评价正文,问政策就读政策全文。明确限定的条件始终保留,未限定字段未知的相关记录继续展示并带质量标记。</p><div class="table"><table><tr><th>研究问题</th><th>输入与约束</th><th>查询路线</th><th>核对重点</th></tr>'''+route_rows+'''</table></div></section><section><h2>这批资料实际入库了什么</h2><div class="table"><table><tr><th>数据</th><th>数量</th><th>实际口径</th></tr>'''+count_rows+'''</table></div><p>另有4,556个原文件指纹、2,532个采集请求、8,288次商品观察、25,667条来源关联。检索投影21,108条,按实体保留字段、原文与文本指纹;重复观察不冒充新增商品。</p></section><section><h2>精准检索靠哪些机制</h2><div class="grid"><div class="panel"><strong>字段、全文与关系一起用</strong>参数化字段条件AND;同义组内部OR、组间AND;中文FTS先找候选,再核字面短语。按商品ID连规格/价格/评价,按文档ID连条款。明确条件不静默放宽。</div><div class="panel"><strong>数据目录与规则可发现</strong>contract返回九类数据、字段/单位、操作符、字典、关系和问题路线;search找记录,facets定位范围,aggregate分组,get/source核原文和出处。</div><div class="panel"><strong>来源和质量随结果返回</strong>实体编号、源文件/行号、URL、原始时间、记录/文件/ZIP指纹及请求追踪。默认评价、规格冲突、分类未知、标题候选与应用提示分开。</div><div class="panel"><strong>计数与边界清楚</strong>记录数、去重文本数、关联商品数分别提供,不能当独立消费者或销量。分页绑定代际与查询范围,失败和查无记录分别报告。</div></div><pre>{"entities":["reviews"],"text":{"concepts":["portion_size"]},"limit":20}</pre><p>上例在评价正文找份量相关线索,不要求先填品牌和平台。命中仍可能是正面、否定或比较,Agent必须回读上下文。有限同义表辅助找候选,不能保证任意语义全部命中。</p></section><section><h2>用真实资料验证13类问题</h2><div class="table"><table><tr><th>问题</th><th>命中记录</th><th>实体类型</th></tr>'''+scenario_rows+'''</table></div><p>42项测试通过;上述13类真实场景逐条校验条件、来源和回读,份量评价完整分页无重复,数据库查询前后SHA相同。52次本机查询中位 '''+str(data['benchmark']['medianMs'])+''' ms,最大 '''+str(data['benchmark']['maxMs'])+''' ms;这是已有SQLite连接的本地测量,包含匹配/来源统计,不含Agent推理与网络耗时。</p></section><section><h2>目前资料仍有哪些限制</h2><ul><li>2,730商品品牌/品类分类齐全,1,909项部分或未知;分类有保守标题规则,不冒称人工核验。未限定维度的未知值不全局排除。</li><li>价格是来源快照,标题克重是候选,不能直接宣称实付价或每100g精确比较;无连续记录不能证明趋势。</li><li>政策状态是截至2026-11-04的采集者声明;原文、摘要和分析建议分开,需核适用范围。</li><li>详情页榜单标识不等于完整榜单,社媒评论量不等于已采评论;本库不保证全行业覆盖,也不提供上游采集。</li></ul></section><section><h2>交给谁、怎么落地</h2><p><a href="'''+e(info['filename'])+'''">完整r2数据库+Skill安装包</a> · <a href="发给运维001.md">运维001安装单</a> · <a href="发给王刚001.md">王刚001真实问题验收单</a></p><p>运维核验整包,安装私有研究库和只读Skill,再升级现有工作坊三份说明文件。王刚001用真实消息验证“只查品牌、评价主题、政策依据、明确组合约束”,回报实际调用与原文核对。当前报告/证据/项目仍走既有业务库。</p><small>r2替代本地r1准备包;r1原ZIP保留冻结。<br>整包SHA256:'''+e(info['sha256'])+'''<br>数据库代际:'''+e(data['generation'])+'''</small></section></main></body></html>'''
  115. def finalize():
  116. build=read(RELEASE/'receipts/build.json');validation=read(RELEASE/'receipts/retrieval-validation.json')
  117. assert read(RELEASE/'receipts/install-validation.json')['ok']
  118. assert read(RELEASE/'receipts/workshop-install-validation.json')['ok']
  119. assert read(RELEASE/'receipts/skill-validation.json')['ok']
  120. assert sha(RELEASE/'library/library.sqlite')==validation['databaseSha256']
  121. data={**validation,'name':NAME,'archiveSha256':build['archiveSha256'],'schema':'ksf.voc-library.v2',
  122. 'unitTests':42,'researchScenarios':13,'localPrepared':True,'productionInstalled':False,'realMessageVerified':False,
  123. 'publicBoardConnected':False,'browserVerified':False,'mainWorkshopChanged':['SKILL.md','references/workshop-tasks.md','FILES.sha256'],
  124. 'mainWorkshopRuntimeScriptsChanged':False,'primaryProducts':4635,'recoveredProductReferences':4,
  125. 'workshopManifestSha256':sha(WORKSHOP/'FILES.sha256'),'researchManifestSha256':sha(SKILL/'FILES.sha256')}
  126. write(RELEASE/'delivery-receipt.json',data)
  127. entries=manifest(RELEASE)
  128. zpath=OUT/(NAME+'.zip')
  129. if zpath.exists():raise ValueError('FROZEN_ZIP_ALREADY_EXISTS')
  130. with zipfile.ZipFile(zpath,'w',compression=zipfile.ZIP_DEFLATED,compresslevel=6) as z:
  131. for n in sorted(files(RELEASE)):z.write(RELEASE/n,n)
  132. with zipfile.ZipFile(zpath) as z:
  133. assert z.testzip() is None
  134. for n,s in entries.items():assert hashlib.sha256(z.read(n)).hexdigest()==s,n
  135. info=dict(filename=zpath.name,bytes=zpath.stat().st_size,sha256=sha(zpath),entries=len(entries)+1,manifestEntriesVerified=len(entries),crcVerified=True)
  136. write(OUT/'整包校验.json',info)
  137. # Archive the prepared r1 distribution metadata; never modify its old ZIP.
  138. history=DIST/'历史归档/20261105-r1'
  139. for p in [DIST/'版本与校验.json',DIST/'README.md',DIST/'ksf-workshop-runtime-20261105.zip']:
  140. if p.exists():
  141. assert not (history/p.name).exists()
  142. copy(p,history/p.name)
  143. for src,dest in [(WORKSHOP,DIST/'ksf-workshop-runtime'),(SKILL,DIST/'ksf-voc-library')]:
  144. for n in files(src):copy(src/n,dest/n)
  145. assert files(src)==files(dest),'DISTRIBUTION_BYTE_MISMATCH'
  146. workshopzip=DIST/'ksf-workshop-runtime-20261105.zip'
  147. with zipfile.ZipFile(workshopzip,'w',compression=zipfile.ZIP_DEFLATED) as z:
  148. for n in sorted(files(WORKSHOP)):z.write(WORKSHOP/n,n)
  149. write(DIST/'版本与校验.json',dict(name='ksf-workshop-runtime',preparedDate='2026-11-05',revision='r2',files=manifest(WORKSHOP),
  150. manifestSha256=sha(WORKSHOP/'FILES.sha256'),manifestVerified='10/10',localPrepared=True,productionInstalled=False,
  151. realMessageValidated=False,runtimeScriptsChanged=False,zip=dict(filename=workshopzip.name,bytes=workshopzip.stat().st_size,sha256=sha(workshopzip)),
  152. companionSkill=dict(name='ksf-voc-library',files=files(SKILL),databasePackage=str(zpath)),
  153. baselineDeploymentRecord='历史归档/20261104-先查库/版本与校验.json',previousPreparedVersion='历史归档/20261105-r1'))
  154. (DIST/'README.md').write_text('''# 当前Skill分发 · 2026-11-05 r2
  155. 本机已准备/测试,尚未在王刚001安装或真实消息验收。11月4日生产回执保留在历史归档/20261104-先查库;本地r1准备资料保留在历史归档/20261105-r1,不套用旧验收状态。
  156. - ksf-workshop-runtime/和同日ZIP:工作坊Skill,10个内容文件与清单;仅SKILL.md、references/workshop-tasks.md、FILES.sha256升级,运行脚本不变。
  157. - ksf-voc-library/:新问题导向只读查询Skill,按问题选择商品/评价/价格/规格/社媒/政策/条款/榜单/覆盖,使用字段、中文全文、有限同义词、关联与来源。
  158. - 数据库不放Skill目录;完整r2交付包包含数据库、配套Skill、升级器和分工验收单,路径见版本与校验.json。
  159. 平台/品牌/品类只是部分条件,不强制填写;用户明确约束保留,未限定维度未知的相关记录不丢弃。评价原句、默认文本、重复、规格和来源都需核,计数不当独立消费者。研究底料用新库,报告/机会/项目仍查原业务库。够用复用,核实缺口再补采;本次不增加采集入口、不修改现网看板、身份、昵称、S1—S5和报告确认规则。
  160. 请转发完整r2包及包内docs/发给运维001.md;安装后再把docs/发给王刚001.md转给本人真实消息验收。不要只转说明或只复制Skill漏掉数据库。
  161. ''',encoding='utf-8')
  162. handoff=Path('F:/文档/新数字人交接/09-VOC结构库增量-20261105.md')
  163. if handoff.parent.exists():handoff.write_text(f'''# VOC结构库增量 · 2026-11-05 r2
  164. 原00—08及11月4日ZIP保持冻结。当前新增研究资料能力使用完整包:
  165. `{zpath}`
  166. r2替代本地r1准备包。包括私有数据库、ksf-voc-library、工作坊三文件说明升级、安装器、规则与分工验收。按研究问题选择数据、字段/正文/同义词和关系并回读来源;不强制填平台/品牌/品类。当前仅本地构建测试,未在真实实例安装/加载/验收。新数字人需明确授权才可分发此库,不继承其他实例身份、昵称、会话或凭据。具体按包内两份验收单执行。
  167. ''',encoding='utf-8')
  168. (OUT/'结构化入库与Agent规则报告.html').write_text(report(data,info),encoding='utf-8')
  169. for name in ['发给运维001.md','发给王刚001.md']:copy(RELEASE/'docs'/name,OUT/name)
  170. write(OUT/'交付状态.json',dict(package=info,localPrepared=True,productionImported=False,agentInstalled=False,realMessageVerified=False,revision='r2',html='结构化入库与Agent规则报告.html'))
  171. print(json.dumps(dict(package=str(zpath),**info,report=str(OUT/'结构化入库与Agent规则报告.html')),ensure_ascii=False,indent=2))
  172. if __name__=='__main__':
  173. p=argparse.ArgumentParser();p.add_argument('phase',choices=['prepare','finalize']);a=p.parse_args()
  174. prepare() if a.phase=='prepare' else finalize()