mirror of
https://github.com/BaaRai/ARTEX.git
synced 2026-11-15 04:30:55 +08:00
handler 对 /exploration/intents 只返回裸数组,intentsPage 带
?page= 参数时期望 {items, has_more} 形状,r.items 取到 undefined
导致 mock 模式下 worker 会话列表始终为空。
279 lines
15 KiB
TypeScript
279 lines
15 KiB
TypeScript
// Mock 路由:把 (method, path) 映射到 lib/mock/data 的静态数据。
|
||
// 未命中的一律返回安全默认([] / {} / {ok:true}),保证任何页面都不崩。
|
||
// 只在 NEXT_PUBLIC_MOCK=1 时经由 api.ts 的 http() 短路进入这里。
|
||
|
||
import * as D from "./data";
|
||
|
||
const delay = (ms = 120) => new Promise((r) => setTimeout(r, ms));
|
||
|
||
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") return { tasks: D.tasks, active: D.ACTIVE_TASK };
|
||
if (path === "/tasks" && m === "POST")
|
||
return { ...D.tasks[0], id: "t-new", description: String(b.description ?? "新任务"), goal: String(b.goal ?? ""), status: "created" };
|
||
if (seg[0] === "tasks" && seg.length === 2 && m === "DELETE") return { deleted: 1 };
|
||
if (seg[0] === "tasks" && seg[2] === "control") return { id: seg[1], paused: b.action === "pause" };
|
||
if (seg[0] === "tasks" && seg[2] === "chat" && seg[3] === "stop") return { status: "stopped" };
|
||
if (path === "/active") return { active: String(b.id ?? D.ACTIVE_TASK) };
|
||
|
||
// ── 覆盖度 / 覆盖图 / 资产关联(任务维度)──
|
||
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));
|
||
|
||
// ── 工作空间文件管理器(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);
|
||
|
||
// ── assets ──
|
||
if (path === "/assets/counts") return D.assetCounts;
|
||
if (path === "/assets" && m === "GET") {
|
||
const type = q.get("type") ?? "";
|
||
const list = type ? D.assets.filter((a) => a.type === type) : D.assets;
|
||
const limit = Number(q.get("limit") ?? 50);
|
||
const offset = Number(q.get("offset") ?? 0);
|
||
return { count: list.length, total: list.length, assets: list.slice(offset, offset + limit) };
|
||
}
|
||
if (path === "/assets" && m === "DELETE") return { deleted: (b.ids as unknown[])?.length ?? 0 };
|
||
|
||
// ── companies ──
|
||
if (path === "/companies" && m === "GET") return D.companies;
|
||
if (path === "/companies" && m === "POST") return { id: 2, created: true, scope_added: 0 };
|
||
if (seg[0] === "companies" && seg[2] === "scope") return { added: 0, skipped: 0, invalid: 0 };
|
||
if (seg[0] === "companies" && seg.length === 2 && m === "DELETE") return { deleted: 1, assets_deleted: 0 };
|
||
|
||
// ── exploration ──
|
||
if (path === "/exploration/frontier") return D.frontier;
|
||
if (path === "/exploration/findings/stats") {
|
||
const vulnclasses = Array.from(new Set(D.findings.map((f) => f.vulnclass))).sort();
|
||
// 「按任务」下拉:有漏洞的任务 + 描述 + 条数(mock 任务 id 是字符串,直接当 id 用)。
|
||
const taskMap = new Map<string, { description: string; count: number }>();
|
||
for (const f of D.findings) {
|
||
if (!f.task_id) continue;
|
||
const cur = taskMap.get(f.task_id) ?? { description: f.task_description ?? "", count: 0 };
|
||
cur.count++;
|
||
taskMap.set(f.task_id, cur);
|
||
}
|
||
const tasks = Array.from(taskMap, ([id, v]) => ({ id, description: v.description, count: v.count }));
|
||
return {
|
||
total: D.findings.length,
|
||
pending: D.findings.filter((f) => f.status === "pending").length,
|
||
critical: D.findings.filter((f) => f.severity === "critical").length,
|
||
high: D.findings.filter((f) => f.severity === "high").length,
|
||
medium: D.findings.filter((f) => f.severity === "medium").length,
|
||
low: D.findings.filter((f) => f.severity === "low").length,
|
||
vulnclasses,
|
||
tasks,
|
||
};
|
||
}
|
||
// 单条 finding:GET 详情 / PATCH 改状态/严重度/名称/类别(demo 直接改内存对象)。
|
||
if (
|
||
seg[0] === "exploration" &&
|
||
seg[1] === "findings" &&
|
||
seg.length === 3 &&
|
||
seg[2] !== "stats"
|
||
) {
|
||
const f = D.findings.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;
|
||
}
|
||
return { ...f, finding_id: f.id };
|
||
}
|
||
if (path === "/exploration/findings") {
|
||
// finding_id=id:真后端用独立表行 id 作为状态/详情句柄,mock 里用自身 id 顶上。
|
||
// report 仅详情接口返回,列表剥掉(与后端一致)。
|
||
const withFid = (f: (typeof D.findings)[number]) => ({
|
||
...f,
|
||
report: undefined,
|
||
finding_id: f.id,
|
||
});
|
||
if (task) return D.findings.filter((f) => f.task_id === task).map(withFid);
|
||
// 全局:带 page/limit → 分页对象;否则裸数组(dashboard)。
|
||
if (!q.has("page") && !q.has("limit")) return D.findings.map(withFid);
|
||
const sev = { critical: 4, high: 3, medium: 2, low: 1 } as const;
|
||
let list = D.findings.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) 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 page = Number(q.get("page") ?? 1);
|
||
const pageSize = Number(q.get("limit") ?? 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 = Number(q.get("limit") ?? 300);
|
||
let list = D.intents;
|
||
if (before > 0) list = list.filter((n) => Number(n.id.replace(/\D/g, "") || n.id) < before);
|
||
return { items: list.slice(0, limit), has_more: list.length > limit };
|
||
}
|
||
return D.intents;
|
||
}
|
||
if (path === "/exploration/tokens") return { workers: D.tokenWorkers, total: D.tokenTotal };
|
||
if (path === "/exploration/graph") return D.explorationGraph;
|
||
if (path === "/exploration/activity" && seg.length === 2) {
|
||
const items = D.activityForTask();
|
||
return { items, cursor: items.length ? items[items.length - 1].seq : 0 };
|
||
}
|
||
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: D.llmRecords, total: D.llmRecords.length };
|
||
if (path === "/llm/records" && m === "DELETE") return { deleted: 0 };
|
||
if (path === "/llm/records/tasks") return { tasks: D.llmTasks };
|
||
if (seg[0] === "llm" && seg[1] === "records" && seg.length === 3 && m === "GET") return D.llmRecordDetail(Number(seg[2]));
|
||
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") return { conversations: D.conversations };
|
||
if (path === "/conversations" && m === "POST") return { id: 3, agent_key: String(b.agent_key ?? "mainagent"), title: String(b.title ?? "新会话"), created_at: "2026-07-26T04:00:00Z", updated_at: "2026-07-26T04:00:00Z" };
|
||
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" && 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 (["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) ? [] : {};
|
||
}
|