zizi 091b66a9bb 重构: 收敛 Agent/Skill 运行时与创作质量闭环
将角色与 Skill 从 .claude 迁入 .agent,移除 Claude CLI 运行时并接入固定 Opus 角色 profile、完整 schema、预算 deadline、raw 与回执证据链。

同步拆分 Skill 职责、复利 lesson、Gate 回放、Dashboard 人审入口、数据库登记和机械门禁;候选设计正文不包含在本提交中。
2026-08-22 02:12:32 +08:00

3063 lines
173 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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

#!/usr/bin/env python3
"""只读可视化看板(muse-example 库)—— 纯 Python 标准库 HTTP 服务。
合同:.agent/docs/architecture/可视化模块合同.md(唯一 SoT)。
布局:对齐 muse 的领域布局(作品 / 知识库 / 智能体 三空间 + 总览),
与领域索引(domains/_index.md)的领域分区一致——看板按领域看库,不按功能切。
命根子(验收按机械门查):
1. 连接只读:会话级 default_transaction_read_only=on,任何写语句数据库直接报错。
2. 代码无写语句:全文件只允许 SELECT,无 INSERT/UPDATE/DELETE/DDL(grep 即门)。
3. 不接会写的通道:不调用 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
# 生产编排 step2 的本地留档目录(只读;不进库的 pipeline/诊断摘要在此)
WRITE_CHAPTER_ARTIFACTS = Path(__file__).resolve().parents[1] / "docs" / "write-chapter" / "artifacts"
# 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", "proposed": "warn",
"reviewing": "warn",
"confirmed": "good", "passed": "good", "done": "good", "parsed": "good",
"completed": "good", "accepted": "good", "active": "good", "canonical": "good",
"published": "good", "promoted": "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": "已归档",
"proposed": "待评审", "reviewing": "评审中", "promoted": "已升格",
"lesson": "教训", "win": "成功",
},
"lesson_kind": {"lesson": "教训", "win": "成功"},
"promotion_target_type": {"pattern": "范式", "skill": "技能", "tool": "工具"},
"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": "访问数据库", "adjudicate-quality-gate": "裁决质量门",
"assemble-context": "组装上下文", "backup-work-extraction": "备份作品抽取",
"call-content-model": "调用内容模型", "check-content-consistency": "检查内容一致性",
"clean-book-text": "清洗书稿正文", "confirm-knowledge-draft": "确认知识草稿",
"decide-candidate": "处理候选决策",
"deconstruct-book": "拆解完整作品", "design-story-foundation": "设计作品前期",
"embed-knowledge": "嵌入知识", "evaluate-frozen-replay": "评估冻结回放",
"execute-role-task": "执行角色任务", "expand-scene": "扩写场景",
"extract-chapter-knowledge": "抽取章节知识", "extract-work-knowledge": "抽取作品知识",
"freeze-context": "冻结上下文", "import-book": "导入书稿",
"inspect-parse-health": "体检拆书终态",
"load-replay-reference-work": "装配回放参考作品",
"merge-story-candidates": "统合前期候选",
"optimize-content-quality": "质量归因建议",
"plan-chapter": "规划章节", "plan-story": "规划作品", "polish-prose": "润色正文",
"promote-ai-flavor-rule": "升格 AI 味规则",
"record-run-evidence": "记录运行证据", "refresh-runtime-probe": "刷新运行探针",
"repair-work-extraction": "修复作品抽取", "replay-writer-gate": "执行正文回放",
"reset-work-extraction": "重置作品抽取", "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"),
("/candidates", "待确认候选", "流程 05 · 人闸"),
("/knowledge", "知识库", "实体 02 · 范式 03"),
("/ai-flavor", "AI 味案例", "质量 06 · 数据库证据"),
("/lessons", "经验登记", "质量 06 · 复利"),
("/agents", "智能体", "智能体 07"),
("/runs", "运行记录", "流程 05 · 质量 06"),
]
# 人审写库不在本只读看板;确认通道默认端口(见 dashboard/lesson_confirm.py)。
LESSON_CONFIRM_BASE = "http://127.0.0.1:8766"
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"),
("投影登记", "example_projection_run"),
("经验升格", "example_lesson"),
]
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_candidates(params):
"""待确认候选:产生候选后须人确认才能入库正式正文并进入下一步(05 §2)。"""
if not table_exists("example_candidate"):
return layout(
"待确认候选",
"<h1>待确认候选</h1>" + empty_gap("候选表未建", "example_candidate 尚未落库"),
"/candidates",
)
gate = (
"<div class='card' style='margin-bottom:14px'><div class='hd'>人闸</div>"
"<p style='padding:12px 16px;margin:0'>流程合同:<b>产出候选 → 你确认 → 才入库正式正文 → 才进入下一步</b>。"
"本页只读。Shadow 候选行可先出现在库里供预览,<b>不等于</b>已入库正式正文;"
"确认/丢弃仍走 <code>decide-candidate</code>(主会话在你明确说采纳/丢弃后执行)。"
"未确认不得自动抽卡确认、不得当 Canonical。</p></div>"
)
_, rows = q(
"SELECT c.id, c.work_id, w.title, c.target_chapter, c.candidate_version, c.run_id, "
"c.run_type, c.state, c.semantic_status, c.acceptance_eligible, c.create_time "
"FROM example_candidate c "
"LEFT JOIN muse_content_work w ON w.id=c.work_id "
"WHERE c.deleted=false AND c.state IN ('draft','checking','passed') "
"ORDER BY c.create_time DESC LIMIT 50"
)
if not rows:
body = (
f"<h1>待确认候选<span class='sub'>正式入库前的人闸 · 流程 05</span></h1>{gate}"
+ empty_gap("当前没有待确认候选", "生产链写出 Shadow 候选后会出现在此。")
)
return layout("待确认候选", body, "/candidates")
trs = "".join(
f"<tr><td class='num'><a href='/candidates/{r[0]}'>{esc(r[0])}</a></td>"
f"<td><a href='/works/{r[1]}'>{esc(r[2] or r[1])}</a></td>"
f"<td class='num'><a href='/works/{r[1]}/ch/{r[3]}'>第{esc(r[3])}章</a></td>"
f"<td><code>{esc(r[4])}</code></td>"
f"<td>{(f'<a href=\"/runs/{esc(r[5])}\"><code>{esc(r[5])}</code></a>') if r[5] else '—'}</td>"
f"<td>{enum_text(r[6], 'run_type')}</td>"
f"<td>{chip(r[7])}</td>"
f"<td>{chip(r[8]) if r[8] else '—'}</td>"
f"<td>{'是' if r[9] else '否'}</td>"
f"<td>{esc(r[10])}</td></tr>"
for r in rows
)
body = (
f"<h1>待确认候选<span class='sub'>最近 50 · 点编号看正文与检测理由</span></h1>{gate}"
f"<div class='card'><table><thead><tr>"
f"<th class='num'>候选 id</th><th>作品</th><th class='num'>章</th><th>版本</th>"
f"<th>运行</th><th>运行类型</th><th>状态</th><th>语义</th><th>可接受</th><th>时间</th>"
f"</tr></thead><tbody>{trs}</tbody></table></div>"
f"<p class='note'>点候选编号进入详情:可读全文 + 语义失败原因,再决定丢弃或修后重产。"
f"状态 <code>passed</code> 且语义通过才允许 accept。</p>"
)
return layout("待确认候选", body, "/candidates")
def _candidate_semantic_findings(candidate_sha256: str | None, run_id: str | None):
"""从质量账本取语义失败 findings(只读)。"""
if not table_exists("example_quality_result"):
return []
clauses = ["judge_kind='detection'", "conclusion='fail'"]
params: list = []
if candidate_sha256:
clauses.append("candidate_sha256=%s")
params.append(candidate_sha256)
elif run_id:
clauses.append("run_id=%s")
params.append(run_id)
else:
return []
_, rows = q(
"SELECT detail, failure_class FROM example_quality_result "
f"WHERE {' AND '.join(clauses)} ORDER BY id DESC LIMIT 3",
tuple(params),
)
findings = []
for detail, failure_class in rows:
if isinstance(detail, str):
try:
detail = json.loads(detail)
except json.JSONDecodeError:
detail = {}
if not isinstance(detail, dict):
continue
for item in detail.get("findings") or []:
if isinstance(item, dict):
findings.append({**item, "_failure_class": failure_class})
if not findings and failure_class:
findings.append({"message": f"语义审查失败:{failure_class}", "severity": "high"})
return findings
def _candidate_generation_summary(run_id: str | None) -> str:
"""候选详情:本次生产链用了哪些环节、writer 吃了什么输入(只读摘要)。"""
if not run_id:
return ("<div class='card'><div class='hd'>本次生成</div>"
"<p class='note' style='padding:12px 16px;margin:0'>无 run_id,无法回溯技能与输入。</p></div>")
# 生产编排固定链路(库不存 skill 名;按编排合同展示)
pipeline_skills = [
("assemble-context", "组装上下文 / 检索基线正文与细纲"),
("diagnose-ai-flavor", "人感前置预防合同(若本轮启用)"),
("write-next-chapter", "writer 真调 + 机械门 + 语义 detector 编排"),
("call-content-model", "底层模型调用落库(caller=writer / semantic_detector)"),
("record-run-evidence / persist", "候选 + 回执 + 质量账本落库"),
]
skill_lis = "".join(
f"<li><code>{esc(name)}</code> — {esc(desc)}</li>" for name, desc in pipeline_skills
)
callers = []
if table_exists("example_llm_call"):
_, call_rows = q(
"SELECT caller, actual_model_id, count(*)::int, "
"coalesce(sum(cost_usd),0), coalesce(sum(out_tokens),0)::bigint "
"FROM example_llm_call WHERE run_id=%s "
"GROUP BY caller, actual_model_id ORDER BY min(id)",
(run_id,),
)
callers = call_rows
call_html = ""
if callers:
trs = "".join(
f"<tr><td><code>{esc(r[0])}</code></td><td><code>{esc(r[1])}</code></td>"
f"<td class='num'>{esc(r[2])}</td><td class='num'>{fmt_num(r[3])}</td>"
f"<td class='num'>{fmt_num(r[4])}</td></tr>"
for r in callers
)
call_html = (
"<p class='note' style='margin:8px 0 4px'>本 run 实际模型调用(库内事实):</p>"
"<table><thead><tr><th>调用方</th><th>模型</th><th class='num'>次数</th>"
"<th class='num'>花费$</th><th class='num'>输出 token</th></tr></thead>"
f"<tbody>{trs}</tbody></table>"
)
else:
call_html = "<p class='note'>本 run 无 example_llm_call 记录。</p>"
# writer prompt → 人看得见的输入摘要
input_bits = []
if table_exists("example_raw_content") and table_exists("example_raw_lease"):
_, prompt_rows = q(
"SELECT c.content FROM example_raw_content c "
"JOIN example_raw_lease l ON l.id=c.lease_id "
"WHERE l.run_id=%s AND c.kind='prompt' AND c.role='writer' "
"ORDER BY c.id LIMIT 1",
(run_id,),
)
if prompt_rows:
raw = prompt_rows[0][0]
try:
payload = json.loads(raw) if isinstance(raw, str) else raw
except (json.JSONDecodeError, TypeError):
payload = {}
bi = payload.get("businessInput") if isinstance(payload, dict) else {}
if not isinstance(bi, dict):
bi = {}
fo = bi.get("fineOutline") if isinstance(bi.get("fineOutline"), dict) else {}
hard = fo.get("hardConstraints") or []
if hard:
items = "".join(f"<li>{esc(h)}</li>" for h in hard)
input_bits.append(
"<details open><summary>细纲硬约束(writer 必跟;勿原句抄进正文)</summary>"
f"<ol style='margin:8px 0 0;padding-left:22px'>{items}</ol></details>"
)
style = bi.get("styleConstraints") or []
if style:
items = "".join(f"<li>{esc(s)}</li>" for s in style)
input_bits.append(
f"<details><summary>文风约束</summary><ul style='margin:8px 0 0'>{items}</ul></details>"
)
facts = bi.get("factConstraints") or fo.get("declaredNewFacts") or []
if facts:
texts = []
for f in facts:
if isinstance(f, dict):
texts.append(f.get("text") or f.get("constraintId") or str(f))
else:
texts.append(str(f))
items = "".join(f"<li>{esc(t)}</li>" for t in texts)
input_bits.append(
f"<details><summary>事实约束 / 本章可新增</summary>"
f"<ul style='margin:8px 0 0'>{items}</ul></details>"
)
excerpts = bi.get("proseExcerpts") or []
if excerpts:
lines = []
for p in excerpts:
if not isinstance(p, dict):
continue
lines.append(
f"第{esc(p.get('chapter'))}章 · {esc(p.get('purpose'))} · "
f"{len(p.get('text') or '')} 字 · <code>{esc(p.get('excerptId'))}</code>"
)
input_bits.append(
"<details><summary>基线正文摘录</summary>"
+ "<ul style='margin:8px 0 0'>"
+ "".join(f"<li>{x}</li>" for x in lines)
+ "</ul></details>"
)
pats = bi.get("patternReferences") or []
input_bits.append(
f"<p class='note' style='margin:8px 0 0'>范式引用:{'空(本轮未注入)' if not pats else esc(str(len(pats)) + ' 条')}</p>"
)
ep = payload.get("executionProfile") if isinstance(payload, dict) else {}
if isinstance(ep, dict) and ep.get("systemPrompt"):
sp = str(ep["systemPrompt"])
input_bits.append(
"<details><summary>writer systemPrompt(节选)</summary>"
f"<pre style='white-space:pre-wrap;max-height:240px;overflow:auto;margin:8px 0 0'>"
f"{esc(sp[:1200])}{'…' if len(sp) > 1200 else ''}</pre></details>"
)
if not input_bits:
input_bits.append(
"<p class='note'>库内无 writer prompt 原文;打开运行详情核对原始全文。</p>"
)
run_href = f"<a href='/runs/{esc(run_id)}'><code>{esc(run_id)}</code></a>"
return (
"<div class='card'><div class='hd'>本次生成 · 技能与输入</div>"
"<div style='padding:12px 16px'>"
f"<p class='note' style='margin:0 0 8px'>完整模型输入/输出在运行页:{run_href}(展开「原始全文」)。"
"看板只读摘要;不代替你读正文。</p>"
f"<p style='margin:0 0 4px'><b>编排链路(Skill)</b></p><ul style='margin:0 0 12px'>{skill_lis}</ul>"
f"{call_html}"
f"<p style='margin:12px 0 4px'><b>喂给 writer 的关键输入</b></p>"
+ "".join(input_bits)
+ "</div></div>"
)
def view_candidate_detail(candidate_id: int):
"""候选详情:正文 + 检测理由,供人判断能否入库。"""
if not table_exists("example_candidate"):
return layout("待确认候选", "<h1>候选表未建</h1>", "/candidates")
_, rows = q(
"SELECT c.id, c.work_id, w.title, c.target_chapter, c.candidate_version, c.run_id, "
"c.run_type, c.state, c.semantic_status, c.acceptance_eligible, c.candidate_sha256, "
"c.candidate_body, c.create_time, c.mode, c.source_role "
"FROM example_candidate c LEFT JOIN muse_content_work w ON w.id=c.work_id "
"WHERE c.id=%s AND c.deleted=false",
(candidate_id,),
)
if not rows:
return layout("待确认候选", "<h1>候选不存在</h1>", "/candidates")
(cid, wid, wtitle, chap, ver, run_id, run_type, state, sem, eligible,
csha, body_text, created, mode, source_role) = rows[0]
can_accept = (state == "passed" and sem == "passed" and eligible and run_type == "production")
if can_accept:
verdict = (
"<div class='card' style='border-color:var(--good,#2a7)'><div class='hd'>判断</div>"
f"<p style='padding:12px 16px;margin:0'><b>可以考虑采纳入库</b>(机械态 passed + 语义 passed)。"
f"满意则选下方「采纳」;不满意选「改」。</p></div>"
)
elif sem == "failed":
verdict = (
"<div class='card' style='border-color:var(--serious,#a40)'><div class='hd'>判断</div>"
f"<p style='padding:12px 16px;margin:0'><b>现在不能入库</b>:语义审查失败,accept 会被拒绝。"
f"请看失败理由后选「改」或「丢弃」(候选 #{esc(cid)})。</p></div>"
)
else:
verdict = (
"<div class='card'><div class='hd'>判断</div>"
f"<p style='padding:12px 16px;margin:0'>当前不可直接入库"
f"(state={esc(state)} · 语义={esc(sem)} · 可接受={eligible} · 类型={esc(run_type)})。</p></div>"
)
# 决策菜单:看板只读,真实执行在主会话(05 §2.3)
accept_note = (
"入库正式正文,并授权下游抽卡/案例草稿自动生成(转正式仍须你确认)"
if can_accept else "当前门未过,说了也会被拒"
)
menu_html = (
"<div class='card'><div class='hd'>决策菜单(在对话里说一句)</div>"
"<ol style='padding:12px 16px 12px 36px;margin:0'>"
f"<li><b>改:…</b> — 不入库;下一轮把你的原话写入人指令槽,同序重产。"
f"例:「改:去掉抽象三连,门符号只能在舰队医疗舱」</li>"
f"<li><b>丢弃候选 {esc(cid)}</b> — 正式正文不变,关掉这条 Shadow</li>"
f"<li><b>采纳候选 {esc(cid)}</b> — {esc(accept_note)}</li>"
"</ol>"
"<p class='note' style='padding:0 16px 12px;margin:0'>看板只读;主会话执行 decide-candidate / 重产编排。</p></div>"
)
findings = _candidate_semantic_findings(csha, run_id)
if findings:
items = []
for f in findings:
sev = f.get("severity") or "—"
quote = f.get("candidateQuote") or ""
msg = f.get("message") or ""
items.append(
f"<li><b>[{esc(sev)}]</b> {esc(msg)}"
+ (f"<div class='note'>引文:{esc(quote)}</div>" if quote else "")
+ "</li>"
)
findings_html = (
"<div class='card'><div class='hd'>语义审查发现</div>"
f"<ol style='padding:12px 16px 12px 36px;margin:0'>{''.join(items)}</ol></div>"
)
else:
findings_html = (
"<div class='card'><div class='hd'>语义审查发现</div>"
"<p style='padding:12px 16px;margin:0' class='note'>库内无失败 findings;"
"若语义仍标 failed,请打开运行详情核对。</p></div>"
)
body_html = (
f"<div class='card'><div class='hd'>候选正文</div>"
f"<pre style='white-space:pre-wrap;padding:14px 16px;margin:0;max-height:70vh;overflow:auto'>"
f"{esc(body_text or '(空)')}</pre></div>"
)
meta = (
f"<div class='card'><table>"
f"<tr><td style='width:120px;color:var(--muted)'>作品</td>"
f"<td><a href='/works/{wid}'>{esc(wtitle or wid)}</a> · "
f"<a href='/works/{wid}/ch/{chap}'>第{esc(chap)}章</a></td></tr>"
f"<tr><td style='color:var(--muted)'>运行</td>"
f"<td>{(f'<a href=\"/runs/{esc(run_id)}\"><code>{esc(run_id)}</code></a>') if run_id else '—'}</td></tr>"
f"<tr><td style='color:var(--muted)'>状态</td><td>{chip(state)} · 语义 {chip(sem) if sem else '—'} · "
f"可接受={'是' if eligible else '否'}</td></tr>"
f"<tr><td style='color:var(--muted)'>版本/模式</td><td><code>{esc(ver)}</code> · {esc(mode)} · "
f"{enum_text(source_role, 'source_role')}</td></tr>"
f"<tr><td style='color:var(--muted)'>时间</td><td>{esc(created)}</td></tr>"
f"</table></div>"
)
gen_html = _candidate_generation_summary(run_id)
page = (
f"<div class='crumb'><a href='/candidates'>待确认候选</a> / #{esc(cid)}</div>"
f"<h1>候选 #{esc(cid)}<span class='sub'>读正文、输入与检测理由后再决定</span></h1>"
f"{verdict}{menu_html}{meta}{gen_html}{findings_html}{body_html}"
)
return layout(f"候选 #{cid}", page, "/candidates")
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}"
f"<h2>待审候选</h2><p class='note'>完整人闸说明见 <a href='/candidates'>待确认候选</a>。</p>{cand}"
f"<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 _artifact_json(run_id: str, suffix: str):
"""读 step2 留档 JSON;不存在或损坏返回 None(看板不得因此 500)。"""
path = WRITE_CHAPTER_ARTIFACTS / f"{run_id}-{suffix}"
if not path.is_file():
return None
try:
return json.loads(path.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError, TypeError):
return None
def _latest_artifact_glob(run_id: str, pattern: str):
"""同前缀多版本时取修改时间最新的一份(如 diagnosis-v4)。"""
matches = sorted(
WRITE_CHAPTER_ARTIFACTS.glob(f"{run_id}-{pattern}"),
key=lambda p: p.stat().st_mtime,
reverse=True,
)
for path in matches:
try:
return json.loads(path.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError, TypeError):
continue
return None
def _run_pipeline_panel(run_id: str) -> str:
"""pipeline 失败码 + 证据缺口(读 artifacts,只读)。"""
pipeline = _artifact_json(run_id, "pipeline-result.json")
semantic = _latest_artifact_glob(run_id, "semantic-output-v*.json") or _artifact_json(
run_id, "semantic-output.json"
)
if pipeline is None and semantic is None:
return (
"<div class='card'><div class='hd'>Pipeline 终态 · 证据缺口</div>"
"<p class='note' style='padding:12px 16px;margin:0'>本机 artifacts 无该 run 的 "
"<code>pipeline-result</code> / <code>semantic-output</code>;"
"库内终态见上方。语义缺口会触发补证重组装(有限环);仍失败则停在此层。</p></div>"
)
status = (pipeline or {}).get("status") or "—"
fail = (pipeline or {}).get("failureCode") or "—"
evidence_n = (pipeline or {}).get("evidenceRequestCount")
rewrite_n = (pipeline or {}).get("rewriteCount")
ver = (pipeline or {}).get("candidateVersion")
report = (semantic or {}).get("report") if isinstance(semantic, dict) else None
if not isinstance(report, dict):
report = {}
gaps = report.get("evidenceGaps") if isinstance(report.get("evidenceGaps"), list) else []
settings = report.get("newSettingCandidates") if isinstance(report.get("newSettingCandidates"), list) else []
gap_lis = []
for g in gaps:
if not isinstance(g, dict):
continue
gap_lis.append(
f"<li><b><code>{esc(g.get('gapId'))}</code></b> · 优先 {esc(g.get('priority') or '—')}"
f"<div class='note'>查询:{esc(g.get('query') or '—')}</div>"
f"<div class='note'>理由:{esc(g.get('reason') or '—')}</div>"
+ (f"<div class='note'>引文:{esc(g.get('candidateQuote'))}</div>" if g.get("candidateQuote") else "")
+ "</li>"
)
gap_html = (
f"<ol style='margin:8px 0 0;padding-left:22px'>{''.join(gap_lis)}</ol>"
if gap_lis
else "<p class='note' style='margin:8px 0 0'>无 evidenceGaps 清单"
+ (f"(pipeline 记 evidenceRequestCount={esc(evidence_n)})" if evidence_n not in (None, 0) else "")
+ "。</p>"
)
setting_lis = []
for item in settings:
if not isinstance(item, dict):
continue
setting_lis.append(
f"<li><b><code>{esc(item.get('settingId'))}</code></b> · {esc(item.get('factType') or '—')}"
f"<div class='note'>{esc(item.get('text') or '—')}</div>"
+ (f"<div class='note'>引文:{esc(item.get('candidateQuote'))}</div>" if item.get("candidateQuote") else "")
+ "</li>"
)
setting_html = (
f"<ol style='margin:8px 0 0;padding-left:22px'>{''.join(setting_lis)}</ol>"
if setting_lis
else "<p class='note' style='margin:8px 0 0'>无 newSettingCandidates。写手新设定应出现在这里,交人决定是否入库。</p>"
)
border = "border-color:var(--serious,#a40)" if status in ("REJECTED", "failed") or (
fail and fail != "—"
) else ""
return (
f"<div class='card' style='{border}'><div class='hd'>Pipeline 终态 · 证据缺口</div>"
"<div style='padding:12px 16px'>"
f"<p style='margin:0 0 8px'>状态 <code>{esc(status)}</code> · 失败码 <code>{esc(fail)}</code>"
f" · 候选版本 <code>{esc(ver)}</code> · 补证请求 {esc(evidence_n)} · 重写 {esc(rewrite_n)}</p>"
f"<p style='margin:0 0 4px'><b>证据缺口</b>({len(gap_lis)})</p>{gap_html}"
f"<p style='margin:12px 0 4px'><b>新设定提案</b>({len(setting_lis)})</p>{setting_html}"
"<p class='note' style='margin:12px 0 0'>来源:本地 <code>docs/write-chapter/artifacts/</code>。"
"对已有正典的缺口才走补证重写;新设定不禁写、不自动入库,由人决定采纳/改/丢弃。"
"仍失败关闭时可能<strong>不落候选行</strong>。</p>"
"</div></div>"
)
def _run_ai_flavor_panel(run_id: str) -> str:
"""AI 味诊断摘要(读 artifacts)。"""
diag = _latest_artifact_glob(run_id, "ai-flavor-diagnosis-v*.json")
if not isinstance(diag, dict):
return (
"<div class='card'><div class='hd'>AI 味诊断</div>"
"<p class='note' style='padding:12px 16px;margin:0'>无 diagnosis 留档。</p></div>"
)
findings = diag.get("findings") if isinstance(diag.get("findings"), list) else []
by_rule: Counter = Counter()
by_disp: Counter = Counter()
by_layer: Counter = Counter()
for item in findings:
if not isinstance(item, dict):
continue
by_rule[str(item.get("rule_id") or "?")] += 1
by_disp[str(item.get("default_disposition") or item.get("decision_proposal") or "?")] += 1
by_layer[str(item.get("layer") or "?")] += 1
top_rules = "".join(
f"<li><code>{esc(rid)}</code> × {esc(n)}</li>" for rid, n in by_rule.most_common(8)
) or "<li class='note'>无 finding</li>"
return (
"<div class='card'><div class='hd'>AI 味诊断摘要</div>"
"<div style='padding:12px 16px'>"
f"<p style='margin:0 0 8px'>发现 <b>{esc(len(findings))}</b> 条 · 模式 <code>{esc(diag.get('mode'))}</code>"
f" · 规则库 <code>{esc(diag.get('rule_library_version'))}</code></p>"
f"<p class='note' style='margin:0 0 4px'>处置:{esc(dict(by_disp))} · 层:{esc(dict(by_layer))}</p>"
f"<details><summary>按规则聚合(前 8)</summary><ul style='margin:8px 0 0'>{top_rules}</ul></details>"
"<p class='note' style='margin:8px 0 0'>只读摘要;改正文须你授权 revise / 改指令重产。</p>"
"</div></div>"
)
def _run_input_summary_panel(run_id: str) -> str:
"""七项输入摘要:优先 writer-context,回退 DB writer prompt。"""
ctx_file = _artifact_json(run_id, "writer-context.json")
context = None
if isinstance(ctx_file, dict):
context = ctx_file.get("context") if isinstance(ctx_file.get("context"), dict) else ctx_file
bi = None
if context is None and table_exists("example_raw_content") and table_exists("example_raw_lease"):
_, prompt_rows = q(
"SELECT c.content FROM example_raw_content c "
"JOIN example_raw_lease l ON l.id=c.lease_id "
"WHERE l.run_id=%s AND c.kind='prompt' AND c.role='writer' "
"ORDER BY c.id LIMIT 1",
(run_id,),
)
if prompt_rows:
try:
payload = json.loads(prompt_rows[0][0]) if isinstance(prompt_rows[0][0], str) else prompt_rows[0][0]
except (json.JSONDecodeError, TypeError):
payload = {}
if isinstance(payload, dict):
bi = payload.get("businessInput") if isinstance(payload.get("businessInput"), dict) else {}
if context is None and bi is None:
return (
"<div class='card'><div class='hd'>本轮输入摘要(七项)</div>"
"<p class='note' style='padding:12px 16px;margin:0'>无 writer-context / writer prompt。</p></div>"
)
def _from_context_or_bi(key, default=None):
if isinstance(context, dict) and key in context:
return context.get(key)
if isinstance(bi, dict):
return bi.get(key, default)
return default
fo = _from_context_or_bi("fineOutline") or {}
if not isinstance(fo, dict):
fo = {}
hard = fo.get("hardConstraints") or []
style = _from_context_or_bi("styleConstraints") or []
if not isinstance(style, list):
style = []
human_dirs = [s for s in style if isinstance(s, str) and s.startswith("本轮人指令:")]
style_only = [s for s in style if isinstance(s, str) and not s.startswith("本轮人指令:")]
pats = _from_context_or_bi("patternReferences") or []
excerpts = _from_context_or_bi("proseExcerpts") or _from_context_or_bi("recentChapters") or []
length = _from_context_or_bi("outputContract") or _from_context_or_bi("lengthContract") or {}
bits = [
f"<li><b>1 库事实</b> · run <code>{esc(run_id)}</code></li>",
f"<li><b>2 细纲硬约束</b> · {esc(len(hard))} 条"
+ (
"<details><summary>展开</summary><ol style='margin:8px 0 0'>"
+ "".join(f"<li>{esc(h if isinstance(h, str) else (h.get('text') if isinstance(h, dict) else h))}</li>" for h in hard)
+ "</ol></details>"
if hard
else ""
)
+ "</li>",
f"<li><b>3 前章基线</b> · {esc(len(excerpts) if isinstance(excerpts, list) else '—')} 段摘录</li>",
f"<li><b>4 文风</b> · {esc(len(style_only))} 条;范式 {esc(len(pats) if isinstance(pats, list) else 0)} 条"
+ (
"<ul style='margin:4px 0 0'>"
+ "".join(f"<li>{esc(s[:160])}{'…' if len(s) > 160 else ''}</li>" for s in style_only[:3])
+ "</ul>"
if style_only
else ""
)
+ "</li>",
"<li><b>5 prevent</b> · 见文风中「避免【…】」约束条(若有)</li>",
"<li><b>6 方法合同</b> · 自动装载待建</li>",
f"<li><b>7 本轮人指令</b> · "
+ (
esc(human_dirs[0][len("本轮人指令:") :])
if human_dirs
else "<span class='note'>空</span>"
)
+ "</li>",
]
if isinstance(length, dict) and length:
bits.append(
f"<li class='note'>篇幅合同 min={esc(length.get('minChars'))} "
f"max={esc(length.get('maxChars'))} target={esc(length.get('targetChars'))}</li>"
)
return (
"<div class='card'><div class='hd'>本轮输入摘要(七项 · 05 §2.3)</div>"
f"<ol style='padding:12px 16px 12px 36px;margin:0'>{''.join(bits)}</ol></div>"
)
def _run_decision_menu_panel(run_id: str, candidates: list, terminal_state, pipeline: dict | None) -> str:
"""决策菜单:有候选按候选态;无候选(失败关闭未落行)只给「改」重产命令。"""
pipe = pipeline or {}
fail = pipe.get("failureCode") or ""
gaps = []
semantic = _latest_artifact_glob(run_id, "semantic-output-v*.json")
# 语义阶段失败时 semantic-output-v*.json 是只有 ok/status/causes 的错误封套,
# 没有 report 键;缺 report 时按无缺口处理,不得让整页渲染崩掉。
semantic_report = semantic.get("report") if isinstance(semantic, dict) else None
if isinstance(semantic_report, dict):
gaps = semantic_report.get("evidenceGaps") or []
gap_hints = []
for g in gaps:
if isinstance(g, dict) and g.get("query"):
gap_hints.append(str(g["query"])[:80])
# 预填改指令:绕开不可证缺口(阶段 0.1 选 A 时用)
avoid = ";".join(f"避开「{h}」" for h in gap_hints[:2]) if gap_hints else "按语义/诊断理由修改"
rewrite_cmd = (
f".venv/bin/python docs/write-chapter/step2_write_chapter.py 3 "
f"--instruction \"改:{avoid}。禁止细纲原句抄进正文;门符号只在舰队医疗舱语境。\""
)
if candidates:
# 取最新一条活跃或任意
c = candidates[-1]
cid = c.get("id")
st = c.get("state")
can_accept = st == "passed"
return (
"<div class='card'><div class='hd'>决策菜单(对话执行 · 看板只读)</div>"
"<ol style='padding:12px 16px 12px 36px;margin:0'>"
f"<li><b>改:…</b> — 不入库;下一轮人指令回灌。可复制:<pre style='white-space:pre-wrap;margin:8px 0 0'>"
f"{esc(rewrite_cmd)}</pre></li>"
f"<li><b>丢弃候选 {esc(cid)}</b> — 正式正文不变</li>"
f"<li><b>采纳候选 {esc(cid)}</b> — "
+ ("入库并授权下游 shadow 草稿" if can_accept else "当前候选态不可 accept")
+ f"</li></ol>"
f"<p class='note' style='padding:0 16px 12px;margin:0'>候选详情 "
f"<a href='/candidates/{esc(cid)}'>#{esc(cid)}</a> · 状态 {chip(st)} · "
f"写库决策请用 <a href='http://127.0.0.1:8767/candidates/{esc(cid)}'>决策通道 :8767</a>"
f"(看板只读)</p></div>"
)
# 无候选行:本 run 失败关闭常见于 REASSEMBLED_CONTEXT_INVALID
return (
"<div class='card' style='border-color:var(--serious,#a40)'><div class='hd'>决策菜单</div>"
"<div style='padding:12px 16px'>"
f"<p style='margin:0 0 8px'><b>本运行未落候选行</b>(库终态 {chip(terminal_state)} · "
f"pipeline <code>{esc(fail or '—')}</code>)。无法采纳/丢弃;只能授权重产。</p>"
f"<p style='margin:0 0 4px'><b>改(重产)</b> — 在仓库根执行:</p>"
f"<pre style='white-space:pre-wrap;margin:0'>{esc(rewrite_cmd)}</pre>"
"<p class='note' style='margin:8px 0 0'>补证重组装已接线:重跑 step2 时语义缺口会自动补证再写。"
"人审采纳/丢弃请用 <a href='http://127.0.0.1:8767/'>决策通道 :8767</a>。"
"看板不执行写操作。</p>"
"</div></div>"
)
def _run_turn_panels_html(run_id: str, candidates: list, terminal_state) -> str:
"""运行页交互回合四块:pipeline/缺口、诊断、七项输入、决策菜单。"""
pipeline = _artifact_json(run_id, "pipeline-result.json")
return (
f"<h2>交互回合(本机 artifacts)</h2>"
f"{_run_pipeline_panel(run_id)}"
f"{_run_ai_flavor_panel(run_id)}"
f"{_run_input_summary_panel(run_id)}"
f"{_run_decision_menu_panel(run_id, candidates, terminal_state, pipeline)}"
)
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 ""
turn_html = _run_turn_panels_html(rid, candidates, state)
body = (f"<div class='crumb'><a href='/runs'>运行记录</a> / <code>{esc(rid)}</code></div>"
f"<h1>运行详情</h1>{info}{error_html}{turn_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")
# ── 经验登记(06:只读浏览 example_lesson;人审写库走确认通道 :8766)──────────
def view_lessons(params):
if not table_exists("example_lesson"):
return layout(
"经验登记",
"<h1>经验登记</h1>" + empty_gap("表未建", "example_lesson(DDL-108)尚未落库"),
"/lessons",
)
status = (params.get("status") or [""])[0].strip()
kind = (params.get("kind") or [""])[0].strip()
work_raw = (params.get("work") or [""])[0].strip()
clauses = ["l.deleted=false"]
args: list = []
if status in ("proposed", "reviewing", "promoted", "rejected"):
clauses.append("l.status=%s")
args.append(status)
if kind in ("lesson", "win"):
clauses.append("l.kind=%s")
args.append(kind)
if work_raw.isdigit():
clauses.append("l.work_id=%s")
args.append(int(work_raw))
where = " AND ".join(clauses)
_, rows = q(
"SELECT l.id, l.work_id, w.title, l.run_id, l.kind, l.title, l.status, l.creator, l.create_time "
"FROM example_lesson l LEFT JOIN muse_content_work w ON w.id=l.work_id "
f"WHERE {where} ORDER BY l.id DESC LIMIT 100",
tuple(args),
)
filters = (
"<form class='knowledge-filter' method='get' action='/lessons'>"
"<label>状态 <select name='status'><option value=''>全部</option>"
+ "".join(
f"<option value='{s}'{' selected' if status == s else ''}>{esc(_ENUM_LABELS['status'].get(s, s))}</option>"
for s in ("proposed", "reviewing", "promoted", "rejected")
)
+ "</select></label> "
"<label>类型 <select name='kind'><option value=''>全部</option>"
+ "".join(
f"<option value='{k}'{' selected' if kind == k else ''}>{esc(_ENUM_LABELS['lesson_kind'].get(k, k))}</option>"
for k in ("win", "lesson")
)
+ "</select></label> "
f"<label>作品 id <input name='work' value='{esc(work_raw)}' size='6'></label> "
"<button type='submit'>筛选</button> "
"<a class='clear' href='/lessons'>清除</a></form>"
)
if not rows:
body = (
f"<h1>经验登记<span class='sub'>表 <code>example_lesson</code> · 质量与复利 06 · 本页只读</span></h1>"
f"{filters}"
+ empty_gap("暂无经验登记", "创作链写入 propose_lesson 后会出现在此。")
+ f"<p class='note'>人审(review / promote / reject)不在本看板:"
f"<a href='{LESSON_CONFIRM_BASE}/'>打开确认通道</a>(会写库)。</p>"
)
return layout("经验登记", body, "/lessons")
trs = "".join(
f"<tr><td class='num'><a href='/lessons/{r[0]}'>{esc(r[0])}</a></td>"
f"<td>{chip(r[4], _ENUM_LABELS['lesson_kind'].get(r[4], r[4]))}</td>"
f"<td><a href='/lessons/{r[0]}'>{esc(r[5])}</a></td>"
f"<td>{chip(r[6])}</td>"
f"<td>{(f'<a href=\"/works/{r[1]}\">{esc(r[2] or r[1])}</a>') if r[1] else '—'}</td>"
f"<td>{(f'<a href=\"/runs/{esc(r[3])}\"><code>{esc(r[3])}</code></a>') if r[3] else '—'}</td>"
f"<td>{esc(r[7])}</td><td>{esc(r[8])}</td></tr>"
for r in rows
)
body = (
f"<h1>经验登记<span class='sub'>最近 100 条 · 只读浏览 · 人审走确认通道</span></h1>"
f"<p class='note'>经验登记 = 某次运行的 win/lesson 证据,不是正文。"
f"评审请用 <a href='{LESSON_CONFIRM_BASE}/'>确认通道 :8766</a>(会写库);"
f"本看板永不写库。</p>{filters}"
f"<div class='card'><table><thead><tr>"
f"<th class='num'>编号</th><th>类型</th><th>标题</th><th>状态</th>"
f"<th>作品</th><th>运行</th><th>登记方</th><th>时间</th>"
f"</tr></thead><tbody>{trs}</tbody></table></div>"
)
return layout("经验登记", body, "/lessons")
def view_lesson_detail(lesson_id: int):
if not table_exists("example_lesson"):
return layout("经验登记", "<h1>表未建</h1>", "/lessons")
_, rows = q(
"SELECT l.id, l.work_id, w.title, l.run_id, l.candidate_sha256, l.kind, l.title, "
"l.detail, l.status, l.promotion_target_type, l.promotion_target_ref, "
"l.decided_by, l.decision_rationale, l.creator, l.create_time, l.update_time "
"FROM example_lesson l LEFT JOIN muse_content_work w ON w.id=l.work_id "
"WHERE l.id=%s AND l.deleted=false",
(lesson_id,),
)
if not rows:
return layout("经验登记", "<h1>经验不存在</h1>", "/lessons")
(lid, work_id, work_title, run_id, csha, kind, title, detail, status,
target_type, target_ref, decided_by, rationale, creator, created, updated) = rows[0]
if isinstance(detail, str):
try:
detail = json.loads(detail)
except json.JSONDecodeError:
detail = {"raw": detail}
detail_html = f"<pre>{esc(json.dumps(detail, ensure_ascii=False, indent=2))}</pre>" if detail else "—"
work_html = (
f"<a href='/works/{work_id}'>{esc(work_title or work_id)}</a>" if work_id else "—"
)
run_html = (
f"<a href='/runs/{esc(run_id)}'><code>{esc(run_id)}</code></a>" if run_id else "—"
)
confirm_href = f"{LESSON_CONFIRM_BASE}/lessons/{lid}"
action_note = (
f"<div class='card' style='margin-top:14px'><div class='hd'>人审</div>"
f"<p style='padding:12px 16px;margin:0'>本页只读。要对这条经验 "
f"<code>review</code> / <code>promote</code> / <code>reject</code>,请打开确认通道:"
f"<a href='{esc(confirm_href)}'>{esc(confirm_href)}</a></p></div>"
)
body = (
f"<div class='crumb'><a href='/lessons'>经验登记</a> / #{esc(lid)}</div>"
f"<h1>{esc(title)}<span class='sub'>{chip(kind, _ENUM_LABELS['lesson_kind'].get(kind, kind))} "
f"{chip(status)}</span></h1>"
f"<div class='card'><table>"
f"<tr><td style='width:140px;color:var(--muted)'>编号</td><td class='num'>{esc(lid)}</td></tr>"
f"<tr><td style='color:var(--muted)'>作品</td><td>{work_html}</td></tr>"
f"<tr><td style='color:var(--muted)'>运行</td><td>{run_html}</td></tr>"
f"<tr><td style='color:var(--muted)'>候选哈希</td><td>"
f"{('<code>' + esc(csha) + '</code>') if csha else '—'}</td></tr>"
f"<tr><td style='color:var(--muted)'>登记方</td><td>{esc(creator)}</td></tr>"
f"<tr><td style='color:var(--muted)'>时间</td><td>{esc(created)} · 更新 {esc(updated)}</td></tr>"
f"<tr><td style='color:var(--muted)'>升格落点</td><td>"
+ (
f"{enum_text(target_type, 'promotion_target_type')} · <code>{esc(target_ref)}</code>"
if target_type else "—"
)
+ "</td></tr>"
f"<tr><td style='color:var(--muted)'>决定人</td><td>{esc(decided_by) or '—'}</td></tr>"
f"<tr><td style='color:var(--muted)'>理据</td><td>{esc(rationale) or '—'}</td></tr>"
f"<tr><td style='color:var(--muted)'>详情</td><td>{detail_html}</td></tr>"
f"</table></div>{action_note}"
)
return layout(f"经验 #{lid}", body, "/lessons")
# ── 路由 ─────────────────────────────────────────────────────────────────────
_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"^/candidates$"), lambda m, p: view_candidates(p)),
(re.compile(r"^/candidates/(\d+)$"), lambda m, p: view_candidate_detail(int(m.group(1)))),
(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"^/lessons$"), lambda m, p: view_lessons(p)),
(re.compile(r"^/lessons/(\d+)$"), lambda m, p: view_lesson_detail(int(m.group(1)))),
(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()