"""创建「商业情报收集链」3 个自定义席位智能体、业务链与定时绑定。 一次性联调脚本(可重复运行,幂等:已存在则更新/跳过): 1. POST/PUT /api/agents ×3 —— 用户级智能体(非内置 seeder),各绑本区块信息技能; 2. POST /api/roundtable-chains —— 业务链 biz-intel-chain(三阶段串行,避免详情并发覆盖); 3. PUT /api/roundtable-chains/{id} is_public=True —— 发布链(自动发布席位智能体); 4. POST /api/task-analysis/external/tasks —— 建任务 003(商业情报定时收集); 5. PUT /api/task-analysis/tasks/{id}/binding —— 绑定链 + cron 每小时整点; 6. 可选 `--trigger`:立即手动触发一轮(默认只配置,不触发生产接口)。 用法(backend 目录):PYTHONPATH=. uv run python scripts/setup_force_situation_chain.py """ from __future__ import annotations import argparse import json import sys import urllib.error import urllib.parse import urllib.request if hasattr(sys.stdout, "reconfigure"): sys.stdout.reconfigure(encoding="utf-8") BASE = "http://localhost:8001" USERNAME = "admin" PASSWORD = "123ewq" SOURCE_TASK_ID = "003" CHAIN_ID = "biz-intel-chain" CRON = "0 * * * *" AGENTS = [ { "id": "biz-insight-seat", "name": "商业情报接口采集智能体", "description": "商业情报链阶段1:读取独立任务详情,调用四类监测接口并由脚本转换、原子合并六个来源区块。", "skills": [ "fs-collect", "fs-task-detail-info", "fs-timeline-info", "fs-insight-info", "fs-target-info", "fs-connection-info", ], "soul": """# 商业情报接口采集智能体(biz-insight-seat) 你是商业情报收集链阶段 1 的自定义接口采集智能体。只执行下面一条命令: ```bash python /mnt/skills/custom/fs-collect/scripts/fs_collect.py --group source --task-id <任务号> ``` `--task-id` 取任务上下文里「数据交付 task_id(调用 task-data-submit 技能入库时的 --task-id 参数)」一行。 - 成功(stdout `"success": true`)后报告已写入的 submitted_keys; - 失败时重试一次,仍失败把 errors 原样回复; - 禁止向用户提问,禁止改写接口数据。请求参数、放宽条件和原始响应由脚本直接显示。 """, }, { "id": "biz-plan-seat", "name": "当前判断接口采集智能体", "description": "商业情报链阶段2:通过独立当前判断技能调用威胁事件接口,转换并原子合并 currentJudgments。", "skills": ["fs-collect", "fs-task-detail-info", "fs-current-judgment-info"], "soul": """# 当前判断接口采集智能体(biz-plan-seat) 你是商业情报收集链阶段 2 的自定义接口智能体。只执行: ```bash python /mnt/skills/custom/fs-collect/scripts/fs_collect.py --group judgment --task-id <任务号> ``` 脚本通过独立技能调用 GET eventMonitorByTime,固定传 discriminant=威胁,逐步放宽条件, 转换并原子合并 currentJudgments。成功后报告 submitted_keys;失败重试一次后原样报告 errors。 禁止提问、禁止改写接口结果、禁止回退到固定池或网络检索。 """, }, { "id": "biz-assembler-seat", "name": "情报摘要与校验智能体", "description": "商业情报链阶段3:基于前两阶段数据由脚本生成三类摘要,校验八个区块和关键来源标识。", "skills": ["fs-collect", "fs-brief-info", "fs-detail-read"], "soul": """# 情报摘要与校验智能体(biz-assembler-seat) 你是商业情报收集链阶段 3 的自定义摘要与校验智能体。前序阶段已串行完成,先执行: ```bash python /mnt/skills/custom/fs-collect/scripts/fs_collect.py --group summary --task-id <任务号> ``` 该命令基于真实采集结果和当前判断生成情报摘要并原子合并入库。随后读取详情校验。 ## 8 个顶层 key `situationBriefs` / `currentJudgments` / `importantTargets` / `importantTimeNodes` / `insightTopics` / `insightRisks` / `insightOpportunities` / `taskConnections` ## 流程 1. 读取详情:`python /mnt/skills/custom/fs-detail-read/scripts/fs_detail_read.py --task-id <任务号>` 2. 比对 8 个 key(区块为空数组/缺失都算缺); 3. 校验关键来源标识:时间节点保留 recUuid,三类事件保留 id/eventId,目标保留 eid,衔接任务保留 taskId; 4. 任一项缺失时明确报告,不伪造补齐;全部齐全后回复“校验通过(8/8 区块)”。 """, }, ] CHAIN = { "id": CHAIN_ID, "title": "商业情报收集链", "description": "商业态势页定时信息收集:来源接口采集、当前判断接口采集、情报摘要与校验三阶段串行,避免并发覆盖详情。", "seats": [ {"agent_id": "biz-insight-seat", "name": "接口数据采集", "description": "读取独立任务详情并采集时间节点、三类事件、重要目标和衔接任务"}, {"agent_id": "biz-plan-seat", "name": "当前判断采集", "description": "独立调用威胁事件接口并转换 currentJudgments"}, {"agent_id": "biz-assembler-seat", "name": "情报摘要与校验", "description": "由脚本生成三类摘要并校验八个区块及关键来源标识"}, ], "stages": [["biz-insight-seat"], ["biz-plan-seat"], ["biz-assembler-seat"]], "stage_goals": [ "接口采集:读取独立任务详情,依次调用监测接口并由脚本转换,六个来源区块一次原子合并入库", "当前判断:独立技能调用 eventMonitorByTime(discriminant=威胁),转换为 currentJudgments 后合并入库", "摘要与校验:基于全部已采集信息生成 situationBriefs,校验八个区块和关键来源 id", ], "coordinator_prompt": ( "严格按三个阶段串行执行:阶段 1 用 fs-collect --group source 采集接口数据;阶段 2 用 fs-collect --group judgment 采集当前判断;" "阶段 3 用 fs-collect --group summary 生成摘要并校验。接口数据由脚本转换,模型禁止改写。" "所有席位无人值守,禁止澄清。" ), "enabled": True, } def call(method: str, path: str, body: dict | None, token: str): headers = {"Content-Type": "application/json", "Accept": "application/json"} if token: headers["Authorization"] = f"Bearer {token}" request = urllib.request.Request( BASE + path, data=json.dumps(body).encode() if body is not None else None, 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: return error.code, json.loads(error.read() or b"{}") def main() -> int: parser = argparse.ArgumentParser(description="配置商业情报自定义智能体、业务链与定时绑定") parser.add_argument("--trigger", action="store_true", help="配置完成后立即触发一轮") args = parser.parse_args() 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") # 手工落盘的新技能保持 custom 分类;登记到当前用户并发布后才能绑定共享业务链席位。 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 ("fs-task-detail-info", "fs-current-judgment-info"): 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 # 0. 仅清理早期旧命名资源;不删除当前任务和执行历史。 for old_agent in ("fs-insight-seat", "fs-plan-seat", "fs-assembler-seat"): status, _ = call("DELETE", f"/api/agents/{old_agent}", None, token) print(f"清理旧智能体 {old_agent}: HTTP {status}") status, _ = call("DELETE", "/api/roundtable-chains/fs-situation-chain", None, token) print(f"清理旧业务链 fs-situation-chain: HTTP {status}") # 1. 智能体(幂等:存在则 PUT 同步,不存在则 POST 创建) for agent in AGENTS: body = { **{k: agent[k] for k in ("id", "name", "description", "soul")}, "published": True, "skills": agent["skills"], "tool_groups": ["web", "file:read", "file:write", "bash"], } status, existing = call("GET", f"/api/agents/{agent['id']}", None, token) if status == 200: status, updated = call("PUT", f"/api/agents/{agent['id']}", body, token) print(f"更新智能体 {agent['id']}: HTTP {status}") else: status, created = call("POST", "/api/agents", body, token) print(f"创建智能体 {agent['id']}: HTTP {status}") if status not in (200, 201): print(body if status != 200 else updated) return 1 # 2. 业务链(幂等:存在则 PUT 同步阶段目标等编排字段,不存在则创建) status, existing_chain = call("GET", f"/api/roundtable-chains/{CHAIN_ID}", None, token) if status == 200: status, updated_chain = call( "PUT", f"/api/roundtable-chains/{CHAIN_ID}", { "title": CHAIN["title"], "description": CHAIN["description"], "seats": CHAIN["seats"], "stages": CHAIN["stages"], "stage_goals": CHAIN["stage_goals"], "coordinator_prompt": CHAIN["coordinator_prompt"], "enabled": True, }, token, ) print(f"更新业务链: HTTP {status}") if status not in (200, 201): print(updated_chain) return 1 else: status, created = call("POST", "/api/roundtable-chains", CHAIN, token) print(f"创建业务链: HTTP {status}", created.get("id") or created) if status not in (200, 201): return 1 # 3. 发布链 status, chain = call("GET", f"/api/roundtable-chains/{CHAIN_ID}", None, token) if status == 200 and not chain.get("is_public"): status, updated = call("PUT", f"/api/roundtable-chains/{CHAIN_ID}", {"is_public": True}, token) print(f"发布业务链: HTTP {status}, 自动发布席位={updated.get('published_seat_agents')}") else: print("业务链已是公开状态") # 4. 任务 003(幂等:已存在跳过) status, detail = call("GET", f"/api/task-analysis/external/tasks/{SOURCE_TASK_ID}/detail", None, token) if status == 200: internal_id = detail["task"]["task_id"] print(f"任务已存在: {SOURCE_TASK_ID} → {internal_id}") else: status, created = call( "POST", "/api/task-analysis/external/tasks", {"source_task_id": SOURCE_TASK_ID, "name": "商业情报定时收集", "description": "商业态势页定时信息收集任务(每小时)"}, token, ) print(f"创建任务: HTTP {status}, task_id={created.get('task_id')}") if status not in (200, 201): print(created) return 1 internal_id = created["task_id"] # 5. 绑定链 + 每小时整点 binding = {"chain_id": CHAIN_ID, "cron": CRON, "timezone": "Asia/Shanghai", "enabled": True, "model": None, "coordinator_prompt": None} status, bound = call("PUT", f"/api/task-analysis/tasks/{internal_id}/binding", binding, token) print(f"绑定定时({CRON}): HTTP {status}, next_run_at={bound.get('binding', {}).get('next_run_at')}") if status not in (200, 201): print(bound) return 1 # 6. 默认不触发,避免真实监测接口尚未上线时产生失败执行。 if args.trigger: status, run = call("POST", f"/api/task-analysis/tasks/{internal_id}/trigger", {}, token) print(f"手动触发: HTTP {status}, run_id={run.get('run', {}).get('id')}, status={run.get('run', {}).get('status')}") else: print("未触发执行;需要立即运行时追加 --trigger") print(f"\n内部任务 id: {internal_id}") print(f"查询执行: GET /api/task-analysis/tasks/{internal_id}/runs") print(f"详情验证: GET /api/force-situation/tasks/{SOURCE_TASK_ID}/detail") return 0 if __name__ == "__main__": sys.exit(main())