feat(v3): T26 两级智能体(大模型规划/审查 + 本地小模型执行,D7)
- run_dual 编排:规划者两阶段 JSON(plan/review,失败回喂重试一次再降级),
redo 时裁决意见回喂执行者,交接上限 agent.max_handoffs(默认 2)
- 交接文档 agent_runs/{id}/handoff.json(智能体版交流文本:instructions/acceptance/exchanges)
- /agent 新增 executor_pool_id;规划者==执行者条目拒绝;整体 token_cap 覆盖两级调用
- ToolLoop 增 emit_final 开关(内层循环不发终态,防前端 SSE 提前收口)
- 前端:执行者选择器 + phase/message 事件渲染(阶段徽标 + 双色消息卡)
- 测试 +3(done/redo/执行者故障),全量 277 passed
- fix(tests): test_config_get_put_reset 增加设置备份/恢复隔离,防止清掉用户真实配置
This commit is contained in:
+215
-21
@@ -41,6 +41,42 @@ AGENT_SYSTEM_PROMPT = (
|
||||
"直接输出给用户的最终答复(中文,不要再调用工具)。"
|
||||
)
|
||||
|
||||
# ── 两级智能体(D7):规划者(大模型)+ 执行者(本地小模型),交接走 handoff 文档 ──
|
||||
PLANNER_SYSTEM_PROMPT = (
|
||||
"你是两级智能体中的**规划者**(大模型)。执行者是一个能力有限的本地小模型,"
|
||||
"只能机械地使用工具。你的职责:把用户任务拆成执行者可照做的**具体指令**,"
|
||||
"并在执行后审查其汇报。输出必须是合法 JSON 对象(不要 markdown 围栏)。"
|
||||
)
|
||||
EXECUTOR_SYSTEM_PROMPT = (
|
||||
"你是两级智能体中的**执行者**(本地小模型)。规划者已给你具体指令,"
|
||||
"你只负责用工具完成指令并在最后**汇报**:做了什么、结果如何、有什么问题。"
|
||||
"严格遵守指令范围,不要自行扩大任务。汇报用中文,是给规划者看的,"
|
||||
"要列出:修改的文件、关键命令输出、未完成项。"
|
||||
)
|
||||
|
||||
# 规划者首轮:产出指令(JSON)
|
||||
_PLAN_SCHEMA_HINT = {
|
||||
"instructions": "string(给执行者的具体步骤指令,<=600字)",
|
||||
"acceptance": "string(验收标准,<=200字)",
|
||||
}
|
||||
# 规划者审查轮:裁决(JSON)
|
||||
_REVIEW_SCHEMA_HINT = {
|
||||
"verdict": "enum(done|redo)",
|
||||
"reply_to_executor": "string(verdict=redo 时给执行者的补充指令;done 时可空)",
|
||||
"final_answer": "string(verdict=done 时给用户的最终答复)",
|
||||
}
|
||||
|
||||
DEFAULT_MAX_HANDOFFS = 2 # 规划者<->执行者交接轮数上限
|
||||
|
||||
|
||||
def _parse_json_loose(content: str) -> Dict[str, Any]:
|
||||
"""宽松解析规划者的 JSON 输出(剥围栏/取首个对象);失败返回 {}。"""
|
||||
try:
|
||||
from router_system.architect import ArchitectClient
|
||||
return ArchitectClient._parse_json(content)
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# OpenAI 兼容工具调用客户端
|
||||
@@ -129,6 +165,8 @@ class AgentRunInfo:
|
||||
completion_tokens: int = 0
|
||||
pool_id: str = ""
|
||||
workspace: str = "" # 本次运行使用的工作区根目录(绝对路径)
|
||||
executor_model: str = "" # 两级模式:执行者模型名(空 = 单模型模式)
|
||||
mode: str = "single" # single | dual
|
||||
asyncio_task: Optional[asyncio.Task] = field(default=None, repr=False)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
@@ -146,6 +184,8 @@ class AgentRunInfo:
|
||||
"completion_tokens": self.completion_tokens,
|
||||
"pool_id": self.pool_id,
|
||||
"workspace": self.workspace,
|
||||
"executor_model": self.executor_model,
|
||||
"mode": self.mode,
|
||||
}
|
||||
|
||||
|
||||
@@ -169,12 +209,15 @@ class AgentService:
|
||||
|
||||
# ---------- 注册与查询 ----------
|
||||
def register(self, request_id: str, task: str, model: str, pool_id: str,
|
||||
workspace: str = "") -> Optional[AgentRunInfo]:
|
||||
workspace: str = "", executor_model: str = "",
|
||||
mode: str = "single") -> Optional[AgentRunInfo]:
|
||||
running = [r for r in self._runs.values() if r.state == STATE_RUNNING]
|
||||
if len(running) >= self.max_running:
|
||||
return None
|
||||
info = AgentRunInfo(request_id=request_id, task=task, model=model,
|
||||
pool_id=pool_id, workspace=workspace, started_at=time.time())
|
||||
pool_id=pool_id, workspace=workspace,
|
||||
executor_model=executor_model, mode=mode,
|
||||
started_at=time.time())
|
||||
self._runs[request_id] = info
|
||||
self._dir(request_id).mkdir(parents=True, exist_ok=True)
|
||||
self._write_status(info)
|
||||
@@ -186,27 +229,28 @@ class AgentService:
|
||||
# ---------- 执行 ----------
|
||||
async def run(self, info: AgentRunInfo, chat: Any, workspace_dir: str | Path,
|
||||
max_rounds: int = 8, token_cap: int = 0,
|
||||
allow_shell: bool = False, shell_timeout_s: int = 20) -> None:
|
||||
"""执行智能体任务(由调用方包成后台协程)。"""
|
||||
tools = WorkspaceTools(workspace_dir, allow_shell=allow_shell,
|
||||
shell_timeout_s=shell_timeout_s)
|
||||
loop = ToolLoop(tools, chat, max_rounds=max_rounds, token_cap=token_cap,
|
||||
on_event=self._make_event_writer(info))
|
||||
allow_shell: bool = False, shell_timeout_s: int = 20,
|
||||
executor_chat: Any = None,
|
||||
max_handoffs: int = DEFAULT_MAX_HANDOFFS) -> None:
|
||||
"""执行智能体任务(由调用方包成后台协程)。
|
||||
|
||||
executor_chat 为空 = 单模型模式(chat 全程包办);
|
||||
提供时进入两级模式:chat 作规划者,executor_chat 作执行者(D7)。
|
||||
"""
|
||||
try:
|
||||
result = await loop.run(info.task, system=AGENT_SYSTEM_PROMPT)
|
||||
info.response = result.get("response", "")
|
||||
info.rounds = int(result.get("rounds", 0))
|
||||
info.prompt_tokens = int(result.get("prompt_tokens", 0))
|
||||
info.completion_tokens = int(result.get("completion_tokens", 0))
|
||||
if result.get("reason") == "error":
|
||||
info.state = STATE_FAILED
|
||||
info.error = result.get("error")
|
||||
elif result.get("reason") in ("token_cap", "max_rounds"):
|
||||
# 触顶属于护栏行为:结果仍交付,但标记部分完成信息
|
||||
info.state = STATE_DONE
|
||||
info.error = result.get("error")
|
||||
if executor_chat is not None:
|
||||
result = await self.run_dual(
|
||||
info, chat, executor_chat, workspace_dir,
|
||||
max_rounds=max_rounds, token_cap=token_cap,
|
||||
allow_shell=allow_shell, shell_timeout_s=shell_timeout_s,
|
||||
max_handoffs=max_handoffs)
|
||||
else:
|
||||
info.state = STATE_DONE
|
||||
tools = WorkspaceTools(workspace_dir, allow_shell=allow_shell,
|
||||
shell_timeout_s=shell_timeout_s)
|
||||
loop = ToolLoop(tools, chat, max_rounds=max_rounds, token_cap=token_cap,
|
||||
on_event=self._make_event_writer(info))
|
||||
result = await loop.run(info.task, system=AGENT_SYSTEM_PROMPT)
|
||||
self._apply_result(info, result)
|
||||
except Exception as exc: # pragma: no cover
|
||||
info.state = STATE_FAILED
|
||||
info.error = f"{type(exc).__name__}: {exc}"
|
||||
@@ -216,6 +260,156 @@ class AgentService:
|
||||
info.finished_at = time.time()
|
||||
self._write_status(info)
|
||||
|
||||
def _apply_result(self, info: AgentRunInfo, result: Dict[str, Any]) -> None:
|
||||
"""把循环结果落到运行状态(单/两级模式共用)。"""
|
||||
info.response = result.get("response", "")
|
||||
info.rounds = int(result.get("rounds", 0))
|
||||
info.prompt_tokens = int(result.get("prompt_tokens", 0))
|
||||
info.completion_tokens = int(result.get("completion_tokens", 0))
|
||||
if result.get("reason") == "error":
|
||||
info.state = STATE_FAILED
|
||||
info.error = result.get("error")
|
||||
elif result.get("reason") in ("token_cap", "max_rounds", "max_handoffs"):
|
||||
# 触顶属于护栏行为:结果仍交付,但标记部分完成信息
|
||||
info.state = STATE_DONE
|
||||
info.error = result.get("error")
|
||||
else:
|
||||
info.state = STATE_DONE
|
||||
|
||||
# ---------- 两级模式(D7):规划者 + 执行者 ----------
|
||||
async def run_dual(self, info: AgentRunInfo, planner_chat: Any, executor_chat: Any,
|
||||
workspace_dir: str | Path, max_rounds: int = 8,
|
||||
token_cap: int = 0, allow_shell: bool = False,
|
||||
shell_timeout_s: int = 20,
|
||||
max_handoffs: int = DEFAULT_MAX_HANDOFFS) -> Dict[str, Any]:
|
||||
"""大模型拆解/审查 + 小模型执行工具轮,交接状态写 handoff.json(智能体版交流文本)。"""
|
||||
info.mode = "dual"
|
||||
tools = WorkspaceTools(workspace_dir, allow_shell=allow_shell,
|
||||
shell_timeout_s=shell_timeout_s)
|
||||
handoff: Dict[str, Any] = {
|
||||
"task": info.task, "planner_model": info.model,
|
||||
"executor_model": info.executor_model, "workspace": info.workspace,
|
||||
"instructions": "", "acceptance": "", "exchanges": [],
|
||||
}
|
||||
spent = {"in": 0, "out": 0}
|
||||
total_rounds = 0
|
||||
|
||||
def _account(usage: Dict[str, Any] | None) -> None:
|
||||
spent["in"] += int((usage or {}).get("prompt_tokens", 0))
|
||||
spent["out"] += int((usage or {}).get("completion_tokens", 0))
|
||||
|
||||
def _save_handoff() -> None:
|
||||
try:
|
||||
(self._dir(info.request_id) / "handoff.json").write_text(
|
||||
json.dumps(handoff, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def _remaining_cap() -> int:
|
||||
return (token_cap - spent["in"] - spent["out"]) if token_cap else 1
|
||||
|
||||
async def _planner_json(user_msg: str) -> Dict[str, Any]:
|
||||
"""调规划者并解析 JSON;解析失败回喂重试一次,再失败降级为 {}(禁止带病继续的软版本)。"""
|
||||
messages = [{"role": "system", "content": PLANNER_SYSTEM_PROMPT},
|
||||
{"role": "user", "content": user_msg}]
|
||||
content = ""
|
||||
for attempt in (1, 2):
|
||||
resp = await planner_chat(messages, [])
|
||||
_account(resp.get("usage"))
|
||||
content = resp.get("content") or ""
|
||||
obj = _parse_json_loose(content)
|
||||
if obj:
|
||||
break
|
||||
if attempt == 1:
|
||||
messages += [{"role": "assistant", "content": content},
|
||||
{"role": "user",
|
||||
"content": "你的输出不是合法 JSON。请重新只输出合法 JSON 对象。"}]
|
||||
self._append_event(info, {"type": "message", "role": "planner",
|
||||
"content": content[:2000]})
|
||||
return obj
|
||||
|
||||
try:
|
||||
# ---- 阶段 1:规划(大模型拆解为执行者指令) ----
|
||||
self._append_event(info, {"type": "phase", "phase": "plan", "model": info.model})
|
||||
plan = await _planner_json(
|
||||
f"用户任务:{info.task}\n\n"
|
||||
"请产出给执行者的指令,仅输出符合如下结构的 JSON:\n"
|
||||
+ json.dumps(_PLAN_SCHEMA_HINT, ensure_ascii=False))
|
||||
instructions = (plan.get("instructions") or info.task).strip()
|
||||
handoff["instructions"] = instructions
|
||||
handoff["acceptance"] = str(plan.get("acceptance", ""))
|
||||
_save_handoff()
|
||||
|
||||
final_text = ""
|
||||
reason = "answer"
|
||||
error = None
|
||||
exec_rounds_total = 0
|
||||
|
||||
# ---- 阶段 2/3:执行 <-> 审查(有界交接) ----
|
||||
for h in range(1, max_handoffs + 1):
|
||||
# 执行(本地小模型跑工具轮)
|
||||
self._append_event(info, {"type": "phase", "phase": "execute",
|
||||
"handoff": h, "model": info.executor_model})
|
||||
loop = ToolLoop(tools, executor_chat, max_rounds=max_rounds,
|
||||
token_cap=max(1, _remaining_cap()),
|
||||
on_event=self._make_event_writer(info),
|
||||
emit_final=False)
|
||||
exec_result = await loop.run(instructions, system=EXECUTOR_SYSTEM_PROMPT)
|
||||
_account({"prompt_tokens": exec_result.get("prompt_tokens", 0),
|
||||
"completion_tokens": exec_result.get("completion_tokens", 0)})
|
||||
exec_rounds_total += int(exec_result.get("rounds", 0))
|
||||
report = exec_result.get("response", "")
|
||||
# 执行者汇报作为消息事件透出(前端可读)
|
||||
self._append_event(info, {"type": "message", "role": "executor",
|
||||
"handoff": h, "content": (report or "")[:4000]})
|
||||
if exec_result.get("reason") == "error":
|
||||
reason, error = "error", exec_result.get("error")
|
||||
final_text = report
|
||||
break
|
||||
|
||||
# 审查(大模型裁决)
|
||||
self._append_event(info, {"type": "phase", "phase": "review",
|
||||
"handoff": h, "model": info.model})
|
||||
review = await _planner_json(
|
||||
f"用户任务:{info.task}\n你之前给出的指令:{instructions}\n"
|
||||
f"验收标准:{handoff['acceptance'] or '(未明确)'}\n\n"
|
||||
f"执行者第 {h} 轮汇报:\n{report[:4000]}\n\n"
|
||||
"请审查是否已按验收标准完成,仅输出符合如下结构的 JSON:\n"
|
||||
+ json.dumps(_REVIEW_SCHEMA_HINT, ensure_ascii=False))
|
||||
verdict = str(review.get("verdict", "done")).lower()
|
||||
handoff["exchanges"].append({
|
||||
"handoff": h, "executor_report": report,
|
||||
"verdict": verdict,
|
||||
"reply_to_executor": str(review.get("reply_to_executor", "")),
|
||||
})
|
||||
_save_handoff()
|
||||
|
||||
if verdict == "done":
|
||||
final_text = str(review.get("final_answer") or report)
|
||||
break
|
||||
# redo:裁决意见作为下一轮执行者指令(带上一轮上下文)
|
||||
instructions = str(review.get("reply_to_executor") or instructions)
|
||||
if h == max_handoffs:
|
||||
reason = "max_handoffs"
|
||||
error = f"交接轮数达上限({max_handoffs}),以执行者汇报收尾"
|
||||
final_text = report
|
||||
else:
|
||||
final_text = final_text or ""
|
||||
|
||||
self._append_event(info, {"type": "final", "round": total_rounds + exec_rounds_total,
|
||||
"reason": reason, "error": error})
|
||||
return {"response": final_text, "rounds": total_rounds + exec_rounds_total,
|
||||
"reason": reason, "error": error,
|
||||
"prompt_tokens": spent["in"], "completion_tokens": spent["out"]}
|
||||
except Exception as exc:
|
||||
reason = "error"
|
||||
error = f"{type(exc).__name__}: {exc}"
|
||||
self._append_event(info, {"type": "final", "round": total_rounds,
|
||||
"reason": reason, "error": error})
|
||||
return {"response": "", "rounds": total_rounds, "reason": reason,
|
||||
"error": error,
|
||||
"prompt_tokens": spent["in"], "completion_tokens": spent["out"]}
|
||||
|
||||
# ---------- 事件 ----------
|
||||
def _make_event_writer(self, info: AgentRunInfo):
|
||||
def _on_event(ev: Dict[str, Any]) -> None:
|
||||
|
||||
+36
-2
@@ -530,13 +530,44 @@ try:
|
||||
workspace_dir = agent_cfg.get("workspace_dir", "agent_workspace")
|
||||
|
||||
chat, model, used_pool_id = _resolve_agent_chat(pool_id)
|
||||
|
||||
# 两级模式(D7):显式指定执行者(本地小模型)时,规划=chat、执行=executor_chat
|
||||
executor_pool_id = str((req or {}).get("executor_pool_id") or "").strip()
|
||||
executor_chat = None
|
||||
executor_model = ""
|
||||
if executor_pool_id:
|
||||
entry = get_pool().get(executor_pool_id)
|
||||
if entry is None:
|
||||
raise HTTPException(status_code=400,
|
||||
detail=f"执行者条目不存在: {executor_pool_id}")
|
||||
if entry["backend"] == "mock":
|
||||
raise HTTPException(status_code=400,
|
||||
detail="mock 模型不能担任执行者,请选择 llama_server 或 openai 条目")
|
||||
from gateway.agent import OpenAICompatChat
|
||||
executor_chat = OpenAICompatChat(
|
||||
base_url=entry["base_url"] or "http://127.0.0.1:8901/v1",
|
||||
api_key=entry.get("api_key") or None,
|
||||
model=entry["model"],
|
||||
temperature=float(entry.get("temperature", 0.3)),
|
||||
max_tokens=int(entry.get("max_tokens", 4096)))
|
||||
executor_model = f"{entry['name']}({entry['model']})"
|
||||
|
||||
if executor_chat is not None and executor_pool_id == used_pool_id:
|
||||
raise HTTPException(status_code=400,
|
||||
detail="规划者与执行者是同一个模型,两级模式无意义;请更换执行者条目")
|
||||
|
||||
service = get_agent_service()
|
||||
request_id = new_request_id()
|
||||
mode = "dual" if executor_chat is not None else "single"
|
||||
info = service.register(request_id, task, model, used_pool_id,
|
||||
workspace=workspace_dir)
|
||||
workspace=workspace_dir,
|
||||
executor_model=executor_model, mode=mode)
|
||||
if info is None:
|
||||
raise HTTPException(status_code=503, detail="智能体同时运行任务已达上限")
|
||||
|
||||
s = settings_store().to_dict()
|
||||
agent_cfg = s.get("agent", {})
|
||||
|
||||
async def _run():
|
||||
try:
|
||||
await service.run(
|
||||
@@ -546,6 +577,8 @@ try:
|
||||
token_cap=int(agent_cfg.get("token_cap", 20000)),
|
||||
allow_shell=bool(agent_cfg.get("allow_shell", False)),
|
||||
shell_timeout_s=int(agent_cfg.get("shell_timeout_s", 20)),
|
||||
executor_chat=executor_chat,
|
||||
max_handoffs=int(agent_cfg.get("max_handoffs", 2)),
|
||||
)
|
||||
except Exception as exc:
|
||||
import traceback
|
||||
@@ -557,7 +590,8 @@ try:
|
||||
|
||||
info.asyncio_task = asyncio.create_task(_run())
|
||||
return {"request_id": request_id, "status": "running", "model": model,
|
||||
"workspace": workspace_dir}
|
||||
"workspace": workspace_dir, "mode": mode,
|
||||
"executor_model": executor_model}
|
||||
|
||||
@app.get("/agent/fs", tags=["agent"])
|
||||
async def agent_fs_browse(path: str = ""):
|
||||
|
||||
@@ -43,6 +43,7 @@ DEFAULTS: Dict[str, Any] = {
|
||||
"token_cap": 20000, # 单次智能体任务 token 熔断
|
||||
"allow_shell": False, # 允许 run_command 执行 shell(默认关)
|
||||
"shell_timeout_s": 20, # shell 命令超时
|
||||
"max_handoffs": 2, # 两级模式:规划者<->执行者交接轮数上限
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
||||
import{A as e,D as t,G as n,I as r,L as i,N as a,O as o,P as s,V as c,W as l,j as u,k as d,s as f,t as p}from"./index-Bi4FCB5S.js";var m={class:`metrics-view`},h={key:0,class:`loading`},g={key:1,class:`error`},_={class:`card-grid`},v={class:`metric-card`},y={class:`kv-list`},b={class:`metric-card`},x={class:`kv-list`},S={key:0,class:`metric-card highlight`},C={class:`kv-list`},w={key:0},T={key:1},E={key:1,class:`metric-card`},D={class:`by-model`},O={class:`mono`},k={key:2,class:`metric-card review-card`},A={class:`review-stats`},j={class:`stat-item`},M={class:`stat-num`},N={class:`stat-item`},P={class:`stat-num`},F={key:0,class:`progress-wrap`},I={class:`review-rate`},L={class:`raw-json`},R=p(a({__name:`MetricsView`,setup(a){let p=c(null),R=c(!1),z=c(``),B=o(()=>p.value?.v2?.by_model||null);async function V(){R.value=!0,z.value=``;try{p.value=await f()}catch(e){z.value=e instanceof Error?e.message:`指标加载失败,请检查后端服务`}finally{R.value=!1}}return s(V),(a,o)=>(r(),u(`div`,m,[d(`header`,{class:`metrics-header`},[o[0]||=d(`h2`,null,`系统指标`,-1),d(`button`,{class:`refresh`,onClick:V},`🔄 刷新`)]),R.value?(r(),u(`div`,h,`加载中…`)):z.value?(r(),u(`div`,g,n(z.value),1)):p.value?(r(),u(t,{key:2},[d(`div`,_,[d(`div`,v,[o[1]||=d(`h3`,null,`路由器(v1)`,-1),d(`div`,y,[(r(!0),u(t,null,i(p.value.router,(e,i)=>(r(),u(t,{key:i},[d(`span`,null,n(i),1),d(`b`,null,n(e),1)],64))),128))])]),d(`div`,b,[o[2]||=d(`h3`,null,`缓存`,-1),d(`div`,x,[(r(!0),u(t,null,i(p.value.cache,(e,i)=>(r(),u(t,{key:i},[d(`span`,null,n(i),1),d(`b`,null,n(e),1)],64))),128))])]),p.value.v2?(r(),u(`div`,S,[o[3]||=d(`h3`,null,`协作管线(v2)`,-1),d(`div`,C,[(r(!0),u(t,null,i(p.value.v2,(i,a)=>(r(),u(t,{key:a},[a===`by_model`?e(``,!0):(r(),u(`span`,w,n(a),1)),a===`by_model`?e(``,!0):(r(),u(`b`,T,n(i),1))],64))),128))])])):e(``,!0),B.value&&Object.keys(B.value).length?(r(),u(`div`,E,[o[5]||=d(`h3`,null,`按模型分账(token / 成本)`,-1),d(`table`,D,[o[4]||=d(`thead`,null,[d(`tr`,null,[d(`th`,null,`模型`),d(`th`,null,`次数`),d(`th`,null,`入`),d(`th`,null,`出`),d(`th`,null,`成本 $`)])],-1),d(`tbody`,null,[(r(!0),u(t,null,i(B.value,(e,t)=>(r(),u(`tr`,{key:t},[d(`td`,O,n(t),1),d(`td`,null,n(e.requests),1),d(`td`,null,n(e.input_tokens),1),d(`td`,null,n(e.output_tokens),1),d(`td`,null,n(e.cost_est_usd),1)]))),128))])]),o[6]||=d(`p`,{class:`hint`},`单价来自模型池条目($/1M tokens);经典设置下的模型成本不计入。`,-1)])):e(``,!0),p.value.review?(r(),u(`div`,k,[o[9]||=d(`h3`,null,`人工检验`,-1),d(`div`,A,[d(`div`,j,[d(`span`,M,n(p.value.review.pending),1),o[7]||=d(`span`,{class:`stat-label`},`待审核`,-1)]),d(`div`,N,[d(`span`,P,n(p.value.review.total),1),o[8]||=d(`span`,{class:`stat-label`},`总提交`,-1)])]),p.value.review.total>0?(r(),u(`div`,F,[d(`div`,{class:`reviewed-bar`,style:l({width:`${(p.value.review.total-p.value.review.pending)/p.value.review.total*100}%`})},null,4)])):e(``,!0),d(`p`,I,` 通过率: `+n(((p.value.review.total-p.value.review.pending)/p.value.review.total*100).toFixed(1))+`% `,1)])):e(``,!0)]),d(`details`,L,[o[10]||=d(`summary`,null,`原始 JSON`,-1),d(`pre`,null,n(JSON.stringify(p.value,null,2)),1)])],64)):e(``,!0)]))}}),[[`__scopeId`,`data-v-ba641559`]]);export{R as default};
|
||||
import{A as e,D as t,G as n,I as r,L as i,N as a,O as o,P as s,V as c,W as l,j as u,k as d,s as f,t as p}from"./index-DtjeaX4S.js";var m={class:`metrics-view`},h={key:0,class:`loading`},g={key:1,class:`error`},_={class:`card-grid`},v={class:`metric-card`},y={class:`kv-list`},b={class:`metric-card`},x={class:`kv-list`},S={key:0,class:`metric-card highlight`},C={class:`kv-list`},w={key:0},T={key:1},E={key:1,class:`metric-card`},D={class:`by-model`},O={class:`mono`},k={key:2,class:`metric-card review-card`},A={class:`review-stats`},j={class:`stat-item`},M={class:`stat-num`},N={class:`stat-item`},P={class:`stat-num`},F={key:0,class:`progress-wrap`},I={class:`review-rate`},L={class:`raw-json`},R=p(a({__name:`MetricsView`,setup(a){let p=c(null),R=c(!1),z=c(``),B=o(()=>p.value?.v2?.by_model||null);async function V(){R.value=!0,z.value=``;try{p.value=await f()}catch(e){z.value=e instanceof Error?e.message:`指标加载失败,请检查后端服务`}finally{R.value=!1}}return s(V),(a,o)=>(r(),u(`div`,m,[d(`header`,{class:`metrics-header`},[o[0]||=d(`h2`,null,`系统指标`,-1),d(`button`,{class:`refresh`,onClick:V},`🔄 刷新`)]),R.value?(r(),u(`div`,h,`加载中…`)):z.value?(r(),u(`div`,g,n(z.value),1)):p.value?(r(),u(t,{key:2},[d(`div`,_,[d(`div`,v,[o[1]||=d(`h3`,null,`路由器(v1)`,-1),d(`div`,y,[(r(!0),u(t,null,i(p.value.router,(e,i)=>(r(),u(t,{key:i},[d(`span`,null,n(i),1),d(`b`,null,n(e),1)],64))),128))])]),d(`div`,b,[o[2]||=d(`h3`,null,`缓存`,-1),d(`div`,x,[(r(!0),u(t,null,i(p.value.cache,(e,i)=>(r(),u(t,{key:i},[d(`span`,null,n(i),1),d(`b`,null,n(e),1)],64))),128))])]),p.value.v2?(r(),u(`div`,S,[o[3]||=d(`h3`,null,`协作管线(v2)`,-1),d(`div`,C,[(r(!0),u(t,null,i(p.value.v2,(i,a)=>(r(),u(t,{key:a},[a===`by_model`?e(``,!0):(r(),u(`span`,w,n(a),1)),a===`by_model`?e(``,!0):(r(),u(`b`,T,n(i),1))],64))),128))])])):e(``,!0),B.value&&Object.keys(B.value).length?(r(),u(`div`,E,[o[5]||=d(`h3`,null,`按模型分账(token / 成本)`,-1),d(`table`,D,[o[4]||=d(`thead`,null,[d(`tr`,null,[d(`th`,null,`模型`),d(`th`,null,`次数`),d(`th`,null,`入`),d(`th`,null,`出`),d(`th`,null,`成本 $`)])],-1),d(`tbody`,null,[(r(!0),u(t,null,i(B.value,(e,t)=>(r(),u(`tr`,{key:t},[d(`td`,O,n(t),1),d(`td`,null,n(e.requests),1),d(`td`,null,n(e.input_tokens),1),d(`td`,null,n(e.output_tokens),1),d(`td`,null,n(e.cost_est_usd),1)]))),128))])]),o[6]||=d(`p`,{class:`hint`},`单价来自模型池条目($/1M tokens);经典设置下的模型成本不计入。`,-1)])):e(``,!0),p.value.review?(r(),u(`div`,k,[o[9]||=d(`h3`,null,`人工检验`,-1),d(`div`,A,[d(`div`,j,[d(`span`,M,n(p.value.review.pending),1),o[7]||=d(`span`,{class:`stat-label`},`待审核`,-1)]),d(`div`,N,[d(`span`,P,n(p.value.review.total),1),o[8]||=d(`span`,{class:`stat-label`},`总提交`,-1)])]),p.value.review.total>0?(r(),u(`div`,F,[d(`div`,{class:`reviewed-bar`,style:l({width:`${(p.value.review.total-p.value.review.pending)/p.value.review.total*100}%`})},null,4)])):e(``,!0),d(`p`,I,` 通过率: `+n(((p.value.review.total-p.value.review.pending)/p.value.review.total*100).toFixed(1))+`% `,1)])):e(``,!0)]),d(`details`,L,[o[10]||=d(`summary`,null,`原始 JSON`,-1),d(`pre`,null,n(JSON.stringify(p.value,null,2)),1)])],64)):e(``,!0)]))}}),[[`__scopeId`,`data-v-ba641559`]]);export{R as default};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{A as e,D as t,E as n,G as r,I as i,L as a,M as o,N as s,O as c,P as l,U as u,V as d,f,j as p,k as m,t as h,v as g,z as _}from"./index-Bi4FCB5S.js";var v={class:`review-view`},y={class:`review-header`},b={class:`controls`},x={key:0,class:`loading`},S={key:1,class:`error`},C={key:2,class:`queue-list`},w={key:0,class:`empty`},T={class:`card-header`},E={class:`card-id`},D={class:`tags`},O={class:`date`},k={class:`query-block`},A={class:`response-block`},j={key:0,class:`actions`},M=[`onUpdate:modelValue`],N={class:`btn-row`},P=[`onClick`],F=[`onClick`],I={key:1,class:`correction`},L=h(s({__name:`ReviewView`,setup(s){let h=d([]),L=d(!1),R=d(``),z=d(`pending`),B=d({}),V=c(()=>z.value===`all`?h.value:h.value.filter(e=>e.verdict===z.value));async function H(){L.value=!0,R.value=``;try{h.value=await f()}catch(e){R.value=e instanceof Error?e.message:String(e)}finally{L.value=!1}}async function U(e,t){try{await g(e,t,B.value[e]||void 0),await H()}catch(e){R.value=e instanceof Error?e.message:String(e)}}return l(H),(s,c)=>(i(),p(`div`,v,[m(`header`,y,[c[4]||=m(`h2`,null,`人工检验队列`,-1),m(`div`,b,[m(`button`,{class:u({active:z.value===`all`}),onClick:c[0]||=e=>z.value=`all`},`全部`,2),m(`button`,{class:u({active:z.value===`pending`}),onClick:c[1]||=e=>z.value=`pending`},`待审核`,2),m(`button`,{class:u({active:z.value===`approved`}),onClick:c[2]||=e=>z.value=`approved`},`已通过`,2),m(`button`,{class:u({active:z.value===`rejected`}),onClick:c[3]||=e=>z.value=`rejected`},`已拒绝`,2),m(`button`,{class:`refresh-btn`,onClick:H},`🔄 刷新`)])]),L.value?(i(),p(`div`,x,`加载中…`)):R.value?(i(),p(`div`,S,r(R.value),1)):(i(),p(`div`,C,[V.value.length?e(``,!0):(i(),p(`div`,w,`队列为空。`)),(i(!0),p(t,null,a(V.value,s=>(i(),p(`div`,{key:s.id,class:`review-card`},[m(`div`,T,[m(`span`,E,`#`+r(s.id),1),m(`span`,{class:u([`verdict-badge`,s.verdict])},r(s.verdict),3),m(`span`,D,[(i(!0),p(t,null,a(s.tags,e=>(i(),p(`span`,{key:e,class:`tag`},r(e),1))),128))]),m(`span`,O,r(s.created_at),1)]),m(`div`,k,[c[5]||=m(`strong`,null,`Query:`,-1),o(r(s.query),1)]),m(`div`,A,[c[6]||=m(`strong`,null,`Response:`,-1),m(`pre`,null,r(s.response),1)]),s.verdict===`pending`?(i(),p(`div`,j,[_(m(`textarea`,{"onUpdate:modelValue":e=>B.value[s.id]=e,placeholder:`修正意见(可选)`,rows:`2`},null,8,M),[[n,B.value[s.id]]]),m(`div`,N,[m(`button`,{class:`approve`,onClick:e=>U(s.id,`approved`)},`✅ 通过`,8,P),m(`button`,{class:`reject`,onClick:e=>U(s.id,`rejected`)},`❌ 拒绝`,8,F)])])):s.correction?(i(),p(`div`,I,[c[7]||=m(`strong`,null,`修正:`,-1),o(r(s.correction),1)])):e(``,!0)]))),128))]))]))}}),[[`__scopeId`,`data-v-19c16eff`]]);export{L as default};
|
||||
import{A as e,D as t,E as n,G as r,I as i,L as a,M as o,N as s,O as c,P as l,U as u,V as d,f,j as p,k as m,t as h,v as g,z as _}from"./index-DtjeaX4S.js";var v={class:`review-view`},y={class:`review-header`},b={class:`controls`},x={key:0,class:`loading`},S={key:1,class:`error`},C={key:2,class:`queue-list`},w={key:0,class:`empty`},T={class:`card-header`},E={class:`card-id`},D={class:`tags`},O={class:`date`},k={class:`query-block`},A={class:`response-block`},j={key:0,class:`actions`},M=[`onUpdate:modelValue`],N={class:`btn-row`},P=[`onClick`],F=[`onClick`],I={key:1,class:`correction`},L=h(s({__name:`ReviewView`,setup(s){let h=d([]),L=d(!1),R=d(``),z=d(`pending`),B=d({}),V=c(()=>z.value===`all`?h.value:h.value.filter(e=>e.verdict===z.value));async function H(){L.value=!0,R.value=``;try{h.value=await f()}catch(e){R.value=e instanceof Error?e.message:String(e)}finally{L.value=!1}}async function U(e,t){try{await g(e,t,B.value[e]||void 0),await H()}catch(e){R.value=e instanceof Error?e.message:String(e)}}return l(H),(s,c)=>(i(),p(`div`,v,[m(`header`,y,[c[4]||=m(`h2`,null,`人工检验队列`,-1),m(`div`,b,[m(`button`,{class:u({active:z.value===`all`}),onClick:c[0]||=e=>z.value=`all`},`全部`,2),m(`button`,{class:u({active:z.value===`pending`}),onClick:c[1]||=e=>z.value=`pending`},`待审核`,2),m(`button`,{class:u({active:z.value===`approved`}),onClick:c[2]||=e=>z.value=`approved`},`已通过`,2),m(`button`,{class:u({active:z.value===`rejected`}),onClick:c[3]||=e=>z.value=`rejected`},`已拒绝`,2),m(`button`,{class:`refresh-btn`,onClick:H},`🔄 刷新`)])]),L.value?(i(),p(`div`,x,`加载中…`)):R.value?(i(),p(`div`,S,r(R.value),1)):(i(),p(`div`,C,[V.value.length?e(``,!0):(i(),p(`div`,w,`队列为空。`)),(i(!0),p(t,null,a(V.value,s=>(i(),p(`div`,{key:s.id,class:`review-card`},[m(`div`,T,[m(`span`,E,`#`+r(s.id),1),m(`span`,{class:u([`verdict-badge`,s.verdict])},r(s.verdict),3),m(`span`,D,[(i(!0),p(t,null,a(s.tags,e=>(i(),p(`span`,{key:e,class:`tag`},r(e),1))),128))]),m(`span`,O,r(s.created_at),1)]),m(`div`,k,[c[5]||=m(`strong`,null,`Query:`,-1),o(r(s.query),1)]),m(`div`,A,[c[6]||=m(`strong`,null,`Response:`,-1),m(`pre`,null,r(s.response),1)]),s.verdict===`pending`?(i(),p(`div`,j,[_(m(`textarea`,{"onUpdate:modelValue":e=>B.value[s.id]=e,placeholder:`修正意见(可选)`,rows:`2`},null,8,M),[[n,B.value[s.id]]]),m(`div`,N,[m(`button`,{class:`approve`,onClick:e=>U(s.id,`approved`)},`✅ 通过`,8,P),m(`button`,{class:`reject`,onClick:e=>U(s.id,`rejected`)},`❌ 拒绝`,8,F)])])):s.correction?(i(),p(`div`,I,[c[7]||=m(`strong`,null,`修正:`,-1),o(r(s.correction),1)])):e(``,!0)]))),128))]))]))}}),[[`__scopeId`,`data-v-19c16eff`]]);export{L as default};
|
||||
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,8 +5,8 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>webapp</title>
|
||||
<script type="module" crossorigin src="/static/assets/index-Bi4FCB5S.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/static/assets/index-zGyQjwum.css">
|
||||
<script type="module" crossorigin src="/static/assets/index-DtjeaX4S.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/static/assets/index-DPz6YNpx.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
Reference in New Issue
Block a user