"""Seal the r10 workshop manual, including a standalone preview and deployment instructions.""" import argparse import hashlib import json from pathlib import Path import zipfile ROOT = Path(__file__).resolve().parents[1] SOURCE = ROOT / 'docs/workshop-manual-20261109' OUT = ROOT / 'docs/workshop-manual-20261110-r1' RELEASE = 'ksf-workshop-manual-20261110-r1' NAMES = ['index.html', 'content.js', 'manual.js', 'manual.css'] sha = lambda b: hashlib.sha256(b).hexdigest() encode = lambda v: (json.dumps(v, ensure_ascii=False, indent=2) + '\n').encode('utf-8') DEPLOY = '''# 康师傅工作坊 r10 · 联合上线执行单 本执行单只更新工作坊技能说明与静态操作手册;数据库迁移、后端运行补丁已经验收,不重跑迁移或重启数据服务。 ## 资源 以本执行单所在的固定 Git 提交目录为根目录,读取 manual-release.json;下载其 archive.path,核字节数及 SHA-256。在新临时目录安全解压并逐项核 FILES.sha256,不覆盖未知目录或改用分支最新地址。包内 manual/ 是四文件站点,ksf-workshop-manual-r10.html 是同内容的单文件预览。 配套技能固定安装说明: {guide} 该说明对应 Skill r10、唯一内部入口 ksf-workshop-runtime,支持完整 r7/r8/r9 备份升级,外部官方 VOC 固定0.3.42。技能与手册联合版本均为r10。 ## 1. 更新原工作坊Agent技能 在原Agent环境按上述安装说明核包、预检、备份升级,再验证43文件。使用已确认的实际技能根,不覆盖身份、授权、绑定及额外文件,不改其他技能。六个业务脚本与runtime-lock.json共7文件必须保持原指纹。保留已退役的旧研究目录为脚本兼容路径,不恢复其SKILL.md;已部署研究运行补丁保持原样。 同版只核验;未知版本或已改动说明停止覆盖并回传差异,不放宽校验。外部VOC如已符合固定版本及已记录的.mcp.json路径适配就复用。更新后的新任务应实际发现并加载r10;文件校验、宿主加载分别反馈。 仅做只读 smoke:默认研究 contract 核 backend=parse-postgresql、mode=ssh-channel(原Agent通道)及实际当前代际;业务 workshop-contract 读取成功。不要新建会话/项目/报告、采集、发布新代际或重跑验收保存记录。 ## 2. 发布静态手册 建议正式地址为 https://kangshifu.fmode.cn/workshop-manual/ ,以维护人核实的Nginx/静态目录和路由为准。manual/四文件保持同目录,以新版本目录暂存后整体切换;存在旧手册先备份,不能直接覆盖来源不明的文件。独立静态路径不要误落回看板SPA首页;不更换看板根首页、不改数据库/API代理。亦可使用已有授权的静态发布域名,回传真实可访问地址。 HTML用text/html、CSS用text/css、JS用application/javascript,均UTF-8。入口和版本资源及时回源;若存在CDN,只刷新这四个手册文件。该站点不含密钥、登录或业务数据,不会写数据库。 ## 3. 一次回传验收 - Skill:版本r10,43文件逐项一致,7运行文件未变;新任务实际加载状态;研究/业务只读合同结果。 - 手册:真实网页URL,4文件HTTP200、Content-Type和SHA与清单一致;浏览器打开不是看板回退页。 - 浏览器:15章导航、完整复制、桌面与390px,无脚本错误;安装提示词指向本执行单固定的r10资源。 - 范围:准备2节、研究13节、三个半天单一路线;没有49天行动计划或两分支;个人勾选只存本机。 - 保持:数据库和既有业务记录不改,身份/连接不改。挂站完成与豆包员工电脑安装生效分开反馈;全量小组展示和豆包实跑按后续任务验收。 ''' def stage(): released = json.loads((ROOT / 'docs/workshop-onboarding-20261109-r5/online-release-receipt.json').read_bytes()) assert released['published'] and released['allPublicFilesReadBack'] and released['workshopSkillVersion'] == 'r10' source = {n: (SOURCE / n).read_bytes() for n in NAMES} assert b'__R10_INSTALL_GUIDE__' not in source['content.js'] assert released['guideUrl'].encode() in source['content.js'] html = source['index.html'].decode('utf-8') html = html.replace('', '') for name in ['content.js', 'manual.js']: script = source[name].decode('utf-8').replace('', '') html = html.replace('', '\n') package = {'manual/' + n: b for n, b in source.items()} package['ksf-workshop-manual-r10.html'] = html.encode('utf-8') package['DEPLOY.md'] = DEPLOY.format(guide=released['guideUrl']).encode('utf-8') meta = {'schema': 'ksf.workshop-manual.v1', 'releaseId': RELEASE, 'workshopSkillVersion': 'r10', 'installationGuide': released['guideUrl'], 'installationCommit': released['commit'], 'sourceFiles': {n: sha(b) for n, b in source.items()}, 'manualFiles': {n: sha(b) for n, b in package.items()}, 'halfDays': 3, 'preparationSteps': 2, 'researchSteps': 13, 'branches': 1, 'promptsExpanded': True, 'postWorkshopPlanIncluded': False, 'databaseIncluded': False, 'credentialsIncluded': False, 'writesBusinessData': False} package['release.json'] = encode(meta) package['FILES.sha256'] = ''.join(sha(b) + ' ' + n + '\n' for n, b in sorted(package.items())).encode('utf-8') for name, b in package.items(): p = OUT / 'package' / name p.parent.mkdir(parents=True, exist_ok=True) p.write_bytes(b) # Convenient local preview, separate from the immutable public archive. (OUT / 'ksf-workshop-manual-r10.html').write_bytes(package['ksf-workshop-manual-r10.html']) return package, meta def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--pack', action='store_true') args = parser.parse_args() archive = OUT / 'publish' / (RELEASE + '.zip') assert not archive.exists(), 'NEVER_OVERWRITE_RELEASE' package, meta = stage() if not args.pack: print(json.dumps({'ok': True, 'staged': len(package), 'preview': str(OUT / 'ksf-workshop-manual-r10.html')})) return browser = json.loads((SOURCE / 'browser-verification.json').read_bytes()) assert browser['ok'] and browser['sourceFiles'] == meta['sourceFiles'] assert browser['standaloneVerified'] and browser['standaloneSha256'] == sha(package['ksf-workshop-manual-r10.html']) archive.parent.mkdir(parents=True, exist_ok=True) with zipfile.ZipFile(archive, 'x', zipfile.ZIP_DEFLATED) as z: for name, b in sorted(package.items()): info = zipfile.ZipInfo(name, (2026, 11, 10, 0, 0, 0)) info.external_attr = 0o100644 << 16 z.writestr(info, b, 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) == b for n, b in package.items()) distribution = {'schema': 'ksf.workshop-manual.distribution.v1', 'releaseId': RELEASE, 'archive': {'path': archive.name, 'bytes': archive.stat().st_size, 'sha256': sha(archive.read_bytes())}, 'workshopSkillVersion': 'r10', 'installationGuide': meta['installationGuide'], 'manualFiles': meta['manualFiles'], 'browserLocalVerified': True, 'employeeWorkshopTrialCompleted': False, 'productionWebsiteDeployed': False, 'suggestedWebsiteUrl': 'https://kangshifu.fmode.cn/workshop-manual/'} (archive.parent / 'manual-release.json').write_bytes(encode(distribution)) (archive.parent / 'DEPLOY.md').write_bytes(package['DEPLOY.md']) (OUT / 'build-receipt.json').write_bytes(encode(distribution)) print(json.dumps(distribution, ensure_ascii=False, indent=2)) if __name__ == '__main__': main()