sync: 鍚屾灞€鍩熺綉鏈€鏂颁唬鐮?(source master @ b69568e4)
This commit is contained in:
1 parent
4f7f07504a
commit
8bc8b0e082
1651 files changed
+278599
-11416
No files matched your search
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Summarize LLM_CONNECT_* records from a DeerFlow backend log."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
RUN_RE = re.compile(r"run_id=([^ ]+)")
|
||||
VALUE_RE = re.compile(r"(first_chunk_ms|total_ms)=([^ ]+)")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("log", nargs="?", type=Path, help="Log file; omit to read stdin")
|
||||
args = parser.parse_args()
|
||||
text = args.log.read_text(encoding="utf-8", errors="replace") if args.log else sys.stdin.read()
|
||||
|
||||
calls: dict[str, dict[str, str]] = {}
|
||||
for line in text.splitlines():
|
||||
if "LLM_" not in line:
|
||||
continue
|
||||
run_match = RUN_RE.search(line)
|
||||
if not run_match:
|
||||
continue
|
||||
run_id = run_match.group(1)
|
||||
row = calls.setdefault(run_id, {"run_id": run_id, "status": "started"})
|
||||
if "LLM_CONNECT_OK" in line:
|
||||
row["status"] = "connected"
|
||||
if "LLM_REQUEST_DONE" in line:
|
||||
row["status"] = "success"
|
||||
if "LLM_REQUEST_FAILED" in line:
|
||||
row["status"] = "failed"
|
||||
for key, value in VALUE_RE.findall(line):
|
||||
row[key] = value
|
||||
|
||||
print("run_id status first_chunk_ms total_ms")
|
||||
for row in calls.values():
|
||||
print(
|
||||
row["run_id"],
|
||||
row.get("status", "-"),
|
||||
row.get("first_chunk_ms", "-"),
|
||||
row.get("total_ms", "-"),
|
||||
)
|
||||
|
||||
succeeded = sum(row.get("status") == "success" for row in calls.values())
|
||||
failed = sum(row.get("status") == "failed" for row in calls.values())
|
||||
unfinished = len(calls) - succeeded - failed
|
||||
print(f"SUMMARY calls={len(calls)} success={succeeded} failed={failed} unfinished={unfinished}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,62 @@
|
||||
"""回填力量态势详情版本历史(一次脚本)。
|
||||
|
||||
对已有 force_situation_details 行但没有任何版本记录的任务,把当前文档补写为版本 1
|
||||
(created_at 取该行的 updated_at),保证时间轴有初始刻度。
|
||||
|
||||
用法(backend 目录):PYTHONPATH=. uv run python scripts/backfill_force_situation_versions.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
|
||||
async def main() -> int:
|
||||
from sqlalchemy import func, select
|
||||
|
||||
from deerflow.config import get_app_config
|
||||
from deerflow.persistence.engine import close_engine, get_session_factory, init_engine_from_config
|
||||
from deerflow.persistence.force_situation.model import (
|
||||
ForceSituationDetailRow,
|
||||
ForceSituationDetailVersionRow,
|
||||
)
|
||||
|
||||
config = get_app_config()
|
||||
await init_engine_from_config(config.database)
|
||||
sf = get_session_factory()
|
||||
try:
|
||||
async with sf() as session:
|
||||
rows = (await session.execute(select(ForceSituationDetailRow))).scalars().all()
|
||||
patched = 0
|
||||
for row in rows:
|
||||
count = (
|
||||
await session.execute(
|
||||
select(func.count())
|
||||
.select_from(ForceSituationDetailVersionRow)
|
||||
.where(ForceSituationDetailVersionRow.task_id == row.task_id)
|
||||
)
|
||||
).scalar() or 0
|
||||
if count:
|
||||
continue
|
||||
session.add(
|
||||
ForceSituationDetailVersionRow(
|
||||
task_id=row.task_id,
|
||||
version=1,
|
||||
detail_json=row.detail_json,
|
||||
updated_by=row.updated_by,
|
||||
created_at=row.updated_at,
|
||||
)
|
||||
)
|
||||
patched += 1
|
||||
print(f"回填 {row.task_id} → 版本 1")
|
||||
await session.commit()
|
||||
print(f"完成:回填 {patched} 个任务,跳过 {len(rows) - patched} 个(已有版本)")
|
||||
finally:
|
||||
await close_engine()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(asyncio.run(main()))
|
||||
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Measure DeerFlow model-factory latency without exposing prompts or secrets."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from langchain_core.messages import HumanMessage
|
||||
|
||||
from deerflow.config import get_app_config
|
||||
from deerflow.models import create_chat_model
|
||||
|
||||
|
||||
def _args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Diagnose DeerFlow LLM first-chunk and completion latency")
|
||||
parser.add_argument("--model", required=True, help="DeerFlow config model name")
|
||||
parser.add_argument("--prompt", default="请先思考,然后只回答:17乘以23等于多少?")
|
||||
parser.add_argument("--prompt-file", type=Path, help="UTF-8 prompt file; content is never printed")
|
||||
parser.add_argument("--repeat", type=int, default=1, help="Repeat the selected prompt N times")
|
||||
parser.add_argument("--max-tokens", type=int, default=256)
|
||||
parser.add_argument("--thinking", action=argparse.BooleanOptionalAction, default=True)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def _reasoning_text(chunk: Any) -> str:
|
||||
kwargs = getattr(chunk, "additional_kwargs", None) or {}
|
||||
value = kwargs.get("reasoning_content") or kwargs.get("reasoning")
|
||||
return value if isinstance(value, str) else ""
|
||||
|
||||
|
||||
def _content_text(chunk: Any) -> str:
|
||||
content = getattr(chunk, "content", "")
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
if isinstance(content, list):
|
||||
return "".join(part.get("text", "") for part in content if isinstance(part, dict))
|
||||
return str(content or "")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = _args()
|
||||
prompt = args.prompt_file.read_text(encoding="utf-8") if args.prompt_file else args.prompt
|
||||
prompt *= max(args.repeat, 1)
|
||||
|
||||
config = get_app_config()
|
||||
model_config = config.get_model_config(args.model)
|
||||
if model_config is None:
|
||||
print(f"ERROR model_not_found={args.model}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
print(
|
||||
"CONFIG "
|
||||
+ json.dumps(
|
||||
{
|
||||
"name": model_config.name,
|
||||
"use": model_config.use,
|
||||
"base_url": str(model_config.base_url or ""),
|
||||
"request_timeout": model_config.request_timeout,
|
||||
"max_retries": model_config.max_retries,
|
||||
"supports_thinking": model_config.supports_thinking,
|
||||
"when_thinking_enabled": model_config.when_thinking_enabled,
|
||||
"prompt_chars": len(prompt),
|
||||
},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
flush=True,
|
||||
)
|
||||
|
||||
build_started = time.monotonic()
|
||||
model = create_chat_model(args.model, thinking_enabled=args.thinking)
|
||||
build_ms = int((time.monotonic() - build_started) * 1000)
|
||||
print(
|
||||
"MODEL "
|
||||
+ json.dumps(
|
||||
{
|
||||
"runtime_class": f"{type(model).__module__}.{type(model).__name__}",
|
||||
"build_ms": build_ms,
|
||||
}
|
||||
),
|
||||
flush=True,
|
||||
)
|
||||
|
||||
messages = [HumanMessage(content=prompt)]
|
||||
if hasattr(model, "_get_request_payload"):
|
||||
payload = model._get_request_payload(messages, max_tokens=args.max_tokens)
|
||||
safe_payload = dict(payload)
|
||||
safe_payload["messages"] = [
|
||||
{
|
||||
"role": item.get("role"),
|
||||
"content_chars": len(str(item.get("content") or "")),
|
||||
"has_tools": bool(item.get("tool_calls")),
|
||||
}
|
||||
for item in payload.get("messages", [])
|
||||
]
|
||||
print("PAYLOAD " + json.dumps(safe_payload, ensure_ascii=False, default=str), flush=True)
|
||||
|
||||
started = time.monotonic()
|
||||
first_chunk_ms: int | None = None
|
||||
first_reasoning_ms: int | None = None
|
||||
first_content_ms: int | None = None
|
||||
chunks = 0
|
||||
reasoning_chars = 0
|
||||
content_chars = 0
|
||||
usage: dict[str, Any] = {}
|
||||
|
||||
try:
|
||||
for chunk in model.stream(messages, max_tokens=args.max_tokens):
|
||||
chunks += 1
|
||||
now_ms = int((time.monotonic() - started) * 1000)
|
||||
if first_chunk_ms is None:
|
||||
first_chunk_ms = now_ms
|
||||
print(f"FIRST_CHUNK elapsed_ms={now_ms}", flush=True)
|
||||
|
||||
reasoning = _reasoning_text(chunk)
|
||||
content = _content_text(chunk)
|
||||
if reasoning:
|
||||
reasoning_chars += len(reasoning)
|
||||
if first_reasoning_ms is None:
|
||||
first_reasoning_ms = now_ms
|
||||
print(f"FIRST_REASONING elapsed_ms={now_ms}", flush=True)
|
||||
if content:
|
||||
content_chars += len(content)
|
||||
if first_content_ms is None:
|
||||
first_content_ms = now_ms
|
||||
print(f"FIRST_CONTENT elapsed_ms={now_ms}", flush=True)
|
||||
|
||||
chunk_usage = getattr(chunk, "usage_metadata", None)
|
||||
if chunk_usage:
|
||||
usage = dict(chunk_usage)
|
||||
except Exception as exc:
|
||||
total_ms = int((time.monotonic() - started) * 1000)
|
||||
print(
|
||||
"FAILED "
|
||||
+ json.dumps(
|
||||
{"total_ms": total_ms, "error_type": type(exc).__name__, "error": str(exc)},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
return 1
|
||||
|
||||
total_ms = int((time.monotonic() - started) * 1000)
|
||||
print(
|
||||
"RESULT "
|
||||
+ json.dumps(
|
||||
{
|
||||
"status": "success",
|
||||
"first_chunk_ms": first_chunk_ms,
|
||||
"first_reasoning_ms": first_reasoning_ms,
|
||||
"first_content_ms": first_content_ms,
|
||||
"total_ms": total_ms,
|
||||
"chunks": chunks,
|
||||
"reasoning_chars": reasoning_chars,
|
||||
"content_chars": content_chars,
|
||||
"usage": usage,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
flush=True,
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -31,6 +31,8 @@ BUSINESS_TABLES = [
|
||||
"feedback",
|
||||
"agents",
|
||||
"skills",
|
||||
"ai_writing_article_types",
|
||||
"ai_writing_sessions",
|
||||
"scheduled_tasks",
|
||||
"scheduled_task_subscriptions",
|
||||
"scheduled_task_runs",
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
"""Opt-in live probe using synthetic input only; never prints keys or provider bodies.
|
||||
|
||||
Run from backend: .venv/Scripts/python scripts/probe_context_recovery.py --live --model NAME --rows 40000
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
|
||||
from langchain.agents.middleware.types import ModelRequest, ModelResponse
|
||||
from langchain_core.messages import HumanMessage, message_chunk_to_message
|
||||
|
||||
from deerflow.agents.middlewares.context_window_guard_middleware import ContextWindowGuardMiddleware, estimate_model_request_tokens
|
||||
from deerflow.agents.middlewares.llm_error_handling_middleware import is_llm_error_message
|
||||
from deerflow.config.app_config import get_app_config
|
||||
from deerflow.models.context_recovery import CONTEXT_RECOVERY_MESSAGE, is_context_length_error
|
||||
from deerflow.models.factory import create_chat_model
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--live", action="store_true", required=True)
|
||||
parser.add_argument("--model", required=True)
|
||||
parser.add_argument("--rows", type=int, default=100)
|
||||
parser.add_argument("--stream", action="store_true")
|
||||
args = parser.parse_args()
|
||||
if not 0 <= args.rows <= 100000:
|
||||
parser.error("rows must be between 0 and 100000")
|
||||
logging.disable(logging.CRITICAL)
|
||||
started = time.monotonic()
|
||||
attempts = []
|
||||
report = {"model": args.model, "rows": args.rows, "stream": args.stream, "attempts": attempts}
|
||||
try:
|
||||
config = get_app_config().model_copy(deep=True)
|
||||
selected = config.get_model_config(args.model)
|
||||
selected.max_retries = 0
|
||||
selected.request_timeout = 90
|
||||
selected.max_tokens = 1024
|
||||
model = create_chat_model(name=args.model, app_config=config, thinking_enabled=False, force_disable_thinking=True)
|
||||
text = "以下是合成测试材料。只回复:验证通过。\n" + "".join(f"记录{i:06d}:甲乙丙丁,气温正常。\n" for i in range(args.rows)) + "\n只回复:验证通过。"
|
||||
request = ModelRequest(model=model, messages=[HumanMessage(content=text)])
|
||||
|
||||
def invoke(current):
|
||||
attempt = {"estimated_input": estimate_model_request_tokens(current)}
|
||||
attempts.append(attempt)
|
||||
try:
|
||||
messages = ([current.system_message] if current.system_message else []) + current.messages
|
||||
if args.stream:
|
||||
chunks = list(model.stream(messages, **current.model_settings))
|
||||
response = message_chunk_to_message(sum(chunks[1:], chunks[0]))
|
||||
else:
|
||||
response = model.invoke(messages, **current.model_settings)
|
||||
attempt["outcome"] = "response"
|
||||
return ModelResponse(result=[response])
|
||||
except Exception as exc:
|
||||
attempt["outcome"] = "context_length" if is_context_length_error(exc) else type(exc).__name__
|
||||
body = getattr(exc, "body", None)
|
||||
if isinstance(body, dict):
|
||||
error = body.get("error", body)
|
||||
if isinstance(error, dict):
|
||||
attempt["provider_code"] = error.get("code")
|
||||
attempt["provider_message"] = str(error.get("message", ""))[:500]
|
||||
raise
|
||||
|
||||
response = ContextWindowGuardMiddleware().wrap_model_call(request, invoke)
|
||||
message = response.result[0] if isinstance(response, ModelResponse) else response
|
||||
text = message.text.strip()
|
||||
text = re.sub(r"<think>.*?</think>", "", text, flags=re.S).strip()
|
||||
if "<think>" in text:
|
||||
text = text.split("<think>", 1)[0].strip()
|
||||
report["outcome"] = "answered" if "验证通过" in text and not is_llm_error_message(message) else "clarification" if text == CONTEXT_RECOVERY_MESSAGE else "unexpected_response"
|
||||
report["visible_chars"] = len(text)
|
||||
except Exception as exc:
|
||||
report["outcome"] = type(exc).__name__
|
||||
report["elapsed_seconds"] = round(time.monotonic() - started, 2)
|
||||
print(json.dumps(report, ensure_ascii=True))
|
||||
return 0 if report["outcome"] == "answered" else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,132 @@
|
||||
"""Opt-in real-model multi-turn probe with checkpointing and accelerated summary triggers.
|
||||
|
||||
Run from backend: .venv/Scripts/python scripts/probe_multiturn_compression.py --live
|
||||
Only synthetic project facts are sent. Config changes stay inside this process.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
|
||||
from langchain.agents import create_agent
|
||||
from langchain_core.messages import HumanMessage
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
|
||||
from deerflow.agents.lead_agent.agent import _create_summarization_middleware
|
||||
from deerflow.agents.middlewares.context_window_guard_middleware import ContextWindowGuardMiddleware
|
||||
from deerflow.agents.middlewares.llm_error_handling_middleware import is_llm_error_message
|
||||
from deerflow.agents.middlewares.summarization_middleware import _clear_global_summary_cache
|
||||
from deerflow.agents.thread_state import ThreadState
|
||||
from deerflow.config.app_config import get_app_config
|
||||
from deerflow.models.factory import create_chat_model
|
||||
|
||||
|
||||
def visible_text(message):
|
||||
text = re.sub(r"<think>.*?</think>", "", message.text, flags=re.S | re.I)
|
||||
return re.split(r"<think>", text, flags=re.I)[0].strip()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--live", action="store_true", required=True)
|
||||
parser.add_argument("--model", default="MiniMax-M2.7")
|
||||
parser.add_argument("--output", default=None)
|
||||
args = parser.parse_args()
|
||||
logging.disable(logging.CRITICAL)
|
||||
config = get_app_config().model_copy(deep=True)
|
||||
selected = config.get_model_config(args.model)
|
||||
selected.max_retries = 0
|
||||
selected.request_timeout = 90
|
||||
selected.max_tokens = 2048
|
||||
# Lower only the summary trigger for this probe; never invent an input ceiling.
|
||||
from deerflow.config.summarization_config import ContextSize
|
||||
|
||||
config.summarization.trigger = [ContextSize(type="messages", value=8)]
|
||||
config.summarization.keep = ContextSize(type="messages", value=2)
|
||||
config.summarization.model_name = args.model
|
||||
main_model = create_chat_model(name=args.model, app_config=config, thinking_enabled=False, force_disable_thinking=True)
|
||||
prompts = [
|
||||
"请记住本次项目:代号 ORBIT-42;预算35万元;截止日期2026-12-20。只回复已记住。",
|
||||
"补充范围:系统需支持资料检索、问答和报告导出。只回复已记住。",
|
||||
"补充约束:不能上传真实客户数据,只能用合成测试材料。只回复已记住。",
|
||||
"补充验收项:检查引用来源、输出格式和接口稳定性。只回复已记住。",
|
||||
"请仅用一句话复述项目代号、预算和截止日期。",
|
||||
"更新项目预算为48万元,代号和其他信息保持不变。只回复已更新。",
|
||||
"补充约束:报告必须用中文,先写结论再写依据。只回复已记住。",
|
||||
"更新截止日期为2026-12-28,其他信息不变。只回复已更新。",
|
||||
"验收补充:必须覆盖多轮问答和长材料。只回复已记住。",
|
||||
"继续补充:需要留存测试报告,不能删除已有聊天记录。只回复已记住。",
|
||||
"补充:测试使用合成材料,不更改刚才确认的代号、预算和日期。只回复已记住。",
|
||||
"请仅输出JSON,包含code、budget、deadline,分别给出当前项目代号、最新预算(万元数字)、最新截止日期。",
|
||||
]
|
||||
saver = InMemorySaver()
|
||||
rows = []
|
||||
started = time.monotonic()
|
||||
report = {"model": args.model, "trigger_messages": 8, "keep_messages": 2, "rows": rows}
|
||||
_clear_global_summary_cache()
|
||||
final = None
|
||||
try:
|
||||
for turn, prompt in enumerate(prompts, 1):
|
||||
if turn == 9:
|
||||
# Emulate a worker restart: only the persisted checkpoint survives.
|
||||
_clear_global_summary_cache()
|
||||
middleware = _create_summarization_middleware(app_config=config, force_enabled=True)
|
||||
action = {"value": "passthrough"}
|
||||
original_plan = middleware._plan_compression
|
||||
|
||||
def inspect_plan(request):
|
||||
plan = original_plan(request)
|
||||
action["value"] = plan[0] if plan else "passthrough"
|
||||
return plan
|
||||
|
||||
middleware._plan_compression = inspect_plan
|
||||
agent = create_agent(model=main_model, middleware=[middleware, ContextWindowGuardMiddleware()], state_schema=ThreadState, checkpointer=saver)
|
||||
turn_started = time.monotonic()
|
||||
previous_ids = set((final.get("context_summary_cache") or {}).get("summarized_ids", [])) if final else set()
|
||||
final = agent.invoke({"messages": [HumanMessage(content=prompt)]}, {"configurable": {"thread_id": "synthetic-multiturn-probe"}})
|
||||
message = final["messages"][-1]
|
||||
visible = visible_text(message)
|
||||
row = {
|
||||
"turn": turn,
|
||||
"compression": action["value"],
|
||||
"history_messages": len(final["messages"]),
|
||||
"has_cache": bool(final.get("context_summary_cache")),
|
||||
"visible": visible[:500],
|
||||
"elapsed_seconds": round(time.monotonic() - turn_started, 2),
|
||||
}
|
||||
row["summary_updated"] = set((final.get("context_summary_cache") or {}).get("summarized_ids", [])) != previous_ids
|
||||
if row["summary_updated"]:
|
||||
row["summary_text"] = final["context_summary_cache"]["summary_text"]
|
||||
rows.append(row)
|
||||
print(json.dumps({key: value for key, value in row.items() if key != "summary_text"}, ensure_ascii=True), flush=True)
|
||||
if not visible or is_llm_error_message(message):
|
||||
raise RuntimeError("empty_or_error_answer")
|
||||
if len(final["messages"]) != turn * 2:
|
||||
raise RuntimeError("history_was_lost")
|
||||
if action["value"] == "summarize" and not row["summary_updated"]:
|
||||
raise RuntimeError("summary_did_not_complete")
|
||||
if turn == 5 and not all(value in visible for value in ("ORBIT-42", "35", "2026")):
|
||||
raise RuntimeError("first_summary_lost_project_facts")
|
||||
text = visible_text(final["messages"][-1])
|
||||
parsed = json.loads(re.search(r"\{.*\}", text, flags=re.S).group())
|
||||
report["fact_check"] = parsed.get("code") == "ORBIT-42" and str(parsed.get("budget")) == "48" and parsed.get("deadline") == "2026-12-28"
|
||||
report["summary_rounds"] = [row["turn"] for row in rows if row["compression"] == "summarize"]
|
||||
report["reuse_rounds"] = [row["turn"] for row in rows if row["compression"] == "reuse"]
|
||||
report["outcome"] = "passed" if report["fact_check"] and len(report["summary_rounds"]) >= 3 and report["reuse_rounds"] else "failed"
|
||||
except Exception as exc:
|
||||
report["outcome"] = type(exc).__name__
|
||||
if str(exc) in {"empty_or_error_answer", "history_was_lost", "summary_did_not_complete", "first_summary_lost_project_facts"}:
|
||||
report["failure_reason"] = str(exc)
|
||||
report["elapsed_seconds"] = round(time.monotonic() - started, 2)
|
||||
if args.output:
|
||||
from pathlib import Path
|
||||
|
||||
Path(args.output).write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(json.dumps({key: value for key, value in report.items() if key != "rows"}, ensure_ascii=True), flush=True)
|
||||
return 0 if report["outcome"] == "passed" else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,397 @@
|
||||
"""预存「商业态势总览」演示数据(task-data 数据交付 · 第一块对接)。
|
||||
|
||||
对齐前端页面(code-1 仓库)``#/situation-overview/action/business-overview?positionId=planning``
|
||||
的 mock 逻辑(``features/business-overview/mock/businessTimelineSnapshots.js``):
|
||||
|
||||
- 默认任务 ``BUSINESS-OVERVIEW-DEMO``(对方系统任务号,前端对接用它);
|
||||
- 13 期时间轴刻度 = 页面小时刻度:2026-06-17 22:00 → 2026-06-18 10:00(每小时一期,
|
||||
Beijing;最后一期为「当前期」);
|
||||
- 每期 ``block_1`` payload = 页面 ``createBusinessTimelineSnapshot`` 同构 JSON
|
||||
(整页 dashboard 对象,前端拿到即可直接渲染),按同一 slot 公式选相位;
|
||||
- ``block_1`` 标题置为「商业态势总览」,其余块保持默认。
|
||||
|
||||
幂等:重复运行会先级联删除演示任务再重建(不影响其它任务)。
|
||||
|
||||
用法(在 backend 目录)
|
||||
======================
|
||||
::
|
||||
|
||||
PYTHONPATH=. uv run python scripts/seed_business_overview_demo.py # 预存
|
||||
PYTHONPATH=. uv run python scripts/seed_business_overview_demo.py --dry-run # 只打印不写库
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
||||
logger = logging.getLogger("seed_business_overview_demo")
|
||||
|
||||
SOURCE_TASK_ID = "BUSINESS-OVERVIEW-DEMO"
|
||||
TZ_BEIJING = timezone(timedelta(hours=8))
|
||||
# 前端 BUSINESS_TIMELINE_ANCHOR = new Date(2026, 5, 18, 10, 30)(本地时区,页面按北京时区使用)。
|
||||
ANCHOR = datetime(2026, 6, 18, 10, 30, tzinfo=TZ_BEIJING)
|
||||
# 页面小时刻度:13 个刻度、终点 = 锚点整点,逐小时回退(与截图范围一致)。
|
||||
TICK_COUNT = 13
|
||||
|
||||
|
||||
def timeline_ticks() -> list[datetime]:
|
||||
end = ANCHOR.replace(minute=0, second=0, microsecond=0)
|
||||
return [end - timedelta(hours=TICK_COUNT - 1 - index) for index in range(TICK_COUNT)]
|
||||
|
||||
|
||||
# ── 前端 dashboardViews.overview(BusinessOverviewDashboard.vue,逐字对齐) ────
|
||||
|
||||
BASE_DASHBOARD: dict = {
|
||||
"leftTitle": "经营概览",
|
||||
"signalTitle": "经营信号",
|
||||
"actionTitle": "本月重点动作",
|
||||
"watchTitle": "经营关注",
|
||||
"pageTitle": "商业态势总览",
|
||||
"pageDescription": "围绕收入增长、回款效率与商机质量,实时掌握经营节奏。",
|
||||
"briefs": [
|
||||
{"title": "当前判断", "label": "经营主线", "description": "收入增长保持稳定,当前工作重点从获取增量转向提升重点项目的签约确定性。", "tone": "blue"},
|
||||
{"title": "协同重点", "label": "本周推进", "description": "销售、交付与客户成功团队围绕到期客户建立共跟机制,确保信息与行动同步。", "tone": "green"},
|
||||
{"title": "需持续关注", "label": "节奏风险", "description": "部分项目的审批周期仍有不确定性,应提前准备替代推进路径。", "tone": "amber"},
|
||||
],
|
||||
"objectives": [
|
||||
{"name": "年度收入目标", "label": "核心目标", "category": "财务增长", "description": "保持收入目标达成率在 82% 以上,确保二季度末进入集中签约窗口。", "level": "core"},
|
||||
{"name": "重点客户续约", "label": "关键目标", "category": "客户经营", "description": "提前锁定 12 个到期项目的续约与增购机会,提升存量收入质量。", "level": "key"},
|
||||
{"name": "回款效率提升", "label": "潜力目标", "category": "现金流", "description": "分层推进逾期账款治理,保障季度现金流目标稳定达成。", "level": "potential"},
|
||||
],
|
||||
"events": [],
|
||||
"operatingMetrics": [
|
||||
{"label": "年度确认收入", "value": "¥128.6M", "change": "+18.4%", "tone": "positive"},
|
||||
{"label": "回款达成率", "value": "94.7%", "change": "+3.1pct", "tone": "positive"},
|
||||
{"label": "活跃客户数", "value": "286", "change": "+22", "tone": "positive"},
|
||||
{"label": "重点商机金额", "value": "¥348.2M", "change": "环比 +9.6%", "tone": "neutral"},
|
||||
],
|
||||
"headlineMetrics": [
|
||||
{"label": "年度收入目标", "value": "¥156.0M", "change": "达成 82.4%", "tone": "positive"},
|
||||
{"label": "综合毛利率", "value": "31.8%", "change": "同比 +1.7pct", "tone": "positive"},
|
||||
{"label": "销售费用率", "value": "12.6%", "change": "预算内", "tone": "neutral"},
|
||||
],
|
||||
"businessNarratives": [],
|
||||
"insights": [],
|
||||
"priorities": [
|
||||
{"index": "01", "title": "华东渠道拓展", "description": "完成 3 家核心伙伴的联合方案签约,扩大区域覆盖。", "status": "推进中", "tone": "blue"},
|
||||
{"index": "02", "title": "大客户续约提升", "description": "聚焦 12 个到期项目,提前锁定续约与增购机会。", "status": "重点", "tone": "amber"},
|
||||
{"index": "03", "title": "回款专项治理", "description": "分层跟进逾期账款,确保季度现金流目标达成。", "status": "进行中", "tone": "blue"},
|
||||
],
|
||||
"signals": [
|
||||
{"title": "商机储备充足", "description": "中高概率商机覆盖未来两个季度收入目标的 2.2 倍。", "tone": "positive"},
|
||||
{"title": "产品组合持续优化", "description": "高毛利订阅业务占比提升至 46%,盈利结构更加稳健。", "tone": "positive"},
|
||||
{"title": "客户活跃度提升", "description": "核心客户月活与增购意向同步上行,续费基础良好。", "tone": "blue"},
|
||||
],
|
||||
"watchItems": [
|
||||
{"title": "两项重点商机审批周期延长", "description": "预计影响 ¥16.8M 的当季确认节奏,已进入管理层协同跟进。", "level": "中风险", "tone": "medium"},
|
||||
{"title": "区域回款进度存在差异", "description": "西南区域 30 天以上应收占比高于整体均值 4.2 个百分点。", "level": "关注", "tone": "low"},
|
||||
],
|
||||
"visualization": {"type": "trend", "title": "收入与经营目标趋势"},
|
||||
"chart": {
|
||||
"title": "收入与经营目标趋势",
|
||||
"actualName": "确认收入",
|
||||
"targetName": "经营目标",
|
||||
"barName": "毛利率",
|
||||
"ariaLabel": "近十二个月确认收入、经营目标与毛利率趋势图",
|
||||
"leftAxisName": "收入(百万元)",
|
||||
"rightAxisName": "毛利率",
|
||||
"months": ["7月", "8月", "9月", "10月", "11月", "12月", "1月", "2月", "3月", "4月", "5月", "6月"],
|
||||
"actual": [7.2, 8.1, 8.8, 9.6, 10.4, 11.8, 8.7, 9.3, 10.5, 11.2, 12.1, 13.4],
|
||||
"target": [7.5, 8.3, 9.1, 9.8, 10.7, 12.2, 9.0, 9.6, 10.9, 11.5, 12.6, 13.0],
|
||||
"bar": [28.1, 28.7, 29.2, 30.1, 29.8, 30.6, 30.2, 30.9, 31.3, 31.1, 31.6, 31.8],
|
||||
"leftAxisMax": 16,
|
||||
"leftAxisInterval": 4,
|
||||
"rightAxisMin": 20,
|
||||
"rightAxisMax": 40,
|
||||
"rightAxisInterval": 5,
|
||||
"rightAxisSuffix": "%",
|
||||
},
|
||||
}
|
||||
|
||||
# ── 前端 timelinePhases(businessTimelineSnapshots.js,逐字对齐) ─────────────
|
||||
|
||||
TIMELINE_PHASES: list[dict] = [
|
||||
{
|
||||
"name": "实时校准",
|
||||
"summary": "重点项目签约口径完成最新校准,经营节奏保持在目标区间。",
|
||||
"chartDelta": 0,
|
||||
"marginDelta": 0,
|
||||
"briefs": [
|
||||
{"label": "实时判断", "description": "重点项目签约口径完成最新校准,确认收入与经营目标之间的差距继续收窄。"},
|
||||
{"label": "联合跟进", "description": "销售、交付与客户成功团队已同步最新客户反馈,关键行动进入同频推进。"},
|
||||
{"label": "审批观察", "description": "两项重点商机仍等待客户内部审批,需要保留替代推进路径与资源窗口。"},
|
||||
],
|
||||
"objectives": [
|
||||
{"description": "根据最新签约反馈,将年度收入目标达成率稳定在 82% 以上,并锁定季度末确认窗口。"},
|
||||
{"description": "完成 12 个重点续约项目的责任人与下一步动作校准,优先保障高价值客户。"},
|
||||
{"description": "按账龄与区域更新回款清单,保障本月现金流目标可追踪、可纠偏。"},
|
||||
],
|
||||
"events": [
|
||||
{"title": "重点商机口径完成实时校准", "content": "华东 3 个渠道项目更新商务状态,预计新增确认收入 ¥8.6M。", "topic": "签约推进"},
|
||||
{"title": "续约客户反馈同步至共跟清单", "content": "客户成功团队补齐关键使用场景,销售侧已更新下一次沟通动作。", "topic": "客户经营"},
|
||||
{"title": "区域回款风险完成重新分层", "content": "西南区域两笔应收进入专项协同,责任人与预计到账节点已明确。", "topic": "现金流"},
|
||||
],
|
||||
"narratives": [
|
||||
{"title": "收入节奏", "value": "健康", "description": "最新确认口径显示,重点项目仍按季度目标窗口稳定推进。"},
|
||||
{"title": "客户结构", "value": "优化", "description": "订阅与增购机会占比继续提升,收入质量保持改善趋势。"},
|
||||
{"title": "资金回笼", "value": "可控", "description": "高风险应收已进入专项协同,当前未形成新增重大敞口。"},
|
||||
],
|
||||
"insights": [
|
||||
{"label": "增长动能", "highlight": "华东渠道与重点客户续约", "description": "仍是当前最确定的增量来源。"},
|
||||
{"label": "即时建议", "highlight": "维持重点项目逐单校准,", "description": "避免审批变化影响季度确认节奏。"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "签约提速",
|
||||
"summary": "渠道项目释放积极信号,当前窗口应优先推动商务条款确认。",
|
||||
"chartDelta": 0.24,
|
||||
"marginDelta": 0.18,
|
||||
"briefs": [
|
||||
{"label": "机会增强", "description": "两项高概率商机完成决策人确认,预计签约节奏较上一时点提前。"},
|
||||
{"label": "商务协同", "description": "法务与交付资源已提前介入,联合处理付款条件和实施边界。"},
|
||||
{"label": "资源提醒", "description": "售前资源短时集中,需要优先保障本周具备成交条件的重点项目。"},
|
||||
],
|
||||
"objectives": [
|
||||
{"description": "抓住高概率项目的商务窗口,优先完成收入确认条件与合同边界核验。"},
|
||||
{"description": "围绕续约增购项目补齐决策链信息,推动 4 个项目进入报价确认。"},
|
||||
{"description": "将回款条款前置到合同协商环节,降低后续账期拉长风险。"},
|
||||
],
|
||||
"events": [
|
||||
{"title": "华东渠道项目进入商务确认", "content": "核心伙伴已接受联合报价框架,合同条款进入最终核验。", "topic": "渠道拓展"},
|
||||
{"title": "战略客户增购范围得到确认", "content": "客户新增两个业务场景,预计提升本次续约合同价值。", "topic": "客户增购"},
|
||||
{"title": "售前资源启动优先级调度", "content": "两名解决方案专家转入高概率项目,保障本周方案澄清效率。", "topic": "资源协同"},
|
||||
],
|
||||
"narratives": [
|
||||
{"title": "签约窗口", "value": "提速", "description": "高概率项目的商务条件趋于清晰,适合集中推动确认。"},
|
||||
{"title": "增购空间", "value": "扩大", "description": "客户新增使用场景带动服务包与订阅组合机会。"},
|
||||
{"title": "资源负荷", "value": "偏高", "description": "售前能力短时集中,需控制低概率项目的并行投入。"},
|
||||
],
|
||||
"insights": [
|
||||
{"label": "最佳动作", "highlight": "优先完成付款条件和交付边界确认,", "description": "可进一步缩短合同流转周期。"},
|
||||
{"label": "转化预测", "highlight": "高概率项目若按计划签约,", "description": "本月确认收入有望上调约 ¥2.1M。"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "续约推进",
|
||||
"summary": "重点客户续约反馈集中回流,客户经营成为当前主要推进线。",
|
||||
"chartDelta": 0.12,
|
||||
"marginDelta": 0.1,
|
||||
"briefs": [
|
||||
{"label": "客户反馈", "description": "续约客户对服务稳定性评价积极,但希望进一步明确增购后的交付节奏。"},
|
||||
{"label": "场景共创", "description": "客户成功团队正在补齐业务收益案例,为增购报价提供量化依据。"},
|
||||
{"label": "决策风险", "description": "部分客户尚未确认最终预算归属,需要加强业务与采购双线沟通。"},
|
||||
],
|
||||
"objectives": [
|
||||
{"description": "保持年度目标节奏,同时将存量客户贡献作为本周收入确定性的主要支撑。"},
|
||||
{"description": "围绕 12 个重点客户逐一确认预算、决策人和续约范围,提升续约确定性。"},
|
||||
{"description": "把续约付款节点纳入共跟清单,避免合同确认与实际到账节奏脱节。"},
|
||||
],
|
||||
"events": [
|
||||
{"title": "重点续约客户完成价值复盘", "content": "客户确认现有服务达到预期,并提出新增数据分析场景。", "topic": "客户经营"},
|
||||
{"title": "增购方案进入联合测算", "content": "销售与交付共同评估资源投入,准备输出分阶段报价方案。", "topic": "方案共创"},
|
||||
{"title": "客户预算路径完成补充核验", "content": "4 个续约项目已明确预算归属,后续转入采购沟通。", "topic": "预算确认"},
|
||||
],
|
||||
"narratives": [
|
||||
{"title": "客户意愿", "value": "积极", "description": "重点客户续约意愿稳定,增购讨论逐步进入量化阶段。"},
|
||||
{"title": "价值证明", "value": "补强", "description": "业务收益案例正在完善,有利于缩短客户内部决策时间。"},
|
||||
{"title": "预算路径", "value": "待确认", "description": "少量项目仍需同步业务负责人和采购侧预算口径。"},
|
||||
],
|
||||
"insights": [
|
||||
{"label": "客户策略", "highlight": "用业务收益案例支撑增购报价,", "description": "比单纯价格优惠更有利于稳定续约价值。"},
|
||||
{"label": "协同建议", "highlight": "销售与客户成功共同参加下一轮沟通,", "description": "同步解决价值与交付问题。"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "回款预警",
|
||||
"summary": "区域到账节奏出现短时偏差,当前应优先压实高账龄应收责任。",
|
||||
"chartDelta": -0.32,
|
||||
"marginDelta": -0.26,
|
||||
"briefs": [
|
||||
{"label": "现金流提醒", "description": "西南区域两笔重点应收预计到账时间后移,月度回款预测需要下修。"},
|
||||
{"label": "专项处置", "description": "财务与客户负责人已建立逐笔跟进机制,优先处理高账龄款项。"},
|
||||
{"label": "目标影响", "description": "若本周未形成明确付款承诺,可能影响月末现金流安全边际。"},
|
||||
],
|
||||
"objectives": [
|
||||
{"description": "收入目标保持不变,但新增合同需同步核验付款条件和客户信用情况。"},
|
||||
{"description": "续约项目继续推进,并将历史回款表现纳入商务条款决策。"},
|
||||
{"description": "集中处置 30 天以上应收,明确管理层协同事项与最晚到账节点。"},
|
||||
],
|
||||
"events": [
|
||||
{"title": "区域到账预测出现偏差", "content": "两笔应收未按原计划进入付款流程,财务已发起风险提示。", "topic": "回款预警"},
|
||||
{"title": "高账龄款项启动管理层协同", "content": "客户负责人补充付款障碍与决策链信息,形成逐笔处置方案。", "topic": "专项治理"},
|
||||
{"title": "新增合同付款条件复核", "content": "商务团队统一检查首付款比例与账期,防止新增现金流风险。", "topic": "合同风控"},
|
||||
],
|
||||
"narratives": [
|
||||
{"title": "到账节奏", "value": "承压", "description": "区域回款偏差扩大,需要以逐笔承诺替代笼统预测。"},
|
||||
{"title": "风险敞口", "value": "中等", "description": "当前问题集中在少数高价值款项,仍具备专项化解条件。"},
|
||||
{"title": "处置进度", "value": "已启动", "description": "财务、销售与管理层已进入联合跟进状态。"},
|
||||
],
|
||||
"insights": [
|
||||
{"label": "风险动作", "highlight": "优先取得客户书面付款承诺,", "description": "并同步准备分期或节点化替代方案。"},
|
||||
{"label": "经营影响", "highlight": "收入确认暂不受影响,", "description": "但现金流预测需保留约 ¥3.4M 风险缓冲。"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "资源协同",
|
||||
"summary": "跨团队依赖集中显现,经营重点转向资源排期与关键节点保障。",
|
||||
"chartDelta": 0.08,
|
||||
"marginDelta": -0.08,
|
||||
"briefs": [
|
||||
{"label": "执行判断", "description": "商机质量保持稳定,交付与售前资源能否按期到位成为主要约束。"},
|
||||
{"label": "排期协同", "description": "重点项目已按收入贡献与成交概率重新排序,资源将向前两级项目倾斜。"},
|
||||
{"label": "交付提醒", "description": "两个项目存在并行上线需求,需要提前确认实施负责人和验收条件。"},
|
||||
],
|
||||
"objectives": [
|
||||
{"description": "通过资源优先级调度保障高价值项目按期签约与确认,稳定年度目标节奏。"},
|
||||
{"description": "对重点续约项目同步售前、交付和客户成功排期,减少内部等待时间。"},
|
||||
{"description": "将资源投入与回款节点联动管理,优先保障付款条件清晰的项目。"},
|
||||
],
|
||||
"events": [
|
||||
{"title": "重点项目资源优先级完成调整", "content": "售前与交付排期按项目价值重新排序,优先保障高概率机会。", "topic": "资源调度"},
|
||||
{"title": "并行上线项目启动依赖核验", "content": "实施负责人、环境准备与验收标准已进入联合确认。", "topic": "交付协同"},
|
||||
{"title": "跨部门问题转入日清机制", "content": "未闭环依赖将在每日经营例会上更新责任人与处理时限。", "topic": "机制优化"},
|
||||
],
|
||||
"narratives": [
|
||||
{"title": "资源供给", "value": "趋紧", "description": "重点项目进入集中阶段,售前与交付资源需要动态调度。"},
|
||||
{"title": "项目排序", "value": "已校准", "description": "当前优先级已综合收入贡献、成交概率、付款质量。"},
|
||||
{"title": "协同效率", "value": "改善", "description": "跨部门问题进入日清机制,等待时间开始缩短。"},
|
||||
],
|
||||
"insights": [
|
||||
{"label": "资源原则", "highlight": "优先保障高概率、高价值、付款清晰项目,", "description": "暂停低确定性定制投入。"},
|
||||
{"label": "节奏建议", "highlight": "每日校准一次关键依赖,", "description": "确保资源变化及时反映到经营预测。"},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# ── 前端 createBusinessTimelineSnapshot 的 Python 复刻 ───────────────────────
|
||||
|
||||
|
||||
def _positive_modulo(value: int, divisor: int) -> int:
|
||||
return value % divisor
|
||||
|
||||
|
||||
def _two_digits(value: int) -> str:
|
||||
return f"{value:02d}"
|
||||
|
||||
|
||||
def _format_snapshot_time(moment: datetime) -> str:
|
||||
return f"{_two_digits(moment.month)}-{_two_digits(moment.day)} {_two_digits(moment.hour)}:{_two_digits(moment.minute)}"
|
||||
|
||||
|
||||
def _merge_rows(source: list[dict], patches: list[dict]) -> list[dict]:
|
||||
return [{**item, **(patches[index] if index < len(patches) else {})} for index, item in enumerate(source)]
|
||||
|
||||
|
||||
def _adjust_series(values: list[float], delta: float) -> list[float]:
|
||||
length = max(len(values), 1)
|
||||
return [round(float(value) + delta * ((index + 1) / length), 1) for index, value in enumerate(values)]
|
||||
|
||||
|
||||
def build_snapshot(moment: datetime) -> dict:
|
||||
"""复刻 ``createBusinessTimelineSnapshot(baseDashboard, timestamp)``。"""
|
||||
minute_slot = round((moment - ANCHOR).total_seconds() / 60)
|
||||
slot = minute_slot + minute_slot // 5 + minute_slot // 60
|
||||
phase = TIMELINE_PHASES[_positive_modulo(slot, len(TIMELINE_PHASES))]
|
||||
time_label = _format_snapshot_time(moment)
|
||||
events = [
|
||||
{**event, "date": (moment - timedelta(minutes=17 * index)).strftime("%H:%M")}
|
||||
for index, event in enumerate(phase["events"])
|
||||
]
|
||||
snapshot = dict(BASE_DASHBOARD)
|
||||
snapshot.update(
|
||||
{
|
||||
"pageDescription": f"{phase['summary']} 当前展示 {time_label} 经营快照。",
|
||||
"snapshotName": phase["name"],
|
||||
"snapshotTime": moment.isoformat(),
|
||||
"briefs": _merge_rows(BASE_DASHBOARD["briefs"], phase["briefs"]),
|
||||
"objectives": _merge_rows(BASE_DASHBOARD["objectives"], phase["objectives"]),
|
||||
"events": events,
|
||||
"businessNarratives": phase["narratives"],
|
||||
"insights": phase["insights"],
|
||||
"chart": {
|
||||
**BASE_DASHBOARD["chart"],
|
||||
"ariaLabel": f"{time_label} {BASE_DASHBOARD['chart']['ariaLabel']}",
|
||||
"actual": _adjust_series(BASE_DASHBOARD["chart"]["actual"], phase["chartDelta"]),
|
||||
"bar": _adjust_series(BASE_DASHBOARD["chart"]["bar"], phase["marginDelta"]),
|
||||
},
|
||||
}
|
||||
)
|
||||
return snapshot
|
||||
|
||||
|
||||
# ── 预存主流程 ───────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
async def _run(dry_run: bool) -> int:
|
||||
from deerflow.config import get_app_config
|
||||
from deerflow.persistence.engine import close_engine, get_session_factory, init_engine_from_config
|
||||
from deerflow.persistence.task_analysis import make_task_analysis_store
|
||||
from deerflow.runtime.task_analysis.data_delivery import submit_block_data
|
||||
|
||||
config = get_app_config()
|
||||
await init_engine_from_config(config.database)
|
||||
sf = get_session_factory()
|
||||
store = make_task_analysis_store(sf)
|
||||
|
||||
ticks = timeline_ticks()
|
||||
snapshots = {tick: build_snapshot(tick) for tick in ticks}
|
||||
|
||||
if dry_run:
|
||||
for tick in ticks:
|
||||
snap = snapshots[tick]
|
||||
logger.info("期 %s → %s · %s · %s", ticks.index(tick) + 1, tick.isoformat(), snap["snapshotName"], snap["pageDescription"][:40])
|
||||
await close_engine()
|
||||
return 0
|
||||
|
||||
# 幂等:演示任务已存在则级联删除后重建。
|
||||
existing = await store.get_task_by_source(SOURCE_TASK_ID)
|
||||
if existing is not None:
|
||||
logger.info("演示任务已存在(id=%s),删除后重建", existing["id"])
|
||||
await store.delete_task_cascade(existing["id"])
|
||||
|
||||
task = await store.create_task(
|
||||
{
|
||||
"source_task_id": SOURCE_TASK_ID,
|
||||
"name": "商业态势总览(演示)",
|
||||
"description": "定时数据交付演示任务:13 期小时刻度(2026-06-17 22:00 → 2026-06-18 10:00),block_1 = 商业态势总览页面数据。",
|
||||
}
|
||||
)
|
||||
await store.update_block_defs(task["id"], [{"block_key": "block_1", "title": "商业态势总览"}])
|
||||
|
||||
for index, tick in enumerate(ticks, start=1):
|
||||
period = await store.open_period(task["id"], source="cron", opened_at=tick)
|
||||
result = await submit_block_data(store, task_id=task["id"], block_key="block_1", data=snapshots[tick])
|
||||
logger.info(
|
||||
"期 %s/%s opened_at=%s 相位=%s keys=%s",
|
||||
period["period_index"], TICK_COUNT, tick.isoformat(), snapshots[tick]["snapshotName"], result["keys"],
|
||||
)
|
||||
|
||||
logger.info("")
|
||||
logger.info("预存完成。前端对接:")
|
||||
logger.info(" 任务号 source_task_id = %s", SOURCE_TASK_ID)
|
||||
logger.info(" 块定义 GET /api/task-analysis/external/tasks/%s/blocks", SOURCE_TASK_ID)
|
||||
logger.info(" 时间轴 GET /api/task-analysis/external/tasks/%s/periods", SOURCE_TASK_ID)
|
||||
logger.info(" 单期数据 GET /api/task-analysis/external/tasks/%s/periods/{n|latest}", SOURCE_TASK_ID)
|
||||
|
||||
await close_engine()
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="预存商业态势总览演示数据(13 期小时刻度 + block_1 快照)")
|
||||
parser.add_argument("--dry-run", action="store_true", help="只打印将生成的期与相位,不写库")
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
return asyncio.run(_run(args.dry_run))
|
||||
except Exception:
|
||||
logger.exception("预存失败")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,151 @@
|
||||
"""预存「力量态势」演示任务详情(force-situation,新类型任务 002)。
|
||||
|
||||
一个任务一份自由 JSON 文档(force_situation_details 表,结构不定死字段):
|
||||
- 时间轴(importantTimeNodes 重要时间节点)+ 当前研判 + 重要目标 + 洞察区块;
|
||||
- 任务衔接(taskConnections):任务字段与任务列表页对齐
|
||||
(taskName/taskDirection/taskLevel/taskStatus/taskContent/sourceType/createTime),
|
||||
另带 connectionType(进攻型/防御型/正面塑势)作为泳道分组。
|
||||
|
||||
用法(backend 目录):PYTHONPATH=. uv run python scripts/seed_force_situation_demo.py [--dry-run]
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
TASK_ID = "002"
|
||||
_LOCATION_SCRIPTS = Path(__file__).resolve().parents[1] / "skills" / "custom" / "fs-location-info" / "scripts"
|
||||
sys.path.insert(0, str(_LOCATION_SCRIPTS))
|
||||
from fs_location_info import attach_coordinates # noqa: E402
|
||||
|
||||
def _connection_rows() -> list[dict]:
|
||||
"""任务衔接演示行(字段与任务列表页对齐)。"""
|
||||
rows = [
|
||||
("T01", "进攻型", "核心客户竞对反制续约", "W方向", 1, 20, "针对竞对报价渗透,输出两家战略客户反制口径并推动续签。", 0, "2026-09-03 20:08:35"),
|
||||
("T02", "进攻型", "华东核心商圈抢点铺货", "W方向", 2, 10, "江苏、浙江核心商圈新增网点 40 个,抢在竞对促销前完成布点。", 0, "2026-09-05 10:12:00"),
|
||||
("T03", "进攻型", "话题热度集中投放", "E方向", 2, 25, "账号编组与媒资就绪后 48 小时内完成话题集中投放。", 1, "2026-09-06 15:40:11"),
|
||||
("T04", "进攻型", "重点群体定向触达", "E方向", 2, 20, "面向关键决策人群体输出定向内容并跟踪触达效果。", 1, "2026-09-08 09:20:45"),
|
||||
("T05", "防御型", "竞对挖角渠道防护", "W方向", 1, 20, "梳理核心经销商合作风险清单,逐户明确稳控责任人与话术。", 0, "2026-09-04 11:30:00"),
|
||||
("T06", "防御型", "低效网点流失止损", "W方向", 3, 10, "对 30 家低效网点分批整改,明确止损退出标准。", 0, "2026-09-09 14:05:22"),
|
||||
("T07", "防御型", "负面舆情稳态控制", "N方向", 1, 25, "7×24 监测渠道与社群负面声量,触发阈值即启动应急预案。", 1, "2026-09-02 08:00:00"),
|
||||
("T08", "正面塑势", "品牌口碑正向传播", "E方向", 2, 25, "围绕客户成功案例组织系列正面传播,扩大口碑基本盘。", 1, "2026-09-07 16:44:30"),
|
||||
("T09", "正面塑势", "战略客户成功案例放大", "E方向", 2, 20, "提炼 3 个战略客户标杆案例,形成多语种传播素材。", 1, "2026-09-10 10:55:00"),
|
||||
("T10", "正面塑势", "经销商信心公开塑造", "W方向", 3, 10, "年中大会前发布渠道政策预告,稳定经销商预期。", 0, "2026-09-11 09:15:40"),
|
||||
]
|
||||
return [
|
||||
{
|
||||
"id": row[0],
|
||||
"connectionType": row[1],
|
||||
"taskName": row[2],
|
||||
"taskDirection": row[3],
|
||||
"taskLevel": row[4],
|
||||
"taskStatus": row[5],
|
||||
"taskContent": row[6],
|
||||
"sourceType": row[7],
|
||||
"createTime": row[8],
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
|
||||
|
||||
DETAIL: dict = {
|
||||
# 当前判断(研判卡)
|
||||
"currentJudgments": [
|
||||
{"id": "J01", "content": "核心客户续约窗口正在收窄,竞对报价已进入两家战略客户决策层,若不在本周完成口径对齐,续约率可能跌破 90%。", "confidence": 86, "conclusion": "风险上升"},
|
||||
{"id": "J02", "content": "华东铺货节奏总体符合计划,江苏、浙江核心商圈网点拓展仍可按既定节点推进,需盯紧安徽渠道落地质量。", "confidence": 78, "conclusion": "持续关注"},
|
||||
{"id": "J03", "content": "经销商活跃度回升至近三个月高位,可借势优化低效网点并扩大覆盖,形成巩固渠道的窗口。", "confidence": 74, "conclusion": "存在战机"},
|
||||
{"id": "J04", "content": "海外发布通道尚未就绪,重点信息触达存在 3–7 天滞后,可能拖慢对目标群体的及时影响。", "confidence": 81, "conclusion": "风险上升"},
|
||||
{"id": "J05", "content": "账号编组与媒体生产能力已就绪,具备本周集中投放条件,可支撑话题热度与公开发布动作。", "confidence": 88, "conclusion": "存在战机"},
|
||||
{"id": "J06", "content": "效果评估数据闭环仍不完整,重点群体沟通成效难以量化,暂不影响当前动作,但需持续补齐监测口径。", "confidence": 69, "conclusion": "持续关注"},
|
||||
],
|
||||
# 重要目标
|
||||
"importantTargets": [
|
||||
{"id": "G01", "name": "G01 核心客户续约目标", "description": "Q3 完成 15 家战略客户合同续签,续约率不低于 90%,锁定年度营收基本盘。"},
|
||||
{"id": "G02", "name": "G02 华东区域铺货目标", "description": "新增终端网点 120 个,覆盖江苏、浙江、安徽核心商圈,Q3 末铺货完成率达到 70% 以上。"},
|
||||
{"id": "G03", "name": "G03 经销商网络巩固", "description": "提升经销商活跃度至 75%,优化低效网点 30 家,防范竞对挖角导致核心渠道松动。"},
|
||||
],
|
||||
# 时间轴:重要时间节点
|
||||
"importantTimeNodes": [
|
||||
{"title": "G01 战略客户续约窗口开启", "content": "15 家战略客户进入合同窗口期,需在两周内完成商务条款对齐,锁定年度营收基本盘。", "topic": "核心续约", "date": "09-18"},
|
||||
{"title": "华东铺货完成率中期考核", "content": "江苏、浙江核心商圈铺货进度复核,目标完成率 70%,安徽渠道落地质量同步盯紧。", "topic": "区域铺货", "date": "09-22"},
|
||||
{"title": "全国经销商年中大会", "content": "集中通报渠道政策与返点节奏,稳定核心经销商合作意愿,防范竞对挖角。", "topic": "渠道巩固", "date": "09-28"},
|
||||
{"title": "海外发布通道开通截止", "content": "重点信息触达通道需在此节点前就绪,避免 3–7 天滞后拖慢对目标群体的影响。", "topic": "发布通道", "date": "10-05"},
|
||||
],
|
||||
# 洞察区块
|
||||
"insightTopics": [
|
||||
{"title": "核心客户续约窗口收窄", "heat": 96},
|
||||
{"title": "华东渠道竞对促销加码", "heat": 88},
|
||||
{"title": "经销商利润承压讨论升温", "heat": 76},
|
||||
{"title": "品牌口碑正面案例扩散", "heat": 64},
|
||||
],
|
||||
"insightRisks": [
|
||||
{"description": "外部扩散风险", "severity": "高", "urgency": "高"},
|
||||
{"description": "群体情绪升温", "severity": "高", "urgency": "中"},
|
||||
{"description": "负面叙事形成", "severity": "中", "urgency": "高"},
|
||||
{"description": "敌对势力借题发挥", "severity": "中", "urgency": "中"},
|
||||
],
|
||||
"insightOpportunities": [
|
||||
{"description": "权威发声窗口", "value": "高", "timing": "紧迫"},
|
||||
{"description": "议题澄清窗口", "value": "高", "timing": "中"},
|
||||
{"description": "正面议题引导", "value": "中", "timing": "中"},
|
||||
],
|
||||
# 任务衔接:任务字段与任务列表页(/situation-overview/action/task)对齐;
|
||||
# connectionType 为衔接类型(进攻型/防御型/正面塑势),taskStatus 用任务列表状态码
|
||||
# (10=未开始 20=进行中 25=已完成分析 30=已结束)。不带坐标。
|
||||
"taskConnections": _connection_rows(),
|
||||
}
|
||||
|
||||
|
||||
def _stamp_locations(detail: dict) -> dict:
|
||||
"""除任务衔接外,按 fs-location-info 补 latitude/longitude。"""
|
||||
for item in detail.get("currentJudgments") or []:
|
||||
attach_coordinates(item, query=item.get("content", ""), item_id=item.get("id", ""))
|
||||
for item in detail.get("importantTargets") or []:
|
||||
attach_coordinates(item, query=item.get("name", ""), item_id=item.get("id", ""))
|
||||
for index, item in enumerate(detail.get("importantTimeNodes") or [], start=1):
|
||||
attach_coordinates(item, query=item.get("title", ""), item_id=f"node-{index}-{item.get('title', '')}")
|
||||
for index, item in enumerate(detail.get("insightTopics") or [], start=1):
|
||||
attach_coordinates(item, query=item.get("title", ""), item_id=f"topic-{index}")
|
||||
for index, item in enumerate(detail.get("insightRisks") or [], start=1):
|
||||
attach_coordinates(item, query=item.get("description", ""), item_id=f"risk-{index}")
|
||||
for index, item in enumerate(detail.get("insightOpportunities") or [], start=1):
|
||||
attach_coordinates(item, query=item.get("description", ""), item_id=f"opp-{index}")
|
||||
return detail
|
||||
|
||||
|
||||
DETAIL = _stamp_locations(DETAIL)
|
||||
|
||||
|
||||
|
||||
async def _run(dry_run: bool) -> int:
|
||||
from deerflow.config import get_app_config
|
||||
from deerflow.persistence.engine import close_engine, get_session_factory, init_engine_from_config
|
||||
from deerflow.persistence.force_situation import make_force_situation_store
|
||||
|
||||
if dry_run:
|
||||
print(json.dumps(DETAIL, ensure_ascii=False, indent=2)[:2000])
|
||||
print("…(dry-run,未写库)")
|
||||
return 0
|
||||
|
||||
config = get_app_config()
|
||||
await init_engine_from_config(config.database)
|
||||
sf = get_session_factory()
|
||||
store = make_force_situation_store(sf)
|
||||
try:
|
||||
record = await store.upsert_detail(TASK_ID, DETAIL, updated_by="seed")
|
||||
print(f"已预存任务 {TASK_ID} 详情:keys={sorted(record['detail'].keys())}")
|
||||
print(f"updated_at={record.get('updated_at')}")
|
||||
finally:
|
||||
await close_engine()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--dry-run", action="store_true")
|
||||
args = parser.parse_args()
|
||||
sys.exit(asyncio.run(_run(args.dry_run)))
|
||||
@@ -0,0 +1,131 @@
|
||||
"""更新「商业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 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 = "http://localhost:8001"
|
||||
USERNAME = "admin"
|
||||
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):
|
||||
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:
|
||||
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")
|
||||
|
||||
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())
|
||||
@@ -0,0 +1,232 @@
|
||||
"""创建「商业态势总览」数据交付链的 3 个席位智能体 + 业务链 + 定时绑定。
|
||||
|
||||
一次性联调脚本(可重复运行,幂等:已存在则跳过/更新):
|
||||
1. POST /api/agents ×3 —— 用户级智能体(非内置 seeder),绑 task-data-submit 技能;
|
||||
2. POST /api/roundtable-chains —— 业务链 bo-overview-chain(两阶段:2 采集并行 → 1 综合);
|
||||
3. PUT /api/roundtable-chains/{id} is_public=True —— 发布链(自动发布席位智能体);
|
||||
4. PUT /api/task-analysis/tasks/{id}/binding —— 演示任务绑定链 + cron 每 10 分钟;
|
||||
5. POST /api/task-analysis/tasks/{id}/trigger —— 立即手动触发一轮(不等 cron)。
|
||||
|
||||
用法(backend 目录):PYTHONPATH=. uv run python scripts/setup_business_overview_chain.py
|
||||
"""
|
||||
|
||||
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 = "BUSINESS-OVERVIEW-DEMO"
|
||||
CHAIN_ID = "bo-overview-chain"
|
||||
CRON = "*/10 * * * *"
|
||||
|
||||
SUBMIT_RULES = """## 数据提交(核心交付方式)
|
||||
|
||||
你唯一的正式交付方式是调用技能 `task-data-submit` 入库(bash):
|
||||
|
||||
```bash
|
||||
python /mnt/skills/custom/task-data-submit/scripts/task_data_submit.py \\
|
||||
--task-id <任务号> --block block_1 --data '@/mnt/user-data/outputs/my-part.json'
|
||||
```
|
||||
|
||||
- `--task-id` 取任务上下文里「数据交付 task_id(调用 task-data-submit 技能入库时的 --task-id 参数):」一行给出的值;
|
||||
- 固定提交到 `block_1`(整页一个 JSON,各席位分 key 共写);
|
||||
- **只提交你负责的顶层 key**,绝不把其他席位负责的 key 抄回来(同 key 后写覆盖会冲掉别人的数据);
|
||||
- payload 必须是 JSON 对象;完成一部分就先提交一部分(进行中的刻度实时可见);
|
||||
- 提交成功(stdout `"success": true`)即交付完成;失败时把 errors 原样写进回复并重试一次。
|
||||
"""
|
||||
|
||||
NO_ASK_RULES = """## 工作纪律
|
||||
|
||||
- 无人值守定时任务:**禁止向用户提问或等待确认**;信息不足时基于合理假设生成当期数据,并在 description/文本中注明「(基于假设的演示数据)」。
|
||||
- 检索工具不可用或无结果时不要卡住:直接用合理的模拟数据完成交付,保证每期都有数据落地。
|
||||
- 每期数据要有变化和连续性(数值小幅波动、事件随时间推进),不要每期输出一模一样的内容。
|
||||
"""
|
||||
|
||||
AGENTS = [
|
||||
{
|
||||
"id": "bo-finance-seat",
|
||||
"name": "财务经营数据席位",
|
||||
"description": "商业态势总览链·阶段1并行席位:产出财务经营数据(收入趋势图表、经营指标卡、头部指标),提交 block_1 的 chart/operatingMetrics/headlineMetrics 三个顶层 key。",
|
||||
"soul": """# 财务经营数据席位(bo-finance-seat)
|
||||
|
||||
你是「商业态势总览」数据交付链的财务数据席位,负责整页 JSON 中**财务经营类**的三个顶层 key。
|
||||
|
||||
## 你负责的顶层 key 与结构(严格遵守,字段名一个都不能改)
|
||||
|
||||
`chart` —— 收入与经营目标趋势图:
|
||||
```json
|
||||
{
|
||||
"title": "收入与经营目标趋势", "actualName": "确认收入", "targetName": "经营目标", "barName": "毛利率",
|
||||
"ariaLabel": "近十二个月确认收入、经营目标与毛利率趋势图",
|
||||
"leftAxisName": "收入(百万元)", "rightAxisName": "毛利率",
|
||||
"months": ["7月","8月","9月","10月","11月","12月","1月","2月","3月","4月","5月","6月"],
|
||||
"actual": [12 个数字], "target": [12 个数字], "bar": [12 个数字],
|
||||
"leftAxisMax": 16, "leftAxisInterval": 4, "rightAxisMin": 20, "rightAxisMax": 40, "rightAxisInterval": 5, "rightAxisSuffix": "%"
|
||||
}
|
||||
```
|
||||
`operatingMetrics` —— 4 个经营指标卡:`[{"label","value","change","tone"}]`,tone ∈ positive/neutral。
|
||||
`headlineMetrics` —— 3 个头部指标:同上结构。
|
||||
|
||||
数字要自洽:actual 围绕 target 上下波动、bar 在 25~35 区间、change 与数值方向一致。
|
||||
|
||||
""" + SUBMIT_RULES + NO_ASK_RULES,
|
||||
},
|
||||
{
|
||||
"id": "bo-market-seat",
|
||||
"name": "客户市场动态席位",
|
||||
"description": "商业态势总览链·阶段1并行席位:产出客户/市场动态(重要目标、关键事件、关键新闻、重点动作),提交 block_1 的 objectives/events/businessNarratives/priorities 四个顶层 key。",
|
||||
"soul": """# 客户市场动态席位(bo-market-seat)
|
||||
|
||||
你是「商业态势总览」数据交付链的客户与市场席位,负责整页 JSON 中**动态信息类**的四个顶层 key。
|
||||
|
||||
## 你负责的顶层 key 与结构(严格遵守,字段名一个都不能改)
|
||||
|
||||
`objectives` —— 3 条重要目标:`[{"name","label","category","description","level"}]`,label ∈ 核心目标/关键目标/潜力目标,level ∈ core/key/potential。
|
||||
`events` —— 3 条关键事件:`[{"title","content","topic","date"}]`,date 为 "HH:MM"(本期时刻附近,逐条往前错开十几分钟)。
|
||||
`businessNarratives` —— 3 条关键新闻:`[{"title","value","description"}]`。
|
||||
`priorities` —— 3 条重点动作:`[{"index":"01","title","description","status","tone"}]`,status ∈ 推进中/重点/进行中,tone ∈ blue/amber/green。
|
||||
|
||||
内容围绕客户续约、渠道拓展、区域线索、行业需求这些经营主题,事件与新闻随期数推进(不要每期重复同一条)。
|
||||
|
||||
""" + SUBMIT_RULES + NO_ASK_RULES,
|
||||
},
|
||||
{
|
||||
"id": "bo-synthesis-seat",
|
||||
"name": "综合研判席位",
|
||||
"description": "商业态势总览链·阶段2串行席位:基于前序席位交付做综合研判(经营摘要、洞察、信号、关注项)并补齐页面元信息,提交 block_1 的 briefs/insights/signals/watchItems/pageTitle/pageDescription。",
|
||||
"soul": """# 综合研判席位(bo-synthesis-seat)
|
||||
|
||||
你是「商业态势总览」数据交付链的**综合研判席位,运行在阶段 2**:任务上下文里会给你
|
||||
「前序各席位已完成的交付」全文(财务数据 + 客户市场动态)。你的判断必须基于这些交付,
|
||||
不要凭空编造与前序数据矛盾的结论。
|
||||
|
||||
## 你负责的顶层 key 与结构(严格遵守,字段名一个都不能改)
|
||||
|
||||
`briefs` —— 3 条经营摘要:`[{"title","label","description","tone"}]`,title 固定为 当前判断/协同重点/需持续关注,tone ∈ blue/green/amber。
|
||||
`insights` —— 2 条经营洞察:`[{"label","highlight","description"}]`。
|
||||
`signals` —— 3 条经营信号:`[{"title","description","tone"}]`,tone ∈ positive/blue。
|
||||
`watchItems` —— 2 条关注项:`[{"title","description","level","tone"}]`,level ∈ 中风险/关注,tone ∈ medium/low。
|
||||
`pageTitle` —— 固定 "商业态势总览"。
|
||||
`pageDescription` —— 一句话本期研判结论,格式:"{本期核心结论一句} 当前展示 {MM-DD HH:mm} 经营快照。"(时间用本期时刻)。
|
||||
|
||||
""" + SUBMIT_RULES + NO_ASK_RULES,
|
||||
},
|
||||
]
|
||||
|
||||
CHAIN = {
|
||||
"id": CHAIN_ID,
|
||||
"title": "商业态势总览采集链",
|
||||
"description": "定时数据交付演示链:阶段1 财务/市场两席位并行采集,阶段2 综合研判席位基于前序交付出结论;全部经 task-data-submit 技能提交到任务 block_1。",
|
||||
"seats": [
|
||||
{"agent_id": "bo-finance-seat", "name": "财务经营数据", "description": "产出 chart/operatingMetrics/headlineMetrics(财务经营类顶层 key)并入库"},
|
||||
{"agent_id": "bo-market-seat", "name": "客户市场动态", "description": "产出 objectives/events/businessNarratives/priorities(动态信息类顶层 key)并入库"},
|
||||
{"agent_id": "bo-synthesis-seat", "name": "综合研判", "description": "基于前序交付产出 briefs/insights/signals/watchItems 与页面元信息并入库"},
|
||||
],
|
||||
"stages": [["bo-finance-seat", "bo-market-seat"], ["bo-synthesis-seat"]],
|
||||
"stage_goals": [
|
||||
"并行采集:财务席位交 chart/operatingMetrics/headlineMetrics,市场席位交 objectives/events/businessNarratives/priorities,各自用 task-data-submit 提交 block_1",
|
||||
"综合研判:基于前序两席位交付产出 briefs/insights/signals/watchItems + pageTitle/pageDescription 并提交",
|
||||
],
|
||||
"coordinator_prompt": (
|
||||
"派活时明确每个席位的顶层 key 边界:财务席位只交 chart/operatingMetrics/headlineMetrics,"
|
||||
"市场席位只交 objectives/events/businessNarratives/priorities,综合席位只交 "
|
||||
"briefs/insights/signals/watchItems/pageTitle/pageDescription。所有席位用 "
|
||||
"task-data-submit 技能提交到 block_1,--task-id 取任务上下文里的「数据交付 task_id」。"
|
||||
"无人值守,禁止澄清。"
|
||||
),
|
||||
"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()
|
||||
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")
|
||||
|
||||
# 1. 智能体(幂等:存在则跳过)
|
||||
for agent in AGENTS:
|
||||
status, existing = call("GET", f"/api/agents/{agent['id']}", None, token)
|
||||
if status == 200:
|
||||
print(f"智能体已存在: {agent['id']}")
|
||||
continue
|
||||
body = {**agent, "published": True, "skills": ["task-data-submit"],
|
||||
"tool_groups": ["web", "file:read", "file:write", "bash"]}
|
||||
status, created = call("POST", "/api/agents", body, token)
|
||||
print(f"创建智能体 {agent['id']}: HTTP {status}", created.get("id") or created)
|
||||
if status not in (200, 201):
|
||||
return 1
|
||||
|
||||
# 2. 业务链(幂等:存在则跳过)
|
||||
status, existing_chain = call("GET", f"/api/roundtable-chains/{CHAIN_ID}", None, token)
|
||||
if status == 200:
|
||||
print(f"业务链已存在: {CHAIN_ID}")
|
||||
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. 发布链(is_public=True;已公开则跳过)
|
||||
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')}, 自动发布席位={updated.get('published_seat_agents')}")
|
||||
else:
|
||||
print("业务链已是公开状态")
|
||||
|
||||
# 4. 演示任务绑定链 + 每 10 分钟
|
||||
status, task = call("GET", f"/api/task-analysis/external/tasks/{SOURCE_TASK_ID}/detail", None, token)
|
||||
if status != 200:
|
||||
print("演示任务不存在", status, task)
|
||||
return 1
|
||||
internal_id = task["task"]["task_id"]
|
||||
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
|
||||
|
||||
# 5. 立即触发一轮
|
||||
if not args.skip_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')}")
|
||||
|
||||
print(f"\n内部任务 id: {internal_id}")
|
||||
print("查询执行: GET /api/task-analysis/tasks/{id}/runs")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,279 @@
|
||||
"""创建「商业情报收集链」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())
|
||||
@@ -0,0 +1,626 @@
|
||||
"""创建「任务指挥」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())
|
||||
@@ -1,233 +1,351 @@
|
||||
-- ============================================================================
|
||||
-- 给【已存在的表】补列(ADD COLUMN)—— MySQL 手动执行版
|
||||
-- ============================================================================
|
||||
--
|
||||
-- 背景:
|
||||
-- 本项目的 alembic 自动迁移是【关闭】的(engine.py 中
|
||||
-- `# await _run_alembic_upgrade(url)` 被注释)。`Base.metadata.create_all`
|
||||
-- 只会建【新表】,对【已存在的表】既不会补建索引、也不会补加新列。
|
||||
-- 因此当我们在某个 ORM model 上新增了一列时,【全新部署】会随建表一并带上,
|
||||
-- 但【已有数据的 MySQL 部署】上这一列并不会自动出现,代码读写该列会报错。
|
||||
--
|
||||
-- 约定(重要):
|
||||
-- 今后只要有代码给某张【已存在的表】新增字段(在 ORM model 上加列),就必须
|
||||
-- 把对应的 `ADD COLUMN` 语句以下面的【幂等】写法补进本文件,并在迁移目录里
|
||||
-- 保留对应的 alembic 版本文件,二者保持一致。运维在升级老库时整文件执行本脚本
|
||||
-- 即可补齐所有缺失的列。
|
||||
--
|
||||
-- 用法:
|
||||
-- 1) 先选中目标库(与 database.mysql_url 里的库名一致):
|
||||
-- USE your_deerflow_db;
|
||||
-- 2) 直接整文件执行即可。本脚本是【幂等】的:已存在的列会跳过,不会报错,
|
||||
-- 可重复执行。
|
||||
--
|
||||
-- 注意:
|
||||
-- * 列定义必须与 ORM model / alembic migration 完全一致(类型、长度、可空性)。
|
||||
-- * 新增列一律建议 NULL 或带 DEFAULT,避免在大表上回填导致长时间锁表。
|
||||
-- * MySQL 8.0 在线 DDL 默认 ALGORITHM=INPLACE 一般不阻塞写入,但仍建议低峰执行。
|
||||
-- ============================================================================
|
||||
|
||||
-- ---- 通用幂等封装:仅当列不存在时才新增 --------------------------------------
|
||||
DELIMITER //
|
||||
DROP PROCEDURE IF EXISTS _add_column_if_missing //
|
||||
CREATE PROCEDURE _add_column_if_missing(
|
||||
IN p_table VARCHAR(64),
|
||||
IN p_column VARCHAR(64),
|
||||
IN p_definition VARCHAR(255) -- 列定义,如 'VARCHAR(128) NULL DEFAULT NULL'
|
||||
)
|
||||
BEGIN
|
||||
DECLARE v_exists INT DEFAULT 0;
|
||||
SELECT COUNT(*) INTO v_exists
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = p_table
|
||||
AND column_name = p_column;
|
||||
|
||||
IF v_exists = 0 THEN
|
||||
SET @ddl = CONCAT('ALTER TABLE `', p_table, '` ADD COLUMN `', p_column, '` ', p_definition);
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
SELECT CONCAT('ADDED ', p_table, '.', p_column) AS result;
|
||||
ELSE
|
||||
SELECT CONCAT('SKIPPED ', p_table, '.', p_column, ' (already exists)') AS result;
|
||||
END IF;
|
||||
END //
|
||||
DELIMITER ;
|
||||
|
||||
-- ---- skills ------------------------------------------------------------------
|
||||
-- name_zh: 技能的中文显示名(用户配置或 LLM 生成),对应 migration 20260609_01。
|
||||
-- ORM: SkillRow.name_zh = String(128), nullable=True。
|
||||
CALL _add_column_if_missing('skills', 'name_zh', 'VARCHAR(128) NULL DEFAULT NULL');
|
||||
-- detail: 技能的长文介绍,对应 migration 20260518_01。
|
||||
-- ORM: SkillRow.detail = Text, nullable=True。
|
||||
CALL _add_column_if_missing('skills', 'detail', 'TEXT NULL DEFAULT NULL');
|
||||
-- square_id: 技能所属的发布广场(发布广场功能),对应 migration 20260611_01。
|
||||
-- ORM: SkillRow.square_id = String(64), nullable=False, default=''。
|
||||
CALL _add_column_if_missing('skills', 'square_id', "VARCHAR(64) NOT NULL DEFAULT ''");
|
||||
-- always_on: 常驻(免压缩) —— 开启技能压缩后仍全文注入系统提示词,对应 migration 20260616_03。
|
||||
-- ORM: SkillRow.always_on = Boolean, nullable=False, default=False。
|
||||
CALL _add_column_if_missing('skills', 'always_on', 'TINYINT(1) NOT NULL DEFAULT 0');
|
||||
|
||||
-- ---- agents ------------------------------------------------------------------
|
||||
-- square_id: 智能体所属的发布广场(发布广场功能),对应 migration 20260611_01。
|
||||
-- ORM: AgentRow.square_id = String(64), nullable=False, default=''。
|
||||
CALL _add_column_if_missing('agents', 'square_id', "VARCHAR(64) NOT NULL DEFAULT ''");
|
||||
|
||||
-- ---- roundtable_drafts -------------------------------------------------------
|
||||
-- task_id: 外部任务 id(无界嵌入抽屉传入),对应 migration 20260611_01。非空时草稿
|
||||
-- 按 task_id 共享、不再按 user_id 分权。ORM: RoundtableDraftRow.task_id =
|
||||
-- String(128), nullable=True, index=True。
|
||||
CALL _add_column_if_missing('roundtable_drafts', 'task_id', 'VARCHAR(128) NULL DEFAULT NULL');
|
||||
|
||||
-- orchestration_plan: dag 模式分层编排计划 JSON(stages/阶段目标/总控提示词/finalSynthesis),
|
||||
-- 对应 migration 20260612_01。驱动后端 _run_dag + 前端大弹窗分层流程图。
|
||||
-- ORM: RoundtableJobRow.orchestration_plan = PortableLongText(LONGTEXT), nullable。
|
||||
CALL _add_column_if_missing('roundtable_jobs', 'orchestration_plan', 'LONGTEXT NULL DEFAULT NULL');
|
||||
|
||||
-- seat_mode: 业务链条「默认席位执行模式」(flash/thinking/pro/ultra),对应 migration 20260615_01。
|
||||
-- 选入研讨时弹窗默认用它(仍可临时改)。ORM: RoundtableChainRow.seat_mode = String(16), nullable。
|
||||
CALL _add_column_if_missing('roundtable_chains', 'seat_mode', 'VARCHAR(16) NULL DEFAULT NULL');
|
||||
|
||||
-- gather_first: 业务链条「研讨前并行取数」开关,对应 migration 20260620_02。True=选入研讨时研讨前先
|
||||
-- 跑一轮全席位并行取数(只调技能、不研讨),产出作为共享资料注入研讨轮。逐条 opt-in。
|
||||
-- ORM: RoundtableChainRow.gather_first = Boolean, NOT NULL, server_default '0'。
|
||||
CALL _add_column_if_missing('roundtable_chains', 'gather_first', 'TINYINT(1) NOT NULL DEFAULT 0');
|
||||
|
||||
-- enabled: 业务链条启停开关,对应 migration 20260819_02。True=启用(可选入研讨);
|
||||
-- False=停用(管理列表仍可见)。默认启用,存量行补列后也为 True。
|
||||
-- ORM: RoundtableChainRow.enabled = Boolean, NOT NULL, server_default '1'。
|
||||
CALL _add_column_if_missing('roundtable_chains', 'enabled', 'TINYINT(1) NOT NULL DEFAULT 1');
|
||||
|
||||
-- ---- 岗位 (positions) 订阅系统,对应 migration 20260616_01 -------------------
|
||||
-- positions / position_tag_bindings 是【新表】,由 create_all 自动建,无需补列。
|
||||
-- 以下是给【已存在的表】补的列:
|
||||
-- users.position_id: 用户所属岗位(1:1)。ORM: UserRow.position_id = String(64) NULL。
|
||||
CALL _add_column_if_missing('users', 'position_id', 'VARCHAR(64) NULL DEFAULT NULL');
|
||||
-- agent_favorites: origin 区分用户自收藏 vs 岗位授予;position_id 记授予岗位。
|
||||
CALL _add_column_if_missing('agent_favorites', 'origin', "VARCHAR(16) NOT NULL DEFAULT 'user'");
|
||||
CALL _add_column_if_missing('agent_favorites', 'position_id', 'VARCHAR(64) NULL DEFAULT NULL');
|
||||
-- scheduled_task_subscriptions: 同上。
|
||||
CALL _add_column_if_missing('scheduled_task_subscriptions', 'origin', "VARCHAR(16) NOT NULL DEFAULT 'user'");
|
||||
CALL _add_column_if_missing('scheduled_task_subscriptions', 'position_id', 'VARCHAR(64) NULL DEFAULT NULL');
|
||||
|
||||
-- knowledge_notes.folder: 用户自建目录(斜杠分隔路径),驱动产品目录树;NULL=未归类。
|
||||
-- (knowledge_folders / knowledge_extract_templates 为【新表】,由 create_all 自动建,无需在此补列。)
|
||||
CALL _add_column_if_missing('knowledge_notes', 'folder', 'VARCHAR(512) NULL DEFAULT NULL');
|
||||
|
||||
-- ---- taskId 深链会商独立存储,对应 migration 20260618_01 ----------------------
|
||||
-- roundtable_task_drafts 是【新表】(taskId 聊天记录,只按 task_id 不分权),由 create_all
|
||||
-- 自动建,无需补列。以下给【已存在的】roundtable_jobs 补 task_id 列:
|
||||
-- task_id: 非空 = task 作业(按 task 共享、不按 user 分权,报告写回 roundtable_task_drafts)。
|
||||
-- ORM: RoundtableJobRow.task_id = String(128), nullable, indexed。
|
||||
CALL _add_column_if_missing('roundtable_jobs', 'task_id', 'VARCHAR(128) NULL DEFAULT NULL');
|
||||
|
||||
-- ---- position_roundtable_sessions: opt-in task sharing, migration 20260808_01 ----
|
||||
-- Existing records remain personal (`task_scoped = 0`). New records become
|
||||
-- shared only when the caller explicitly sends a non-empty route taskId.
|
||||
CALL _add_column_if_missing('position_roundtable_sessions', 'task_scoped', 'TINYINT(1) NOT NULL DEFAULT 0');
|
||||
|
||||
-- ---- 按钮管理「外部链接」追加登录参数,对应 migration 20260625_01 ----------------
|
||||
-- login_params_json: url 外链追加的登录参数列表 JSON([{key,source,custom_value},...]),
|
||||
-- 由前端运行时从 userInfo 解析后拼到地址尾部;NULL/空 = 不追加(旧行为)。
|
||||
-- ORM: TaskButtonRow.login_params_json = PortableJSON()(MySQL 落 TEXT)。
|
||||
CALL _add_column_if_missing('task_buttons', 'login_params_json', 'TEXT NULL DEFAULT NULL');
|
||||
|
||||
-- ---- users.approved: 登录白名单放行标记(黑名单模式),对应 migration 20260625_03 ----
|
||||
-- 系统设置 login_whitelist.enabled 打开后,仅被管理员显式「取消放行」(approved=0) 的
|
||||
-- 用户被拦;新账号(含从未登录过的用户)默认 1(已放行),admin 永远放行。DEFAULT 1 让
|
||||
-- 存量用户(执行本脚本补列时)一并置为已放行,避免首次打开开关把老用户锁死。
|
||||
-- ORM: UserRow.approved = Boolean, NOT NULL, default True, server_default '1'。
|
||||
CALL _add_column_if_missing('users', 'approved', 'TINYINT(1) NOT NULL DEFAULT 1');
|
||||
|
||||
-- ---- users.login_password: 账号级「登录口令」(明文),对应 migration 20260625_04 ----
|
||||
-- 管理员在白名单管理页给个别账号设置的明文登录口令。非空 → 该账号用户名直登
|
||||
-- (/api/v1/auth/login/username) 必须带匹配的 ?password=(优先于全局共享口令门);
|
||||
-- NULL/空 → 走原有共享口令门/免口令逻辑。明文存储以便随时复制完整登录链接。
|
||||
-- ORM: UserRow.login_password = String(255), nullable。
|
||||
CALL _add_column_if_missing('users', 'login_password', 'VARCHAR(255) NULL DEFAULT NULL');
|
||||
|
||||
-- ---- compact_menu_overrides.menu_type: 简介模式菜单分套(A/B),对应 migration 20260819_01 ----
|
||||
-- 两套网页部署各自一份左侧简介菜单。存量行视为类型 A;类型 B 的 node_id 带 B:: 前缀,
|
||||
-- 以免在仍以 node_id 为单列主键的旧库上撞主键。
|
||||
-- ORM: CompactMenuOverrideRow.menu_type = String(32), NOT NULL, default A, server_default A。
|
||||
CALL _add_column_if_missing('compact_menu_overrides', 'menu_type', "VARCHAR(32) NOT NULL DEFAULT 'A'");
|
||||
|
||||
-- ---- 清理临时存储过程 --------------------------------------------------------
|
||||
DROP PROCEDURE IF EXISTS _add_column_if_missing;
|
||||
|
||||
-- ---- roundtable_drafts.task_id 索引(幂等) -----------------------------------
|
||||
SET @idx_exists = (
|
||||
SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'roundtable_drafts'
|
||||
AND index_name = 'ix_roundtable_drafts_task_id'
|
||||
);
|
||||
SET @idx_ddl = IF(@idx_exists = 0,
|
||||
'CREATE INDEX `ix_roundtable_drafts_task_id` ON `roundtable_drafts` (`task_id`)',
|
||||
'SELECT ''SKIPPED ix_roundtable_drafts_task_id (already exists)'' AS result');
|
||||
PREPARE stmt FROM @idx_ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- ---- knowledge_notes.folder 索引(幂等) --------------------------------------
|
||||
SET @idx_exists = (
|
||||
SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'knowledge_notes'
|
||||
AND index_name = 'ix_knowledge_notes_folder'
|
||||
);
|
||||
SET @idx_ddl = IF(@idx_exists = 0,
|
||||
'CREATE INDEX `ix_knowledge_notes_folder` ON `knowledge_notes` (`folder`(191))',
|
||||
'SELECT ''SKIPPED ix_knowledge_notes_folder (already exists)'' AS result');
|
||||
PREPARE stmt FROM @idx_ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- ---- roundtable_jobs.task_id 索引(幂等) -------------------------------------
|
||||
SET @idx_exists = (
|
||||
SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'roundtable_jobs'
|
||||
AND index_name = 'ix_roundtable_jobs_task_id'
|
||||
);
|
||||
SET @idx_ddl = IF(@idx_exists = 0,
|
||||
'CREATE INDEX `ix_roundtable_jobs_task_id` ON `roundtable_jobs` (`task_id`)',
|
||||
'SELECT ''SKIPPED ix_roundtable_jobs_task_id (already exists)'' AS result');
|
||||
PREPARE stmt FROM @idx_ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- ---- task_buttons ------------------------------------------------------------
|
||||
-- 任务工作区跳转按钮的「追加登录态」选项,对应 migration 20260625_02。url 按钮可在外链
|
||||
-- 后追加 token(token 登录的上游 token)+ username(登录用户名),供外部系统识别当前用户。
|
||||
-- ORM: TaskButtonRow.append_auth = Boolean NOT NULL default False;
|
||||
-- auth_token_param / auth_name_param = String(64), nullable, 读时归一化为 token/username。
|
||||
CALL _add_column_if_missing('task_buttons', 'append_auth', 'TINYINT(1) NOT NULL DEFAULT 0');
|
||||
CALL _add_column_if_missing('task_buttons', 'auth_token_param', "VARCHAR(64) NULL DEFAULT 'token'");
|
||||
CALL _add_column_if_missing('task_buttons', 'auth_name_param', "VARCHAR(64) NULL DEFAULT 'username'");
|
||||
|
||||
-- ---- report_structures.retrieval_directions ---------------------------------
|
||||
-- 报告结构可选「检索方向」JSON 列表。空 / NULL = 沿用原收集员拆词检索;
|
||||
-- 非空则素材收集按指定方向检索。ORM: PortableJSON() 在 MySQL 落 TEXT。
|
||||
CALL _add_column_if_missing('report_structures', 'retrieval_directions', 'TEXT NULL');
|
||||
|
||||
-- ---- report_structures.retrieval_skills -------------------------------------
|
||||
-- 报告结构可选「检索来源」JSON 列表(技能名)。空 / NULL = 沿用原收集员工具;
|
||||
-- 非空则素材收集调用指定技能。ORM: PortableJSON() 在 MySQL 落 TEXT。
|
||||
-- ============================================================================
|
||||
-- 给【已存在的表】补列(ADD COLUMN)—— MySQL 手动执行版
|
||||
-- ============================================================================
|
||||
--
|
||||
-- 背景:
|
||||
-- 本项目的 alembic 自动迁移是【关闭】的(engine.py 中
|
||||
-- `# await _run_alembic_upgrade(url)` 被注释)。`Base.metadata.create_all`
|
||||
-- 只会建【新表】,对【已存在的表】既不会补建索引、也不会补加新列。
|
||||
-- 因此当我们在某个 ORM model 上新增了一列时,【全新部署】会随建表一并带上,
|
||||
-- 但【已有数据的 MySQL 部署】上这一列并不会自动出现,代码读写该列会报错。
|
||||
--
|
||||
-- 约定(重要):
|
||||
-- 今后只要有代码给某张【已存在的表】新增字段(在 ORM model 上加列),就必须
|
||||
-- 把对应的 `ADD COLUMN` 语句以下面的【幂等】写法补进本文件,并在迁移目录里
|
||||
-- 保留对应的 alembic 版本文件,二者保持一致。运维在升级老库时整文件执行本脚本
|
||||
-- 即可补齐所有缺失的列。
|
||||
--
|
||||
-- 用法:
|
||||
-- 1) 先选中目标库(与 database.mysql_url 里的库名一致):
|
||||
-- USE your_deerflow_db;
|
||||
-- 2) 直接整文件执行即可。本脚本是【幂等】的:已存在的列会跳过,不会报错,
|
||||
-- 可重复执行。
|
||||
--
|
||||
-- 注意:
|
||||
-- * 列定义必须与 ORM model / alembic migration 完全一致(类型、长度、可空性)。
|
||||
-- * 新增列一律建议 NULL 或带 DEFAULT,避免在大表上回填导致长时间锁表。
|
||||
-- * MySQL 8.0 在线 DDL 默认 ALGORITHM=INPLACE 一般不阻塞写入,但仍建议低峰执行。
|
||||
-- ============================================================================
|
||||
|
||||
-- ---- 通用幂等封装:仅当列不存在时才新增 --------------------------------------
|
||||
DELIMITER //
|
||||
DROP PROCEDURE IF EXISTS _add_column_if_missing //
|
||||
CREATE PROCEDURE _add_column_if_missing(
|
||||
IN p_table VARCHAR(64),
|
||||
IN p_column VARCHAR(64),
|
||||
IN p_definition VARCHAR(255) -- 列定义,如 'VARCHAR(128) NULL DEFAULT NULL'
|
||||
)
|
||||
BEGIN
|
||||
DECLARE v_exists INT DEFAULT 0;
|
||||
SELECT COUNT(*) INTO v_exists
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = p_table
|
||||
AND column_name = p_column;
|
||||
|
||||
IF v_exists = 0 THEN
|
||||
SET @ddl = CONCAT('ALTER TABLE `', p_table, '` ADD COLUMN `', p_column, '` ', p_definition);
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
SELECT CONCAT('ADDED ', p_table, '.', p_column) AS result;
|
||||
ELSE
|
||||
SELECT CONCAT('SKIPPED ', p_table, '.', p_column, ' (already exists)') AS result;
|
||||
END IF;
|
||||
END //
|
||||
DELIMITER ;
|
||||
|
||||
-- ---- skills ------------------------------------------------------------------
|
||||
-- name_zh: 技能的中文显示名(用户配置或 LLM 生成),对应 migration 20260609_01。
|
||||
-- ORM: SkillRow.name_zh = String(128), nullable=True。
|
||||
CALL _add_column_if_missing('skills', 'name_zh', 'VARCHAR(128) NULL DEFAULT NULL');
|
||||
-- detail: 技能的长文介绍,对应 migration 20260518_01。
|
||||
-- ORM: SkillRow.detail = Text, nullable=True。
|
||||
CALL _add_column_if_missing('skills', 'detail', 'TEXT NULL DEFAULT NULL');
|
||||
-- square_id: 技能所属的发布广场(发布广场功能),对应 migration 20260611_01。
|
||||
-- ORM: SkillRow.square_id = String(64), nullable=False, default=''。
|
||||
CALL _add_column_if_missing('skills', 'square_id', "VARCHAR(64) NOT NULL DEFAULT ''");
|
||||
-- always_on: 常驻(免压缩) —— 开启技能压缩后仍全文注入系统提示词,对应 migration 20260616_03。
|
||||
-- ORM: SkillRow.always_on = Boolean, nullable=False, default=False。
|
||||
CALL _add_column_if_missing('skills', 'always_on', 'TINYINT(1) NOT NULL DEFAULT 0');
|
||||
|
||||
-- ---- agents ------------------------------------------------------------------
|
||||
-- square_id: 智能体所属的发布广场(发布广场功能),对应 migration 20260611_01。
|
||||
-- ORM: AgentRow.square_id = String(64), nullable=False, default=''。
|
||||
CALL _add_column_if_missing('agents', 'square_id', "VARCHAR(64) NOT NULL DEFAULT ''");
|
||||
|
||||
-- ---- roundtable_drafts -------------------------------------------------------
|
||||
-- task_id: 外部任务 id(无界嵌入抽屉传入),对应 migration 20260611_01。非空时草稿
|
||||
-- 按 task_id 共享、不再按 user_id 分权。ORM: RoundtableDraftRow.task_id =
|
||||
-- String(128), nullable=True, index=True。
|
||||
CALL _add_column_if_missing('roundtable_drafts', 'task_id', 'VARCHAR(128) NULL DEFAULT NULL');
|
||||
|
||||
-- orchestration_plan: dag 模式分层编排计划 JSON(stages/阶段目标/总控提示词/finalSynthesis),
|
||||
-- 对应 migration 20260612_01。驱动后端 _run_dag + 前端大弹窗分层流程图。
|
||||
-- ORM: RoundtableJobRow.orchestration_plan = PortableLongText(LONGTEXT), nullable。
|
||||
CALL _add_column_if_missing('roundtable_jobs', 'orchestration_plan', 'LONGTEXT NULL DEFAULT NULL');
|
||||
|
||||
-- seat_mode: 业务链条「默认席位执行模式」(flash/thinking/pro/ultra),对应 migration 20260615_01。
|
||||
-- 选入研讨时弹窗默认用它(仍可临时改)。ORM: RoundtableChainRow.seat_mode = String(16), nullable。
|
||||
CALL _add_column_if_missing('roundtable_chains', 'seat_mode', 'VARCHAR(16) NULL DEFAULT NULL');
|
||||
|
||||
-- gather_first: 业务链条「研讨前并行取数」开关,对应 migration 20260620_02。True=选入研讨时研讨前先
|
||||
-- 跑一轮全席位并行取数(只调技能、不研讨),产出作为共享资料注入研讨轮。逐条 opt-in。
|
||||
-- ORM: RoundtableChainRow.gather_first = Boolean, NOT NULL, server_default '0'。
|
||||
CALL _add_column_if_missing('roundtable_chains', 'gather_first', 'TINYINT(1) NOT NULL DEFAULT 0');
|
||||
|
||||
-- enabled: 业务链条启停开关,对应 migration 20260819_02。True=启用(可选入研讨);
|
||||
-- False=停用(管理列表仍可见)。默认启用,存量行补列后也为 True。
|
||||
-- ORM: RoundtableChainRow.enabled = Boolean, NOT NULL, server_default '1'。
|
||||
CALL _add_column_if_missing('roundtable_chains', 'enabled', 'TINYINT(1) NOT NULL DEFAULT 1');
|
||||
|
||||
-- ---- 岗位 (positions) 订阅系统,对应 migration 20260616_01 -------------------
|
||||
-- positions / position_tag_bindings 是【新表】,由 create_all 自动建,无需补列。
|
||||
-- 以下是给【已存在的表】补的列:
|
||||
-- users.position_id: 用户所属岗位(1:1)。ORM: UserRow.position_id = String(64) NULL。
|
||||
CALL _add_column_if_missing('users', 'position_id', 'VARCHAR(64) NULL DEFAULT NULL');
|
||||
-- agent_favorites: origin 区分用户自收藏 vs 岗位授予;position_id 记授予岗位。
|
||||
CALL _add_column_if_missing('agent_favorites', 'origin', "VARCHAR(16) NOT NULL DEFAULT 'user'");
|
||||
CALL _add_column_if_missing('agent_favorites', 'position_id', 'VARCHAR(64) NULL DEFAULT NULL');
|
||||
-- scheduled_task_subscriptions: 同上。
|
||||
CALL _add_column_if_missing('scheduled_task_subscriptions', 'origin', "VARCHAR(16) NOT NULL DEFAULT 'user'");
|
||||
CALL _add_column_if_missing('scheduled_task_subscriptions', 'position_id', 'VARCHAR(64) NULL DEFAULT NULL');
|
||||
|
||||
-- knowledge_notes.folder: 用户自建目录(斜杠分隔路径),驱动产品目录树;NULL=未归类。
|
||||
-- (knowledge_folders / knowledge_extract_templates 为【新表】,由 create_all 自动建,无需在此补列。)
|
||||
CALL _add_column_if_missing('knowledge_notes', 'folder', 'VARCHAR(512) NULL DEFAULT NULL');
|
||||
|
||||
-- ---- taskId 深链会商独立存储,对应 migration 20260618_01 ----------------------
|
||||
-- roundtable_task_drafts 是【新表】(taskId 聊天记录,只按 task_id 不分权),由 create_all
|
||||
-- 自动建,无需补列。以下给【已存在的】roundtable_jobs 补 task_id 列:
|
||||
-- task_id: 非空 = task 作业(按 task 共享、不按 user 分权,报告写回 roundtable_task_drafts)。
|
||||
-- ORM: RoundtableJobRow.task_id = String(128), nullable, indexed。
|
||||
CALL _add_column_if_missing('roundtable_jobs', 'task_id', 'VARCHAR(128) NULL DEFAULT NULL');
|
||||
|
||||
-- ---- position_roundtable_sessions: opt-in task sharing, migration 20260808_01 ----
|
||||
-- Existing records remain personal (`task_scoped = 0`). New records become
|
||||
-- shared only when the caller explicitly sends a non-empty route taskId.
|
||||
CALL _add_column_if_missing('position_roundtable_sessions', 'task_scoped', 'TINYINT(1) NOT NULL DEFAULT 0');
|
||||
|
||||
-- ---- 按钮管理「外部链接」追加登录参数,对应 migration 20260625_01 ----------------
|
||||
-- login_params_json: url 外链追加的登录参数列表 JSON([{key,source,custom_value},...]),
|
||||
-- 由前端运行时从 userInfo 解析后拼到地址尾部;NULL/空 = 不追加(旧行为)。
|
||||
-- ORM: TaskButtonRow.login_params_json = PortableJSON()(MySQL 落 TEXT)。
|
||||
CALL _add_column_if_missing('task_buttons', 'login_params_json', 'TEXT NULL DEFAULT NULL');
|
||||
|
||||
-- ---- users.approved: 登录白名单放行标记(黑名单模式),对应 migration 20260625_03 ----
|
||||
-- 系统设置 login_whitelist.enabled 打开后,仅被管理员显式「取消放行」(approved=0) 的
|
||||
-- 用户被拦;新账号(含从未登录过的用户)默认 1(已放行),admin 永远放行。DEFAULT 1 让
|
||||
-- 存量用户(执行本脚本补列时)一并置为已放行,避免首次打开开关把老用户锁死。
|
||||
-- ORM: UserRow.approved = Boolean, NOT NULL, default True, server_default '1'。
|
||||
CALL _add_column_if_missing('users', 'approved', 'TINYINT(1) NOT NULL DEFAULT 1');
|
||||
|
||||
-- ---- users.login_password: 账号级「登录口令」(明文),对应 migration 20260625_04 ----
|
||||
-- 管理员在白名单管理页给个别账号设置的明文登录口令。非空 → 该账号用户名直登
|
||||
-- (/api/v1/auth/login/username) 必须带匹配的 ?password=(优先于全局共享口令门);
|
||||
-- NULL/空 → 走原有共享口令门/免口令逻辑。明文存储以便随时复制完整登录链接。
|
||||
-- ORM: UserRow.login_password = String(255), nullable。
|
||||
CALL _add_column_if_missing('users', 'login_password', 'VARCHAR(255) NULL DEFAULT NULL');
|
||||
|
||||
-- ---- compact_menu_overrides.menu_type: 简介模式菜单分套(A/B),对应 migration 20260819_01 ----
|
||||
-- 两套网页部署各自一份左侧简介菜单。存量行视为类型 A;类型 B 的 node_id 带 B:: 前缀,
|
||||
-- 以免在仍以 node_id 为单列主键的旧库上撞主键。
|
||||
-- ORM: CompactMenuOverrideRow.menu_type = String(32), NOT NULL, default A, server_default A。
|
||||
CALL _add_column_if_missing('compact_menu_overrides', 'menu_type', "VARCHAR(32) NOT NULL DEFAULT 'A'");
|
||||
|
||||
-- ---- assistant answer feedback, migration 20260916_02 -----------------------
|
||||
-- New columns first land nullable/defaulted so existing rows can be backfilled.
|
||||
-- target_key is a SHA-256 idempotency digest; a four-string utf8mb4 unique key
|
||||
-- would exceed legacy MySQL's index-byte limit.
|
||||
CALL _add_column_if_missing('feedback', 'agent_id', "VARCHAR(128) NULL DEFAULT 'lead_agent'");
|
||||
CALL _add_column_if_missing('feedback', 'target_key', 'VARCHAR(64) NULL DEFAULT NULL');
|
||||
CALL _add_column_if_missing('feedback', 'reasons', 'TEXT NULL DEFAULT NULL');
|
||||
CALL _add_column_if_missing('feedback', 'updated_at', 'DATETIME NULL DEFAULT NULL');
|
||||
|
||||
UPDATE feedback AS f
|
||||
LEFT JOIN runs AS r ON r.run_id = f.run_id
|
||||
SET f.agent_id = COALESCE(NULLIF(r.assistant_id, ''), NULLIF(f.agent_id, ''), 'lead_agent'),
|
||||
f.reasons = COALESCE(f.reasons, '[]'),
|
||||
f.updated_at = COALESCE(f.updated_at, f.created_at);
|
||||
UPDATE feedback
|
||||
SET target_key = SHA2(
|
||||
CONCAT(
|
||||
COALESCE(user_id, ''), CHAR(31),
|
||||
thread_id, CHAR(31),
|
||||
COALESCE(message_id, CONCAT('run:', run_id)), CHAR(31),
|
||||
agent_id
|
||||
),
|
||||
256
|
||||
)
|
||||
WHERE target_key IS NULL OR target_key = '';
|
||||
-- Legacy anonymous rows could bypass the old three-column unique index because
|
||||
-- MySQL treats NULL values as distinct. Keep one canonical deterministic key
|
||||
-- and make only duplicate historical rows unique by their feedback id.
|
||||
UPDATE feedback AS duplicate_row
|
||||
JOIN feedback AS canonical_row
|
||||
ON canonical_row.target_key = duplicate_row.target_key
|
||||
AND canonical_row.feedback_id < duplicate_row.feedback_id
|
||||
SET duplicate_row.target_key = SHA2(
|
||||
CONCAT(duplicate_row.target_key, CHAR(31), 'legacy:', duplicate_row.feedback_id),
|
||||
256
|
||||
);
|
||||
|
||||
ALTER TABLE feedback
|
||||
MODIFY COLUMN agent_id VARCHAR(128) NOT NULL,
|
||||
MODIFY COLUMN target_key VARCHAR(64) NOT NULL,
|
||||
MODIFY COLUMN reasons TEXT NOT NULL,
|
||||
MODIFY COLUMN updated_at DATETIME NOT NULL;
|
||||
|
||||
SET @idx_exists = (
|
||||
SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'feedback'
|
||||
AND index_name = 'uq_feedback_thread_run_user'
|
||||
);
|
||||
SET @idx_ddl = IF(@idx_exists > 0,
|
||||
'ALTER TABLE `feedback` DROP INDEX `uq_feedback_thread_run_user`',
|
||||
'SELECT ''SKIPPED uq_feedback_thread_run_user (not present)'' AS result');
|
||||
PREPARE stmt FROM @idx_ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @idx_exists = (
|
||||
SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'feedback'
|
||||
AND index_name = 'uq_feedback_target_key'
|
||||
);
|
||||
SET @idx_ddl = IF(@idx_exists = 0,
|
||||
'CREATE UNIQUE INDEX `uq_feedback_target_key` ON `feedback` (`target_key`)',
|
||||
'SELECT ''SKIPPED uq_feedback_target_key (already exists)'' AS result');
|
||||
PREPARE stmt FROM @idx_ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @idx_exists = (
|
||||
SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'feedback'
|
||||
AND index_name = 'ix_feedback_agent_created'
|
||||
);
|
||||
SET @idx_ddl = IF(@idx_exists = 0,
|
||||
'CREATE INDEX `ix_feedback_agent_created` ON `feedback` (`agent_id`, `created_at`)',
|
||||
'SELECT ''SKIPPED ix_feedback_agent_created (already exists)'' AS result');
|
||||
PREPARE stmt FROM @idx_ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- ---- llmwiki_wiki_sync_states.last_sync_result -----------------------------
|
||||
-- 最近一次增量同步的租约围栏统计(JSON;PortableJSON 在 MySQL 落 TEXT)。
|
||||
-- 对应 reconciliation migration 20260920_02。
|
||||
CALL _add_column_if_missing('llmwiki_wiki_sync_states', 'last_sync_result', 'TEXT NULL');
|
||||
|
||||
-- ---- roundtable_drafts.task_id 索引(幂等) -----------------------------------
|
||||
SET @idx_exists = (
|
||||
SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'roundtable_drafts'
|
||||
AND index_name = 'ix_roundtable_drafts_task_id'
|
||||
);
|
||||
SET @idx_ddl = IF(@idx_exists = 0,
|
||||
'CREATE INDEX `ix_roundtable_drafts_task_id` ON `roundtable_drafts` (`task_id`)',
|
||||
'SELECT ''SKIPPED ix_roundtable_drafts_task_id (already exists)'' AS result');
|
||||
PREPARE stmt FROM @idx_ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- ---- knowledge_notes.folder 索引(幂等) --------------------------------------
|
||||
SET @idx_exists = (
|
||||
SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'knowledge_notes'
|
||||
AND index_name = 'ix_knowledge_notes_folder'
|
||||
);
|
||||
SET @idx_ddl = IF(@idx_exists = 0,
|
||||
'CREATE INDEX `ix_knowledge_notes_folder` ON `knowledge_notes` (`folder`(191))',
|
||||
'SELECT ''SKIPPED ix_knowledge_notes_folder (already exists)'' AS result');
|
||||
PREPARE stmt FROM @idx_ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- ---- roundtable_jobs.task_id 索引(幂等) -------------------------------------
|
||||
SET @idx_exists = (
|
||||
SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'roundtable_jobs'
|
||||
AND index_name = 'ix_roundtable_jobs_task_id'
|
||||
);
|
||||
SET @idx_ddl = IF(@idx_exists = 0,
|
||||
'CREATE INDEX `ix_roundtable_jobs_task_id` ON `roundtable_jobs` (`task_id`)',
|
||||
'SELECT ''SKIPPED ix_roundtable_jobs_task_id (already exists)'' AS result');
|
||||
PREPARE stmt FROM @idx_ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- ---- task_buttons ------------------------------------------------------------
|
||||
-- 任务工作区跳转按钮的「追加登录态」选项,对应 migration 20260625_02。url 按钮可在外链
|
||||
-- 后追加 token(token 登录的上游 token)+ username(登录用户名),供外部系统识别当前用户。
|
||||
-- ORM: TaskButtonRow.append_auth = Boolean NOT NULL default False;
|
||||
-- auth_token_param / auth_name_param = String(64), nullable, 读时归一化为 token/username。
|
||||
CALL _add_column_if_missing('task_buttons', 'append_auth', 'TINYINT(1) NOT NULL DEFAULT 0');
|
||||
CALL _add_column_if_missing('task_buttons', 'auth_token_param', "VARCHAR(64) NULL DEFAULT 'token'");
|
||||
CALL _add_column_if_missing('task_buttons', 'auth_name_param', "VARCHAR(64) NULL DEFAULT 'username'");
|
||||
|
||||
-- ---- report_structures.retrieval_directions ---------------------------------
|
||||
-- 报告结构可选「检索方向」JSON 列表。空 / NULL = 沿用原收集员拆词检索;
|
||||
-- 非空则素材收集按指定方向检索。ORM: PortableJSON() 在 MySQL 落 TEXT。
|
||||
CALL _add_column_if_missing('report_structures', 'retrieval_directions', 'TEXT NULL');
|
||||
|
||||
-- ---- report_structures.retrieval_skills -------------------------------------
|
||||
-- 报告结构可选「检索来源」JSON 列表(技能名)。空 / NULL = 沿用原收集员工具;
|
||||
-- 非空则素材收集调用指定技能。ORM: PortableJSON() 在 MySQL 落 TEXT。
|
||||
CALL _add_column_if_missing('report_structures', 'retrieval_skills', 'TEXT NULL');
|
||||
|
||||
-- ---- 执行后自检:确认本批次涉及的列都已存在 ---------------------------------
|
||||
SELECT table_name, column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND ((table_name = 'skills' AND column_name IN ('name_zh', 'detail', 'square_id', 'always_on'))
|
||||
OR (table_name = 'agents' AND column_name IN ('square_id'))
|
||||
OR (table_name = 'roundtable_drafts' AND column_name = 'task_id')
|
||||
OR (table_name = 'roundtable_jobs' AND column_name = 'task_id')
|
||||
OR (table_name = 'position_roundtable_sessions' AND column_name = 'task_scoped')
|
||||
OR (table_name = 'task_buttons' AND column_name IN ('append_auth', 'auth_token_param', 'auth_name_param'))
|
||||
OR (table_name = 'compact_menu_overrides' AND column_name = 'menu_type')
|
||||
-- ---- llmwiki_knowledge_bases retrieval profile -----------------------------
|
||||
-- 映射级测试问题与检索参数。ORM: PortableJSON() 在 MySQL 落 TEXT。
|
||||
-- 对应 migration 20260920_01;20260920_02 会兼容修复旧歧义版本号漏列。
|
||||
CALL _add_column_if_missing('llmwiki_knowledge_bases', 'test_questions', 'TEXT NULL');
|
||||
CALL _add_column_if_missing('llmwiki_knowledge_bases', 'retrieval_settings', 'TEXT NULL');
|
||||
|
||||
-- ---- llmwiki_knowledge_bases reference parent directories ------------------
|
||||
-- DeerFlow 持有的 Wiki 参考父级目录 JSON 列表。ORM: PortableJSON() 在 MySQL 落 TEXT。
|
||||
-- 对应 migration 20261006_01。
|
||||
CALL _add_column_if_missing('llmwiki_knowledge_bases', 'wiki_parent_directories', 'TEXT NULL');
|
||||
|
||||
-- ---- llmwiki_knowledge_bases Wiki generation config ------------------------
|
||||
-- 目录树、重点要求与高级指令均保存 raw 值;有效 WeKnora 指令由 Gateway 编译。
|
||||
-- 对应 migration 20261014_02。PortableJSON() 在 MySQL 落 TEXT。
|
||||
CALL _add_column_if_missing('llmwiki_knowledge_bases', 'wiki_directory_paths', 'TEXT NULL');
|
||||
CALL _add_column_if_missing('llmwiki_knowledge_bases', 'wiki_focus_instructions', 'TEXT NULL');
|
||||
CALL _add_column_if_missing('llmwiki_knowledge_bases', 'wiki_content_instructions_raw', 'TEXT NULL');
|
||||
CALL _add_column_if_missing('llmwiki_knowledge_bases', 'wiki_extraction_instructions_raw', 'TEXT NULL');
|
||||
CALL _add_column_if_missing('llmwiki_knowledge_bases', 'wiki_generation_managed', 'TINYINT(1) NULL');
|
||||
CALL _add_column_if_missing('llmwiki_knowledge_bases', 'wiki_config_schema_version', 'INT NULL');
|
||||
CALL _add_column_if_missing('llmwiki_knowledge_bases', 'wiki_config_revision', 'INT NULL');
|
||||
CALL _add_column_if_missing('llmwiki_knowledge_bases', 'wiki_config_apply_status', 'VARCHAR(32) NULL');
|
||||
CALL _add_column_if_missing('llmwiki_knowledge_bases', 'wiki_config_apply_error', 'TEXT NULL');
|
||||
|
||||
-- ---- knowledge_retrieval_settings management UI ---------------------------
|
||||
-- 高级知识库管理入口总开关;默认关闭。对应 migration 20261019_01。
|
||||
CALL _add_column_if_missing('knowledge_retrieval_settings', 'advanced_management_enabled', 'TINYINT(1) NOT NULL DEFAULT 0');
|
||||
|
||||
-- 所有 CALL 必须位于 DROP 之前;否则后半段补列会因存储过程已删除而失败。
|
||||
DROP PROCEDURE IF EXISTS _add_column_if_missing;
|
||||
|
||||
-- ---- 执行后自检:确认本批次涉及的列都已存在 ---------------------------------
|
||||
SELECT table_name, column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND ((table_name = 'skills' AND column_name IN ('name_zh', 'detail', 'square_id', 'always_on'))
|
||||
OR (table_name = 'agents' AND column_name IN ('square_id'))
|
||||
OR (table_name = 'roundtable_drafts' AND column_name = 'task_id')
|
||||
OR (table_name = 'roundtable_jobs' AND column_name = 'task_id')
|
||||
OR (table_name = 'position_roundtable_sessions' AND column_name = 'task_scoped')
|
||||
OR (table_name = 'task_buttons' AND column_name IN ('append_auth', 'auth_token_param', 'auth_name_param'))
|
||||
OR (table_name = 'compact_menu_overrides' AND column_name = 'menu_type')
|
||||
OR (table_name = 'roundtable_chains' AND column_name = 'enabled')
|
||||
OR (table_name = 'llmwiki_wiki_sync_states' AND column_name = 'last_sync_result')
|
||||
OR (table_name = 'knowledge_retrieval_settings' AND column_name = 'advanced_management_enabled')
|
||||
OR (table_name = 'llmwiki_knowledge_bases' AND column_name IN ('test_questions', 'retrieval_settings', 'wiki_parent_directories', 'wiki_directory_paths', 'wiki_focus_instructions', 'wiki_content_instructions_raw', 'wiki_extraction_instructions_raw', 'wiki_generation_managed', 'wiki_config_schema_version', 'wiki_config_revision', 'wiki_config_apply_status', 'wiki_config_apply_error'))
|
||||
OR (table_name = 'report_structures' AND column_name IN ('retrieval_directions', 'retrieval_skills')))
|
||||
ORDER BY table_name, column_name;
|
||||
ORDER BY table_name, column_name;
|
||||
Reference in new issue
Block a user