被多个 Skill 或看板消费的连接、模型、嵌入、Claude 运行时与声音账入口 各只保留一份实现;Skill 只留 CLI/落库,看板只读 muse-db,门禁锁死跨域注入。 Co-authored-by: Cursor <cursoragent@cursor.com>
2265 lines
133 KiB
Python
2265 lines
133 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. 不接会写的通道:不调用 decide-candidate / access-database Skill,只自己只读查库。
|
||
4. 挂了不牵连:进程崩了不影响库内正式内容与创作链。
|
||
视觉系统:dataviz 方法(已验证默认调色板,light/dark 双模式,状态色固定不主题化)。
|
||
跑法:.venv/bin/python dashboard/server.py (默认 http://127.0.0.1:8765)
|
||
"""
|
||
import html
|
||
import json
|
||
import re
|
||
import traceback
|
||
from collections import Counter
|
||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||
from pathlib import Path
|
||
from urllib.parse import parse_qs, urlencode, urlparse
|
||
|
||
from muse_db import WINDOW_BUDGET_USD, WINDOW_CALL_CAP, connect
|
||
|
||
HOST, PORT = "127.0.0.1", 8765
|
||
|
||
# AI 味案例的正式来源是 muse-example;离线 JSON 只作为数据库不可用时的恢复/回退证据。
|
||
AI_FLAVOR_REPORT_DIR = Path(__file__).resolve().parent / "fixtures"
|
||
AI_FLAVOR_REVALIDATION_SCHEMA = "ai-flavor-revalidation-v1"
|
||
AI_FLAVOR_CASE_SCHEMA = "ai-flavor-case-v1"
|
||
AI_FLAVOR_TENANT_ID = 1
|
||
AI_FLAVOR_PAGE_SIZE = 40
|
||
_AI_FLAVOR_PATTERN_LABELS = {
|
||
"lexical.meta_disclaimer": "元话语套式",
|
||
"lexical.stock_micro_expression": "库存微表情",
|
||
"lexical.abstract_atmosphere": "抽象气氛",
|
||
}
|
||
_AI_FLAVOR_LABELS = {
|
||
"unclassified": "未分类",
|
||
"sf": "当前认为应修",
|
||
"snf": "表面相似但不应修",
|
||
"boundary": "需上下文裁决",
|
||
"regression": "修订回归",
|
||
}
|
||
_AI_FLAVOR_REVALIDATION_LABELS = {
|
||
"verified": "来源已验证",
|
||
"stale": "来源已变化",
|
||
"unavailable": "来源不可得",
|
||
"card_mismatch": "位置不匹配",
|
||
"missing": "尚未重验",
|
||
}
|
||
|
||
|
||
def q(sql, params=()):
|
||
"""只读查询,返回 (列名, 行)。每次请求实时查库,不缓存(合同 §7)。
|
||
|
||
readonly=True 是会话级锁死:写语句会被 PostgreSQL 拒绝(机械门,不靠自觉)。
|
||
"""
|
||
with connect(readonly=True) 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,))
|
||
|
||
|
||
def table_counts(names):
|
||
"""一次只读往返返回一组表的活行数,缺表不阻断总览。"""
|
||
requested = list(dict.fromkeys(names))
|
||
if not requested:
|
||
return {}
|
||
_, existing_rows = q(
|
||
"SELECT tablename FROM pg_tables WHERE schemaname='public' AND tablename = ANY(%s)",
|
||
(requested,),
|
||
)
|
||
existing = [row[0] for row in existing_rows]
|
||
if not existing:
|
||
return {}
|
||
_, deleted_rows = q(
|
||
"SELECT table_name FROM information_schema.columns "
|
||
"WHERE table_schema='public' AND column_name='deleted' AND table_name = ANY(%s)",
|
||
(existing,),
|
||
)
|
||
has_deleted = {row[0] for row in deleted_rows}
|
||
parts = []
|
||
params = []
|
||
for name in existing:
|
||
# name 来自 pg_tables 且只取请求白名单,双引号转义仍保留防御性边界。
|
||
identifier = '"' + name.replace('"', '""') + '"'
|
||
where = " WHERE deleted=false" if name in has_deleted else ""
|
||
parts.append(f"SELECT %s::text AS table_name, count(*)::bigint AS row_count FROM {identifier}{where}")
|
||
params.append(name)
|
||
_, rows = q(" UNION ALL ".join(parts), tuple(params))
|
||
return {row[0]: int(row[1]) for row in rows}
|
||
|
||
|
||
# ── 视觉系统: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); }
|
||
.chip code, .field-code { font-size:11px; font-weight:500; color:inherit; opacity:.78; }
|
||
.enum-code { font-size:11px; color:var(--muted); white-space:nowrap; }
|
||
|
||
.knowledge-grid, .planning-grid { display:grid; grid-template-columns:minmax(0,1.1fr) minmax(320px,.9fr);
|
||
gap:14px; align-items:start; margin-top:14px; }
|
||
.knowledge-grid > .card, .planning-grid > .card { margin-top:0; }
|
||
.knowledge-filter { display:flex; flex-wrap:wrap; gap:8px; align-items:center; margin-top:14px; }
|
||
.knowledge-filter input, .knowledge-filter select, .knowledge-filter button { min-height:34px;
|
||
border:1px solid var(--baseline); border-radius:5px; background:var(--surface); color:var(--ink);
|
||
font:inherit; padding:5px 9px; }
|
||
.knowledge-filter input { flex:1 1 220px; min-width:180px; }
|
||
.knowledge-filter button { cursor:pointer; background:var(--ink); color:var(--plane); border-color:var(--ink); }
|
||
.knowledge-filter button:hover { opacity:.86; }
|
||
.knowledge-filter .clear { color:var(--s1); font-size:13px; text-decoration:none; }
|
||
.knowledge-filter .clear:hover { text-decoration:underline; }
|
||
.knowledge-card-list { display:flex; flex-direction:column; }
|
||
.knowledge-card-item { display:block; padding:13px 16px; border-top:1px solid var(--grid); color:var(--ink);
|
||
text-decoration:none; }
|
||
.knowledge-card-item:first-child { border-top:none; }
|
||
.knowledge-card-item:hover, .knowledge-card-item.selected { background:var(--plane); }
|
||
.knowledge-card-item.selected { box-shadow:inset 3px 0 var(--s1); }
|
||
.knowledge-card-item .title-row { display:flex; gap:8px; align-items:baseline; justify-content:space-between; }
|
||
.knowledge-card-item .title { font-weight:650; overflow-wrap:anywhere; }
|
||
.knowledge-card-item .meta, .knowledge-card-item .summary { color:var(--muted); font-size:12px; margin-top:4px; }
|
||
.knowledge-card-item .summary { color:var(--ink2); line-height:1.5; }
|
||
.planning-item { display:block; padding:13px 16px; border-top:1px solid var(--grid); color:var(--ink);
|
||
text-decoration:none; }
|
||
.planning-item:first-child { border-top:none; }
|
||
.planning-item:hover, .planning-item.selected { background:var(--plane); }
|
||
.planning-item.selected { box-shadow:inset 3px 0 var(--s1); }
|
||
.planning-item .title-row { display:flex; gap:8px; align-items:baseline; justify-content:space-between; }
|
||
.planning-item .title { font-weight:650; overflow-wrap:anywhere; }
|
||
.planning-item .meta, .planning-item .summary { color:var(--muted); font-size:12px; margin-top:4px; }
|
||
.planning-item .summary { color:var(--ink2); line-height:1.5; }
|
||
.pager { display:flex; flex-wrap:wrap; align-items:center; gap:6px; padding:12px 16px; border-top:1px solid var(--grid); }
|
||
.pager a, .pager span { min-width:30px; padding:4px 8px; border:1px solid var(--grid); border-radius:4px;
|
||
color:var(--s1); text-align:center; text-decoration:none; font-size:12px; }
|
||
.pager span.current { background:var(--ink); border-color:var(--ink); color:var(--plane); }
|
||
.pager span.total { margin-right:auto; border:none; color:var(--muted); text-align:left; }
|
||
.knowledge-detail { position:sticky; top:18px; }
|
||
.knowledge-detail .detail-head { padding:14px 16px; border-bottom:1px solid var(--grid); }
|
||
.knowledge-detail .detail-head h3 { font-size:18px; line-height:1.35; overflow-wrap:anywhere; }
|
||
.knowledge-detail .detail-head .sub { color:var(--muted); font-size:12px; margin-top:4px; }
|
||
.detail-table { width:100%; border-collapse:collapse; font-size:13px; }
|
||
.detail-table th { width:112px; color:var(--muted); font-weight:600; text-align:left; vertical-align:top; }
|
||
.detail-table th, .detail-table td { padding:9px 16px; border-bottom:1px solid var(--grid); }
|
||
.detail-table tr:last-child th, .detail-table tr:last-child td { border-bottom:none; }
|
||
.detail-text { white-space:pre-wrap; overflow-wrap:anywhere; color:var(--ink2); }
|
||
.detail-list { margin:0; padding-left:18px; color:var(--ink2); }
|
||
.detail-list li + li { margin-top:5px; }
|
||
pre.data-block { margin:10px 16px 16px; padding:12px; max-height:520px; overflow:auto; background:var(--plane);
|
||
border:1px solid var(--grid); border-radius:5px; color:var(--ink2); font:12px/1.55 ui-monospace,SFMono-Regular,Menlo,monospace;
|
||
white-space:pre-wrap; overflow-wrap:anywhere; }
|
||
pre.data-block code { font:inherit; color:inherit; white-space:inherit; }
|
||
details.data-details { border-top:1px solid var(--grid); }
|
||
details.data-details summary { cursor:pointer; padding:10px 16px; color:var(--s1); font-size:13px; }
|
||
.error-panel { margin-top:14px; border:1px solid color-mix(in srgb,var(--crit) 45%,var(--ring));
|
||
border-radius:8px; background:color-mix(in srgb,var(--crit) 7%,var(--surface)); }
|
||
.error-panel .hd { padding:11px 14px; border-bottom:1px solid var(--grid); font-weight:650; color:var(--crit); }
|
||
.error-item { padding:11px 14px; border-bottom:1px solid var(--grid); }
|
||
.error-item:last-child { border-bottom:none; }
|
||
.error-item .label { color:var(--muted); font-size:12px; }
|
||
.error-item .value { margin-top:3px; color:var(--ink2); overflow-wrap:anywhere; }
|
||
.run-evidence details { border-top:1px solid var(--grid); }
|
||
.run-evidence details:first-child { border-top:none; }
|
||
.run-evidence summary { cursor:pointer; padding:11px 16px; color:var(--s1); }
|
||
.run-evidence pre { margin:0 16px 16px; }
|
||
@media (max-width:900px) {
|
||
.knowledge-grid, .planning-grid { grid-template-columns:1fr; }
|
||
.knowledge-detail { position:static; }
|
||
}
|
||
|
||
.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",
|
||
}
|
||
|
||
|
||
_ENUM_LABELS = {
|
||
"status": {
|
||
"pending": "待确认", "draft": "草稿", "checking": "检查中", "parsing": "解析中",
|
||
"running": "运行中", "writing": "写作中", "shadow": "待审",
|
||
"confirmed": "已确认", "passed": "通过", "done": "完成", "parsed": "已解析",
|
||
"completed": "已完成", "accepted": "已采纳", "active": "有效", "canonical": "正式",
|
||
"published": "已发布", "rejected": "已拒绝", "failed": "失败", "revise": "待修订",
|
||
"stale": "已过期", "discarded": "已丢弃", "error": "错误", "unauthorized": "未授权",
|
||
"ignored": "已忽略", "not_built": "未建", "open": "开放", "closed": "已关闭",
|
||
"migrating": "归档中", "migrated": "已归档",
|
||
},
|
||
"section_type": {
|
||
"setting": "设定", "outline": "大纲", "fine_outline": "细纲",
|
||
"state": "叙事状态", "assembly": "装配",
|
||
},
|
||
"draft_type": {"entity": "实体卡", "relation": "关系卡"},
|
||
"entity_type": {
|
||
"character": "人物", "character_relation": "人物关系", "location": "地点",
|
||
"faction": "势力", "item": "物品", "event": "事件", "power_system": "力量体系",
|
||
"scene_pattern": "场景范式", "combat": "战斗范式", "craft": "写法范式",
|
||
"emotion": "情绪范式", "trope": "套路范式", "chapter": "章节",
|
||
"generation_context": "生成上下文", "narrative_state": "叙事状态",
|
||
"novel_work": "作品", "outline": "大纲", "pacing": "节奏",
|
||
"reference_work": "参考书", "scene": "场景", "style": "文风",
|
||
"work_core": "作品核心", "world": "世界观",
|
||
},
|
||
"run_type": {"production": "生产", "eval": "评测", "replay": "回放"},
|
||
"source_role": {
|
||
"writer": "写手", "extractor": "章后抽卡", "planner": "规划器",
|
||
"detector": "语义检测器", "judge": "质量评审器", "continuation": "续写编排",
|
||
},
|
||
"agent_role": {
|
||
"writer": "写手", "extractor": "章后抽卡", "planner": "规划器",
|
||
"detector": "语义检测器", "judge": "质量评审器",
|
||
},
|
||
"stage_kind": {
|
||
"generation": "生成", "detection": "检测", "supplement": "补证",
|
||
"rewrite": "改写", "accept": "接受",
|
||
},
|
||
"judge_kind": {
|
||
"detection": "检测", "scoring": "评分", "review": "审核", "experiment": "实验",
|
||
},
|
||
"decision": {"accept": "采纳", "merge": "合并", "discard": "丢弃"},
|
||
"conclusion": {"pass": "通过", "fail": "失败", "revise": "需修订", "pending": "待评判"},
|
||
"source_type": {
|
||
"chapter_extract": "章后抽取", "parse_book": "拆书解析", "upgrade_book": "作品升格",
|
||
"ai_candidate": "模型候选", "manual": "人工录入",
|
||
},
|
||
"trigger_source": {
|
||
"user": "用户触发", "system": "系统触发", "api": "接口触发", "scheduled": "定时触发",
|
||
},
|
||
"purpose": {
|
||
"llm_call": "模型调用", "planning_persistence": "规划落库",
|
||
"production_generation": "正文生产", "evaluation": "评测",
|
||
},
|
||
"skill_name": {
|
||
"access-database": "访问数据库", "assemble-context": "组装上下文",
|
||
"call-content-model": "调用内容模型", "check-content-consistency": "检查内容一致性",
|
||
"clean-book-text": "清洗书稿正文", "decide-candidate": "处理候选决策",
|
||
"deconstruct-book": "拆解完整作品", "design-story-foundation": "设计作品前期",
|
||
"embed-knowledge": "嵌入知识", "evaluate-frozen-replay": "评估冻结回放",
|
||
"execute-claude-task": "执行 Claude 任务", "expand-scene": "扩写场景",
|
||
"extract-chapter-knowledge": "抽取章节知识", "extract-work-knowledge": "抽取作品知识",
|
||
"freeze-context": "冻结上下文", "import-book": "导入书稿",
|
||
"maintain-work-extraction": "维护作品抽取", "optimize-content-quality": "优化内容质量",
|
||
"plan-chapter": "规划章节", "plan-story": "规划作品", "polish-prose": "润色正文",
|
||
"record-run-evidence": "记录运行证据", "review-knowledge-cards": "审核知识卡",
|
||
"rewrite-selection": "改写指定范围", "score-content-quality": "评估内容质量",
|
||
"search-knowledge": "检索知识", "write-next-chapter": "续写下一章",
|
||
},
|
||
}
|
||
|
||
_ENUM_ROLES = {
|
||
"decision": {"accept": "good", "merge": "warn", "discard": "crit"},
|
||
"conclusion": {"pass": "good", "fail": "serious", "revise": "warn", "pending": "warn"},
|
||
"raw_status": {"closed": "good", "migrated": "good", "open": "warn", "migrating": "warn"},
|
||
}
|
||
|
||
_REASON_LABELS = {
|
||
"completed": "已完成", "failed": "失败", "stop": "异常停止", "tool_use": "工具调用结束",
|
||
"process_exit_without_terminal_report": "进程退出且未上报终态",
|
||
"InvalidTextRepresentation": "文本格式错误", "ExtractionContractError": "抽取合同错误",
|
||
}
|
||
|
||
_TEXT_TERM_LABELS = {
|
||
"writer sandbox": "写手隔离环境", "semantic detector": "语义检测器", "Claude CLI": "模型命令行客户端",
|
||
"writer": "写手", "detector": "检测器", "asOf": "截至章",
|
||
"LLM": "大模型", "New-API": "模型接口", "MiniMax": "模型服务", "Claude": "模型", "CLI": "命令行客户端", "scenario": "场景",
|
||
"continuation": "续写", "validation": "校验", "consistency_check": "一致性检查",
|
||
"fine_outline_replay": "细纲回放", "full_parse": "全书解析", "quality_gate": "质量门",
|
||
"writer_replay": "写手回放", "fieldAiContext": "字段上下文策略", "pgvector": "向量检索库",
|
||
"embed": "向量化", "dimensions": "维度数", "judge": "评审", "prompt": "提示词",
|
||
"expansion": "扩写", "extraction": "抽取", "planning": "规划", "polish": "润色",
|
||
"rewrite": "改写", "fine_outline": "细纲", "review": "审核", "as_of": "截至章",
|
||
"fresh": "全新", "sandbox": "隔离环境", "deadline": "截止时间",
|
||
"structured_output": "结构化输出", "aiContext": "智能上下文", "fail-closed": "失败即关闭", "build_snapshot": "构建快照",
|
||
"check_snapshot": "校验快照", "audit_leakage": "泄漏审计", "upgrade_book": "作品升格",
|
||
"revision journal": "修订日志", "raw vault": "原始证据仓", "muse-example": "实验库", "advisory lock": "咨询锁", "git commit": "提交操作",
|
||
"file_cas": "文件内容寻址存储", "CAS": "内容寻址存储", "journal": "日志", "revision": "修订号",
|
||
"raw": "原始", "vault": "存储仓", "skill": "技能", "block": "正文块", "replay-eval": "回放评测", "quality-gate": "质量门",
|
||
"detect": "语义检测", "restore": "恢复", "SQL": "数据库查询语言",
|
||
"DML": "数据操作语言", "DDL": "数据定义语言", "txt": "文本", "Git": "版本库",
|
||
"PostgreSQL": "数据库", "scripts/": "脚本目录",
|
||
"MiniMax-M3": "模型", "Qwen3-Embedding-8B": "嵌入模型",
|
||
}
|
||
_TEXT_TERM_PATTERN = re.compile(
|
||
r"(?<![A-Za-z0-9_])(?:"
|
||
+ "|".join(re.escape(term) for term in sorted(_TEXT_TERM_LABELS, key=len, reverse=True))
|
||
+ r")(?![A-Za-z0-9_])"
|
||
)
|
||
|
||
|
||
def _is_human_text(value):
|
||
return isinstance(value, str) and any(ord(char) > 127 for char in value)
|
||
|
||
|
||
def enum_text(value, group="status"):
|
||
"""枚举展示:中文语义优先,英文仅作为可复制的字符串编码。"""
|
||
if value is None:
|
||
return esc(value)
|
||
raw = str(value)
|
||
if _is_human_text(raw):
|
||
return html.escape(raw)
|
||
label = _ENUM_LABELS.get(group, {}).get(raw)
|
||
if label is None:
|
||
label = "未登记"
|
||
return f"{html.escape(label)} <span class='enum-code'><code>{html.escape(raw)}</code></span>"
|
||
|
||
|
||
def field_label(label, code):
|
||
"""字段标题使用中文,数据库/协议字段名只作为编码保留。"""
|
||
return f"{html.escape(label)} <code class='field-code'>{html.escape(code)}</code>"
|
||
|
||
|
||
def annotated_text(value):
|
||
"""自由文本中的受控技术术语显示中文语义,原词保留为编码。"""
|
||
if value is None:
|
||
return esc(value)
|
||
escaped = html.escape(str(value))
|
||
return _TEXT_TERM_PATTERN.sub(
|
||
lambda match: f"{html.escape(_TEXT_TERM_LABELS[match.group(0)])} "
|
||
f"<span class='enum-code'><code>{match.group(0)}</code></span>",
|
||
escaped,
|
||
)
|
||
|
||
|
||
def short_text(value, limit=56):
|
||
"""截断说明时不切断英文编码,避免出现 ``fine_outl`` 这类伪字段。"""
|
||
text = str(value or "")
|
||
if len(text) <= limit:
|
||
return text
|
||
cut = text[:limit]
|
||
while cut and len(cut) < len(text):
|
||
next_char = text[len(cut)]
|
||
last_char = cut[-1]
|
||
if not (last_char.isalnum() or last_char in "_-") or not (next_char.isalnum() or next_char in "_-"):
|
||
break
|
||
cut = cut[:-1]
|
||
return cut.rstrip() + "…"
|
||
|
||
|
||
def _json_text(value):
|
||
"""JSONB 详情使用可读缩进,标量和空值也保持稳定文本。"""
|
||
if value is None:
|
||
return ""
|
||
if isinstance(value, str):
|
||
return value
|
||
try:
|
||
return json.dumps(value, ensure_ascii=False, indent=2, default=str)
|
||
except (TypeError, ValueError):
|
||
return str(value)
|
||
|
||
|
||
def _data_block(value):
|
||
text = _json_text(value)
|
||
return f"<pre class='data-block'><code>{esc(text)}</code></pre>"
|
||
|
||
|
||
def _content_text(value):
|
||
"""原始 prompt/response 若是 JSON,格式化后再展示;普通文本原样保留。"""
|
||
if value is None:
|
||
return ""
|
||
text = str(value)
|
||
try:
|
||
return json.dumps(json.loads(text), ensure_ascii=False, indent=2)
|
||
except (TypeError, ValueError, json.JSONDecodeError):
|
||
return text
|
||
|
||
|
||
_RAW_KIND_LABELS = {
|
||
"prompt": "模型输入", "response": "模型输出", "stderr": "错误输出", "error": "错误信息",
|
||
}
|
||
|
||
|
||
def _raw_kind_label(value):
|
||
raw = "" if value is None else str(value)
|
||
return f"{html.escape(_RAW_KIND_LABELS.get(raw, '原始证据'))} <span class='enum-code'><code>{html.escape(raw)}</code></span>"
|
||
|
||
|
||
def _reason_text(value):
|
||
if value is None or value == "":
|
||
return "—"
|
||
raw = str(value)
|
||
label = _REASON_LABELS.get(raw, "未登记原因")
|
||
return f"{html.escape(label)} <span class='enum-code'><code>{html.escape(raw)}</code></span>"
|
||
|
||
|
||
def _row_dicts(cols, rows):
|
||
return [dict(zip(cols, row)) for row in rows]
|
||
|
||
|
||
def _query_href(path, current, **updates):
|
||
"""保留当前筛选条件,生成可复制的 GET 下钻地址。"""
|
||
values = {
|
||
key: vals[0]
|
||
for key, vals in (current or {}).items()
|
||
if vals and vals[0] not in (None, "")
|
||
}
|
||
for key, value in updates.items():
|
||
if value in (None, ""):
|
||
values.pop(key, None)
|
||
else:
|
||
values[key] = value
|
||
query = urlencode(values)
|
||
return f"{path}?{query}" if query else path
|
||
|
||
|
||
def _chapter_text(chapter_id, order_no, title, *, link_work=None):
|
||
"""章节主键只作为编码显示,用户看到章节序号和标题。"""
|
||
if order_no is None:
|
||
label = "未关联章节"
|
||
else:
|
||
label = f"第{order_no}章{f'《{title}》' if title else ''}"
|
||
if link_work is not None and order_no is not None:
|
||
label = f"<a href='/works/{link_work}/ch/{order_no}'>{html.escape(label)}</a>"
|
||
else:
|
||
label = html.escape(label)
|
||
if chapter_id is not None:
|
||
label += f" <code>chapter_id={html.escape(str(chapter_id))}</code>"
|
||
return label
|
||
|
||
|
||
def _mapping_html(mapping):
|
||
if not isinstance(mapping, dict) or not mapping:
|
||
return "<span class='muted'>暂无</span>"
|
||
return ("<table class='detail-table'>"
|
||
+ "".join(f"<tr><th>{esc(key)}</th><td class='detail-text'>{esc(_json_text(value))}</td></tr>"
|
||
for key, value in mapping.items())
|
||
+ "</table>")
|
||
|
||
|
||
def display_trigger(value):
|
||
raw = "" if value is None else str(value)
|
||
if raw in _ENUM_LABELS["trigger_source"]:
|
||
return enum_text(raw, "trigger_source")
|
||
return annotated_text(value)
|
||
|
||
|
||
def _operator_text(value):
|
||
"""运行调用方可能是角色,也可能是技能;两者都先显示中文语义。"""
|
||
raw = "" if value is None else str(value)
|
||
if raw in _ENUM_LABELS.get("source_role", {}):
|
||
return enum_text(raw, "source_role")
|
||
return enum_text(raw, "skill_name")
|
||
|
||
|
||
def enum_chip(value, group="status"):
|
||
raw = "" if value is None else str(value)
|
||
visual = _ENUM_ROLES.get(group, {}).get(raw, raw)
|
||
label = _ENUM_LABELS.get(group, {}).get(raw)
|
||
if label is None:
|
||
label = raw if _is_human_text(raw) else "未登记"
|
||
return chip(visual, label, code=raw)
|
||
|
||
|
||
def chip(state, label=None, *, code=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]
|
||
raw = code if code is not None else state
|
||
if label is None:
|
||
label = _ENUM_LABELS.get("status", {}).get(str(raw), "未登记")
|
||
suffix = ""
|
||
if code is not None or (state and str(state).lower() not in _STATUS):
|
||
suffix = f" <code>{html.escape(str(raw))}</code>"
|
||
return (f'<span class="chip {r}"><span class="dot"></span>{icon} '
|
||
f'{html.escape(str(label))}{suffix}</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"),
|
||
("/ai-flavor", "AI 味案例", "质量 06 · 数据库证据"),
|
||
("/agents", "智能体", "智能体 07"),
|
||
("/runs", "运行记录", "流程 05 · 质量 06"),
|
||
]
|
||
|
||
|
||
def layout(title, body, active="/", footer_note=None):
|
||
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>')
|
||
footer_note = footer_note or "数据实时查库,无缓存 · 数字为当前库内事实 · 空白即落库缺口"
|
||
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)} · 创作看板</title><style>{CSS}</style></head><body>
|
||
<nav><div class="brand">创作看板<small><code>muse</code> · <code>muse-example</code> · 只读 · 按领域看库</small></div>{"".join(links)}
|
||
<div class="ro">只读连接<br>本看板不写任何数据<br>接受 / 丢弃仍走确认通道</div></nav>
|
||
<main>{body}
|
||
<footer>{footer_note}</footer>
|
||
</main></body></html>"""
|
||
|
||
|
||
def _latest_ai_flavor_report():
|
||
"""数据库不可用时取仓库内最新的回填报告;不扫描原文。"""
|
||
reports = sorted(AI_FLAVOR_REPORT_DIR.glob("backfill-inventory-*.json"))
|
||
return reports[-1] if reports else None
|
||
|
||
|
||
def _load_ai_flavor_report():
|
||
path = _latest_ai_flavor_report()
|
||
if path is None:
|
||
return None, "尚未生成离线回填报告"
|
||
try:
|
||
report = json.loads(path.read_text(encoding="utf-8"))
|
||
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
|
||
return None, f"回填报告读取失败:{exc}"
|
||
if not isinstance(report, dict) or report.get("schema_version") != "ai-flavor-inventory-v1":
|
||
return None, "回填报告版本不受支持"
|
||
if not isinstance(report.get("cards"), list) or not isinstance(report.get("works"), list):
|
||
return None, "回填报告缺少 works/cards 数组"
|
||
return report, None
|
||
|
||
|
||
def _latest_ai_flavor_revalidation_report():
|
||
"""数据库不可用时取仓内重验证回执;页面不因浏览而读取原文。"""
|
||
reports = sorted(AI_FLAVOR_REPORT_DIR.glob("revalidation-*.json"))
|
||
return reports[-1] if reports else None
|
||
|
||
|
||
def _load_ai_flavor_revalidation_report():
|
||
path = _latest_ai_flavor_revalidation_report()
|
||
if path is None:
|
||
return None, "尚未生成来源重验证回执"
|
||
try:
|
||
report = json.loads(path.read_text(encoding="utf-8"))
|
||
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
|
||
return None, f"重验证回执读取失败:{exc}"
|
||
if not isinstance(report, dict) or report.get("schema_version") != AI_FLAVOR_REVALIDATION_SCHEMA:
|
||
return None, "重验证回执版本不受支持"
|
||
if not isinstance(report.get("cards"), list) or not isinstance(report.get("works"), list):
|
||
return None, "重验证回执缺少 works/cards 数组"
|
||
return report, None
|
||
|
||
|
||
def _ai_db_available():
|
||
"""只检查正式三表是否存在;查询失败由调用方转为离线回退。"""
|
||
return all(table_exists(name) for name in (
|
||
"example_ai_flavor_case",
|
||
"example_ai_flavor_revalidation_batch",
|
||
"example_ai_flavor_revalidation",
|
||
))
|
||
|
||
|
||
def _ai_db_bundle():
|
||
"""从 muse-example 组装看板所需的 inventory + 最新重验证回执视图。"""
|
||
if not _ai_db_available():
|
||
return None, None, "AI 味案例正式表尚未建立"
|
||
_, card_rows = q(
|
||
"SELECT card_id,schema_version,card_type,state,label,layer,carrier,capture_mode,"
|
||
"source_kind,source_license,source_sha256,excerpt_sha256,source_ref,work_ref,"
|
||
"source_location,surface_location,excerpt,context,observation,feedback,review,rule_candidate_ids "
|
||
"FROM example_ai_flavor_case WHERE tenant_id=%s AND deleted=false "
|
||
"ORDER BY work_ref, (source_location->>'char_start')::bigint, card_id",
|
||
(AI_FLAVOR_TENANT_ID,),
|
||
)
|
||
cards = []
|
||
for row in card_rows:
|
||
(card_id, schema_version, card_type, state, label, layer, carrier, capture_mode,
|
||
source_kind, source_license, source_sha256, excerpt_sha256, source_ref, work_ref,
|
||
source_location, surface_location, excerpt, context, observation, feedback, review,
|
||
rule_candidate_ids) = row
|
||
cards.append({
|
||
"schema_version": schema_version,
|
||
"id": card_id,
|
||
"card_type": card_type,
|
||
"state": state,
|
||
"label": label,
|
||
"layer": layer,
|
||
"carrier": carrier,
|
||
"capture_mode": capture_mode,
|
||
"excerpt": excerpt or "",
|
||
"context": context or "",
|
||
"source": {
|
||
"kind": source_kind, "license": source_license,
|
||
"source_sha256": source_sha256, "excerpt_sha256": excerpt_sha256,
|
||
"source_ref": source_ref or "", "work_ref": work_ref,
|
||
"location": source_location or {}, "surface_location": surface_location or {},
|
||
},
|
||
"observation": observation or {},
|
||
**({"feedback": feedback} if feedback is not None else {}),
|
||
**({"review": review} if review is not None else {}),
|
||
"rule_candidate_ids": rule_candidate_ids or [],
|
||
})
|
||
_, batch_rows = q(
|
||
"SELECT id,batch_ref,report_sha256,schema_version,checked_on,source_root_ref,usable,totals,works,report_path "
|
||
"FROM example_ai_flavor_revalidation_batch WHERE tenant_id=%s "
|
||
"ORDER BY checked_on DESC,id DESC LIMIT 1",
|
||
(AI_FLAVOR_TENANT_ID,),
|
||
)
|
||
if not batch_rows:
|
||
work_map = {}
|
||
for card in cards:
|
||
source = card["source"]
|
||
key = (source.get("work_ref", ""), source.get("source_ref", ""))
|
||
work = work_map.setdefault(key, {
|
||
"work_ref": key[0], "source_ref": key[1],
|
||
"source_sha256": source.get("source_sha256"), "card_count": 0,
|
||
"pattern_counts": {},
|
||
})
|
||
work["card_count"] += 1
|
||
pattern = card.get("observation", {}).get("pattern_key")
|
||
work["pattern_counts"][pattern] = work["pattern_counts"].get(pattern, 0) + 1
|
||
return {
|
||
"schema_version": "ai-flavor-inventory-v1", "generated_on": None,
|
||
"source_root_ref": None, "source_license": "unknown", "totals": {},
|
||
"works": list(work_map.values()), "cards": cards, "_authority": "postgresql",
|
||
}, None, None
|
||
(batch_id, batch_ref, report_sha256, revalidation_schema, checked_on, source_root_ref,
|
||
usable, batch_totals, batch_works, report_path) = batch_rows[0]
|
||
_, receipt_rows = q(
|
||
"SELECT card_id,work_ref,source_ref,expected_source_sha256,actual_source_sha256,status,reason,checked_on "
|
||
"FROM example_ai_flavor_revalidation WHERE tenant_id=%s AND batch_id=%s ORDER BY card_id",
|
||
(AI_FLAVOR_TENANT_ID, batch_id),
|
||
)
|
||
receipts = [{
|
||
"card_id": row[0], "work_ref": row[1], "source_ref": row[2],
|
||
"expected_source_sha256": row[3], "actual_source_sha256": row[4],
|
||
"status": row[5], "reason": row[6], "checked_on": row[7].isoformat() if row[7] else None,
|
||
} for row in receipt_rows]
|
||
by_id = {item["card_id"]: item for item in receipts}
|
||
for card in cards:
|
||
card["_revalidation"] = by_id.get(card["id"], {"status": "missing"})
|
||
work_map = {}
|
||
pattern_counts = Counter()
|
||
for card in cards:
|
||
source = card["source"]
|
||
key = (source.get("work_ref", ""), source.get("source_ref", ""))
|
||
work = work_map.setdefault(key, {
|
||
"work_ref": key[0], "source_ref": key[1],
|
||
"source_sha256": source.get("source_sha256"), "card_count": 0,
|
||
"pattern_counts": {},
|
||
})
|
||
work["card_count"] += 1
|
||
pattern = card.get("observation", {}).get("pattern_key")
|
||
work["pattern_counts"][pattern] = work["pattern_counts"].get(pattern, 0) + 1
|
||
pattern_counts.update([pattern])
|
||
aggregate_totals = {
|
||
"books": len(work_map), "cards": len(cards),
|
||
"pattern_counts": dict(sorted(pattern_counts.items())),
|
||
}
|
||
report = {
|
||
"schema_version": "ai-flavor-inventory-v1",
|
||
"generated_on": checked_on.isoformat() if checked_on else None,
|
||
"source_root_ref": source_root_ref,
|
||
"source_license": next((card["source"]["license"] for card in cards), "unknown"),
|
||
"totals": {**aggregate_totals, **(batch_totals or {})},
|
||
"works": list(work_map.values()), "cards": cards,
|
||
"_authority": "postgresql", "_batch_id": batch_id,
|
||
"_batch_ref": batch_ref, "_report_sha256": report_sha256,
|
||
"_report_path": report_path, "_usable": usable,
|
||
}
|
||
return report, {"schema_version": revalidation_schema, "checked_on": report["generated_on"],
|
||
"usable": usable, "totals": batch_totals or {}, "works": list(work_map.values()),
|
||
"cards": receipts, "_authority": "postgresql", "_batch_id": batch_id,
|
||
"_report_sha256": report_sha256, "_report_path": report_path}, None
|
||
|
||
|
||
def _load_ai_flavor_bundle():
|
||
"""数据库优先;只有数据库表/连接不可用才退回仓内证据文件。"""
|
||
try:
|
||
report, revalidation, error = _ai_db_bundle()
|
||
if error is None and report is not None:
|
||
return report, revalidation, None, "postgresql"
|
||
except Exception as exc: # 看板不能阻断创作链,回退原因要显式显示
|
||
error = f"数据库读取失败:{exc}"
|
||
report, report_error = _load_ai_flavor_report()
|
||
revalidation, revalidation_error = _load_ai_flavor_revalidation_report()
|
||
if report_error:
|
||
return None, None, error or report_error, "offline"
|
||
return report, revalidation, error or revalidation_error, "offline"
|
||
|
||
|
||
def _ai_first(params, key, default=""):
|
||
values = (params or {}).get(key, [])
|
||
return values[0] if values else default
|
||
|
||
|
||
def _ai_pattern_label(key):
|
||
return _AI_FLAVOR_PATTERN_LABELS.get(key, "未登记模式")
|
||
|
||
|
||
def _ai_revalidation_label(status):
|
||
return _AI_FLAVOR_REVALIDATION_LABELS.get(status, "未登记复核状态")
|
||
|
||
|
||
def _ai_revalidation_chip(status):
|
||
status = status or "missing"
|
||
role = {"verified": "good", "missing": "warn"}.get(status, "serious")
|
||
icon = {"verified": "✓", "missing": "●"}.get(status, "▲")
|
||
return (f"<span class='chip {role}'><span class='dot'>{icon}</span>"
|
||
f"{esc(_ai_revalidation_label(status))} <code>{esc(status)}</code></span>")
|
||
|
||
|
||
def _ai_location(card):
|
||
location = card.get("source", {}).get("surface_location") or card.get("source", {}).get("location") or {}
|
||
line_start = location.get("line_start", "?")
|
||
line_end = location.get("line_end", line_start)
|
||
char_start = location.get("char_start", "?")
|
||
char_end = location.get("char_end", char_start)
|
||
line = f"第 {line_start} 行" if line_start == line_end else f"第 {line_start}-{line_end} 行"
|
||
return f"{line} · 字符 {char_start}-{char_end}"
|
||
|
||
|
||
def _ai_pager(path, params, page, pages):
|
||
links = [f"<span class='total'>共 {fmt_num(pages)} 页</span>"]
|
||
if page > 1:
|
||
links.append(f"<a href='{esc(_query_href(path, params, page=page - 1, card=None))}'>上一页</a>")
|
||
window = range(max(1, page - 2), min(pages, page + 2) + 1)
|
||
for number in window:
|
||
if number == page:
|
||
links.append(f"<span class='current'>{number}</span>")
|
||
else:
|
||
links.append(f"<a href='{esc(_query_href(path, params, page=number, card=None))}'>{number}</a>")
|
||
if page < pages:
|
||
links.append(f"<a href='{esc(_query_href(path, params, page=page + 1, card=None))}'>下一页</a>")
|
||
return "<div class='pager'>" + "".join(links) + "</div>"
|
||
|
||
|
||
def _ai_card_detail(card):
|
||
source = card.get("source", {})
|
||
observation = card.get("observation", {})
|
||
verification = card.get("_revalidation") or {}
|
||
checks = observation.get("function_check") or []
|
||
check_html = "".join(f"<li>{esc(item)}</li>" for item in checks)
|
||
label = _AI_FLAVOR_LABELS.get(card.get("label"), "未登记标签")
|
||
state = card.get("state")
|
||
verification_status = verification.get("status", "missing")
|
||
if state == "canonical" and verification_status == "verified":
|
||
usability = "可进入样例投影或规则候选评审;仍不能直接成为 active 规则"
|
||
elif state in {"rejected", "archived"}:
|
||
usability = "仅保留审计,不参与当前诊断、样例或规则消费"
|
||
elif verification_status in {"stale", "unavailable", "card_mismatch", "missing"}:
|
||
usability = "当前失效:需重新取得来源并通过重验证;历史卡和回执继续保留"
|
||
else:
|
||
usability = "仅供本卡/本作品人工复核与候选提示;不可注入生成上下文或作为 blocking 规则"
|
||
scope = f"{source.get('work_ref') or '未绑定作品'} · {source.get('source_ref') or '未登记来源'} · {card.get('capture_mode') or '未知入口'}"
|
||
return (
|
||
"<div class='card knowledge-detail' id='ai-flavor-detail'>"
|
||
"<div class='detail-head'><h3>案例卡详情</h3>"
|
||
f"<div class='sub'><code>{esc(card.get('id'))}</code> · 数据库当前投影 · 规则仍需评审</div></div>"
|
||
"<table class='detail-table'>"
|
||
f"<tr><th>状态</th><td>{chip(card.get('state'), 'Shadow · 待复核', code=card.get('state'))}</td></tr>"
|
||
f"<tr><th>来源重验证</th><td>{_ai_revalidation_chip(verification.get('status'))}<br><small>{esc(verification.get('reason', '页面未执行重验证'))}</small></td></tr>"
|
||
f"<tr><th>人工标签</th><td>{esc(label)} <span class='enum-code'><code>{esc(card.get('label'))}</code></span></td></tr>"
|
||
f"<tr><th>当前用途</th><td>{esc(usability)}</td></tr>"
|
||
f"<tr><th>作用域</th><td><code>{esc(scope)}</code></td></tr>"
|
||
f"<tr><th>模式</th><td>{esc(_ai_pattern_label(observation.get('pattern_key')))} <span class='enum-code'><code>{esc(observation.get('pattern_key'))}</code></span></td></tr>"
|
||
f"<tr><th>作品</th><td><code>{esc(source.get('work_ref'))}</code></td></tr>"
|
||
f"<tr><th>来源位置</th><td><code>{esc(source.get('source_ref'))}</code><br>{esc(_ai_location(card))}</td></tr>"
|
||
f"<tr><th>表面命中</th><td><code>{esc(observation.get('surface'))}</code></td></tr>"
|
||
f"<tr><th>来源许可</th><td>{esc(source.get('license'))} · {'原文片段未保存' if not card.get('excerpt') else '已按授权保存片段'}</td></tr>"
|
||
f"<tr><th>全文 SHA-256</th><td><code>{esc(source.get('source_sha256'))}</code></td></tr>"
|
||
f"<tr><th>命中 SHA-256</th><td><code>{esc(source.get('excerpt_sha256'))}</code></td></tr>"
|
||
f"<tr><th>诊断</th><td>{esc(observation.get('diagnosis'))}</td></tr>"
|
||
f"<tr><th>改动风险</th><td>{esc(observation.get('risk_if_changed'))}</td></tr>"
|
||
f"<tr><th>建议动作</th><td>{esc(observation.get('suggested_action'))}</td></tr>"
|
||
"</table>"
|
||
f"<div style='padding:10px 16px 14px;color:var(--muted);font-size:12px'>功能复核项</div>"
|
||
f"<ul class='detail-list' style='padding-bottom:14px'>{check_html}</ul>"
|
||
"</div>"
|
||
)
|
||
|
||
|
||
def view_ai_flavor(params=None):
|
||
"""展示数据库正式案例卡;数据库不可用时明确标注离线回退。"""
|
||
report, revalidation, load_note, authority = _load_ai_flavor_bundle()
|
||
if report is None:
|
||
body = ("<h1>AI 味案例<span class='sub'>已有作品反向回填 · 数据库案例卡</span></h1>"
|
||
+ empty_gap("暂无可展示案例", load_note or "数据库和离线恢复证据均不可用"))
|
||
return layout("AI 味案例", body, "/ai-flavor",
|
||
"质量证据页 · 尚未形成可读数据 · 看板只读")
|
||
|
||
revalidation_by_id = {
|
||
item.get("card_id"): item for item in (revalidation or {}).get("cards", [])
|
||
if isinstance(item, dict) and item.get("card_id")
|
||
}
|
||
# 只给渲染副本附加回执,不改写磁盘报告;页面本身不读取原文。
|
||
cards = []
|
||
for card in report["cards"]:
|
||
rendered = dict(card)
|
||
rendered["_revalidation"] = rendered.get("_revalidation") or revalidation_by_id.get(card.get("id"), {"status": "missing"})
|
||
cards.append(rendered)
|
||
works = report["works"]
|
||
totals = report.get("totals") or {}
|
||
selected_work = _ai_first(params, "work")
|
||
selected_pattern = _ai_first(params, "pattern")
|
||
selected_card_id = _ai_first(params, "card")
|
||
try:
|
||
page = max(1, int(_ai_first(params, "page", "1")))
|
||
except ValueError:
|
||
page = 1
|
||
|
||
filtered = [card for card in cards
|
||
if (not selected_work or card.get("source", {}).get("work_ref") == selected_work)
|
||
and (not selected_pattern or card.get("observation", {}).get("pattern_key") == selected_pattern)]
|
||
filtered.sort(key=lambda card: (
|
||
card.get("source", {}).get("work_ref", ""),
|
||
card.get("source", {}).get("location", {}).get("char_start", 0),
|
||
card.get("id", ""),
|
||
))
|
||
pages = max(1, (len(filtered) + AI_FLAVOR_PAGE_SIZE - 1) // AI_FLAVOR_PAGE_SIZE)
|
||
page = min(page, pages)
|
||
visible = filtered[(page - 1) * AI_FLAVOR_PAGE_SIZE:page * AI_FLAVOR_PAGE_SIZE]
|
||
selected = next((card for card in cards if card.get("id") == selected_card_id), None)
|
||
|
||
pattern_counts = Counter(card.get("observation", {}).get("pattern_key") for card in filtered)
|
||
state_counts = Counter(card.get("state") for card in cards)
|
||
verification_counts = Counter(card.get("_revalidation", {}).get("status", "missing") for card in cards)
|
||
source_license = report.get("source_license", "unknown")
|
||
generated_on = report.get("generated_on", "unknown")
|
||
report_rel = report.get("_report_path") or report.get("_batch_ref") or "数据库当前投影"
|
||
# 批次历史可能来自旧运行的本机临时路径;页面只暴露可迁移的逻辑引用。
|
||
if authority == "postgresql" and report_rel:
|
||
report_rel = f"capture://ai-flavor/{Path(str(report_rel)).name}"
|
||
if authority == "offline":
|
||
try:
|
||
report_rel = str(_latest_ai_flavor_report().relative_to(Path(__file__).resolve().parents[1]))
|
||
except (AttributeError, ValueError):
|
||
report_rel = "仓内离线回退文件"
|
||
|
||
tiles = ("<div class='tiles'>"
|
||
f"<div class='tile'><div class='v'>{fmt_num(totals.get('books', len(works)))}</div><div class='l'>作品</div></div>"
|
||
f"<div class='tile'><div class='v'>{fmt_num(totals.get('cards', len(cards)))}</div><div class='l'>候选卡</div><div class='d'>当前筛选 {fmt_num(len(filtered))}</div></div>"
|
||
f"<div class='tile'><div class='v'>{fmt_num(state_counts.get('shadow', 0))}</div><div class='l'>Shadow · 待复核</div></div>"
|
||
f"<div class='tile'><div class='v'>{fmt_num(verification_counts.get('verified', 0))}</div><div class='l'>来源已验证</div><div class='d'>非已验证 {fmt_num(len(cards) - verification_counts.get('verified', 0))}</div></div>"
|
||
f"<div class='tile'><div class='v'>{esc(source_license)}</div><div class='l'>来源许可</div></div>"
|
||
"</div>")
|
||
|
||
work_rows = []
|
||
for work in works:
|
||
work_ref = work.get("work_ref", "")
|
||
active = "" if selected_work == work_ref else ""
|
||
href = _query_href("/ai-flavor", params, work=work_ref, pattern=None, page=1, card=None)
|
||
work_rows.append(
|
||
f"<tr><td><a href='{esc(href)}'>{esc(work.get('source_ref'))}</a>"
|
||
f"<div class='enum-code'><code>{esc(work_ref)}</code></div></td>"
|
||
f"<td class='num'>{fmt_num(work.get('card_count', 0))}</td>"
|
||
f"<td>{', '.join(f'{esc(_ai_pattern_label(k))} {fmt_num(v)}' for k, v in (work.get('pattern_counts') or {}).items())}</td>"
|
||
f"<td><code>{esc(work.get('source_sha256'))}</code></td></tr>"
|
||
)
|
||
work_filter = (f"<a class='clear' href='/ai-flavor'>清除作品筛选</a>" if selected_work else "")
|
||
work_table = (f"<div class='card'><div class='hd'>按作品汇总({fmt_num(len(works))} 本)</div>"
|
||
"<table><thead><tr><th>来源文件 / work_ref</th><th class='num'>命中卡</th><th>模式分布</th><th>全文 SHA-256</th></tr></thead>"
|
||
f"<tbody>{''.join(work_rows)}</tbody></table>{work_filter}</div>")
|
||
|
||
pattern_rows = []
|
||
for key, count in sorted(pattern_counts.items(), key=lambda item: (-item[1], item[0])):
|
||
href = _query_href("/ai-flavor", params, pattern=key, page=1, card=None)
|
||
pattern_rows.append(f"<tr><td><a href='{esc(href)}'>{esc(_ai_pattern_label(key))}</a> <span class='enum-code'><code>{esc(key)}</code></span></td><td class='num'>{fmt_num(count)}</td></tr>")
|
||
pattern_table = ("<div class='card'><div class='hd'>当前筛选的模式分布</div>"
|
||
"<table><thead><tr><th>模式</th><th class='num'>命中卡</th></tr></thead>"
|
||
f"<tbody>{''.join(pattern_rows) or '<tr><td colspan=2>暂无</td></tr>'}</tbody></table></div>")
|
||
|
||
card_rows = []
|
||
for card in visible:
|
||
source = card.get("source", {})
|
||
observation = card.get("observation", {})
|
||
href = _query_href("/ai-flavor", params, card=card.get("id"))
|
||
card_rows.append(
|
||
f"<tr><td><a href='{esc(href)}'><code>{esc(card.get('id'))}</code></a></td>"
|
||
f"<td>{esc(source.get('work_ref'))}</td>"
|
||
f"<td>{esc(_ai_pattern_label(observation.get('pattern_key')))}<div class='enum-code'><code>{esc(observation.get('pattern_key'))}</code></div></td>"
|
||
f"<td><code>{esc(observation.get('surface'))}</code></td>"
|
||
f"<td>{esc(_ai_location(card))}</td>"
|
||
f"<td>{chip(card.get('state'), 'Shadow · 待复核', code=card.get('state'))}</td>"
|
||
f"<td>{_ai_revalidation_chip(card.get('_revalidation', {}).get('status'))}</td></tr>"
|
||
)
|
||
cards_table = ("<div class='card'><div class='hd'>案例卡明细"
|
||
f"({fmt_num(len(filtered))} 张,当前第 {page}/{pages} 页)</div>"
|
||
"<table><thead><tr><th>卡片 ID</th><th>作品</th><th>模式</th><th>表面命中</th><th>位置</th><th>状态</th><th>来源重验证</th></tr></thead>"
|
||
f"<tbody>{''.join(card_rows) or '<tr><td colspan=7>当前筛选暂无案例</td></tr>'}</tbody></table>"
|
||
f"{_ai_pager('/ai-flavor', params, page, pages)}</div>")
|
||
detail = _ai_card_detail(selected) if selected else empty_gap("选择一张案例卡", "点击明细表中的卡片 ID 查看来源锚点和人工复核要求。")
|
||
filters = ("<form class='knowledge-filter' method='get' action='/ai-flavor'>"
|
||
f"<input name='work' value='{esc(selected_work)}' placeholder='按 work_ref 筛选'>"
|
||
f"<input name='pattern' value='{esc(selected_pattern)}' placeholder='按 pattern_key 筛选'>"
|
||
"<button type='submit'>筛选</button><a class='clear' href='/ai-flavor'>清除</a></form>")
|
||
if revalidation:
|
||
if authority == "postgresql":
|
||
receipt_ref = f"batch_id={esc((revalidation or {}).get('_batch_id'))} · report_sha256={esc((revalidation or {}).get('_report_sha256'))}"
|
||
else:
|
||
latest = _latest_ai_flavor_revalidation_report()
|
||
receipt_ref = (str(latest.relative_to(Path(__file__).resolve().parents[1]))
|
||
if latest else "仓内离线回执")
|
||
revalidation_note = (f"重验证 <code>{esc(receipt_ref)}</code> · "
|
||
f"检查于 <code>{esc((revalidation or {}).get('checked_on', 'unknown'))}</code> · "
|
||
f"来源已验证 {fmt_num(verification_counts.get('verified', 0))}/{fmt_num(len(cards))}")
|
||
else:
|
||
revalidation_note = f"重验证:{esc(revalidation_error or '未提供回执')}"
|
||
authority_label = "PostgreSQL muse-example 正式库" if authority == "postgresql" else "离线回退(数据库当前不可读)"
|
||
fallback_note = f"<br><span class='chip serious'>回退</span> {esc(load_note)}" if authority == "offline" and load_note else ""
|
||
source_storage_note = "研究限定/未授权来源原文片段未保存" if source_license in {"research_only", "unauthorized"} else "授权来源可在评审后保存受控片段"
|
||
meta = (f"<p class='note'>来源 <code>{esc(authority_label)}</code> · 批次/报告 <code>{esc(report_rel)}</code> · 生成于 <code>{esc(generated_on)}</code> · "
|
||
f"确定性模式命中数不是人工确认的 AI 味结论;{esc(source_storage_note)},当前状态仍须按评审门禁升级。<br>"
|
||
f"{revalidation_note}。来源重验证只在确认、投影或规则使用前作为门禁,打开本页不会重新读取原文。{fallback_note}</p>")
|
||
body = ("<h1>AI 味案例<span class='sub'>已有作品反向回填 · 数据库案例卡 · 候选命中(非确认结论)</span></h1>"
|
||
f"{meta}{tiles}{filters}<div class='knowledge-grid'><div>{work_table}{pattern_table}{cards_table}</div>{detail}</div>")
|
||
return layout("AI 味案例", body, "/ai-flavor",
|
||
"质量证据页 · 数据库只读渲染 · 案例卡可追溯 · 重验证回执 append-only · 规则仍须人工/回放门禁")
|
||
|
||
|
||
# ── 总览(08 数据权威:落库验收面 + 健康数字 + 额度水位)──────────────────────
|
||
# 落库缺口板:一切输入产出该落哪张表,建没建、有没有数(合同 §4.2 的验收面)
|
||
_LEDGER = [
|
||
("AI 味案例卡", "example_ai_flavor_case"),
|
||
("AI 味重验证批次", "example_ai_flavor_revalidation_batch"),
|
||
("AI 味逐卡回执", "example_ai_flavor_revalidation"),
|
||
("待审候选", "example_candidate"),
|
||
("用户决策", "example_user_decision"),
|
||
("运行回执", "example_run_receipt"),
|
||
("质量评判", "example_quality_result"),
|
||
("模型调用明细", "example_llm_call"),
|
||
("运行注册", "example_run"),
|
||
("原始全文", "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'>额度窗口 <code>{esc(wk)}</code></span></h2>"
|
||
f"<div class='card'><div style='padding:14px 16px'>"
|
||
f"{meter(usd, WINDOW_BUDGET_USD, f'模型 <code>MiniMax</code> 花费 ${usd:.4f} / ${WINDOW_BUDGET_USD:.0f}')}"
|
||
f"{meter(calls, WINDOW_CALL_CAP, f'成功调用 {calls:,} / {WINDOW_CALL_CAP:,} 次')}</div></div>")
|
||
|
||
|
||
def view_home():
|
||
ledger_names = [tbl for _, tbl in _LEDGER]
|
||
counts = table_counts([
|
||
"muse_content_work", "muse_content_chapter", "muse_content_block",
|
||
"muse_knowledge_draft", "muse_knowledge_entity", "example_reference_work",
|
||
"example_clean_log", *ledger_names,
|
||
])
|
||
works = counts.get("muse_content_work", 0)
|
||
chapters = counts.get("muse_content_chapter", 0)
|
||
blocks = counts.get("muse_content_block", 0)
|
||
drafts = counts.get("muse_knowledge_draft", 0)
|
||
entities = counts.get("muse_knowledge_entity", 0)
|
||
refs = counts.get("example_reference_work", 0)
|
||
cleans = counts.get("example_clean_log", 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 tbl not in counts:
|
||
st, cnt = chip("not_built", "未建 · 待落库"), "—"
|
||
else:
|
||
n = counts[tbl]
|
||
st = chip("active", "已建 · 有数据") if n > 0 else chip("pending", "已建 · 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 w.id,w.title,w.status,w.genre,COUNT(DISTINCT c.id)::bigint AS chapter_count, "
|
||
"COALESCE(SUM(b.word_count),0)::bigint AS word_count "
|
||
"FROM muse_content_work w "
|
||
"LEFT JOIN muse_content_chapter c ON c.work_id=w.id AND c.deleted=false "
|
||
"LEFT JOIN muse_content_block b ON b.chapter_id=c.id AND b.deleted=false "
|
||
"WHERE w.deleted=false GROUP BY w.id,w.title,w.status,w.genre ORDER BY w.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'>正式内容以库为准(表编码 <code>muse_content_work</code>)· 作品领域 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 _knowledge_card_detail(wid, card):
|
||
payload = card.get("draft_payload") or {}
|
||
entity_id = card.get("entity_id")
|
||
source = payload.get("source") if isinstance(payload.get("source"), dict) else {}
|
||
target = payload.get("target") if isinstance(payload.get("target"), dict) else {}
|
||
name = (card.get("entity_name") or payload.get("name") or payload.get("名称") or
|
||
source.get("name") or target.get("name") or "未命名卡")
|
||
card_type = card.get("entity_type") or payload.get("type") or payload.get("型") or card.get("draft_type")
|
||
brief = (card.get("entity_description") or payload.get("brief") or payload.get("description") or
|
||
payload.get("摘要") or "")
|
||
fields = payload.get("fields") or payload.get("attributes") or {}
|
||
extract_run_id = payload.get("extractRunId") or payload.get("extract_run_id")
|
||
run_link = (f"<a href='/runs/{html.escape(str(extract_run_id))}'><code>{html.escape(str(extract_run_id))}</code></a>"
|
||
if extract_run_id else "—")
|
||
chapter = _chapter_text(card.get("chapter_id"), card.get("chapter_order"), card.get("chapter_title"), link_work=wid)
|
||
entity_html = ""
|
||
if entity_id:
|
||
entity_html = (
|
||
"<details class='data-details' open><summary>正式实体快照</summary>"
|
||
f"<table class='detail-table'><tr><th>实体编号</th><td><code>{esc(entity_id)}</code></td></tr>"
|
||
f"<tr><th>实体状态</th><td>{chip(card.get('entity_status'))}</td></tr>"
|
||
f"<tr><th>实体名称</th><td>{esc(card.get('entity_name'))}</td></tr>"
|
||
f"<tr><th>实体说明</th><td class='detail-text'>{esc(card.get('entity_description'))}</td></tr></table>"
|
||
f"{_data_block(card.get('entity_attributes'))}"
|
||
"</details>"
|
||
)
|
||
relation_html = ""
|
||
if source or target:
|
||
relation_html = ("<tr><th>关系两端</th><td class='detail-text'>"
|
||
f"{esc(source.get('name', '未命名'))} → {esc(target.get('name', '未命名'))}</td></tr>")
|
||
return (
|
||
"<div class='card knowledge-detail' id='knowledge-detail'>"
|
||
f"<div class='detail-head'><h3>{esc(name)}</h3><div class='sub'>{enum_text(card_type, 'entity_type')} · 卡片编号 <code>{esc(card.get('draft_id'))}</code></div></div>"
|
||
"<table class='detail-table'>"
|
||
f"<tr><th>卡片类型</th><td>{enum_text(card.get('draft_type'), 'draft_type')}</td></tr>"
|
||
f"<tr><th>实体/关系类型</th><td>{enum_text(card_type, 'entity_type')}</td></tr>"
|
||
f"<tr><th>状态</th><td>{chip(card.get('status'))}</td></tr>"
|
||
f"<tr><th>置信度</th><td>{esc(card.get('confidence'))}</td></tr>"
|
||
f"<tr><th>来源章节</th><td>{chapter}</td></tr>"
|
||
f"<tr><th>抽取运行</th><td>{run_link}</td></tr>"
|
||
f"<tr><th>更新时间</th><td>{esc(card.get('update_time'))}</td></tr>"
|
||
f"<tr><th>来源类型</th><td>{enum_text(card.get('source_type'), 'source_type')} · <code>source_id={esc(card.get('source_id'))}</code></td></tr>"
|
||
f"{relation_html}"
|
||
f"<tr><th>摘要</th><td class='detail-text'>{esc(brief) if brief else '—'}</td></tr>"
|
||
f"<tr><th>证据原文</th><td class='detail-text'>{esc(payload.get('evidence')) if payload.get('evidence') else '—'}</td></tr>"
|
||
"</table>"
|
||
f"<div class='detail-head'><strong>字段</strong></div>{_mapping_html(fields)}"
|
||
f"{entity_html}"
|
||
f"<details class='data-details'><summary>查看完整卡载荷</summary>{_data_block(payload)}</details>"
|
||
+ (f"<details class='data-details'><summary>查看拟议变更</summary>{_data_block(card.get('proposed_changes'))}</details>"
|
||
if card.get("proposed_changes") else "")
|
||
+ (f"<details class='data-details'><summary>查看当前正式快照</summary>{_data_block(card.get('current_canonical_snapshot'))}</details>"
|
||
if card.get("current_canonical_snapshot") else "")
|
||
+ "</div>"
|
||
)
|
||
|
||
|
||
def _work_knowledge_panel(wid, params=None):
|
||
"""作品级知识链下钻:可筛选卡片、右侧详情、抽取运行与向量状态。"""
|
||
params = params or {}
|
||
if not table_exists("muse_knowledge_draft"):
|
||
return empty_gap("知识抽取 · 待落库", "知识草稿表尚未建立。")
|
||
_, summary = q(
|
||
"""SELECT
|
||
COUNT(*) FILTER (WHERE d.status='pending'),
|
||
COUNT(*) FILTER (WHERE d.status='confirmed'),
|
||
COUNT(*) FILTER (WHERE d.draft_type='entity' AND d.status='pending'),
|
||
COUNT(*) FILTER (WHERE d.draft_type='relation' AND d.status='pending'),
|
||
(SELECT COUNT(*) FROM muse_knowledge_entity e
|
||
WHERE e.work_id=%s AND e.deleted=false),
|
||
(SELECT COUNT(*) FROM muse_knowledge_relation r
|
||
WHERE r.work_id=%s AND r.deleted=false),
|
||
(SELECT COUNT(*) FROM muse_knowledge_binding b
|
||
WHERE b.work_id=%s AND b.deleted=false AND b.binding_status='active'),
|
||
(SELECT COUNT(*) FROM example_knowledge_embedding e
|
||
WHERE e.deleted=false AND (
|
||
EXISTS (SELECT 1 FROM muse_knowledge_draft d2
|
||
WHERE d2.id=e.draft_id AND d2.work_id=%s AND d2.deleted=false)
|
||
OR EXISTS (SELECT 1 FROM muse_knowledge_entity e2
|
||
WHERE e2.id=e.entity_id AND e2.work_id=%s AND e2.deleted=false)
|
||
))
|
||
FROM muse_knowledge_draft d
|
||
WHERE d.work_id=%s AND d.deleted=false""",
|
||
(wid, wid, wid, wid, wid, wid),
|
||
)
|
||
pending, confirmed_drafts, pending_entities, pending_relations, entities, relations, bindings, embeddings = summary[0]
|
||
|
||
search = (params.get("q") or [""])[0].strip()
|
||
type_filter = (params.get("type") or [""])[0].strip()
|
||
status_filter = (params.get("status") or [""])[0].strip()
|
||
chapter_filter = (params.get("chapter") or [""])[0].strip()
|
||
selected_card = (params.get("card") or [""])[0].strip()
|
||
try:
|
||
selected_card_id = int(selected_card) if selected_card else None
|
||
except ValueError:
|
||
selected_card_id = None
|
||
|
||
_, type_rows = q(
|
||
"""SELECT DISTINCT COALESCE(e.entity_type,d.draft_payload->>'type',d.draft_payload->>'型',d.draft_type)
|
||
FROM muse_knowledge_draft d LEFT JOIN muse_knowledge_entity e ON e.id=d.entity_id AND e.deleted=false
|
||
WHERE d.work_id=%s AND d.deleted=false ORDER BY 1""",
|
||
(wid,),
|
||
)
|
||
_, chapter_rows = q(
|
||
"SELECT id,order_no,title FROM muse_content_chapter WHERE work_id=%s AND deleted=false ORDER BY order_no",
|
||
(wid,),
|
||
)
|
||
type_options = [row[0] for row in type_rows if row[0]]
|
||
_, status_rows = q(
|
||
"SELECT DISTINCT status FROM muse_knowledge_draft WHERE work_id=%s AND deleted=false ORDER BY status",
|
||
(wid,),
|
||
)
|
||
status_options = [row[0] for row in status_rows if row[0]]
|
||
|
||
conditions = ["d.work_id=%s", "d.deleted=false"]
|
||
query_params = [wid]
|
||
if search:
|
||
like = f"%{search}%"
|
||
conditions.append("(d.draft_payload::text ILIKE %s OR COALESCE(e.normalized_name,'') ILIKE %s OR COALESCE(e.description,'') ILIKE %s)")
|
||
query_params.extend([like, like, like])
|
||
if type_filter:
|
||
conditions.append("COALESCE(e.entity_type,d.draft_payload->>'type',d.draft_payload->>'型',d.draft_type)=%s")
|
||
query_params.append(type_filter)
|
||
if status_filter:
|
||
conditions.append("d.status=%s")
|
||
query_params.append(status_filter)
|
||
if chapter_filter:
|
||
try:
|
||
chapter_order = int(chapter_filter)
|
||
except ValueError:
|
||
chapter_order = None
|
||
if chapter_order is not None:
|
||
conditions.append("c.order_no=%s")
|
||
query_params.append(chapter_order)
|
||
|
||
list_cols, list_rows = q(
|
||
"""SELECT d.id AS draft_id,d.entity_id,d.draft_type,d.status,d.confidence,d.source_type,d.source_id,
|
||
c.id AS chapter_id,c.order_no AS chapter_order,c.title AS chapter_title,
|
||
d.update_time,d.create_time,
|
||
COALESCE(e.normalized_name,d.draft_payload->>'name',d.draft_payload->>'名称',
|
||
d.draft_payload->'source'->>'name',d.draft_payload->'target'->>'name','未命名卡') AS entity_name,
|
||
COALESCE(e.entity_type,d.draft_payload->>'type',d.draft_payload->>'型',d.draft_type) AS entity_type,
|
||
COALESCE(e.description,d.draft_payload->>'brief',d.draft_payload->>'description','') AS summary,
|
||
EXISTS (SELECT 1 FROM example_knowledge_embedding emb
|
||
WHERE emb.deleted=false AND (emb.draft_id=d.id OR emb.entity_id=d.entity_id)) AS has_embedding
|
||
FROM muse_knowledge_draft d
|
||
LEFT JOIN muse_knowledge_entity e ON e.id=d.entity_id AND e.deleted=false
|
||
LEFT JOIN muse_content_chapter c ON c.id=d.source_id AND c.work_id=d.work_id AND c.deleted=false
|
||
WHERE """ + " AND ".join(conditions) +
|
||
" ORDER BY d.update_time DESC NULLS LAST,d.create_time DESC,d.id DESC",
|
||
tuple(query_params),
|
||
)
|
||
rows = _row_dicts(list_cols, list_rows)
|
||
|
||
detail = ("<div class='card knowledge-detail'><div class='hd'>卡详情</div>"
|
||
"<div class='empty'>点击左侧实体/卡列表,右侧显示完整内容和来源证据。</div></div>")
|
||
if selected_card_id is not None:
|
||
detail_cols, detail_rows = q(
|
||
"""SELECT d.id AS draft_id,d.entity_id,d.draft_type,d.status,d.confidence,d.source_type,d.source_id,
|
||
d.revision,d.create_time,d.update_time,d.draft_payload,d.proposed_changes,
|
||
d.current_canonical_snapshot,d.source_snapshot_id,d.authorization_snapshot_id,
|
||
d.source_status,d.source_action_policy,
|
||
c.id AS chapter_id,c.order_no AS chapter_order,c.title AS chapter_title,
|
||
e.id AS entity_id_joined,e.normalized_name AS entity_name,e.entity_type AS entity_type,
|
||
e.description AS entity_description,e.attributes AS entity_attributes,e.status AS entity_status,
|
||
e.update_time AS entity_update_time,e.lineage_payload AS entity_lineage_payload
|
||
FROM muse_knowledge_draft d
|
||
LEFT JOIN muse_content_chapter c ON c.id=d.source_id AND c.work_id=d.work_id AND c.deleted=false
|
||
LEFT JOIN muse_knowledge_entity e ON e.id=d.entity_id AND e.deleted=false
|
||
WHERE d.id=%s AND d.work_id=%s AND d.deleted=false""",
|
||
(selected_card_id, wid),
|
||
)
|
||
if detail_rows:
|
||
card = dict(zip(detail_cols, detail_rows[0]))
|
||
card["entity_id"] = card.get("entity_id_joined")
|
||
detail = _knowledge_card_detail(wid, card)
|
||
else:
|
||
detail = ("<div class='card knowledge-detail'><div class='hd'>卡详情</div>"
|
||
"<div class='empty'>该卡不属于当前作品,或已经被软删除。</div></div>")
|
||
|
||
def option(value, label, selected):
|
||
return f"<option value='{esc(value)}'{' selected' if str(value) == str(selected) else ''}>{esc(label)}</option>"
|
||
|
||
type_opts = "".join(option(value, _ENUM_LABELS.get("entity_type", {}).get(value, value), type_filter)
|
||
for value in type_options)
|
||
status_opts = "".join(option(value, _ENUM_LABELS.get("status", {}).get(value, value), status_filter)
|
||
for value in status_options)
|
||
chapter_opts = "".join(option(row[1], f"第{row[1]}章《{row[2]}》" if row[2] else f"第{row[1]}章", chapter_filter)
|
||
for row in chapter_rows)
|
||
filter_form = (
|
||
"<form class='knowledge-filter' method='get'>"
|
||
f"<input name='q' value='{esc(search)}' placeholder='搜索名称、描述或证据' aria-label='搜索名称、描述或证据'>"
|
||
f"<select name='type' aria-label='筛选类型'><option value=''>全部类型</option>{type_opts}</select>"
|
||
f"<select name='status' aria-label='筛选状态'><option value=''>全部状态</option>{status_opts}</select>"
|
||
f"<select name='chapter' aria-label='筛选来源章'><option value=''>全部来源章</option>{chapter_opts}</select>"
|
||
"<button type='submit'>筛选</button>"
|
||
f"<a class='clear' href='/works/{wid}'>清除条件</a></form>"
|
||
)
|
||
card_rows = []
|
||
for row in rows:
|
||
selected = " selected" if row["draft_id"] == selected_card_id else ""
|
||
current = dict(params or {})
|
||
href = _query_href(f"/works/{wid}", current, card=row["draft_id"])
|
||
summary_text = short_text(row["summary"], 120) if row["summary"] else "暂无摘要"
|
||
source_text = _chapter_text(row["chapter_id"], row["chapter_order"], row["chapter_title"])
|
||
card_rows.append(
|
||
f"<a class='knowledge-card-item{selected}' href='{html.escape(href)}' id='card-{esc(row['draft_id'])}'"
|
||
f"{' aria-current=\"true\"' if selected else ''} aria-label='查看 {esc(row['entity_name'])} 卡片详情'>"
|
||
f"<div class='title-row'><span class='title'>{esc(row['entity_name'])}</span>{enum_text(row['entity_type'], 'entity_type')}</div>"
|
||
f"<div class='meta'>{enum_text(row['draft_type'], 'draft_type')} · {source_text} · 更新于 {esc(row['update_time'])}</div>"
|
||
f"<div class='summary'>{esc(summary_text)} · {chip(row['status'])}</div></a>"
|
||
)
|
||
list_body = "".join(card_rows) if card_rows else empty_gap("没有匹配的实体/卡", "调整搜索或筛选条件后重试。")
|
||
list_html = (
|
||
f"<div class='card'><div class='hd'>实体/卡列表({len(rows)})<span class='sub'>默认按更新时间倒序</span></div>"
|
||
f"<div class='knowledge-card-list'>{list_body}</div></div>"
|
||
)
|
||
cards = (
|
||
f"<div class='tiles'>"
|
||
f"<div class='tile'><div class='v'>{fmt_num(pending)}</div><div class='l'>待确认草稿</div>"
|
||
f"<div class='d'>实体 {fmt_num(pending_entities)} · 关系 {fmt_num(pending_relations)}</div></div>"
|
||
f"<div class='tile'><div class='v'>{fmt_num(confirmed_drafts)}</div><div class='l'>已确认草稿</div></div>"
|
||
f"<div class='tile'><div class='v'>{fmt_num(entities)}</div><div class='l'>正式实体</div></div>"
|
||
f"<div class='tile'><div class='v'>{fmt_num(relations)}</div><div class='l'>正式关系</div></div>"
|
||
f"<div class='tile'><div class='v'>{fmt_num(embeddings)}</div><div class='l'>草稿向量</div></div>"
|
||
f"<div class='tile'><div class='v'>{fmt_num(bindings)}</div><div class='l'>有效知识库绑定</div></div>"
|
||
f"</div>"
|
||
)
|
||
knowledge_grid = f"<div class='knowledge-grid'>{list_html}{detail}</div>"
|
||
runs = _runs_for_work(wid, 30)
|
||
runs_html = (
|
||
f"<div class='card'><div class='hd'>作品运行链(最近 {len(runs)})</div>"
|
||
f"{_RUN_COLS}{_run_rows(runs)}</tbody></table></div>"
|
||
) if runs else empty_gap("作品暂无运行", "运行登记后会显示模型、回执和原始证据下钻入口。")
|
||
return f"<h2>知识闭环</h2><p class='note'>抽取 → 草稿 → 向量 → 用户确认 → 正式实体/关系;看板只读,确认仍走确认通道。</p>{cards}{filter_form}{knowledge_grid}{runs_html}"
|
||
|
||
|
||
_SCHEMA_LABELS = {
|
||
"outline": "大纲结构", "novel_work": "作品设定", "narrative_state": "叙事状态", "assembly": "装配结构",
|
||
}
|
||
|
||
|
||
def _planning_title(payload, section_type, target_chapter):
|
||
payload = payload if isinstance(payload, dict) else {}
|
||
if payload.get("title"):
|
||
return str(payload["title"])
|
||
if payload.get("chapterGoal"):
|
||
return f"第{target_chapter}章目标" if target_chapter is not None else "章节目标"
|
||
if isinstance(payload.get("state"), dict) and payload["state"].get("currentSituation"):
|
||
return "当前叙事状态"
|
||
if section_type == "assembly":
|
||
return "范式装配结果"
|
||
return _ENUM_LABELS.get("section_type", {}).get(section_type, "规划内容")
|
||
|
||
|
||
def _planning_summary(payload):
|
||
payload = payload if isinstance(payload, dict) else {}
|
||
candidates = [
|
||
payload.get("chapterGoal"), payload.get("overall_arc"), payload.get("core_premise"),
|
||
payload.get("currentSituation"), payload.get("chapterEndHook"), payload.get("handoff"),
|
||
]
|
||
state = payload.get("state")
|
||
if isinstance(state, dict):
|
||
candidates.insert(3, state.get("currentSituation"))
|
||
return next((str(value) for value in candidates if value), "暂无摘要")
|
||
|
||
|
||
def _planning_detail(wid, section):
|
||
payload = section.get("payload") or {}
|
||
section_type = section.get("section_type")
|
||
title = _planning_title(payload, section_type, section.get("target_chapter"))
|
||
summary_rows = []
|
||
summary_keys = [
|
||
("title", "标题"), ("chapterGoal", "章节目标"), ("overall_arc", "总体弧线"),
|
||
("core_premise", "核心设定"), ("currentSituation", "当前局势"),
|
||
("chapterEndHook", "章末钩子"), ("handoff", "交接信息"),
|
||
]
|
||
for key, label in summary_keys:
|
||
value = payload.get(key)
|
||
if value:
|
||
summary_rows.append(f"<tr><th>{label}</th><td class='detail-text'>{esc(_json_text(value))}</td></tr>")
|
||
state = payload.get("state")
|
||
if isinstance(state, dict) and state.get("currentSituation") and not payload.get("currentSituation"):
|
||
summary_rows.append(f"<tr><th>当前局势</th><td class='detail-text'>{esc(_json_text(state['currentSituation']))}</td></tr>")
|
||
summary_html = ("<div class='detail-head'><strong>规划摘要</strong></div><table class='detail-table'>"
|
||
+ "".join(summary_rows) + "</table>") if summary_rows else ""
|
||
chapter = _chapter_text(section.get("chapter_id"), section.get("chapter_order"), section.get("chapter_title"), link_work=wid)
|
||
return (
|
||
"<div class='card knowledge-detail' id='planning-detail'>"
|
||
f"<div class='detail-head'><h3>{esc(title)}</h3><div class='sub'>{enum_text(section_type, 'section_type')} · 规划编号 <code>{esc(section.get('planning_id'))}</code></div></div>"
|
||
"<table class='detail-table'>"
|
||
f"<tr><th>规划类型</th><td>{enum_text(section_type, 'section_type')}</td></tr>"
|
||
f"<tr><th>结构类型</th><td>{_SCHEMA_LABELS.get(section.get('schema_type'), '未登记')} <code>{esc(section.get('schema_type'))}</code></td></tr>"
|
||
f"<tr><th>目标章</th><td>{chapter}</td></tr>"
|
||
f"<tr><th>版本号</th><td><code>{esc(section.get('version'))}</code></td></tr>"
|
||
f"<tr><th>状态</th><td>{chip(section.get('state'))}</td></tr>"
|
||
f"<tr><th>确认时间</th><td>{esc(section.get('confirmed_at'))}</td></tr>"
|
||
f"<tr><th>创建时间</th><td>{esc(section.get('create_time'))}</td></tr>"
|
||
f"<tr><th>更新时间</th><td>{esc(section.get('update_time'))}</td></tr>"
|
||
"</table>"
|
||
f"{summary_html}"
|
||
f"<details class='data-details' open><summary>查看完整规划载荷</summary>{_data_block(payload)}</details>"
|
||
"</div>"
|
||
)
|
||
|
||
|
||
def _planning_panel(wid, params=None):
|
||
"""规划列表:更新时间倒序、服务端筛选分页,选中项右侧展示完整 payload。"""
|
||
params = params or {}
|
||
if not table_exists("example_planning_section"):
|
||
return empty_gap("规划 · 待落库", "规划表尚未建立(编码:100)。")
|
||
|
||
keyword = (params.get("plan_q") or [""])[0].strip()
|
||
type_filter = (params.get("plan_type") or [""])[0].strip()
|
||
schema_filter = (params.get("plan_schema") or [""])[0].strip()
|
||
state_filter = (params.get("plan_state") or [""])[0].strip()
|
||
chapter_filter = (params.get("plan_chapter") or [""])[0].strip()
|
||
selected_raw = (params.get("plan") or [""])[0].strip()
|
||
try:
|
||
selected_id = int(selected_raw) if selected_raw else None
|
||
except ValueError:
|
||
selected_id = None
|
||
try:
|
||
page = max(1, int((params.get("plan_page") or ["1"])[0]))
|
||
except ValueError:
|
||
page = 1
|
||
page_size = 10
|
||
|
||
_, type_rows = q("SELECT DISTINCT section_type FROM example_planning_section WHERE work_id=%s AND deleted=false ORDER BY 1", (wid,))
|
||
_, schema_rows = q("SELECT DISTINCT schema_type FROM example_planning_section WHERE work_id=%s AND deleted=false ORDER BY 1", (wid,))
|
||
_, state_rows = q("SELECT DISTINCT state FROM example_planning_section WHERE work_id=%s AND deleted=false ORDER BY 1", (wid,))
|
||
_, chapter_rows = q("SELECT order_no,title FROM muse_content_chapter WHERE work_id=%s AND deleted=false ORDER BY order_no", (wid,))
|
||
|
||
conditions = ["p.work_id=%s", "p.deleted=false"]
|
||
query_params = [wid]
|
||
if keyword:
|
||
like = f"%{keyword}%"
|
||
conditions.append("(p.payload::text ILIKE %s OR p.section_type ILIKE %s OR p.schema_type ILIKE %s)")
|
||
query_params.extend([like, like, like])
|
||
if type_filter:
|
||
conditions.append("p.section_type=%s")
|
||
query_params.append(type_filter)
|
||
if schema_filter:
|
||
conditions.append("p.schema_type=%s")
|
||
query_params.append(schema_filter)
|
||
if state_filter:
|
||
conditions.append("p.state=%s")
|
||
query_params.append(state_filter)
|
||
if chapter_filter == "none":
|
||
conditions.append("p.target_chapter IS NULL")
|
||
elif chapter_filter:
|
||
try:
|
||
chapter_number = int(chapter_filter)
|
||
except ValueError:
|
||
chapter_number = None
|
||
if chapter_number is not None:
|
||
conditions.append("p.target_chapter=%s")
|
||
query_params.append(chapter_number)
|
||
where_sql = " AND ".join(conditions)
|
||
total = int(one("SELECT count(*) FROM example_planning_section p WHERE " + where_sql, tuple(query_params)) or 0)
|
||
page_count = max(1, (total + page_size - 1) // page_size)
|
||
page = min(page, page_count)
|
||
list_cols, list_rows = q(
|
||
"""SELECT p.id AS planning_id,p.section_type,p.schema_type,p.target_chapter,p.version,p.state,p.confirmed_at,
|
||
p.create_time,p.update_time,p.payload,c.id AS chapter_id,c.order_no AS chapter_order,c.title AS chapter_title
|
||
FROM example_planning_section p
|
||
LEFT JOIN muse_content_chapter c ON c.work_id=p.work_id AND c.order_no=p.target_chapter AND c.deleted=false
|
||
WHERE """ + where_sql +
|
||
" ORDER BY p.update_time DESC NULLS LAST,p.create_time DESC,p.id DESC LIMIT %s OFFSET %s",
|
||
tuple(query_params) + (page_size, (page - 1) * page_size),
|
||
)
|
||
rows = _row_dicts(list_cols, list_rows)
|
||
|
||
def option(value, label, selected):
|
||
return f"<option value='{esc(value)}'{' selected' if str(value) == str(selected) else ''}>{esc(label)}</option>"
|
||
|
||
type_opts = "".join(option(row[0], _ENUM_LABELS.get("section_type", {}).get(row[0], row[0]), type_filter) for row in type_rows)
|
||
schema_opts = "".join(option(row[0], _SCHEMA_LABELS.get(row[0], row[0]), schema_filter) for row in schema_rows)
|
||
state_opts = "".join(option(row[0], _ENUM_LABELS.get("status", {}).get(row[0], row[0]), state_filter) for row in state_rows)
|
||
chapter_opts = "<option value='none'{}>无目标章</option>".format(" selected" if chapter_filter == "none" else "")
|
||
chapter_opts += "".join(option(row[0], f"第{row[0]}章《{row[1]}》" if row[1] else f"第{row[0]}章", chapter_filter) for row in chapter_rows)
|
||
filter_form = (
|
||
"<form class='knowledge-filter' method='get'>"
|
||
f"<input name='plan_q' value='{esc(keyword)}' placeholder='搜索规划标题、目标或载荷' aria-label='搜索规划标题、目标或载荷'>"
|
||
f"<select name='plan_type' aria-label='筛选规划类型'><option value=''>全部规划类型</option>{type_opts}</select>"
|
||
f"<select name='plan_schema' aria-label='筛选结构类型'><option value=''>全部结构类型</option>{schema_opts}</select>"
|
||
f"<select name='plan_state' aria-label='筛选规划状态'><option value=''>全部状态</option>{state_opts}</select>"
|
||
f"<select name='plan_chapter' aria-label='筛选目标章'><option value=''>全部目标章</option>{chapter_opts}</select>"
|
||
"<button type='submit'>筛选</button>"
|
||
f"<a class='clear' href='/works/{wid}'>清除规划条件</a></form>"
|
||
)
|
||
items = []
|
||
for row in rows:
|
||
selected = " selected" if row["planning_id"] == selected_id else ""
|
||
href = _query_href(f"/works/{wid}", params, plan=row["planning_id"], plan_page=page)
|
||
title = _planning_title(row["payload"], row["section_type"], row["target_chapter"])
|
||
chapter = _chapter_text(row["chapter_id"], row["chapter_order"], row["chapter_title"])
|
||
items.append(
|
||
f"<a class='planning-item{selected}' href='{html.escape(href)}' id='planning-{esc(row['planning_id'])}'"
|
||
f"{' aria-current=\"true\"' if selected else ''} aria-label='查看 {esc(title)} 规划详情'>"
|
||
f"<div class='title-row'><span class='title'>{esc(title)}</span>{enum_text(row['section_type'], 'section_type')}</div>"
|
||
f"<div class='meta'>目标:{chapter} · 版本 <code>{esc(row['version'])}</code> · 更新于 {esc(row['update_time'])}</div>"
|
||
f"<div class='summary'>{esc(short_text(_planning_summary(row['payload']), 130))} · {chip(row['state'])}</div></a>"
|
||
)
|
||
list_body = "".join(items) if items else empty_gap("没有匹配的规划", "调整搜索或筛选条件后重试。")
|
||
pager_parts = [f"<span class='total'>第 {page}/{page_count} 页 · 共 {total} 条</span>"]
|
||
if page > 1:
|
||
pager_parts.append(f"<a href='{html.escape(_query_href(f'/works/{wid}', params, plan_page=page - 1))}'>上一页</a>")
|
||
page_start = max(1, page - 2)
|
||
page_end = min(page_count, page + 2)
|
||
if page_start > 1:
|
||
pager_parts.append(f"<a href='{html.escape(_query_href(f'/works/{wid}', params, plan_page=1))}'>1</a>")
|
||
if page_start > 2:
|
||
pager_parts.append("<span>…</span>")
|
||
pager_parts.extend(
|
||
f"<span class='current'>{number}</span>" if number == page else
|
||
f"<a href='{html.escape(_query_href(f'/works/{wid}', params, plan_page=number))}'>{number}</a>"
|
||
for number in range(page_start, page_end + 1)
|
||
)
|
||
if page_end < page_count:
|
||
if page_end < page_count - 1:
|
||
pager_parts.append("<span>…</span>")
|
||
pager_parts.append(f"<a href='{html.escape(_query_href(f'/works/{wid}', params, plan_page=page_count))}'>{page_count}</a>")
|
||
if page < page_count:
|
||
pager_parts.append(f"<a href='{html.escape(_query_href(f'/works/{wid}', params, plan_page=page + 1))}'>下一页</a>")
|
||
pager = "<div class='pager' aria-label='规划分页'>" + "".join(pager_parts) + "</div>"
|
||
list_html = (f"<div class='card'><div class='hd'>规划列表({len(rows)} / {total})<span class='sub'>默认按更新时间倒序</span></div>"
|
||
f"<div class='knowledge-card-list'>{list_body}</div>{pager}</div>")
|
||
|
||
detail = ("<div class='card knowledge-detail'><div class='hd'>规划详情</div>"
|
||
"<div class='empty'>点击左侧规划列表,右侧显示完整规划内容和版本信息。</div></div>")
|
||
if selected_id is not None:
|
||
detail_cols, detail_rows = q(
|
||
"""SELECT p.id AS planning_id,p.section_type,p.schema_type,p.target_chapter,p.version,p.state,p.confirmed_at,
|
||
p.create_time,p.update_time,p.payload,c.id AS chapter_id,c.order_no AS chapter_order,c.title AS chapter_title
|
||
FROM example_planning_section p
|
||
LEFT JOIN muse_content_chapter c ON c.work_id=p.work_id AND c.order_no=p.target_chapter AND c.deleted=false
|
||
WHERE p.id=%s AND p.work_id=%s AND p.deleted=false""",
|
||
(selected_id, wid),
|
||
)
|
||
if detail_rows:
|
||
detail = _planning_detail(wid, dict(zip(detail_cols, detail_rows[0])))
|
||
else:
|
||
detail = ("<div class='card knowledge-detail'><div class='hd'>规划详情</div>"
|
||
"<div class='empty'>该规划不属于当前作品,或已经被软删除。</div></div>")
|
||
return f"{filter_form}<div class='planning-grid'>{list_html}{detail}</div>"
|
||
|
||
|
||
def view_work(wid, params=None):
|
||
_, wrows = q(
|
||
"SELECT w.id,w.title,w.status,w.genre,w.summary, "
|
||
"COALESCE(SUM(b.word_count),0)::bigint AS word_count, "
|
||
"COUNT(DISTINCT c.id)::bigint AS chapter_count "
|
||
"FROM muse_content_work w "
|
||
"LEFT JOIN muse_content_chapter c ON c.work_id=w.id AND c.deleted=false "
|
||
"LEFT JOIN muse_content_block b ON b.chapter_id=c.id AND b.deleted=false "
|
||
"WHERE w.id=%s AND w.deleted=false GROUP BY w.id,w.title,w.status,w.genre,w.summary",
|
||
(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("还没有章节", "")
|
||
plan = _planning_panel(wid, params)
|
||
knowledge = _work_knowledge_panel(wid, params)
|
||
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}<h2>规划</h2>{plan}{knowledge}")
|
||
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, group=None):
|
||
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)]})"
|
||
display_name = enum_text(name, group) if group else esc(name)
|
||
out.append(f"<div class='bar-row'><span class='name'>{display_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(params=None):
|
||
params = params or {}
|
||
selected = (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], selected)
|
||
if selected:
|
||
try:
|
||
work_id = int(selected)
|
||
except ValueError:
|
||
return layout("知识库", "<h1>作品参数非法</h1>", "/knowledge")
|
||
_, work = q("SELECT title FROM muse_content_work WHERE id=%s AND deleted=false", (work_id,))
|
||
if not work:
|
||
return layout("知识库", "<h1>作品不存在</h1>", "/knowledge")
|
||
body = (f"<div class='crumb'><a href='/knowledge'>知识库</a> / {esc(work[0][0])}</div>"
|
||
f"<h1>知识库<span class='sub'>作品级创作闭环 · {esc(work[0][0])}</span></h1>"
|
||
f"{selector}{_work_knowledge_panel(work_id)}")
|
||
return layout("知识库 · " + str(work[0][0]), body, "/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
|
||
|
||
# 型别看载荷字段,不看 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">公共范式卡(<code>work_id=0</code>)</div></div>
|
||
<div class="tile"><div class="v">{fmt_num(work_cards)}</div><div class="l">作品级卡(<code>work_id≠0</code>)</div></div>
|
||
</div>
|
||
<div class="card"><div class="hd">公共范式卡 · 按型分布(载荷字段 <code>payload</code> 的中文键「型」)</div>{_barset(pub_types,[1,2,3,4,5], 'entity_type')}</div>
|
||
<div class="card"><div class="hd">作品级卡 · 按型分布(类型字段 <code>type</code>)</div>{_barset(wk_types,[1,2,3,4,5], 'entity_type')}</div>
|
||
<p class="note">型别看载荷字段 <code>draft_payload</code>,<b>不看</b> <code>draft_type</code> 列(该列恒为实体草稿,无区分力)。
|
||
文风与节奏(<code>style</code> / <code>pacing</code>)已注册 schema,但当前 0 张卡。草稿≠已确认:确认前正式实体表为 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'><code>{esc(r[0])}</code> · <code>{esc(r[2])}</code></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'>实体与范式(表编码 <code>muse_knowledge_*</code>)+ 它们的生产线(拆书·清洗)· 实体 02 · 范式 03</span></h1>"
|
||
f"{selector}{cards}<h2>参考书 · 拆书 · 清洗</h2>{reftbl}{pt_html}{clean_html}")
|
||
return layout("知识库", body, "/knowledge")
|
||
|
||
|
||
# ── 运行记录(05 流程 + 06 质量:运行列表按作品/按角色双维 + 运行详情下钻)────────
|
||
_RUN_COLS = ("<table><thead><tr><th>运行编号 <code>run_id</code></th><th>作品</th><th>运行内容</th>"
|
||
"<th class='num'>目标章</th><th>触发来源</th><th>终态</th><th>开始时间</th></tr></thead><tbody>")
|
||
|
||
|
||
def _run_description(run):
|
||
"""把机器运行登记拼成用户能判断用途的中文描述。"""
|
||
run_id, work_id, work_title, target_chapter, trigger, terminal, started = run[:7]
|
||
chapter_id, chapter_order, chapter_title = run[7:10]
|
||
adapter_role, stage_kind, run_type, source_role, purpose = run[10:15]
|
||
if purpose == "planning_persistence" or str(run_id).startswith("planning-persist"):
|
||
kind = "规划落库"
|
||
elif purpose == "production_generation" or run_type == "production" or str(run_id).startswith("run-prod"):
|
||
kind = "正文生产"
|
||
elif adapter_role == "extractor" or source_role == "extractor" or str(run_id).startswith("extract-knowledge"):
|
||
kind = "章后抽卡"
|
||
elif purpose == "evaluation" or run_type == "eval":
|
||
kind = "质量评测"
|
||
elif adapter_role == "planner":
|
||
kind = "规划生成"
|
||
else:
|
||
kind = _ENUM_LABELS.get("stage_kind", {}).get(stage_kind, "运行")
|
||
actor = source_role or adapter_role
|
||
actor_html = f" · {enum_text(actor, 'source_role')}" if actor else ""
|
||
chapter_html = ""
|
||
if chapter_order is not None:
|
||
chapter_html = f" · {_chapter_text(chapter_id, chapter_order, chapter_title)}"
|
||
return f"{html.escape(kind)}{actor_html}{chapter_html}"
|
||
|
||
|
||
def _run_rows(runs):
|
||
return "".join(
|
||
f"<tr><td><a href='/runs/{esc(r[0])}'><code>{esc(r[0])}</code></a></td>"
|
||
f"<td>{esc(r[2])} <code>work_id={esc(r[1])}</code></td><td>{_run_description(r)}</td>"
|
||
f"<td class='num'>{esc(r[3])}</td><td>{display_trigger(r[4])}</td><td>{chip(r[5])}</td><td>{esc(r[6])}</td></tr>"
|
||
for r in runs)
|
||
|
||
|
||
def _runs_query(where_sql="r.deleted=false", params=(), limit=50):
|
||
candidate_join = (
|
||
"LEFT JOIN LATERAL (SELECT ec.run_type,ec.source_role FROM example_candidate ec "
|
||
"WHERE ec.run_id=r.run_id AND ec.deleted=false ORDER BY ec.create_time,ec.id LIMIT 1) cand ON TRUE"
|
||
if table_exists("example_candidate") else
|
||
"LEFT JOIN LATERAL (SELECT NULL::text AS run_type,NULL::text AS source_role) cand ON TRUE"
|
||
)
|
||
raw_join = (
|
||
"LEFT JOIN LATERAL (SELECT rl.purpose FROM example_raw_lease rl "
|
||
"WHERE rl.run_id=r.run_id ORDER BY rl.create_time,rl.id LIMIT 1) raw ON TRUE"
|
||
if table_exists("example_raw_lease") else
|
||
"LEFT JOIN LATERAL (SELECT NULL::text AS purpose) raw ON TRUE"
|
||
)
|
||
receipt_join = (
|
||
"LEFT JOIN LATERAL (SELECT er.adapter_role,er.stage_kind FROM example_run_receipt er "
|
||
"WHERE er.run_id=r.run_id ORDER BY er.create_time,er.id LIMIT 1) first_receipt ON TRUE"
|
||
if table_exists("example_run_receipt") else
|
||
"LEFT JOIN LATERAL (SELECT NULL::text AS adapter_role,NULL::text AS stage_kind) first_receipt ON TRUE"
|
||
)
|
||
sql = (
|
||
"SELECT r.run_id,r.work_id,w.title AS work_title,r.target_chapter,r.trigger_source,r.terminal_state,r.started_at,"
|
||
"c.id AS chapter_id,c.order_no AS chapter_order,c.title AS chapter_title,"
|
||
"first_receipt.adapter_role,first_receipt.stage_kind,cand.run_type,cand.source_role,raw.purpose "
|
||
"FROM example_run r LEFT JOIN muse_content_work w ON w.id=r.work_id "
|
||
"LEFT JOIN muse_content_chapter c ON c.work_id=r.work_id AND c.order_no=r.target_chapter AND c.deleted=false "
|
||
f"{receipt_join} {candidate_join} {raw_join} WHERE {where_sql} "
|
||
"ORDER BY r.started_at DESC LIMIT %s"
|
||
)
|
||
_, runs = q(sql, tuple(params) + (limit,))
|
||
return runs
|
||
|
||
|
||
def _all_runs(limit=50):
|
||
return _runs_query(limit=limit)
|
||
|
||
|
||
def _runs_for_work(work_id, limit=50):
|
||
return _runs_query("r.deleted=false AND r.work_id=%s", (work_id,), limit)
|
||
|
||
|
||
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'>候选编号 <code>candidate_id</code></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>{enum_text(c[4], 'run_type')}</td><td>{chip(c[5])}</td><td>{enum_text(c[6], 'source_role')}</td></tr>" for c in cands)
|
||
+ "</tbody></table></div>") if cands else "<div class='empty'>候选表已建 · 暂无待审候选</div>"
|
||
else:
|
||
cand = empty_gap("待审候选 · 待落库", "候选表尚未建立(编码:99),模型候选正文暂无持久化登记。")
|
||
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>{enum_chip(d[2], 'decision')}</td>"
|
||
f"<td>{annotated_text((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),接受/合并/丢弃暂以版本库提交留痕。")
|
||
body = (f"<h1>运行记录<span class='sub'>智能体的操作与产出 · 流程 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 = _runs_for_work(sel, 50)
|
||
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 = _runs_query(
|
||
"r.deleted=false AND EXISTS (SELECT 1 FROM example_run_receipt role_receipt "
|
||
"WHERE role_receipt.run_id=r.run_id AND role_receipt.adapter_role=%s)",
|
||
(sel,), 50,
|
||
)
|
||
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 r.run_id,r.work_id,r.target_chapter,r.trigger_source,r.started_at,r.finished_at,r.terminal_state,
|
||
w.title AS work_title,c.id AS chapter_id,c.order_no AS chapter_order,c.title AS chapter_title
|
||
FROM example_run r
|
||
LEFT JOIN muse_content_work w ON w.id=r.work_id
|
||
LEFT JOIN muse_content_chapter c ON c.work_id=r.work_id AND c.order_no=r.target_chapter AND c.deleted=false
|
||
WHERE r.run_id=%s AND r.deleted=false""",
|
||
(run_id,),
|
||
)
|
||
if not rr:
|
||
return layout("运行", "<h1>运行不存在</h1>", "/runs")
|
||
rid, wid, chap, trig, started, finished, state, work_title, chapter_id, chapter_order, chapter_title = rr[0]
|
||
rc_cols, rc_rows = q(
|
||
"""SELECT id,sample_id,revision,stage_kind,adapter_role,attempt,loop_seq,candidate_version,is_error,
|
||
stop_reason,terminal_reason,safe_summary,raw_content_id,requested_model_id,actual_model_id,
|
||
total_cost_usd,usage,create_time
|
||
FROM example_run_receipt WHERE run_id=%s ORDER BY sample_id,revision""",
|
||
(run_id,),
|
||
)
|
||
rc = _row_dicts(rc_cols, rc_rows)
|
||
q_cols, q_rows = q(
|
||
"""SELECT judge_kind,dimension,scale_version,score,conclusion,failure_class,detail,raw_content_id,create_time
|
||
FROM example_quality_result WHERE run_id=%s ORDER BY create_time""",
|
||
(run_id,),
|
||
)
|
||
qres = _row_dicts(q_cols, q_rows)
|
||
call_cols, call_rows = q(
|
||
"""SELECT id,call_time,caller,requested_model_id,actual_model_id,in_tokens,cached_tokens,out_tokens,
|
||
cost_usd,stop_reason,duration_ms,prompt_sha256,raw_content_id
|
||
FROM example_llm_call WHERE run_id=%s ORDER BY call_time""",
|
||
(run_id,),
|
||
)
|
||
calls = _row_dicts(call_cols, call_rows)
|
||
candidates = []
|
||
if table_exists("example_candidate"):
|
||
candidate_cols, candidate_rows = q(
|
||
"SELECT id, target_chapter, candidate_version, run_type, state, source_role, "
|
||
"candidate_sha256, context_sha256 FROM example_candidate "
|
||
"WHERE run_id=%s AND deleted=false ORDER BY target_chapter, candidate_version",
|
||
(run_id,),
|
||
)
|
||
candidates = _row_dicts(candidate_cols, candidate_rows)
|
||
cand = (
|
||
"<div class='card'><div class='hd'>候选(本运行)</div><table><thead>"
|
||
"<tr><th class='num'>候选编号 <code>candidate_id</code></th><th class='num'>目标章</th><th>版本编码</th><th>运行类型</th>"
|
||
"<th>状态</th><th>来源角色</th><th>正文哈希</th><th>上下文哈希</th></tr></thead><tbody>"
|
||
+ "".join(
|
||
f"<tr><td class='num'>{esc(c['id'])}</td><td class='num'>{esc(c['target_chapter'])}</td>"
|
||
f"<td><code>{esc(c['candidate_version'])}</code></td><td>{enum_text(c['run_type'], 'run_type')}</td><td>{chip(c['state'])}</td>"
|
||
f"<td>{enum_text(c['source_role'], 'source_role')}</td><td><code>{esc(c['candidate_sha256'])}</code></td><td><code>{esc(c['context_sha256'])}</code></td></tr>"
|
||
for c in candidates
|
||
)
|
||
+ "</tbody></table></div>"
|
||
if candidates else "<div class='empty'>候选表已建 · 该运行暂无候选</div>"
|
||
)
|
||
else:
|
||
cand = empty_gap("待审候选 · 待落库", "候选表尚未建立(编码:99)。")
|
||
if table_exists("example_user_decision") and table_exists("example_candidate"):
|
||
decision_cols, decision_rows = q(
|
||
"SELECT d.id,d.decision,d.rationale,d.decided_by,d.canonical_block_id,d.create_time "
|
||
"FROM example_user_decision d JOIN example_candidate c ON c.id=d.candidate_id "
|
||
"WHERE c.run_id=%s ORDER BY d.create_time",
|
||
(run_id,),
|
||
)
|
||
decisions = _row_dicts(decision_cols, decision_rows)
|
||
dec = (
|
||
"<div class='card'><div class='hd'>用户决策(本运行)</div><table><thead>"
|
||
"<tr><th class='num'>决策编号 <code>decision_id</code></th><th>决策</th><th>理据</th><th>决策人</th>"
|
||
"<th class='num'>正式正文块</th><th>时间</th></tr></thead><tbody>"
|
||
+ "".join(
|
||
f"<tr><td class='num'>{esc(d['id'])}</td><td>{enum_chip(d['decision'], 'decision')}</td>"
|
||
f"<td>{annotated_text(d['rationale'])}</td><td>{esc(d['decided_by'])}</td><td class='num'>{esc(d['canonical_block_id'])}</td><td>{esc(d['create_time'])}</td></tr>"
|
||
for d in decisions
|
||
)
|
||
+ "</tbody></table></div>"
|
||
if decisions else "<div class='empty'>决策表已建 · 该运行暂无决策</div>"
|
||
)
|
||
else:
|
||
dec = empty_gap("用户决策 · 待落库", "决策表(99)或候选表未建。")
|
||
raw_rows = []
|
||
if table_exists("example_raw_lease"):
|
||
raw_cols, raw_data = q(
|
||
"SELECT l.id,l.purpose,l.status,count(c.id),max(c.create_time) "
|
||
"FROM example_raw_lease l LEFT JOIN example_raw_content c ON c.lease_id=l.id "
|
||
"WHERE l.run_id=%s GROUP BY l.id,l.purpose,l.status ORDER BY l.id",
|
||
(run_id,),
|
||
)
|
||
raw_rows = [dict(zip(raw_cols, row)) for row in raw_data]
|
||
raw_html = (
|
||
"<div class='card'><div class='hd'>原始证据(本运行)</div><table><thead>"
|
||
"<tr><th class='num'>租约编号 <code>lease_id</code></th><th>用途</th><th>状态</th><th class='num'>内容数</th><th>最近写入</th></tr></thead><tbody>"
|
||
+ "".join(
|
||
f"<tr><td class='num'><code>{esc(r['id'])}</code></td><td>{enum_text(r['purpose'], 'purpose')}</td><td>{enum_chip(r['status'], 'status')}</td>"
|
||
f"<td class='num'>{esc(r['count'])}</td><td>{esc(r['max'])}</td></tr>" for r in raw_rows
|
||
)
|
||
+ "</tbody></table></div>"
|
||
if raw_rows else empty_gap("原始证据 · 该运行暂无记录", "模型输入输出落库后在此显示。")
|
||
)
|
||
else:
|
||
raw_html = empty_gap("原始证据 · 待落库", "原始证据表尚未建立(编码:example_raw_lease)。")
|
||
raw_content = []
|
||
if table_exists("example_raw_content"):
|
||
raw_content_cols, raw_content_rows = q(
|
||
"SELECT id,kind,role,content_sha256,content,create_time FROM example_raw_content "
|
||
"WHERE run_id=%s ORDER BY create_time,id", (run_id,)
|
||
)
|
||
raw_content = _row_dicts(raw_content_cols, raw_content_rows)
|
||
raw_details = []
|
||
for index, content in enumerate(raw_content):
|
||
open_attr = " open" if index == len(raw_content) - 1 else ""
|
||
raw_details.append(
|
||
f"<details{open_attr}><summary>{_raw_kind_label(content['kind'])} · 内容编号 <code>{esc(content['id'])}</code> · "
|
||
f"{len(content['content'] or '')} 字 · 哈希 <code>{esc(content['content_sha256'])}</code></summary>"
|
||
f"<pre class='data-block'><code>{esc(_content_text(content['content']))}</code></pre></details>"
|
||
)
|
||
raw_detail_html = (
|
||
"<div class='card run-evidence'><div class='hd'>模型输入 / 输出正文</div>"
|
||
+ "".join(raw_details) + "</div>"
|
||
if raw_details else empty_gap("暂无模型输入输出", "原始内容落库后在此显示完整正文。")
|
||
)
|
||
if table_exists("example_context_freeze") and wid is not None and chap is not None:
|
||
_, freezes = q(
|
||
"SELECT id,as_of_chapter,manifest_sha256,context_sha256,token_budget,create_time "
|
||
"FROM example_context_freeze WHERE work_id=%s AND target_chapter=%s "
|
||
"ORDER BY create_time DESC",
|
||
(wid, chap),
|
||
)
|
||
freeze_html = (
|
||
"<div class='card'><div class='hd'>上下文冻结(匹配本作品/目标章)</div><table><thead>"
|
||
"<tr><th class='num'>冻结编号 <code>freeze_id</code></th><th class='num'>基线章 <code>as_of_chapter</code></th><th>来源清单哈希</th><th>上下文哈希</th><th class='num'>令牌预算</th><th>时间</th></tr></thead><tbody>"
|
||
+ "".join(
|
||
f"<tr><td class='num'>{esc(f[0])}</td><td class='num'>{esc(f[1])}</td>"
|
||
f"<td><code>{esc(f[2])}</code></td><td><code>{esc(f[3])}</code></td>"
|
||
f"<td class='num'>{esc(f[4])}</td><td>{esc(f[5])}</td></tr>" for f in freezes
|
||
)
|
||
+ "</tbody></table></div>"
|
||
if freezes else empty_gap("上下文冻结 · 该目标章暂无记录", "上下文装配后的冻结清单落库后在此显示。")
|
||
)
|
||
else:
|
||
freeze_html = empty_gap("上下文冻结 · 待落库", "冻结表尚未建立(编码:100),或该运行不是章级运行。")
|
||
|
||
first_receipt = rc[0] if rc else {}
|
||
first_candidate = candidates[0] if candidates else {}
|
||
first_raw = raw_rows[0] if raw_rows else {}
|
||
run_content = _run_description((
|
||
rid, wid, work_title, chap, trig, state, started, chapter_id, chapter_order, chapter_title,
|
||
first_receipt.get("adapter_role"), first_receipt.get("stage_kind"), first_candidate.get("run_type"),
|
||
first_candidate.get("source_role") or first_receipt.get("adapter_role"), first_raw.get("purpose"),
|
||
))
|
||
chapter_html = _chapter_text(chapter_id, chapter_order, chapter_title, link_work=wid)
|
||
info = (f"<div class='card'><table>"
|
||
f"<tr><td style='width:140px;color:var(--muted)'>{field_label('运行编号', 'run_id')}</td><td><code>{esc(rid)}</code></td></tr>"
|
||
f"<tr><td style='color:var(--muted)'>作品</td><td>{esc(work_title)} <code>work_id={esc(wid)}</code></td></tr>"
|
||
f"<tr><td style='color:var(--muted)'>运行内容</td><td>{run_content}</td></tr>"
|
||
f"<tr><td style='color:var(--muted)'>目标章</td><td>{chapter_html}</td></tr>"
|
||
f"<tr><td style='color:var(--muted)'>触发来源</td><td>{display_trigger(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_html = ("<div class='card'><div class='hd'>回执链(按样本 · 修订链 <code>revision</code>)</div><table><thead>"
|
||
"<tr><th>样本编号</th><th class='num'>修订号 <code>revision</code></th><th>阶段</th><th>角色</th><th class='num'>尝试次数 <code>attempt</code></th><th class='num'>补证次数 <code>loop_seq</code></th><th>候选版本</th><th>结果</th><th>诊断</th></tr></thead><tbody>"
|
||
+ "".join(
|
||
f"<tr><td><code>{esc(r['sample_id'])}</code></td><td class='num'>{esc(r['revision'])}</td>"
|
||
f"<td>{enum_text(r['stage_kind'], 'stage_kind')}</td><td>{enum_text(r['adapter_role'], 'source_role')}</td>"
|
||
f"<td class='num'>{esc(r['attempt'])}</td><td class='num'>{esc(r['loop_seq'])}</td><td><code>{esc(r['candidate_version'])}</code></td>"
|
||
f"<td>{chip('failed' if r['is_error'] else 'passed')}</td>"
|
||
f"<td>{_reason_text(r['stop_reason'])} / {_reason_text(r['terminal_reason'])}</td></tr>"
|
||
for r in rc)
|
||
+ "</tbody></table></div>") if rc else empty_gap("暂无回执", "该运行的回执落库后在此显示(修订链编码:revision)。")
|
||
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><th>详细信息</th></tr></thead><tbody>"
|
||
+ "".join(
|
||
f"<tr><td>{enum_chip(u['judge_kind'], 'judge_kind')}</td><td><code>{esc(u['dimension'])}</code></td><td><code>{esc(u['scale_version'])}</code></td>"
|
||
f"<td class='num'>{esc(u['score'])}</td><td>{enum_chip(u['conclusion'], 'conclusion')}</td><td><code>{esc(u['failure_class'])}</code></td>"
|
||
f"<td>{('<details><summary>查看</summary>' + _data_block(u['detail']) + '</details>') if u['detail'] else '—'}</td></tr>"
|
||
for u in qres)
|
||
+ "</tbody></table></div>") if qres else empty_gap("暂无质量评判", "")
|
||
call_html = ("<div class='card'><div class='hd'>模型调用</div><table><thead>"
|
||
"<tr><th>时间</th><th>调用方</th><th>请求模型编码</th><th>实际模型编码</th><th class='num'>输入令牌</th><th class='num'>缓存令牌</th><th class='num'>输出令牌</th><th>停止原因</th><th class='num'>耗时毫秒</th><th>原始内容</th></tr></thead><tbody>"
|
||
+ "".join(
|
||
f"<tr><td>{esc(c['call_time'])}</td><td>{_operator_text(c['caller'])}</td><td><code>{esc(c['requested_model_id'])}</code></td><td><code>{esc(c['actual_model_id'])}</code></td>"
|
||
f"<td class='num'>{fmt_num(c['in_tokens'])}</td><td class='num'>{fmt_num(c['cached_tokens'])}</td><td class='num'>{fmt_num(c['out_tokens'])}</td>"
|
||
f"<td>{_reason_text(c['stop_reason'])}</td><td class='num'>{fmt_num(c['duration_ms'])}</td><td><code>{esc(c['raw_content_id'])}</code></td></tr>"
|
||
for c in calls)
|
||
+ "</tbody></table></div>") if calls else empty_gap("暂无模型调用", "")
|
||
|
||
error_items = []
|
||
for r in rc:
|
||
abnormal_stop = r["stop_reason"] not in (None, "", "completed", "tool_use")
|
||
abnormal_terminal = r["terminal_reason"] not in (None, "", "completed")
|
||
if r["is_error"] or abnormal_stop or abnormal_terminal:
|
||
summary = _data_block(r["safe_summary"]) if r["safe_summary"] else "—"
|
||
error_items.append(
|
||
f"<div class='error-item'><div class='label'>回执 · 样本 <code>{esc(r['sample_id'])}</code> · 修订 <code>{esc(r['revision'])}</code></div>"
|
||
f"<div class='value'>停止原因:{_reason_text(r['stop_reason'])} · 终态原因:{_reason_text(r['terminal_reason'])}</div>"
|
||
f"<div class='value'>安全摘要:{summary}</div></div>"
|
||
)
|
||
for u in qres:
|
||
if u["failure_class"] or u["conclusion"] == "fail":
|
||
error_items.append(
|
||
f"<div class='error-item'><div class='label'>质量评判 · 类型 {enum_chip(u['judge_kind'], 'judge_kind')}</div>"
|
||
f"<div class='value'>失败类别:<code>{esc(u['failure_class'])}</code></div>"
|
||
f"<div class='value'>判定细节:{_data_block(u['detail']) if u['detail'] else '—'}</div></div>"
|
||
)
|
||
if state == "failed" and not error_items:
|
||
error_items.append("<div class='error-item'><div class='value'>运行终态为失败,但回执和质量表没有提供更细错误记录。</div></div>")
|
||
error_html = ("<div class='error-panel'><div class='hd'>错误与诊断</div>" + "".join(error_items) + "</div>") if error_items else ""
|
||
body = (f"<div class='crumb'><a href='/runs'>运行记录</a> / <code>{esc(rid)}</code></div>"
|
||
f"<h1>运行详情</h1>{info}{error_html}"
|
||
f"<h2>回执链</h2>{rc_html}<h2>质量评判</h2>{q_html}<h2>模型调用</h2>{call_html}"
|
||
f"<h2>候选与决策</h2>{cand}{dec}<h2>证据与上下文</h2>{raw_html}{raw_detail_html}{freeze_html}")
|
||
return layout("运行详情", body, "/runs")
|
||
|
||
|
||
# ── 智能体(07:读 102 登记表 example_agent_role / example_skill;权威在 Git 侧,登记脚本同步)────
|
||
def _fmt_list(v):
|
||
"""JSONB 列表 → 技术编码逗号串;非列表原样转义。"""
|
||
if isinstance(v, list):
|
||
return ", ".join(f"<code>{esc(x)}</code>" for x in v) if v else '<span style="color:var(--muted)">∅</span>'
|
||
return esc(v)
|
||
|
||
|
||
_TOOL_LABELS = {
|
||
"Read": "读取", "Write": "写入", "Grep": "文本搜索", "Glob": "文件匹配",
|
||
}
|
||
|
||
|
||
def _fmt_tools(v):
|
||
"""工具名称使用中文语义,CLI 工具名作为编码保留。"""
|
||
if not isinstance(v, list):
|
||
return esc(v)
|
||
if not v:
|
||
return '<span style="color:var(--muted)">∅</span>'
|
||
return ", ".join(
|
||
f"{html.escape(_TOOL_LABELS.get(str(item), '未登记'))} "
|
||
f"<span class='enum-code'><code>{esc(item)}</code></span>"
|
||
for item in 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 _ENUM_LABELS['agent_role'].get(r[0], '未登记'))}</a> "
|
||
f"<span class='enum-code'><code>{esc(r[0])}</code></span></td>"
|
||
f"<td><code>{esc(r[2])}</code></td><td>{_fmt_tools(r[3])}</td></tr>" for r in roles)
|
||
skill_rows = "".join(
|
||
f"<tr><td><a href='/agents/skill/{esc(s[0])}'>{enum_text(s[0], 'skill_name')}</a></td>"
|
||
f"<td>{annotated_text(short_text(s[1]))}</td><td><code>{esc(s[2])}</code></td></tr>" for s in skills)
|
||
body = (f"<h1>智能体<span class='sub'>角色与技能 · 读取登记表(表编码 <code>example_agent_role</code> / <code>example_skill</code>)· 智能体 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 _ENUM_LABELS['agent_role'].get(role_, '未登记'))} <span class='enum-code'><code>{esc(role_)}</code></span></div>"
|
||
f"<table>"
|
||
f"<tr><td style='width:120px;color:var(--muted)'>模型归属</td><td><code>{esc(model)}</code></td></tr>"
|
||
f"<tr><td style='color:var(--muted)'>工具</td><td>{_fmt_tools(tools)}</td></tr>"
|
||
f"<tr><td style='color:var(--muted)'>可配置项</td><td>工作强度 <code>effort</code>:<code>{esc(effort)}</code> · 预算:<code>{esc(budget)}</code></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>{annotated_text(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>运行编号 <code>run_id</code></th><th>阶段</th><th class='num'>修订号 <code>revision</code></th><th>时间</th></tr></thead><tbody>"
|
||
+ "".join(f"<tr><td><code>{esc(r[0])}</code></td><td>{enum_text(r[1], 'stage_kind')}</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'>输出令牌数</th><th class='num'>成本(美元)</th></tr></thead><tbody>"
|
||
+ "".join(f"<tr><td>{esc(c[0])}</td><td><code>{esc(c[1])}</code></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>{enum_text(u[0], 'judge_kind')}</td><td><code>{esc(u[1])}</code></td><td>{enum_chip(u[2], 'conclusion')}</td><td><code>{esc(u[3])}</code></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 _ENUM_LABELS['agent_role'].get(role_, '未登记'))}</div>"
|
||
f"<h1>{esc(disp or _ENUM_LABELS['agent_role'].get(role_, '未登记'))}</h1>{info}<h2>运行与产出</h2>{runs_html}{quals_html}{calls_html}")
|
||
return layout(f"角色 · {disp or _ENUM_LABELS['agent_role'].get(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]
|
||
skill_title = _ENUM_LABELS['skill_name'].get(sn, '未登记')
|
||
model_html = f"<code>{esc(model_used)}</code>" if model_used else "确定性工具(不用模型)"
|
||
body = (f"<div class='crumb'><a href='/agents'>智能体</a> / 技能 / {esc(skill_title)}</div>"
|
||
f"<h1>{esc(skill_title)} <span class='enum-code'><code>{esc(sn)}</code></span></h1>"
|
||
f"<div class='card'><table>"
|
||
f"<tr><td style='width:120px;color:var(--muted)'>用途</td><td>{annotated_text(purpose)}</td></tr>"
|
||
f"<tr><td style='color:var(--muted)'>给谁用</td><td>{annotated_text(consumer) if consumer else '<span style=\"color:var(--muted)\">待补</span>'}</td></tr>"
|
||
f"<tr><td style='color:var(--muted)'>用模型</td><td>{model_html}</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>{annotated_text(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"技能 · {skill_title}", 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)), p)),
|
||
(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(p)),
|
||
(re.compile(r"^/ai-flavor$"), lambda m, p: view_ai_flavor(p)),
|
||
(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()
|