diff --git a/.agent/docs/architecture/可视化模块合同.md b/.agent/docs/architecture/可视化模块合同.md index 0d5b6f5..f96097f 100644 --- a/.agent/docs/architecture/可视化模块合同.md +++ b/.agent/docs/architecture/可视化模块合同.md @@ -89,7 +89,7 @@ 1. 落库表分两类:**运行注册 / 运行回执 / 质量评判(98)与模型调用(97)已建**,但写路径未接通,视图暂空;候选 / 用户决策 / raw 全文 / 规划冻结(99/100/101)尚未建。没有它们 §4.2 的视图就是空的。 2. `db/ddl/96` 授权快照表**决定不启用**(领域索引 §9,单用户本地不做多租户授权),DDL 留存不 apply。 -3. 看板首版已建成(总览 / 作品 / 知识库 / 智能体 / 运行记录五空间,只读 HTTP + §4.1 部分视图 + 待落库占位格;智能体空间读 102 登记表);待建 = 运行详情下钻、作品/章工作区标签、沿来源下钻链。页面与信息架构见 §10。 +3. 看板首版已建成(总览 / 作品 / 知识库 / 智能体 / 运行记录五空间,只读 HTTP + §4.1 部分视图 + 待落库占位格;智能体空间读 102 登记表;运行记录含按作品/按角色双维 + 运行详情下钻);待建 = 作品/章工作区标签、沿来源下钻链(raw 下钻依赖 101)。页面与信息架构见 §10。 ## 10. 页面与信息架构(产品形态 SoT) diff --git a/dashboard/server.py b/dashboard/server.py index 57d0150..ebb7f4c 100644 --- a/dashboard/server.py +++ b/dashboard/server.py @@ -16,6 +16,7 @@ import html import re import traceback from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from urllib.parse import urlparse, parse_qs import psycopg @@ -435,71 +436,125 @@ def view_knowledge(): return layout("知识库", body, "/knowledge") -# ── 运行记录(05 流程 + 06 质量:运行/候选/决策/质量/模型调用;智能体空间待 102 登记表建好后另加)──────── -def view_runs_page(): - # 运行注册(98 已建) - if table_exists("example_run"): - _, runs = q("SELECT run_id, work_id, target_chapter, trigger_source, terminal_state, started_at " - "FROM example_run WHERE deleted=false ORDER BY started_at DESC LIMIT 50") - else: - runs = None - if runs: - trs = "".join( - f"{esc(r[0])}{esc(r[1])}{esc(r[2])}" - f"{esc(r[3])}{chip(r[4])}{esc(r[5])}" for r in runs) - run_html = (f"
运行(最近 50)
" - f"" - f"{trs}
run_id作品章触发终态开始
") - else: - run_html = empty_gap("暂无运行记录", "运行注册表已就位,等创作链(写作/检测/审核)落库后,每次运行登记一行。") +# ── 运行记录(05 流程 + 06 质量:运行列表按作品/按角色双维 + 运行详情下钻)──────── +_RUN_COLS = "" - # 质量评判(98 已建) - if table_exists("example_quality_result"): - _, qres = q("SELECT judge_kind, dimension, scale_version, conclusion, failure_class, candidate_sha256 " - "FROM example_quality_result ORDER BY create_time DESC LIMIT 50") - if qres: - trs = "".join( - f"" - f"" for r in qres) - q_html = (f"
质量评判(最近 50 · 有评判就记录就可见)
run_id作品章触发终态开始
{chip(r[0])}{esc(r[1])}{esc(r[2])}{chip(r[3])}{esc(r[4])}{esc((r[5] or '')[:12])}
" - f"" - f"{trs}
类型维度量表版本结论失败类别候选
") - else: - q_html = empty_gap("暂无质量评判", "检测/评分/审核/实验落库后逐条记录(绑候选哈希 + 量表版本)。") - else: - q_html = empty_gap("质量评判 · 待落库", "质量结果表尚未建。") - # 候选与决策(99 待建) - cand_html = (empty_gap("待审候选 · 待落库", "候选表尚未建,AI 候选正文目前无持久化登记。") - if not table_exists("example_candidate") - else "
候选表已建 · 暂无候选
") - dec_html = (empty_gap("用户决策 · 待落库", "决策记录表尚未建,接受/合并/丢弃暂以 git 提交留痕。") - if not table_exists("example_user_decision") - else "
决策表已建 · 暂无决策
") +def _run_rows(runs): + return "".join( + f"{esc(r[0])}{esc(r[1])}" + f"{esc(r[2])}{esc(r[3])}{chip(r[4])}{esc(r[5])}" + for r in runs) - # 模型调用明细(97 已建) - if table_exists("example_llm_call"): - _, calls = q("SELECT call_time, caller, requested_model_id, actual_model_id, out_tokens, cost_usd, stop_reason " - "FROM example_llm_call ORDER BY call_time DESC LIMIT 50") - if calls: - trs = "".join( - f"{esc(c[0])}{esc(c[1])}{esc(c[2])}{esc(c[3])}" - f"{fmt_num(c[4])}${float(c[5] or 0):.5f}{esc(c[6])}" for c in calls) - call_html = (f"
模型调用明细(最近 50 · 逐次记录)
" - f"" - f"" - f"{trs}
时间调用方请求模型实际模型输出token成本结束
") - else: - call_html = empty_gap("暂无逐次调用明细", "调用明细表已就位,模型调用落库后逐次记录(提示词全文受访问控制,单独存 raw)。") - else: - call_html = empty_gap("模型调用明细 · 待落库", "调用明细表尚未建。") - body = (f"

