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