diff --git a/dashboard/server.py b/dashboard/server.py new file mode 100644 index 0000000..de56117 --- /dev/null +++ b/dashboard/server.py @@ -0,0 +1,541 @@ +#!/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'{icon} {html.escape(str(text))}') + + +def esc(v): + if v is None: + return '∅' + 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"
{esc(title)}
{esc(desc)}
" + + +# ── 布局:对齐 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'{html.escape(label)}' + f'
{html.escape(dom)}
') + return f""" + +{html.escape(title)} · muse 看板 + +
{body} + +
""" + + +# ── 总览(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 ("

额度水位

" + 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"
" + f"
{text}(上限 {cap:,.0f})
") + return (f"

额度水位 窗口 {esc(wk)}

" + f"
" + 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:,} 次')}
") + + +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""" +
+
{fmt_num(works)}
作品
+
{fmt_num(chapters)}
章节
+
{fmt_num(blocks)}
正文块
+
{fmt_num(drafts)}
知识卡(草稿)
+
已确认 {fmt_num(entities)} 张
+
{fmt_num(refs)}
参考书
+
{fmt_num(cleans)}
清洗记录
+
""" + + 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"{esc(label)}{esc(tbl)}" + f"{cnt}{st}") + ledger = f""" +
落库账本 —— 一切输入产出,落没落库一目了然
+ + {"".join(rows)}
输入产出承接表行数状态
+

「未建」= 表还没建(合同要求落库,属缺口);「已建 · 0 行」= 表已就位,等创作链写入。 + 知识卡已确认 {fmt_num(entities)} 张(草稿≠已确认,全部确认前此数为 0 是正常状态)。

""" + + body = (f"

总览 · 健康库里现在有什么、缺什么——这就是「一切落库』的验收面

" + 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("作品", "

作品

" + empty_gap("库里还没有作品", "导入或创建后在此显示"), "/works") + trs = "".join( + f"{esc(r[1])}{chip(r[2])}" + f"{esc(r[3])}{esc(r[4])}{fmt_num(r[5])}" + for r in rows) + body = (f"

作品正式内容以库为准(muse_content_work)· 作品领域 01

" + f"
" + f"{trs}
作品状态类型章数字数
") + 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("作品", "

作品不存在

", "/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"{esc(c[1])}" + f"{esc(c[2]) or f'第{c[1]}章'}" + f"{chip(c[3])}{esc(c[4])}" for c in chs) + chap = (f"
章节({len(chs)})
" + f"" + f"{trs}
序标题状态修订
") + else: + chap = empty_gap("还没有章节", "") + body = (f"
作品 / {esc(title)}
" + f"

{esc(title)} {chip(status)}

" + f"

{esc(genre)} · {fmt_num(wc)} 字 · {esc(cc)} 章

" + f"

{esc(summary)}

{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("章节", "

作品不存在

", "/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("章节", "

章节不存在

", "/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"
作品 / {esc(wtitle)} " + f"/ 第{esc(order_no)}章
" + f"

{esc(ctitle) or f'第{order_no}章'}{fmt_num(sum(b[1] or 0 for b in blocks))} 字 · 正文全文(实时查库)

" + f"
{esc(text) if text.strip() else '(本章正文为空)'}
") + return layout(f"{wtitle} · 第{order_no}章", body, "/works") + + +# ── 知识库(02 实体 + 03 范式:知识卡 + 参考书·拆书·清洗)───────────────────── +def _barset(rows, slots): + if not rows: + return "
无数据
" + 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"
{esc(name)}" + f"" + f"{fmt_num(n)} · {n/total*100:.0f}%
") + 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"""
+
{fmt_num(drafts)}
草稿卡(待确认)
+
{fmt_num(entities)}
已确认知识
+
{"当前为 0 · 全部草稿待确认" if entities==0 else ""}
+
{fmt_num(public)}
公共范式卡(work_id=0)
+
{fmt_num(work_cards)}
作品级卡(work_id≠0)
+
+
公共范式卡 · 按型分布(型在 payload 中文键「型」)
{_barset(pub_types,[1,2,3,4,5])}
+
作品级卡 · 按型分布(型在 payload 英文键 type)
{_barset(wk_types,[1,2,3,4,5])}
+

型别看 draft_payload,不看 draft_type 列(该列恒为 entity,无区分力)。 + style / pacing 已注册 schema 但当前 0 张卡。草稿≠已确认:确认前 entity 表为 0 是正常状态。

""" + + # 参考书·拆书·清洗(知识从哪来——拆书是知识的生产线) + _, 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"{esc(r[1])}{esc(r[2])}" + f"{esc(r[3])} / {esc(r[4] or '?')}{fmt_num(r[5])}" + f"{chip(r[6])}" for r in refs) + reftbl = (f"
参考书档案({len(refs)})
" + f"" + f"{trs}
原文件作者导入/声明章数字符数拆书状态
") + 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 = ("
拆书任务状态(章 × 两阶段)
" + + "".join(f"
脚手架 {chip(s)} · 范式 {chip(p)}" + f"{fmt_num(n)} 章
" for s, p, n in pt) + "
" + ) 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"
清洗审计(共 {fmt_num(cleans)} 条 · 最近 10)
" + + "".join(f"
{esc(r[0])} · {esc(r[2])}" + f"{esc(r[1])}" + f"{esc(r[3])} 处
" for r in recent) + "
" + ) if recent else "" + + body = (f"

知识库实体与范式(muse_knowledge_*)+ 它们的生产线(拆书·清洗)· 实体 02 · 范式 03

" + f"{cards}

参考书 · 拆书 · 清洗

{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"{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("暂无运行记录", "运行注册表已就位,等创作链(写作/检测/审核)落库后,每次运行登记一行。") + + # 质量评判(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"{chip(r[0])}{esc(r[1])}{esc(r[2])}" + f"{chip(r[3])}{esc(r[4])}{esc((r[5] or '')[:12])}" for r in qres) + q_html = (f"
质量评判(最近 50 · 有评判就记录就可见)
" + 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 "
决策表已建 · 暂无决策
") + + # 模型调用明细(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}") + 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("未找到", "

404 · 页面不存在

", "")) + except Exception: # 客户端给固定文案,细节写服务端 stderr(不泄露 DB 内部结构;看板挂了不牵连库) + traceback.print_exc() + self._send(500, layout("出错", "

看板渲染出错

该请求处理失败,细节记录在服务端日志。

", "")) + + 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()