M3a:息壤三级记忆域——归真触发提取管线/白名单明忆/重忆确认/正影版本链/删除墓碑/双通道召回注入,退役旧记忆-抽取-反思-大周天-演化管线(迁移 004,验收 7 用例)

This commit is contained in:
chenxin committed 2026-08-27 09:16:32 +08:00
1 parent 2a640a901b
commit 9c98c2f85e
69 files changed
+1193 -6033

No files matched your search

-166
View File
@@ -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>
);
}
-41
View File
@@ -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>
);
}
-48
View File
@@ -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>
);
}
+1 -7
View File
@@ -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>
);
}
-37
View File
@@ -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");
});
});
-37
View File
@@ -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`,
);
}
-2
View File
@@ -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";
-67
View File
@@ -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");
});
});
-45
View File
@@ -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}`);
}