M8:天工阁、叩心与辰正——受托任务后台并行、叩心四约束、时态感知投影

- 天工阁域 domains/tiangong:backstage_tasks(委托包+验收标准+结果证据);有限分解(parent 链)与受控并行(各自后台 Run);子任务全终态级联父任务进入待验收;按证据 accept/reject
- kernel.start_backstage_run(kind=background_task):loop 后台分支不占前台席位、与前台对话并行;断言放宽容纳无人物功能任务;结果只经回调回呈任务——无身份写入路径
- 叩心域 domains/kouxin:knocking_candidates(event/time_anchor/manual 触发 + 草稿 + 裁决快照);四约束裁决(总开关→静默窗口【本地时区判定】→24h 频率→预算),dispatch 未过 429 且快照留痕;候选默认不自动发送;expire_stale 过期推进不触碰缘契
- 辰正 domains/zhengchen:temporal_context() 复用 time_sense 计算核(快照/农历/季节),产出【时态感知】投影段接入镜树生成 prompt(客观时间语境,不附情绪臆测——守一纪律)
- 退役:modules/goal_orchestrator|planner|perception|big_cycle、application/planning、services/governance|goal_persist、candidates 表链(imports 候选抽取 API 面)、goals/plans/agent_roster/perception_channels/big_cycles/旧候选表(迁移 009);前端 admin goals/plans/agents/perception/world 页与六组件;governance 只留 GenerationPreferences;admin 治理面收敛为司命+百工两枢;pivots 健康以域表计数重建
- 验收(test_m8_tiangong 五用例):后台任务无身份写入(记忆/魂籍/缘契零新增);后台与前台并行;分解级联;叩心四约束逐项;不回应不降低关系(缘契维度/状态/版本零变化)
- 四套件:后端 345、前端 203、BFF 4、神识 5 包全绿(2 个预存环境失败不变);迁移 009 净库干净
This commit is contained in:
chenxin committed 2026-08-28 09:13:39 +08:00
1 parent b419e0afde
commit c63b75152a
68 files changed
+1205 -4750

No files matched your search

