706 lines
44 KiB
Python
706 lines
44 KiB
Python
#!/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
|
||
from urllib.parse import urlparse, parse_qs
|
||
|
||
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"),
|
||
("/agents", "智能体", "Agent 07"),
|
||
("/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 质量:运行列表按作品/按角色双维 + 运行详情下钻)────────
|
||
_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 %s", (limit,))
|
||
return runs
|
||
|
||
|
||
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>")
|
||
|
||
|
||
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)。")
|
||
if table_exists("example_candidate"):
|
||
_, cands = q("SELECT id, work_id, target_chapter, candidate_version, run_type, state, source_role "
|
||
"FROM example_candidate WHERE deleted=false AND state IN ('draft','checking','passed') "
|
||
"ORDER BY create_time DESC LIMIT 30")
|
||
cand = ("<div class='card'><div class='hd'>待审候选(活跃)</div><table><thead>"
|
||
"<tr><th class='num'>id</th><th>作品</th><th class='num'>章</th><th>版本</th><th>类型</th><th>状态</th><th>角色</th></tr></thead><tbody>"
|
||
+ "".join(f"<tr><td class='num'>{esc(c[0])}</td><td>{esc(c[1])}</td><td class='num'>{esc(c[2])}</td>"
|
||
f"<td><code>{esc(c[3])}</code></td><td>{esc(c[4])}</td><td>{chip(c[5])}</td><td>{esc(c[6])}</td></tr>" for c in cands)
|
||
+ "</tbody></table></div>") if cands else "<div class='empty'>候选表已建 · 暂无待审候选</div>"
|
||
else:
|
||
cand = empty_gap("待审候选 · 待落库", "候选表(99)未建,AI 候选正文暂无持久化登记。")
|
||
if table_exists("example_user_decision"):
|
||
_, decs = q("SELECT work_id, target_chapter, decision, rationale, decided_by, create_time "
|
||
"FROM example_user_decision ORDER BY create_time DESC LIMIT 30")
|
||
dec = ("<div class='card'><div class='hd'>用户决策(最近 30)</div><table><thead>"
|
||
"<tr><th>作品</th><th class='num'>章</th><th>决策</th><th>理据</th><th>决策人</th><th>时间</th></tr></thead><tbody>"
|
||
+ "".join(f"<tr><td>{esc(d[0])}</td><td class='num'>{esc(d[1])}</td><td>{chip({'accept':'good','merge':'warn','discard':'crit'}.get(d[2],'warn'), d[2])}</td>"
|
||
f"<td>{esc((d[3] or '')[:40])}</td><td>{esc(d[4])}</td><td>{esc(d[5])}</td></tr>" for d in decs)
|
||
+ "</tbody></table></div>") if decs else "<div class='empty'>决策表已建 · 暂无决策</div>"
|
||
else:
|
||
dec = empty_gap("用户决策 · 待落库", "决策记录表(99)未建,接受/合并/丢弃暂以 git 提交留痕。")
|
||
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 列表 → 逗号串;非列表原样转义。"""
|
||
if isinstance(v, list):
|
||
return esc(", ".join(str(x) for x in v)) if v else '<span style="color:var(--muted)">∅</span>'
|
||
return esc(v)
|
||
|
||
|
||
def view_agents():
|
||
_, roles = q("SELECT role, display_name, model, tools FROM example_agent_role "
|
||
"WHERE deleted=false ORDER BY role")
|
||
_, skills = q("SELECT skill_name, purpose, model_used FROM example_skill "
|
||
"WHERE deleted=false ORDER BY skill_name")
|
||
role_rows = "".join(
|
||
f"<tr><td><a href='/agents/role/{esc(r[0])}'>{esc(r[1] or r[0])}</a> "
|
||
f"<span style='color:var(--muted)'>{esc(r[0])}</span></td>"
|
||
f"<td>{esc(r[2])}</td><td>{_fmt_list(r[3])}</td></tr>" for r in roles)
|
||
skill_rows = "".join(
|
||
f"<tr><td><a href='/agents/skill/{esc(s[0])}'>{esc(s[0])}</a></td>"
|
||
f"<td>{esc((s[1] or '')[:56])}</td><td>{esc(s[2])}</td></tr>" for s in skills)
|
||
body = (f"<h1>智能体<span class='sub'>角色与技能 · 读 102 登记表(权威在 Git 侧,登记脚本同步)· Agent 07</span></h1>"
|
||
f"<div class='card'><div class='hd'>角色({len(roles)})</div>"
|
||
f"<table><thead><tr><th>角色</th><th>模型</th><th>工具</th></tr></thead><tbody>{role_rows}</tbody></table></div>"
|
||
f"<div class='card'><div class='hd'>技能({len(skills)})</div>"
|
||
f"<table><thead><tr><th>技能</th><th>用途</th><th>用模型</th></tr></thead><tbody>{skill_rows}</tbody></table></div>")
|
||
return layout("智能体", body, "/agents")
|
||
|
||
|
||
def view_agent_role(role):
|
||
_, rr = q("SELECT role, display_name, model, effort, budget_usd, tools, responsibility, source_ref, synced_at "
|
||
"FROM example_agent_role WHERE role=%s AND deleted=false", (role,))
|
||
if not rr:
|
||
return layout("角色", "<h1>角色不存在</h1>", "/agents")
|
||
role_, disp, model, effort, budget, tools, resp, src, synced = rr[0]
|
||
info = (f"<div class='card'><div class='hd'>{esc(disp or role_)} <span style='color:var(--muted)'>{esc(role_)}</span></div>"
|
||
f"<table>"
|
||
f"<tr><td style='width:120px;color:var(--muted)'>模型归属</td><td>{esc(model)}</td></tr>"
|
||
f"<tr><td style='color:var(--muted)'>工具</td><td>{_fmt_list(tools)}</td></tr>"
|
||
f"<tr><td style='color:var(--muted)'>可配置项</td><td>effort {esc(effort)} · 预算 {esc(budget)}(暂空待补)</td></tr>"
|
||
f"<tr><td style='color:var(--muted)'>出处</td><td><code>{esc(src)}</code> · 同步于 {esc(synced)}</td></tr>"
|
||
f"<tr><td style='color:var(--muted)'>职责</td><td>{esc(resp)}</td></tr>"
|
||
f"</table></div>")
|
||
_, runs = q("SELECT run_id, stage_kind, revision, create_time FROM example_run_receipt "
|
||
"WHERE adapter_role=%s ORDER BY create_time DESC LIMIT 20", (role,))
|
||
runs_html = ("<div class='card'><div class='hd'>本角色运行(回执,最近 20)</div><table><thead>"
|
||
"<tr><th>run_id</th><th>阶段</th><th class='num'>revision</th><th>时间</th></tr></thead><tbody>"
|
||
+ "".join(f"<tr><td><code>{esc(r[0])}</code></td><td>{esc(r[1])}</td><td class='num'>{esc(r[2])}</td><td>{esc(r[3])}</td></tr>" for r in runs)
|
||
+ "</tbody></table></div>") if runs else empty_gap("本角色暂无运行回执", "创作链落库后,该角色的运行会显示在这里。")
|
||
_, calls = q("SELECT call_time, requested_model_id, out_tokens, cost_usd FROM example_llm_call "
|
||
"WHERE caller=%s ORDER BY call_time DESC LIMIT 20", (role,))
|
||
calls_html = ("<div class='card'><div class='hd'>本角色模型调用(最近 20)</div><table><thead>"
|
||
"<tr><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 class='num'>{fmt_num(c[2])}</td><td class='num'>${float(c[3] or 0):.5f}</td></tr>" for c in calls)
|
||
+ "</tbody></table></div>") if calls else empty_gap("本角色暂无模型调用", "")
|
||
_, quals = q("SELECT q.judge_kind, q.dimension, q.conclusion, q.failure_class FROM example_quality_result q "
|
||
"JOIN example_run_receipt r ON r.id=q.receipt_id WHERE r.adapter_role=%s ORDER BY q.create_time DESC LIMIT 20", (role,))
|
||
quals_html = ("<div class='card'><div class='hd'>本角色质量评判(最近 20)</div><table><thead>"
|
||
"<tr><th>类型</th><th>维度</th><th>结论</th><th>失败类别</th></tr></thead><tbody>"
|
||
+ "".join(f"<tr><td>{esc(u[0])}</td><td>{esc(u[1])}</td><td>{chip(u[2])}</td><td>{esc(u[3])}</td></tr>" for u in quals)
|
||
+ "</tbody></table></div>") if quals else empty_gap("本角色暂无质量评判", "")
|
||
body = (f"<div class='crumb'><a href='/agents'>智能体</a> / {esc(disp or role_)}</div>"
|
||
f"<h1>{esc(disp or role_)}</h1>{info}<h2>运行与产出</h2>{runs_html}{quals_html}{calls_html}")
|
||
return layout(f"角色 · {disp or role_}", body, "/agents")
|
||
|
||
|
||
def view_skill(name):
|
||
_, ss = q("SELECT skill_name, purpose, consumer, reads, writes, red_lines, model_used, source_ref, synced_at "
|
||
"FROM example_skill WHERE skill_name=%s AND deleted=false", (name,))
|
||
if not ss:
|
||
return layout("技能", "<h1>技能不存在</h1>", "/agents")
|
||
sn, purpose, consumer, reads, writes, red_lines, model_used, src, synced = ss[0]
|
||
body = (f"<div class='crumb'><a href='/agents'>智能体</a> / 技能 / {esc(sn)}</div>"
|
||
f"<h1>{esc(sn)}</h1>"
|
||
f"<div class='card'><table>"
|
||
f"<tr><td style='width:120px;color:var(--muted)'>用途</td><td>{esc(purpose)}</td></tr>"
|
||
f"<tr><td style='color:var(--muted)'>给谁用</td><td>{esc(consumer) if consumer else '<span style=\"color:var(--muted)\">待补</span>'}</td></tr>"
|
||
f"<tr><td style='color:var(--muted)'>用模型</td><td>{esc(model_used) if model_used else '确定性工具(不用模型)'}</td></tr>"
|
||
f"<tr><td style='color:var(--muted)'>涉及表</td><td>{_fmt_list(reads)}<div style='color:var(--muted);font-size:12px'>自动抽取,读/写方向待人工核</div></td></tr>"
|
||
f"<tr><td style='color:var(--muted)'>红线</td><td>{esc(red_lines) if red_lines else '<span style=\"color:var(--muted)\">待补</span>'}</td></tr>"
|
||
f"<tr><td style='color:var(--muted)'>出处</td><td><code>{esc(src)}</code> · 同步于 {esc(synced)}</td></tr>"
|
||
f"</table></div>")
|
||
return layout(f"技能 · {sn}", body, "/agents")
|
||
|
||
|
||
# ── 路由 ─────────────────────────────────────────────────────────────────────
|
||
_ROUTES = [
|
||
(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):
|
||
parsed = urlparse(self.path)
|
||
params = parse_qs(parsed.query)
|
||
try:
|
||
for pat, fn in _ROUTES:
|
||
m = pat.match(parsed.path)
|
||
if m:
|
||
self._send(200, fn(m, params))
|
||
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()
|