package-workshop-onboarding-r4.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. """Publish-ready current workshop kit and corrected production target identity migration delivery."""
  2. import argparse
  3. import ast
  4. import hashlib
  5. import json
  6. from pathlib import Path
  7. import re
  8. import zipfile
  9. ROOT = Path(__file__).resolve().parents[1]
  10. OUT = ROOT / 'docs/workshop-onboarding-20261109-r4'
  11. RELEASE = 'ksf-workshop-onboarding-20261109-r4'
  12. sha = lambda value: hashlib.sha256(value).hexdigest()
  13. encode = lambda value: (json.dumps(value, ensure_ascii=False, indent=2) + '\n').encode('utf-8')
  14. GUIDE = '''# 康师傅产品创新工作坊 · 安装与升级
  15. 学员复制初始化提示词即可,由当前数字人下载、校验和安装资源,无需附件。Skill适用于具备相应技能与执行能力的宿主,不绑定模型或客户端。
  16. ## 固定资源
  17. 以本说明所在的固定 Git 提交目录为根目录,读取同目录 distribution.json。下载 archive.path 对应的 ZIP,先核字节数与 SHA-256。在新临时目录安全解压:拒绝重复路径、路径越界、符号链接,再按 FILES.sha256 验证全部文件。安装脚本和本地检查脚本的指纹另列于 distribution.json。不要改用 main 或未经验证的新版本。
  18. 本版内含康师傅产品创新工作坊 Skill r9(43个公开文件),沿用既有研究闭环、主动澄清、报告设计和逐环节交付细则;本次按运维只读实连结果修正单库目标身份为 FKieRbIoGt_master / FKieRbIoGt_admin,保持严格校验;保留既有业务运行组件。康师傅工作坊唯一入口为 ksf-workshop-runtime,原内部研究 Skill 不再单独安装或注册。研究查询运行组件由平台部署。
  19. 外部 VOC 工具继续使用官方 @vocmarket/voc-skill@0.3.42;安装器直接从官方 npm 下载并校验原包及169个成员,在当前宿主的正常目录中安装为 voc-intelligence。这是外部工具包,与已经合并的内部研究模块不同。发行 ZIP 不包含该第三方原包。
  20. ## 识别环境
  21. 从宿主实际的技能管理、配置说明或受支持接口识别 Skills 目录和工具接入方式,不猜 .codex/skills、.claude/skills 或 /opt/data/skills。使用当前宿主正式支持的导入/管理机制;文件安装只是其中一步。若只有聊天或网页阅读能力,如实指出缺少的安装入口,不把读到 SKILL.md 称为技能已加载。
  22. 执行安装需要 Python 3.9+、Node.js 18+ 和 npm。运行时业务连接沿用当前平台授权,不向学员索要数据库密钥,也不复制其他人的身份或凭据。
  23. ## 安装或升级
  24. 在已校验的解压目录执行,参数由数字人使用实际绝对路径填写:
  25. ```text
  26. python -B install.py --skills-dir <宿主实际Skills目录>
  27. python -B install.py --skills-dir <宿主实际Skills目录> --out <新的结果目录> --apply --upgrade --prepare-runtime
  28. ```
  29. 第一步只预检。首次使用可完整安装;已有同版只核验复用。已有完整 r7 或 r8 时,--upgrade 只更新变动的公开说明,并先在结果目录 before/ 中备份。六个业务脚本和 runtime-lock.json 必须与既有版本一致,本机身份、绑定、授权及额外文件保留。未知版本或本地已改动文件会停止升级,先核差异,不覆盖也不另造重复入口。
  30. VOC 原包 .mcp.json 含作者机器路径,原包校验通过后只对安装副本的这一份配置适配当前路径并记录前后指纹;结果目录另有 voc-mcp.fragment.json。用宿主正常机制接入工具,不手写技能索引,不覆盖其他连接器。配置片段生成不等于工具已接入。
  31. ## 加载与能力核实
  32. 从宿主技能管理实际加载 ksf-workshop-runtime;按本题读取研究闭环、主动澄清、报告设计和研究资料操作。核外部 VOC 入口及接口说明,实际完成本地工具发现、接口检索、参数文档读取。宿主技能发现、原生工具注册与本地脚本执行分开记录;若新任务仍无法发现,使用宿主支持的导入机制,不能仅凭目录存在称通过。
  33. 企业资料读取先执行实际通道 contract,业务保存先核 workshop-contract 与当前身份。技能安装不会创建数据库连接或解除业务运行平台限制。Windows 本地能安装文件,不代表旧的 Linux 业务桥已经可执行;缺少平台接入时保留本地分析与文件交付能力,明确缺少的读写通道。
  34. 初始化只做安装和最小只读核实,不采业务资料、不创建项目/报告/会话,不接着运行旧案例。报告区分:文件完整、宿主加载、VOC本地脚本、宿主工具接入、企业资料读取、成果保存。完成到哪一层就反馈到哪一层,不要求每次业务回复重复安装核验信息。
  35. ## 单库迁移与运行服务
  36. 康师傅业务与研究资料的持久数据目标为同一 FKieRbIoGt_master;研究查询与业务操作仍按各自合同分工。内存 SQLite/FTS5 只是从 PostgreSQL 重建的查询投影,不是另一个持久研究库。
  37. 发行时生产迁移尚待运维执行,不因安装 r9 声称生产已经切换。运维资源位于同一固定提交目录 operations/,按 migration.json 核 ZIP 后读取包内执行单。包内只提供代码、结构和验收工具,不含源数据、凭据或业务运行二进制;必须以生产当时的全量源代际迁移,禁止用本地旧资料覆盖线上。实际切换须在运维完成备份、导入、查询一致性、读写切换、预热及回滚验证后确认。
  38. '''
  39. def stage():
  40. old = json.loads((ROOT / 'docs/workshop-onboarding-20261109-r2/package/release.json').read_bytes())
  41. previous = json.loads((ROOT / 'docs/workshop-onboarding-20261109-r3/package/release.json').read_bytes())
  42. skill = ROOT / 'skills/ksf-workshop-runtime'
  43. workshop = {}
  44. package = {}
  45. for line in (skill / 'FILES.sha256').read_text('utf-8').splitlines():
  46. h, name = line.split(' ', 1)
  47. assert re.fullmatch('[A-Za-z0-9_./-]+', name) and '..' not in Path(name).parts
  48. raw = (skill / name).read_bytes()
  49. assert sha(raw) == h, name
  50. workshop[name] = h
  51. package['skills/ksf-workshop-runtime/' + name] = raw
  52. raw = (skill / 'FILES.sha256').read_bytes()
  53. workshop['FILES.sha256'] = sha(raw)
  54. package['skills/ksf-workshop-runtime/FILES.sha256'] = raw
  55. assert len(workshop) == 43 and set(workshop) == set(old['workshop']['files'])
  56. protected = [n for n in workshop if n.startswith('scripts/') or n == 'runtime-lock.json']
  57. assert len(protected) == 7
  58. assert all(workshop[n] == old['workshop']['files'][n] for n in protected)
  59. links = 0
  60. for name in workshop:
  61. if not name.endswith('.md'):
  62. continue
  63. p = skill / name
  64. for target in re.findall(r'\]\(([^)]+)\)', p.read_text('utf-8')):
  65. if '://' in target or target.startswith('#'):
  66. continue
  67. resolved = (p.parent / target.split('#', 1)[0]).resolve()
  68. assert resolved.is_relative_to(skill.resolve()) and resolved.is_file(), target
  69. links += 1
  70. meta = {'schema': 'ksf.workshop-onboarding.v1', 'releaseId': RELEASE,
  71. 'workshop': {'name': 'ksf-workshop-runtime', 'version': 'r9', 'files': workshop,
  72. 'relativeLinks': links, 'protectedFiles': protected},
  73. 'upgradeFrom': {'r7': old['workshop']['files'], 'r8': previous['workshop']['files']}, 'voc': old['voc'],
  74. 'installation': {'freshInstallSupported': True, 'matchingVersion': 'verify-only',
  75. 'supportedUpgradeFrom': ['r7', 'r8'], 'unknownVersion': 'stop-preserve',
  76. 'hostSkillsDirectory': 'must-be-identified-from-host',
  77. 'hostConfigurationModifiedByInstaller': False},
  78. 'workshopEntrypoints': ['ksf-workshop-runtime'], 'credentialsIncluded': False,
  79. 'databaseIncluded': False, 'productionDatabaseCutoverConfirmed': False}
  80. package.update({'release.json': encode(meta), 'README.md': GUIDE.encode('utf-8'),
  81. 'install.py': (ROOT / 'scripts/workshop-onboarding/install.py').read_bytes(),
  82. 'check-voc.cjs': (ROOT / 'scripts/workshop-onboarding/check-voc.cjs').read_bytes()})
  83. ast.parse(package['install.py'].decode('utf-8-sig'), feature_version=(3, 9))
  84. assert [n for n in package if n.endswith('/SKILL.md')] == ['skills/ksf-workshop-runtime/SKILL.md']
  85. package['FILES.sha256'] = ''.join(sha(raw) + ' ' + n + '\n' for n, raw in sorted(package.items())).encode('utf-8')
  86. for n, raw in package.items():
  87. p = OUT / 'package' / n
  88. p.parent.mkdir(parents=True, exist_ok=True)
  89. p.write_bytes(raw)
  90. return package, meta
  91. def main():
  92. p = argparse.ArgumentParser(description=__doc__)
  93. p.add_argument('--pack', action='store_true')
  94. args = p.parse_args()
  95. package, meta = stage()
  96. if not args.pack:
  97. print(json.dumps({'ok': True, 'staged': len(package), 'workshopFiles': 43}))
  98. return
  99. validation = json.loads((OUT / 'local-validation.json').read_bytes())
  100. assert validation['ok'] and validation['installerSha256'] == sha(package['install.py'])
  101. pub = OUT / 'publish'
  102. pub.mkdir(parents=True, exist_ok=True)
  103. archive = pub / (RELEASE + '.zip')
  104. assert not archive.exists(), 'NEVER_OVERWRITE_RELEASE'
  105. with zipfile.ZipFile(archive, 'x', zipfile.ZIP_DEFLATED) as z:
  106. for n, raw in sorted(package.items()):
  107. info = zipfile.ZipInfo(n, (2026, 11, 9, 0, 0, 0))
  108. info.external_attr = 0o100600 << 16
  109. z.writestr(info, raw, compress_type=zipfile.ZIP_DEFLATED)
  110. with zipfile.ZipFile(archive) as z:
  111. assert z.testzip() is None and set(z.namelist()) == set(package)
  112. assert all(z.read(n) == raw for n, raw in package.items())
  113. migration = ROOT / 'docs/unified-database-migration-20261109/release-r3'
  114. ops = json.loads((migration / 'delivery-receipt.json').read_bytes())
  115. ops_file = migration / 'ksf-unified-database-migration-20261109-r3.zip'
  116. assert sha(ops_file.read_bytes()) == ops['sha256']
  117. ops_dir = pub / 'operations'
  118. ops_dir.mkdir()
  119. (ops_dir / ops_file.name).write_bytes(ops_file.read_bytes())
  120. (ops_dir / 'migration.json').write_bytes(encode({
  121. 'release': ops['release'], 'archive': ops_file.name,
  122. 'sha256': ops['sha256'], 'bytes': ops_file.stat().st_size,
  123. 'database': 'FKieRbIoGt_master', 'role': 'FKieRbIoGt_admin',
  124. 'productionDeployed': False, 'sourceDataIncluded': False,
  125. 'credentialsIncluded': False, 'workshopSkillVersionEquivalent': 'r9',
  126. 'executionSheet': '执行单.md (inside archive)'}))
  127. distribution = {
  128. 'schema': 'ksf.workshop-onboarding-distribution.v1', 'releaseId': RELEASE,
  129. 'archive': {'path': archive.name, 'bytes': archive.stat().st_size, 'sha256': sha(archive.read_bytes())},
  130. 'installerSha256': sha(package['install.py']), 'runtimeCheckSha256': sha(package['check-voc.cjs']),
  131. 'workshopSkill': {'version': 'r9', 'files': 43, 'entrySha256': meta['workshop']['files']['SKILL.md']},
  132. 'voc': {k: meta['voc'][k] for k in ('name', 'version', 'url', 'sha256', 'bytes')},
  133. 'supportedUpgradeFrom': ['r7', 'r8'], 'workshopEntrypoints': ['ksf-workshop-runtime'],
  134. 'operationsManifest': 'operations/migration.json', 'thirdPartyArtifactIncluded': False,
  135. 'credentialsIncluded': False, 'databaseIncluded': False, 'userAttachmentRequired': False,
  136. 'localRuntimeValidated': True, 'targetHostAcceptanceVerified': False,
  137. 'productionDatabaseCutoverConfirmed': False}
  138. (pub / 'distribution.json').write_bytes(encode(distribution))
  139. (pub / 'INSTALL.md').write_text(GUIDE, encoding='utf-8')
  140. (OUT / 'build-receipt.json').write_bytes(encode(distribution))
  141. print(json.dumps(distribution, ensure_ascii=False, indent=2))
  142. if __name__ == '__main__':
  143. main()