feat(v3): T27 会话式智能体(多轮上下文/停止/对话式 UI,对齐 dsh 范式)
- ToolLoop 增 history 参数(既往轮次折叠为对话上下文,近 6 轮)
- 会话制:AgentSession/SessionStore 持久化 agent_runs/sessions/{sid}.json,
/agent/sessions CRUD + /agent 带 session_id(继承会话工作区与执行者配置,busy 并发控制)
- POST /agent/{id}/cancel:取消运行中任务
- AgentView 重构为对话式:会话列表 + 居中消息流(用户蓝气泡/助手白卡)+ 底部 composer
(工作区/执行者/命令 chips,Enter 发送,运行中红色停止钮),工具过程折叠收纳
- 工具步数统计统一至事件写入层并透出 status.tool_calls
- fix(tests): 会话存储测试隔离,防测试数据泄漏进真实 sessions 目录
- 测试 +4,全量 281 passed
This commit is contained in:
+150
-2
@@ -167,6 +167,7 @@ class AgentRunInfo:
|
||||
workspace: str = "" # 本次运行使用的工作区根目录(绝对路径)
|
||||
executor_model: str = "" # 两级模式:执行者模型名(空 = 单模型模式)
|
||||
mode: str = "single" # single | dual
|
||||
tool_calls: int = 0 # 本次运行的工具调用步数
|
||||
asyncio_task: Optional[asyncio.Task] = field(default=None, repr=False)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
@@ -186,6 +187,7 @@ class AgentRunInfo:
|
||||
"workspace": self.workspace,
|
||||
"executor_model": self.executor_model,
|
||||
"mode": self.mode,
|
||||
"tool_calls": self.tool_calls,
|
||||
}
|
||||
|
||||
|
||||
@@ -231,12 +233,16 @@ class AgentService:
|
||||
max_rounds: int = 8, token_cap: int = 0,
|
||||
allow_shell: bool = False, shell_timeout_s: int = 20,
|
||||
executor_chat: Any = None,
|
||||
max_handoffs: int = DEFAULT_MAX_HANDOFFS) -> None:
|
||||
max_handoffs: int = DEFAULT_MAX_HANDOFFS,
|
||||
session: Optional["AgentSession"] = None) -> None:
|
||||
"""执行智能体任务(由调用方包成后台协程)。
|
||||
|
||||
executor_chat 为空 = 单模型模式(chat 全程包办);
|
||||
提供时进入两级模式:chat 作规划者,executor_chat 作执行者(D7)。
|
||||
session 提供时:既往轮次作为对话上下文,完成后把本轮追加进会话。
|
||||
"""
|
||||
history = self._history_from_session(session)
|
||||
|
||||
try:
|
||||
if executor_chat is not None:
|
||||
result = await self.run_dual(
|
||||
@@ -249,7 +255,8 @@ class AgentService:
|
||||
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)
|
||||
result = await loop.run(info.task, system=AGENT_SYSTEM_PROMPT,
|
||||
history=history)
|
||||
self._apply_result(info, result)
|
||||
except Exception as exc: # pragma: no cover
|
||||
info.state = STATE_FAILED
|
||||
@@ -259,6 +266,33 @@ class AgentService:
|
||||
finally:
|
||||
info.finished_at = time.time()
|
||||
self._write_status(info)
|
||||
if session is not None:
|
||||
session.data["turns"].append({
|
||||
"request_id": info.request_id,
|
||||
"task": info.task,
|
||||
"response": info.response,
|
||||
"state": info.state,
|
||||
"tool_calls": info.tool_calls,
|
||||
"tokens": info.prompt_tokens + info.completion_tokens,
|
||||
"error": info.error,
|
||||
"ts": info.finished_at,
|
||||
})
|
||||
get_session_store().save(session)
|
||||
|
||||
@staticmethod
|
||||
def _history_from_session(session: Optional["AgentSession"],
|
||||
max_turns: int = 6,
|
||||
max_chars: int = 1500) -> List[Dict[str, Any]]:
|
||||
"""把会话既往轮次折叠成对话上下文(不含工具细节)。"""
|
||||
if session is None:
|
||||
return []
|
||||
turns = [t for t in session.data.get("turns", [])
|
||||
if t.get("state") == STATE_DONE and t.get("response")]
|
||||
out: List[Dict[str, Any]] = []
|
||||
for t in turns[-max_turns:]:
|
||||
out.append({"role": "user", "content": str(t["task"])[:max_chars]})
|
||||
out.append({"role": "assistant", "content": str(t["response"])[:max_chars]})
|
||||
return out
|
||||
|
||||
def _apply_result(self, info: AgentRunInfo, result: Dict[str, Any]) -> None:
|
||||
"""把循环结果落到运行状态(单/两级模式共用)。"""
|
||||
@@ -413,6 +447,8 @@ class AgentService:
|
||||
# ---------- 事件 ----------
|
||||
def _make_event_writer(self, info: AgentRunInfo):
|
||||
def _on_event(ev: Dict[str, Any]) -> None:
|
||||
if ev.get("type") == "tool_call":
|
||||
info.tool_calls += 1 # 工具步数统计(单/两级模式统一在此)
|
||||
self._append_event(info, ev)
|
||||
return _on_event
|
||||
|
||||
@@ -509,3 +545,115 @@ def reset_agent_service() -> None:
|
||||
|
||||
def new_request_id() -> str:
|
||||
return "ag" + uuid.uuid4().hex[:10]
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 会话(dsh 式:工作区内多轮对话,持久化到磁盘)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
SESSIONS_DIR = Path("agent_runs") / "sessions"
|
||||
|
||||
|
||||
class AgentSession:
|
||||
"""一个智能体会话:多轮任务 + 配置快照(磁盘持久化)。"""
|
||||
|
||||
def __init__(self, data: Dict[str, Any]):
|
||||
self.data = data
|
||||
|
||||
@classmethod
|
||||
def new(cls, sid: str, title: str, workspace: str,
|
||||
pool_id: str = "", executor_pool_id: str = "") -> "AgentSession":
|
||||
now = time.time()
|
||||
return cls({
|
||||
"id": sid, "title": title[:24] or "新会话", "workspace": workspace,
|
||||
"pool_id": pool_id, "executor_pool_id": executor_pool_id,
|
||||
"created_at": now, "updated_at": now, "busy": False,
|
||||
"turns": [], # [{request_id, task, response, state, tool_calls, tokens}]
|
||||
})
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return dict(self.data)
|
||||
|
||||
def view(self, include_turns: bool = True) -> Dict[str, Any]:
|
||||
out = self.to_dict()
|
||||
if not include_turns:
|
||||
out["turns"] = len(self.data.get("turns", []))
|
||||
return out
|
||||
|
||||
|
||||
class SessionStore:
|
||||
"""会话注册表(内存索引 + sessions/{sid}.json 持久化)。"""
|
||||
|
||||
def __init__(self, root: Path = SESSIONS_DIR):
|
||||
self.root = Path(root)
|
||||
self.root.mkdir(parents=True, exist_ok=True)
|
||||
self._cache: Dict[str, AgentSession] = {}
|
||||
|
||||
def _path(self, sid: str) -> Path:
|
||||
return self.root / f"{sid}.json"
|
||||
|
||||
def create(self, title: str, workspace: str,
|
||||
pool_id: str = "", executor_pool_id: str = "") -> AgentSession:
|
||||
sid = "as" + uuid.uuid4().hex[:10]
|
||||
sess = AgentSession.new(sid, title or "新会话", workspace, pool_id, executor_pool_id)
|
||||
self._cache[sid] = sess
|
||||
self._save(sess)
|
||||
return sess
|
||||
|
||||
def get(self, sid: str) -> Optional[AgentSession]:
|
||||
if sid in self._cache:
|
||||
return self._cache[sid]
|
||||
p = self._path(sid)
|
||||
if not p.exists():
|
||||
return None
|
||||
try:
|
||||
sess = AgentSession(json.loads(p.read_text(encoding="utf-8")))
|
||||
self._cache[sid] = sess
|
||||
return sess
|
||||
except (json.JSONDecodeError, OSError):
|
||||
return None
|
||||
|
||||
def list(self) -> List[Dict[str, Any]]:
|
||||
out = []
|
||||
for p in sorted(self.root.glob("*.json"),
|
||||
key=lambda x: x.stat().st_mtime, reverse=True):
|
||||
try:
|
||||
out.append(json.loads(p.read_text(encoding="utf-8")))
|
||||
except (json.JSONDecodeError, OSError):
|
||||
continue
|
||||
return out
|
||||
|
||||
def delete(self, sid: str) -> bool:
|
||||
self._cache.pop(sid, None)
|
||||
p = self._path(sid)
|
||||
if p.exists():
|
||||
p.unlink()
|
||||
return True
|
||||
return False
|
||||
|
||||
def save(self, sess: AgentSession) -> None:
|
||||
self._cache[sess.data["id"]] = sess
|
||||
self._save(sess)
|
||||
|
||||
def _save(self, sess: AgentSession) -> None:
|
||||
sess.data["updated_at"] = time.time()
|
||||
try:
|
||||
self._path(sess.data["id"]).write_text(
|
||||
json.dumps(sess.data, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
_session_store: Optional[SessionStore] = None
|
||||
|
||||
|
||||
def get_session_store() -> SessionStore:
|
||||
global _session_store
|
||||
if _session_store is None:
|
||||
_session_store = SessionStore()
|
||||
return _session_store
|
||||
|
||||
|
||||
def reset_session_store() -> None:
|
||||
"""测试用。"""
|
||||
global _session_store
|
||||
_session_store = None
|
||||
|
||||
+83
-6
@@ -501,12 +501,13 @@ try:
|
||||
|
||||
@app.post("/agent", tags=["agent"])
|
||||
async def agent_run(req: dict):
|
||||
"""提交智能体任务:{"task", "pool_id"?, "workspace"?}。
|
||||
"""提交智能体任务:{"task", "pool_id"?, "workspace"?, "executor_pool_id"?, "session_id"?}。
|
||||
|
||||
workspace 为用户选择的工作目录(绝对路径);缺省用设置里的 agent.workspace_dir。
|
||||
workspace 为用户选择的工作目录;缺省继承会话目录,再缺省用设置默认值。
|
||||
session_id 提供时任务在会话内执行(多轮上下文 + 轮次记录)。
|
||||
立即返回 request_id;过程事件经 GET /agent/{id}/stream (SSE) 推送。
|
||||
"""
|
||||
from gateway.agent import get_agent_service, new_request_id
|
||||
from gateway.agent import get_agent_service, get_session_store, new_request_id
|
||||
from router_system.tools import WorkspaceTools
|
||||
|
||||
task = str((req or {}).get("task") or "").strip()
|
||||
@@ -518,7 +519,23 @@ try:
|
||||
|
||||
s = settings_store().to_dict()
|
||||
agent_cfg = s.get("agent", {})
|
||||
|
||||
# 会话(可选):须存在且空闲;工作区缺省继承会话目录
|
||||
session = None
|
||||
session_id = str((req or {}).get("session_id") or "").strip()
|
||||
if session_id:
|
||||
session = get_session_store().get(session_id)
|
||||
if session is None:
|
||||
raise HTTPException(status_code=404, detail=f"会话不存在: {session_id}")
|
||||
if session.data.get("busy"):
|
||||
raise HTTPException(status_code=409, detail="该会话有任务正在运行,请稍候")
|
||||
# 会话级配置继承(创建时指定,后续轮次沿用)
|
||||
if not pool_id:
|
||||
pool_id = str(session.data.get("pool_id") or "")
|
||||
|
||||
ws_raw = str((req or {}).get("workspace") or "").strip()
|
||||
if not ws_raw and session is not None:
|
||||
ws_raw = str(session.data.get("workspace") or "")
|
||||
if ws_raw:
|
||||
ws_path = Path(ws_raw)
|
||||
if not ws_path.exists():
|
||||
@@ -533,6 +550,8 @@ try:
|
||||
|
||||
# 两级模式(D7):显式指定执行者(本地小模型)时,规划=chat、执行=executor_chat
|
||||
executor_pool_id = str((req or {}).get("executor_pool_id") or "").strip()
|
||||
if not executor_pool_id and session is not None:
|
||||
executor_pool_id = str(session.data.get("executor_pool_id") or "")
|
||||
executor_chat = None
|
||||
executor_model = ""
|
||||
if executor_pool_id:
|
||||
@@ -565,8 +584,11 @@ try:
|
||||
if info is None:
|
||||
raise HTTPException(status_code=503, detail="智能体同时运行任务已达上限")
|
||||
|
||||
s = settings_store().to_dict()
|
||||
agent_cfg = s.get("agent", {})
|
||||
if session is not None:
|
||||
session.data["busy"] = True
|
||||
if not session.data.get("workspace"):
|
||||
session.data["workspace"] = workspace_dir
|
||||
get_session_store().save(session)
|
||||
|
||||
async def _run():
|
||||
try:
|
||||
@@ -579,6 +601,7 @@ try:
|
||||
shell_timeout_s=int(agent_cfg.get("shell_timeout_s", 20)),
|
||||
executor_chat=executor_chat,
|
||||
max_handoffs=int(agent_cfg.get("max_handoffs", 2)),
|
||||
session=session,
|
||||
)
|
||||
except Exception as exc:
|
||||
import traceback
|
||||
@@ -587,11 +610,65 @@ try:
|
||||
info.error = str(exc)
|
||||
info.finished_at = __import__("time").time()
|
||||
service._write_status(info)
|
||||
finally:
|
||||
if session is not None:
|
||||
session.data["busy"] = False
|
||||
get_session_store().save(session)
|
||||
|
||||
info.asyncio_task = asyncio.create_task(_run())
|
||||
return {"request_id": request_id, "status": "running", "model": model,
|
||||
"workspace": workspace_dir, "mode": mode,
|
||||
"executor_model": executor_model}
|
||||
"executor_model": executor_model, "session_id": session_id or None}
|
||||
|
||||
@app.post("/agent/{request_id}/cancel", tags=["agent"])
|
||||
async def agent_cancel(request_id: str):
|
||||
"""停止运行中的智能体任务。"""
|
||||
from gateway.agent import get_agent_service
|
||||
service = get_agent_service()
|
||||
info = service.get(request_id)
|
||||
if info is None:
|
||||
raise HTTPException(status_code=404, detail=f"智能体任务不存在: {request_id}")
|
||||
if info.state != "running":
|
||||
return {"ok": False, "detail": f"任务已结束({info.state})"}
|
||||
if info.asyncio_task is not None:
|
||||
info.asyncio_task.cancel()
|
||||
info.state = "failed"
|
||||
info.error = "cancelled_by_user"
|
||||
info.finished_at = __import__("time").time()
|
||||
service._write_status(info)
|
||||
return {"ok": True}
|
||||
|
||||
# ---------------- 会话(dsh 式多轮对话) ----------------
|
||||
@app.post("/agent/sessions", tags=["agent"])
|
||||
async def agent_session_create(req: dict = None):
|
||||
"""创建会话:{"title"?, "workspace"?, "pool_id"?, "executor_pool_id"?}"""
|
||||
from gateway.agent import get_session_store
|
||||
r = req or {}
|
||||
sess = get_session_store().create(
|
||||
title=str(r.get("title") or "").strip(),
|
||||
workspace=str(r.get("workspace") or "").strip(),
|
||||
pool_id=str(r.get("pool_id") or ""),
|
||||
executor_pool_id=str(r.get("executor_pool_id") or ""))
|
||||
return sess.view()
|
||||
|
||||
@app.get("/agent/sessions", tags=["agent"])
|
||||
async def agent_sessions():
|
||||
"""会话列表(按更新时间倒序)。"""
|
||||
from gateway.agent import get_session_store
|
||||
return get_session_store().list()
|
||||
|
||||
@app.get("/agent/sessions/{sid}", tags=["agent"])
|
||||
async def agent_session_detail(sid: str):
|
||||
from gateway.agent import get_session_store
|
||||
sess = get_session_store().get(sid)
|
||||
if sess is None:
|
||||
raise HTTPException(status_code=404, detail=f"会话不存在: {sid}")
|
||||
return sess.view()
|
||||
|
||||
@app.delete("/agent/sessions/{sid}", tags=["agent"])
|
||||
async def agent_session_delete(sid: str):
|
||||
from gateway.agent import get_session_store
|
||||
return {"ok": get_session_store().delete(sid)}
|
||||
|
||||
@app.get("/agent/fs", tags=["agent"])
|
||||
async def agent_fs_browse(path: str = ""):
|
||||
|
||||
+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-8a-EFu_T.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-8b237097`]]);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-DdcvGqdd.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-8b237097`]]);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-8a-EFu_T.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-d5b38f1c`]]);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-DdcvGqdd.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-d5b38f1c`]]);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>端云协同 LLM 协作系统</title>
|
||||
<script type="module" crossorigin src="/static/assets/index-8a-EFu_T.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/static/assets/index-DtUWLrmy.css">
|
||||
<script type="module" crossorigin src="/static/assets/index-DdcvGqdd.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/static/assets/index-CzrTgC9J.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
Reference in New Issue
Block a user