diff --git a/.agent/docs/architecture/可视化模块合同.md b/.agent/docs/architecture/可视化模块合同.md
index da6dd3a..0d5b6f5 100644
--- a/.agent/docs/architecture/可视化模块合同.md
+++ b/.agent/docs/architecture/可视化模块合同.md
@@ -89,7 +89,7 @@
1. 落库表分两类:**运行注册 / 运行回执 / 质量评判(98)与模型调用(97)已建**,但写路径未接通,视图暂空;候选 / 用户决策 / raw 全文 / 规划冻结(99/100/101)尚未建。没有它们 §4.2 的视图就是空的。
2. `db/ddl/96` 授权快照表**决定不启用**(领域索引 §9,单用户本地不做多租户授权),DDL 留存不 apply。
-3. 看板首版已建成(总览 / 作品 / 知识库 / 运行记录四空间,只读 HTTP + §4.1 部分视图 + 待落库占位格);待建 = 运行记录的分维列表与运行详情下钻、智能体空间(读 102 登记表)、作品/章工作区标签、沿来源下钻链。页面与信息架构见 §10。
+3. 看板首版已建成(总览 / 作品 / 知识库 / 智能体 / 运行记录五空间,只读 HTTP + §4.1 部分视图 + 待落库占位格;智能体空间读 102 登记表);待建 = 运行详情下钻、作品/章工作区标签、沿来源下钻链。页面与信息架构见 §10。
## 10. 页面与信息架构(产品形态 SoT)
@@ -196,7 +196,7 @@
**智能体页的 ⚠ 来源(A 方案,2026-07-30 拍板)**:角色画像 / 可配置项 / 技能合同在 Git 侧(`.claude/agents/`、`.claude/skills/`),库里没有。看板只读库,故新增**智能体/技能登记表**(`example_agent_role` + `example_skill`,设计见落库设计稿),把角色、模型归属、可配置项、技能读写合同**登记入库**——智能体配置也纳入留痕,看板只读这张表。
-**分阶段点亮(认)**:首版实现为四空间(总览 / 作品 / 知识库 / 运行记录;运行数据暂列运行记录空间,智能体空间待 102 登记表建好后上)。作品工作区**先上 [章节]**(正文已有),其余标签(大纲/细纲/设定/叙事状态、章工作区的细纲与出场卡)随对应表落库(规划表、候选表,落库设计稿第 3/2 步)逐格点亮;点亮前显示"待落库"占位,不假装已有。
+**分阶段点亮(认)**:首版实现为五空间(总览 / 作品 / 知识库 / 智能体 / 运行记录;智能体空间读 102 登记表 `example_agent_role`/`example_skill`,登记表由 `db/scripts/sync_agent_registry.py` 从 Git 侧同步,Git 仍是配置权威)。作品工作区**先上 [章节]**(正文已有),其余标签(大纲/细纲/设定/叙事状态、章工作区的细纲与出场卡)随对应表落库(规划表、候选表,落库设计稿第 3/2 步)逐格点亮;点亮前显示"待落库"占位,不假装已有。
### 10.2 UI 形态
diff --git a/dashboard/server.py b/dashboard/server.py
index de56117..57d0150 100644
--- a/dashboard/server.py
+++ b/dashboard/server.py
@@ -205,6 +205,7 @@ NAV = [
("/", "总览 · 健康", "数据权威 08"),
("/works", "作品", "作品领域 01"),
("/knowledge", "知识库", "实体 02 · 范式 03"),
+ ("/agents", "智能体", "Agent 07"),
("/runs", "运行记录", "流程 05 · 质量 06"),
]
@@ -499,6 +500,90 @@ def view_runs_page():
return layout("运行记录", body, "/runs")
+# ── 智能体(07:读 102 登记表 example_agent_role / example_skill;权威在 Git 侧,登记脚本同步)────
+def _fmt_list(v):
+ """JSONB 列表 → 逗号串;非列表原样转义。"""
+ if isinstance(v, list):
+ return esc(", ".join(str(x) for x in v)) if v else '∅'
+ return esc(v)
+
+
+def view_agents():
+ _, roles = q("SELECT role, display_name, model, tools FROM example_agent_role "
+ "WHERE deleted=false ORDER BY role")
+ _, skills = q("SELECT skill_name, purpose, model_used FROM example_skill "
+ "WHERE deleted=false ORDER BY skill_name")
+ role_rows = "".join(
+ f"
| {esc(r[1] or r[0])} "
+ f"{esc(r[0])} | "
+ f"{esc(r[2])} | {_fmt_list(r[3])} |
" for r in roles)
+ skill_rows = "".join(
+ f"| {esc(s[0])} | "
+ f"{esc((s[1] or '')[:56])} | {esc(s[2])} |
" for s in skills)
+ body = (f"智能体角色与技能 · 读 102 登记表(权威在 Git 侧,登记脚本同步)· Agent 07
"
+ f""
+ f"")
+ 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("角色", "角色不存在
", "/agents")
+ role_, disp, model, effort, budget, tools, resp, src, synced = rr[0]
+ info = (f"{esc(disp or role_)} {esc(role_)}
"
+ f"
"
+ f"| 模型归属 | {esc(model)} |
"
+ f"| 工具 | {_fmt_list(tools)} |
"
+ f"| 可配置项 | effort {esc(effort)} · 预算 {esc(budget)}(暂空待补) |
"
+ f"| 出处 | {esc(src)} · 同步于 {esc(synced)} |
"
+ f"| 职责 | {esc(resp)} |
"
+ f"
")
+ _, 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 = ("本角色运行(回执,最近 20)
"
+ "| run_id | 阶段 | revision | 时间 |
"
+ + "".join(f"{esc(r[0])} | {esc(r[1])} | {esc(r[2])} | {esc(r[3])} |
" for r in runs)
+ + "
") 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 = ("本角色模型调用(最近 20)
"
+ "| 时间 | 模型 | 输出token | 成本 |
"
+ + "".join(f"| {esc(c[0])} | {esc(c[1])} | {fmt_num(c[2])} | ${float(c[3] or 0):.5f} |
" for c in calls)
+ + "
") 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 = ("本角色质量评判(最近 20)
"
+ "| 类型 | 维度 | 结论 | 失败类别 |
"
+ + "".join(f"| {esc(u[0])} | {esc(u[1])} | {chip(u[2])} | {esc(u[3])} |
" for u in quals)
+ + "
") if quals else empty_gap("本角色暂无质量评判", "")
+ body = (f"智能体 / {esc(disp or role_)}
"
+ f"{esc(disp or role_)}
{info}运行与产出
{runs_html}{quals_html}{calls_html}")
+ return layout(f"角色 · {disp or role_}", body, "/agents")
+
+
+def view_skill(name):
+ _, ss = q("SELECT skill_name, purpose, consumer, reads, writes, red_lines, model_used, source_ref, synced_at "
+ "FROM example_skill WHERE skill_name=%s AND deleted=false", (name,))
+ if not ss:
+ return layout("技能", "技能不存在
", "/agents")
+ sn, purpose, consumer, reads, writes, red_lines, model_used, src, synced = ss[0]
+ body = (f""
+ f"{esc(sn)}
"
+ f""
+ f"| 用途 | {esc(purpose)} |
"
+ f"| 给谁用 | {esc(consumer) if consumer else '待补'} |
"
+ f"| 用模型 | {esc(model_used) if model_used else '确定性工具(不用模型)'} |
"
+ f"| 涉及表 | {_fmt_list(reads)} 自动抽取,读/写方向待人工核 |
"
+ f"| 红线 | {esc(red_lines) if red_lines else '待补'} |
"
+ f"| 出处 | {esc(src)} · 同步于 {esc(synced)} |
"
+ f"
")
+ return layout(f"技能 · {sn}", body, "/agents")
+
+
# ── 路由 ─────────────────────────────────────────────────────────────────────
_ROUTES = [
(re.compile(r"^/$"), lambda m: view_home()),
@@ -506,6 +591,9 @@ _ROUTES = [
(re.compile(r"^/works/(\d+)$"), lambda m: view_work(int(m.group(1)))),
(re.compile(r"^/works/(\d+)/ch/(\d+)$"), lambda m: view_chapter(int(m.group(1)), int(m.group(2)))),
(re.compile(r"^/knowledge$"), lambda m: view_knowledge()),
+ (re.compile(r"^/agents$"), lambda m: view_agents()),
+ (re.compile(r"^/agents/role/([\w-]+)$"), lambda m: view_agent_role(m.group(1))),
+ (re.compile(r"^/agents/skill/([\w-]+)$"), lambda m: view_skill(m.group(1))),
(re.compile(r"^/runs$"), lambda m: view_runs_page()),
]