Files
ARTEX/web/src/lib/mock/handler.ts
T

1303 lines
56 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Mock 路由:把 (method, path) 映射到 lib/mock/data 的静态数据。
// 未命中的一律返回安全默认([] / {} / {ok:true}),保证任何页面都不崩。
// 只在 NEXT_PUBLIC_MOCK=1 时经由 api.ts 的 http() 短路进入这里。
import {
classifyCompanyScopeLine,
companyScopeRuleError,
isCompanyScopeKind,
MAX_COMPANY_SCOPE_RULES,
normalizeCompanyScopeValue,
} from "../company-scope";
import type {
Asset,
BatchControlItem,
Company,
CompanyScopeRule,
Conversation,
IntentAsset,
ScopeRow,
Task,
TaskAssetMutation,
TaskAssetScopeMutation,
TaskCategory,
TaskLLMResolution,
TaskScopeRow,
TaskTemplate,
} from "../types";
import * as D from "./data";
const delay = (ms = 120) => new Promise((r) => setTimeout(r, ms));
// Requests mutate a runtime copy, never the exported fixtures. This keeps module
// initialization deterministic for tests/HMR while preserving state across mock calls.
const mockTasks = structuredClone(D.tasks);
const mockFindings = structuredClone(D.findings);
const mockLLMRecords = structuredClone(D.llmRecords);
const mockTaskTemplates = structuredClone(D.taskTemplates);
const mockTaskCategories = structuredClone(D.taskCategories);
const mockConversations = structuredClone(D.conversations);
const mockIntents = structuredClone(D.intents);
const mockCompanies = structuredClone(D.companies);
const mockAssets = structuredClone(D.assets);
const mockTaskAssetIDs = new Map(D.tasks.map((task, index) => [task.id, index + 1]));
const mockTaskScopes = new Map<string, TaskScopeRow[]>();
let nextMockTaskAssetID = D.tasks.length + 1;
let mockActiveTask = D.ACTIVE_TASK;
function mockTaskAssetID(taskID: string): number | undefined {
const numeric = Number(taskID);
if (Number.isInteger(numeric) && numeric > 0) return numeric;
return mockTaskAssetIDs.get(taskID);
}
function mockAssetCounts(taskID?: string | null): Record<string, number> {
const numericTaskID = taskID ? mockTaskAssetID(taskID) : undefined;
return mockAssets.reduce<Record<string, number>>((counts, asset) => {
if (taskID && (numericTaskID === undefined || !asset.task_ids.includes(numericTaskID))) return counts;
counts[asset.type] = (counts[asset.type] ?? 0) + 1;
return counts;
}, {});
}
function mockAssetMatchesDSL(asset: Asset, dsl: string): boolean {
const query = dsl
.replaceAll(/[()"]/g, " ")
.replaceAll(/\b(?:AND|OR)\b/gi, " ")
.replaceAll(/\b[a-z_][a-z0-9_]*(?:==|!=|>=|<=|=|>|<)/gi, " ")
.trim()
.toLowerCase();
if (!query) return true;
const haystack = JSON.stringify(asset).toLowerCase();
return query.split(/\s+/).every((term) => haystack.includes(term));
}
function mockTaskCategorySnapshot(): TaskCategory[] {
return mockTaskCategories.map((category) => ({
...category,
task_count: mockTasks.filter((task) => task.category_id === category.id).length,
}));
}
function mockScopeRows(
companyID: number,
input: unknown,
existing: ScopeRow[] = [],
): { rows: ScopeRow[]; invalid: number; skipped: number } {
if (!Array.isArray(input)) return { rows: [], invalid: 0, skipped: 0 };
if (input.length > MAX_COMPANY_SCOPE_RULES) throw new Error(`企业范围最多支持 ${MAX_COMPANY_SCOPE_RULES} 条`);
let nextID =
mockCompanies.flatMap((company) => company.scope ?? []).reduce((max, row) => Math.max(max, row.id), 0) + 1;
const rows: ScopeRow[] = [];
let invalid = 0;
let skipped = 0;
const keys = new Set(
existing.map((row) => `${row.kind}|${row.domain ?? row.net ?? row.value ?? row.raw.trim().toLowerCase()}`),
);
for (const [index, candidate] of input.entries()) {
let rule: CompanyScopeRule | undefined;
if (typeof candidate === "string") {
rule = classifyCompanyScopeLine(candidate, index + 1).rule;
} else if (candidate && typeof candidate === "object") {
const item = candidate as { kind?: unknown; value?: unknown };
const value = String(item.value ?? "").trim();
if (item.kind === undefined || item.kind === "") rule = classifyCompanyScopeLine(value, index + 1).rule;
else if (isCompanyScopeKind(item.kind)) rule = { kind: item.kind, value };
}
if (!rule || companyScopeRuleError(rule)) {
invalid++;
continue;
}
const normalized = normalizeCompanyScopeValue(rule);
const row: ScopeRow = { id: nextID++, company_id: companyID, kind: rule.kind, raw: rule.value.trim() };
if (rule.kind === "domain") row.domain = normalized;
else if (rule.kind === "ip") row.net = `${normalized}/${normalized.includes(":") ? 128 : 32}`;
else if (rule.kind === "cidr") row.net = normalized;
else row.value = normalized;
const key = `${row.kind}|${row.domain ?? row.net ?? row.value}`;
if (keys.has(key)) {
skipped++;
continue;
}
keys.add(key);
rows.push(row);
}
if (existing.length + rows.length > MAX_COMPANY_SCOPE_RULES) {
throw new Error(`企业范围规则过多:每个企业最多 ${MAX_COMPANY_SCOPE_RULES} 条`);
}
return { rows, invalid, skipped };
}
function mockProfileResolution(profileID: number | undefined, source: TaskLLMResolution["source"]): TaskLLMResolution {
const profile = D.llmProfiles.find((item) => Number(item.id) === profileID);
if (!profile) {
return { name: "", format: "", model: "", source, available: false, reason: "LLM 配置不存在" };
}
return {
profile_id: Number(profile.id),
name: profile.name,
format: profile.format,
model: profile.model,
source,
available: Boolean(profile.api_key_hint),
reason: profile.api_key_hint ? undefined : "LLM 配置未设置 API Key",
};
}
// Mirrors the backend precedence in server/task_resolution.go:
// Agent 绑定 → 任务 LLM 配置链 → 全局配置 → 环境配置。
function mockRoleResolution(task: Task, agentKey: "mainagent" | "planner" | "worker"): TaskLLMResolution {
const agent = D.agents.find((item) => item.key === agentKey);
if (agent?.llm_profile_id) {
const bound = mockProfileResolution(agent.llm_profile_id, "agent_binding");
if (bound.available) return bound;
}
if (task.llm_profile_ids?.length) {
if (task.active_llm_profile_id === undefined) {
return {
name: "",
format: "",
model: "",
source: "task_chain",
available: false,
reason: "任务 LLM 配置链额度已耗尽",
};
}
return mockProfileResolution(task.active_llm_profile_id, "task_chain");
}
const globalProfile = D.llmProfiles.find((item) => item.is_default);
if (globalProfile) return mockProfileResolution(Number(globalProfile.id), "global_profile");
return {
name: "全局配置",
format: D.llmConfig.provider,
model: D.llmConfig.model,
source: "environment",
available: true,
};
}
function bodyIDs(value: unknown): string[] {
if (!Array.isArray(value)) return [];
const ids = [...new Set(value.map((item) => String(item)).filter(Boolean))];
if (ids.length > 100) throw new Error("批量操作最多支持 100 个 ID");
return ids;
}
type MockIntentControlResult = { ok: boolean; state?: string; error?: string };
function controlMockIntent(id: string, action: "pause" | "resume"): MockIntentControlResult {
const intent = mockIntents.find((item) => item.id === id);
if (!intent) return { ok: false, error: "意图不存在" };
const requiredState = action === "pause" ? "running" : "paused";
if (intent.inherited || intent.state !== requiredState) {
return { ok: false, state: intent.state, error: "Worker 状态已变化" };
}
intent.state = action === "pause" ? "paused" : "open";
return { ok: true, state: intent.state };
}
function controlMockTask(id: string, action: "pause" | "resume"): BatchControlItem {
const task = mockTasks.find((item) => item.id === id);
if (!task) return { id, ok: false, error: "任务不存在" };
if (task.status === "done" || task.status === "failed" || task.status === "timeout") {
return { id, ok: false, status: task.status, error: "终态任务不可控制" };
}
if (action === "pause") {
if (task.paused || task.status === "paused") {
return { id, ok: false, status: task.status, error: "任务已经暂停" };
}
task.paused = true;
task.queued = false;
task.status = "paused";
task.engine_mode = "paused";
} else {
if (!task.paused && task.status !== "paused") {
return { id, ok: false, status: task.status, error: "任务未暂停" };
}
task.paused = false;
task.queued = false;
task.status = "running";
task.engine_mode = "exploring";
}
return { id, ok: true, status: task.status, queued: false };
}
function sortMockConversations() {
mockConversations.sort((a, b) => {
const aPinned = a.pinned_at ? 1 : 0;
const bPinned = b.pinned_at ? 1 : 0;
if (aPinned !== bPinned) return bPinned - aPinned;
const aTime = a.pinned_at ?? a.updated_at;
const bTime = b.pinned_at ?? b.updated_at;
return bTime.localeCompare(aTime) || b.id - a.id;
});
}
function sortMockTasks() {
mockTasks.sort((a, b) => {
const aPinned = a.pinned_at ? 1 : 0;
const bPinned = b.pinned_at ? 1 : 0;
if (aPinned !== bPinned) return bPinned - aPinned;
if (a.pinned_at !== b.pinned_at) return (b.pinned_at ?? "").localeCompare(a.pinned_at ?? "");
return b.id.localeCompare(a.id, undefined, { numeric: true });
});
}
function normalizedTemplateName(value: unknown): string {
return String(value ?? "")
.trim()
.split(/\s+/)
.join(" ");
}
function parseBody(body?: BodyInit | null): Record<string, unknown> {
if (typeof body !== "string") return {};
try {
return JSON.parse(body) as Record<string, unknown>;
} catch {
return {};
}
}
export async function mockHandle<T>(method: string, rawPath: string, body?: BodyInit | null): Promise<T> {
await delay();
const [path, qs] = rawPath.split("?");
const q = new URLSearchParams(qs ?? "");
const seg = path.split("/").filter(Boolean); // ["exploration","activity"]
const m = method.toUpperCase();
const b = parseBody(body);
return route(m, path, seg, q, b) as T;
}
function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Record<string, unknown>): unknown {
const task = q.get("task") ?? undefined;
// ── auth:让 demo 直接进主界面 ──
if (path === "/auth/status") return { initialized: true };
if (path === "/auth/login" || path === "/auth/init") return { token: "mock-demo" };
if (path === "/auth/change-password") return { ok: true };
// ── tasks ──
if (path === "/tasks" && m === "GET") {
sortMockTasks();
return { tasks: mockTasks, active: mockActiveTask };
}
if (path === "/tasks" && m === "POST") {
let suffix = 1;
while (mockTasks.some((item) => item.id === `t-new-${suffix}`)) suffix++;
const id = `t-new-${suffix}`;
const now = new Date();
const profileIDs = [...((b.llm_profile_ids as number[] | undefined) ?? [])];
const sourceTaskIDs = [...((b.source_task_ids as string[] | undefined) ?? [])];
const companyIDs = [...((b.company_ids as number[] | undefined) ?? [])];
const categoryID = typeof b.category_id === "number" ? b.category_id : undefined;
const category = categoryID === undefined ? undefined : mockTaskCategories.find((item) => item.id === categoryID);
if (categoryID !== undefined && !category) throw new Error("任务分类不存在");
const created: Task = {
id,
name: String(b.name ?? ""),
category_id: category?.id,
category_name: category?.name,
description: String(b.description ?? "新任务"),
goal: String(b.goal ?? ""),
status: "created",
created_at: now.toISOString(),
created_unix: Math.floor(now.getTime() / 1000),
paused: false,
active: true,
in_flight: 0,
stalled: false,
goals_total: 0,
goals_met: 0,
engine_mode: "idle",
tokens: { input_tokens: 0, output_tokens: 0, cache_read_tokens: 0, cache_write_tokens: 0 },
llm_profile_id: profileIDs[0],
llm_profile_ids: profileIDs,
active_llm_profile_id: profileIDs[0],
llm_failover_state: profileIDs.length ? "ready" : "default",
source_task_ids: sourceTaskIDs,
company_ids: companyIDs,
};
mockTaskAssetIDs.set(id, nextMockTaskAssetID++);
for (const item of mockTasks) item.active = false;
mockTasks.unshift(created);
mockActiveTask = id;
return created;
}
if (path === "/task-categories" && m === "GET") return { categories: mockTaskCategorySnapshot() };
if (path === "/task-categories" && m === "POST") {
const name = normalizedTemplateName(b.name);
if (!name) throw new Error("分类名称不能为空");
if (mockTaskCategories.some((category) => category.name.toLowerCase() === name.toLowerCase())) {
throw new Error("分类名称已存在");
}
const now = new Date().toISOString();
const category: TaskCategory = {
id: mockTaskCategories.reduce((maximum, item) => Math.max(maximum, item.id), 0) + 1,
name,
task_count: 0,
created_at: now,
updated_at: now,
};
mockTaskCategories.push(category);
return category;
}
if (seg[0] === "task-categories" && seg.length === 2 && m === "PATCH") {
const category = mockTaskCategories.find((item) => item.id === Number(seg[1]));
if (!category) throw new Error("任务分类不存在");
const name = normalizedTemplateName(b.name);
if (!name) throw new Error("分类名称不能为空");
if (mockTaskCategories.some((item) => item.id !== category.id && item.name.toLowerCase() === name.toLowerCase())) {
throw new Error("分类名称已存在");
}
category.name = name;
category.updated_at = new Date().toISOString();
for (const task of mockTasks) {
if (task.category_id === category.id) task.category_name = name;
}
return { ...category, task_count: mockTasks.filter((task) => task.category_id === category.id).length };
}
if (seg[0] === "task-categories" && seg.length === 2 && m === "DELETE") {
const categoryID = Number(seg[1]);
const index = mockTaskCategories.findIndex((item) => item.id === categoryID);
if (index < 0) throw new Error("任务分类不存在");
mockTaskCategories.splice(index, 1);
for (const task of mockTasks) {
if (task.category_id !== categoryID) continue;
task.category_id = undefined;
task.category_name = undefined;
}
return { deleted: categoryID };
}
if (path === "/tasks/category/batch" && m === "POST") {
const requested = Array.isArray(b.task_ids) ? b.task_ids.map(String) : [];
const taskIDs = [...new Set(requested)];
if (taskIDs.length === 0 || taskIDs.length > 100) throw new Error("task_ids 数量必须为 1-100");
const categoryID = typeof b.category_id === "number" ? b.category_id : undefined;
const category = categoryID === undefined ? undefined : mockTaskCategories.find((item) => item.id === categoryID);
if (categoryID !== undefined && !category) throw new Error("任务分类不存在");
const items = taskIDs.map((id) => {
const task = mockTasks.find((item) => item.id === id);
if (!task) return { id, ok: false, error: "task not found" };
task.category_id = category?.id;
task.category_name = category?.name;
return { id, ok: true };
});
return {
items,
category: category ? mockTaskCategorySnapshot().find((item) => item.id === category.id) : null,
};
}
if (seg[0] === "tasks" && seg[2] === "category" && seg.length === 3 && m === "PATCH") {
const task = mockTasks.find((item) => item.id === seg[1]);
if (!task) throw new Error("任务不存在");
const categoryID = typeof b.category_id === "number" ? b.category_id : undefined;
const category = categoryID === undefined ? undefined : mockTaskCategories.find((item) => item.id === categoryID);
if (categoryID !== undefined && !category) throw new Error("任务分类不存在");
task.category_id = category?.id;
task.category_name = category?.name;
return task;
}
if (path === "/task-templates" && m === "GET") return { templates: mockTaskTemplates };
if (path === "/task-templates" && m === "POST") {
const now = new Date().toISOString();
const name = normalizedTemplateName(b.name);
const description = String(b.description ?? "").trim();
const goal = String(b.goal ?? "").trim();
if (!name || !description || !goal) throw new Error("请填写模板名称、描述和目标");
if (
mockTaskTemplates.some((template) => normalizedTemplateName(template.name).toLowerCase() === name.toLowerCase())
) {
throw new Error("模板名称已存在");
}
const nextID = mockTaskTemplates.reduce((max, template) => Math.max(max, template.id), 0) + 1;
const created: TaskTemplate = {
id: nextID,
name,
description,
goal,
created_at: now,
updated_at: now,
};
mockTaskTemplates.unshift(created);
return created;
}
if (seg[0] === "task-templates" && seg.length === 2 && m === "PATCH") {
const template = mockTaskTemplates.find((item) => item.id === Number(seg[1]));
if (!template) return {};
const name = typeof b.name === "string" ? normalizedTemplateName(b.name) : template.name;
const description = typeof b.description === "string" ? b.description.trim() : template.description;
const goal = typeof b.goal === "string" ? b.goal.trim() : template.goal;
if (!name || !description || !goal) throw new Error("请填写模板名称、描述和目标");
if (
mockTaskTemplates.some(
(item) => item.id !== template.id && normalizedTemplateName(item.name).toLowerCase() === name.toLowerCase(),
)
) {
throw new Error("模板名称已存在");
}
template.name = name;
template.description = description;
template.goal = goal;
template.updated_at = new Date().toISOString();
mockTaskTemplates.sort((a, b) => b.updated_at.localeCompare(a.updated_at) || b.id - a.id);
return template;
}
if (seg[0] === "task-templates" && seg.length === 2 && m === "DELETE") {
const id = Number(seg[1]);
const index = mockTaskTemplates.findIndex((item) => item.id === id);
if (index >= 0) mockTaskTemplates.splice(index, 1);
return { deleted: id };
}
if (seg[0] === "tasks" && seg.length === 2 && m === "PATCH") {
const task = mockTasks.find((item) => item.id === seg[1]);
if (!task) throw new Error("任务不存在");
if (typeof b.name === "string") task.name = b.name.trim();
if (typeof b.pinned === "boolean") {
task.pinned = b.pinned;
task.pinned_at = b.pinned ? (task.pinned_at ?? new Date().toISOString()) : null;
}
sortMockTasks();
return structuredClone(task);
}
if (seg[0] === "tasks" && seg.length === 2 && m === "DELETE") {
const id = seg[1];
const index = mockTasks.findIndex((item) => item.id === id);
if (index >= 0) mockTasks.splice(index, 1);
mockTaskAssetIDs.delete(id);
let findingsDeleted = 0;
if (b.delete_findings) {
for (let i = mockFindings.length - 1; i >= 0; i--) {
if (mockFindings[i].task_id !== id) continue;
mockFindings.splice(i, 1);
findingsDeleted++;
}
} else {
// PostgreSQL uses ON DELETE SET NULL for retained findings. Keep the mock
// grouped view consistent by moving them into the unassigned/deleted bucket.
for (const finding of mockFindings) {
if (finding.task_id !== id) continue;
finding.task_id = undefined;
finding.task_description = "";
}
}
let llmRecordsDeleted = 0;
if (b.delete_llm_records) {
for (let i = mockLLMRecords.length - 1; i >= 0; i--) {
if (mockLLMRecords[i].task_id !== id) continue;
mockLLMRecords.splice(i, 1);
llmRecordsDeleted++;
}
}
if (mockActiveTask === id) {
const nextActive = mockTasks[0]?.id ?? "";
mockActiveTask = nextActive;
for (const item of mockTasks) item.active = item.id === nextActive;
}
return {
deleted: id,
assets_deleted: b.delete_assets ? 1 : 0,
assets_detached: 0,
traffic_deleted: b.delete_traffic ? 1 : 0,
files_deleted: Boolean(b.delete_files),
findings_deleted: findingsDeleted,
llm_records_deleted: llmRecordsDeleted,
};
}
if (seg[0] === "tasks" && seg[2] === "llm" && m === "PUT") {
const ids = [...((b.llm_profile_ids as number[] | undefined) ?? [])];
const activeID = typeof b.active_llm_profile_id === "number" ? b.active_llm_profile_id : ids[0];
const target = mockTasks.find((item) => item.id === seg[1]);
if (target) {
target.llm_profile_ids = ids;
target.llm_profile_id = ids[0];
target.active_llm_profile_id = activeID;
target.llm_failover_state = ids.length ? "ready" : "default";
target.llm_failover_reason = undefined;
}
return {
id: seg[1],
llm_profile_ids: ids,
active_llm_profile_id: activeID,
llm_failover_state: ids.length ? "ready" : "default",
reopened_intents: 0,
};
}
if (seg[0] === "tasks" && seg[2] === "llm" && seg[3] === "resolution" && m === "GET") {
const task = mockTasks.find((item) => item.id === seg[1]);
if (!task) throw new Error("任务不存在");
return {
mainagent: mockRoleResolution(task, "mainagent"),
planner: mockRoleResolution(task, "planner"),
worker: mockRoleResolution(task, "worker"),
};
}
if (path === "/tasks/control/batch" && m === "POST") {
const action = b.action === "resume" ? "resume" : "pause";
return { items: bodyIDs(b.task_ids).map((id) => controlMockTask(id, action)) };
}
if (seg[0] === "tasks" && seg[2] === "intents" && seg[4] === "control" && m === "POST") {
const id = seg[3];
if (b.action === "cancel") {
const index = mockIntents.findIndex((item) => item.id === id);
const intent = mockIntents[index];
if (!intent || intent.inherited || (intent.state !== "running" && intent.state !== "paused")) {
throw new Error("Worker 状态已变化");
}
mockIntents.splice(index, 1);
return {
id: Number(id.replace(/\D/g, "")) || 0,
state: "cancelled",
deleted: {
intents: 1,
facts: 1,
findings: 1,
activities: D.activity.filter((item) => item.intent_id === id).length,
},
};
}
const action = b.action === "resume" ? "resume" : "pause";
const result = controlMockIntent(id, action);
if (!result.ok) throw new Error(result.error ?? "Worker 状态已变化");
return { id: Number(id.replace(/\D/g, "")) || 0, state: result.state };
}
if (seg[0] === "tasks" && seg.length === 3 && seg[2] === "control" && m === "POST") {
const action = b.action === "resume" ? "resume" : "pause";
const result = controlMockTask(seg[1], action);
if (!result.ok) throw new Error(result.error ?? "任务状态已变化");
const task = mockTasks.find((item) => item.id === seg[1]);
return { id: seg[1], paused: Boolean(task?.paused), queued: Boolean(task?.queued), status: task?.status ?? "" };
}
if (seg[0] === "tasks" && seg[2] === "chat" && seg[3] === "status") return { running: false };
if (seg[0] === "tasks" && seg[2] === "chat" && seg[3] === "stop") return { status: "stopped" };
if (path === "/active") {
const id = String(b.id ?? mockActiveTask);
if (mockTasks.some((item) => item.id === id)) {
mockActiveTask = id;
for (const item of mockTasks) item.active = item.id === id;
}
return { active: mockActiveTask };
}
// ── 覆盖度 / 覆盖图 / 资产关联(任务维度)──
if (seg[0] === "tasks" && seg[2] === "coverage" && seg.length === 3) return D.coverage;
if (seg[0] === "tasks" && seg[2] === "coverage-graph") return D.coverageGraph;
if (seg[0] === "tasks" && seg[2] === "asset-refs") return D.assetRefsFor(Number(q.get("asset_id") ?? 0));
// ── 任务测试范围(增删查)──
if (seg[0] === "tasks" && seg[2] === "scope" && seg.length === 3 && m === "GET") {
return { scope: mockTaskScopes.get(seg[1]) ?? [] };
}
if (seg[0] === "tasks" && seg[2] === "scope" && seg.length === 3 && m === "POST") {
const current = mockTaskScopes.get(seg[1]) ?? [];
const kind = String(b.kind ?? "") as TaskScopeRow["kind"];
const value = String(b.value ?? "").trim();
const row: TaskScopeRow = {
id: Date.now(),
task_id: mockTaskAssetID(seg[1]) ?? Number(seg[1]),
kind,
source: "manual",
};
if (kind === "root_domain" || kind === "subdomain") row.domain = value;
else if (kind === "ip" || kind === "cidr") row.net = value;
else row.value = value;
mockTaskScopes.set(seg[1], [...current, row]);
return row;
}
if (seg[0] === "tasks" && seg[2] === "scope" && seg.length === 4 && m === "DELETE") {
const id = Number(seg[3]);
mockTaskScopes.set(
seg[1],
(mockTaskScopes.get(seg[1]) ?? []).filter((row) => row.id !== id),
);
return { ok: true };
}
// ── 全局 llm_usage 聚合(仪表盘新版视图,demo)──
if (path === "/tokens/usage")
return {
by_profile: [
{
profile_name: "default",
calls: 60,
tasks: 4,
input_tokens: 1570000,
output_tokens: 110000,
cache_read_tokens: 1120000,
cache_write_tokens: 140000,
},
],
daily: [
{
profile_name: "default",
date: "2026-08-18",
input_tokens: 520000,
output_tokens: 38000,
cache_read_tokens: 370000,
},
{
profile_name: "default",
date: "2026-08-19",
input_tokens: 640000,
output_tokens: 45000,
cache_read_tokens: 460000,
},
{
profile_name: "default",
date: "2026-08-20",
input_tokens: 410000,
output_tokens: 27000,
cache_read_tokens: 290000,
},
],
};
// ── 按模型 token 用量(demo:一条示例)──
if (path === "/llm/records/by-model")
return {
models: [
{
model: "claude-opus-4-6",
calls: 42,
input_tokens: 1250000,
output_tokens: 86000,
cache_read_tokens: 940000,
cache_write_tokens: 120000,
},
{
model: "claude-haiku-4-5",
calls: 18,
input_tokens: 320000,
output_tokens: 24000,
cache_read_tokens: 180000,
cache_write_tokens: 20000,
},
],
};
// ── 工作空间文件管理器(demo:静态示例树;写/建/删走下方写兜底 {ok:true})──
if (path === "/workspace/list") return D.workspaceList(q.get("path") ?? "");
if (path === "/workspace/read") return D.workspaceRead(q.get("path") ?? "");
// ── stats ──
if (path === "/stats") {
return D.stats(task, { tasks: mockTasks, findings: mockFindings, activeTask: mockActiveTask });
}
// ── assets ──
if (path === "/assets/counts") return mockAssetCounts(q.get("task_id"));
if (path === "/assets" && m === "GET") {
const type = q.get("type") ?? "";
const taskID = q.get("task_id");
const numericTaskID = taskID ? mockTaskAssetID(taskID) : undefined;
const dsl = q.get("dsl") ?? "";
const list = mockAssets.filter((asset) => {
if (type && asset.type !== type) return false;
if (taskID && (numericTaskID === undefined || !asset.task_ids.includes(numericTaskID))) return false;
return !dsl || mockAssetMatchesDSL(asset, dsl);
});
const limit = Number(q.get("limit") ?? 50);
const offset = Number(q.get("offset") ?? 0);
const page = list.slice(offset, offset + limit);
return { count: page.length, total: list.length, assets: page };
}
if (path === "/assets" && m === "DELETE") {
const ids = new Set(Array.isArray(b.ids) ? b.ids.map(Number) : []);
let deleted = 0;
for (let index = mockAssets.length - 1; index >= 0; index--) {
if (!ids.has(mockAssets[index].id)) continue;
mockAssets.splice(index, 1);
deleted++;
}
return { deleted };
}
if (seg[0] === "tasks" && seg[2] === "assets" && seg.length === 3 && m === "POST") {
const task = mockTasks.find((item) => item.id === seg[1]);
const numericTaskID = mockTaskAssetID(seg[1]);
if (!task || numericTaskID === undefined) throw new Error("任务不存在");
if (Array.isArray(b.scope)) {
if (b.scope.length === 0 || b.scope.length > MAX_COMPANY_SCOPE_RULES) throw new Error("请填写有效测试范围");
const rules: CompanyScopeRule[] = b.scope.map((candidate, index) => {
if (typeof candidate === "string") {
const issue = classifyCompanyScopeLine(candidate, index + 1);
if (!issue.rule || issue.error) throw new Error(`第 ${index + 1} 条范围无效:${issue.error ?? "无法识别"}`);
return issue.rule;
}
const item = candidate as { kind?: unknown; value?: unknown };
const value = String(item?.value ?? "").trim();
const rule =
item?.kind && isCompanyScopeKind(item.kind)
? { kind: item.kind, value }
: classifyCompanyScopeLine(value, index + 1).rule;
const error = rule ? companyScopeRuleError(rule) : "无法识别";
if (!rule || error) throw new Error(`第 ${index + 1} 条范围无效:${error}`);
return rule;
});
const mutation: TaskAssetScopeMutation = {
requested: rules.length,
assets_linked: 0,
assets_existing: 0,
scopes_added: 0,
scopes_existing: 0,
};
const currentScopes = mockTaskScopes.get(seg[1]) ?? [];
const scopeKeys = new Set(
currentScopes.map((row) => `${row.kind}|${row.domain ?? row.net ?? row.value ?? row.company_id ?? ""}`),
);
for (const rule of rules) {
const normalized = normalizeCompanyScopeValue(rule);
const scope: TaskScopeRow = {
id: Date.now() + currentScopes.length,
task_id: numericTaskID,
kind: rule.kind === "domain" ? "root_domain" : rule.kind,
source: "manual",
reason: "用户在测试资产页手工新增",
};
if (rule.kind === "domain") scope.domain = normalized;
else if (rule.kind === "ip") scope.net = `${normalized}/${normalized.includes(":") ? 128 : 32}`;
else if (rule.kind === "cidr") scope.net = normalized;
else scope.value = normalized;
const scopeKey = `${scope.kind}|${scope.domain ?? scope.net ?? scope.value ?? ""}`;
if (scopeKeys.has(scopeKey)) mutation.scopes_existing++;
else {
scopeKeys.add(scopeKey);
currentScopes.push(scope);
mutation.scopes_added++;
}
if (rule.kind !== "domain" && rule.kind !== "ip") continue;
const type = rule.kind === "domain" ? "root_domain" : "ip";
let asset = mockAssets.find((item) =>
type === "root_domain"
? item.type === type && item.domain === normalized
: item.type === type && item.ip === normalized,
);
const alreadyLinked = asset?.task_ids.includes(numericTaskID) ?? false;
if (!asset) {
const nextID = mockAssets.reduce((max, item) => Math.max(max, item.id), 0) + 1;
asset = {
id: nextID,
type,
task_ids: [],
...(type === "root_domain" ? { domain: normalized, root_domain: normalized } : { ip: normalized }),
last_seen: new Date().toISOString(),
};
mockAssets.push(asset);
}
if (alreadyLinked) mutation.assets_existing++;
else {
asset.task_ids.push(numericTaskID);
mutation.assets_linked++;
}
asset.task_source = "manual";
asset.task_source_summary = "用户在测试资产页手工新增";
asset.task_source_node_id = undefined;
}
mockTaskScopes.set(seg[1], currentScopes);
return mutation;
}
const ids = [...new Set(Array.isArray(b.asset_ids) ? b.asset_ids.map(Number) : [])];
const sourceSummary = String(b.source_summary ?? "").trim();
if (ids.length === 0 || ids.length > 100 || !sourceSummary) throw new Error("请选择资产并填写来源说明");
const requestedAssets = ids.map((id) => mockAssets.find((asset) => asset.id === id));
if (requestedAssets.some((asset) => !asset)) throw new Error("资产不存在");
const mutation: TaskAssetMutation = { requested: ids.length, attached: 0, existing: 0 };
for (const asset of requestedAssets) {
if (!asset) continue;
if (asset.task_ids.includes(numericTaskID)) mutation.existing++;
else {
asset.task_ids.push(numericTaskID);
mutation.attached++;
}
asset.task_source = "manual";
asset.task_source_summary = sourceSummary;
asset.task_source_node_id = undefined;
}
return mutation;
}
if (seg[0] === "tasks" && seg[2] === "assets" && seg.length === 4 && m === "DELETE") {
const numericTaskID = mockTaskAssetID(seg[1]);
const asset = mockAssets.find((item) => item.id === Number(seg[3]));
if (numericTaskID === undefined || !asset) throw new Error("任务或资产不存在");
if (!asset.task_ids.includes(numericTaskID)) throw new Error("资产未关联当前任务");
asset.task_ids = asset.task_ids.filter((id) => id !== numericTaskID);
if (asset.task_ids.length === 0) {
asset.task_source = undefined;
asset.task_source_summary = undefined;
asset.task_source_node_id = undefined;
}
return { detached: asset.id };
}
if (seg[0] === "tasks" && seg[2] === "intent-assets" && seg.length === 3 && m === "GET") {
let mappings: Array<{ intentID: string; assetID: number; summary: string }> = [];
if (seg[1] === "t-acme-web") {
mappings = [
{ intentID: "i3", assetID: 6, summary: "后台功能枚举意图从前序子域发现中选定" },
{ intentID: "i5", assetID: 3, summary: "订单接口测试意图从 API 任务目标中选定" },
];
} else if (seg[1] === "t-acme-api") {
mappings = [{ intentID: "i5", assetID: 3, summary: "订单接口测试意图从 API 任务目标中选定" }];
}
const sourceTaskID = mockTaskAssetID(seg[1]) ?? 0;
const assets: IntentAsset[] = mappings.flatMap((mapping) => {
const asset = mockAssets.find((item) => item.id === mapping.assetID);
if (!asset) return [];
return [
{
intent_id: mapping.intentID,
asset_id: asset.id,
type: asset.type,
label: asset.domain ?? asset.ip ?? asset.app_name ?? asset.url ?? asset.service_name ?? `#${asset.id}`,
source: asset.task_source ?? "agent",
source_summary: asset.task_source_summary ?? mapping.summary,
source_node_id: asset.task_source_node_id,
source_task_id: sourceTaskID,
inherited: false,
},
];
});
return { assets };
}
// ── companies ──
if (path === "/companies" && m === "GET") return structuredClone(mockCompanies);
if (path === "/companies" && m === "POST") {
const name = String(b.name ?? "").trim();
if (!name) throw new Error("企业名称不能为空");
if (mockCompanies.some((company) => company.name.toLowerCase() === name.toLowerCase())) {
throw new Error("企业已存在");
}
const id = mockCompanies.reduce((max, company) => Math.max(max, company.id), 0) + 1;
const scopeResult = mockScopeRows(id, b.scope);
const company: Company = { id, name, asset_count: 0, scope: scopeResult.rows };
mockCompanies.push(company);
return {
id,
created: true,
scope_added: scopeResult.rows.length,
scope_skipped: scopeResult.skipped,
scope_invalid: scopeResult.invalid,
};
}
if (seg[0] === "companies" && seg[2] === "scope" && m === "POST") {
const company = mockCompanies.find((item) => item.id === Number(seg[1]));
if (!company) throw new Error("企业不存在");
const reset = b.reset === true;
const scopeResult = mockScopeRows(company.id, b.scope, reset ? [] : (company.scope ?? []));
if (reset && scopeResult.invalid > 0) throw new Error("企业范围包含无效规则,未覆盖原有范围");
company.scope = reset ? scopeResult.rows : [...(company.scope ?? []), ...scopeResult.rows];
return { added: scopeResult.rows.length, skipped: scopeResult.skipped, invalid: scopeResult.invalid };
}
if (seg[0] === "companies" && seg.length === 2 && m === "DELETE") {
const id = Number(seg[1]);
const index = mockCompanies.findIndex((item) => item.id === id);
if (index < 0) throw new Error("企业不存在");
mockCompanies.splice(index, 1);
let assetsDeleted = 0;
for (let assetIndex = mockAssets.length - 1; assetIndex >= 0; assetIndex--) {
const asset: Asset = mockAssets[assetIndex];
if (asset.company_id !== id) continue;
if (b.delete_assets === true) {
mockAssets.splice(assetIndex, 1);
assetsDeleted++;
} else {
delete asset.company_id;
}
}
return { deleted: 1, assets_deleted: assetsDeleted };
}
// ── exploration ──
if (path === "/exploration/frontier") return D.frontier;
if (path === "/exploration/findings/stats") {
const vulnclasses = Array.from(new Set(mockFindings.map((f) => f.vulnclass))).sort();
// 「按任务」下拉:有漏洞的任务 + 描述 + 条数(mock 任务 id 是字符串,直接当 id 用)。
const taskMap = new Map<string, { name: string; description: string; count: number }>();
for (const f of mockFindings) {
if (!f.task_id) continue;
const owner = mockTasks.find((candidate) => candidate.id === f.task_id);
const cur = taskMap.get(f.task_id) ?? {
name: owner?.name ?? "",
description: f.task_description ?? "",
count: 0,
};
cur.count++;
taskMap.set(f.task_id, cur);
}
const tasks = Array.from(taskMap, ([id, v]) => ({ id, name: v.name, description: v.description, count: v.count }));
return {
total: mockFindings.length,
pending: mockFindings.filter((f) => f.status === "pending").length,
critical: mockFindings.filter((f) => f.severity === "critical").length,
high: mockFindings.filter((f) => f.severity === "high").length,
medium: mockFindings.filter((f) => f.severity === "medium").length,
low: mockFindings.filter((f) => f.severity === "low").length,
vulnclasses,
tasks,
};
}
if (path === "/exploration/findings/groups") {
const severityOrder = { critical: 4, high: 3, medium: 2, low: 1 } as const;
let list = mockFindings.slice();
const filterSeverity = q.get("severity");
const filterStatus = q.get("status");
const filterVulnclass = q.get("vulnclass");
const filterTask = q.get("task_id");
if (filterSeverity) list = list.filter((finding) => finding.severity === filterSeverity);
if (filterStatus) list = list.filter((finding) => finding.status === filterStatus);
if (filterVulnclass) list = list.filter((finding) => finding.vulnclass === filterVulnclass);
if (filterTask === "__unassigned__") list = list.filter((finding) => !finding.task_id);
else if (filterTask) list = list.filter((finding) => finding.task_id === filterTask);
const grouped = new Map<string, typeof list>();
for (const finding of list) {
const key = finding.task_id ?? "__unassigned__";
grouped.set(key, [...(grouped.get(key) ?? []), finding]);
}
const groups = Array.from(grouped, ([key, items]) => {
const owner = mockTasks.find((candidate) => candidate.id === key);
return {
task_id: key === "__unassigned__" ? null : key,
task_name: owner?.name ?? "",
task_description: owner?.description ?? items[0]?.task_description ?? "",
task_status: owner?.status ?? "",
count: items.length,
critical: items.filter((finding) => finding.severity === "critical").length,
high: items.filter((finding) => finding.severity === "high").length,
medium: items.filter((finding) => finding.severity === "medium").length,
low: items.filter((finding) => finding.severity === "low").length,
last_found_at: items.reduce((latest, finding) => (finding.ts > latest ? finding.ts : latest), ""),
max_severity: Math.max(...items.map((finding) => severityOrder[finding.severity])),
};
});
groups.sort((left, right) =>
q.get("sort") === "severity"
? right.max_severity - left.max_severity || right.last_found_at.localeCompare(left.last_found_at)
: right.last_found_at.localeCompare(left.last_found_at),
);
const rawPage = Number(q.get("page") ?? 1);
const rawPageSize = Number(q.get("limit") ?? 10);
const page = Number.isFinite(rawPage) && rawPage > 0 ? Math.floor(rawPage) : 1;
const pageSize = Number.isFinite(rawPageSize) && rawPageSize > 0 ? Math.min(100, Math.floor(rawPageSize)) : 10;
return {
items: groups.slice((page - 1) * pageSize, page * pageSize),
total: groups.length,
finding_total: list.length,
page,
page_size: pageSize,
};
}
if (seg[0] === "exploration" && seg[1] === "findings" && seg[3] === "deepen" && m === "POST") {
const finding = mockFindings.find((candidate) => candidate.id === seg[2]);
const description = String(b.description ?? "").trim();
if (!description) throw new Error("description is required");
if ([...description].length > 4000) throw new Error("description must be at most 4000 characters");
if (!finding) throw new Error("finding not found");
if (!finding.task_id || !mockTasks.some((candidate) => candidate.id === finding.task_id)) {
throw new Error("finding origin task or node is no longer available");
}
return {
task_id: finding.task_id,
intent_id: `mock-deepen-${Date.now()}`,
state: "open",
queued: false,
};
}
// 单条 finding:GET 详情 / PATCH 改状态/严重度/名称/类别(demo 直接改内存对象)。
if (seg[0] === "exploration" && seg[1] === "findings" && seg.length === 3 && seg[2] !== "stats") {
const f = mockFindings.find((x) => x.id === seg[2]);
if (!f) return {};
if (m === "PATCH") {
if (typeof b.status === "string") f.status = b.status as typeof f.status;
if (typeof b.severity === "string") f.severity = b.severity as typeof f.severity;
if (typeof b.name === "string") f.name = b.name;
if (typeof b.vulnclass === "string") f.vulnclass = b.vulnclass;
}
const contextTaskId = q.get("context_task");
const contextTask = contextTaskId ? mockTasks.find((item) => item.id === contextTaskId) : undefined;
const inherited = !!(
contextTask &&
f.task_id &&
f.task_id !== contextTask.id &&
contextTask.source_task_ids?.includes(f.task_id)
);
return {
...f,
finding_id: f.id,
...(inherited ? { inherited: true, source_task_id: f.task_id } : {}),
};
}
if (path === "/exploration/findings") {
// finding_id=id:真后端用独立表行 id 作为状态/详情句柄,mock 里用自身 id 顶上。
// report 仅详情接口返回,列表剥掉(与后端一致)。
const withFid = (f: (typeof mockFindings)[number]) => ({
...f,
report: undefined,
finding_id: f.id,
});
if (task) {
const owner = mockTasks.find((item) => item.id === task);
const sources = new Set(owner?.source_task_ids ?? []);
return mockFindings
.filter((f) => f.task_id === task || (!!f.task_id && sources.has(f.task_id)))
.map((f) => ({
...withFid(f),
...(f.task_id !== task ? { inherited: true, source_task_id: f.task_id } : {}),
}));
}
// 全局:带 page/limit → 分页对象;否则裸数组(dashboard)。
if (!q.has("page") && !q.has("limit")) return mockFindings.map(withFid);
const sev = { critical: 4, high: 3, medium: 2, low: 1 } as const;
let list = mockFindings.slice();
const fSev = q.get("severity");
const fStatus = q.get("status");
const fVuln = q.get("vulnclass");
const fTask = q.get("task_id");
if (fSev) list = list.filter((f) => f.severity === fSev);
if (fStatus) list = list.filter((f) => f.status === fStatus);
if (fVuln) list = list.filter((f) => f.vulnclass === fVuln);
if (fTask === "__unassigned__") list = list.filter((f) => !f.task_id);
else if (fTask) list = list.filter((f) => f.task_id === fTask);
list.sort((a, b) =>
q.get("sort") === "severity"
? sev[b.severity] - sev[a.severity] || +new Date(b.ts) - +new Date(a.ts)
: +new Date(b.ts) - +new Date(a.ts),
);
const rawPage = Number(q.get("page") ?? 1);
const rawPageSize = Number(q.get("limit") ?? 20);
const page = Number.isFinite(rawPage) && rawPage > 0 ? Math.floor(rawPage) : 1;
const pageSize = Number.isFinite(rawPageSize) && rawPageSize > 0 ? Math.min(200, Math.floor(rawPageSize)) : 20;
return {
items: list.slice((page - 1) * pageSize, page * pageSize).map(withFid),
total: list.length,
page,
page_size: pageSize,
};
}
if (path === "/exploration/intents") {
if (q.has("page")) {
const before = Number(q.get("before") ?? 0);
const limit = Math.max(1, Number(q.get("limit") ?? 300));
let list = mockIntents;
if (before > 0) list = list.filter((intent) => Number(intent.id.replace(/\D/g, "") || intent.id) < before);
return { items: list.slice(0, limit), has_more: list.length > limit };
}
return mockIntents;
}
if (path === "/exploration/tokens") {
const selectedTask = task ? mockTasks.find((item) => item.id === task) : undefined;
return {
workers: D.tokenWorkers,
sessions: D.tokenSessions,
total: selectedTask?.tokens ?? D.tokenTotal,
};
}
if (path === "/exploration/graph") return D.explorationGraph;
if (path === "/exploration/activity" && seg.length === 2) {
const since = Number(q.get("since") ?? 0);
const limit = Math.max(1, Number(q.get("limit") ?? 300));
const items = D.activityForTask()
.filter((item) => item.seq > since)
.slice(0, limit);
return { items, cursor: items.length ? items[items.length - 1].seq : since };
}
if (seg[0] === "exploration" && seg[1] === "activity" && seg.length === 3) {
const a = D.activity.find((x) => x.seq === Number(seg[2]));
return { detail: a?.detail ?? a?.summary ?? "" };
}
if (path === "/tokens/daily") return D.dailyTokens;
if (path === "/tokens/conversations") return D.convTokens;
// ── traffic / audit / settings ──
if (path === "/audit") return D.audit;
if (path === "/traffic" && m === "DELETE") return { deleted: 0 };
if (path === "/traffic/hosts" && m === "DELETE") return { deleted: (b.hosts as unknown[])?.length ?? 0 };
if (path === "/traffic/hosts") return { hosts: D.trafficHosts };
if (path === "/traffic") return D.traffic;
if (path === "/traffic/exchange") return D.trafficDetail;
if (path === "/settings" && m === "GET") return D.settings;
if (path === "/settings" && m === "PUT") return { ...D.settings, ...b };
if (path === "/settings/web-search/test") return { ok: true, count: 5, backend: D.settings.web_search_backend };
if (path === "/settings/python/detect") return { python_interpreter: "/usr/bin/python3" };
if (path === "/chat")
return { reply: "(demo)我已把该建议注入为一条高优意图,work agent 会尽快执行。", mode: "hint" };
if (path === "/gc") return { removed: 0 };
// ── 工具执行历史 ──
if (path === "/commands" && m === "GET") return { commands: D.commandRecords, total: D.commandRecords.length };
// ── LLM ──
if (path === "/llm/records" && m === "GET") return { records: mockLLMRecords, total: mockLLMRecords.length };
if (path === "/llm/records" && m === "DELETE") return { deleted: 0 };
if (path === "/llm/records/tasks") {
const counts = new Map<string, number>();
for (const record of mockLLMRecords) {
if (record.task_id) counts.set(record.task_id, (counts.get(record.task_id) ?? 0) + 1);
}
return { tasks: [...counts].map(([task_id, count]) => ({ task_id, count })) };
}
if (seg[0] === "llm" && seg[1] === "records" && seg.length === 3 && m === "GET") {
return D.llmRecordDetail(Number(seg[2]), mockLLMRecords);
}
if (path === "/llm" && m === "GET") return D.llmConfig;
if (path === "/llm" && m === "POST") return { ok: true };
if (path === "/llm/test") return { ok: true, latency_ms: 128, model: String(b.model ?? "claude-opus-4-8") };
if (path === "/llm/profiles" && m === "GET") return { profiles: D.llmProfiles };
if (path === "/llm/profiles" && m === "POST") return { id: Number(b.id) || 3 };
if (path === "/llm/profiles/active") return { ok: true };
if (path === "/llm/pool" && m === "GET") return D.llmPool;
if (path === "/llm/pool/reset")
return { ...D.llmPool, chain: D.llmPool.chain.map((c) => ({ ...c, state: "ok", fails: 0, cooldown_secs: 0 })) };
if (seg[0] === "llm" && seg[1] === "profiles" && seg.length === 3 && m === "DELETE")
return { deleted: Number(seg[2]) };
// ── agents ──
if (path === "/agents" && m === "GET") return { agents: D.agents };
if (path === "/agents" && m === "POST")
return {
id: "9",
key: String(b.key ?? "custom"),
name: String(b.name ?? ""),
role: "custom",
builtin: false,
enabled: true,
};
if (seg[0] === "agents" && seg.length === 2 && m === "GET") return D.agentDetail(seg[1]);
if (seg[0] === "agents" && seg[2] === "triggers" && m === "GET") return { triggers: [] };
if (seg[0] === "agents" && seg[2] === "prompts") return { versions: D.agentDetail(seg[1]).versions };
if (seg[0] === "agents" && seg[2] === "variables") return { variables: D.agentDetail(seg[1]).variables };
if (seg[0] === "agents" && seg[2] === "prompt" && seg[3] === "preview")
return { rendered: String(b.template ?? "").replace(/\{\{\.(\w+)\}\}/g, "«$1»") };
if (seg[0] === "agents" && seg[2] === "visibility" && m === "GET") return D.agentDetail(seg[1]).visibility;
// ── conversations ──
if (path === "/conversations" && m === "GET") {
sortMockConversations();
return { conversations: structuredClone(mockConversations) };
}
if (path === "/conversations" && m === "POST") {
const now = new Date().toISOString();
const title = String(b.title ?? "").trim() || "新对话";
const conversation: Conversation = {
id: mockConversations.reduce((max, item) => Math.max(max, item.id), 0) + 1,
agent_key: String(b.agent_key ?? "mainagent"),
title,
llm_profile_id: typeof b.llm_profile_id === "number" ? b.llm_profile_id : undefined,
pinned: false,
created_at: now,
updated_at: now,
};
mockConversations.unshift(conversation);
return structuredClone(conversation);
}
if (seg[0] === "conversations" && seg.length === 2 && m === "PATCH") {
const conversation = mockConversations.find((item) => item.id === Number(seg[1]));
if (!conversation) return {};
if (typeof b.title === "string") conversation.title = b.title.trim();
if (typeof b.pinned === "boolean") {
conversation.pinned = b.pinned;
conversation.pinned_at = b.pinned ? (conversation.pinned_at ?? new Date().toISOString()) : null;
}
conversation.updated_at = new Date().toISOString();
sortMockConversations();
return structuredClone(conversation);
}
if (seg[0] === "conversations" && seg.length === 2 && m === "DELETE") {
const id = Number(seg[1]);
const index = mockConversations.findIndex((item) => item.id === id);
if (index >= 0) mockConversations.splice(index, 1);
return { deleted: id };
}
if (path === "/conversations/delete/batch" && m === "POST") {
const ids = Array.isArray(b.ids)
? [...new Set(b.ids.map(Number).filter((id) => Number.isInteger(id) && id > 0))]
: [];
const items = ids.map((id) => {
const index = mockConversations.findIndex((item) => item.id === id);
if (index < 0) return { id, ok: false, error: "conversation not found" };
mockConversations.splice(index, 1);
return { id, ok: true };
});
return { items };
}
if (seg[0] === "conversations" && seg[2] === "messages" && seg.length === 3 && m === "GET") {
const items = D.conversationMessages[Number(seg[1])] ?? [];
return { items, cursor: items.length ? items[items.length - 1].seq : 0, running: false };
}
if (seg[0] === "conversations" && seg[2] === "messages" && seg.length === 4) {
const msgs = D.conversationMessages[Number(seg[1])] ?? [];
const a = msgs.find((x) => x.seq === Number(seg[3]));
return { detail: a?.detail ?? a?.summary ?? "" };
}
if (seg[0] === "conversations" && seg[2] === "messages" && m === "POST") return { status: "ok" };
if (seg[0] === "conversations" && seg[2] === "stop") return { status: "stopped" };
// ── tools ──
if (path === "/tools" && m === "GET") return { tools: D.tools };
if (path === "/tools/custom" && m === "POST") return { key: String(b.key ?? "custom-tool") };
if (path === "/tools/custom/test") return { output: "(demo)工具执行输出示例。", is_error: false };
// ── mcp ──
if (path === "/mcp" && m === "GET") return { servers: D.mcpServers };
if (path === "/mcp" && m === "POST") return { id: 3 };
if (seg[0] === "mcp" && seg[2] === "tools") return { tools: D.mcpToolsById[Number(seg[1])] ?? [] };
if (seg[0] === "mcp" && seg[2] === "refresh") return { tools: D.mcpToolsById[Number(seg[1])] ?? [] };
if (seg[0] === "mcp" && seg.length === 2 && m === "DELETE") return { deleted: Number(seg[1]) };
// ── scopesentry(demo:未配置)──
if (path === "/sync/scopesentry/status")
return { exists: false, configured: false, enabled: false, reachable: false, tools: [] };
if (path === "/sync/scopesentry/projects") return { projects: [], tag: {} };
if (path === "/sync/scopesentry/tasks") return { tasks: [] };
if (path === "/sync/scopesentry/sync") return { synced: {}, companies: null, warnings: null, errors: null };
// ── skills ──
if (path === "/skills" && m === "GET") return { skills: D.skills };
if (path === "/skills/missing") return { missing: D.missingSkills };
if (seg[0] === "skills" && seg[2] === "usage") return { calls: D.skillCalls };
if (path === "/skills" && m === "POST") return { name: String(b.name ?? "new-skill") };
if (seg[0] === "skills" && seg[2] === "files" && seg.length === 3) return { files: ["SKILL.md"] };
if (seg[0] === "skills" && seg[2] === "files" && seg.length >= 4)
return { content: "# SKILL.md\n\n(demo)这是该 skill 的说明文件示例。", file: seg.slice(3).join("/") };
// ── visibility ──
if (seg[0] === "visibility" && m === "GET") return { agents: [] };
// ── intercept ──
if (path === "/intercept/rules" && m === "GET") return { rules: D.interceptRules };
if (seg[0] === "intercept" && seg[1] === "rules" && seg[3] === "toggle")
return { ok: true, enabled: b.enabled ?? true };
if (path === "/intercept/pending" && m === "GET") return { pending: D.interceptPending };
if (seg[0] === "intercept" && seg[1] === "pending" && seg[3] === "decide") return { ok: true };
if (seg[0] === "intercept" && seg[1] === "pending" && seg.length === 3 && m === "GET")
return D.interceptPending.find((p) => p.id === Number(seg[2])) ?? null;
if (path === "/intercept/history") return { items: D.interceptHistory };
if (seg[0] === "intercept" && seg[1] === "task")
return { items: D.interceptHistory.filter((r) => r.task_id === seg[2]) };
if (path === "/intercept/tool-config") return { enabled_tools: ["bash"] };
if (path === "/intercept/judge" && m === "GET")
return {
enabled: false,
profile_id: 0,
prompt: "",
timeout_seconds: 15,
fail_action: "allow",
ask_timeout_seconds: 300,
ask_timeout_action: "deny",
};
if (path === "/intercept/judge" && m === "PUT") return { ok: true };
// ── 写操作兜底:成功但不落库 ──
if (["POST", "PUT", "PATCH", "DELETE"].includes(m)) return { ok: true };
// ── 读兜底:集合类给 [],其余 {} ──
return /(\/(tasks|profiles|conversations|rules|history|projects|tokens|agents|servers|skills|tools|findings|intents)s?$)|s$/.test(
path,
)
? []
: {};
}