框架(看板): 运行记录空间补全——按作品/按角色双维 + 运行详情下钻

- /runs 落地页(运行列表+常驻双维入口+候选决策占位);/runs/by-work、/runs/by-role 服务端下拉筛选(无JS)
- /runs/<run_id> 运行详情:运行信息+回执链(样本·revision哈希链)+质量评判+模型调用+候选与决策,沿来源下钻起点
- do_GET 解析 query 参数;合同 §9 待建清单收敛(运行详情/双维已建成)
This commit is contained in:
zizi 2026-07-31 06:36:15 +08:00
parent b936de5bef
commit 338e3a7dbb
2 changed files with 130 additions and 71 deletions

View File

@ -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)

View File

@ -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"):
# ── 运行记录(05 流程 + 06 质量:运行列表按作品/按角色双维 + 运行详情下钻)────────
_RUN_COLS = "<table><thead><tr><th>run_id</th><th>作品</th><th class='num'>章</th><th>触发</th><th>终态</th><th>开始</th></tr></thead><tbody>"
def _run_rows(runs):
return "".join(
f"<tr><td><a href='/runs/{esc(r[0])}'><code>{esc(r[0])}</code></a></td><td>{esc(r[1])}</td>"
f"<td class='num'>{esc(r[2])}</td><td>{esc(r[3])}</td><td>{chip(r[4])}</td><td>{esc(r[5])}</td></tr>"
for r in runs)
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 50")
else:
runs = None
if runs:
trs = "".join(
f"<tr><td><code>{esc(r[0])}</code></td><td>{esc(r[1])}</td><td class='num'>{esc(r[2])}</td>"
f"<td>{esc(r[3])}</td><td>{chip(r[4])}</td><td>{esc(r[5])}</td></tr>" for r in runs)
run_html = (f"<div class='card'><div class='hd'>运行(最近 50)</div><table><thead>"
f"<tr><th>run_id</th><th>作品</th><th class='num'>章</th><th>触发</th><th>终态</th><th>开始</th></tr>"
f"</thead><tbody>{trs}</tbody></table></div>")
else:
run_html = empty_gap("暂无运行记录", "运行注册表已就位,等创作链(写作/检测/审核)落库后,每次运行登记一行。")
"FROM example_run WHERE deleted=false ORDER BY started_at DESC LIMIT %s", (limit,))
return runs
# 质量评判(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"<tr><td>{chip(r[0])}</td><td>{esc(r[1])}</td><td>{esc(r[2])}</td>"
f"<td>{chip(r[3])}</td><td>{esc(r[4])}</td><td><code>{esc((r[5] or '')[:12])}</code></td></tr>" for r in qres)
q_html = (f"<div class='card'><div class='hd'>质量评判(最近 50 · 有评判就记录就可见)</div><table><thead>"
f"<tr><th>类型</th><th>维度</th><th>量表版本</th><th>结论</th><th>失败类别</th><th>候选</th></tr>"
f"</thead><tbody>{trs}</tbody></table></div>")
else:
q_html = empty_gap("暂无质量评判", "检测/评分/审核/实验落库后逐条记录(绑候选哈希 + 量表版本)。")
else:
q_html = empty_gap("质量评判 · 待落库", "质量结果表尚未建。")
# 候选与决策(99 待建)
cand_html = (empty_gap("待审候选 · 待落库", "候选表尚未建,AI 候选正文目前无持久化登记。")
if not table_exists("example_candidate")
else "<div class='empty'>候选表已建 · 暂无候选</div>")
dec_html = (empty_gap("用户决策 · 待落库", "决策记录表尚未建,接受/合并/丢弃暂以 git 提交留痕。")
if not table_exists("example_user_decision")
else "<div class='empty'>决策表已建 · 暂无决策</div>")
def _selector(label, name, options, sel):
"""服务端渲染的下拉筛选(GET 表单,无 JS)。options=[(值,显示名)]。"""
opts = "".join(f"<option value='{esc(v)}'{' selected' if str(v) == str(sel) else ''}>{esc(t)}</option>"
for v, t in options)
return (f"<form method='get' style='margin:14px 0;display:flex;gap:8px;align-items:center'>"
f"<label style='color:var(--muted)'>{esc(label)}</label>"
f"<select name='{name}'><option value=''>— 全部 —</option>{opts}</select>"
f"<button type='submit'>查看</button></form>")
# 模型调用明细(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"<tr><td>{esc(c[0])}</td><td>{esc(c[1])}</td><td>{esc(c[2])}</td><td>{esc(c[3])}</td>"
f"<td class='num'>{fmt_num(c[4])}</td><td class='num'>${float(c[5] or 0):.5f}</td><td>{esc(c[6])}</td></tr>" for c in calls)
call_html = (f"<div class='card'><div class='hd'>模型调用明细(最近 50 · 逐次记录)</div>"
f"<table><thead><tr><th>时间</th><th>调用方</th><th>请求模型</th><th>实际模型</th>"
f"<th class='num'>输出token</th><th class='num'>成本</th><th>结束</th></tr></thead>"
f"<tbody>{trs}</tbody></table></div>")
else:
call_html = empty_gap("暂无逐次调用明细", "调用明细表已就位,模型调用落库后逐次记录(提示词全文受访问控制,单独存 raw)。")
else:
call_html = empty_gap("模型调用明细 · 待落库", "调用明细表尚未建。")
body = (f"<h1>运行记录<span class='sub'>agentic 的操作与产出——运行、候选、决策、质量评判、模型调用 · 流程 05 · 质量 06(智能体空间待 102 登记表建好后另加)</span></h1>"
f"<h2>运行</h2>{run_html}<h2>质量评判</h2>{q_html}"
f"<h2>待审候选</h2>{cand_html}<h2>用户决策</h2>{dec_html}<h2>模型调用</h2>{call_html}")
def view_runs():
runs = _all_runs()
entries = ("<p class='note'>按维度查看:<a href='/runs/by-work'>按作品看运行</a> · "
"<a href='/runs/by-role'>按角色看运行</a></p>")
runs_html = (f"<div class='card'><div class='hd'>运行(最近 50)</div>"
+ _RUN_COLS + _run_rows(runs) + "</tbody></table></div>") if runs else \
empty_gap("暂无运行记录", "创作链落库后,每次运行登记一行(example_run)。")
cand = (empty_gap("待审候选 · 待落库", "候选表(99)未建,AI 候选正文暂无持久化登记。")
if not table_exists("example_candidate") else "<div class='empty'>候选表已建 · 暂无候选</div>")
dec = (empty_gap("用户决策 · 待落库", "决策记录表(99)未建,接受/合并/丢弃暂以 git 提交留痕。")
if not table_exists("example_user_decision") else "<div class='empty'>决策表已建 · 暂无决策</div>")
body = (f"<h1>运行记录<span class='sub'>agentic 的操作与产出 · 流程 05 · 质量 06</span></h1>"
f"{entries}{runs_html}<h2>待审候选</h2>{cand}<h2>用户决策</h2>{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) + "</tbody></table>") if runs else "<div class='empty'>暂无运行</div>"
body = (f"<div class='crumb'><a href='/runs'>运行记录</a> / 按作品</div>"
f"<h1>按作品看运行</h1>{selector}<div class='card'>{tbl}</div>")
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) + "</tbody></table>") if runs else "<div class='empty'>该角色暂无运行</div>"
body = (f"<div class='crumb'><a href='/runs'>运行记录</a> / 按角色</div>"
f"<h1>按角色看运行</h1>{selector}<div class='card'>{tbl}</div>")
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("运行", "<h1>运行不存在</h1>", "/runs")
rid, wid, chap, trig, started, finished, state = rr[0]
info = (f"<div class='card'><table>"
f"<tr><td style='width:110px;color:var(--muted)'>run_id</td><td><code>{esc(rid)}</code></td></tr>"
f"<tr><td style='color:var(--muted)'>作品</td><td>{esc(wid)}</td></tr>"
f"<tr><td style='color:var(--muted)'>目标章</td><td>{esc(chap)}</td></tr>"
f"<tr><td style='color:var(--muted)'>触发</td><td>{esc(trig)}</td></tr>"
f"<tr><td style='color:var(--muted)'>终态</td><td>{chip(state)}</td></tr>"
f"<tr><td style='color:var(--muted)'>起止</td><td>{esc(started)} → {esc(finished)}</td></tr>"
f"</table></div>")
_, 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 = ("<div class='card'><div class='hd'>回执链(按样本 · revision 哈希链)</div><table><thead>"
"<tr><th>样本</th><th class='num'>rev</th><th>阶段</th><th>角色</th><th class='num'>attempt</th><th class='num'>补证</th><th>候选版本</th><th>结果</th></tr></thead><tbody>"
+ "".join(f"<tr><td><code>{esc(r[0])}</code></td><td class='num'>{esc(r[1])}</td><td>{esc(r[2])}</td><td>{esc(r[3])}</td>"
f"<td class='num'>{esc(r[4])}</td><td class='num'>{esc(r[5])}</td><td>{esc(r[6])}</td><td>{chip('failed' if r[7] else 'passed')}</td></tr>" for r in rc)
+ "</tbody></table></div>") 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 = ("<div class='card'><div class='hd'>质量评判</div><table><thead>"
"<tr><th>类型</th><th>维度</th><th>量表版本</th><th class='num'>分</th><th>结论</th><th>失败类别</th></tr></thead><tbody>"
+ "".join(f"<tr><td>{chip(u[0])}</td><td>{esc(u[1])}</td><td>{esc(u[2])}</td><td class='num'>{esc(u[3])}</td><td>{chip(u[4])}</td><td>{esc(u[5])}</td></tr>" for u in qres)
+ "</tbody></table></div>") 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 = ("<div class='card'><div class='hd'>模型调用</div><table><thead>"
"<tr><th>时间</th><th>调用方</th><th>请求模型</th><th>实际模型</th><th class='num'>输出token</th><th class='num'>成本</th></tr></thead><tbody>"
+ "".join(f"<tr><td>{esc(c[0])}</td><td>{esc(c[1])}</td><td>{esc(c[2])}</td><td>{esc(c[3])}</td>"
f"<td class='num'>{fmt_num(c[4])}</td><td class='num'>${float(c[5] or 0):.5f}</td></tr>" for c in calls)
+ "</tbody></table></div>") if calls else empty_gap("暂无模型调用", "")
cand = (empty_gap("待审候选 · 待落库", "候选表(99)未建。") if not table_exists("example_candidate")
else "<div class='empty'>候选表已建 · 该运行暂无候选</div>")
dec = (empty_gap("用户决策 · 待落库", "决策记录表(99)未建。") if not table_exists("example_user_decision")
else "<div class='empty'>决策表已建 · 该运行暂无决策</div>")
body = (f"<div class='crumb'><a href='/runs'>运行记录</a> / <code>{esc(rid)}</code></div>"
f"<h1>运行详情</h1>{info}"
f"<h2>回执链</h2>{rc_html}<h2>质量评判</h2>{q_html}<h2>模型调用</h2>{call_html}"
f"<h2>候选与决策</h2>{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("未找到", "<h1>404 · 页面不存在</h1>", ""))
except Exception: # 客户端给固定文案,细节写服务端 stderr(不泄露 DB 内部结构;看板挂了不牵连库)