框架(看板): 新增智能体空间(第五空间,读 102 登记表)

- /agents 角色列表(5)+技能列表(24);/agents/role/<role> 角色详情(模型/工具/职责+本角色运行/质量/调用);
  /agents/skill/<name> 技能详情(用途/涉及表/出处)
- 导航升级为五空间(总览/作品/知识库/智能体/运行记录),对齐合同 §10 领域布局菜单树
- 合同 §9/§10.1 同步:智能体空间已建、读 102 登记表、登记脚本说明
This commit is contained in:
zizi 2026-07-31 01:03:22 +08:00
parent 6728815174
commit b936de5bef
2 changed files with 90 additions and 2 deletions

View File

@ -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 形态

View File

@ -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 '<span style="color:var(--muted)">∅</span>'
return esc(v)
def view_agents():
_, roles = q("SELECT role, display_name, model, tools FROM example_agent_role "
"WHERE deleted=false ORDER BY role")
_, skills = q("SELECT skill_name, purpose, model_used FROM example_skill "
"WHERE deleted=false ORDER BY skill_name")
role_rows = "".join(
f"<tr><td><a href='/agents/role/{esc(r[0])}'>{esc(r[1] or r[0])}</a> "
f"<span style='color:var(--muted)'>{esc(r[0])}</span></td>"
f"<td>{esc(r[2])}</td><td>{_fmt_list(r[3])}</td></tr>" for r in roles)
skill_rows = "".join(
f"<tr><td><a href='/agents/skill/{esc(s[0])}'>{esc(s[0])}</a></td>"
f"<td>{esc((s[1] or '')[:56])}</td><td>{esc(s[2])}</td></tr>" for s in skills)
body = (f"<h1>智能体<span class='sub'>角色与技能 · 读 102 登记表(权威在 Git 侧,登记脚本同步)· Agent 07</span></h1>"
f"<div class='card'><div class='hd'>角色({len(roles)})</div>"
f"<table><thead><tr><th>角色</th><th>模型</th><th>工具</th></tr></thead><tbody>{role_rows}</tbody></table></div>"
f"<div class='card'><div class='hd'>技能({len(skills)})</div>"
f"<table><thead><tr><th>技能</th><th>用途</th><th>用模型</th></tr></thead><tbody>{skill_rows}</tbody></table></div>")
return layout("智能体", body, "/agents")
def view_agent_role(role):
_, rr = q("SELECT role, display_name, model, effort, budget_usd, tools, responsibility, source_ref, synced_at "
"FROM example_agent_role WHERE role=%s AND deleted=false", (role,))
if not rr:
return layout("角色", "<h1>角色不存在</h1>", "/agents")
role_, disp, model, effort, budget, tools, resp, src, synced = rr[0]
info = (f"<div class='card'><div class='hd'>{esc(disp or role_)} <span style='color:var(--muted)'>{esc(role_)}</span></div>"
f"<table>"
f"<tr><td style='width:120px;color:var(--muted)'>模型归属</td><td>{esc(model)}</td></tr>"
f"<tr><td style='color:var(--muted)'>工具</td><td>{_fmt_list(tools)}</td></tr>"
f"<tr><td style='color:var(--muted)'>可配置项</td><td>effort {esc(effort)} · 预算 {esc(budget)}(暂空待补)</td></tr>"
f"<tr><td style='color:var(--muted)'>出处</td><td><code>{esc(src)}</code> · 同步于 {esc(synced)}</td></tr>"
f"<tr><td style='color:var(--muted)'>职责</td><td>{esc(resp)}</td></tr>"
f"</table></div>")
_, runs = q("SELECT run_id, stage_kind, revision, create_time FROM example_run_receipt "
"WHERE adapter_role=%s ORDER BY create_time DESC LIMIT 20", (role,))
runs_html = ("<div class='card'><div class='hd'>本角色运行(回执,最近 20)</div><table><thead>"
"<tr><th>run_id</th><th>阶段</th><th class='num'>revision</th><th>时间</th></tr></thead><tbody>"
+ "".join(f"<tr><td><code>{esc(r[0])}</code></td><td>{esc(r[1])}</td><td class='num'>{esc(r[2])}</td><td>{esc(r[3])}</td></tr>" for r in runs)
+ "</tbody></table></div>") if runs else empty_gap("本角色暂无运行回执", "创作链落库后,该角色的运行会显示在这里。")
_, calls = q("SELECT call_time, requested_model_id, out_tokens, cost_usd FROM example_llm_call "
"WHERE caller=%s ORDER BY call_time DESC LIMIT 20", (role,))
calls_html = ("<div class='card'><div class='hd'>本角色模型调用(最近 20)</div><table><thead>"
"<tr><th>时间</th><th>模型</th><th class='num'>输出token</th><th class='num'>成本</th></tr></thead><tbody>"
+ "".join(f"<tr><td>{esc(c[0])}</td><td>{esc(c[1])}</td><td class='num'>{fmt_num(c[2])}</td><td class='num'>${float(c[3] or 0):.5f}</td></tr>" for c in calls)
+ "</tbody></table></div>") if calls else empty_gap("本角色暂无模型调用", "")
_, quals = q("SELECT q.judge_kind, q.dimension, q.conclusion, q.failure_class FROM example_quality_result q "
"JOIN example_run_receipt r ON r.id=q.receipt_id WHERE r.adapter_role=%s ORDER BY q.create_time DESC LIMIT 20", (role,))
quals_html = ("<div class='card'><div class='hd'>本角色质量评判(最近 20)</div><table><thead>"
"<tr><th>类型</th><th>维度</th><th>结论</th><th>失败类别</th></tr></thead><tbody>"
+ "".join(f"<tr><td>{esc(u[0])}</td><td>{esc(u[1])}</td><td>{chip(u[2])}</td><td>{esc(u[3])}</td></tr>" for u in quals)
+ "</tbody></table></div>") if quals else empty_gap("本角色暂无质量评判", "")
body = (f"<div class='crumb'><a href='/agents'>智能体</a> / {esc(disp or role_)}</div>"
f"<h1>{esc(disp or role_)}</h1>{info}<h2>运行与产出</h2>{runs_html}{quals_html}{calls_html}")
return layout(f"角色 · {disp or role_}", body, "/agents")
def view_skill(name):
_, ss = q("SELECT skill_name, purpose, consumer, reads, writes, red_lines, model_used, source_ref, synced_at "
"FROM example_skill WHERE skill_name=%s AND deleted=false", (name,))
if not ss:
return layout("技能", "<h1>技能不存在</h1>", "/agents")
sn, purpose, consumer, reads, writes, red_lines, model_used, src, synced = ss[0]
body = (f"<div class='crumb'><a href='/agents'>智能体</a> / 技能 / {esc(sn)}</div>"
f"<h1>{esc(sn)}</h1>"
f"<div class='card'><table>"
f"<tr><td style='width:120px;color:var(--muted)'>用途</td><td>{esc(purpose)}</td></tr>"
f"<tr><td style='color:var(--muted)'>给谁用</td><td>{esc(consumer) if consumer else '<span style=\"color:var(--muted)\">待补</span>'}</td></tr>"
f"<tr><td style='color:var(--muted)'>用模型</td><td>{esc(model_used) if model_used else '确定性工具(不用模型)'}</td></tr>"
f"<tr><td style='color:var(--muted)'>涉及表</td><td>{_fmt_list(reads)}<div style='color:var(--muted);font-size:12px'>自动抽取,读/写方向待人工核</div></td></tr>"
f"<tr><td style='color:var(--muted)'>红线</td><td>{esc(red_lines) if red_lines else '<span style=\"color:var(--muted)\">待补</span>'}</td></tr>"
f"<tr><td style='color:var(--muted)'>出处</td><td><code>{esc(src)}</code> · 同步于 {esc(synced)}</td></tr>"
f"</table></div>")
return layout(f"技能 · {sn}", body, "/agents")
# ── 路由 ─────────────────────────────────────────────────────────────────────
_ROUTES = [
(re.compile(r"^/$"), lambda m: 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()),
]