from pathlib import Path import json,hashlib,tarfile,shutil root=Path('C:/kefu'); release=root/'deploy/knowledge-v3-20260916';release.mkdir(exist_ok=True) p=root/'wechat_rpa/KNOWLEDGE.md';s=p.read_text(encoding='utf-8').replace('模型整理使用管理端配置的主答模型,只发送规则预处理后的脱敏问答。', '模型整理可指定管理端已启用的文本模型,创建时固定所选出口;仅发送规则预处理后的脱敏问答。') s+=''' ## 第三版:模型任务恢复与驳回删除 - 创建模型任务时选择整理模型、单次等待时间(30–180 秒,默认 90 秒)及调用总上限;知识整理请求输出上限为 4096 token,独立于实时客服调用配置。Dify 应用自己的模型配置与限制仍由上游控制。 - 失败或暂停任务点击「配置 / 继续」,可换模型、增加等待时间或调用总上限,亦可改成规则整理。已完成草稿保留,新配置只作用于剩余问答。原模型停用时直接报错,不会偷偷切换到其他出口。 - 扫描游标和待处理问答先持久化,每生成一条草稿立即提交。失败重试不再重做该批已成功的问答;在途请求遇到暂停、取消或进程中断仍可能计费并需要重试。 - 错误区分超时、授权、限流、网络与输出格式问题,服务端不保存上游原始错误正文、密钥或地址。页面字符量统计成功处理的输入/输出,并非计费 token 数。 - 已驳回知识支持单条及批量确认删除,需知识编辑与来源正文查看权限。删除知识、来源快照、版本记录和关键词索引,保留原始聊天归档及删除审计;无法从管理界面恢复。租户内保留内容指纹,避免完全相同的已删除草稿被再次生成;不同模型改写仍需人工去重。 - 新增 `knowledge_discarded` 表,无需回填归档数据。任务待处理问答保存在私有 `state_json`,接口仅返回待处理数量。 回归检查:`python -m unittest test_knowledge test_knowledge_tasks test_model_gateway -q`;前端批量操作测试、类型检查及隔离浏览器中的失败换模型续跑与删除流程。 ''' p.write_text(s,encoding='utf-8',newline='\n') files=[root/'wechat_rpa'/name for name in ['knowledge_api.py','knowledge_store.py','knowledge_worker.py','knowledge_models.py','test_knowledge.py','test_knowledge_tasks.py','KNOWLEDGE.md']] files += [root/'admin-web/apps/web-antd/src/api/knowledge.ts'] files += list((root/'admin-web/apps/web-antd/src/views/knowledge').glob('*')) files += [p for p in (root/'admin-web/apps/web-antd/dist').rglob('*') if p.is_file()] manifest=[] with tarfile.open(release/'payload.tar.gz','w:gz') as tf: for p in files: relative=p.relative_to(root).as_posix();data=p.read_bytes() tf.add(p,arcname=relative) manifest.append({'path':relative,'bytes':len(data),'sha256':hashlib.sha256(data).hexdigest()}) (release/'manifest.json').write_text(json.dumps(manifest,ensure_ascii=False,indent=2),encoding='utf-8') (release/'Dockerfile.overlay').write_text('''ARG BASE_IMAGE=zyt/wecom-admin:before-knowledge-v3-20260916 FROM ${BASE_IMAGE} COPY --chown=app:app payload/wechat_rpa/*.py /app/wechat_rpa/ COPY --chown=app:app payload/admin-web/apps/web-antd/dist/ /app/admin-web/apps/web-antd/dist/ WORKDIR /app/wechat_rpa ''',encoding='utf-8') previous=root/'deploy/knowledge-v2-20260916' for filename in ['01-stage.sh','03-deploy.sh','04-verify.sh','rollback.sh']: text=(previous/filename).read_text(encoding='utf-8').replace('knowledge-v2-20260916','knowledge-v3-20260916').replace('Second-version','Third-version').replace('second-version','third-version') if filename=='01-stage.sh': text=text.replace('sha256:061e8735ca4cc37fe09143f06095149ec7475a159fa0f1923d0410b50bfd9b22','sha256:c73e1952e9edcdb1ce5c516b0f54d728ce5b11f57de77e1bee31b1effc51d763').replace('/home/ps/knowledge-20260916-100045/manifest.json','/home/ps/knowledge-v2-20260916/manifest.json').replace('verified first release','verified second release') if filename=='04-verify.sh': text=text.replace("('/api/v2/knowledge/overview',401,'GET')", "('/api/v2/knowledge/overview',401,'GET'),('/api/v2/knowledge/models',401,'GET')") if filename=='rollback.sh': text=text.replace('docker compose up -d --no-build --wait --wait-timeout 180 api gateway knowledge-worker', '''# V2 cannot consume V3's pending candidate checkpoint. Keep worker stopped if one exists. pending=$(docker run --rm --network=none --mount type=bind,source=/opt/im-admin/deploy/im-admin/data,target=/data,readonly zyt/wecom-admin:before-knowledge-v3-20260916 python -c "import sqlite3,json; db=sqlite3.connect('file:/data/backend.db?mode=ro',uri=True); print(sum(bool(json.loads(r[0]).get('_pending')) for r in db.execute(\\\"SELECT state_json FROM knowledge_job WHERE status NOT IN ('completed','cancelled')\\\")))") if [ "$pending" = 0 ]; then docker compose up -d --no-build --wait --wait-timeout 180 api gateway knowledge-worker else docker compose up -d --no-build --wait --wait-timeout 180 api gateway echo 'Worker remains stopped to preserve pending V3 checkpoints. Redeploy V3 before processing jobs.' fi''') (release/filename).write_text(text,encoding='utf-8',newline='\n') print(json.dumps({'files':len(manifest),'archive_bytes':(release/'payload.tar.gz').stat().st_size}))