- 百工域 domains/baigong:capabilities(统一能力契约:执行类型 builtin/qiankun/mcp/http + 四维风险标注 + 来源版本)、mandates(授契 grant / 行契 charter 预算时间窗,撤销立即生效)、tool_calls(净化摘要 + 观察标证);请命复用 pending_confirmations 加 expires_at/brief_json(超时=拒绝执行 HJ-1302,六要素说明) - 司命裁决器 siming.py:四维取最高档(能力/数据/后果/情境);禁域为代码常量不可配置(递归删根/格式化/写裸设备/fork 炸弹、file:// 协议、密钥材料外发)——专项测试断言恶意标注与篡改对象仍拒 HJ-1301 - 执行链:禁域→裁决(HJ-1303 未授契拒且双留痕)→请命→执行(乾坤经 SandboxClient HMAC;MCP 经 call_tool;内置无副作用)→净化标证(观察非事实,敏感遮蔽+体量截断)→行动账 - MCP 登记/启停/刷新自动派生与撤下 capabilities(博闻登记接入点);内置能力种子(echo/datetime/qiankun.shell/web.fetch) - 端点:/capabilities /mandates /petitions /tool-calls + /tools 兼容投影;前端百工页四区(能力目录四维标注/授契行契/请命批准拒绝/行动账观察标证) - 退役:modules/safety|skills|tool_runtime|agent、application/tool_runtime、services/skills|safety、tool_policies/skills/model_endpoint_overrides 表(迁移 006)、前端 SkillManager/ToolsConsole/旧 tools 页 - 验收(test_baigong_siming 四用例):未授契被拒留痕、请命超时不执行、禁域不可关闭、结果为观察不入忆;后端 454、前端 220、BFF 4、神识 5 包全绿(2 个预存环境失败不变)
1230 lines
37 KiB
TypeScript
1230 lines
37 KiB
TypeScript
/**
|
||
* Mock 拦截层入口。
|
||
*
|
||
* 由环境变量 NEXT_PUBLIC_ENABLE_MOCK 开关:为 "true" 时,apiRequest 命中下列路由表
|
||
* 即返回 Mock 数据,不再发起真实请求;未命中的端点放行至真实请求(并打印 warn)。
|
||
* SSE 流的 Mock 见 lib/sse-client.ts。接入真实数据后,关闭开关即可,无需改组件。
|
||
*/
|
||
|
||
import type {
|
||
CreateAgentRequest,
|
||
CreateCharacterRequest,
|
||
CreateGoalRequest,
|
||
CreateImportRequest,
|
||
CreateMemoryRequest,
|
||
CreatePerceptionChannelRequest,
|
||
CreatePlanRequest,
|
||
CreateSafetyPolicyRequest,
|
||
CreateSessionRequest,
|
||
CreateToolRequest,
|
||
CreateWorldEntityRequest,
|
||
BatchReviewRequest,
|
||
CreateMcpServerRequest,
|
||
CreateSkillRequest,
|
||
CreateWorldRelationRequest,
|
||
UpdateGenerationPreferencesRequest,
|
||
UpdateMemoryRequest,
|
||
UpdateModelPolicyRequest,
|
||
UpdateProfileRequest,
|
||
UpdateSessionRequest,
|
||
} from "@/types/api";
|
||
import * as data from "./data";
|
||
|
||
/** 是否启用 Mock(NEXT_PUBLIC_ 前缀使其在服务端与浏览器均可用) */
|
||
export const USE_MOCK = process.env.NEXT_PUBLIC_ENABLE_MOCK === "true";
|
||
|
||
/** 模拟网络延迟,让加载/过渡更接近真实 */
|
||
export const MOCK_LATENCY_MS = 180;
|
||
|
||
/** mock 内存态:当前激活人物 id,支持切换后 getActiveCharacter 正确返回 */
|
||
let _mockActiveCharacterId = data.MOCK_CHARACTER_ID;
|
||
|
||
/** mock 内存态:纪念日列表,支持 CRUD */
|
||
const _annivList: Array<Record<string, unknown>> = [];
|
||
|
||
export const mockDelay = (ms: number): Promise<void> =>
|
||
new Promise((resolve) => setTimeout(resolve, ms));
|
||
|
||
export interface MockResult {
|
||
status: number;
|
||
data: unknown;
|
||
}
|
||
|
||
const ok = (body: unknown, status = 200): MockResult => ({
|
||
status,
|
||
data: body,
|
||
});
|
||
|
||
function paginate<T>(items: T[]) {
|
||
return {
|
||
items,
|
||
page: 1,
|
||
page_size: items.length,
|
||
total: items.length,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 路由匹配:命中返回 MockResult,未命中返回 undefined。
|
||
* @param method HTTP 方法
|
||
* @param path 不含 /api/v1 前缀的路径(如 /characters/active)
|
||
* @param query 查询参数
|
||
* @param json 请求体
|
||
*/
|
||
export function resolveMock(
|
||
method: string,
|
||
path: string,
|
||
query?: Record<string, unknown>,
|
||
json?: unknown,
|
||
): MockResult | undefined {
|
||
const m = method.toUpperCase();
|
||
|
||
// —— 健康检查 ——
|
||
if (m === "GET" && path === "/health") return ok(data.health);
|
||
|
||
// —— 百工:能力目录 / 授契 / 请命 / 行动账 ——
|
||
if (m === "GET" && path === "/capabilities") {
|
||
return ok([
|
||
{
|
||
id: "cap_mock_001",
|
||
name: "builtin.echo",
|
||
description: "回显输入(无副作用演示能力)",
|
||
executor_type: "builtin",
|
||
executor_ref: "echo",
|
||
risk_dims: {
|
||
capability: "L0",
|
||
data: "L0",
|
||
consequence: "L0",
|
||
context: "L0",
|
||
},
|
||
data_scope: "",
|
||
destinations: "",
|
||
source: "builtin",
|
||
version: "1",
|
||
consent_mode: "allow",
|
||
enabled: true,
|
||
mandated: true,
|
||
},
|
||
{
|
||
id: "cap_mock_002",
|
||
name: "qiankun.shell",
|
||
description: "在乾坤沙箱中执行命令(隔离执行,HMAC 授契)",
|
||
executor_type: "qiankun",
|
||
executor_ref: "",
|
||
risk_dims: {
|
||
capability: "L2",
|
||
data: "L1",
|
||
consequence: "L3",
|
||
context: "L1",
|
||
},
|
||
data_scope: "沙箱工作目录",
|
||
destinations: "本机沙箱进程",
|
||
source: "builtin",
|
||
version: "1",
|
||
consent_mode: "ask",
|
||
enabled: true,
|
||
mandated: false,
|
||
},
|
||
]);
|
||
}
|
||
if (m === "GET" && path === "/mandates") {
|
||
return ok([
|
||
{
|
||
id: "mdt_mock_001",
|
||
capability_id: "cap_mock_001",
|
||
character_id: null,
|
||
kind: "charter",
|
||
scope: {},
|
||
budget_total: 20,
|
||
budget_used: 3,
|
||
status: "active",
|
||
granted_at: "2026-08-27T10:00:00Z",
|
||
expires_at: null,
|
||
},
|
||
]);
|
||
}
|
||
if (m === "POST" && path === "/mandates") {
|
||
return ok({ id: "mdt_mock_002", status: "active" }, 201);
|
||
}
|
||
if (m === "DELETE" && path.startsWith("/mandates/")) {
|
||
return ok(null, 204);
|
||
}
|
||
if (m === "GET" && path === "/petitions") {
|
||
return ok([
|
||
{
|
||
id: "confirm_mock_001",
|
||
capability: "qiankun.shell",
|
||
risk_level: "L3",
|
||
reason: "请命:高风险动作需当次确认",
|
||
brief: {
|
||
who: "character:character_mock",
|
||
what: "在乾坤沙箱中执行命令",
|
||
capability: "qiankun.shell",
|
||
executor: "qiankun",
|
||
destinations: "本机沙箱进程",
|
||
consequences: "风险等级 L3",
|
||
alternatives: "拒绝后可调整参数或改用低风险能力",
|
||
},
|
||
run_id: "run_mock_001",
|
||
character_id: "character_mock",
|
||
created_at: "2026-08-27T10:05:00Z",
|
||
expires_at: "2026-08-27T10:15:00Z",
|
||
},
|
||
]);
|
||
}
|
||
if (
|
||
m === "POST" &&
|
||
path.startsWith("/petitions/") &&
|
||
path.endsWith("/approve")
|
||
) {
|
||
return ok({ status: "resolved", approved: true });
|
||
}
|
||
if (
|
||
m === "POST" &&
|
||
path.startsWith("/petitions/") &&
|
||
path.endsWith("/deny")
|
||
) {
|
||
return ok({ status: "cancelled", approved: false });
|
||
}
|
||
if (m === "GET" && path === "/tool-calls") {
|
||
return ok([
|
||
{
|
||
id: "tc_mock_001",
|
||
capability_id: "cap_mock_001",
|
||
run_id: "run_mock_001",
|
||
character_id: "character_mock",
|
||
trace_id: "trace_mock_001",
|
||
decision: "allow",
|
||
mandate_id: "mdt_mock_001",
|
||
status: "ok",
|
||
error_code: null,
|
||
input_digest: '{"text":"琉璃之镜"}',
|
||
output_digest: "[echo] 琉璃之镜",
|
||
evidence: {
|
||
kind: "observation",
|
||
capability: "builtin.echo",
|
||
epistemic: "observation_not_fact",
|
||
},
|
||
duration_ms: 3,
|
||
created_at: "2026-08-27T10:06:00Z",
|
||
},
|
||
{
|
||
id: "tc_mock_002",
|
||
capability_id: "cap_mock_002",
|
||
run_id: null,
|
||
character_id: null,
|
||
trace_id: null,
|
||
decision: "denied_unmandated",
|
||
mandate_id: null,
|
||
status: "rejected",
|
||
error_code: "HJ-1303",
|
||
input_digest: '{"command":"ls"}',
|
||
output_digest: "",
|
||
evidence: { kind: "observation", capability: "qiankun.shell" },
|
||
duration_ms: 0,
|
||
created_at: "2026-08-27T10:07:00Z",
|
||
},
|
||
]);
|
||
}
|
||
|
||
// —— 神识阁:BYOK 密钥(脱敏)——
|
||
if (m === "GET" && path === "/me/byok-keys") {
|
||
return ok({
|
||
keys: [
|
||
{
|
||
id: "byok_mock_001",
|
||
name: "主力 DeepSeek",
|
||
type: "openai_compatible",
|
||
base_url: "https://api.deepseek.com",
|
||
model: "deepseek-v4-flash",
|
||
api_key_mask: "sk-****",
|
||
},
|
||
],
|
||
});
|
||
}
|
||
if (m === "POST" && path === "/me/byok-keys") {
|
||
const body = (json ?? {}) as Record<string, unknown>;
|
||
return ok(
|
||
{
|
||
id: "byok_mock_002",
|
||
name: String(body.name ?? ""),
|
||
type: String(body.type ?? ""),
|
||
base_url: String(body.base_url ?? ""),
|
||
model: String(body.model ?? ""),
|
||
api_key_mask: "sk-****",
|
||
},
|
||
201,
|
||
);
|
||
}
|
||
if (m === "DELETE" && path.startsWith("/me/byok-keys/")) {
|
||
return ok(null, 204);
|
||
}
|
||
|
||
// —— 神识阁:用量 ——
|
||
if (m === "GET" && path === "/me/usage") {
|
||
return ok({
|
||
rows: [
|
||
{
|
||
user_id: "user_mock",
|
||
model: "deepseek-v4-flash",
|
||
endpoint: "deepseek",
|
||
requests: 12,
|
||
ok: 11,
|
||
prompt_tokens: 8400,
|
||
completion_tokens: 3100,
|
||
},
|
||
],
|
||
quota: { budget: 1_000_000, used: 11_500, reserved: 0 },
|
||
});
|
||
}
|
||
|
||
// —— 设置:生成参数偏好(GET / PATCH /settings/preferences)——
|
||
if (m === "GET" && path === "/settings/preferences") {
|
||
return ok(data.generationPreferences);
|
||
}
|
||
if (m === "PATCH" && path === "/settings/preferences") {
|
||
return ok(
|
||
data.updateGenerationPreferences(
|
||
(json ?? {}) as UpdateGenerationPreferencesRequest,
|
||
),
|
||
);
|
||
}
|
||
|
||
// —— 司命台 · 九枢健康度聚合(API §19.1)——
|
||
if (m === "GET" && path === "/admin/pivots/health") {
|
||
return ok(data.pivotHealthReport);
|
||
}
|
||
|
||
// —— 司命台 · 朱批待办(API §19.2)——
|
||
if (m === "GET" && path === "/admin/safety/pending") {
|
||
return ok(paginate(data.pendingApprovals));
|
||
}
|
||
|
||
// —— 安全审计(GET /safety/audit-events,按人物/枢过滤)——
|
||
if (m === "GET" && path === "/safety/audit-events") {
|
||
const pivot = query?.pivot as string | undefined;
|
||
const items = pivot
|
||
? data.auditTimeline.filter((e) => e.pivot === pivot)
|
||
: data.auditTimeline;
|
||
return ok(paginate(items));
|
||
}
|
||
|
||
// —— 安全策略(§16;开放集合:增删改,随工具增长;后端返回全量 list 非分页)——
|
||
if (m === "GET" && path === "/safety/policies") {
|
||
return ok(data.safetyPolicies);
|
||
}
|
||
if (m === "POST" && path === "/safety/policies") {
|
||
return ok(data.createPolicy((json ?? {}) as CreateSafetyPolicyRequest));
|
||
}
|
||
{
|
||
const mt = path.match(/^\/safety\/policies\/([^/]+)$/);
|
||
if (mt && m === "PATCH") {
|
||
const body = (json ?? {}) as Record<string, unknown>;
|
||
const found = data.safetyPolicies.find((p) => p.tool === mt[1]);
|
||
// 乐观:新增策略不在初始列表,find 不到时合成 base 再合并
|
||
const base =
|
||
found ??
|
||
data.createPolicy({
|
||
tool: mt[1],
|
||
risk_level: "L2",
|
||
permission_policy: "ask",
|
||
});
|
||
return ok({ ...base, ...body, tool: mt[1] });
|
||
}
|
||
if (mt && m === "DELETE") return ok(undefined, 204);
|
||
}
|
||
|
||
// —— 司命台 · 起居注审计(API §19.3),支持 pivot 过滤 ——
|
||
if (m === "GET" && path === "/admin/audit/timeline") {
|
||
const pivot = query?.pivot as string | undefined;
|
||
const items = pivot
|
||
? data.auditTimeline.filter((e) => e.pivot === pivot)
|
||
: data.auditTimeline;
|
||
return ok(paginate(items));
|
||
}
|
||
|
||
// —— 司命台 · 后续阶段治理端点(API §19.4 预留)——
|
||
// 太一枢 · 目标层级(开放集合:增删改)
|
||
if (m === "GET" && path === "/admin/goals") {
|
||
const characterId = query?.character_id as string | undefined;
|
||
return ok(
|
||
characterId
|
||
? data.goals.filter((g) => g.character_id === characterId)
|
||
: data.goals,
|
||
);
|
||
}
|
||
if (m === "POST" && path === "/admin/goals") {
|
||
return ok(data.createGoal((json ?? {}) as CreateGoalRequest));
|
||
}
|
||
{
|
||
const mt = path.match(/^\/admin\/goals\/([^/]+)$/);
|
||
if (mt && m === "PATCH") {
|
||
const found = data.goals.find((g) => g.id === mt[1]);
|
||
const body = (json ?? {}) as Record<string, unknown>;
|
||
// 乐观:新增项不在初始列表,find 不到时合成 base 再合并,保证可继续编辑
|
||
const base =
|
||
found ??
|
||
data.createGoal({
|
||
title: String(body.title ?? "未命名目标"),
|
||
level: "task",
|
||
});
|
||
return ok({
|
||
...base,
|
||
...body,
|
||
id: mt[1],
|
||
updated_at: new Date().toISOString(),
|
||
});
|
||
}
|
||
if (mt && m === "DELETE") return ok(undefined, 204);
|
||
}
|
||
// 烛照枢 · 感知通道授权(开放集合:增删改;默认六类,可自定义感知源)
|
||
if (m === "GET" && path === "/admin/perception/channels")
|
||
return ok(data.perceptionChannels);
|
||
if (m === "POST" && path === "/admin/perception/channels") {
|
||
return ok(
|
||
data.createPerceptionChannel(
|
||
(json ?? {}) as CreatePerceptionChannelRequest,
|
||
),
|
||
);
|
||
}
|
||
{
|
||
const mt = path.match(/^\/admin\/perception\/channels\/([^/]+)$/);
|
||
if (mt && m === "PATCH") {
|
||
const found = data.perceptionChannels.find((c) => c.channel === mt[1]);
|
||
const body = (json ?? {}) as Record<string, unknown>;
|
||
const base =
|
||
found ??
|
||
data.createPerceptionChannel({
|
||
channel: mt[1],
|
||
name: String(body.name ?? mt[1]),
|
||
description: "自定义感知源",
|
||
});
|
||
return ok({ ...base, ...body, channel: mt[1] });
|
||
}
|
||
if (mt && m === "DELETE") return ok(undefined, 204);
|
||
}
|
||
// 河图枢 · 世界实体(开放集合:增删改)
|
||
if (m === "GET" && path === "/admin/world/entities")
|
||
return ok(data.worldEntities);
|
||
if (m === "POST" && path === "/admin/world/entities") {
|
||
return ok(data.createWorldEntity((json ?? {}) as CreateWorldEntityRequest));
|
||
}
|
||
{
|
||
const mt = path.match(/^\/admin\/world\/entities\/([^/]+)$/);
|
||
if (mt && m === "PATCH") {
|
||
const found = data.worldEntities.find((e) => e.id === mt[1]);
|
||
const body = (json ?? {}) as Record<string, unknown>;
|
||
const base =
|
||
found ??
|
||
data.createWorldEntity({
|
||
type: "concept",
|
||
name: String(body.name ?? "未命名实体"),
|
||
});
|
||
return ok({
|
||
...base,
|
||
...body,
|
||
id: mt[1],
|
||
updated_at: new Date().toISOString(),
|
||
});
|
||
}
|
||
if (mt && m === "DELETE") return ok(undefined, 204);
|
||
}
|
||
// 洛书枢 · 计划(开放集合:增删改)
|
||
if (m === "GET" && path === "/admin/plans") return ok(data.plans);
|
||
if (m === "POST" && path === "/admin/plans") {
|
||
return ok(data.createPlan((json ?? {}) as CreatePlanRequest));
|
||
}
|
||
{
|
||
const mt = path.match(/^\/admin\/plans\/([^/]+)$/);
|
||
if (mt && m === "PATCH") {
|
||
const found = data.plans.find((p) => p.id === mt[1]);
|
||
const body = (json ?? {}) as Record<string, unknown>;
|
||
const base =
|
||
found ?? data.createPlan({ title: String(body.title ?? "未命名计划") });
|
||
return ok({
|
||
...base,
|
||
...body,
|
||
id: mt[1],
|
||
updated_at: new Date().toISOString(),
|
||
});
|
||
}
|
||
if (mt && m === "DELETE") return ok(undefined, 204);
|
||
}
|
||
// 天工阁枢 · 角色在岗(开放集合:增删改;默认九角色,可自定义新角色)
|
||
if (m === "GET" && path === "/admin/agents/roster")
|
||
return ok(data.agentRoster);
|
||
if (m === "POST" && path === "/admin/agents/roster") {
|
||
return ok(data.createAgent((json ?? {}) as CreateAgentRequest));
|
||
}
|
||
{
|
||
const mt = path.match(/^\/admin\/agents\/roster\/([^/]+)$/);
|
||
if (mt && m === "PATCH") {
|
||
const found = data.agentRoster.find((a) => a.role === mt[1]);
|
||
const body = (json ?? {}) as Record<string, unknown>;
|
||
const base =
|
||
found ??
|
||
data.createAgent({
|
||
role: mt[1],
|
||
name: String(body.name ?? mt[1]),
|
||
responsibility: "自定义角色",
|
||
});
|
||
return ok({ ...base, ...body, role: mt[1] });
|
||
}
|
||
if (mt && m === "DELETE") return ok(undefined, 204);
|
||
}
|
||
|
||
// —— 司命台 · 链路追踪 GET /admin/traces(§19.5)——
|
||
if (m === "GET" && path === "/admin/traces") {
|
||
return ok({ items: [], total: 0 });
|
||
}
|
||
{
|
||
const mt = path.match(/^\/admin\/traces\/([^/]+)$/);
|
||
if (mt && m === "GET") {
|
||
return ok({
|
||
trace_id: mt[1],
|
||
events: [],
|
||
audit_entries: [],
|
||
idempotency_entries: [],
|
||
session_ids: [],
|
||
character_ids: [],
|
||
});
|
||
}
|
||
}
|
||
|
||
// —— 司命台 · 时间事件 GET /admin/time-events(§19.5)——
|
||
if (m === "GET" && path === "/admin/time-events") {
|
||
return ok({ items: [], total: 0, page: 1, page_size: 50 });
|
||
}
|
||
|
||
// —— 司命台 · 租户管理(管理员用)——
|
||
if (m === "GET" && path === "/admin/tenants/overview") {
|
||
return ok([]);
|
||
}
|
||
{
|
||
const mt = path.match(/^\/admin\/tenants\/([^/]+)\/characters$/);
|
||
if (mt && m === "GET") return ok([]);
|
||
}
|
||
{
|
||
const mt = path.match(/^\/admin\/tenants\/([^/]+)\/memories$/);
|
||
if (mt && m === "GET") return ok([]);
|
||
}
|
||
|
||
// —— 用户管理(§19.6)——
|
||
if (m === "GET" && path === "/admin/users") {
|
||
return ok([]);
|
||
}
|
||
if (m === "POST" && path === "/admin/users") {
|
||
return ok({
|
||
id: `user-${Date.now()}`,
|
||
username: (json as Record<string, unknown>)?.username ?? "new_user",
|
||
email: "",
|
||
is_active: true,
|
||
is_admin: false,
|
||
created_at: new Date().toISOString(),
|
||
});
|
||
}
|
||
{
|
||
const mt = path.match(/^\/admin\/users\/([^/]+)$/);
|
||
if (mt && m === "PATCH") {
|
||
return ok({ id: mt[1], ...(json as Record<string, unknown>) });
|
||
}
|
||
}
|
||
|
||
// —— 工具(§15;开放集合:增删改,器用治理)——
|
||
if (m === "GET" && path === "/tools") {
|
||
return ok(data.tools);
|
||
}
|
||
if (m === "POST" && path === "/tools") {
|
||
return ok(data.createTool((json ?? {}) as CreateToolRequest));
|
||
}
|
||
// 工具调用详情(GET /tools/calls/{tool_call_id})
|
||
{
|
||
const mt = path.match(/^\/tools\/calls\/([^/]+)$/);
|
||
if (mt && m === "GET") {
|
||
return ok({
|
||
id: mt[1],
|
||
tool: "unknown",
|
||
status: "completed",
|
||
started_at: new Date().toISOString(),
|
||
completed_at: new Date().toISOString(),
|
||
});
|
||
}
|
||
}
|
||
// 工具调用(POST /tools/{tool_name}/invoke,非流式)
|
||
{
|
||
const mt = path.match(/^\/tools\/([^/]+)\/invoke$/);
|
||
if (mt && m === "POST") {
|
||
return ok({
|
||
status: "completed",
|
||
result: {
|
||
ok: true,
|
||
exit_code: 0,
|
||
stdout: "",
|
||
stderr: "",
|
||
duration_ms: 100,
|
||
timeout: false,
|
||
truncated: false,
|
||
},
|
||
trace_id: `trace-${mt[1]}-${Date.now()}`,
|
||
});
|
||
}
|
||
}
|
||
{
|
||
const mt = path.match(/^\/tools\/([^/]+)$/);
|
||
if (mt && m === "PATCH") {
|
||
const body = (json ?? {}) as Record<string, unknown>;
|
||
const found = data.tools.find((t) => t.name === mt[1]);
|
||
const base =
|
||
found ??
|
||
data.createTool({
|
||
name: mt[1],
|
||
description: "自定义工具",
|
||
risk_level: "L2",
|
||
permission_policy: "ask",
|
||
side_effect: false,
|
||
audit_required: false,
|
||
});
|
||
return ok({ ...base, ...body, name: mt[1] });
|
||
}
|
||
if (mt && m === "DELETE") return ok(undefined, 204);
|
||
}
|
||
{
|
||
const mt = path.match(/^\/safety\/confirmations\/([^/]+)\/(approve|deny)$/);
|
||
if (mt && m === "POST") {
|
||
return mt[2] === "approve"
|
||
? ok({
|
||
status: "completed",
|
||
result: {
|
||
ok: true,
|
||
exit_code: 0,
|
||
stdout: "",
|
||
stderr: "",
|
||
duration_ms: 120,
|
||
timeout: false,
|
||
truncated: false,
|
||
trace_id: `trace-${mt[1]}`,
|
||
},
|
||
})
|
||
: ok({ status: "cancelled", confirmation_id: mt[1] });
|
||
}
|
||
}
|
||
|
||
// —— 人物 ——
|
||
if (m === "GET" && path === "/characters/active") {
|
||
const found = data.characters.find((c) => c.id === _mockActiveCharacterId);
|
||
return ok(found ?? data.activeCharacter);
|
||
}
|
||
if (m === "GET" && path === "/characters") {
|
||
return ok(paginate(data.characterList));
|
||
}
|
||
if (m === "GET" && path === "/characters/templates") {
|
||
return ok(data.characterTemplates);
|
||
}
|
||
{
|
||
const mt = path.match(/^\/characters\/([^/]+)$/);
|
||
if (mt && m === "GET") {
|
||
const found = data.characters.find((c) => c.id === mt[1]);
|
||
return ok(found ?? data.activeCharacter);
|
||
}
|
||
}
|
||
{
|
||
const mt = path.match(/^\/characters\/([^/]+)\/activate$/);
|
||
if (mt && m === "POST") {
|
||
_mockActiveCharacterId = mt[1];
|
||
return ok({ active_character_id: mt[1], status: "active" });
|
||
}
|
||
}
|
||
if (m === "POST" && path === "/characters") {
|
||
const created = data.createCharacter(
|
||
(json ?? {}) as CreateCharacterRequest,
|
||
);
|
||
// 入库:加入 mock 列表,使后续查询可访问
|
||
data.addCharacterToList(created);
|
||
// 模板创建的人物自动设为激活,与前端的创建后跳转表现一致
|
||
if ((json as CreateCharacterRequest)?.template_key) {
|
||
created.status = "active";
|
||
// 同步更新列表项的状态
|
||
const item = data.characterList.find((c) => c.id === created.id);
|
||
if (item) item.status = "active";
|
||
_mockActiveCharacterId = created.id;
|
||
}
|
||
return ok(created);
|
||
}
|
||
{
|
||
const mt = path.match(/^\/characters\/([^/]+)\/profile$/);
|
||
if (mt && m === "PATCH") {
|
||
return ok(
|
||
data.updateCharacterProfile(
|
||
mt[1],
|
||
(json ?? {}) as UpdateProfileRequest,
|
||
),
|
||
);
|
||
}
|
||
}
|
||
{
|
||
const mt = path.match(/^\/characters\/([^/]+)\/model-policy$/);
|
||
if (mt && m === "PATCH") {
|
||
return ok(
|
||
data.updateCharacterModelPolicy(
|
||
mt[1],
|
||
(json ?? {}) as UpdateModelPolicyRequest,
|
||
),
|
||
);
|
||
}
|
||
}
|
||
{
|
||
const mt = path.match(/^\/characters\/([^/]+)\/(pause|resume|archive)$/);
|
||
if (mt && m === "POST") {
|
||
const found = data.characters.find((c) => c.id === mt[1]);
|
||
const base = found ?? data.activeCharacter;
|
||
const status =
|
||
mt[2] === "pause"
|
||
? "paused"
|
||
: mt[2] === "resume"
|
||
? "active"
|
||
: "archived";
|
||
return ok({
|
||
...base,
|
||
id: mt[1],
|
||
status,
|
||
updated_at: new Date().toISOString(),
|
||
});
|
||
}
|
||
}
|
||
|
||
// —— 记忆 ——
|
||
if (m === "GET" && path === "/memory") {
|
||
return ok(paginate(data.memories));
|
||
}
|
||
if (m === "POST" && path === "/memory") {
|
||
return ok(data.createMemory(json as CreateMemoryRequest));
|
||
}
|
||
{
|
||
const mt = path.match(/^\/memory\/([^/]+)$/);
|
||
if (mt && m === "PATCH") {
|
||
return ok(data.updateMemory(mt[1], json as UpdateMemoryRequest));
|
||
}
|
||
if (mt && m === "DELETE") {
|
||
return ok(undefined, 204);
|
||
}
|
||
}
|
||
|
||
// —— 人格 ——
|
||
if (m === "GET" && path === "/personality/current") {
|
||
return ok(data.personality);
|
||
}
|
||
if (m === "GET" && path === "/personality/versions") {
|
||
return ok(paginate(data.personalityVersions));
|
||
}
|
||
if (m === "POST" && path === "/personality/propose-update") {
|
||
const body = (json ?? {}) as {
|
||
reason?: string;
|
||
fields?: Record<string, unknown>;
|
||
};
|
||
return ok({
|
||
proposal_id: `personality_proposal_${Date.now()}`,
|
||
character_id: data.MOCK_CHARACTER_ID,
|
||
status: "pending",
|
||
reason: body.reason ?? "",
|
||
fields: body.fields ?? {},
|
||
created_at: new Date().toISOString(),
|
||
});
|
||
}
|
||
{
|
||
const mt = path.match(
|
||
/^\/personality\/proposals\/([^/]+)\/(approve|reject)$/,
|
||
);
|
||
if (mt && m === "POST") {
|
||
const approved = mt[2] === "approve";
|
||
return ok({
|
||
proposal_id: mt[1],
|
||
character_id: data.MOCK_CHARACTER_ID,
|
||
status: approved ? "approved" : "rejected",
|
||
reason: "",
|
||
fields: {},
|
||
resulting_version_id: approved
|
||
? `pv-jingwu-00${data.personalityVersions.length + 1}`
|
||
: null,
|
||
reviewed_at: new Date().toISOString(),
|
||
created_at: new Date().toISOString(),
|
||
});
|
||
}
|
||
}
|
||
{
|
||
const mt = path.match(/^\/personality\/versions\/([^/]+)\/rollback$/);
|
||
if (mt && m === "POST") {
|
||
const found = data.personalityVersions.find((v) => v.id === mt[1]);
|
||
return ok(found ?? data.personality);
|
||
}
|
||
}
|
||
|
||
// —— 对话 ——
|
||
if (m === "GET" && path === "/chat/sessions") {
|
||
const characterId = query?.character_id as string | undefined;
|
||
const status = query?.status as string | undefined;
|
||
return ok(
|
||
paginate(
|
||
data.sessions.filter(
|
||
(s) =>
|
||
s.status !== "deleted" &&
|
||
(!characterId || s.character_id === characterId) &&
|
||
(!status || s.status === status),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
if (m === "POST" && path === "/chat/sessions") {
|
||
return ok(data.createSession((json ?? {}) as CreateSessionRequest));
|
||
}
|
||
{
|
||
const mt = path.match(/^\/chat\/sessions\/([^/]+)$/);
|
||
if (mt && m === "PATCH") {
|
||
const updated = data.updateSession(
|
||
mt[1],
|
||
(json ?? {}) as UpdateSessionRequest,
|
||
);
|
||
return ok(
|
||
updated ?? {
|
||
session_id: mt[1],
|
||
character_id: data.MOCK_CHARACTER_ID,
|
||
status: "active",
|
||
created_at: new Date().toISOString(),
|
||
},
|
||
);
|
||
}
|
||
if (mt && m === "DELETE") {
|
||
data.deleteSession(mt[1]);
|
||
return ok(undefined, 204);
|
||
}
|
||
}
|
||
{
|
||
const mt = path.match(/^\/chat\/sessions\/([^/]+)\/messages$/);
|
||
if (mt && m === "POST") {
|
||
return ok(data.sendMessage(mt[1]));
|
||
}
|
||
if (mt && m === "GET") {
|
||
// mock 不自动注入预设历史——进入/新建会话均为空,消息由本次对话在内存持有。
|
||
return ok(paginate([]));
|
||
}
|
||
}
|
||
{
|
||
const mt = path.match(
|
||
/^\/chat\/sessions\/([^/]+)\/messages\/([^/]+)\/version$/,
|
||
);
|
||
if (mt && m === "PATCH") {
|
||
const body = (json ?? {}) as { version_idx?: number };
|
||
return ok({
|
||
message_id: mt[2],
|
||
active_version_idx: body.version_idx ?? 0,
|
||
versions: [],
|
||
});
|
||
}
|
||
}
|
||
|
||
// —— 运行 ——
|
||
// 列表(GET /runs,支持分页参数)
|
||
if (m === "GET" && path === "/runs") {
|
||
return ok(paginate([data.getRun("run-mock-001")]));
|
||
}
|
||
{
|
||
const mt = path.match(/^\/runs\/([^/]+)$/);
|
||
if (mt && m === "GET") return ok(data.getRun(mt[1]));
|
||
}
|
||
{
|
||
const mt = path.match(/^\/runs\/([^/]+)\/cancel$/);
|
||
if (mt && m === "POST") return ok({ id: mt[1], status: "cancelled" });
|
||
}
|
||
{
|
||
const mt = path.match(/^\/runs\/([^/]+)\/events$/);
|
||
if (mt && m === "GET") return ok(data.listRunEvents(mt[1]));
|
||
}
|
||
|
||
// —— 世界模型(独立 /world 域:实体 + 关系;实体编辑/删除仍走 admin)——
|
||
if (m === "GET" && path === "/world/entities") {
|
||
return ok(paginate(data.worldEntities));
|
||
}
|
||
if (m === "POST" && path === "/world/entities") {
|
||
return ok(data.createWorldEntity((json ?? {}) as CreateWorldEntityRequest));
|
||
}
|
||
{
|
||
const mt = path.match(/^\/world\/entities\/([^/]+)$/);
|
||
if (mt && m === "GET") {
|
||
const found = data.worldEntities.find((e) => e.id === mt[1]);
|
||
return ok(found ?? data.worldEntities[0]);
|
||
}
|
||
}
|
||
if (m === "GET" && path === "/world/relations") {
|
||
return ok(paginate(data.mockWorldRelations));
|
||
}
|
||
if (m === "POST" && path === "/world/relations") {
|
||
return ok(
|
||
data.createWorldRelation((json ?? {}) as CreateWorldRelationRequest),
|
||
);
|
||
}
|
||
{
|
||
const mt = path.match(/^\/world\/relations\/([^/]+)$/);
|
||
if (mt && m === "DELETE") return ok(undefined, 204);
|
||
}
|
||
|
||
// —— 候选审核(跨来源统一入口:记忆候选 / 世界实体候选)——
|
||
{
|
||
const mt = path.match(/^\/characters\/([^/]+)\/memory-candidates$/);
|
||
if (mt && m === "GET") {
|
||
const source = query?.source as string | undefined;
|
||
const items = source
|
||
? data.mockMemoryCandidates.filter((c) =>
|
||
source === "cycle" ? c.cycle_id : c.import_job_id,
|
||
)
|
||
: data.mockMemoryCandidates;
|
||
return ok(paginate(items));
|
||
}
|
||
}
|
||
{
|
||
const mt = path.match(/^\/memory-candidates\/([^/]+)\/(approve|reject)$/);
|
||
if (mt && m === "POST") {
|
||
return ok({
|
||
id: mt[1],
|
||
status: mt[2] === "approve" ? "approved" : "rejected",
|
||
});
|
||
{
|
||
const mt = path.match(/^\/world\/relations\/([^/]+)$/);
|
||
if (mt && m === "DELETE") return ok(undefined, 204);
|
||
}
|
||
}
|
||
}
|
||
{
|
||
const mt = path.match(/^\/characters\/([^/]+)\/world\/entity-proposals$/);
|
||
if (mt && m === "GET") return ok(paginate(data.mockWorldEntityProposals));
|
||
}
|
||
{
|
||
const mt = path.match(/^\/world\/entities\/([^/]+)\/(approve|reject)$/);
|
||
if (mt && m === "POST") {
|
||
return ok({
|
||
id: mt[1],
|
||
status: mt[2] === "approve" ? "confirmed" : "rejected",
|
||
});
|
||
}
|
||
}
|
||
|
||
// —— 资料导入流水线(§10,挂在 /characters/{id}/imports 下)——
|
||
{
|
||
const mt = path.match(/^\/characters\/([^/]+)\/imports$/);
|
||
if (mt && m === "POST") {
|
||
return ok(data.createImportJob((json ?? {}) as CreateImportRequest));
|
||
}
|
||
if (mt && m === "GET") return ok(paginate([data.mockImportJob]));
|
||
}
|
||
{
|
||
const mt = path.match(/^\/characters\/([^/]+)\/imports\/([^/]+)$/);
|
||
if (mt && m === "GET") return ok(data.mockImportJob);
|
||
}
|
||
{
|
||
const mt = path.match(/^\/characters\/([^/]+)\/imports\/([^/]+)\/items$/);
|
||
if (mt && m === "GET") return ok(paginate(data.mockImportItems));
|
||
}
|
||
{
|
||
const mt = path.match(
|
||
/^\/characters\/([^/]+)\/imports\/([^/]+)\/profile-candidates$/,
|
||
);
|
||
if (mt && m === "POST") return ok({ status: "ok" });
|
||
if (mt && m === "GET") return ok(paginate(data.mockProfileCandidates));
|
||
}
|
||
{
|
||
const mt = path.match(
|
||
/^\/characters\/([^/]+)\/imports\/([^/]+)\/memory-candidates$/,
|
||
);
|
||
if (mt && m === "POST") return ok({ status: "ok" });
|
||
if (mt && m === "GET") return ok(paginate(data.mockMemoryCandidates));
|
||
}
|
||
{
|
||
const mt = path.match(
|
||
/^\/characters\/([^/]+)\/imports\/([^/]+)\/personality-proposals$/,
|
||
);
|
||
if (mt && m === "POST") return ok({ status: "ok" });
|
||
}
|
||
{
|
||
const mt = path.match(
|
||
/^\/characters\/([^/]+)\/profile-candidates\/batch-review$/,
|
||
);
|
||
if (mt && m === "POST") {
|
||
const body = (json ?? {}) as BatchReviewRequest;
|
||
const approve =
|
||
body.decisions?.filter((d) => d.decision === "approve").length ?? 0;
|
||
const reject =
|
||
body.decisions?.filter((d) => d.decision === "reject").length ?? 0;
|
||
return ok({ applied: approve, rejected: reject, skipped: 0 });
|
||
}
|
||
}
|
||
{
|
||
const mt = path.match(
|
||
/^\/characters\/([^/]+)\/memory-candidates\/batch-review$/,
|
||
);
|
||
if (mt && m === "POST") {
|
||
const body = (json ?? {}) as BatchReviewRequest;
|
||
const approve =
|
||
body.decisions?.filter((d) => d.decision === "approve").length ?? 0;
|
||
const reject =
|
||
body.decisions?.filter((d) => d.decision === "reject").length ?? 0;
|
||
return ok({ applied: approve, rejected: reject, skipped: 0 });
|
||
}
|
||
}
|
||
|
||
// —— Skill 能力包(百工枢,/tools/skills)——
|
||
if (m === "GET" && path === "/tools/skills") {
|
||
const status = query?.status as string | undefined;
|
||
const items = status
|
||
? data.mockSkills.filter((s) => s.status === status)
|
||
: data.mockSkills;
|
||
return ok(items);
|
||
}
|
||
if (m === "POST" && path === "/tools/skills") {
|
||
return ok(data.createSkillData((json ?? {}) as CreateSkillRequest));
|
||
}
|
||
{
|
||
const mt = path.match(/^\/tools\/skills\/([^/]+)$/);
|
||
if (mt && m === "PATCH") {
|
||
const body = (json ?? {}) as Record<string, unknown>;
|
||
const found = data.mockSkills.find((s) => s.id === mt[1]);
|
||
const base =
|
||
found ?? data.createSkillData({ name: String(body.name ?? mt[1]) });
|
||
return ok({
|
||
...base,
|
||
...body,
|
||
id: mt[1],
|
||
updated_at: new Date().toISOString(),
|
||
});
|
||
}
|
||
if (mt && m === "DELETE") return ok(undefined, 204);
|
||
}
|
||
|
||
// —— MCP server(百工枢,/tools/mcp-servers)——
|
||
if (m === "GET" && path === "/tools/mcp-servers") {
|
||
const status = query?.status as string | undefined;
|
||
const items = status
|
||
? data.mockMcpServers.filter((s) => s.status === status)
|
||
: data.mockMcpServers;
|
||
return ok(items);
|
||
}
|
||
if (m === "POST" && path === "/tools/mcp-servers") {
|
||
return ok(data.createMcpServerData((json ?? {}) as CreateMcpServerRequest));
|
||
}
|
||
{
|
||
const mt = path.match(/^\/tools\/mcp-servers\/([^/]+)$/);
|
||
if (mt && m === "DELETE") return ok(undefined, 204);
|
||
if (mt && m === "PATCH") {
|
||
const found = data.mockMcpServers.find((s) => s.id === mt[1]);
|
||
const body = (json ?? {}) as Record<string, unknown>;
|
||
const base =
|
||
found ?? data.createMcpServerData({ name: String(body.name ?? mt[1]) });
|
||
return ok({ ...base, ...body, id: mt[1] });
|
||
}
|
||
}
|
||
{
|
||
const mt = path.match(/^\/tools\/mcp-servers\/([^/]+)\/status$/);
|
||
if (mt && m === "GET") {
|
||
return ok({
|
||
server_id: mt[1],
|
||
process: "stopped",
|
||
pid: null,
|
||
uptime_seconds: null,
|
||
});
|
||
}
|
||
}
|
||
{
|
||
const mt = path.match(
|
||
/^\/tools\/mcp-servers\/([^/]+)\/(start|stop|restart|refresh)$/,
|
||
);
|
||
if (mt && m === "POST") {
|
||
if (mt[2] === "start") {
|
||
return ok({
|
||
server_id: mt[1],
|
||
process: "running",
|
||
pid: 12345,
|
||
uptime_seconds: 0,
|
||
});
|
||
}
|
||
if (mt[2] === "stop") {
|
||
return ok({
|
||
server_id: mt[1],
|
||
process: "stopped",
|
||
pid: null,
|
||
uptime_seconds: null,
|
||
});
|
||
}
|
||
if (mt[2] === "restart") {
|
||
return ok({
|
||
server_id: mt[1],
|
||
process: "running",
|
||
pid: 12346,
|
||
uptime_seconds: 0,
|
||
});
|
||
}
|
||
if (mt[2] === "refresh") {
|
||
return ok(
|
||
data.mockMcpServers.find((s) => s.id === mt[1]) ??
|
||
data.mockMcpServers[0],
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
// —— 统一能力列举(GET /tools/capabilities,builtin/mcp/skill 三源)——
|
||
if (m === "GET" && path === "/tools/capabilities") {
|
||
return ok(data.mockCapabilities);
|
||
}
|
||
|
||
// —— 辰正时态(/time/*)——
|
||
if (m === "GET" && path === "/time/snapshot") {
|
||
const now = new Date();
|
||
const h = now.getHours();
|
||
const dayPeriod =
|
||
h < 5
|
||
? "凌晨"
|
||
: h < 8
|
||
? "清晨"
|
||
: h < 12
|
||
? "上午"
|
||
: h < 14
|
||
? "正午"
|
||
: h < 18
|
||
? "下午"
|
||
: h < 21
|
||
? "傍晚"
|
||
: "深夜";
|
||
const shichenIdx = Math.floor(((h + 1) % 24) / 2);
|
||
const shichenNames = [
|
||
"zi",
|
||
"chou",
|
||
"yin",
|
||
"mao",
|
||
"chen",
|
||
"si",
|
||
"wu",
|
||
"wei",
|
||
"shen",
|
||
"you",
|
||
"xu",
|
||
"hai",
|
||
];
|
||
return ok({
|
||
snapshot: {
|
||
now_utc: now.toISOString(),
|
||
local_time: now.toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }),
|
||
timezone: "Asia/Shanghai",
|
||
local_hour: h,
|
||
shichen: shichenNames[shichenIdx],
|
||
day_period: dayPeriod,
|
||
weekday: now.getDay(),
|
||
is_weekend: now.getDay() === 0 || now.getDay() === 6,
|
||
solar_date: now.toISOString().slice(0, 10),
|
||
lunar: {
|
||
year: now.getFullYear(),
|
||
month: now.getMonth() + 1,
|
||
day: now.getDate(),
|
||
leap: false,
|
||
text: "腊月十五",
|
||
},
|
||
season:
|
||
h >= 3 && h < 6
|
||
? "春"
|
||
: h >= 6 && h < 9
|
||
? "夏"
|
||
: h >= 9 && h < 12
|
||
? "秋"
|
||
: "冬",
|
||
solar_term: null,
|
||
festivals: [],
|
||
acquaintance: { total_days: 365, natural_language: "相识一载" },
|
||
offline: { seconds: 120, natural_language: "刚刚还在" },
|
||
next_milestone: {
|
||
days_until: 30,
|
||
name: "相识周年",
|
||
date: now.toISOString().slice(0, 10),
|
||
},
|
||
active_milestones: [],
|
||
home_tz_diff_minutes: 0,
|
||
location: { city: "上海", country: "中国", timezone: "Asia/Shanghai" },
|
||
},
|
||
factors: {
|
||
festival_joy: 0.3,
|
||
late_night_fragility:
|
||
dayPeriod === "深夜" || dayPeriod === "凌晨" ? 0.7 : 0.2,
|
||
reunion_intensity: 0.1,
|
||
weekend_relax: now.getDay() === 0 || now.getDay() === 6 ? 0.6 : 0.2,
|
||
season_resonance: 0.5,
|
||
same_period_resonance: 0.4,
|
||
care_suggest: dayPeriod === "深夜" ? 0.8 : 0.3,
|
||
care_kind: dayPeriod === "深夜" ? "jetlag" : null,
|
||
},
|
||
context_text:
|
||
dayPeriod === "深夜"
|
||
? "夜深了,万籁俱寂。这是思绪最容易沉降的时刻——适合温缓的承接,不宜急于追问。"
|
||
: "白日的喧嚣渐远,此刻宜静不宜动。让话语如湖面般展开,不催不赶。",
|
||
});
|
||
}
|
||
if (m === "GET" && path === "/time/anniversaries") {
|
||
return ok(_annivList);
|
||
}
|
||
if (m === "POST" && path === "/time/anniversaries") {
|
||
const body = (json ?? {}) as Record<string, unknown>;
|
||
const anniv = {
|
||
id: `anniv-${Date.now()}`,
|
||
...body,
|
||
recurs_yearly: true,
|
||
source: body.source ?? "manual",
|
||
};
|
||
_annivList.push(anniv);
|
||
return ok(anniv);
|
||
}
|
||
{
|
||
const mt = path.match(/^\/time\/anniversaries\/([^/]+)$/);
|
||
if (mt && m === "GET") {
|
||
const found = _annivList.find((a) => a.id === mt[1]);
|
||
return ok(
|
||
found ?? {
|
||
id: mt[1],
|
||
name: "纪念日",
|
||
calendar: "solar",
|
||
date: "2025-01-01",
|
||
recurs_yearly: true,
|
||
kind: "anniversary",
|
||
subject_type: "relationship",
|
||
source: "manual",
|
||
},
|
||
);
|
||
}
|
||
if (mt && m === "PATCH") {
|
||
const idx = _annivList.findIndex((a) => a.id === mt[1]);
|
||
const body = (json ?? {}) as Record<string, unknown>;
|
||
if (idx >= 0) {
|
||
_annivList[idx] = { ..._annivList[idx], ...body, id: mt[1] };
|
||
return ok(_annivList[idx]);
|
||
}
|
||
return ok({ id: mt[1], ...body, recurs_yearly: true });
|
||
}
|
||
if (mt && m === "DELETE") {
|
||
const idx = _annivList.findIndex((a) => a.id === mt[1]);
|
||
if (idx >= 0) _annivList.splice(idx, 1);
|
||
return ok(undefined, 204);
|
||
}
|
||
}
|
||
if (m === "GET" && path === "/time/acquaintance") {
|
||
return ok({ total_days: 365, natural_language: "相识一载,如镜映月" });
|
||
}
|
||
if (m === "GET" && path === "/time/config") {
|
||
return ok({
|
||
user_timezone: "Asia/Shanghai",
|
||
home_timezone: "Asia/Shanghai",
|
||
default_city: "上海",
|
||
offline_active_minutes: 5,
|
||
offline_short_absence_minutes: 30,
|
||
offline_long_absence_minutes: 180,
|
||
offline_reunion_impact_minutes: 15,
|
||
care_enabled: true,
|
||
jetlag_threshold_minutes: 120,
|
||
});
|
||
}
|
||
|
||
return undefined;
|
||
}
|