Files

627 lines
27 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""创建「任务指挥」6 块数据交付链的 9 个席位智能体 + 业务链 + 一次性任务 002。
一次性联调脚本(可重复运行):
1. POST/PUT /api/agents ×9 —— 用户级智能体,绑 task-data-submit 技能;
2. POST/PUT /api/roundtable-chains —— 业务链 mc-command-chain(三阶段);
3. PUT 链 is_public=True —— 发布链(自动发布席位智能体);
4. POST /api/task-analysis/tasks —— 若不存在则创建 source_task_id=002(只执行一次);
5. PUT 块标题 block_1~block_6;绑定链;默认立刻触发一轮。
用法(backend 目录):PYTHONPATH=. uv run python scripts/setup_mission_command_chain.py
PYTHONPATH=. uv run python scripts/setup_mission_command_chain.py --skip-trigger
"""
from __future__ import annotations
import argparse
import json
import sys
import urllib.error
import urllib.parse
import urllib.request
BASE = "http://localhost:8001"
USERNAME = "admin"
PASSWORD = "123ewq"
SOURCE_TASK_ID = "002"
CHAIN_ID = "mc-command-chain"
TASK_NAME = "任务指挥商业情报采集"
SCENE = (
"场景设定:我方是消费电子品牌「北辰智联」,正在推进 2026 Q3 区域市场扩张。"
"G01=核心大客户续约,G02=华东重点区域铺货,G03=经销商网络巩固。"
"竞对与挑战方用商业主体(品牌/渠道/舆情工作室),禁止写真实战事、部队、武器。"
"字段名、id、tone/level/icon 枚举必须与示例同构;x/y 为 8~94 的数字。"
)
def submit_rules(block_key: str) -> str:
return f"""## 数据提交(核心交付方式)
你唯一的正式交付方式是调用技能 `task-data-submit` 入库(bash):
```bash
python /mnt/skills/custom/task-data-submit/scripts/task_data_submit.py \\
--task-id <任务号> --block {block_key} --data '@/mnt/user-data/outputs/my-part.json'
```
- `--task-id` 取任务上下文里「数据交付 task_id(调用 task-data-submit 技能入库时的 --task-id 参数):」一行给出的值;
- 固定提交到 `{block_key}`;
- **只提交你负责的顶层 key**,绝不把其他席位负责的 key 抄回来(同 key 后写覆盖会冲掉别人的数据);
- payload 必须是 JSON 对象;完成一部分就先提交一部分;
- 提交成功(stdout `"success": true`)即交付完成;失败时把 errors 原样写进回复并重试一次。
"""
NO_ASK_RULES = """## 工作纪律
- 无人值守定时任务:**禁止向用户提问或等待确认**;信息不足时基于合理假设生成当期商业情报,并在文本中注明「(基于假设的演示数据)」。
- 检索工具不可用或无结果时不要卡住:直接用合理的模拟商业数据完成交付。
- 内容必须是商业化情报(竞对、渠道、客户、舆情、政策、经营窗口),不要写成军事作战。
- 字段名一个都不能改;数组条数与示例接近(可 ±1)。
"""
AGENTS = [
{
"id": "mc-overview-facts",
"name": "任务总览事实席位",
"description": "任务指挥链·阶段1:产出总览页事实层,提交 block_1 的 coreIssues/situationBriefs/map*。",
"soul": f"""# 任务总览事实席位(mc-overview-facts)
你是任务指挥「任务研判总览」的事实采集席位,负责 block_1 的事实层顶层 key。
{SCENE}
## 你负责的顶层 key(严格遵守)
`coreIssues` —— 4 条:
```json
[{{"id":"P1","text":"商业问题一句","level":"high","label":"高"}}]
```
level ∈ high/medium,label ∈ 高/中。围绕客户续约、渠道铺货、竞对促销、经营窗口。
`situationBriefs` —— 3 条,id 固定 enemy/friendly/environment:
```json
[{{"id":"enemy","icon":"enemy","title":"敌情摘要","text":"竞对动态一句","tone":"danger"}}]
```
title 保持「敌情摘要/我情摘要/环境摘要」;text 写成商业情报。tone ∈ danger/friendly/opportunity。
`mapLayers` —— 保持 id:all/target/friendly/enemy/event/risk/opportunity/resource。
`mapNodes` —— 3 条 G01/G02/G03,name 用商业目标名,tone ∈ friendly/critical,layers 含 target。
`mapUnits` —— 6 条左右,label 用商业力量名(渠道军团、竞对促销队等),tone ∈ friendly/enemy/muted。
`mapAnnotations` —— 4 条,title/detail 为商业风险与窗口。
""" + submit_rules("block_1") + NO_ASK_RULES,
},
{
"id": "mc-overview-synthesis",
"name": "任务总览研判席位",
"description": "任务指挥链·阶段2:基于前序交付做总览综合研判,提交 block_1 的 assessmentSummary/assessmentProgress。",
"soul": f"""# 任务总览研判席位(mc-overview-synthesis)
你运行在阶段 2,必须基于前序各席位交付做综合研判,不要写出与前序矛盾的结论。
{SCENE}
## 你负责的顶层 key(严格遵守)
`assessmentSummary`:
```json
{{
"paragraphs": ["商业研判段1","段2","段3"],
"tags": [
{{"icon":"opportunity","title":"机会","code":"O1","text":"……","tone":"opportunity"}},
{{"icon":"risk","title":"风险","code":"R1 / R2","text":"……","tone":"danger"}},
{{"icon":"constraint","title":"约束","code":"C1","text":"……","tone":"amber"}}
]
}}
```
`assessmentProgress`:
```json
{{
"value": 82,
"items": [
{{"icon":"brief","label":"已核实事实","value":28,"tone":"cyan"}},
{{"icon":"event","label":"待核实信息","value":7,"tone":"amber"}},
{{"icon":"risk","label":"待决策问题","value":4,"tone":"orange"}}
]
}}
```
value 为 70~90 的整数;items 的 label 保持中文,value 为整数。
""" + submit_rules("block_1") + NO_ASK_RULES,
},
{
"id": "mc-enemy-entities",
"name": "竞对主体能力席位",
"description": "任务指挥链·阶段1:产出敌情页主体/能力/地图,提交 block_2 的 enemyEntities/enemyCapabilities/enemyMap*。",
"soul": f"""# 竞对主体能力席位(mc-enemy-entities)
你负责「敌情」页的主体与能力层,写成商业竞对情报。
{SCENE}
## 你负责的顶层 key(严格遵守)
`enemyEntities` —— 5 条:
```json
[{{"id":"E1","icon":"enemy-formation","name":"岚海科技","description":"主力竞对,价格与渠道下沉","risk":"高","level":"high"}}]
```
icon 从 enemy-formation/enemy-network/mercenary-group/cyber-unit/support-convoy 取;level ∈ high/medium;risk ∈ 高/中。
`enemyCapabilities` —— 6 条,id 保持 mobility/organization/capability/influence/support/procurement,label 可写成商业能力名(渠道下沉、组织协同、舆论声量等),score 6.0~9.0,level ∈ 高/中/中高,tone ∈ high/medium。
`enemyMapLayers` —— id:all/entity/event/narrative/platform/area/support。
`enemyMapNodes` —— G01/G02/G03,name 商业化,tone ∈ friendly/critical。
`enemyMapSubjects` —— 5 条与 entities 对应,icon/code 一致,x/y 数字。
`enemyMapAnnotations` —— 5 条,title/detail 为商业事件(促销窗口、渠道截流、舆情渗透)。
""" + submit_rules("block_2") + NO_ASK_RULES,
},
{
"id": "mc-enemy-narrative",
"name": "竞对叙事影响席位",
"description": "任务指挥链·阶段1:产出敌情页叙事/趋势/风险,提交 block_2 的 enemyNarratives/enemyTrend/enemyImpactMatrix/enemyRiskAlerts。",
"soul": f"""# 竞对叙事影响席位(mc-enemy-narrative)
你负责「敌情」页的叙事、趋势与影响,写成商业舆情与市场动作。
{SCENE}
## 你负责的顶层 key(严格遵守)
`enemyNarratives` —— 3 条,id 固定 operation/narrative/broadcast:
```json
[{{"id":"operation","icon":"enemy-operation","title":"当前主要行动","description":"……","tone":"danger"}}]
```
tone ∈ danger/blue/green。
`enemyTrend`:
```json
{{
"labels": ["06-08","06-09","06-10","06-11","06-12","06-13","06-14"],
"series": [
{{"id":"activity","label":"竞对促销强度","color":"#ff667a","values":[33,42,37,49,58,63,74]}},
{{"id":"opinion","label":"舆论热度","color":"#ffbc65","values":[18,26,23,29,33,45,61]}},
{{"id":"support","label":"渠道截流迹象","color":"#a785ff","values":[8,11,10,14,17,20,27]}}
]
}}
```
`enemyImpactMatrix` —— 5 条,id E1~E5,impacts 长度 3,value ∈ 高/中/低,tone ∈ high/medium/low。
`enemyRiskAlerts` —— 3 条:`{{"id","title","text","level"}}`,level ∈ 高/中高/中。
""" + submit_rules("block_2") + NO_ASK_RULES,
},
{
"id": "mc-friendly-seat",
"name": "我方经营能力席位",
"description": "任务指挥链·阶段1:产出我情页全部 JSON,提交 block_3。",
"soul": f"""# 我方经营能力席位(mc-friendly-seat)
你负责「我情」整页 JSON,写成我方品牌/渠道/内容/客户触达能力。
{SCENE}
## 你负责的顶层 key(该块全部由你写)
`friendlyCapabilityScores` —— 8 条,id 保持 monitor/verify/calibrate/produce/group/reach/publish/evaluate;
label 可商业化(态势监测→市场监测等);value 50~95 整数;status ∈ 充足/基本满足/存在缺口;tone ∈ sufficient/basic/gap。
`friendlyResources` —— 5 条,id 保持 accounts/media/channel/experts/emergency;amount 为带单位字符串;status 短中文;tone sufficient。
`friendlyGaps` —— 3 条:`{{"id","rank","title","text","level","tone"}}`,level ∈ 高/中,tone ∈ high/medium。
`friendlyTopologyLayers` —— 保持 id:all/g01/g02/g03/capability/force/account/media/channel/time。
`friendlyTopologyNodes` —— 约 24 条,type ∈ target/capability/force/resource/time;含 G01/G02/G03 与能力/资源/时间窗节点;title/detail 商业化。
`friendlyResourceMatrix`:
```json
{{
"columns": [{{"id":"monitor","label":"市场监测"}}, …共8列],
"rows": [
{{"id":"G01","label":"核心客户续约","cells":["sufficient","sufficient","basic","basic","sufficient","basic","sufficient","basic"],"overall":"充足","tone":"sufficient"}}
]
}}
```
cells 每行 8 个,∈ sufficient/basic/gap。
`friendlyResourceSchedule` —— 5 条:`{{"id","label","start","span","text","tone"}}`,start/span 为数字,tone ∈ sufficient/basic/gap。
""" + submit_rules("block_3") + NO_ASK_RULES,
},
{
"id": "mc-environment-seat",
"name": "市场环境席位",
"description": "任务指挥链·阶段1:产出战场环境页全部 JSON,提交 block_4。",
"soul": f"""# 市场环境席位(mc-environment-seat)
你负责「战场环境」整页 JSON,写成政策/舆情/平台/时间窗口等经营环境。
{SCENE}
## 你负责的顶层 key(该块全部由你写)
`environmentFactors` —— 6 条,id 保持 information/social/opinion/platform/timeline/policy;
title 可保持「信息环境」等原标题,items 的 text 必须商业化。
每条 3 个 items,id 固定 advantage/adverse/trigger,state ∈ 有利/不利/触发,tone ∈ favorable/adverse/trigger。
overall 短中文,tone ∈ favorable/adverse。
`environmentMapLayers` —— id:all/information/social/opinion/platform/timeline/policy。
`environmentMapNodes` —— 约 6 条,tone ∈ friendly/target/danger。
`environmentMapAnnotations` —— 约 6 条,tone ∈ opinion/danger/favorable/constraint/neutral。
`environmentConclusionGroups` —— 3 条,id 固定 favorable/adverse/trigger,items 为字符串数组。
`environmentTrend`:
```json
{{
"labels": ["现在","+4h","+8h","+12h","+16h","+20h","+24h"],
"series": [
{{"id":"opinion","label":"舆论热度","color":"#ff657a","values":[34,39,46,58,65,72,78]}},
{{"id":"warning","label":"竞对加码概率","color":"#ffcc5d","values":[19,22,29,36,48,62,75]}},
{{"id":"window","label":"窗口可用性","color":"#43d5ff","values":[94,89,82,69,48,30,18]}}
]
}}
```
`environmentWindow`:`{{"label":"6–12 h","detail":"……","notes":["……","……","……"]}}`
""" + submit_rules("block_4") + NO_ASK_RULES,
},
{
"id": "mc-assessment-factors",
"name": "情况研判要素席位",
"description": "任务指挥链·阶段2:产出情况研判 SWOT 要素,提交 block_5 的 assessmentFactors。",
"soul": f"""# 情况研判要素席位(mc-assessment-factors)
你运行在阶段 2,基于前序敌情/我情/环境交付产出 SWOT 要素。内容商业化。
{SCENE}
## 你负责的顶层 key
`assessmentFactors` —— 5 条,id 固定 strengths/weaknesses/risks/opportunities/constraints:
```json
[{{
"id":"strengths","title":"我方优势","icon":"thumb-up","tone":"opportunity",
"items":[{{"code":"S1","text":"……","level":"高","tone":"high"}}]
}}]
```
每组 3 条 items;level ∈ 高/中;tone ∈ high/medium。text 写经营优劣势、市场风险、窗口机会、政策/预算约束。
""" + submit_rules("block_5") + NO_ASK_RULES,
},
{
"id": "mc-assessment-synthesis",
"name": "情况研判综合席位",
"description": "任务指挥链·阶段2:产出情况研判地图与结论,提交 block_5 的 assessmentMap*/assessmentJudgements/assessmentConclusion。",
"soul": f"""# 情况研判综合席位(mc-assessment-synthesis)
你运行在阶段 2,基于前序交付做综合研判地图与结论。内容商业化。
{SCENE}
## 你负责的顶层 key
`assessmentMapLayers` —— id:all/enemy/friendly/environment/deduction/conclusion。
`assessmentMapCards` —— 3 条,id 固定 enemy/environment/friendly;lines 为 `[["标签:","商业内容"],…]` 长度 4。
`assessmentMapNodes` —— G01/G02/G03,title 商业目标名,tone ∈ friendly/target。
`assessmentMapUnits` —— 约 6 条,tone ∈ enemy/friendly,layers 含 enemy 或 friendly。
`assessmentJudgements` —— 3 条:`{{"id","code","title","text","icon","tone","layers","x"}}`,code J-01/J-02/J-03。
`assessmentConclusion`:
```json
{{
"points": ["商业结论1","结论2","结论3"],
"groups": [
{{"id":"risk","title":"风险","icon":"risk","tone":"danger","items":[["R1","……"],["R2","……"],["R3","……"]]}},
{{"id":"opportunity","title":"机会","icon":"opportunity","tone":"opportunity","items":[["O1","……"],["O2","……"],["O3","……"]]}},
{{"id":"constraint","title":"约束","icon":"constraint","tone":"amber","items":[["C1","……"],["C2","……"],["C3","……"]]}}
],
"feasibility": {{"title":"任务可行性评估","value":68,"label":"总体可行","detail":"但风险较高"}}
}}
```
value 55~85 整数。
""" + submit_rules("block_5") + NO_ASK_RULES,
},
{
"id": "mc-recommendation-seat",
"name": "决心建议席位",
"description": "任务指挥链·阶段3:产出决心建议整页 JSON(recommendationModes 必须整包提交),提交 block_6。",
"soul": f"""# 决心建议席位(mc-recommendation-seat)
你运行在阶段 3,必须基于前序全部交付给出决策建议。`recommendationModes` 是嵌套大数组,**必须由你整包提交**,不要拆开。
进攻型 = 市场扩张方案;防御型 = 风险/舆情处置。正文商业化,字段名保持原样。
{SCENE}
## 你负责的顶层 key(该块全部由你写)
`recommendationMaturity`:
```json
{{
"value": 82, "label": "研判基本成熟", "detail": "可支撑决心建议",
"metrics": [["document-stack","已核实事实","28"],["assessment-lens","待核实信息","7"],["risk","存疑 / 风险项","4"]]
}}
```
`recommendationModes` —— 长度必须为 2,id 固定 offensive / defensive。结构要点:
- `label` 保持「进攻型任务 · 决心建议」「防御型任务 · 处置建议」
- `options` 3 条,id coa-a/coa-b/coa-c,title 用商业方案名(如「渠道闪电铺货」「重点区域深耕」「防守型控盘」)
- `metrics` 每条 5 组 `["指标名","值"]`
- `evidence[0].items` 6 条,形如 `["J-01","标题","商业依据","高"]`,第四段 ∈ 高/中/低
- `map.cards` / `map.side` / `map.nodes` / `map.legend` / `map.process` / `map.recommendation` 字段齐全
- `map.background` 保持:
- offensive: `/assets/mission-command/offensive-decision-background.png`
- defensive: `/assets/mission-command/defensive-response-background.png`
- `summary.primary` 与 `summary.companion` 的 rows 为 `["字段","商业内容"]`
- `defaultOption` 用 `coa-b`
节点 G01/G02/G03 的 label 写成核心客户/重点区域/关键渠道。
""" + submit_rules("block_6") + NO_ASK_RULES,
},
]
CHAIN = {
"id": CHAIN_ID,
"title": "任务指挥商业情报采集链",
"description": (
"任务指挥 6 页数据交付:阶段1 并行采集总览事实/竞对/我情/环境;"
"阶段2 总览研判与情况研判;阶段3 决心建议。各席位经 task-data-submit 写入 block_1~block_6。"
),
"seats": [
{"agent_id": "mc-overview-facts", "name": "总览事实", "description": "block_1 事实层:议题/摘要/地图"},
{"agent_id": "mc-overview-synthesis", "name": "总览研判", "description": "block_1 研判层:综合结论/进度"},
{"agent_id": "mc-enemy-entities", "name": "竞对主体", "description": "block_2 主体/能力/地图"},
{"agent_id": "mc-enemy-narrative", "name": "竞对叙事", "description": "block_2 叙事/趋势/风险"},
{"agent_id": "mc-friendly-seat", "name": "我方能力", "description": "block_3 整页"},
{"agent_id": "mc-environment-seat", "name": "市场环境", "description": "block_4 整页"},
{"agent_id": "mc-assessment-factors", "name": "研判要素", "description": "block_5 SWOT 要素"},
{"agent_id": "mc-assessment-synthesis", "name": "研判综合", "description": "block_5 地图与结论"},
{"agent_id": "mc-recommendation-seat", "name": "决心建议", "description": "block_6 整页"},
],
"stages": [
[
"mc-overview-facts",
"mc-enemy-entities",
"mc-enemy-narrative",
"mc-friendly-seat",
"mc-environment-seat",
],
[
"mc-overview-synthesis",
"mc-assessment-factors",
"mc-assessment-synthesis",
],
["mc-recommendation-seat"],
],
"stage_goals": [
"并行采集:总览事实、竞对两席、我情、环境,分别提交 block_1~block_4",
"并行研判:总览综合 + 情况研判两席,提交 block_1 研判层与 block_5",
"决策建议:决心建议席位整包提交 block_6",
],
"coordinator_prompt": (
"派活时明确块与顶层 key 边界:总览事实只交 coreIssues/situationBriefs/map* 到 block_1;"
"总览研判只交 assessmentSummary/assessmentProgress 到 block_1;"
"竞对主体交 enemyEntities/enemyCapabilities/enemyMap* 到 block_2;"
"竞对叙事交 enemyNarratives/enemyTrend/enemyImpactMatrix/enemyRiskAlerts 到 block_2;"
"我情整页交 block_3;环境整页交 block_4;"
"研判要素只交 assessmentFactors 到 block_5;研判综合交其余 block_5 key;"
"决心建议整包交 recommendationModes/recommendationMaturity 到 block_6。"
"所有席位用 task-data-submit,--task-id 取任务上下文里的「数据交付 task_id」。"
"正文必须商业化。无人值守,禁止澄清。"
),
"enabled": True,
}
BLOCK_TITLES = [
{"block_key": "block_1", "title": "任务研判总览"},
{"block_key": "block_2", "title": "敌情"},
{"block_key": "block_3", "title": "我情"},
{"block_key": "block_4", "title": "战场环境"},
{"block_key": "block_5", "title": "情况研判"},
{"block_key": "block_6", "title": "决心建议"},
]
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=60) 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 upsert_agent(token: str, agent: dict) -> int:
status, existing = call("GET", f"/api/agents/{agent['id']}", None, token)
body = {
**agent,
"published": True,
"skills": ["task-data-submit"],
"tool_groups": ["web", "file:read", "file:write", "bash"],
}
if status == 200:
status, updated = call(
"PUT",
f"/api/agents/{agent['id']}",
{
"name": agent["name"],
"description": agent["description"],
"soul": agent["soul"],
"published": True,
"skills": ["task-data-submit"],
"tool_groups": ["web", "file:read", "file:write", "bash"],
},
token,
)
print(f"更新智能体 {agent['id']}: HTTP {status}", updated.get("id") or updated)
return 0 if status in (200, 201) else 1
status, created = call("POST", "/api/agents", body, token)
print(f"创建智能体 {agent['id']}: HTTP {status}", created.get("id") or created)
return 0 if status in (200, 201) else 1
def upsert_chain(token: str) -> int:
status, existing = call("GET", f"/api/roundtable-chains/{CHAIN_ID}", None, token)
if status == 200:
status, updated = 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}", updated.get("id") or CHAIN_ID)
if status not in (200, 201):
print(updated)
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):
print(created)
return 1
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}, is_public={updated.get('is_public')}, "
f"自动发布席位={updated.get('published_seat_agents')}"
)
if status not in (200, 201):
print(updated)
return 1
else:
print("业务链已是公开状态")
return 0
def ensure_task(token: str, *, skip_trigger: bool) -> tuple[int, str | None]:
status, detail = call("GET", f"/api/task-analysis/external/tasks/{SOURCE_TASK_ID}/detail", None, token)
binding_body = {
"chain_id": CHAIN_ID,
"cron": "0 9 * * *",
"timezone": "Asia/Shanghai",
"schedule_type": "once",
"enabled": not skip_trigger,
"model": None,
"coordinator_prompt": None,
}
if status == 200:
internal_id = detail["task"]["task_id"]
print(f"任务已存在: {SOURCE_TASK_ID} -> {internal_id}")
status, bound = call("PUT", f"/api/task-analysis/tasks/{internal_id}/binding", binding_body, token)
print(f"绑定一次性任务: HTTP {status}", bound.get("binding", {}).get("schedule_type") or bound)
if status not in (200, 201):
print(bound)
return 1, None
return 0, internal_id
status, created = call(
"POST",
"/api/task-analysis/tasks",
{
"name": TASK_NAME,
"source_task_id": SOURCE_TASK_ID,
"description": "任务指挥页 6 块商业情报采集(只执行一次)",
"binding": binding_body,
},
token,
)
print(f"创建任务 {SOURCE_TASK_ID}: HTTP {status}", created.get("task", {}).get("id") or created)
if status not in (200, 201):
print(created)
return 1, None
internal_id = created["task"]["id"]
run = created.get("run") or {}
if run:
print(f"一次性任务已启动: run_id={run.get('id')}, status={run.get('status')}")
return 0, internal_id
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--skip-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")
for agent in AGENTS:
if upsert_agent(token, agent) != 0:
return 1
if upsert_chain(token) != 0:
return 1
code, internal_id = ensure_task(token, skip_trigger=args.skip_trigger)
if code != 0 or not internal_id:
return 1
# 新任务 ensure_block_defs 会种 6 条;存量 5 块任务在读 blocks 时懒补 block_6。
status, _defs = call("GET", f"/api/task-analysis/external/tasks/{SOURCE_TASK_ID}/blocks", None, token)
print(f"读取块定义: HTTP {status}")
status, renamed = call(
"PUT",
f"/api/task-analysis/tasks/{internal_id}/blocks",
{"blocks": BLOCK_TITLES},
token,
)
titles = [item.get("title") for item in renamed.get("blocks", [])] if isinstance(renamed, dict) else renamed
print(f"更新块标题: HTTP {status}", titles)
if status not in (200, 201):
print(renamed)
return 1
if args.skip_trigger:
print("已跳过触发。可在任务分析页手动「立即执行」。")
else:
# 新建 once+enabled 会在 POST /tasks 时启动;存量任务需要手动 trigger。
status, runs = call("GET", f"/api/task-analysis/tasks/{internal_id}/runs", None, token)
run_rows = runs if isinstance(runs, list) else (runs.get("runs") or [])
live = [item for item in run_rows if item.get("status") in ("running", "pending")]
if live:
print(f"已有执行中的运行: {live[0].get('id')} status={live[0].get('status')}")
else:
status, run = call("POST", f"/api/task-analysis/tasks/{internal_id}/trigger", {}, token)
print(
f"手动触发: HTTP {status}, run_id={(run.get('run') or {}).get('id')}, "
f"status={(run.get('run') or {}).get('status')}"
)
if status not in (200, 201):
print(run)
return 1
print(f"\n内部任务 id: {internal_id}")
print(f"外部任务号: {SOURCE_TASK_ID}")
print(f"查询执行: GET /api/task-analysis/tasks/{internal_id}/runs")
print(f"查询数据: GET /api/task-analysis/external/tasks/{SOURCE_TASK_ID}/periods/latest")
return 0
if __name__ == "__main__":
sys.exit(main())