package-voc-database.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. """Prepare a database-only supplement for the already installed Nov 6 query skills."""
  2. import argparse
  3. import hashlib
  4. import importlib.util
  5. import json
  6. from pathlib import Path
  7. import shutil
  8. import sys
  9. import tempfile
  10. import zipfile
  11. ROOT = Path(__file__).resolve().parents[1]
  12. WORK = ROOT / 'work/voc-database-delivery-20261106'
  13. OUT = ROOT / 'artifacts/研究数据库补装-20261106'
  14. NAME = 'ksf-voc-database-20261106-r1'
  15. RELEASE = OUT / NAME
  16. DIST = Path('F:/文档/康师傅skill')
  17. SKILL = ROOT / 'skills/ksf-voc-library'
  18. EXAMPLES = ROOT / 'artifacts/数据库精准检索规则-20261106/ksf-agent-query-rules-20261106-r1/examples'
  19. def digest(path):
  20. with Path(path).open('rb') as stream:
  21. return hashlib.file_digest(stream, 'sha256').hexdigest()
  22. def write(path, data):
  23. path.parent.mkdir(parents=True, exist_ok=True)
  24. path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
  25. def manifest(folder):
  26. entries = {p.relative_to(folder).as_posix(): digest(p) for p in sorted(folder.rglob('*')) if p.is_file() and p.name != 'FILES.sha256'}
  27. (folder / 'FILES.sha256').write_text(''.join(sha + ' ' + name + '\n' for name, sha in entries.items()), encoding='utf-8', newline='\n')
  28. def load_module(name, path):
  29. spec = importlib.util.spec_from_file_location(name, path)
  30. module = importlib.util.module_from_spec(spec)
  31. spec.loader.exec_module(module)
  32. return module
  33. def prepare():
  34. if RELEASE.exists():
  35. raise ValueError('FROZEN_RELEASE_ALREADY_EXISTS')
  36. sys.path.insert(0, str(ROOT / 'server/voc-library'))
  37. import voc_db
  38. registry = ROOT / 'work/library-preview/registry'
  39. pointer = json.loads((registry / 'current.json').read_text(encoding='utf-8'))
  40. source = voc_db.registry_database(registry)
  41. if digest(source) != pointer['databaseSha256']:
  42. raise ValueError('LOCAL_BOARD_POINTER_HASH_MISMATCH')
  43. with voc_db.readonly(source) as con:
  44. meta = dict(con.execute('SELECT key,value FROM meta'))
  45. entity_counts = voc_db.table_counts(con)
  46. counts = {**entity_counts, 'retrieval_documents': con.execute('SELECT count(*) FROM retrieval_documents').fetchone()[0]}
  47. database = dict(schema=meta['schema'], generation=meta['generation'], bytes=source.stat().st_size, sha256=digest(source),
  48. originalArchiveSha256=meta['archive_sha256'], entityCounts=entity_counts, counts=counts)
  49. destination = RELEASE / 'library/library.sqlite'
  50. destination.parent.mkdir(parents=True)
  51. shutil.copyfile(source, destination)
  52. if digest(destination) != pointer['databaseSha256']:
  53. raise ValueError('COPIED_DATABASE_HASH_MISMATCH')
  54. required_skill = digest(SKILL / 'FILES.sha256')
  55. if required_skill != '4da410bb882dc73e277c740d13407918d9c9d5da2196ac9211f6ff7c009692fb':
  56. raise ValueError('INSTALLED_QUERY_RELEASE_NO_LONGER_MATCHES')
  57. write(RELEASE / 'DATABASE.json', dict(release=NAME, database=database,
  58. requiredResearchSkillManifestSha256=required_skill, source='Codex local board current database snapshot',
  59. matchesLocalBoardDatabaseBytes=True, storageMode='static-snapshot', automaticUpdatesConfigured=False,
  60. productionInstalled=False, productionBoardAligned=False, realMessageVerified=False,
  61. skillIncluded=False, skillInstallationRequired=False, defaultDatabasePath='/opt/data/ksf-voc-library/library.sqlite'))
  62. for name in ('install-voc-database.py', 'verify-installed-voc-database.py'):
  63. shutil.copyfile(ROOT / 'scripts' / name, RELEASE / name)
  64. shutil.copytree(EXAMPLES, RELEASE / 'examples')
  65. shutil.copytree(ROOT / 'docs/voc-database-delivery-20261106', RELEASE / 'docs')
  66. manifest(RELEASE)
  67. print(json.dumps(dict(prepared=str(RELEASE), database=database), ensure_ascii=False, indent=2))
  68. def validate():
  69. WORK.mkdir(parents=True, exist_ok=True)
  70. installer = load_module('seed_install', RELEASE / 'install-voc-database.py')
  71. verifier = load_module('seed_verify', RELEASE / 'verify-installed-voc-database.py')
  72. with tempfile.TemporaryDirectory(prefix='simulate-', dir=WORK) as temporary:
  73. root = Path(temporary)
  74. target = root / 'ksf-voc-library'
  75. plan = installer.install(RELEASE, target)
  76. if target.exists() or not plan['readOnly']:
  77. raise ValueError('DRY_PLAN_WROTE_TARGET')
  78. applied = installer.install(RELEASE, target, True)
  79. landed = target / 'library.sqlite'
  80. before = (digest(landed), landed.stat().st_mtime_ns)
  81. repeated = installer.install(RELEASE, target, True)
  82. if repeated['applied'] or (digest(landed), landed.stat().st_mtime_ns) != before:
  83. raise ValueError('REPEAT_CHANGED_DATABASE')
  84. verification = verifier.verify(RELEASE, SKILL, landed)
  85. if (digest(landed), landed.stat().st_mtime_ns) != before:
  86. raise ValueError('READBACK_CHANGED_DATABASE')
  87. result = dict(ok=True, package=NAME, installPlan=plan, installSimulation=applied,
  88. exactRepeatVerified=True, installedReadback=verification,
  89. skillFilesChanged=False, productionInstalled=False, realMessageVerified=False)
  90. write(WORK / 'local-validation.json', result)
  91. print(json.dumps(dict(ok=True, generation=verification['generation'], scenarios=len(verification['scenarios']),
  92. sourceChecks=len(verification['citations']), installedDefaultPathSimulated=True, exactRepeatVerified=True), ensure_ascii=False, indent=2))
  93. def finalize():
  94. validation = json.loads((WORK / 'local-validation.json').read_text(encoding='utf-8'))
  95. if not validation['ok']:
  96. raise ValueError('VALIDATION_REQUIRED')
  97. package_meta = json.loads((RELEASE / 'DATABASE.json').read_text(encoding='utf-8'))
  98. if digest(RELEASE / 'library/library.sqlite') != validation['installedReadback']['databaseSha256'] or package_meta['database']['generation'] != validation['installedReadback']['generation']:
  99. raise ValueError('DATABASE_CHANGED_AFTER_VALIDATION')
  100. archive = OUT / (NAME + '.zip')
  101. if archive.exists():
  102. raise ValueError('ZIP_ALREADY_FROZEN')
  103. shutil.copyfile(WORK / 'local-validation.json', RELEASE / 'local-validation.json')
  104. manifest(RELEASE)
  105. with zipfile.ZipFile(archive, 'x', zipfile.ZIP_DEFLATED) as z:
  106. for path in sorted(RELEASE.rglob('*')):
  107. if path.is_file():
  108. z.write(path, path.relative_to(RELEASE).as_posix())
  109. with zipfile.ZipFile(archive) as z:
  110. entries = z.read('FILES.sha256').decode('utf-8').splitlines()
  111. for line in entries:
  112. expected, name = line.split(' ', 1)
  113. if hashlib.sha256(z.read(name)).hexdigest() != expected:
  114. raise ValueError('ZIP_READBACK_MISMATCH')
  115. receipt = dict(release=NAME, filename=archive.name, bytes=archive.stat().st_size, sha256=digest(archive),
  116. entries=len(entries)+1, rootManifestVerified=str(len(entries)) + '/' + str(len(entries)),
  117. database=json.loads((RELEASE / 'DATABASE.json').read_text(encoding='utf-8'))['database'],
  118. localValidated=True, databaseIncluded=True, skillIncluded=False, productionInstalled=False,
  119. realMessageVerified=False, productionBoardAligned=False)
  120. write(OUT / 'delivery-receipt.json', receipt)
  121. shutil.copyfile(archive, DIST / archive.name)
  122. shutil.copyfile(OUT / 'delivery-receipt.json', DIST / '研究数据库补包校验-20261106.json')
  123. note = '''
  124. ## 研究库补装 · 2026-11-06
  125. 已收到运维与王刚001回报:本目录11月6日两份Skill已安装且实际加载;研究数据库此前未安装,查询停在DATABASE_NOT_INSTALLED。对此不需要重装或回退Skill。
  126. 请只转交 **ksf-voc-database-20261106-r1.zip** 与其包内docs/发给运维001-补装研究数据库.md;校验见研究数据库补包校验-20261106.json。该包只含数据库本体、安装/验收工具和样例,不含Skill,装到既定默认路径,无须新凭据或改网关。
  127. 补包取自Codex本地看板当前快照;尚未安装到远端,尚未对齐远端生产看板或配置后续自动同步。数据库装好再由王刚001继续原五步真实消息验收。原r2整包中的旧Skill安装器不要再执行。
  128. '''
  129. readme = (DIST / 'README.md').read_text(encoding='utf-8')
  130. readme = readme.replace('不含数据库,沿用已安装v2研究库。缺库时需先用原完整r2资料包建立授权库;不能只复制Skill就声称能查到资料。',
  131. '查询规则包不含数据库。缺库时用本目录新增的数据库专用补包落地,不再执行原r2整包中的旧Skill安装器。')
  132. readme = readme.replace('本机准备和验证完成,未在王刚001生产实例安装/加载/真实消息验收。请将当前完整ZIP与包内docs/发给运维001.md一起转运维;安装后将docs/发给王刚001.md交本人真实消息验证。',
  133. '已收到运维001报告两份Skill安装完成,王刚001确认真实加载;数据库此前未安装,真实数据查询尚未验收。请按下方数据库补装说明继续,不重复安装Skill。')
  134. (DIST / 'README.md').write_text(readme + note, encoding='utf-8')
  135. version_file = DIST / '版本与校验.json'
  136. version = json.loads(version_file.read_text(encoding='utf-8'))
  137. version['deploymentReport20261106'] = dict(source='user forwarded Operations001 and WangGang001 reports',
  138. skillsInstalledReported=True, skillsLoadedReported=True, databaseInstalledReported=False,
  139. realMessageQueryVerified=False, interpretation='historical report before this database supplement; package preparation is not remote installation')
  140. version['databaseSupplement'] = dict(zip=receipt, installInstructions=str(ROOT / 'docs/voc-database-delivery-20261106/发给运维001-补装研究数据库.md'))
  141. write(version_file, version)
  142. handoff = Path('F:/文档/新数字人交接')
  143. (handoff / '11-研究数据库补装-20261106.md').write_text('''# 研究数据库补装 · 2026-11-06
  144. 11月6日查询规则包仅含Skill,研究数据库须独立落地。两份Skill已由运维报告安装并经王刚001实际加载,查询因DATABASE_NOT_INSTALLED未开始。
  145. 当前补装包:F:/文档/康师傅skill/ksf-voc-database-20261106-r1.zip;核验文件:同目录研究数据库补包校验-20261106.json。
  146. 只补数据库到默认路径,不重装旧r2 Skill,不改身份、会话、网关或生产看板。按包内运维单安装/回读后,再由用户将王刚001续验单转入真实企微消息。数据库源于本地看板同字节快照,远端共享存储和后续自动同步尚未接通。
  147. ''', encoding='utf-8')
  148. print(json.dumps({key: value for key, value in receipt.items() if key != 'database'}, ensure_ascii=False, indent=2))
  149. if __name__ == '__main__':
  150. parser = argparse.ArgumentParser(description=__doc__)
  151. parser.add_argument('phase', choices=['prepare', 'validate', 'finalize'])
  152. args = parser.parse_args()
  153. globals()[args.phase]()