运行记录agentic 的操作与产出——运行、候选、决策、质量评判、模型调用 · 流程 05 · 质量 06(智能体空间待 102 登记表建好后另加)

" - f"

运行

{run_html}

质量评判

{q_html}" - f"

待审候选

{cand_html}

用户决策

{dec_html}

模型调用

{call_html}") +def _all_runs(limit=50): + _, runs = q("SELECT run_id, work_id, target_chapter, trigger_source, terminal_state, started_at " + "FROM example_run WHERE deleted=false ORDER BY started_at DESC LIMIT %s", (limit,)) + return runs + + +def _selector(label, name, options, sel): + """服务端渲染的下拉筛选(GET 表单,无 JS)。options=[(值,显示名)]。""" + opts = "".join(f"" + for v, t in options) + return (f"
" + f"" + f"" + f"
") + + +def view_runs(): + runs = _all_runs() + entries = ("

按维度查看:按作品看运行 · " + "按角色看运行

") + runs_html = (f"
运行(最近 50)
" + + _RUN_COLS + _run_rows(runs) + "
") if runs else \ + empty_gap("暂无运行记录", "创作链落库后,每次运行登记一行(example_run)。") + cand = (empty_gap("待审候选 · 待落库", "候选表(99)未建,AI 候选正文暂无持久化登记。") + if not table_exists("example_candidate") else "
候选表已建 · 暂无候选
") + dec = (empty_gap("用户决策 · 待落库", "决策记录表(99)未建,接受/合并/丢弃暂以 git 提交留痕。") + if not table_exists("example_user_decision") else "
决策表已建 · 暂无决策
") + body = (f"

运行记录agentic 的操作与产出 · 流程 05 · 质量 06

" + f"{entries}{runs_html}

待审候选

{cand}

用户决策

{dec}") return layout("运行记录", body, "/runs") +def view_runs_by_work(params): + sel = (params.get("work") or [""])[0] + _, works = q("SELECT id, title FROM muse_content_work WHERE deleted=false ORDER BY id") + selector = _selector("选作品:", "work", [(w[0], w[1]) for w in works], sel) + if sel: + _, runs = q("SELECT run_id, work_id, target_chapter, trigger_source, terminal_state, started_at " + "FROM example_run WHERE work_id=%s AND deleted=false ORDER BY started_at DESC LIMIT 50", (sel,)) + else: + runs = _all_runs() + tbl = (_RUN_COLS + _run_rows(runs) + "") if runs else "
暂无运行
" + body = (f"
运行记录 / 按作品
" + f"

按作品看运行

{selector}
{tbl}
") + return layout("按作品看运行", body, "/runs") + + +def view_runs_by_role(params): + sel = (params.get("role") or [""])[0] + _, roles = q("SELECT role, display_name FROM example_agent_role WHERE deleted=false ORDER BY role") + selector = _selector("选角色:", "role", [(r[0], r[1] or r[0]) for r in roles], sel) + if sel: + _, runs = q("SELECT DISTINCT n.run_id, n.work_id, n.target_chapter, n.trigger_source, n.terminal_state, n.started_at " + "FROM example_run_receipt r JOIN example_run n ON n.run_id=r.run_id " + "WHERE r.adapter_role=%s AND n.deleted=false ORDER BY n.started_at DESC LIMIT 50", (sel,)) + else: + runs = _all_runs() + tbl = (_RUN_COLS + _run_rows(runs) + "") if runs else "
该角色暂无运行
" + body = (f"
运行记录 / 按角色
" + f"

按角色看运行

{selector}
{tbl}
") + return layout("按角色看运行", body, "/runs") + + +def view_run_detail(run_id): + _, rr = q("SELECT run_id, work_id, target_chapter, trigger_source, started_at, finished_at, terminal_state " + "FROM example_run WHERE run_id=%s AND deleted=false", (run_id,)) + if not rr: + return layout("运行", "

运行不存在

", "/runs") + rid, wid, chap, trig, started, finished, state = rr[0] + info = (f"
" + f"" + f"" + f"" + f"" + f"" + f"" + f"
run_id{esc(rid)}
作品{esc(wid)}
目标章{esc(chap)}
触发{esc(trig)}
终态{chip(state)}
起止{esc(started)} → {esc(finished)}
") + _, rc = q("SELECT sample_id, revision, stage_kind, adapter_role, attempt, loop_seq, candidate_version, is_error " + "FROM example_run_receipt WHERE run_id=%s ORDER BY sample_id, revision", (run_id,)) + rc_html = ("
回执链(按样本 · revision 哈希链)
" + "" + + "".join(f"" + f"" for r in rc) + + "
样本rev阶段角色attempt补证候选版本结果
{esc(r[0])}{esc(r[1])}{esc(r[2])}{esc(r[3])}{esc(r[4])}{esc(r[5])}{esc(r[6])}{chip('failed' if r[7] else 'passed')}
") if rc else empty_gap("暂无回执", "该运行的回执落库后在此显示(revision 哈希链)。") + _, qres = q("SELECT judge_kind, dimension, scale_version, score, conclusion, failure_class " + "FROM example_quality_result WHERE run_id=%s ORDER BY create_time", (run_id,)) + q_html = ("
质量评判
" + "" + + "".join(f"" for u in qres) + + "
类型维度量表版本分结论失败类别
{chip(u[0])}{esc(u[1])}{esc(u[2])}{esc(u[3])}{chip(u[4])}{esc(u[5])}
") if qres else empty_gap("暂无质量评判", "") + _, calls = q("SELECT call_time, caller, requested_model_id, actual_model_id, out_tokens, cost_usd " + "FROM example_llm_call WHERE run_id=%s ORDER BY call_time", (run_id,)) + call_html = ("
模型调用
" + "" + + "".join(f"" + f"" for c in calls) + + "
时间调用方请求模型实际模型输出token成本
{esc(c[0])}{esc(c[1])}{esc(c[2])}{esc(c[3])}{fmt_num(c[4])}${float(c[5] or 0):.5f}
") if calls else empty_gap("暂无模型调用", "") + cand = (empty_gap("待审候选 · 待落库", "候选表(99)未建。") if not table_exists("example_candidate") + else "
候选表已建 · 该运行暂无候选
") + dec = (empty_gap("用户决策 · 待落库", "决策记录表(99)未建。") if not table_exists("example_user_decision") + else "
决策表已建 · 该运行暂无决策
") + body = (f"
运行记录 / {esc(rid)}
" + f"

运行详情

{info}" + f"

回执链

{rc_html}

质量评判

{q_html}

模型调用

{call_html}" + f"

候选与决策

{cand}{dec}") + return layout(f"运行 · {rid}", body, "/runs") + + # ── 智能体(07:读 102 登记表 example_agent_role / example_skill;权威在 Git 侧,登记脚本同步)──── def _fmt_list(v): """JSONB 列表 → 逗号串;非列表原样转义。""" @@ -586,26 +641,30 @@ def view_skill(name): # ── 路由 ───────────────────────────────────────────────────────────────────── _ROUTES = [ - (re.compile(r"^/$"), lambda m: view_home()), - (re.compile(r"^/works$"), lambda m: view_works()), - (re.compile(r"^/works/(\d+)$"), lambda m: view_work(int(m.group(1)))), - (re.compile(r"^/works/(\d+)/ch/(\d+)$"), lambda m: view_chapter(int(m.group(1)), int(m.group(2)))), - (re.compile(r"^/knowledge$"), lambda m: view_knowledge()), - (re.compile(r"^/agents$"), lambda m: view_agents()), - (re.compile(r"^/agents/role/([\w-]+)$"), lambda m: view_agent_role(m.group(1))), - (re.compile(r"^/agents/skill/([\w-]+)$"), lambda m: view_skill(m.group(1))), - (re.compile(r"^/runs$"), lambda m: view_runs_page()), + (re.compile(r"^/$"), lambda m, p: view_home()), + (re.compile(r"^/works$"), lambda m, p: view_works()), + (re.compile(r"^/works/(\d+)$"), lambda m, p: view_work(int(m.group(1)))), + (re.compile(r"^/works/(\d+)/ch/(\d+)$"), lambda m, p: view_chapter(int(m.group(1)), int(m.group(2)))), + (re.compile(r"^/knowledge$"), lambda m, p: view_knowledge()), + (re.compile(r"^/agents$"), lambda m, p: view_agents()), + (re.compile(r"^/agents/role/([\w-]+)$"), lambda m, p: view_agent_role(m.group(1))), + (re.compile(r"^/agents/skill/([\w-]+)$"), lambda m, p: view_skill(m.group(1))), + (re.compile(r"^/runs$"), lambda m, p: view_runs()), + (re.compile(r"^/runs/by-work$"), lambda m, p: view_runs_by_work(p)), + (re.compile(r"^/runs/by-role$"), lambda m, p: view_runs_by_role(p)), + (re.compile(r"^/runs/([\w.-]+)$"), lambda m, p: view_run_detail(m.group(1))), ] class Handler(BaseHTTPRequestHandler): def do_GET(self): - path = self.path.split("?", 1)[0] + parsed = urlparse(self.path) + params = parse_qs(parsed.query) try: for pat, fn in _ROUTES: - m = pat.match(path) + m = pat.match(parsed.path) if m: - self._send(200, fn(m)) + self._send(200, fn(m, params)) return self._send(404, layout("未找到", "

404 · 页面不存在

", "")) except Exception: # 客户端给固定文案,细节写服务端 stderr(不泄露 DB 内部结构;看板挂了不牵连库)