"""Prepare an audited source integration without overwriting protocol or runtime state.""" from pathlib import Path import ast,json,hashlib,os,shutil root=Path('C:/kefu');source=root/'wechat_rpa';target=Path('C:/wechat_rpa') folder=Path(__file__).resolve().parent;stage=folder/'payload';stage.mkdir(exist_ok=True) inventory=json.loads((folder/'inventory.json').read_text(encoding='utf-8')) replacements=['admin_api.py','admin_backend.py','archive_api.py','archive_store.py','model_gateway.py', 'backend_client.py','rbac.py','run_backend.py','test_admin_api.py','BACKEND.md'] def put(name,text): p=stage/name;p.parent.mkdir(parents=True,exist_ok=True);p.write_text(text,encoding='utf-8',newline='\n') for name in inventory['source_only']+replacements: put(name,(source/name).read_text(encoding='utf-8-sig')) # Merge knowledge UI methods only, retaining protocol bridge, listeners and build hooks. src=(source/'wechat_gui_qt.py').read_text(encoding='utf-8-sig') dst=(target/'wechat_gui_qt.py').read_text(encoding='utf-8-sig') def method(text,name): nodes=[n for n in ast.walk(ast.parse(text)) if isinstance(n,ast.FunctionDef) and n.name==name] assert len(nodes)==1,name n=nodes[0];return n,'\n'.join(text.splitlines()[n.lineno-1:n.end_lineno]) for name in ['editKnowledge','_console_ai_state']: _,new=method(src,name);_,old=method(dst,name);assert old in dst;dst=dst.replace(old,new,1) old='manage_knowledge.clicked.connect(self._focus_mcp_editor)' if old not in dst: # Capture the existing connection without changing any other controls. old = """manage_knowledge.clicked.connect( lambda: QDesktopServices.openUrl(QUrl.fromLocalFile(str(SCRIPT_DIR))) )""" new='''def open_knowledge_center(): import backend_client QDesktopServices.openUrl(QUrl(backend_client.knowledge_management_url())) manage_knowledge.clicked.connect(open_knowledge_center)''' assert dst.count(old)==1;dst=dst.replace(old,new,1) put('wechat_gui_qt.py',dst) # This frontend uses hash routing, including in the standalone folder. p=stage/'backend_client.py';s=p.read_text(encoding='utf-8') s=s.replace(' + "/knowledge/center"',' + "/#/knowledge/center"');put('backend_client.py',s) # A standalone checkout keeps the complete admin app below itself; repo layout still works. p=stage/'admin_api.py';s=p.read_text(encoding='utf-8') old=' return Path(__file__).resolve().parent.parent / "admin-web" / "apps" / "web-antd" / "dist"' new=''' directory = Path(__file__).resolve().parent embedded = directory / "admin-web" / "apps" / "web-antd" / "dist" if (embedded / "index.html").is_file(): return embedded return directory.parent / "admin-web" / "apps" / "web-antd" / "dist"''' assert s.count(old)==1;s=s.replace(old,new) point=' # ── 前端静态资源' middleware=''' @app.middleware("http") async def frontend_api_prefix(request: Request, call_next): # The bundled production frontend uses /api; direct Python hosting needs no Nginx. path = request.scope["path"] if path.startswith("/api/") and not path.startswith("/api/v2/"): request.scope["path"] = path.replace("/api/", "/api/v2/", 1) raw = request.scope.get("raw_path", path.encode()) request.scope["raw_path"] = raw.replace(b"/api/", b"/api/v2/", 1) return await call_next(request) ''' assert s.count(point)==1;s=s.replace(point,middleware+point) s=s.replace(' candidate = dist / full_path',''' candidate = (dist / full_path).resolve() if not candidate.is_relative_to(dist.resolve()): raise HTTPException(status_code=404, detail="资源不存在")''') put('admin_api.py',s) # Propagate references and review risk through the protocol response path. s=(target/'ai_chat.py').read_text(encoding='utf-8-sig') point=' "task_id": task_id,\n }\n _TRACE.last' assert s.count(point)==1 s=s.replace(point,' "task_id": task_id,\n "knowledge": data.get("knowledge") if isinstance(data.get("knowledge"), dict) else {},\n }\n _TRACE.last') put('ai_chat.py',s) s=(target/'protocol_engine.py').read_text(encoding='utf-8-sig') old=" if ai_chat.gateway_provider() is not None:return ai_chat.get_ai_reply(chat_text=context['text'])" new=''' if ai_chat.gateway_provider() is not None: ai_chat.take_last_gateway_trace() reply=ai_chat.get_ai_reply(chat_text=context['text']) trace=ai_chat.take_last_gateway_trace() key=session_key(self.account,context['conv_id']) with self._lock: if key in self._pending: self._pending[key]['judge_risk']=str((trace.get('judge') or {}).get('risk') or '') self._pending[key]['knowledge_trace']=trace.get('knowledge') or {} self._pending[key]['model_task_id']=trace.get('task_id') or '' return reply''' assert s.count(old)==1;s=s.replace(old,new);put('protocol_engine.py',s) # Complete frontend source and verified production assets, excluding local caches and secrets. web=root/'admin-web';skip={'node_modules','.git','.turbo','.pnpm-store','coverage','test-results','playwright-report','__pycache__'} for directory,dirs,names in os.walk(web): directory=Path(directory);rel=directory.relative_to(web) dirs[:]=[d for d in dirs if d not in skip and not (directory/d).is_symlink()] if rel==Path('.'): dirs[:]=[d for d in dirs if d in {'apps','packages','internal','scripts'}] if rel==Path('apps'): dirs[:]=[d for d in dirs if d=='web-antd'] if rel!=Path('apps/web-antd'): dirs[:]=[d for d in dirs if d!='dist'] for name in names: if name.endswith(('.local','.log','.zip','.tsbuildinfo')) or '.local.' in name:continue path=directory/name if path.is_symlink():continue destination=stage/'admin-web'/rel/name;destination.parent.mkdir(parents=True,exist_ok=True) shutil.copyfile(path,destination) # Deploy layout adjusted to this standalone root, with only explicit server inputs. deploy=root/'deploy/im-admin' for name in ['compose.yaml','Dockerfile','requirements.txt','.env.example','im.zhenyangtang.cn.conf']: text=(deploy/name).read_text(encoding='utf-8-sig') if name=='Dockerfile':text=text.replace('COPY --chown=app:app wechat_rpa/*.py','COPY --chown=app:app *.py') if name=='compose.yaml':text=text.replace('zyt/wecom-admin:current','zyt/wecom-admin:protocol-integrated') put('deploy/im-admin/'+name,text) put('.dockerignore','''** !*.py !deploy/ !deploy/im-admin/ !deploy/im-admin/Dockerfile !deploy/im-admin/requirements.txt !admin-web/ !admin-web/apps/ !admin-web/apps/web-antd/ !admin-web/apps/web-antd/dist/ !admin-web/apps/web-antd/dist/** ''') manifest=[] for path in sorted(stage.rglob('*')): if not path.is_file():continue relative=path.relative_to(stage).as_posix();old=target/relative manifest.append({'path':relative,'sha256':hashlib.sha256(path.read_bytes()).hexdigest(), 'before_sha256':hashlib.sha256(old.read_bytes()).hexdigest() if old.is_file() else None,'bytes':path.stat().st_size}) (folder/'manifest.json').write_text(json.dumps(manifest,ensure_ascii=False,indent=2),encoding='utf-8') print(json.dumps({'files':len(manifest),'bytes':sum(r['bytes'] for r in manifest),'existing_replaced':sum(r['before_sha256'] is not None for r in manifest),'protocol_version':'1.4.16 preserved'},ensure_ascii=False))