133 lines
7.4 KiB
Python
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))
|