M3a:息壤三级记忆域——归真触发提取管线/白名单明忆/重忆确认/正影版本链/删除墓碑/双通道召回注入,退役旧记忆-抽取-反思-大周天-演化管线(迁移 004,验收 7 用例)
This commit is contained in:
1 parent
2a640a901b
commit
9c98c2f85e
69 files changed
+1193
-6033
No files matched your search
@@ -1,166 +0,0 @@
|
||||
import { EvolutionList } from "@/components/evolution/EvolutionList";
|
||||
import { PageContainer } from "@/components/layout/PageContainer";
|
||||
import { ErrorState } from "@/components/shared/StateViews";
|
||||
|
||||
import { serverApi } from "@/lib/api/server-client";
|
||||
import { describeError } from "@/lib/utils/error";
|
||||
import type { Character, EvolutionCandidate, Paginated } from "@/types/api";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
/**
|
||||
* 演化候选页 · 大周天自我评估生成的演化增量在此审核。
|
||||
* 设计稿 evolution-preview.html — 周天四镜 + 候选层积 + 朱砂待裁 + 应用沉积。
|
||||
* L4 核心变更(人格 / 使命 / 安全边界)需人工通过才写入;低风险类生成时已自动应用。
|
||||
*/
|
||||
export default async function EvolutionPage() {
|
||||
let characterId: string | null = null;
|
||||
let characterName = "";
|
||||
let initial: EvolutionCandidate[] = [];
|
||||
let counts = { pending: 0, applied: 0, rejected: 0 };
|
||||
let error: string | null = null;
|
||||
|
||||
try {
|
||||
const active = await serverApi.get<Character>("/characters/active");
|
||||
characterId = active.id;
|
||||
characterName = active.display_name || active.name;
|
||||
const [pending, applied, rejected] = await Promise.all([
|
||||
serverApi.get<Paginated<EvolutionCandidate>>(
|
||||
`/characters/${active.id}/evolution-candidates`,
|
||||
{ status: "pending", page_size: 50 },
|
||||
),
|
||||
serverApi.get<Paginated<EvolutionCandidate>>(
|
||||
`/characters/${active.id}/evolution-candidates`,
|
||||
{ status: "applied", page_size: 1 },
|
||||
),
|
||||
serverApi.get<Paginated<EvolutionCandidate>>(
|
||||
`/characters/${active.id}/evolution-candidates`,
|
||||
{ status: "rejected", page_size: 1 },
|
||||
),
|
||||
]);
|
||||
initial = pending.items;
|
||||
counts = {
|
||||
pending: pending.total,
|
||||
applied: applied.total,
|
||||
rejected: rejected.total,
|
||||
};
|
||||
} catch (e) {
|
||||
error = describeError(e);
|
||||
}
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
title="演化"
|
||||
description="大周天 · 演化候选"
|
||||
meta={
|
||||
<>
|
||||
{characterName && (
|
||||
<span className="char-pill">
|
||||
<span className="avatar-mini">{characterName.charAt(0)}</span>
|
||||
{characterName}
|
||||
</span>
|
||||
)}
|
||||
<span className="page-meta">
|
||||
待裁 <b>{counts.pending}</b> · 半自动{" "}
|
||||
<b style={{ color: "var(--patina)" }}>{counts.applied}</b> · 已应用{" "}
|
||||
<b>{counts.applied}</b>
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
maxWidth="max-w-5xl"
|
||||
>
|
||||
{error ? (
|
||||
<ErrorState message={error} />
|
||||
) : characterId ? (
|
||||
<div className="space-y-5">
|
||||
{/* 周天四镜(签名) */}
|
||||
<div className="cycle-card">
|
||||
<div className="cycle-head">
|
||||
<span className="cycle-title">大周天 · 候选阶段</span>
|
||||
<span className="cycle-id">
|
||||
cycle_01JZK7Q3B…4F · 触发自会话结束
|
||||
</span>
|
||||
<span className="cycle-state">候选生成中</span>
|
||||
</div>
|
||||
<div className="mirror-chain">
|
||||
<div className="mc-step done">
|
||||
<div className="mc-mirror">
|
||||
<div className="disc">评</div>
|
||||
</div>
|
||||
<span className="mc-name">评估</span>
|
||||
<span className="mc-sub">evaluation</span>
|
||||
<span className="mc-yield">态势 ✓</span>
|
||||
</div>
|
||||
<span className="mc-link done"></span>
|
||||
<div className="mc-step done">
|
||||
<div className="mc-mirror">
|
||||
<div className="disc">演</div>
|
||||
</div>
|
||||
<span className="mc-name">演化</span>
|
||||
<span className="mc-sub">evolution</span>
|
||||
<span className="mc-yield">策略 ✓</span>
|
||||
</div>
|
||||
<span className="mc-link done"></span>
|
||||
<div className="mc-step active">
|
||||
<div className="mc-mirror">
|
||||
<div className="disc">候</div>
|
||||
</div>
|
||||
<span className="mc-name">候选</span>
|
||||
<span className="mc-sub">candidate · 当阶</span>
|
||||
<span className="mc-yield">{counts.pending} 条草拟</span>
|
||||
</div>
|
||||
<span className="mc-link"></span>
|
||||
<div className="mc-step">
|
||||
<div className="mc-mirror">
|
||||
<div className="disc">藏</div>
|
||||
</div>
|
||||
<span className="mc-name">入藏</span>
|
||||
<span className="mc-sub">memory · 未达</span>
|
||||
<span className="mc-yield">-</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="snapshot-row">
|
||||
<div className="snap-mini">
|
||||
<div className="k">最近记忆</div>
|
||||
<div className="v">
|
||||
12<small>条入参</small>
|
||||
</div>
|
||||
</div>
|
||||
<div className="snap-mini">
|
||||
<div className="k">人格版本</div>
|
||||
<div className="v">
|
||||
v7<small>当前</small>
|
||||
</div>
|
||||
</div>
|
||||
<div className="snap-mini">
|
||||
<div className="k">世界实体</div>
|
||||
<div className="v">
|
||||
—<small>个</small>
|
||||
</div>
|
||||
</div>
|
||||
<div className="snap-mini">
|
||||
<div className="k">内驱力</div>
|
||||
<div className="v">
|
||||
{counts.pending}
|
||||
<small>条好奇</small>
|
||||
</div>
|
||||
</div>
|
||||
<div className="snap-verdict">
|
||||
<b>评估结论:</b>近 12
|
||||
条记忆呈「回应偏长」信号,可收敛风格字段;身份画稳定,无核心变更必要。建议生成
|
||||
1 条风格类半自动候选,其余维持现状。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<EvolutionList
|
||||
characterId={characterId}
|
||||
initial={initial}
|
||||
counts={counts}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { PageContainer } from "@/components/layout/PageContainer";
|
||||
import { ErrorState } from "@/components/shared/StateViews";
|
||||
import { MemoryList } from "@/components/memory/MemoryList";
|
||||
|
||||
import { serverApi } from "@/lib/api/server-client";
|
||||
import { describeError } from "@/lib/utils/error";
|
||||
import type { Character, Memory, Paginated } from "@/types/api";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function MemoryPage() {
|
||||
let memories: Memory[] = [];
|
||||
let characterId: string | undefined;
|
||||
let error: string | null = null;
|
||||
|
||||
try {
|
||||
const active = await serverApi
|
||||
.get<Character>("/characters/active")
|
||||
.catch(() => null);
|
||||
characterId = active?.id;
|
||||
const data = await serverApi.get<Paginated<Memory>>("/memory", {
|
||||
page_size: 100,
|
||||
});
|
||||
memories = data.items;
|
||||
} catch (e) {
|
||||
error = describeError(e);
|
||||
}
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
title="记忆"
|
||||
description="当前人物的长期记忆,支持手动新增、编辑与删除。"
|
||||
>
|
||||
{error ? (
|
||||
<ErrorState message={error} />
|
||||
) : (
|
||||
<MemoryList initialMemories={memories} characterId={characterId} />
|
||||
)}
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import { PageContainer } from "@/components/layout/PageContainer";
|
||||
import { ErrorState, EmptyState } from "@/components/shared/StateViews";
|
||||
import { MemoryAdminList } from "@/components/admin/MemoryAdminList";
|
||||
|
||||
import { serverApi } from "@/lib/api/server-client";
|
||||
import { describeError } from "@/lib/utils/error";
|
||||
import type { Memory, Paginated } from "@/types/api";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
/**
|
||||
* 息壤枢 · 长期记忆。
|
||||
*
|
||||
* 取所有记忆列表,经 MemoryAdminList 组件做删除/详情查看。
|
||||
* 数据来自 GET /memory(API §8)。
|
||||
*/
|
||||
export default async function AdminMemoryPage() {
|
||||
let memories: Memory[] = [];
|
||||
let error: string | null = null;
|
||||
|
||||
try {
|
||||
const data = await serverApi.get<Paginated<Memory>>("/memory", {
|
||||
page_size: 100,
|
||||
});
|
||||
memories = data.items;
|
||||
} catch (e) {
|
||||
error = describeError(e);
|
||||
}
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
title="息壤枢"
|
||||
description="长期记忆 · 自生不息"
|
||||
maxWidth="max-w-3xl"
|
||||
>
|
||||
{error ? (
|
||||
<ErrorState message={error} />
|
||||
) : memories.length === 0 ? (
|
||||
<EmptyState
|
||||
title="暂无记忆"
|
||||
description="当前人物还没有沉淀任何长期记忆。"
|
||||
/>
|
||||
) : (
|
||||
<MemoryAdminList initial={memories} />
|
||||
)}
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
@@ -6,7 +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 { MemoryAdminList } from "@/components/admin/MemoryAdminList";
|
||||
import { PersonalityEditor } from "@/components/admin/PersonalityEditor";
|
||||
import { GoalTree } from "@/components/admin/GoalTree";
|
||||
import { PerceptionConsentMatrix } from "@/components/admin/PerceptionConsentMatrix";
|
||||
@@ -46,7 +45,6 @@ const VALID_PIVOTS: Pivot[] = [
|
||||
"perception",
|
||||
"world_model",
|
||||
"planner",
|
||||
"memory",
|
||||
"tool_runtime",
|
||||
"agent_society",
|
||||
];
|
||||
@@ -74,20 +72,6 @@ export default async function PivotDetailPage({
|
||||
return <ToolRuntimeDetail />;
|
||||
case "personality_core":
|
||||
return <PersonalityCoreDetail />;
|
||||
case "memory":
|
||||
return <MemorySystemDetail />;
|
||||
case "goal_orchestrator":
|
||||
return <GoalOrchestratorDetail />;
|
||||
case "perception":
|
||||
return <PerceptionDetail />;
|
||||
case "world_model":
|
||||
return <WorldModelDetail />;
|
||||
case "planner":
|
||||
return <PlannerDetail />;
|
||||
case "agent_society":
|
||||
return <AgentSocietyDetail />;
|
||||
default:
|
||||
notFound();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,39 +269,6 @@ async function PersonalityCoreDetail() {
|
||||
);
|
||||
}
|
||||
|
||||
/** 息壤枢:长期记忆列表(自生不息) */
|
||||
async function MemorySystemDetail() {
|
||||
let memories: Memory[] = [];
|
||||
let error: string | null = null;
|
||||
try {
|
||||
const data = await serverApi.get<Paginated<Memory>>("/memory", {
|
||||
page_size: 100,
|
||||
});
|
||||
memories = data.items;
|
||||
} catch (e) {
|
||||
error = describeError(e);
|
||||
}
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
title="息壤枢"
|
||||
description="长期记忆 · 自生不息"
|
||||
maxWidth="max-w-3xl"
|
||||
>
|
||||
{error ? (
|
||||
<ErrorState message={error} />
|
||||
) : memories.length === 0 ? (
|
||||
<EmptyState
|
||||
title="暂无记忆"
|
||||
description="当前人物还没有沉淀任何长期记忆。"
|
||||
/>
|
||||
) : (
|
||||
<MemoryAdminList initial={memories} />
|
||||
)}
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
/** 太一枢:目标层级树(最高目标 → 任务,可编辑标题/优先级/状态) */
|
||||
async function GoalOrchestratorDetail() {
|
||||
let goals: GoalNode[] = [];
|
||||
|
||||
@@ -1,251 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { memoryApi } from "@/lib/api";
|
||||
import type { MemoryType } from "@/types/domain";
|
||||
import type { Memory } from "@/types/api";
|
||||
import { MEMORY_TYPE_LABEL, MEMORY_SOURCE_LABEL } from "@/lib/constants/labels";
|
||||
|
||||
const TYPE_OPTIONS: MemoryType[] = [
|
||||
"identity",
|
||||
"user",
|
||||
"episodic",
|
||||
"semantic",
|
||||
"procedural",
|
||||
"relational",
|
||||
"emotional",
|
||||
"world",
|
||||
"reflection",
|
||||
"evolution",
|
||||
];
|
||||
|
||||
/**
|
||||
* 息壤枢记忆治理列表(可增删改)。
|
||||
* 治理者可补录记忆(admin 校正/手动沉淀)、inline 编辑、软删除。
|
||||
* 复用 §10 memoryApi.createMemory/updateMemory/deleteMemory。
|
||||
*/
|
||||
export function MemoryAdminList({ initial }: { initial: Memory[] }) {
|
||||
const [memories, setMemories] = useState(initial);
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [draft, setDraft] = useState({ title: "", content: "" });
|
||||
const [busyId, setBusyId] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showAdd, setShowAdd] = useState(false);
|
||||
const [newMem, setNewMem] = useState<{
|
||||
type: MemoryType;
|
||||
title: string;
|
||||
content: string;
|
||||
}>({
|
||||
type: "user",
|
||||
title: "",
|
||||
content: "",
|
||||
});
|
||||
|
||||
function startEdit(m: Memory) {
|
||||
setEditingId(m.id);
|
||||
setDraft({ title: m.title ?? "", content: m.content });
|
||||
setError(null);
|
||||
}
|
||||
|
||||
async function save(id: string) {
|
||||
setBusyId(id);
|
||||
setError(null);
|
||||
try {
|
||||
const updated = await memoryApi.updateMemory(id, draft);
|
||||
setMemories((prev) =>
|
||||
prev.map((m) => (m.id === id ? { ...m, ...updated } : m)),
|
||||
);
|
||||
setEditingId(null);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "保存失败,请重试。");
|
||||
} finally {
|
||||
setBusyId(null);
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(id: string) {
|
||||
setBusyId(id);
|
||||
setError(null);
|
||||
try {
|
||||
await memoryApi.deleteMemory(id);
|
||||
setMemories((prev) => prev.filter((m) => m.id !== id));
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "删除失败,请重试。");
|
||||
} finally {
|
||||
setBusyId(null);
|
||||
}
|
||||
}
|
||||
|
||||
async function add() {
|
||||
if (!newMem.content.trim()) {
|
||||
setError("请填写记忆内容。");
|
||||
return;
|
||||
}
|
||||
setBusyId("__new");
|
||||
setError(null);
|
||||
try {
|
||||
const created = await memoryApi.createMemory({
|
||||
type: newMem.type,
|
||||
title: newMem.title.trim() || undefined,
|
||||
content: newMem.content.trim(),
|
||||
});
|
||||
setMemories((prev) => [created, ...prev]);
|
||||
setNewMem({ type: "user", title: "", content: "" });
|
||||
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={newMem.type}
|
||||
onChange={(e) =>
|
||||
setNewMem((s) => ({ ...s, type: e.target.value as MemoryType }))
|
||||
}
|
||||
aria-label="记忆类型"
|
||||
className="w-20 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}>
|
||||
{MEMORY_TYPE_LABEL[t]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
value={newMem.title}
|
||||
onChange={(e) =>
|
||||
setNewMem((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"
|
||||
/>
|
||||
</div>
|
||||
<textarea
|
||||
value={newMem.content}
|
||||
onChange={(e) =>
|
||||
setNewMem((s) => ({ ...s, content: e.target.value }))
|
||||
}
|
||||
rows={2}
|
||||
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={busyId === "__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="space-y-2">
|
||||
{memories.map((m) => (
|
||||
<li key={m.id} className="glass rounded-xl px-4 py-2.5">
|
||||
{editingId === m.id ? (
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
value={draft.title}
|
||||
onChange={(e) =>
|
||||
setDraft((d) => ({ ...d, title: e.target.value }))
|
||||
}
|
||||
placeholder="标题"
|
||||
className="w-full rounded-lg border border-border-glass bg-glass px-2 py-1 text-sm text-fg"
|
||||
/>
|
||||
<textarea
|
||||
value={draft.content}
|
||||
onChange={(e) =>
|
||||
setDraft((d) => ({ ...d, content: e.target.value }))
|
||||
}
|
||||
rows={3}
|
||||
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={() => save(m.id)}
|
||||
disabled={busyId === m.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="flex items-baseline gap-2">
|
||||
<span className="rounded bg-patina/15 px-1.5 py-0.5 text-[10px] text-patina">
|
||||
{MEMORY_TYPE_LABEL[m.type]}
|
||||
</span>
|
||||
<span className="truncate font-serif text-sm font-medium text-fg">
|
||||
{m.title ?? m.content.slice(0, 16)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 line-clamp-2 text-xs text-muted">
|
||||
{m.content}
|
||||
</p>
|
||||
<div className="mt-1.5 flex items-center gap-3 text-[10px] text-muted">
|
||||
<span>{MEMORY_SOURCE_LABEL[m.source]}</span>
|
||||
{m.confidence != null && (
|
||||
<span>置信 {m.confidence.toFixed(2)}</span>
|
||||
)}
|
||||
<div className="ml-auto flex gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => startEdit(m)}
|
||||
className="text-patina hover:underline"
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => remove(m.id)}
|
||||
disabled={busyId === m.id}
|
||||
className="text-cinnabar hover:underline disabled:opacity-50"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{error && <p className="mt-2 text-xs text-cinnabar">{error}</p>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,255 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import type {
|
||||
EvolutionCandidate,
|
||||
EvolutionCandidateType,
|
||||
EvolutionScope,
|
||||
} from "@/types/api";
|
||||
import { cn } from "@/lib/utils/cn";
|
||||
|
||||
const TYPE_LABEL: Record<EvolutionCandidateType, string> = {
|
||||
personality: "人格",
|
||||
personality_weights: "人格权重",
|
||||
retrieval_strategy: "召回策略",
|
||||
tool_affinity: "工具偏好",
|
||||
memory: "记忆",
|
||||
tool_strategy: "工具策略",
|
||||
safety_rule: "安全规则",
|
||||
world_model: "世界模型",
|
||||
procedural: "程序记忆",
|
||||
mood: "心情",
|
||||
};
|
||||
|
||||
const SCOPE_LABEL: Record<EvolutionScope, string> = {
|
||||
character_scope: "人物域",
|
||||
user_scope: "用户域",
|
||||
task_scope: "任务域",
|
||||
global_scope: "全局",
|
||||
};
|
||||
|
||||
/** 候选字符映射(与设计稿 cand-glyph 示例一致) */
|
||||
function glyphForType(type: string): string {
|
||||
const map: Record<string, string> = {
|
||||
personality: "本",
|
||||
personality_weights: "权",
|
||||
retrieval_strategy: "召",
|
||||
tool_affinity: "工",
|
||||
memory: "忆",
|
||||
tool_strategy: "策",
|
||||
safety_rule: "安",
|
||||
world_model: "世",
|
||||
procedural: "序",
|
||||
mood: "情",
|
||||
};
|
||||
return map[type] ?? "·";
|
||||
}
|
||||
|
||||
export function CandidateCard({
|
||||
candidate,
|
||||
onApprove,
|
||||
onReject,
|
||||
busy,
|
||||
}: {
|
||||
candidate: EvolutionCandidate;
|
||||
onApprove: (id: string) => void;
|
||||
onReject: (id: string) => void;
|
||||
busy: boolean;
|
||||
}) {
|
||||
const isPending = candidate.status === "pending";
|
||||
const isL4 = candidate.level === "L4";
|
||||
const isAuto = candidate.auto_apply_allowed && !isL4;
|
||||
|
||||
return (
|
||||
<div className={cn(isL4 ? "proposal" : "cand-card", isAuto && "auto")}>
|
||||
{isL4 ? (
|
||||
/* L4 朱砂待裁样式 */
|
||||
<>
|
||||
<div className="prop-head">
|
||||
<span className="prop-seal">朱批待裁</span>
|
||||
<span className="prop-type">
|
||||
{TYPE_LABEL[candidate.type]} · {candidate.level}
|
||||
</span>
|
||||
<span className="prop-risk">L4 · 满朱 · 须人工</span>
|
||||
</div>
|
||||
<div className="prop-title">{candidate.title}</div>
|
||||
<div className="prop-reason">{candidate.proposal}</div>
|
||||
{Object.entries(candidate.delta ?? {}).length > 0 && (
|
||||
<div className="prop-diff">
|
||||
{Object.entries(candidate.delta ?? {}).map(([k, v]) => (
|
||||
<div key={k}>
|
||||
<span className="field-tag">{k}</span>
|
||||
{typeof v === "string" ? (
|
||||
<>
|
||||
<span className="del">{v}</span>
|
||||
<br />
|
||||
<span className="add">{v}</span>
|
||||
</>
|
||||
) : (
|
||||
<span>{JSON.stringify(v)}</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
/* 普通/半自动候选 */
|
||||
<>
|
||||
<div className="cand-top">
|
||||
<div className="cand-glyph">{glyphForType(candidate.type)}</div>
|
||||
<span className="cand-title">{candidate.title}</span>
|
||||
<span className="cand-tag">
|
||||
{TYPE_LABEL[candidate.type]} · {SCOPE_LABEL[candidate.scope]}
|
||||
</span>
|
||||
{isAuto && (
|
||||
<span className="cand-judge auto">
|
||||
{candidate.level} · 已应用
|
||||
</span>
|
||||
)}
|
||||
{!isAuto && !isPending && (
|
||||
<span className="cand-judge manual">{candidate.status}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="cand-desc">{candidate.proposal}</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 三维评分(score-trio) */}
|
||||
{!isL4 && (
|
||||
<div className="score-trio">
|
||||
<div className="score benefit">
|
||||
<div className="k">收益</div>
|
||||
<div className="track">
|
||||
<div
|
||||
className="fill"
|
||||
style={{
|
||||
width: `${Math.round(Math.max(0, Math.min(1, candidate.benefit_score)) * 100)}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="tv">{candidate.benefit_score.toFixed(2)}</div>
|
||||
</div>
|
||||
<div className="score risk">
|
||||
<div className="k">风险</div>
|
||||
<div className="track">
|
||||
<div
|
||||
className="fill"
|
||||
style={{
|
||||
width: `${Math.round(Math.max(0, Math.min(1, candidate.risk_score)) * 100)}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="tv">{candidate.risk_score.toFixed(2)}</div>
|
||||
</div>
|
||||
<div className="score conf">
|
||||
<div className="k">置信</div>
|
||||
<div className="track">
|
||||
<div
|
||||
className="fill"
|
||||
style={{
|
||||
width: `${Math.round(Math.max(0, Math.min(1, candidate.confidence)) * 100)}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="tv">{candidate.confidence.toFixed(2)}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 证据引用 */}
|
||||
{candidate.evidence_ids.length > 0 && (
|
||||
<div className="prop-foot" style={{ marginTop: isL4 ? 0 : 8 }}>
|
||||
{candidate.evidence_ids.slice(0, 4).map((id) => (
|
||||
<span key={id} className="evi-link">
|
||||
{id.slice(0, 10)}…
|
||||
</span>
|
||||
))}
|
||||
{candidate.evidence_ids.length > 4 && (
|
||||
<span className="evi-link">
|
||||
+{candidate.evidence_ids.length - 4}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 操作区 */}
|
||||
{isPending && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
justifyContent: "flex-end",
|
||||
paddingTop: isL4 ? 12 : 8,
|
||||
borderTop: isL4 ? "none" : "1px dashed var(--rule)",
|
||||
marginTop: isL4 ? 8 : 0,
|
||||
}}
|
||||
>
|
||||
{isL4 && (
|
||||
<span
|
||||
style={{
|
||||
fontSize: 11,
|
||||
color: "var(--fg-faint)",
|
||||
marginRight: "auto",
|
||||
fontFamily: '"JetBrains Mono", ui-monospace, monospace',
|
||||
}}
|
||||
>
|
||||
L4 须人工裁定方可写入
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onReject(candidate.id)}
|
||||
disabled={busy}
|
||||
className="vbtn"
|
||||
style={{
|
||||
borderColor: isL4 ? "var(--cinnabar)" : "var(--rule)",
|
||||
color: isL4 ? "var(--cinnabar)" : "var(--fg)",
|
||||
background: isL4 ? "rgba(184,57,46,0.08)" : "transparent",
|
||||
}}
|
||||
>
|
||||
驳回
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onApprove(candidate.id)}
|
||||
disabled={busy}
|
||||
className="vbtn"
|
||||
style={{
|
||||
borderColor: isL4 ? "var(--cinnabar)" : "var(--patina-dim)",
|
||||
color: isL4 ? "var(--cinnabar)" : "var(--patina)",
|
||||
background: isL4 ? "rgba(184,57,46,0.08)" : "var(--patina-wash)",
|
||||
}}
|
||||
>
|
||||
{busy ? "处理中…" : isL4 ? "准予写入" : "通过并应用"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isPending && !isL4 && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
paddingTop: 8,
|
||||
borderTop: "1px solid var(--rule)",
|
||||
marginTop: 8,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={
|
||||
candidate.status === "applied"
|
||||
? "cand-judge auto"
|
||||
: "cand-judge manual"
|
||||
}
|
||||
>
|
||||
{candidate.status === "applied"
|
||||
? `已应用 · ${candidate.resulting_target_id?.slice(0, 12) ?? ""}`
|
||||
: "已驳回"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,248 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { EmptyState, ErrorState } from "@/components/shared/StateViews";
|
||||
import { evolutionApi } from "@/lib/api";
|
||||
import type { EvolutionCandidate } from "@/types/api";
|
||||
import { describeError } from "@/lib/utils/error";
|
||||
import { cn } from "@/lib/utils/cn";
|
||||
import { CardGridSkeleton } from "@/components/ui/Skeleton";
|
||||
|
||||
import { CandidateCard } from "./CandidateCard";
|
||||
|
||||
type FilterStatus = "pending" | "applied" | "rejected";
|
||||
|
||||
const FILTERS: Array<{ key: FilterStatus; label: string }> = [
|
||||
{ key: "pending", label: "待审核" },
|
||||
{ key: "applied", label: "已应用" },
|
||||
{ key: "rejected", label: "已驳回" },
|
||||
];
|
||||
|
||||
/** 应用沉积示例数据(与设计稿对齐) */
|
||||
const STRATA = [
|
||||
{
|
||||
ver: "风格简练 +1",
|
||||
when: "07-12",
|
||||
reason: "expression_style · 半自动 · 源自大周天",
|
||||
rollback: true,
|
||||
rejected: false,
|
||||
},
|
||||
{
|
||||
ver: "深夜不打扰",
|
||||
when: "07-09",
|
||||
reason: "procedural · 半自动",
|
||||
rollback: false,
|
||||
rejected: false,
|
||||
},
|
||||
{
|
||||
ver: "人格权重校准",
|
||||
when: "06-28",
|
||||
reason: "personality_weights · 尽责 +0.05",
|
||||
rollback: true,
|
||||
rejected: false,
|
||||
},
|
||||
{
|
||||
ver: "工具亲和调整",
|
||||
when: "06-12",
|
||||
reason: "tool_strategy · 驳回 · 证据不足",
|
||||
rollback: false,
|
||||
rejected: true,
|
||||
},
|
||||
{
|
||||
ver: "初始种魂",
|
||||
when: "04 月",
|
||||
reason: "自模板而来 · v1",
|
||||
rollback: false,
|
||||
rejected: false,
|
||||
faded: true,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* 演化候选审核列表(设计稿 evolution-preview.html):下区分两栏。
|
||||
* 左栏 cand-main = 候选列表(待裁 + 半自动);右栏 strata-rail = 应用沉积时间线。
|
||||
*/
|
||||
export function EvolutionList({
|
||||
characterId,
|
||||
initial,
|
||||
counts: initialCounts,
|
||||
}: {
|
||||
characterId: string;
|
||||
initial: EvolutionCandidate[];
|
||||
counts: Record<FilterStatus, number>;
|
||||
}) {
|
||||
const [filter, setFilter] = useState<FilterStatus>("pending");
|
||||
const [candidates, setCandidates] = useState(initial);
|
||||
const [counts, setCounts] = useState(initialCounts);
|
||||
const [busyId, setBusyId] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
async function switchFilter(next: FilterStatus) {
|
||||
if (next === filter) return;
|
||||
setFilter(next);
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await evolutionApi.listCandidates(characterId, {
|
||||
status: next,
|
||||
page_size: 50,
|
||||
});
|
||||
setCandidates(res.items);
|
||||
} catch (e) {
|
||||
setError(describeError(e));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function resolve(id: string, action: "approve" | "reject") {
|
||||
setBusyId(id);
|
||||
setError(null);
|
||||
try {
|
||||
if (action === "approve") {
|
||||
await evolutionApi.approveCandidate(id);
|
||||
setCounts((c) => ({
|
||||
...c,
|
||||
pending: Math.max(0, c.pending - 1),
|
||||
applied: c.applied + 1,
|
||||
}));
|
||||
} else {
|
||||
await evolutionApi.rejectCandidate(id);
|
||||
setCounts((c) => ({
|
||||
...c,
|
||||
pending: Math.max(0, c.pending - 1),
|
||||
rejected: c.rejected + 1,
|
||||
}));
|
||||
}
|
||||
setCandidates((list) => list.filter((c) => c.id !== id));
|
||||
} catch (e) {
|
||||
setError(describeError(e));
|
||||
} finally {
|
||||
setBusyId(null);
|
||||
}
|
||||
}
|
||||
|
||||
/** 区分 L4 与普通候选 */
|
||||
const l4Candidates = candidates.filter((c) => c.level === "L4");
|
||||
const normalCandidates = candidates.filter((c) => c.level !== "L4");
|
||||
|
||||
return (
|
||||
<div className="lower-region">
|
||||
{/* 候选主区 */}
|
||||
<div className="cand-main">
|
||||
{/* 筛选按钮 */}
|
||||
<div className="flex gap-1">
|
||||
{FILTERS.map(({ key, label }) => (
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
onClick={() => switchFilter(key)}
|
||||
className={cn(
|
||||
"rounded-lg px-3 py-1.5 text-xs font-medium transition-colors",
|
||||
filter === key
|
||||
? "glass-strong text-patina"
|
||||
: "text-muted hover:text-fg",
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
<span className="ml-1.5 tabular-nums text-muted">
|
||||
{counts[key]}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{error && <ErrorState message={error} />}
|
||||
|
||||
{loading ? (
|
||||
<CardGridSkeleton count={3} className="py-6" />
|
||||
) : candidates.length === 0 ? (
|
||||
<EmptyState
|
||||
title={
|
||||
filter === "pending" ? "暂无待审核的演化候选" : "暂无候选记录"
|
||||
}
|
||||
description={
|
||||
filter === "pending"
|
||||
? "大周天自我评估后会把可演化的增量生成到这里,L4 核心变更需你确认,低风险类已自动应用。"
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{/* L4 朱砂待裁 */}
|
||||
{l4Candidates.map((c) => (
|
||||
<CandidateCard
|
||||
key={c.id}
|
||||
candidate={c}
|
||||
onApprove={(id) => resolve(id, "approve")}
|
||||
onReject={(id) => resolve(id, "reject")}
|
||||
busy={busyId === c.id}
|
||||
/>
|
||||
))}
|
||||
{/* 普通候选 */}
|
||||
{normalCandidates.map((c) => (
|
||||
<CandidateCard
|
||||
key={c.id}
|
||||
candidate={c}
|
||||
onApprove={(id) => resolve(id, "approve")}
|
||||
onReject={(id) => resolve(id, "reject")}
|
||||
busy={busyId === c.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 应用沉积侧栏 */}
|
||||
<div className="strata-rail">
|
||||
<div className="strata-card">
|
||||
<h3
|
||||
style={{
|
||||
fontFamily: '"Songti SC", serif',
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
color: "var(--fg)",
|
||||
marginBottom: 12,
|
||||
}}
|
||||
>
|
||||
应用沉积{" "}
|
||||
<span
|
||||
style={{
|
||||
fontSize: 12,
|
||||
fontWeight: 400,
|
||||
color: "var(--fg-dim)",
|
||||
fontFamily: '"JetBrains Mono", ui-monospace, monospace',
|
||||
marginLeft: 6,
|
||||
}}
|
||||
>
|
||||
演化之层积
|
||||
</span>
|
||||
</h3>
|
||||
<div className="strata-list">
|
||||
{STRATA.map((s, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={cn("stratum", s.rejected && "rejected")}
|
||||
style={s.faded ? { opacity: 0.6 } : undefined}
|
||||
>
|
||||
<span className="node"></span>
|
||||
<div className="stratum-head">
|
||||
<span className="stratum-ver">{s.ver}</span>
|
||||
<span className="stratum-when">{s.when}</span>
|
||||
</div>
|
||||
<div className="stratum-reason">{s.reason}</div>
|
||||
{s.rollback && (
|
||||
<button type="button" className="stratum-rollback">
|
||||
回滚此层
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -11,8 +11,6 @@ import {
|
||||
CharacterIcon,
|
||||
CosmosIcon,
|
||||
DriveIcon,
|
||||
EvolutionIcon,
|
||||
MemoryIcon,
|
||||
ModelIcon,
|
||||
PersonalityIcon,
|
||||
RunIcon,
|
||||
@@ -65,7 +63,6 @@ const NAV_GROUPS: NavGroup[] = [
|
||||
{ href: "/wuxing", label: "五行", icon: <WuxingIcon /> },
|
||||
{ href: "/characters", label: "人物", icon: <CharacterIcon /> },
|
||||
{ href: "/personality", label: "人格", icon: <PersonalityIcon /> },
|
||||
{ href: "/evolution", label: "演化", icon: <EvolutionIcon /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -87,10 +84,7 @@ const NAV_GROUPS: NavGroup[] = [
|
||||
},
|
||||
{
|
||||
title: "藏识",
|
||||
items: [
|
||||
{ href: "/memory", label: "记忆", icon: <MemoryIcon /> },
|
||||
{ href: "/archive", label: "归档", icon: <ArchiveIcon /> },
|
||||
],
|
||||
items: [{ href: "/archive", label: "归档", icon: <ArchiveIcon /> }],
|
||||
},
|
||||
{
|
||||
title: "百工",
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { SensitivityBadge } from "@/components/ui/StatusBadge";
|
||||
import { describeError } from "@/lib/utils/error";
|
||||
import { MEMORY_SOURCE_LABEL, MEMORY_TYPE_LABEL } from "@/lib/constants/labels";
|
||||
import { memoryApi } from "@/lib/api";
|
||||
import { formatDateTime, formatPercent } from "@/lib/utils/format";
|
||||
import { cn } from "@/lib/utils/cn";
|
||||
import type { CreateMemoryRequest, Memory } from "@/types/api";
|
||||
import { MemoryEditor } from "./MemoryEditor";
|
||||
|
||||
/** 单条记忆卡片(设计稿 memory-preview.html §mem-card)
|
||||
* 类型色标(.mem-type)、置信度/重要性进度条(.meta-bar .fill)、
|
||||
* 敏感度徽章、证据来源(.mem-foot)*/
|
||||
export function MemoryCard({
|
||||
memory,
|
||||
characterId,
|
||||
onChanged,
|
||||
onError,
|
||||
}: {
|
||||
memory: Memory;
|
||||
characterId?: string;
|
||||
onChanged: (kind: "update" | "delete", payload: Memory | string) => void;
|
||||
onError: (message: string) => void;
|
||||
}) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [confirming, setConfirming] = useState(false);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const handleUpdate = async (body: CreateMemoryRequest) => {
|
||||
setBusy(true);
|
||||
try {
|
||||
const updated = await memoryApi.updateMemory(memory.id, body);
|
||||
onChanged("update", updated);
|
||||
setEditing(false);
|
||||
} catch (err) {
|
||||
onError(describeError(err));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
setBusy(true);
|
||||
try {
|
||||
await memoryApi.deleteMemory(memory.id);
|
||||
onChanged("delete", memory.id);
|
||||
} catch (err) {
|
||||
onError(describeError(err));
|
||||
setConfirming(false);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (editing) {
|
||||
return (
|
||||
<MemoryEditor
|
||||
characterId={characterId}
|
||||
initial={memory}
|
||||
onSubmit={handleUpdate}
|
||||
onCancel={() => setEditing(false)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const sensClass = memory.sensitivity === "high" ? "high-sens" : "";
|
||||
|
||||
return (
|
||||
<div className={cn("mem-card", sensClass)}>
|
||||
{/* 标题行(设计稿 .mem-head) */}
|
||||
<div className="mem-head">
|
||||
<span className="mem-title">{memory.title || "无标题记忆"}</span>
|
||||
<span className={cn("mem-type", memory.type)}>
|
||||
{MEMORY_TYPE_LABEL[memory.type] ?? memory.type}
|
||||
</span>
|
||||
<SensitivityBadge level={memory.sensitivity} />
|
||||
</div>
|
||||
|
||||
{/* 记忆内容(设计稿 .mem-content) */}
|
||||
<p className="mem-content">{memory.content}</p>
|
||||
|
||||
{/* 元数据行:置信度 + 重要性进度条(设计稿 .mem-meta) */}
|
||||
<div className="mem-meta">
|
||||
<span className="meta-item conf">
|
||||
<span className="lab">置信</span>
|
||||
<span className="meta-bar">
|
||||
<span
|
||||
className="fill"
|
||||
style={{
|
||||
width: `${Math.round((memory.confidence ?? 0) * 100)}%`,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<span className="vv">{formatPercent(memory.confidence)}</span>
|
||||
</span>
|
||||
<span className="meta-item imp">
|
||||
<span className="lab">重要</span>
|
||||
<span className="meta-bar">
|
||||
<span
|
||||
className="fill"
|
||||
style={{
|
||||
width: `${Math.round((memory.importance ?? 0) * 100)}%`,
|
||||
background: "var(--gilt)",
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<span className="vv">{formatPercent(memory.importance)}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 脚注(设计稿 .mem-foot) */}
|
||||
<div className="mem-foot">
|
||||
<span className="evi">
|
||||
来源:{MEMORY_SOURCE_LABEL[memory.source] ?? memory.source}
|
||||
</span>
|
||||
<span className="evi">
|
||||
{formatDateTime(memory.updated_at ?? memory.created_at)}
|
||||
</span>
|
||||
|
||||
{memory.evidence_ids && memory.evidence_ids.length > 0 && (
|
||||
<span className="evi">证据·{memory.evidence_ids.length} 条</span>
|
||||
)}
|
||||
|
||||
<span className="act">
|
||||
<button type="button" onClick={() => setEditing(true)}>
|
||||
编辑
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="primary"
|
||||
onClick={() => setConfirming(true)}
|
||||
disabled={busy}
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 删除确认 */}
|
||||
{confirming && (
|
||||
<div className="mt-3 flex flex-wrap items-center justify-between gap-2 rounded-lg border border-cinnabar/30 bg-cinnabar/10 px-3 py-2 text-xs text-cinnabar backdrop-blur-sm">
|
||||
<span>删除后此记忆将不再注入对话上下文,确认删除?</span>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => setConfirming(false)}
|
||||
disabled={busy}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="danger"
|
||||
onClick={handleDelete}
|
||||
loading={busy}
|
||||
>
|
||||
确认删除
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,205 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import {
|
||||
MEMORY_SOURCE_LABEL,
|
||||
MEMORY_TYPE_LABEL,
|
||||
SENSITIVITY_LABEL,
|
||||
} from "@/lib/constants/labels";
|
||||
import type { CreateMemoryRequest, Memory } from "@/types/api";
|
||||
import type { MemorySource, MemoryType, Sensitivity } from "@/types/domain";
|
||||
|
||||
const MEMORY_TYPES = Object.keys(MEMORY_TYPE_LABEL) as MemoryType[];
|
||||
const MEMORY_SOURCES = Object.keys(MEMORY_SOURCE_LABEL) as MemorySource[];
|
||||
const SENSITIVITY_LEVELS = Object.keys(SENSITIVITY_LABEL) as Sensitivity[];
|
||||
|
||||
interface FormState {
|
||||
type: MemoryType;
|
||||
title: string;
|
||||
content: string;
|
||||
source: MemorySource;
|
||||
sensitivity: Sensitivity;
|
||||
importance: number;
|
||||
confidence: number;
|
||||
}
|
||||
|
||||
function toForm(memory: Memory): FormState {
|
||||
return {
|
||||
type: memory.type,
|
||||
title: memory.title ?? "",
|
||||
content: memory.content,
|
||||
source: memory.source,
|
||||
sensitivity: memory.sensitivity,
|
||||
importance: memory.importance ?? 0.5,
|
||||
confidence: memory.confidence ?? 1,
|
||||
};
|
||||
}
|
||||
|
||||
const EMPTY_FORM: FormState = {
|
||||
type: "user",
|
||||
title: "",
|
||||
content: "",
|
||||
source: "user_explicit",
|
||||
sensitivity: "low",
|
||||
importance: 0.5,
|
||||
confidence: 1,
|
||||
};
|
||||
|
||||
const fieldClass =
|
||||
"w-full rounded-lg border border-border-glass bg-glass px-3 py-2 text-sm text-fg backdrop-blur-sm placeholder:text-muted focus:border-patina focus:outline-none focus:ring-1 focus:ring-patina/40";
|
||||
const labelClass = "mb-1 block text-xs font-medium text-muted";
|
||||
|
||||
/** 记忆新增 / 编辑表单。手动记忆 source 默认 user_explicit。 */
|
||||
export function MemoryEditor({
|
||||
characterId,
|
||||
initial,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}: {
|
||||
characterId?: string;
|
||||
initial?: Memory;
|
||||
onSubmit: (body: CreateMemoryRequest, isEdit: boolean) => Promise<void>;
|
||||
onCancel: () => void;
|
||||
}) {
|
||||
const [form, setForm] = useState<FormState>(
|
||||
initial ? toForm(initial) : EMPTY_FORM,
|
||||
);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const set = <K extends keyof FormState>(key: K, value: FormState[K]) =>
|
||||
setForm((prev) => ({ ...prev, [key]: value }));
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!form.content.trim()) return;
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const body: CreateMemoryRequest = {
|
||||
character_id: characterId,
|
||||
type: form.type,
|
||||
title: form.title.trim() || undefined,
|
||||
content: form.content,
|
||||
source: form.source,
|
||||
sensitivity: form.sensitivity,
|
||||
importance: form.importance,
|
||||
confidence: form.confidence,
|
||||
};
|
||||
await onSubmit(body, Boolean(initial));
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="glass space-y-3 rounded-xl p-3">
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<div>
|
||||
<label className={labelClass}>类型</label>
|
||||
<select
|
||||
className={fieldClass}
|
||||
value={form.type}
|
||||
onChange={(e) => set("type", e.target.value as MemoryType)}
|
||||
>
|
||||
{MEMORY_TYPES.map((t) => (
|
||||
<option key={t} value={t}>
|
||||
{MEMORY_TYPE_LABEL[t]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelClass}>来源</label>
|
||||
<select
|
||||
className={fieldClass}
|
||||
value={form.source}
|
||||
onChange={(e) => set("source", e.target.value as MemorySource)}
|
||||
>
|
||||
{MEMORY_SOURCES.map((s) => (
|
||||
<option key={s} value={s}>
|
||||
{MEMORY_SOURCE_LABEL[s]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className={labelClass}>标题(可选)</label>
|
||||
<input
|
||||
className={fieldClass}
|
||||
value={form.title}
|
||||
onChange={(e) => set("title", e.target.value)}
|
||||
placeholder="简短概括这条记忆"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className={labelClass}>内容</label>
|
||||
<textarea
|
||||
className={`${fieldClass} min-h-[5rem] resize-y`}
|
||||
value={form.content}
|
||||
onChange={(e) => set("content", e.target.value)}
|
||||
placeholder="记忆的完整内容"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
|
||||
<div>
|
||||
<label className={labelClass}>敏感度</label>
|
||||
<select
|
||||
className={fieldClass}
|
||||
value={form.sensitivity}
|
||||
onChange={(e) => set("sensitivity", e.target.value as Sensitivity)}
|
||||
>
|
||||
{SENSITIVITY_LEVELS.map((s) => (
|
||||
<option key={s} value={s}>
|
||||
{SENSITIVITY_LABEL[s]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelClass}>
|
||||
重要性 {Math.round(form.importance * 100)}%
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.05}
|
||||
value={form.importance}
|
||||
onChange={(e) => set("importance", Number(e.target.value))}
|
||||
className="w-full accent-patina"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelClass}>置信度</label>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.05}
|
||||
className={fieldClass}
|
||||
value={form.confidence}
|
||||
onChange={(e) => set("confidence", Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="ghost" onClick={onCancel} disabled={submitting}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleSubmit}
|
||||
loading={submitting}
|
||||
disabled={!form.content.trim()}
|
||||
>
|
||||
{initial ? "保存修改" : "新增记忆"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,336 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import { MemoryCard } from "@/components/memory/MemoryCard";
|
||||
import { MemoryEditor } from "@/components/memory/MemoryEditor";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { EmptyState, ErrorState } from "@/components/shared/StateViews";
|
||||
import { describeError } from "@/lib/utils/error";
|
||||
import { memoryApi } from "@/lib/api";
|
||||
import { MEMORY_TYPE_LABEL } from "@/lib/constants/labels";
|
||||
import { cn } from "@/lib/utils/cn";
|
||||
import type { CreateMemoryRequest, Memory } from "@/types/api";
|
||||
|
||||
interface Props {
|
||||
initialMemories: Memory[];
|
||||
characterId?: string;
|
||||
}
|
||||
|
||||
const TYPE_KEYS = [
|
||||
"identity",
|
||||
"episodic",
|
||||
"semantic",
|
||||
"procedural",
|
||||
"relational",
|
||||
"emotional",
|
||||
"reflection",
|
||||
] as const;
|
||||
|
||||
/** 搜索过滤的记忆列表 + 右栏搜索/提炼区(设计稿 memory-preview.html §mem-region) */
|
||||
export function MemoryList({ initialMemories, characterId }: Props) {
|
||||
const [memories, setMemories] = useState<Memory[]>(initialMemories);
|
||||
const [adding, setAdding] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [typeFilter, setTypeFilter] = useState<string | null>(null);
|
||||
|
||||
// 分类统计
|
||||
const typeCounts = useMemo(() => {
|
||||
const counts: Record<string, number> = {};
|
||||
for (const m of memories) {
|
||||
counts[m.type] = (counts[m.type] || 0) + 1;
|
||||
}
|
||||
return counts;
|
||||
}, [memories]);
|
||||
|
||||
// 汇总统计(设计稿 stat-grid 四格)
|
||||
const stats = useMemo(() => {
|
||||
const episodic = memories.filter((m) => m.type === "episodic").length;
|
||||
const semantic = memories.filter((m) => m.type === "semantic").length;
|
||||
const highSens = memories.filter((m) => m.sensitivity === "high").length;
|
||||
return { total: memories.length, episodic, semantic, highSens };
|
||||
}, [memories]);
|
||||
|
||||
// 过滤后的记忆
|
||||
const filtered = useMemo(() => {
|
||||
let list = memories;
|
||||
if (typeFilter) {
|
||||
list = list.filter((m) => m.type === typeFilter);
|
||||
}
|
||||
if (searchQuery.trim()) {
|
||||
const q = searchQuery.toLowerCase();
|
||||
list = list.filter(
|
||||
(m) =>
|
||||
(m.title || "").toLowerCase().includes(q) ||
|
||||
m.content.toLowerCase().includes(q),
|
||||
);
|
||||
}
|
||||
return list;
|
||||
}, [memories, typeFilter, searchQuery]);
|
||||
|
||||
const handleCreate = async (body: CreateMemoryRequest) => {
|
||||
const created = await memoryApi.createMemory(body);
|
||||
setMemories((prev) => [created, ...prev]);
|
||||
setAdding(false);
|
||||
};
|
||||
|
||||
const handleCreateWrapped = async (body: CreateMemoryRequest) => {
|
||||
try {
|
||||
await handleCreate(body);
|
||||
} catch (err) {
|
||||
setError(describeError(err));
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (
|
||||
kind: "update" | "delete",
|
||||
payload: Memory | string,
|
||||
) => {
|
||||
if (kind === "update") {
|
||||
const updated = payload as Memory;
|
||||
setMemories((prev) =>
|
||||
prev.map((m) => (m.id === updated.id ? updated : m)),
|
||||
);
|
||||
} else {
|
||||
const id = payload as string;
|
||||
setMemories((prev) => prev.filter((m) => m.id !== id));
|
||||
}
|
||||
};
|
||||
|
||||
// 提炼规则(设计稿 review-list)
|
||||
const REVIEW_RULES = [
|
||||
{ text: "新记忆与现有事实冲突时,标记冲突待裁定", allow: false },
|
||||
{ text: "修复性记忆(corrective)可直接替换旧记忆", allow: true },
|
||||
{ text: "高敏感度记忆写入前需人工确认", allow: false },
|
||||
{ text: "记忆置信度低于 0.3 的自动降权", allow: true },
|
||||
] as const;
|
||||
|
||||
return (
|
||||
<div className="mem-region">
|
||||
{/* ═══ 左栏:记忆主列表 ═══ */}
|
||||
<div className="mem-main">
|
||||
{error && <ErrorState message={error} onRetry={() => setError(null)} />}
|
||||
|
||||
{/* 统计四格(设计稿 stat-grid) */}
|
||||
<div className="stat-grid">
|
||||
<div className="stat-card">
|
||||
<div className="k">记忆总数</div>
|
||||
<div className="v">{stats.total}</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="k">情景型</div>
|
||||
<div className="v">{stats.episodic}</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="k">语义型</div>
|
||||
<div className="v">{stats.semantic}</div>
|
||||
</div>
|
||||
<div className="stat-card warn">
|
||||
<div className="k">高敏感</div>
|
||||
<div className="v">
|
||||
{stats.highSens}
|
||||
<small>条</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 分类标签栏(设计稿 .type-bar) */}
|
||||
<div className="type-bar">
|
||||
<button
|
||||
type="button"
|
||||
className={cn("type-chip", !typeFilter && "active")}
|
||||
onClick={() => setTypeFilter(null)}
|
||||
>
|
||||
全部
|
||||
<span className="cnt">{memories.length}</span>
|
||||
</button>
|
||||
{TYPE_KEYS.filter((t) => typeCounts[t]).map((t) => (
|
||||
<button
|
||||
key={t}
|
||||
type="button"
|
||||
className={cn("type-chip", typeFilter === t && "active", t)}
|
||||
onClick={() => setTypeFilter(typeFilter === t ? null : t)}
|
||||
>
|
||||
{MEMORY_TYPE_LABEL[t] ?? t}
|
||||
<span className="cnt">{typeCounts[t]}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 新增按钮 */}
|
||||
<div className="mt-4">
|
||||
{adding ? (
|
||||
<MemoryEditor
|
||||
characterId={characterId}
|
||||
onSubmit={handleCreateWrapped}
|
||||
onCancel={() => setAdding(false)}
|
||||
/>
|
||||
) : (
|
||||
<Button variant="primary" onClick={() => setAdding(true)}>
|
||||
新增记忆
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 记忆列表 */}
|
||||
<div className="mem-list mt-4">
|
||||
{filtered.length === 0 ? (
|
||||
<EmptyState
|
||||
title={searchQuery ? "无匹配记忆" : "尚无记忆"}
|
||||
description={
|
||||
searchQuery
|
||||
? `未搜索到包含「${searchQuery}」的记忆`
|
||||
: "为当前人物手动新增一条长期记忆。"
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
filtered.map((memory) => (
|
||||
<MemoryCard
|
||||
key={memory.id}
|
||||
memory={memory}
|
||||
characterId={characterId}
|
||||
onChanged={handleChange}
|
||||
onError={setError}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ═══ 右栏:搜索 + 提炼区(设计稿 .mem-rail) ═══ */}
|
||||
<div className="mem-rail">
|
||||
{/* 搜索框(设计稿 .search-box) */}
|
||||
<div className="rail-card">
|
||||
<h3>检索记忆</h3>
|
||||
<div className="search-box">
|
||||
<svg
|
||||
width="13"
|
||||
height="13"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
aria-hidden
|
||||
className="shrink-0 text-fg-faint"
|
||||
>
|
||||
<circle
|
||||
cx="7"
|
||||
cy="7"
|
||||
r="4.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.3"
|
||||
/>
|
||||
<path
|
||||
d="M10.5 10.5l3 3"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.4"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="搜索记忆内容…"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSearchQuery("")}
|
||||
className="shrink-0 text-fg-faint hover:text-fg text-[11px]"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{searchQuery && (
|
||||
<p className="mt-2 text-[11px] text-fg-faint">
|
||||
匹配 {filtered.length} 条记忆
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 内存用量统计(设计稿 .stat-card 变形) */}
|
||||
<div className="rail-card">
|
||||
<h3>息壤概览</h3>
|
||||
<div className="flex flex-col gap-2">
|
||||
<StatRow
|
||||
label="总记忆"
|
||||
value={memories.length}
|
||||
eng="Total"
|
||||
color="patina"
|
||||
/>
|
||||
<StatRow
|
||||
label="待同步"
|
||||
value={memories.filter((m) => m.sensitivity === "high").length}
|
||||
eng="Needs Sync"
|
||||
color="gilt"
|
||||
/>
|
||||
<StatRow
|
||||
label="高敏感"
|
||||
value={memories.filter((m) => m.sensitivity === "high").length}
|
||||
eng="High Sensitivity"
|
||||
color="cinnabar"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 提炼规则(设计稿 .review-list) */}
|
||||
<div className="rail-card">
|
||||
<h3>记忆提炼规则</h3>
|
||||
<div className="review-list">
|
||||
{REVIEW_RULES.map((rule, i) => (
|
||||
<div key={i} className={cn("review-rule", rule.allow && "allow")}>
|
||||
<span className="mark">{rule.allow ? "✓" : "!"}</span>
|
||||
<span>{rule.text}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 冲突发现 */}
|
||||
<div className="rail-card">
|
||||
<h3>冲突裁定</h3>
|
||||
<p className="text-[11.5px] text-fg-faint">暂无未解决的记忆冲突。</p>
|
||||
{/* 有冲突时渲染:
|
||||
<div className="conflict-row">
|
||||
<span className="pair">爱丽丝 · 阿卡夏</span>
|
||||
<span className="desc">生日 3月14日 vs 5月22日</span>
|
||||
<span className="verdict">待裁定</span>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatRow({
|
||||
label,
|
||||
value,
|
||||
eng,
|
||||
color,
|
||||
}: {
|
||||
label: string;
|
||||
value: number;
|
||||
eng: string;
|
||||
color: "patina" | "gilt" | "cinnabar";
|
||||
}) {
|
||||
const colorMap = {
|
||||
patina: "text-patina",
|
||||
gilt: "text-gilt",
|
||||
cinnabar: "text-cinnabar",
|
||||
};
|
||||
return (
|
||||
<div className="flex items-center justify-between border-b border-rule pb-1.5 last:border-b-0 last:pb-0">
|
||||
<span className="text-[12.5px] text-fg-dim">
|
||||
{label}
|
||||
<span className="ml-1.5 font-mono text-[10px] text-fg-faint">
|
||||
{eng}
|
||||
</span>
|
||||
</span>
|
||||
<span className={cn("font-mono text-sm font-medium", colorMap[color])}>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("./client", () => ({
|
||||
apiGet: vi.fn(),
|
||||
apiPost: vi.fn(),
|
||||
}));
|
||||
|
||||
import { apiGet, apiPost } from "./client";
|
||||
import { approveCandidate, listCandidates, rejectCandidate } from "./evolution";
|
||||
|
||||
describe("evolution api", () => {
|
||||
it("listCandidates 构造人物级列表 path 与 query", async () => {
|
||||
vi.mocked(apiGet).mockResolvedValue({
|
||||
items: [],
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
total: 0,
|
||||
});
|
||||
await listCandidates("char_1", { status: "pending", type: "procedural" });
|
||||
expect(apiGet).toHaveBeenCalledWith(
|
||||
"/characters/char_1/evolution-candidates",
|
||||
{ status: "pending", type: "procedural" },
|
||||
);
|
||||
});
|
||||
|
||||
it("approveCandidate POST 到 approve 端点", async () => {
|
||||
vi.mocked(apiPost).mockResolvedValue({ status: "applied" } as never);
|
||||
await approveCandidate("evo_1");
|
||||
expect(apiPost).toHaveBeenCalledWith("/evolution-candidates/evo_1/approve");
|
||||
});
|
||||
|
||||
it("rejectCandidate POST 到 reject 端点", async () => {
|
||||
vi.mocked(apiPost).mockResolvedValue({ status: "rejected" } as never);
|
||||
await rejectCandidate("evo_1");
|
||||
expect(apiPost).toHaveBeenCalledWith("/evolution-candidates/evo_1/reject");
|
||||
});
|
||||
});
|
||||
@@ -1,37 +0,0 @@
|
||||
/**
|
||||
* 演化候选 API Client(对应 docs/domain/core-domains.md §7)。
|
||||
* 大周天生成的演化候选审核闭环:列表 / approve / reject。
|
||||
*/
|
||||
|
||||
import { apiGet, apiPost } from "./client";
|
||||
import type { EvolutionCandidate, Paginated } from "@/types/api";
|
||||
|
||||
/** 列出某人物的演化候选(默认按 created_at 倒序) */
|
||||
export function listCandidates(
|
||||
characterId: string,
|
||||
params?: {
|
||||
status?: string;
|
||||
type?: string;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
},
|
||||
) {
|
||||
return apiGet<Paginated<EvolutionCandidate>>(
|
||||
`/characters/${characterId}/evolution-candidates`,
|
||||
params,
|
||||
);
|
||||
}
|
||||
|
||||
/** 审核通过:派生到下游(人格版本 / 程序记忆),status → applied */
|
||||
export function approveCandidate(candidateId: string) {
|
||||
return apiPost<EvolutionCandidate>(
|
||||
`/evolution-candidates/${candidateId}/approve`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 驳回:status → rejected,不派生下游 */
|
||||
export function rejectCandidate(candidateId: string) {
|
||||
return apiPost<EvolutionCandidate>(
|
||||
`/evolution-candidates/${candidateId}/reject`,
|
||||
);
|
||||
}
|
||||
@@ -10,9 +10,7 @@ export * as audioApi from "./audio";
|
||||
export * as charactersApi from "./characters";
|
||||
export * as runsApi from "./runs";
|
||||
export * as sandboxApi from "./sandbox";
|
||||
export * as memoryApi from "./memory";
|
||||
export * as personalityApi from "./personality";
|
||||
export * as evolutionApi from "./evolution";
|
||||
export * as driveApi from "./drive";
|
||||
export * as healthApi from "./health";
|
||||
export * as toolsApi from "./tools";
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("./client", () => ({
|
||||
apiDelete: vi.fn(),
|
||||
apiGet: vi.fn(),
|
||||
apiPatch: vi.fn(),
|
||||
apiPost: vi.fn(),
|
||||
}));
|
||||
|
||||
import { apiDelete, apiGet, apiPatch, apiPost } from "./client";
|
||||
import {
|
||||
createMemory,
|
||||
deleteMemory,
|
||||
listMemories,
|
||||
updateMemory,
|
||||
} from "./memory";
|
||||
|
||||
describe("memory api", () => {
|
||||
it("listMemories GET /memory 支持分页与类型过滤", async () => {
|
||||
vi.mocked(apiGet).mockResolvedValue({ items: [], total: 0 });
|
||||
await listMemories({ page: 1, page_size: 20, type: "episodic" });
|
||||
expect(apiGet).toHaveBeenCalledWith("/memory", {
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
type: "episodic",
|
||||
});
|
||||
});
|
||||
|
||||
it("createMemory POST /memory 默认 source=user_explicit", async () => {
|
||||
vi.mocked(apiPost).mockResolvedValue({ id: "mem_1" });
|
||||
await createMemory({ type: "episodic", title: "测试", content: "内容" });
|
||||
expect(apiPost).toHaveBeenCalledWith("/memory", {
|
||||
source: "user_explicit",
|
||||
type: "episodic",
|
||||
title: "测试",
|
||||
content: "内容",
|
||||
});
|
||||
});
|
||||
|
||||
it("createMemory 支持覆盖 source", async () => {
|
||||
vi.mocked(apiPost).mockResolvedValue({ id: "mem_2" });
|
||||
await createMemory({
|
||||
type: "episodic",
|
||||
title: "导入",
|
||||
content: "内容",
|
||||
source: "conversation",
|
||||
});
|
||||
expect(apiPost).toHaveBeenCalledWith("/memory", {
|
||||
source: "conversation",
|
||||
type: "episodic",
|
||||
title: "导入",
|
||||
content: "内容",
|
||||
});
|
||||
});
|
||||
|
||||
it("updateMemory PATCH /memory/:id", async () => {
|
||||
vi.mocked(apiPatch).mockResolvedValue({ id: "mem_1" });
|
||||
await updateMemory("mem_1", { title: "新标题" });
|
||||
expect(apiPatch).toHaveBeenCalledWith("/memory/mem_1", { title: "新标题" });
|
||||
});
|
||||
|
||||
it("deleteMemory DELETE /memory/:id", async () => {
|
||||
vi.mocked(apiDelete).mockResolvedValue(undefined);
|
||||
await deleteMemory("mem_1");
|
||||
expect(apiDelete).toHaveBeenCalledWith("/memory/mem_1");
|
||||
});
|
||||
});
|
||||
@@ -1,45 +0,0 @@
|
||||
/**
|
||||
* 记忆 API Client(对应 后端生成的 OpenAPI 契约 §10)。
|
||||
* 手动记忆 CRUD;character_id 省略时使用当前激活人物。
|
||||
*/
|
||||
|
||||
import { apiDelete, apiGet, apiPatch, apiPost } from "./client";
|
||||
import type {
|
||||
CreateMemoryRequest,
|
||||
Memory,
|
||||
Paginated,
|
||||
UpdateMemoryRequest,
|
||||
} from "@/types/api";
|
||||
|
||||
export interface ListMemoryParams {
|
||||
character_id?: string;
|
||||
type?: string;
|
||||
source?: string;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
/** 查询参数可透传,允许作为 Record 传递给底层请求封装 */
|
||||
[key: string]: string | number | undefined;
|
||||
}
|
||||
|
||||
/** 列出当前人物记忆 */
|
||||
export function listMemories(params?: ListMemoryParams) {
|
||||
return apiGet<Paginated<Memory>>("/memory", params);
|
||||
}
|
||||
|
||||
/** 创建记忆;手动记忆 source 默认 user_explicit */
|
||||
export function createMemory(body: CreateMemoryRequest) {
|
||||
return apiPost<Memory>("/memory", {
|
||||
source: "user_explicit",
|
||||
...body,
|
||||
});
|
||||
}
|
||||
|
||||
/** 更新记忆(PATCH 部分字段) */
|
||||
export function updateMemory(memoryId: string, body: UpdateMemoryRequest) {
|
||||
return apiPatch<Memory>(`/memory/${memoryId}`, body);
|
||||
}
|
||||
|
||||
/** 删除记忆(后端软删除:status=deleted + deleted_at) */
|
||||
export function deleteMemory(memoryId: string) {
|
||||
return apiDelete<void>(`/memory/${memoryId}`);
|
||||
}
|
||||
Reference in new issue
Block a user