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:
1 parent
b419e0afde
commit
c63b75152a
68 files changed
+1205
-4750
No files matched your search
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user