package-workshop-onboarding-r5.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. """Build the r10 Skill release; preserve the seven deployed business runtime files."""
  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-r5'
  11. RELEASE = 'ksf-workshop-onboarding-20261109-r5'
  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 = '''# 康师傅产品创新工作坊 · r10 安装与升级
  15. 复制初始化提示词即可,由数字人取得资源并安装,无需附件。工作坊唯一内部技能为 ksf-workshop-runtime;官方 VOC 工具 voc-intelligence 作为外部工具配套安装,不是第二个康师傅技能。
  16. ## 固定资源与校验
  17. 以本说明所在的固定 Git 提交目录为根目录,先读取同目录 distribution.json。下载 archive.path 对应的 ZIP,核字节数与 SHA-256;在新的临时目录安全解压,拒绝重复路径、路径越界及符号链接,再按 FILES.sha256 校验全部成员。安装器与本地工具检查脚本的指纹另列于 distribution.json,不改用分支最新地址。
  18. 本版 Skill r10 共43个公开文件,联合优化三个半天的研究任务、主动澄清、产品判断、证据边界及同题接续;课堂以概念提案和评审汇总收束。保留六个业务脚本及 runtime-lock.json 的既有字节,不改变服务、数据库或身份配置。
  19. 外部工具固定为官方 @vocmarket/voc-skill@0.3.42。安装器直接从官方 npm 下载,核原包及169个成员;安装为 voc-intelligence,第三方原包不放入本发行 ZIP。
  20. ## 识别当前宿主
  21. 先从当前宿主实际配置、帮助或受支持接口确认技能根目录,使用用户可持久安装的位置;不把内置技能受管目录当作用户目录,不手写索引。不得只凭熟悉的其他客户端路径推断本机位置。使用宿主正式支持的技能导入和工具接入方式。
  22. 只有聊天或网页阅读能力时,说明缺少的安装入口;读到 SKILL.md 只证明文件可读。执行安装需 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、r9 先备份变更文件再升级。本机身份、绑定、授权、额外文件及其他技能保留。未知版本或文件被本地改动时停止覆盖,报告准确差异;不能另建重复入口掩盖冲突。
  30. 原包 .mcp.json 含作者机器路径,原包校验通过后仅对此文件的安装副本适配路径,记录前后指纹。结果目录另有 voc-mcp.fragment.json,由宿主正常机制导入;配置生成与原生工具注册分别核实,不覆盖其他连接器。若 VOC 已配置,只按原版及已记录的路径适配差异核验复用。
  31. ## 最小验收
  32. 1. 核 Skill r10 的43个文件、官方 VOC 包版本及成员,说明唯一可接受的本机路径适配差异。
  33. 2. 核宿主能否发现并实际加载 ksf-workshop-runtime 与外部 VOC;按本题读取研究闭环、主动澄清、报告设计和研究资料操作。需要新任务才加载时明确这一项待核,不能凭文件目录或自动索引替代实际加载。
  34. 3. 实际完成 VOC 本地工具发现、接口清单检索和参数文档读取。本地 stdio 可运行与宿主原生 MCP 已注册分开报告;只查清单与文档,不采集业务数据。
  35. 4. 已有企业连接时,只读核研究 contract 的 backend/mode/当前代际,以及 workshop-contract 的保存合同。无通道或业务桥不支持当前平台时说明具体条件,保留已可用的研究规则及本地交付能力。
  36. 最终用简表分别报告:文件完整、宿主加载、VOC本地执行、原生工具、企业资料、成果保存。初始化不创建会话、项目或报告,不新增业务采集,不续跑旧案例;成果保存本次只核入口,不试写记录。
  37. ## 统一数据库状态与能力边界
  38. 截至本版发布,数据库迁移及现有数据、读取、保存、编号回读和看板链路已完成验收:生产资料、监测台账和业务成果统一持久化到既有企业库 FKieRbIoGt_master,旧 SQLite 封存。执行时仍按实际 contract 检查当前后端与代际;安装技能不会自动为另一台员工电脑建立企业连接。内存检索投影和独立暂存文件不构成第二个生产资料库。
  39. 真实新研究材料的首次新代际发布留待该任务实际验收;企微通讯录实名同步仍待环境配置。这两项不等于既有保存链失败。Windows 可安装技能文件,不代表原 Linux 业务桥可在 Windows 执行;缺通道时不改校验、不搬凭据、不冒称看板保存成功。
  40. '''
  41. def stage():
  42. baselines = {
  43. 'r7': 'docs/workshop-onboarding-20261109-r2/package/release.json',
  44. 'r8': 'docs/workshop-onboarding-20261109-r3/package/release.json',
  45. 'r9': 'docs/workshop-onboarding-20261109-r4/package/release.json',
  46. }
  47. old = {v: json.loads((ROOT / p).read_bytes()) for v, p in baselines.items()}
  48. skill = ROOT / 'skills/ksf-workshop-runtime'
  49. workshop, package = {}, {}
  50. for line in (skill / 'FILES.sha256').read_text('utf-8').splitlines():
  51. h, name = line.split(' ', 1)
  52. assert re.fullmatch('[A-Za-z0-9_./-]+', name) and '..' not in Path(name).parts
  53. raw = (skill / name).read_bytes()
  54. assert sha(raw) == h, name
  55. workshop[name] = h
  56. package['skills/ksf-workshop-runtime/' + name] = raw
  57. raw = (skill / 'FILES.sha256').read_bytes()
  58. workshop['FILES.sha256'] = sha(raw)
  59. package['skills/ksf-workshop-runtime/FILES.sha256'] = raw
  60. assert len(workshop) == 43
  61. assert all(set(workshop) == set(b['workshop']['files']) for b in old.values())
  62. protected = [n for n in workshop if n.startswith('scripts/') or n == 'runtime-lock.json']
  63. assert len(protected) == 7
  64. assert all(workshop[n] == b['workshop']['files'][n] for b in old.values() for n in protected)
  65. links = 0
  66. for name in workshop:
  67. if not name.endswith('.md'):
  68. continue
  69. p = skill / name
  70. for target in re.findall(r'\]\(([^)]+)\)', p.read_text('utf-8')):
  71. if '://' in target or target.startswith('#'):
  72. continue
  73. resolved = (p.parent / target.split('#', 1)[0]).resolve()
  74. assert resolved.is_relative_to(skill.resolve()) and resolved.is_file(), target
  75. links += 1
  76. meta = {
  77. 'schema': 'ksf.workshop-onboarding.v1', 'releaseId': RELEASE,
  78. 'workshop': {'name': 'ksf-workshop-runtime', 'version': 'r10', 'files': workshop,
  79. 'relativeLinks': links, 'protectedFiles': protected},
  80. 'upgradeFrom': {v: b['workshop']['files'] for v, b in old.items()}, 'voc': old['r7']['voc'],
  81. 'installation': {'freshInstallSupported': True, 'matchingVersion': 'verify-only',
  82. 'supportedUpgradeFrom': list(old), 'unknownVersion': 'stop-preserve',
  83. 'hostSkillsDirectory': 'must-be-identified-from-host',
  84. 'hostConfigurationModifiedByInstaller': False},
  85. 'workshopEntrypoints': ['ksf-workshop-runtime'], 'credentialsIncluded': False,
  86. 'databaseIncluded': False, 'productionDatabaseCutoverConfirmed': True,
  87. }
  88. package.update({'release.json': encode(meta), 'README.md': GUIDE.encode('utf-8'),
  89. 'install.py': (ROOT / 'scripts/workshop-onboarding/install.py').read_bytes(),
  90. 'check-voc.cjs': (ROOT / 'scripts/workshop-onboarding/check-voc.cjs').read_bytes()})
  91. ast.parse(package['install.py'].decode('utf-8-sig'), feature_version=(3, 9))
  92. assert [n for n in package if n.endswith('/SKILL.md')] == ['skills/ksf-workshop-runtime/SKILL.md']
  93. package['FILES.sha256'] = ''.join(sha(raw) + ' ' + n + '\n' for n, raw in sorted(package.items())).encode('utf-8')
  94. for name, raw in package.items():
  95. p = OUT / 'package' / name
  96. p.parent.mkdir(parents=True, exist_ok=True)
  97. p.write_bytes(raw)
  98. return package, meta
  99. def main():
  100. parser = argparse.ArgumentParser(description=__doc__)
  101. parser.add_argument('--pack', action='store_true')
  102. args = parser.parse_args()
  103. package, meta = stage()
  104. if not args.pack:
  105. print(json.dumps({'ok': True, 'staged': len(package), 'workshopFiles': 43}))
  106. return
  107. validation = json.loads((OUT / 'local-validation.json').read_bytes())
  108. assert validation['ok'] and validation['installerSha256'] == sha(package['install.py'])
  109. assert validation['workshopEntrySha256'] == meta['workshop']['files']['SKILL.md']
  110. pub = OUT / 'publish'
  111. pub.mkdir(parents=True, exist_ok=True)
  112. archive = pub / (RELEASE + '.zip')
  113. assert not archive.exists(), 'NEVER_OVERWRITE_RELEASE'
  114. with zipfile.ZipFile(archive, 'x', zipfile.ZIP_DEFLATED) as z:
  115. for name, raw in sorted(package.items()):
  116. info = zipfile.ZipInfo(name, (2026, 11, 9, 0, 0, 0))
  117. info.external_attr = 0o100600 << 16
  118. z.writestr(info, raw, compress_type=zipfile.ZIP_DEFLATED)
  119. with zipfile.ZipFile(archive) as z:
  120. assert z.testzip() is None and set(z.namelist()) == set(package)
  121. assert all(z.read(n) == raw for n, raw in package.items())
  122. distribution = {
  123. 'schema': 'ksf.workshop-onboarding-distribution.v1', 'releaseId': RELEASE,
  124. 'archive': {'path': archive.name, 'bytes': archive.stat().st_size, 'sha256': sha(archive.read_bytes())},
  125. 'installerSha256': sha(package['install.py']), 'runtimeCheckSha256': sha(package['check-voc.cjs']),
  126. 'workshopSkill': {'version': 'r10', 'files': 43, 'entrySha256': meta['workshop']['files']['SKILL.md']},
  127. 'voc': {k: meta['voc'][k] for k in ('name', 'version', 'url', 'sha256', 'bytes')},
  128. 'supportedUpgradeFrom': ['r7', 'r8', 'r9'], 'workshopEntrypoints': ['ksf-workshop-runtime'],
  129. 'thirdPartyArtifactIncluded': False, 'credentialsIncluded': False, 'databaseIncluded': False,
  130. 'userAttachmentRequired': False, 'localRuntimeValidated': True,
  131. 'targetHostAcceptanceVerified': False, 'productionDatabaseCutoverConfirmed': True,
  132. 'newResearchGenerationAcceptance': 'awaiting-genuine-new-material',
  133. 'employeeRealNameSync': 'awaiting-wecom-directory-configuration',
  134. }
  135. (pub / 'distribution.json').write_bytes(encode(distribution))
  136. (pub / 'INSTALL.md').write_text(GUIDE, encoding='utf-8')
  137. (OUT / 'build-receipt.json').write_bytes(encode(distribution))
  138. print(json.dumps(distribution, ensure_ascii=False, indent=2))
  139. if __name__ == '__main__':
  140. main()