-33
View File
@@ -1,33 +0,0 @@
import { PageContainer } from "@/components/layout/PageContainer";
import { AgentRoster } from "@/components/admin/AgentRoster";
import { ErrorState } from "@/components/shared/StateViews";
import { serverApi } from "@/lib/api/server-client";
import { describeError } from "@/lib/utils/error";
import type { AgentRosterEntry } from "@/types/api";
export const dynamic = "force-dynamic";
export default async function AdminAgentsPage() {
let roster: AgentRosterEntry[] = [];
let error: string | null = null;
try {
roster = await serverApi.get("/admin/agents/roster");
} catch (e) {
error = describeError(e);
}
return (
<PageContainer
title="天工阁枢"
description="多智能体角色在岗管理——九角色可增删改。"
maxWidth="max-w-5xl"
>
{error ? (
<ErrorState message={error} />
) : (
<AgentRoster roster={roster ?? []} />
)}
</PageContainer>
);
}
-33
View File
@@ -1,33 +0,0 @@
import { PageContainer } from "@/components/layout/PageContainer";
import { GoalTree } from "@/components/admin/GoalTree";
import { ErrorState } from "@/components/shared/StateViews";
import { serverApi } from "@/lib/api/server-client";
import { describeError } from "@/lib/utils/error";
import type { GoalNode } from "@/types/api";
export const dynamic = "force-dynamic";
export default async function AdminGoalsPage() {
let goals: GoalNode[] = [];
let error: string | null = null;
try {
goals = await serverApi.get("/admin/goals");
} catch (e) {
error = describeError(e);
}
return (
<PageContainer
title="太一枢"
description="目标层级树——从使命到行动的分解,支持增删改。"
maxWidth="max-w-5xl"
>
{error ? (
<ErrorState message={error} />
) : (
<GoalTree goals={goals ?? []} />
)}
</PageContainer>
);
}
@@ -1,33 +0,0 @@
import { PageContainer } from "@/components/layout/PageContainer";
import { PerceptionConsentMatrix } from "@/components/admin/PerceptionConsentMatrix";
import { ErrorState } from "@/components/shared/StateViews";
import { serverApi } from "@/lib/api/server-client";
import { describeError } from "@/lib/utils/error";
import type { PerceptionChannel } from "@/types/api";
export const dynamic = "force-dynamic";
export default async function AdminPerceptionPage() {
let channels: PerceptionChannel[] = [];
let error: string | null = null;
try {
channels = await serverApi.get("/admin/perception/channels");
} catch (e) {
error = describeError(e);
}
return (
<PageContainer
title="烛照枢"
description="感知通道授权——管理六类感知源的同意状态与隐私级别。"
maxWidth="max-w-5xl"
>
{error ? (
<ErrorState message={error} />
) : (
<PerceptionConsentMatrix channels={channels ?? []} />
)}
</PageContainer>
);
}
+1 -136
View File
@@ -6,11 +6,6 @@ import { VermilionAction } from "@/components/admin/VermilionAction";
import { PolicyMatrix } from "@/components/admin/PolicyMatrix";
import { AuditConsole } from "@/components/admin/AuditConsole";
import { ToolRegistry } from "@/components/admin/ToolRegistry";
import { GoalTree } from "@/components/admin/GoalTree";
import { PerceptionConsentMatrix } from "@/components/admin/PerceptionConsentMatrix";
import { WorldEntityList } from "@/components/admin/WorldEntityList";
import { PlanConsole } from "@/components/admin/PlanConsole";
import { AgentRoster } from "@/components/admin/AgentRoster";
import { McpServerPanel } from "@/components/admin/mcp/McpServerPanel";
import { ToolRuntimeTabs } from "@/components/admin/mcp/ToolRuntimeTabs";
import { serverApi } from "@/lib/api/server-client";
@@ -34,15 +29,7 @@ import type {
export const dynamic = "force-dynamic";
const VALID_PIVOTS: Pivot[] = [
"goal_orchestrator",
"safety_governor",
"perception",
"world_model",
"planner",
"tool_runtime",
"agent_society",
];
const VALID_PIVOTS: Pivot[] = ["safety_governor", "tool_runtime"];
const PIVOT_SET = new Set<string>(VALID_PIVOTS);
@@ -211,133 +198,11 @@ async function ToolRuntimeDetail() {
}
/** 太一枢:目标层级树(最高目标 → 任务,可编辑标题/优先级/状态) */
async function GoalOrchestratorDetail() {
let goals: GoalNode[] = [];
let error: string | null = null;
try {
goals = await serverApi.get<GoalNode[]>("/admin/goals");
} catch (e) {
error = describeError(e);
}
return (
<PageContainer
title="太一枢"
description="目标统御 · 最高目标与任务调度"
maxWidth="max-w-3xl"
>
{error ? (
<ErrorState message={error} />
) : goals.length === 0 ? (
<EmptyState title="暂无目标" description="太一枢尚未编排任何目标。" />
) : (
<GoalTree goals={goals} />
)}
</PageContainer>
);
}
/** 烛照枢:感知通道授权矩阵(启用 / 授权 / 隐私路由) */
async function PerceptionDetail() {
let channels: PerceptionChannel[] = [];
let error: string | null = null;
try {
channels = await serverApi.get<PerceptionChannel[]>(
"/admin/perception/channels",
);
} catch (e) {
error = describeError(e);
}
return (
<PageContainer
title="烛照枢"
description="感知世界 · 通道授权与隐私边界"
maxWidth="max-w-3xl"
>
{error ? (
<ErrorState message={error} />
) : (
<PerceptionConsentMatrix channels={channels} />
)}
</PageContainer>
);
}
/** 河图枢:世界实体列表(类型 / 事实-推断 / 置信度) */
async function WorldModelDetail() {
let entities: WorldEntity[] = [];
let error: string | null = null;
try {
entities = await serverApi.get<WorldEntity[]>("/admin/world/entities");
} catch (e) {
error = describeError(e);
}
return (
<PageContainer
title="河图枢"
description="世界模型 · 实体与关系图式"
maxWidth="max-w-3xl"
>
{error ? (
<ErrorState message={error} />
) : entities.length === 0 ? (
<EmptyState
title="暂无世界实体"
description="河图枢尚未建模任何实体。"
/>
) : (
<WorldEntityList entities={entities} />
)}
</PageContainer>
);
}
/** 洛书枢:计划控制台(状态 / 假设 / 风险) */
async function PlannerDetail() {
let plans: PlanSummary[] = [];
let error: string | null = null;
try {
plans = await serverApi.get<PlanSummary[]>("/admin/plans");
} catch (e) {
error = describeError(e);
}
return (
<PageContainer
title="洛书枢"
description="推演变化 · 规划与流程编排"
maxWidth="max-w-3xl"
>
{error ? (
<ErrorState message={error} />
) : plans.length === 0 ? (
<EmptyState title="暂无计划" description="洛书枢尚未编排任何计划。" />
) : (
<PlanConsole plans={plans} />
)}
</PageContainer>
);
}
/** 天工阁枢:九角色在岗名册(启用 / 停用) */
async function AgentSocietyDetail() {
let roster: AgentRosterEntry[] = [];
let error: string | null = null;
try {
roster = await serverApi.get<AgentRosterEntry[]>("/admin/agents/roster");
} catch (e) {
error = describeError(e);
}
return (
<PageContainer
title="天工阁枢"
description="众智能协作 · 九角色分工与协作"
maxWidth="max-w-3xl"
>
{error ? <ErrorState message={error} /> : <AgentRoster roster={roster} />}
</PageContainer>
);
}
-33
View File
@@ -1,33 +0,0 @@
import { PageContainer } from "@/components/layout/PageContainer";
import { PlanConsole } from "@/components/admin/PlanConsole";
import { ErrorState } from "@/components/shared/StateViews";
import { serverApi } from "@/lib/api/server-client";
import { describeError } from "@/lib/utils/error";
import type { PlanSummary } from "@/types/api";
export const dynamic = "force-dynamic";
export default async function AdminPlansPage() {
let plans: PlanSummary[] = [];
let error: string | null = null;
try {
plans = await serverApi.get("/admin/plans");
} catch (e) {
error = describeError(e);
}
return (
<PageContainer
title="洛书枢"
description="计划控制台——规划治理,支持增删改。"
maxWidth="max-w-5xl"
>
{error ? (
<ErrorState message={error} />
) : (
<PlanConsole plans={plans ?? []} />
)}
</PageContainer>
);
}
-33
View File
@@ -1,33 +0,0 @@
import { PageContainer } from "@/components/layout/PageContainer";
import { WorldEntityList } from "@/components/admin/WorldEntityList";
import { ErrorState } from "@/components/shared/StateViews";
import { serverApi } from "@/lib/api/server-client";
import { describeError } from "@/lib/utils/error";
import type { WorldEntity } from "@/types/api";
export const dynamic = "force-dynamic";
export default async function AdminWorldPage() {
let entities: WorldEntity[] = [];
let error: string | null = null;
try {
entities = await serverApi.get("/admin/world/entities");
} catch (e) {
error = describeError(e);
}
return (
<PageContainer
title="河图枢"
description="世界实体管理——人物对世界的认知实体,支持增删改。"
maxWidth="max-w-5xl"
>
{error ? (
<ErrorState message={error} />
) : (
<WorldEntityList entities={entities ?? []} />
)}
</PageContainer>
);
}
@@ -1,201 +0,0 @@
"use client";
import { useState } from "react";
import { adminApi } from "@/lib/api";
import type { AgentRosterEntry } from "@/types/api";
import { AGENT_PRESENCE_LABEL } from "@/lib/constants/labels";
import { cn } from "@/lib/utils/cn";
/**
* 天工阁枢角色名册 —— 多智能体治理(可增删改)。
* 默认九角色(契约字典 §5.14),亦可新增自定义角色。inline toggle 在编/停用、删除。
* POST/PATCH/DELETE /admin/agents/roster。在编 jade、停用 muted;呈现态静态点(设计 §8 克制)。
*/
export function AgentRoster({
roster: initial,
}: {
roster: AgentRosterEntry[];
}) {
const [roster, setRoster] = useState(initial);
const [savingRole, setSavingRole] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [showAdd, setShowAdd] = useState(false);
const [newAgent, setNewAgent] = useState<{
role: string;
name: string;
responsibility: string;
}>({
role: "",
name: "",
responsibility: "",
});
async function toggle(entry: AgentRosterEntry) {
setSavingRole(entry.role);
setError(null);
try {
const updated = await adminApi.updateAgent(entry.role, {
enabled: !entry.enabled,
});
setRoster((prev) =>
prev.map((a) =>
a.role === entry.role ? { ...a, enabled: updated.enabled } : a,
),
);
} catch (e) {
setError(e instanceof Error ? e.message : "更新失败,请重试。");
} finally {
setSavingRole(null);
}
}
async function remove(role: string) {
setSavingRole(role);
setError(null);
try {
await adminApi.deleteAgent(role);
setRoster((prev) => prev.filter((a) => a.role !== role));
} catch (e) {
setError(e instanceof Error ? e.message : "删除失败,请重试。");
} finally {
setSavingRole(null);
}
}
async function add() {
if (!newAgent.role.trim() || !newAgent.name.trim()) {
setError("角色标识与名称必填。");
return;
}
setSavingRole("__new");
setError(null);
try {
const created = await adminApi.createAgent({
role: newAgent.role.trim(),
name: newAgent.name.trim(),
responsibility: newAgent.responsibility.trim() || "自定义角色",
});
setRoster((prev) => [...prev, created]);
setNewAgent({ role: "", name: "", responsibility: "" });
setShowAdd(false);
} catch (e) {
setError(e instanceof Error ? e.message : "新增失败,请重试。");
} finally {
setSavingRole(null);
}
}
return (
<div>
{showAdd ? (
<div className="glass mb-2 space-y-2 rounded-xl px-4 py-3">
<div className="flex flex-wrap items-center gap-2">
<input
value={newAgent.role}
onChange={(e) =>
setNewAgent((s) => ({ ...s, role: e.target.value }))
}
placeholder="角色标识(如 archivist)"
className="w-40 shrink-0 rounded-lg border border-border-glass bg-glass px-2 py-1 font-mono text-xs text-fg"
/>
<input
value={newAgent.name}
onChange={(e) =>
setNewAgent((s) => ({ ...s, name: e.target.value }))
}
placeholder="中文名(如 典藏)"
className="min-w-0 flex-1 rounded-lg border border-border-glass bg-glass px-2 py-1 text-sm text-fg"
/>
</div>
<input
value={newAgent.responsibility}
onChange={(e) =>
setNewAgent((s) => ({ ...s, responsibility: e.target.value }))
}
placeholder="职责(可选)"
className="w-full rounded-lg border border-border-glass bg-glass px-2 py-1 text-sm text-fg"
/>
<div className="flex gap-2">
<button
type="button"
onClick={add}
disabled={savingRole === "__new"}
className="rounded-lg bg-patina/20 px-3 py-1 text-xs font-medium text-patina disabled:opacity-50"
>
新增
</button>
<button
type="button"
onClick={() => setShowAdd(false)}
className="rounded-lg border border-border-glass bg-glass px-3 py-1 text-xs text-muted"
>
取消
</button>
</div>
</div>
) : (
<button
type="button"
onClick={() => setShowAdd(true)}
className="mb-2 rounded-lg border border-patina/40 bg-patina/10 px-3 py-1.5 text-xs font-medium text-patina transition-colors hover:bg-patina/20"
>
+ 新增角色
</button>
)}
<ul className="grid gap-2 sm:grid-cols-2">
{roster.map((a) => (
<li
key={a.role}
className={cn(
"glass rounded-xl px-4 py-3 transition-opacity",
!a.enabled && "opacity-55",
)}
>
<div className="flex items-center gap-2">
<span
className={cn(
"h-2 w-2 shrink-0 rounded-full",
a.presence === "active" ? "bg-patina" : "bg-muted/40",
)}
/>
<span className="font-serif text-sm font-medium text-fg">
{a.name}
</span>
<span className="shrink-0 text-[10px] text-muted">
{AGENT_PRESENCE_LABEL[a.presence]}
</span>
<button
type="button"
onClick={() => toggle(a)}
disabled={savingRole === a.role}
aria-label={`${a.name} 启用`}
className={cn(
"shrink-0 rounded-full px-2.5 py-0.5 text-[10px] font-medium transition-colors disabled:opacity-50",
a.enabled
? "bg-patina/20 text-patina"
: "border border-border-glass text-muted hover:text-fg",
)}
>
{a.enabled ? "在编" : "停用"}
</button>
</div>
<div className="mt-1 flex items-center gap-2 pl-4">
<p className="flex-1 text-xs text-muted">{a.responsibility}</p>
<button
type="button"
onClick={() => remove(a.role)}
disabled={savingRole === a.role}
className="shrink-0 text-[11px] text-cinnabar hover:underline disabled:opacity-50"
>
删除
</button>
</div>
</li>
))}
</ul>
{error && <p className="mt-2 px-1 text-xs text-cinnabar">{error}</p>}
</div>
);
}
-340
View File
@@ -1,340 +0,0 @@
"use client";
import { useState } from "react";
import { adminApi } from "@/lib/api";
import type { GoalLevel, GoalStatus } from "@/types/domain";
import type { GoalNode } from "@/types/api";
import {
GOAL_LEVEL_LABEL,
GOAL_STATUS_LABEL,
GOAL_TYPE_LABEL,
} from "@/lib/constants/labels";
import { cn } from "@/lib/utils/cn";
const LEVEL_ORDER: GoalLevel[] = [
"highest",
"long_term",
"session",
"task",
"action",
];
/** 新增可选层级(不含 highest 使命级——使命不可手动创建) */
const LEVEL_OPTIONS: GoalLevel[] = ["long_term", "session", "task", "action"];
const STATUS_OPTIONS: GoalStatus[] = [
"pending",
"active",
"completed",
"blocked",
"cancelled",
];
const STATUS_DOT: Record<GoalStatus, string> = {
pending: "bg-muted/50",
active: "bg-patina",
completed: "bg-gold",
blocked: "bg-cinnabar",
cancelled: "bg-muted/30",
};
/**
* 太一枢目标层级树 —— 目标统御治理视图(可增删改)。
* 按 level 分层;新增目标(非 highest)、编辑标题/优先级、select 改状态、删除。
* highest 为使命级:状态锁定、不可删(契约字典 §5.11)。状态圆点静态着色(设计 §8 克制)。
*/
export function GoalTree({ goals: initial }: { goals: GoalNode[] }) {
const [goals, setGoals] = useState(initial);
const [editingId, setEditingId] = useState<string | null>(null);
const [draft, setDraft] = useState<{ title: string; priority: number }>({
title: "",
priority: 0,
});
const [busyId, setBusyId] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [showAdd, setShowAdd] = useState(false);
const [newGoal, setNewGoal] = useState<{ level: GoalLevel; title: string }>({
level: "task",
title: "",
});
function startEdit(g: GoalNode) {
setEditingId(g.id);
setDraft({ title: g.title, priority: g.priority });
setError(null);
}
async function changeStatus(g: GoalNode, next: GoalStatus) {
setBusyId(g.id);
setError(null);
try {
const updated = await adminApi.updateGoal(g.id, { status: next });
setGoals((prev) =>
prev.map((x) => (x.id === g.id ? { ...x, status: updated.status } : x)),
);
} catch (e) {
setError(e instanceof Error ? e.message : "更新失败,请重试。");
} finally {
setBusyId(null);
}
}
async function save(id: string) {
setBusyId(id);
setError(null);
try {
const updated = await adminApi.updateGoal(id, {
title: draft.title,
priority: draft.priority,
});
setGoals((prev) =>
prev.map((x) =>
x.id === id
? { ...x, title: updated.title, priority: updated.priority }
: x,
),
);
setEditingId(null);
} catch (e) {
setError(e instanceof Error ? e.message : "保存失败,请重试。");
} finally {
setBusyId(null);
}
}
async function remove(g: GoalNode) {
setBusyId(g.id);
setError(null);
try {
await adminApi.deleteGoal(g.id);
setGoals((prev) => prev.filter((x) => x.id !== g.id));
} catch (e) {
setError(e instanceof Error ? e.message : "删除失败,请重试。");
} finally {
setBusyId(null);
}
}
async function add() {
if (!newGoal.title.trim()) {
setError("请填写目标标题。");
return;
}
setBusyId("__new");
setError(null);
try {
const created = await adminApi.createGoal({
title: newGoal.title.trim(),
level: newGoal.level,
});
setGoals((prev) => [...prev, created]);
setNewGoal({ level: "task", title: "" });
setShowAdd(false);
} catch (e) {
setError(e instanceof Error ? e.message : "新增失败,请重试。");
} finally {
setBusyId(null);
}
}
return (
<div className="space-y-5">
{showAdd ? (
<div className="glass space-y-2 rounded-xl px-4 py-3">
<div className="flex flex-wrap items-center gap-2">
<select
value={newGoal.level}
onChange={(e) =>
setNewGoal((s) => ({
...s,
level: e.target.value as GoalLevel,
}))
}
aria-label="目标层级"
className="w-24 shrink-0 rounded-lg border border-border-glass bg-glass px-2 py-1 text-xs text-fg backdrop-blur"
>
{LEVEL_OPTIONS.map((l) => (
<option key={l} value={l}>
{GOAL_LEVEL_LABEL[l]}
</option>
))}
</select>
<input
value={newGoal.title}
onChange={(e) =>
setNewGoal((s) => ({ ...s, title: e.target.value }))
}
placeholder="目标标题…"
className="min-w-0 flex-1 rounded-lg border border-border-glass bg-glass px-2 py-1 text-sm text-fg"
/>
<button
type="button"
onClick={add}
disabled={busyId === "__new"}
className="shrink-0 rounded-lg bg-patina/20 px-3 py-1 text-xs font-medium text-patina disabled:opacity-50"
>
新增
</button>
<button
type="button"
onClick={() => setShowAdd(false)}
className="shrink-0 rounded-lg border border-border-glass bg-glass px-3 py-1 text-xs text-muted"
>
取消
</button>
</div>
</div>
) : (
<button
type="button"
onClick={() => setShowAdd(true)}
className="rounded-lg border border-patina/40 bg-patina/10 px-3 py-1.5 text-xs font-medium text-patina transition-colors hover:bg-patina/20"
>
+ 新增目标
</button>
)}
{LEVEL_ORDER.map((level) => {
const items = goals.filter((g) => g.level === level);
if (items.length === 0) return null;
return (
<section key={level}>
<div className="mb-2 flex items-center gap-2">
<span className="font-serif text-xs font-medium text-gold">
{GOAL_LEVEL_LABEL[level]}
</span>
<span className="h-px flex-1 bg-gold/20" />
</div>
<ul className="space-y-2">
{items.map((g) => (
<li key={g.id} className="glass rounded-xl px-4 py-2.5">
{editingId === g.id ? (
<div className="space-y-2">
<input
value={draft.title}
onChange={(e) =>
setDraft((d) => ({ ...d, title: e.target.value }))
}
className="w-full rounded-lg border border-border-glass bg-glass px-2 py-1 text-sm text-fg"
/>
<div className="flex items-center gap-2">
<label className="text-[10px] text-muted">优先级</label>
<input
type="number"
value={draft.priority}
onChange={(e) =>
setDraft((d) => ({
...d,
priority: Number(e.target.value),
}))
}
className="w-20 rounded-lg border border-border-glass bg-glass px-2 py-1 text-xs text-fg"
/>
<div className="ml-auto flex gap-2">
<button
type="button"
onClick={() => save(g.id)}
disabled={busyId === g.id}
className="rounded-lg bg-patina/20 px-3 py-1 text-xs font-medium text-patina disabled:opacity-50"
>
保存
</button>
<button
type="button"
onClick={() => setEditingId(null)}
className="rounded-lg border border-border-glass bg-glass px-3 py-1 text-xs text-muted"
>
取消
</button>
</div>
</div>
</div>
) : (
<>
<div className="flex items-center gap-2">
<span
className={cn(
"h-2 w-2 shrink-0 rounded-full",
STATUS_DOT[g.status],
)}
/>
<span className="font-serif text-sm font-medium text-fg">
{g.title}
</span>
<span className="ml-auto shrink-0 font-mono text-[10px] text-muted">
P{g.priority}
</span>
<select
value={g.status}
onChange={(e) =>
changeStatus(g, e.target.value as GoalStatus)
}
disabled={busyId === g.id || g.level === "highest"}
aria-label={`${g.title} 状态`}
className="w-24 shrink-0 rounded-lg border border-border-glass bg-glass px-2 py-1 text-xs text-fg backdrop-blur disabled:opacity-50"
>
{STATUS_OPTIONS.map((s) => (
<option key={s} value={s}>
{GOAL_STATUS_LABEL[s]}
</option>
))}
</select>
</div>
{/* 进度条 */}
<div className="mt-2 flex items-center gap-2 pl-4">
<div className="h-1.5 flex-1 overflow-hidden rounded-full bg-muted/20">
<div
className="h-full rounded-full bg-patina transition-all"
style={{
width: `${Math.round(g.progress * 100)}%`,
}}
/>
</div>
<span className="font-mono text-[10px] text-muted">
{Math.round(g.progress * 100)}%
</span>
</div>
{/* 类型 */}
{g.type && (
<span className="mt-1 block pl-4 text-[10px] text-muted">
类型:{GOAL_TYPE_LABEL[g.type] ?? g.type}
</span>
)}
<p className="mt-1 pl-4 text-xs text-muted">
{g.description}
</p>
{g.constraints.length > 0 && (
<p className="mt-1 pl-4 text-[10px] text-muted">
约束:{g.constraints.join(" · ")}
</p>
)}
<div className="mt-1.5 flex gap-3 pl-4">
<button
type="button"
onClick={() => startEdit(g)}
className="text-[11px] text-patina hover:underline"
>
编辑标题/优先级
</button>
{g.level !== "highest" && (
<button
type="button"
onClick={() => remove(g)}
disabled={busyId === g.id}
className="text-[11px] text-cinnabar hover:underline disabled:opacity-50"
>
删除
</button>
)}
</div>
</>
)}
</li>
))}
</ul>
</section>
);
})}
{error && <p className="text-xs text-cinnabar">{error}</p>}
</div>
);
}
@@ -1,259 +0,0 @@
"use client";
import { useState } from "react";
import { adminApi } from "@/lib/api";
import type { ConsentStatus, PrivacyLevel } from "@/types/domain";
import type { PerceptionChannel } from "@/types/api";
import { CONSENT_STATUS_LABEL } from "@/lib/constants/labels";
import { cn } from "@/lib/utils/cn";
const CONSENT_OPTIONS: ConsentStatus[] = [
"unknown",
"granted",
"restricted",
"revoked",
];
const PRIVACY_OPTIONS: PrivacyLevel[] = ["normal", "sensitive", "local_only"];
/** 隐私路由中文呈现(privacy_level,契约字典 §5.4) */
const PRIVACY_LABEL: Record<PrivacyLevel, string> = {
normal: "普通",
sensitive: "敏感",
local_only: "仅本地",
};
const CONSENT_DOT: Record<ConsentStatus, string> = {
unknown: "bg-muted/40",
granted: "bg-patina",
restricted: "bg-gold",
revoked: "bg-cinnabar",
};
/**
* 烛照枢感知通道授权矩阵 —— 隐私边界治理(可增删改)。
* 默认六类通道,亦可新增自定义感知源。每通道 inline 编辑 enabled / consent / privacy、删除。
* POST/PATCH/DELETE /admin/perception/channels。授权 jade、撤销 cinnabar。
*/
export function PerceptionConsentMatrix({
channels: initial,
}: {
channels: PerceptionChannel[];
}) {
const [channels, setChannels] = useState(initial);
const [savingCh, setSavingCh] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [showAdd, setShowAdd] = useState(false);
const [newCh, setNewCh] = useState<{
channel: string;
name: string;
description: string;
}>({
channel: "",
name: "",
description: "",
});
async function patch(
channel: string,
body: Partial<{
enabled: boolean;
consent_status: ConsentStatus;
privacy_level: PrivacyLevel;
}>,
) {
setSavingCh(channel);
setError(null);
try {
const updated = await adminApi.updatePerceptionChannel(channel, body);
setChannels((prev) =>
prev.map((c) => (c.channel === channel ? { ...c, ...updated } : c)),
);
} catch (e) {
setError(e instanceof Error ? e.message : "更新失败,请重试。");
} finally {
setSavingCh(null);
}
}
async function remove(channel: string) {
setSavingCh(channel);
setError(null);
try {
await adminApi.deletePerceptionChannel(channel);
setChannels((prev) => prev.filter((c) => c.channel !== channel));
} catch (e) {
setError(e instanceof Error ? e.message : "删除失败,请重试。");
} finally {
setSavingCh(null);
}
}
async function add() {
if (!newCh.channel.trim() || !newCh.name.trim()) {
setError("通道标识与名称必填。");
return;
}
setSavingCh("__new");
setError(null);
try {
const created = await adminApi.createPerceptionChannel({
channel: newCh.channel.trim(),
name: newCh.name.trim(),
description: newCh.description.trim() || "自定义感知源",
});
setChannels((prev) => [...prev, created]);
setNewCh({ channel: "", name: "", description: "" });
setShowAdd(false);
} catch (e) {
setError(e instanceof Error ? e.message : "新增失败,请重试。");
} finally {
setSavingCh(null);
}
}
return (
<div>
{showAdd ? (
<div className="glass mb-2 space-y-2 rounded-xl px-4 py-3">
<div className="flex flex-wrap items-center gap-2">
<input
value={newCh.channel}
onChange={(e) =>
setNewCh((s) => ({ ...s, channel: e.target.value }))
}
placeholder="通道标识(如 sensor_gps)"
className="w-40 shrink-0 rounded-lg border border-border-glass bg-glass px-2 py-1 font-mono text-xs text-fg"
/>
<input
value={newCh.name}
onChange={(e) =>
setNewCh((s) => ({ ...s, name: e.target.value }))
}
placeholder="中文名(如 定位)"
className="min-w-0 flex-1 rounded-lg border border-border-glass bg-glass px-2 py-1 text-sm text-fg"
/>
</div>
<input
value={newCh.description}
onChange={(e) =>
setNewCh((s) => ({ ...s, description: e.target.value }))
}
placeholder="描述(可选)"
className="w-full rounded-lg border border-border-glass bg-glass px-2 py-1 text-sm text-fg"
/>
<div className="flex gap-2">
<button
type="button"
onClick={add}
disabled={savingCh === "__new"}
className="rounded-lg bg-patina/20 px-3 py-1 text-xs font-medium text-patina disabled:opacity-50"
>
新增
</button>
<button
type="button"
onClick={() => setShowAdd(false)}
className="rounded-lg border border-border-glass bg-glass px-3 py-1 text-xs text-muted"
>
取消
</button>
</div>
</div>
) : (
<button
type="button"
onClick={() => setShowAdd(true)}
className="mb-2 rounded-lg border border-patina/40 bg-patina/10 px-3 py-1.5 text-xs font-medium text-patina transition-colors hover:bg-patina/20"
>
+ 新增感知通道
</button>
)}
<div className="glass overflow-hidden rounded-2xl">
<ul className="divide-y divide-border-glass">
{channels.map((c) => (
<li key={c.channel} className="px-4 py-3">
<div className="flex flex-wrap items-center gap-2">
<button
type="button"
onClick={() => patch(c.channel, { enabled: !c.enabled })}
disabled={savingCh === c.channel}
aria-label={`${c.name} 启用开关`}
className={cn(
"relative h-4 w-7 shrink-0 rounded-full transition-colors disabled:opacity-50",
c.enabled ? "bg-patina/60" : "bg-muted/30",
)}
>
<span
className={cn(
"absolute top-0.5 h-3 w-3 rounded-full bg-glass-strong transition-all",
c.enabled ? "left-3.5" : "left-0.5",
)}
/>
</button>
<span
className={cn(
"h-2 w-2 shrink-0 rounded-full",
CONSENT_DOT[c.consent_status],
)}
/>
<span className="font-serif text-sm font-medium text-fg">
{c.name}
</span>
<span className="ml-auto flex shrink-0 items-center gap-1.5">
<select
value={c.consent_status}
onChange={(e) =>
patch(c.channel, {
consent_status: e.target.value as ConsentStatus,
})
}
disabled={savingCh === c.channel}
aria-label={`${c.name} 授权状态`}
className="w-20 rounded-lg border border-border-glass bg-glass px-1.5 py-1 text-[11px] text-fg backdrop-blur disabled:opacity-50"
>
{CONSENT_OPTIONS.map((cs) => (
<option key={cs} value={cs}>
{CONSENT_STATUS_LABEL[cs]}
</option>
))}
</select>
<select
value={c.privacy_level}
onChange={(e) =>
patch(c.channel, {
privacy_level: e.target.value as PrivacyLevel,
})
}
disabled={savingCh === c.channel}
aria-label={`${c.name} 隐私路由`}
className="w-20 rounded-lg border border-border-glass bg-glass px-1.5 py-1 text-[11px] text-fg backdrop-blur disabled:opacity-50"
>
{PRIVACY_OPTIONS.map((pl) => (
<option key={pl} value={pl}>
{PRIVACY_LABEL[pl]}
</option>
))}
</select>
</span>
</div>
<div className="mt-1 flex items-center gap-2 pl-8">
<p className="flex-1 text-xs text-muted">{c.description}</p>
<button
type="button"
onClick={() => remove(c.channel)}
disabled={savingCh === c.channel}
className="shrink-0 text-[11px] text-cinnabar hover:underline disabled:opacity-50"
>
删除
</button>
</div>
</li>
))}
</ul>
</div>
{error && <p className="mt-2 px-1 text-xs text-cinnabar">{error}</p>}
</div>
);
}
@@ -1,279 +0,0 @@
"use client";
import { useState } from "react";
import { adminApi } from "@/lib/api";
import type { PlanStatus } from "@/types/domain";
import type { PlanSummary } from "@/types/api";
import { PLAN_STATUS_LABEL } from "@/lib/constants/labels";
import { cn } from "@/lib/utils/cn";
const STATUS_OPTIONS: PlanStatus[] = [
"draft",
"approved",
"running",
"completed",
"failed",
];
const STATUS_DOT: Record<PlanStatus, string> = {
draft: "bg-gold",
approved: "bg-patina",
running: "bg-patina",
completed: "bg-muted/50",
failed: "bg-cinnabar",
};
/**
* 洛书枢计划控制台 —— 规划治理(可增删改)。
* 新增计划、inline select 改状态、展开编辑假设 / 风险、删除。
* PATCH /admin/plans/{id};假设与风险以换行分隔编辑,保存时拆为数组;风险以 cinnabar 提示。
*/
export function PlanConsole({ plans: initial }: { plans: PlanSummary[] }) {
const [plans, setPlans] = useState(initial);
const [editingId, setEditingId] = useState<string | null>(null);
const [draft, setDraft] = useState<{ assumptions: string; risks: string }>({
assumptions: "",
risks: "",
});
const [busyId, setBusyId] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [showAdd, setShowAdd] = useState(false);
const [newPlan, setNewPlan] = useState("");
function startEdit(p: PlanSummary) {
setEditingId(p.id);
setDraft({
assumptions: p.assumptions.join("\n"),
risks: p.risks.join("\n"),
});
setError(null);
}
async function changeStatus(p: PlanSummary, next: PlanStatus) {
setBusyId(p.id);
setError(null);
try {
const updated = await adminApi.updatePlan(p.id, { status: next });
setPlans((prev) =>
prev.map((x) => (x.id === p.id ? { ...x, status: updated.status } : x)),
);
} catch (e) {
setError(e instanceof Error ? e.message : "更新失败,请重试。");
} finally {
setBusyId(null);
}
}
async function save(id: string) {
setBusyId(id);
setError(null);
try {
const assumptions = draft.assumptions
.split("\n")
.map((s) => s.trim())
.filter(Boolean);
const risks = draft.risks
.split("\n")
.map((s) => s.trim())
.filter(Boolean);
const updated = await adminApi.updatePlan(id, { assumptions, risks });
setPlans((prev) =>
prev.map((x) =>
x.id === id
? { ...x, assumptions: updated.assumptions, risks: updated.risks }
: x,
),
);
setEditingId(null);
} catch (e) {
setError(e instanceof Error ? e.message : "保存失败,请重试。");
} finally {
setBusyId(null);
}
}
async function remove(p: PlanSummary) {
setBusyId(p.id);
setError(null);
try {
await adminApi.deletePlan(p.id);
setPlans((prev) => prev.filter((x) => x.id !== p.id));
} catch (e) {
setError(e instanceof Error ? e.message : "删除失败,请重试。");
} finally {
setBusyId(null);
}
}
async function add() {
if (!newPlan.trim()) {
setError("请填写计划标题。");
return;
}
setBusyId("__new");
setError(null);
try {
const created = await adminApi.createPlan({ title: newPlan.trim() });
setPlans((prev) => [created, ...prev]);
setNewPlan("");
setShowAdd(false);
} catch (e) {
setError(e instanceof Error ? e.message : "新增失败,请重试。");
} finally {
setBusyId(null);
}
}
return (
<>
{showAdd ? (
<div className="glass mb-2 space-y-2 rounded-xl px-4 py-3">
<div className="flex flex-wrap items-center gap-2">
<input
value={newPlan}
onChange={(e) => setNewPlan(e.target.value)}
placeholder="计划标题…"
className="min-w-0 flex-1 rounded-lg border border-border-glass bg-glass px-2 py-1 text-sm text-fg"
/>
<button
type="button"
onClick={add}
disabled={busyId === "__new"}
className="shrink-0 rounded-lg bg-patina/20 px-3 py-1 text-xs font-medium text-patina disabled:opacity-50"
>
新增
</button>
<button
type="button"
onClick={() => setShowAdd(false)}
className="shrink-0 rounded-lg border border-border-glass bg-glass px-3 py-1 text-xs text-muted"
>
取消
</button>
</div>
</div>
) : (
<button
type="button"
onClick={() => setShowAdd(true)}
className="mb-2 rounded-lg border border-patina/40 bg-patina/10 px-3 py-1.5 text-xs font-medium text-patina transition-colors hover:bg-patina/20"
>
+ 新增计划
</button>
)}
<ul className="space-y-2">
{plans.map((p) => (
<li key={p.id} className="glass rounded-xl px-4 py-3">
<div className="flex items-center gap-2">
<span
className={cn(
"h-2 w-2 shrink-0 rounded-full",
STATUS_DOT[p.status],
)}
/>
<span className="truncate font-serif text-sm font-medium text-fg">
{p.title}
</span>
<span className="ml-auto shrink-0 font-mono text-[10px] text-muted">
{p.completed_steps}/{p.step_count}
</span>
<select
value={p.status}
onChange={(e) => changeStatus(p, e.target.value as PlanStatus)}
disabled={busyId === p.id}
aria-label={`${p.title} 状态`}
className="w-20 shrink-0 rounded-lg border border-border-glass bg-glass px-2 py-1 text-xs text-fg backdrop-blur disabled:opacity-50"
>
{STATUS_OPTIONS.map((s) => (
<option key={s} value={s}>
{PLAN_STATUS_LABEL[s]}
</option>
))}
</select>
</div>
{editingId === p.id ? (
<div className="mt-2 space-y-2">
<div>
<label className="text-[10px] text-muted">
假设(每行一条)
</label>
<textarea
value={draft.assumptions}
onChange={(e) =>
setDraft((d) => ({ ...d, assumptions: e.target.value }))
}
rows={2}
className="w-full rounded-lg border border-border-glass bg-glass px-2 py-1 text-xs text-fg"
/>
</div>
<div>
<label className="text-[10px] text-muted">
风险(每行一条)
</label>
<textarea
value={draft.risks}
onChange={(e) =>
setDraft((d) => ({ ...d, risks: e.target.value }))
}
rows={2}
className="w-full rounded-lg border border-border-glass bg-glass px-2 py-1 text-xs text-fg"
/>
</div>
<div className="flex gap-2">
<button
type="button"
onClick={() => save(p.id)}
disabled={busyId === p.id}
className="rounded-lg bg-patina/20 px-3 py-1 text-xs font-medium text-patina disabled:opacity-50"
>
保存
</button>
<button
type="button"
onClick={() => setEditingId(null)}
className="rounded-lg border border-border-glass bg-glass px-3 py-1 text-xs text-muted"
>
取消
</button>
</div>
</div>
) : (
<>
<div className="mt-1 space-y-0.5 pl-4 text-[10px] text-muted">
{p.assumptions.length > 0 && (
<p>假设:{p.assumptions.join(";")}</p>
)}
{p.risks.length > 0 && (
<p className="text-cinnabar/80">
风险:{p.risks.join(";")}
</p>
)}
</div>
<div className="mt-1.5 flex gap-3 pl-4">
<button
type="button"
onClick={() => startEdit(p)}
className="text-[11px] text-patina hover:underline"
>
编辑假设/风险
</button>
<button
type="button"
onClick={() => remove(p)}
disabled={busyId === p.id}
className="text-[11px] text-cinnabar hover:underline disabled:opacity-50"
>
删除
</button>
</div>
</>
)}
</li>
))}
</ul>
{error && <p className="mt-2 text-xs text-cinnabar">{error}</p>}
</>
);
}
-29
View File
@@ -1,29 +0,0 @@
"use client";
import { useRouter } from "next/navigation";
import type { PivotHealthSummary } from "@/types/api";
import type { Pivot } from "@/types/domain";
import { Astrolabe } from "@/components/ui/Astrolabe";
/**
* 九枢星盘 · 司命台适配层。
* 渲染已抽取到 components/ui/Astrolabe(数据驱动、路由无关);
* 此处仅负责把枢点点击接到司命台枢详情路由。签名与旧实现一致,admin 页零改动。
*/
export function StarMap({
pivots,
className,
}: {
pivots: PivotHealthSummary[];
className?: string;
}) {
const router = useRouter();
return (
<Astrolabe
pivots={pivots}
className={className}
onSelectPivot={(pivot: Pivot) => router.push(`/admin/pivots/${pivot}`)}
/>
);
}
@@ -1,322 +0,0 @@
"use client";
import { useState } from "react";
import { adminApi } from "@/lib/api";
import type { WorldEntityType, WorldStateType } from "@/types/domain";
import type { WorldEntity } from "@/types/api";
import {
WORLD_ENTITY_TYPE_LABEL,
WORLD_STATE_TYPE_LABEL,
} from "@/lib/constants/labels";
import { cn } from "@/lib/utils/cn";
const STATE_OPTIONS: WorldStateType[] = [
"fact",
"observation",
"inference",
"hypothesis",
"stale",
];
/** 新增可选类型(不含 self——自身实体不可手动创建) */
const TYPE_OPTIONS: WorldEntityType[] = [
"person",
"object",
"place",
"device",
"project",
"task",
"concept",
];
const STATE_DOT: Record<WorldStateType, string> = {
fact: "bg-patina",
observation: "bg-patina",
inference: "bg-gold",
hypothesis: "bg-gold",
stale: "bg-cinnabar",
};
/**
* 河图枢世界实体列表 —— 世界模型治理(可增删改)。
* 区分事实与推断(state_type 着色);新增实体(非 self)、编辑名称/置信度、
* select 改状态类型、删除。推断与假设召回时降权,不得伪装成事实(契约字典 §11.2)。
*/
export function WorldEntityList({
entities: initial,
}: {
entities: WorldEntity[];
}) {
const [entities, setEntities] = useState(initial);
const [editingId, setEditingId] = useState<string | null>(null);
const [draft, setDraft] = useState<{ name: string; confidence: number }>({
name: "",
confidence: 0,
});
const [busyId, setBusyId] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [showAdd, setShowAdd] = useState(false);
const [newEntity, setNewEntity] = useState<{
type: WorldEntityType;
name: string;
}>({
type: "concept",
name: "",
});
function startEdit(e: WorldEntity) {
setEditingId(e.id);
setDraft({ name: e.name, confidence: e.confidence });
setError(null);
}
async function changeState(e: WorldEntity, next: WorldStateType) {
setBusyId(e.id);
setError(null);
try {
const updated = await adminApi.updateWorldEntity(e.id, {
state_type: next,
});
setEntities((prev) =>
prev.map((x) =>
x.id === e.id ? { ...x, state_type: updated.state_type } : x,
),
);
} catch (err) {
setError(err instanceof Error ? err.message : "更新失败,请重试。");
} finally {
setBusyId(null);
}
}
async function save(id: string) {
setBusyId(id);
setError(null);
try {
const updated = await adminApi.updateWorldEntity(id, {
name: draft.name,
confidence: draft.confidence,
});
setEntities((prev) =>
prev.map((x) =>
x.id === id
? { ...x, name: updated.name, confidence: updated.confidence }
: x,
),
);
setEditingId(null);
} catch (err) {
setError(err instanceof Error ? err.message : "保存失败,请重试。");
} finally {
setBusyId(null);
}
}
async function remove(e: WorldEntity) {
setBusyId(e.id);
setError(null);
try {
await adminApi.deleteWorldEntity(e.id);
setEntities((prev) => prev.filter((x) => x.id !== e.id));
} catch (err) {
setError(err instanceof Error ? err.message : "删除失败,请重试。");
} finally {
setBusyId(null);
}
}
async function add() {
if (!newEntity.name.trim()) {
setError("请填写实体名称。");
return;
}
setBusyId("__new");
setError(null);
try {
const created = await adminApi.createWorldEntity({
type: newEntity.type,
name: newEntity.name.trim(),
});
setEntities((prev) => [...prev, created]);
setNewEntity({ type: "concept", name: "" });
setShowAdd(false);
} catch (e) {
setError(e instanceof Error ? e.message : "新增失败,请重试。");
} finally {
setBusyId(null);
}
}
return (
<>
{showAdd ? (
<div className="glass mb-2 space-y-2 rounded-xl px-4 py-3">
<div className="flex flex-wrap items-center gap-2">
<select
value={newEntity.type}
onChange={(e) =>
setNewEntity((s) => ({
...s,
type: e.target.value as WorldEntityType,
}))
}
aria-label="实体类型"
className="w-24 shrink-0 rounded-lg border border-border-glass bg-glass px-2 py-1 text-xs text-fg backdrop-blur"
>
{TYPE_OPTIONS.map((t) => (
<option key={t} value={t}>
{WORLD_ENTITY_TYPE_LABEL[t]}
</option>
))}
</select>
<input
value={newEntity.name}
onChange={(e) =>
setNewEntity((s) => ({ ...s, name: e.target.value }))
}
placeholder="实体名称…"
className="min-w-0 flex-1 rounded-lg border border-border-glass bg-glass px-2 py-1 text-sm text-fg"
/>
<button
type="button"
onClick={add}
disabled={busyId === "__new"}
className="shrink-0 rounded-lg bg-patina/20 px-3 py-1 text-xs font-medium text-patina disabled:opacity-50"
>
新增
</button>
<button
type="button"
onClick={() => setShowAdd(false)}
className="shrink-0 rounded-lg border border-border-glass bg-glass px-3 py-1 text-xs text-muted"
>
取消
</button>
</div>
</div>
) : (
<button
type="button"
onClick={() => setShowAdd(true)}
className="mb-2 rounded-lg border border-patina/40 bg-patina/10 px-3 py-1.5 text-xs font-medium text-patina transition-colors hover:bg-patina/20"
>
+ 新增世界实体
</button>
)}
<ul className="space-y-2">
{entities.map((e) => (
<li key={e.id} className="glass rounded-xl px-4 py-2.5">
{editingId === e.id ? (
<div className="space-y-2">
<input
value={draft.name}
onChange={(ev) =>
setDraft((d) => ({ ...d, name: ev.target.value }))
}
className="w-full rounded-lg border border-border-glass bg-glass px-2 py-1 text-sm text-fg"
/>
<div className="flex items-center gap-2">
<label className="text-[10px] text-muted">置信度</label>
<input
type="number"
min={0}
max={1}
step={0.05}
value={draft.confidence}
onChange={(ev) =>
setDraft((d) => ({
...d,
confidence: Number(ev.target.value),
}))
}
className="w-20 rounded-lg border border-border-glass bg-glass px-2 py-1 text-xs text-fg"
/>
<div className="ml-auto flex gap-2">
<button
type="button"
onClick={() => save(e.id)}
disabled={busyId === e.id}
className="rounded-lg bg-patina/20 px-3 py-1 text-xs font-medium text-patina disabled:opacity-50"
>
保存
</button>
<button
type="button"
onClick={() => setEditingId(null)}
className="rounded-lg border border-border-glass bg-glass px-3 py-1 text-xs text-muted"
>
取消
</button>
</div>
</div>
</div>
) : (
<>
<div className="flex items-center gap-2">
<span
className={cn(
"h-2 w-2 shrink-0 rounded-full",
STATE_DOT[e.state_type],
)}
/>
<span className="rounded bg-gold/15 px-1.5 py-0.5 text-[10px] text-gold">
{WORLD_ENTITY_TYPE_LABEL[e.type]}
</span>
<span className="truncate font-serif text-sm font-medium text-fg">
{e.name}
</span>
<span className="ml-auto shrink-0 font-mono text-[10px] text-muted">
{e.confidence.toFixed(2)}
</span>
<select
value={e.state_type}
onChange={(ev) =>
changeState(e, ev.target.value as WorldStateType)
}
disabled={busyId === e.id}
aria-label={`${e.name} 状态类型`}
className="w-20 shrink-0 rounded-lg border border-border-glass bg-glass px-2 py-1 text-xs text-fg backdrop-blur disabled:opacity-50"
>
{STATE_OPTIONS.map((s) => (
<option key={s} value={s}>
{WORLD_STATE_TYPE_LABEL[s]}
</option>
))}
</select>
</div>
<div className="mt-1 flex flex-wrap gap-x-3 gap-y-0.5 pl-4 text-[10px] text-muted">
{Object.entries(e.attributes).map(([k, v]) => (
<span key={k}>
{k}:{v}
</span>
))}
</div>
<div className="mt-1.5 flex gap-3 pl-4">
<button
type="button"
onClick={() => startEdit(e)}
className="text-[11px] text-patina hover:underline"
>
编辑名称/置信度
</button>
{e.type !== "self" && (
<button
type="button"
onClick={() => remove(e)}
disabled={busyId === e.id}
className="text-[11px] text-cinnabar hover:underline disabled:opacity-50"
>
删除
</button>
)}
</div>
</>
)}
</li>
))}
</ul>
{error && <p className="mt-2 text-xs text-cinnabar">{error}</p>}
</>
);
}