zizi eeac03373c 框架(看板): 新增只读可视化看板首版(总览/作品/知识库/运行记录四空间)
- 纯 Python 标准库 HTTP 服务端渲染,零 web 框架、零 JS、零新增依赖
- 只读机械门:会话级 default_transaction_read_only=on(写语句被 PG 拒);全文件仅 SELECT
- 布局对齐 muse 领域:总览(落库账本+额度水位) / 作品(作品→章→正文) / 知识库(范式分布+参考书拆书清洗) / 运行记录(运行/质量/候选/决策/调用)
- 防误导:型别看 draft_payload 不看 draft_type、公共/作品看 work_id、已确认 0 行如实显示、实时查库无缓存
- 500 页不泄露 DB 内部错误(固定文案+细节进日志)
- 相对合同 §10 五空间为首版:智能体空间(读102登记表)、运行详情下钻、工作区标签随后补
2026-07-31 00:31:16 +08:00

542 lines
31 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""只读可视化看板(muse-example 库)—— 纯 Python 标准库 HTTP 服务。
合同:.agent/docs/architecture/可视化模块合同.md(唯一 SoT)。
布局:对齐 muse 的领域布局(作品 / 知识库 / 智能体 三空间 + 总览),
与领域索引(domains/_index.md)的领域分区一致——看板按领域看库,不按功能切。
命根子(验收按机械门查):
1. 连接只读:会话级 default_transaction_read_only=on,任何写语句数据库直接报错。
2. 代码无写语句:全文件只允许 SELECT,无 INSERT/UPDATE/DELETE/DDL(grep 即门)。
3. 不接会写的通道:不调用 confirm / db skill,只自己只读查库。
4. 挂了不牵连:进程崩了不影响库内正式内容与创作链。
视觉系统:dataviz 方法(已验证默认调色板,light/dark 双模式,状态色固定不主题化)。
跑法:.venv/bin/python dashboard/server.py (默认 http://127.0.0.1:8765)
"""
import html
import re
import traceback
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import psycopg
# 与 db skill 同一库,但独立只读连接(合同 §2:连接串与写通道分开,只读是机械门)
DSN = "postgresql://root:f6710e2d0294eb1c10e26a805a64bc54@100.64.0.8:5433/muse-example"
HOST, PORT = "127.0.0.1", 8765
# 额度治理上限(llm skill 合同:5 小时窗 $24 / 6000 次)
QUOTA_USD_CAP, QUOTA_CALL_CAP = 24.0, 6000
def ro_connect():
"""只读连接:会话级锁死只读,写语句会被 PostgreSQL 拒绝(机械门,不靠自觉)。"""
return psycopg.connect(DSN, options="-c default_transaction_read_only=on")
def q(sql, params=()):
"""只读查询,返回 (列名, 行)。每次请求实时查库,不缓存(合同 §7)。"""
with ro_connect() as conn:
cur = conn.execute(sql, params)
cols = [d.name for d in cur.description] if cur.description else []
return cols, cur.fetchall()
def one(sql, params=()):
"""取单值。"""
_, rows = q(sql, params)
return rows[0][0] if rows else None
def table_exists(name):
return one("SELECT 1 FROM pg_tables WHERE schemaname='public' AND tablename=%s", (name,))
# ── 视觉系统:dataviz 已验证调色板(light/dark),状态色固定不主题化 ──────────────
CSS = """
:root {
color-scheme: light;
--plane:#f9f9f7; --surface:#fcfcfb; --ink:#0b0b0b; --ink2:#52514e; --muted:#898781;
--grid:#e1e0d9; --baseline:#c3c2b7; --ring:rgba(11,11,11,.10);
--s1:#2a78d6; --s2:#008300; --s3:#e87ba4; --s4:#eda100; --s5:#1baf7a;
--s6:#eb6834; --s7:#4a3aa7; --s8:#e34948;
--good:#0ca30c; --warn:#fab219; --serious:#ec835a; --crit:#d03b3b; --goodtxt:#006300;
}
@media (prefers-color-scheme: dark) {
:root {
color-scheme: dark;
--plane:#0d0d0d; --surface:#1a1a19; --ink:#ffffff; --ink2:#c3c2b7; --muted:#898781;
--grid:#2c2c2a; --baseline:#383835; --ring:rgba(255,255,255,.10);
--s1:#3987e5; --s2:#008300; --s3:#d55181; --s4:#c98500; --s5:#199e70;
--s6:#d95926; --s7:#9085e9; --s8:#e66767; --goodtxt:#0ca30c;
}
}
* { box-sizing:border-box; margin:0; padding:0; }
body {
background:var(--plane); color:var(--ink);
font:15px/1.6 system-ui,-apple-system,"Segoe UI","PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif;
display:flex; min-height:100vh;
}
nav {
width:212px; flex:none; background:var(--surface); border-right:1px solid var(--grid);
padding:22px 14px; position:sticky; top:0; height:100vh;
}
nav .brand { font-weight:700; font-size:16px; letter-spacing:.02em; padding:0 8px 16px; }
nav .brand small { display:block; font-weight:400; color:var(--muted); font-size:12px; }
nav a {
display:block; padding:8px 10px; border-radius:6px; color:var(--ink2);
text-decoration:none; font-size:14px; transition:background .12s,color .12s;
}
nav a:hover { background:var(--grid); color:var(--ink); }
nav a.on { background:var(--grid); color:var(--ink); font-weight:600; }
nav .dom { font-size:10.5px; color:var(--muted); padding:2px 10px 0; letter-spacing:.04em; }
nav .ro { margin-top:24px; padding:10px; font-size:11px; color:var(--muted);
border:1px dashed var(--baseline); border-radius:6px; }
main { flex:1; padding:30px 38px 60px; max-width:1080px; }
h1 { font-size:26px; font-weight:750; letter-spacing:-.01em; }
h1 .sub { display:block; font-size:13px; font-weight:400; color:var(--muted); margin-top:4px; }
h2 { font-size:16px; font-weight:650; margin:30px 0 12px; }
p.note { color:var(--ink2); font-size:13.5px; margin:8px 0; }
.tiles { display:grid; grid-template-columns:repeat(auto-fill,minmax(168px,1fr)); gap:12px; margin-top:18px; }
.tile { background:var(--surface); border:1px solid var(--ring); border-radius:8px; padding:14px 16px;
transition:transform .12s, box-shadow .12s; }
.tile:hover { transform:translateY(-1px); box-shadow:0 3px 10px var(--ring); }
.tile .v { font-size:28px; font-weight:720; letter-spacing:-.02em; line-height:1.15; } /* 大数字用比例数字 */
.tile .l { font-size:12.5px; color:var(--muted); margin-top:2px; }
.tile .d { font-size:12px; margin-top:5px; color:var(--ink2); }
.tile.zero .v { color:var(--muted); }
.card { background:var(--surface); border:1px solid var(--ring); border-radius:8px; margin-top:14px; overflow:hidden; }
.card > .hd { padding:12px 16px; border-bottom:1px solid var(--grid); font-weight:650; font-size:14px; }
table { width:100%; border-collapse:collapse; font-size:13.5px; }
th { text-align:left; color:var(--muted); font-weight:600; font-size:12px; padding:9px 14px;
border-bottom:1px solid var(--grid); white-space:nowrap; }
td { padding:9px 14px; border-bottom:1px solid var(--grid); color:var(--ink2); vertical-align:top; }
tr:last-child td { border-bottom:none; }
tbody tr { transition:background .1s; }
tbody tr:hover { background:var(--plane); }
td.num, th.num { text-align:right; font-variant-numeric:tabular-nums; } /* 对齐的数字列用等宽数字 */
td a { color:var(--s1); text-decoration:none; }
td a:hover { text-decoration:underline; }
.chip { display:inline-flex; align-items:center; gap:6px; font-size:12px; font-weight:600;
padding:2px 9px; border-radius:999px; white-space:nowrap; border:1px solid transparent; }
.chip .dot { width:7px; height:7px; border-radius:50%; flex:none; }
.chip.good { color:var(--goodtxt); background:color-mix(in srgb,var(--good) 12%, transparent); }
.chip.good .dot { background:var(--good); }
.chip.warn { color:var(--ink2); background:color-mix(in srgb,var(--warn) 16%, transparent); }
.chip.warn .dot { background:var(--warn); }
.chip.serious { color:var(--ink2); background:color-mix(in srgb,var(--serious) 16%, transparent); }
.chip.serious .dot { background:var(--serious); }
.chip.crit { color:var(--crit); background:color-mix(in srgb,var(--crit) 12%, transparent); }
.chip.crit .dot { background:var(--crit); }
.chip.gap { color:var(--muted); border:1px dashed var(--baseline); background:transparent; }
.chip.gap .dot { background:var(--baseline); }
.meter { margin:10px 0; }
.meter .track { height:10px; border-radius:5px; background:var(--grid); overflow:hidden; }
.meter .fill { height:100%; border-radius:5px; background:var(--s1); transition:width .3s; }
.meter .cap { font-size:12px; color:var(--muted); margin-top:4px; font-variant-numeric:tabular-nums; }
.bar-row { display:flex; align-items:center; gap:12px; padding:7px 16px; font-size:13.5px; }
.bar-row .name { width:150px; flex:none; color:var(--ink2); }
.bar-row .track { flex:1; height:16px; background:var(--grid); border-radius:4px; overflow:hidden; }
.bar-row .fill { height:100%; border-radius:4px; }
.bar-row .val { width:110px; flex:none; text-align:right; font-variant-numeric:tabular-nums; color:var(--ink2); }
.empty { padding:34px 16px; text-align:center; color:var(--muted); font-size:13.5px; }
.empty.gap { border:1px dashed var(--baseline); border-radius:8px; margin-top:14px; background:transparent; }
.empty b { color:var(--ink2); }
article.prose { background:var(--surface); border:1px solid var(--ring); border-radius:8px;
padding:30px 40px; margin-top:16px; max-width:760px;
font-size:16.5px; line-height:2; white-space:pre-wrap; color:var(--ink); }
.crumb { font-size:13px; color:var(--muted); margin-bottom:6px; }
.crumb a { color:var(--s1); text-decoration:none; }
footer { margin-top:44px; font-size:11.5px; color:var(--muted); border-top:1px solid var(--grid); padding-top:12px; }
"""
# 状态 → (角色, 图标)。状态色保留语义,图标+文字同在,绝不单靠颜色(dataviz 规矩)。
_STATUS = {
"good": ("good", "✓"), "warn": ("warn", "●"), "serious": ("serious", "▲"),
"crit": ("crit", "✕"), "gap": ("gap", "◌"),
}
_STATE_MAP = {
"pending": "warn", "draft": "warn", "checking": "warn", "parsing": "warn",
"running": "warn", "writing": "warn", "shadow": "warn",
"confirmed": "good", "passed": "good", "done": "good", "parsed": "good",
"completed": "good", "accepted": "good", "active": "good", "canonical": "good",
"published": "good",
"rejected": "serious", "failed": "serious", "revise": "serious", "stale": "serious",
"discarded": "crit", "error": "crit", "unauthorized": "crit", "ignored": "crit",
}
def chip(state, label=None):
"""状态标签:颜色按语义角色,图标+文字同在。未识别状态归 warn(待审)。"""
role = _STATE_MAP.get(str(state).lower(), "warn") if state else "gap"
if state in (None, "", "not_built"):
role = "gap"
r, icon = _STATUS[role]
text = label if label is not None else (state or "未建")
return (f'<span class="chip {r}"><span class="dot"></span>{icon} {html.escape(str(text))}</span>')
def esc(v):
if v is None:
return '<span style="color:var(--muted)">∅</span>'
return html.escape(str(v))
def fmt_num(v):
try:
return f"{int(v):,}"
except (TypeError, ValueError):
return esc(v)
def empty_gap(title, desc):
"""待落库空状态:中性虚线,与"失败"红严格区分。"""
return f"<div class='empty gap'><b>{esc(title)}</b><br>{esc(desc)}</div>"
# ── 布局:对齐 muse 领域布局(总览 / 作品 / 知识库 / 智能体)────────────────────
# 每个导航项标注对应领域(domains/_index.md 的分区),看板按领域看库。
NAV = [
("/", "总览 · 健康", "数据权威 08"),
("/works", "作品", "作品领域 01"),
("/knowledge", "知识库", "实体 02 · 范式 03"),
("/runs", "运行记录", "流程 05 · 质量 06"),
]
def layout(title, body, active="/"):
links = []
for href, label, dom in NAV:
cls = "on" if href == active else ""
links.append(f'<a class="{cls}" href="{href}">{html.escape(label)}</a>'
f'<div class="dom">{html.escape(dom)}</div>')
return f"""<!doctype html><html lang="zh"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>{html.escape(title)} · muse 看板</title><style>{CSS}</style></head><body>
<nav><div class="brand">muse 看板<small>muse-example · 只读 · 按领域看库</small></div>{"".join(links)}
<div class="ro">只读连接<br>本看板不写任何数据<br>接受 / 丢弃仍走 confirm</div></nav>
<main>{body}
<footer>数据实时查库,无缓存 · 数字为当前库内事实 · 空白即落库缺口</footer>
</main></body></html>"""
# ── 总览(08 数据权威:落库验收面 + 健康数字 + 额度水位)──────────────────────
# 落库缺口板:一切输入产出该落哪张表,建没建、有没有数(合同 §4.2 的验收面)
_LEDGER = [
("待审候选", "example_candidate"),
("用户决策", "example_user_decision"),
("运行回执", "example_run_receipt"),
("质量评判", "example_quality_result"),
("模型调用明细", "example_llm_call"),
("运行注册", "example_run"),
("raw 全文", "example_raw_content"),
("规划 / 细纲", "example_planning_section"),
("上下文冻结", "example_context_freeze"),
]
def _quota_block():
_, rows = q("SELECT window_key, minimax_usd, total_calls FROM example_llm_quota "
"ORDER BY window_key DESC LIMIT 1")
if not rows:
return ("<h2>额度水位</h2>" + empty_gap("暂无额度记录", "发生模型调用后自动记录"))
wk, usd, calls = rows[0]
usd, calls = float(usd or 0), int(calls or 0)
def meter(val, cap, text):
pct = min(100.0, val / cap * 100) if cap else 0
return (f"<div class='meter'><div class='track'><div class='fill' style='width:{pct:.1f}%'></div></div>"
f"<div class='cap'>{text}(上限 {cap:,.0f})</div></div>")
return (f"<h2>额度水位 <span style='font-weight:400;color:var(--muted);font-size:12px'>窗口 {esc(wk)}</span></h2>"
f"<div class='card'><div style='padding:14px 16px'>"
f"{meter(usd, QUOTA_USD_CAP, f'MiniMax 花费 ${usd:.4f} / ${QUOTA_USD_CAP:.0f}')}"
f"{meter(calls, QUOTA_CALL_CAP, f'成功调用 {calls:,} / {QUOTA_CALL_CAP:,} 次')}</div></div>")
def view_home():
works = one("SELECT count(*) FROM muse_content_work WHERE deleted=false") or 0
chapters = one("SELECT count(*) FROM muse_content_chapter WHERE deleted=false") or 0
blocks = one("SELECT count(*) FROM muse_content_block WHERE deleted=false") or 0
drafts = one("SELECT count(*) FROM muse_knowledge_draft WHERE deleted=false") or 0
entities = one("SELECT count(*) FROM muse_knowledge_entity WHERE deleted=false") or 0
refs = one("SELECT count(*) FROM example_reference_work WHERE deleted=false") or 0
cleans = one("SELECT count(*) FROM example_clean_log WHERE deleted=false") or 0
tiles = f"""
<div class="tiles">
<div class="tile"><div class="v">{fmt_num(works)}</div><div class="l">作品</div></div>
<div class="tile"><div class="v">{fmt_num(chapters)}</div><div class="l">章节</div></div>
<div class="tile"><div class="v">{fmt_num(blocks)}</div><div class="l">正文块</div></div>
<div class="tile"><div class="v">{fmt_num(drafts)}</div><div class="l">知识卡(草稿)</div>
<div class="d">已确认 {fmt_num(entities)} 张</div></div>
<div class="tile {'zero' if refs == 0 else ''}"><div class="v">{fmt_num(refs)}</div><div class="l">参考书</div></div>
<div class="tile"><div class="v">{fmt_num(cleans)}</div><div class="l">清洗记录</div></div>
</div>"""
rows = []
for label, tbl in _LEDGER:
if not table_exists(tbl):
st, cnt = chip("not_built", "未建 · 待落库"), "—"
else:
n = one(f'SELECT count(*) FROM "{tbl}"') or 0
st = chip("good", "已建 · 有数据") if n > 0 else chip("warn", "已建 · 0 行")
cnt = fmt_num(n)
rows.append(f"<tr><td>{esc(label)}</td><td><code>{esc(tbl)}</code></td>"
f"<td class='num'>{cnt}</td><td>{st}</td></tr>")
ledger = f"""
<div class="card"><div class="hd">落库账本 —— 一切输入产出,落没落库一目了然</div>
<table><thead><tr><th>输入产出</th><th>承接表</th><th class="num">行数</th><th>状态</th></tr></thead>
<tbody>{"".join(rows)}</tbody></table></div>
<p class="note">「未建」= 表还没建(合同要求落库,属缺口);「已建 · 0 行」= 表已就位,等创作链写入。
知识卡已确认 {fmt_num(entities)} 张(草稿≠已确认,全部确认前此数为 0 是正常状态)。</p>"""
body = (f"<h1>总览 · 健康<span class='sub'>库里现在有什么、缺什么——这就是「一切落库』的验收面</span></h1>"
f"{tiles}{ledger}{_quota_block()}")
return layout("总览", body, "/")
# ── 作品(01 作品领域:作品 → 章节 → 正文全文)────────────────────────────────
def view_works():
_, rows = q("SELECT id,title,status,genre,chapter_count,word_count FROM muse_content_work "
"WHERE deleted=false ORDER BY id")
if not rows:
return layout("作品", "<h1>作品</h1>" + empty_gap("库里还没有作品", "导入或创建后在此显示"), "/works")
trs = "".join(
f"<tr><td><a href='/works/{r[0]}'>{esc(r[1])}</a></td><td>{chip(r[2])}</td>"
f"<td>{esc(r[3])}</td><td class='num'>{esc(r[4])}</td><td class='num'>{fmt_num(r[5])}</td></tr>"
for r in rows)
body = (f"<h1>作品<span class='sub'>正式内容以库为准(muse_content_work)· 作品领域 01</span></h1>"
f"<div class='card'><table><thead><tr><th>作品</th><th>状态</th><th>类型</th>"
f"<th class='num'>章数</th><th class='num'>字数</th></tr></thead><tbody>{trs}</tbody></table></div>")
return layout("作品", body, "/works")
def view_work(wid):
_, wrows = q("SELECT id,title,status,genre,summary,word_count,chapter_count "
"FROM muse_content_work WHERE id=%s AND deleted=false", (wid,))
if not wrows:
return layout("作品", "<h1>作品不存在</h1>", "/works")
wid_, title, status, genre, summary, wc, cc = wrows[0]
_, chs = q("SELECT id, order_no, title, status, revision FROM muse_content_chapter "
"WHERE work_id=%s AND deleted=false ORDER BY order_no", (wid,))
if chs:
trs = "".join(
f"<tr><td class='num'>{esc(c[1])}</td>"
f"<td><a href='/works/{wid}/ch/{c[1]}'>{esc(c[2]) or f'第{c[1]}章'}</a></td>"
f"<td>{chip(c[3])}</td><td class='num'>{esc(c[4])}</td></tr>" for c in chs)
chap = (f"<div class='card'><div class='hd'>章节({len(chs)})</div>"
f"<table><thead><tr><th class='num'>序</th><th>标题</th><th>状态</th><th class='num'>修订</th></tr></thead>"
f"<tbody>{trs}</tbody></table></div>")
else:
chap = empty_gap("还没有章节", "")
body = (f"<div class='crumb'><a href='/works'>作品</a> / {esc(title)}</div>"
f"<h1>{esc(title)} <span style='font-size:14px'>{chip(status)}</span></h1>"
f"<p class='note'>{esc(genre)} · {fmt_num(wc)} 字 · {esc(cc)} 章</p>"
f"<p class='note'>{esc(summary)}</p>{chap}")
return layout(title, body, "/works")
def view_chapter(wid, order_no):
_, wrow = q("SELECT title FROM muse_content_work WHERE id=%s AND deleted=false", (wid,))
if not wrow:
return layout("章节", "<h1>作品不存在</h1>", "/works")
wtitle = wrow[0][0]
_, ch = q("SELECT id,title,order_no FROM muse_content_chapter "
"WHERE work_id=%s AND order_no=%s AND deleted=false", (wid, order_no))
if not ch:
return layout("章节", "<h1>章节不存在</h1>", "/works")
cid, ctitle, _ = ch[0]
_, blocks = q("SELECT content_text, word_count FROM muse_content_block "
"WHERE chapter_id=%s AND deleted=false ORDER BY order_no", (cid,))
text = "\n\n".join((b[0] or "") for b in blocks)
body = (f"<div class='crumb'><a href='/works'>作品</a> / <a href='/works/{wid}'>{esc(wtitle)}</a> "
f"/ 第{esc(order_no)}章</div>"
f"<h1>{esc(ctitle) or f'第{order_no}章'}<span class='sub'>{fmt_num(sum(b[1] or 0 for b in blocks))} 字 · 正文全文(实时查库)</span></h1>"
f"<article class='prose'>{esc(text) if text.strip() else '(本章正文为空)'}</article>")
return layout(f"{wtitle} · 第{order_no}章", body, "/works")
# ── 知识库(02 实体 + 03 范式:知识卡 + 参考书·拆书·清洗)─────────────────────
def _barset(rows, slots):
if not rows:
return "<div class='empty'>无数据</div>"
mx = max(r[1] for r in rows)
total = sum(r[1] for r in rows)
out = []
for i, (name, n) in enumerate(rows):
color = f"var(--s{slots[i % len(slots)]})"
out.append(f"<div class='bar-row'><span class='name'>{esc(name)}</span>"
f"<span class='track'><span class='fill' style='width:{n/mx*100:.1f}%;background:{color}'></span></span>"
f"<span class='val'>{fmt_num(n)} · {n/total*100:.0f}%</span></div>")
return "".join(out)
def view_knowledge():
drafts = one("SELECT count(*) FROM muse_knowledge_draft WHERE deleted=false") or 0
entities = one("SELECT count(*) FROM muse_knowledge_entity WHERE deleted=false") or 0
public = one("SELECT count(*) FROM muse_knowledge_draft WHERE work_id=0 AND deleted=false") or 0
work_cards = one("SELECT count(*) FROM muse_knowledge_draft WHERE work_id<>0 AND deleted=false") or 0
# 型别看 payload,不看 draft_type 列(库内约定,防误导):公共卡用中文键「型」,作品卡用英文键 type
_, pub_types = q("SELECT COALESCE(draft_payload->>'型','(无型)') t, count(*) FROM muse_knowledge_draft "
"WHERE work_id=0 AND deleted=false GROUP BY 1 ORDER BY 2 DESC")
_, wk_types = q("SELECT COALESCE(draft_payload->>'type','(无型)') t, count(*) FROM muse_knowledge_draft "
"WHERE work_id<>0 AND deleted=false GROUP BY 1 ORDER BY 2 DESC")
cards = f"""<div class="tiles">
<div class="tile"><div class="v">{fmt_num(drafts)}</div><div class="l">草稿卡(待确认)</div></div>
<div class="tile {'zero' if entities==0 else ''}"><div class="v">{fmt_num(entities)}</div><div class="l">已确认知识</div>
<div class="d">{"当前为 0 · 全部草稿待确认" if entities==0 else ""}</div></div>
<div class="tile"><div class="v">{fmt_num(public)}</div><div class="l">公共范式卡(work_id=0)</div></div>
<div class="tile"><div class="v">{fmt_num(work_cards)}</div><div class="l">作品级卡(work_id≠0)</div></div>
</div>
<div class="card"><div class="hd">公共范式卡 · 按型分布(型在 payload 中文键「型」)</div>{_barset(pub_types,[1,2,3,4,5])}</div>
<div class="card"><div class="hd">作品级卡 · 按型分布(型在 payload 英文键 type)</div>{_barset(wk_types,[1,2,3,4,5])}</div>
<p class="note">型别看 <code>draft_payload</code>,<b>不看</b> <code>draft_type</code> 列(该列恒为 entity,无区分力)。
style / pacing 已注册 schema 但当前 0 张卡。草稿≠已确认:确认前 entity 表为 0 是正常状态。</p>"""
# 参考书·拆书·清洗(知识从哪来——拆书是知识的生产线)
_, refs = q("SELECT id, source_file, author, imported_chapter_count, declared_chapter_count, "
"char_count, parse_status FROM example_reference_work WHERE deleted=false ORDER BY id")
if refs:
trs = "".join(
f"<tr><td>{esc(r[1])}</td><td>{esc(r[2])}</td>"
f"<td class='num'>{esc(r[3])} / {esc(r[4] or '?')}</td><td class='num'>{fmt_num(r[5])}</td>"
f"<td>{chip(r[6])}</td></tr>" for r in refs)
reftbl = (f"<div class='card'><div class='hd'>参考书档案({len(refs)})</div>"
f"<table><thead><tr><th>原文件</th><th>作者</th><th class='num'>导入/声明章数</th>"
f"<th class='num'>字符数</th><th>拆书状态</th></tr></thead><tbody>{trs}</tbody></table></div>")
else:
reftbl = empty_gap("还没有参考书", "导入参考书后在此显示")
_, pt = q("SELECT scaffold_status, pattern_status, count(*) FROM example_parse_task "
"WHERE deleted=false GROUP BY 1,2 ORDER BY 3 DESC")
pt_html = ("<div class='card'><div class='hd'>拆书任务状态(章 × 两阶段)</div>" +
"".join(f"<div class='bar-row'><span class='name'>脚手架 {chip(s)} · 范式 {chip(p)}</span>"
f"<span class='val'>{fmt_num(n)} 章</span></div>" for s, p, n in pt) + "</div>"
) if pt else ""
cleans = one("SELECT count(*) FROM example_clean_log WHERE deleted=false") or 0
_, recent = q("SELECT batch, reason, model, occurrences, create_time FROM example_clean_log "
"WHERE deleted=false ORDER BY create_time DESC LIMIT 10")
clean_html = (f"<div class='card'><div class='hd'>清洗审计(共 {fmt_num(cleans)} 条 · 最近 10)</div>" +
"".join(f"<div class='bar-row'><span class='name'>{esc(r[0])} · {esc(r[2])}</span>"
f"<span class='track' style='background:transparent'>{esc(r[1])}</span>"
f"<span class='val'>{esc(r[3])} 处</span></div>" for r in recent) + "</div>"
) if recent else ""
body = (f"<h1>知识库<span class='sub'>实体与范式(muse_knowledge_*)+ 它们的生产线(拆书·清洗)· 实体 02 · 范式 03</span></h1>"
f"{cards}<h2>参考书 · 拆书 · 清洗</h2>{reftbl}{pt_html}{clean_html}")
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"<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("暂无运行记录", "运行注册表已就位,等创作链(写作/检测/审核)落库后,每次运行登记一行。")
# 质量评判(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>")
# 模型调用明细(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}")
return layout("运行记录", body, "/runs")
# ── 路由 ─────────────────────────────────────────────────────────────────────
_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"^/runs$"), lambda m: view_runs_page()),
]
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
path = self.path.split("?", 1)[0]
try:
for pat, fn in _ROUTES:
m = pat.match(path)
if m:
self._send(200, fn(m))
return
self._send(404, layout("未找到", "<h1>404 · 页面不存在</h1>", ""))
except Exception: # 客户端给固定文案,细节写服务端 stderr(不泄露 DB 内部结构;看板挂了不牵连库)
traceback.print_exc()
self._send(500, layout("出错", "<h1>看板渲染出错</h1><p class='note'>该请求处理失败,细节记录在服务端日志。</p>", ""))
def _send(self, code, body):
data = body.encode("utf-8")
self.send_response(code)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
def log_message(self, fmt, *args): # 静音默认访问日志
pass
if __name__ == "__main__":
print(f"muse 只读看板:http://{HOST}:{PORT} (Ctrl-C 退出;只读连接,不写任何数据)")
ThreadingHTTPServer((HOST, PORT), Handler).serve_forever()