"""更新「商业QB助理」智能体并绑定 3q-0 配置位。 SOUL 从当前 DeerFlow 智能体目录读取,避免维护两份行为规则。运行前应先部署 ``skills/custom/task-context``、``skills/custom/task-detail-update`` 和智能体 SOUL。 用法(backend 目录):PYTHONPATH=. uv run python scripts/setup_biz_detail_assistant.py """ from __future__ import annotations import json import os import sys import urllib.error import urllib.parse import urllib.request from pathlib import Path if hasattr(sys.stdout, "reconfigure"): sys.stdout.reconfigure(encoding="utf-8") BASE = os.environ.get("DEERFLOW_SETUP_BASE", "http://localhost:8001").rstrip("/") USERNAME = os.environ.get("DEERFLOW_SETUP_USERNAME", "admin") PASSWORD = os.environ.get("DEERFLOW_SETUP_PASSWORD", "123ewq") AGENT_ID = "biz-detail-assistant" AGENT_NAME = "商业QB助理(全量检索版)" MAPPING_CODE = "3q-0" SKILLS = ["task-context", "task-detail-update"] TOOL_GROUPS = ["web", "file:read", "file:write", "bash"] DESCRIPTION = ( "任务详情页右侧问答智能体:结合用户输入、首轮任务四要素与相关上传文件进行分析或检索;" "“态势研判”固定覆盖态势、风险、战机三模块。情报摘要基于任务信息、相关文件证据与六个" "现有模块的压缩证据生成敌情、我情、环境三条快照。方案确认后原子写回,任务衔接不在范围内。" ) AGENT_DIR = Path(__file__).resolve().parents[1] / ".deer-flow" / "agents" / AGENT_ID SOUL_PATH = AGENT_DIR / "SOUL.md" def call(method: str, path: str, body: dict | None, token: str | None, *, form: dict | None = None): if body is not None and form is not None: raise ValueError("请求不能同时包含 JSON 与表单数据") headers = {"Accept": "application/json"} if form is not None: headers["Content-Type"] = "application/x-www-form-urlencoded" data = urllib.parse.urlencode(form).encode() else: headers["Content-Type"] = "application/json" data = json.dumps(body).encode() if body is not None else None if token: headers["Authorization"] = f"Bearer {token}" request = urllib.request.Request( BASE + path, data=data, headers=headers, method=method, ) try: with urllib.request.urlopen(request, timeout=30) as response: return response.status, json.loads(response.read() or b"{}") except urllib.error.HTTPError as error: raw = error.read().decode("utf-8", "replace") try: return error.code, json.loads(raw or b"{}") except json.JSONDecodeError: return error.code, {"raw": raw} def main() -> int: if not SOUL_PATH.is_file(): print(f"缺少当前智能体 SOUL,拒绝用旧模板覆盖:{SOUL_PATH}") return 1 soul = SOUL_PATH.read_text(encoding="utf-8") status, login = call( "POST", "/api/v1/auth/login/local", None, None, form={"username": USERNAME, "password": PASSWORD}, ) # 与两个任务详情技能保持一致:旧部署关闭密码登录、但仍开启 SSO 兼容直登时, # 仅对策略拒绝(403)回退;凭据错误、限流等失败不能绕过账号密码校验。 if status == 403: query = urllib.parse.urlencode({"password": PASSWORD}) status, login = call( "POST", f"/api/v1/auth/login/username?{query}", {"username": USERNAME}, None, ) if status != 200: print("登录失败", status, login) return 1 token = login["access_token"] print("登录 OK") status, reconciled = call("POST", "/api/skills/reconcile-custom", None, token) print(f"登记自定义技能: HTTP {status}, adopted={reconciled.get('adopted', [])}") if status not in (200, 201): return 1 for skill_name in SKILLS: status, published = call( "PUT", f"/api/skills/custom/{skill_name}/published", {"published": True}, token, ) print(f"发布自定义技能 {skill_name}: HTTP {status}, published={published.get('published')}") if status not in (200, 201): return 1 body = { "id": AGENT_ID, "name": AGENT_NAME, "description": DESCRIPTION, "soul": soul, "published": True, "skills": SKILLS, "tool_groups": TOOL_GROUPS, } status, _existing = call("GET", f"/api/agents/{AGENT_ID}", None, token) if status == 200: status, agent = call("PUT", f"/api/agents/{AGENT_ID}", body, token) action = "更新" else: status, agent = call("POST", "/api/agents", body, token) action = "创建" print(f"{action}智能体 {AGENT_ID}: HTTP {status}, skills={agent.get('skills')}") if status not in (200, 201): print(agent) return 1 status, mapping = call( "PUT", f"/api/business-mapping/{MAPPING_CODE}", {"agent_id": AGENT_ID, "agent_name": AGENT_NAME}, token, ) print(f"绑定配置位 {MAPPING_CODE}: HTTP {status}, agent_id={mapping.get('agent_id')}") if status not in (200, 201): print(mapping) return 1 return 0 if __name__ == "__main__": sys.exit(main())