Files
kefu/deploy/protocol-integration-20260916/stage.py
T
2026-09-21 10:34:06 +08:00

133 lines
7.4 KiB
Python

"""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))