同步
This commit is contained in:
1 parent
e67ff26e90
commit
58f23a2773
61 files changed
+11611
-443
No files matched your search
@@ -6,6 +6,7 @@
|
||||
import { MOCK } from "@/lib/mock/enabled";
|
||||
import { mockHandle } from "@/lib/mock/handler";
|
||||
import type {
|
||||
<<<<<<< Updated upstream
|
||||
Task, Stats, Asset, AssetNode, Edge, Company, TaskNode, Finding, Severity, FindingStatus,
|
||||
FindingsPage, FindingStats, FindingQuery, Activity,
|
||||
Audit, TrafficResp, TrafficDetail, TrafficHost, Settings, LLMProfile, Agent, AgentDetail, PromptVar,
|
||||
@@ -15,6 +16,73 @@ import type {
|
||||
TaskAssetView, SSProject, SSTask, ConvTokenSummary, CoverageGraphData, CoverageAssetRefs,
|
||||
WorkspaceListing, WorkspaceFile,
|
||||
CommandRecord, LLMRecordItem, LLMRecordDetail, LLMTask,
|
||||
=======
|
||||
Activity,
|
||||
Agent,
|
||||
AgentDetail,
|
||||
AgentTrigger,
|
||||
Asset,
|
||||
Audit,
|
||||
BatchControlItem,
|
||||
ChatAttachment,
|
||||
CommandRecord,
|
||||
Company,
|
||||
CompanyScopeRule,
|
||||
Conversation,
|
||||
ConvTokenSummary,
|
||||
CoverageAssetRefs,
|
||||
CoverageGraphData,
|
||||
DailyTokenBucket,
|
||||
DeleteTaskOptions,
|
||||
DeleteTaskResult,
|
||||
Edge,
|
||||
Finding,
|
||||
FindingDeepenResponse,
|
||||
FindingGroupsPage,
|
||||
FindingQuery,
|
||||
FindingStats,
|
||||
FindingStatus,
|
||||
FindingsPage,
|
||||
InterceptApprovalRow,
|
||||
InterceptPending,
|
||||
InterceptRule,
|
||||
JudgeConfig,
|
||||
LLMPoolStatus,
|
||||
LLMProfile,
|
||||
LLMRecordDetail,
|
||||
LLMRecordItem,
|
||||
LLMTask,
|
||||
MCPServer,
|
||||
MCPTool,
|
||||
MissingSkill,
|
||||
ModelTokenStat,
|
||||
PromptVar,
|
||||
PromptVersion,
|
||||
SessionTokenUsage,
|
||||
Settings,
|
||||
Severity,
|
||||
SkillCall,
|
||||
SkillItem,
|
||||
SSProject,
|
||||
SSTask,
|
||||
Stats,
|
||||
Task,
|
||||
TaskConstraint,
|
||||
TaskGoal,
|
||||
TaskLLMResolutions,
|
||||
TaskNode,
|
||||
TaskScopeRow,
|
||||
TaskTemplate,
|
||||
TokenTotal,
|
||||
TokenUsage,
|
||||
Tool,
|
||||
TrafficDetail,
|
||||
TrafficHost,
|
||||
TrafficResp,
|
||||
UsageStats,
|
||||
WorkspaceFile,
|
||||
WorkspaceListing,
|
||||
>>>>>>> Stashed changes
|
||||
} from "@/lib/types";
|
||||
|
||||
function getToken(): string | null {
|
||||
@@ -112,6 +180,7 @@ export const api = {
|
||||
post<{ ok: boolean }>("/auth/change-password", { old_password: oldPassword, new_password: newPassword }),
|
||||
|
||||
// ---- tasks ----
|
||||
<<<<<<< Updated upstream
|
||||
tasks: () => get<{ tasks: Task[]; active: string }>("/tasks").then((r) => ({ tasks: arr(r.tasks), active: r.active ?? "" })),
|
||||
createTask: (description: string, goal: string, llmProfileId?: number, timeoutSeconds?: number, seedFirstIntent?: boolean, planHeartbeatSeconds?: number) =>
|
||||
post<Task>("/tasks", {
|
||||
@@ -124,6 +193,64 @@ export const api = {
|
||||
}),
|
||||
deleteTask: (id: string) => del<{ deleted: number }>(`/tasks/${id}`),
|
||||
controlTask: (id: string, action: "pause" | "resume") => post<{ id: string; paused: boolean }>(`/tasks/${id}/control`, { action }),
|
||||
=======
|
||||
tasks: () =>
|
||||
get<{ tasks: Task[]; active: string }>("/tasks").then((r) => ({ tasks: arr(r.tasks), active: r.active ?? "" })),
|
||||
createTask: (input: {
|
||||
name?: string;
|
||||
description: string;
|
||||
goal: string;
|
||||
llmProfileIds?: number[];
|
||||
sourceTaskIds?: string[];
|
||||
companyIds?: number[];
|
||||
timeoutSeconds?: number;
|
||||
seedFirstIntent?: boolean;
|
||||
planHeartbeatSeconds?: number;
|
||||
coverageEnabled?: boolean;
|
||||
}) =>
|
||||
post<Task>("/tasks", {
|
||||
name: input.name ?? "",
|
||||
description: input.description,
|
||||
goal: input.goal,
|
||||
llm_profile_ids: input.llmProfileIds ?? [],
|
||||
source_task_ids: input.sourceTaskIds ?? [],
|
||||
company_ids: input.companyIds ?? [],
|
||||
timeout_seconds: input.timeoutSeconds ?? 0,
|
||||
seed_first_intent: input.seedFirstIntent ?? false,
|
||||
plan_heartbeat_seconds: input.planHeartbeatSeconds ?? 0, // 0 = 后端归一到默认 600(10min)
|
||||
coverage_enabled: input.coverageEnabled ?? true, // 默认开;false=关闭资产覆盖度功能
|
||||
}),
|
||||
taskTemplates: () => get<{ templates: TaskTemplate[] }>("/task-templates").then((r) => arr(r.templates)),
|
||||
createTaskTemplate: (input: Pick<TaskTemplate, "name" | "description" | "goal">) =>
|
||||
post<TaskTemplate>("/task-templates", input),
|
||||
updateTaskTemplate: (id: number, input: Partial<Pick<TaskTemplate, "name" | "description" | "goal">>) =>
|
||||
patch<TaskTemplate>(`/task-templates/${id}`, input),
|
||||
deleteTaskTemplate: (id: number) => del<{ deleted: number }>(`/task-templates/${id}`),
|
||||
updateTaskLLMProfiles: (id: string, llmProfileIds: number[], activeLLMProfileId?: number) =>
|
||||
put<{
|
||||
id: string;
|
||||
llm_profile_ids: number[];
|
||||
active_llm_profile_id?: number;
|
||||
llm_failover_state: string;
|
||||
reopened_intents: number;
|
||||
switch_event?: Activity;
|
||||
}>(`/tasks/${id}/llm`, {
|
||||
llm_profile_ids: llmProfileIds,
|
||||
active_llm_profile_id: activeLLMProfileId ?? null,
|
||||
}),
|
||||
deleteTask: (id: string, options: DeleteTaskOptions) => del<DeleteTaskResult>(`/tasks/${id}`, options),
|
||||
controlTask: (id: string, action: "pause" | "resume") =>
|
||||
post<{ id: string; paused: boolean; queued: boolean; status: string }>(`/tasks/${id}/control`, { action }),
|
||||
controlTasksBatch: (taskIds: string[], action: "pause" | "resume") =>
|
||||
post<{ items: BatchControlItem[] }>("/tasks/control/batch", { task_ids: taskIds, action }),
|
||||
controlIntent: (taskId: string, intentId: string, action: "pause" | "resume" | "cancel", reason?: string) =>
|
||||
post<{
|
||||
id: number;
|
||||
state: "paused" | "open" | "stopped";
|
||||
deleted?: { intents: number; facts: number; findings: number; activities: number };
|
||||
}>(`/tasks/${taskId}/intents/${intentId}/control`, { action, reason: reason ?? "" }),
|
||||
taskLLMResolution: (id: string) => get<TaskLLMResolutions>(`/tasks/${id}/llm/resolution`),
|
||||
>>>>>>> Stashed changes
|
||||
// 重跑一条没跑成功的意图(blocked/exhausted/stopped):置回 open,worker 会重新认领、从头再跑。
|
||||
rerunIntent: (taskId: string, intentId: string) => post<{ id: string; reopened: number }>(`/tasks/${taskId}/intents/${intentId}/rerun`),
|
||||
// 批量重跑本任务全部 blocked 意图(一次网络/LLM 断连导致多条 blocked 时一键全部重试)。
|
||||
@@ -134,6 +261,7 @@ export const api = {
|
||||
// 资产测试覆盖度(粗估,供参考):范围内资产被 fact 碰过的占比 + 按类型的 总数/已测。
|
||||
taskCoverage: (id: string) =>
|
||||
get<{
|
||||
enabled: boolean; // 资产覆盖度功能是否开启;false 时其余字段为零值
|
||||
scope_rows: number;
|
||||
denominator: number;
|
||||
tested: number;
|
||||
@@ -141,8 +269,46 @@ export const api = {
|
||||
by_type: { type: string; total: number; tested: number }[];
|
||||
}>(`/tasks/${id}/coverage`),
|
||||
// 资产覆盖图:范围内全部资产 + 连接用的根域名/公司节点,含 tested/in_scope。
|
||||
<<<<<<< Updated upstream
|
||||
taskCoverageGraph: (id: string) =>
|
||||
get<CoverageGraphData>(`/tasks/${id}/coverage-graph`),
|
||||
=======
|
||||
taskCoverageGraph: (id: string) => get<CoverageGraphData>(`/tasks/${id}/coverage-graph`),
|
||||
// 全局 llm_usage 聚合(仪表盘新版 token 视图):按 profile 总量 + 按天分桶。
|
||||
usageStats: (days = 365) => get<UsageStats>(`/tokens/usage?days=${days}`),
|
||||
// ---- 目标管理(总览)----
|
||||
// 本任务全部目标(text/vulnclass/state)。
|
||||
taskGoals: (id: string) => get<{ goals: TaskGoal[] }>(`/tasks/${id}/goals`),
|
||||
// 人工新增目标:写入图谱并通知 planner、复活任务。
|
||||
addGoal: (id: string, text: string, vulnclass?: string) =>
|
||||
post<TaskGoal>(`/tasks/${id}/goals`, { text, vulnclass: vulnclass ?? "" }),
|
||||
// 人工修改目标文本(及 vulnclass):通知 planner「由 old 变为 new」、复活任务。
|
||||
updateGoal: (id: string, goalId: string, text: string, vulnclass?: string) =>
|
||||
patch<TaskGoal>(`/tasks/${id}/goals/${goalId}`, { text, vulnclass: vulnclass ?? "" }),
|
||||
// 人工删除目标(硬删除):通知 planner,删除不复活任务。
|
||||
deleteGoal: (id: string, goalId: string) => del<{ ok: boolean }>(`/tasks/${id}/goals/${goalId}`),
|
||||
|
||||
// ---- 操作约束管理(总览)----
|
||||
// 本任务全部操作约束(allow/deny)。
|
||||
taskConstraints: (id: string) => get<{ constraints: TaskConstraint[] }>(`/tasks/${id}/constraints`),
|
||||
// 新增约束(不通知 planner,下一轮规划自然读到)。
|
||||
addConstraint: (id: string, text: string, kind: TaskConstraint["kind"]) =>
|
||||
post<TaskConstraint>(`/tasks/${id}/constraints`, { text, kind }),
|
||||
// 修改约束(文本 + allow/deny)。
|
||||
updateConstraint: (id: string, constraintId: string, text: string, kind: TaskConstraint["kind"]) =>
|
||||
patch<TaskConstraint>(`/tasks/${id}/constraints/${constraintId}`, { text, kind }),
|
||||
// 删除约束。
|
||||
deleteConstraint: (id: string, constraintId: string) =>
|
||||
del<{ ok: boolean }>(`/tasks/${id}/constraints/${constraintId}`),
|
||||
|
||||
// 本任务测试范围列表(含继承自来源任务的范围)。
|
||||
taskScope: (id: string) => get<{ scope: TaskScopeRow[] }>(`/tasks/${id}/scope`),
|
||||
// 手动新增一条测试范围(kind=company/root_domain/subdomain/ip/cidr)。
|
||||
addTaskScope: (id: string, kind: string, value: string, reason?: string) =>
|
||||
post<TaskScopeRow>(`/tasks/${id}/scope`, { kind, value, reason }),
|
||||
// 删除本任务的一条测试范围。
|
||||
deleteTaskScope: (id: string, scopeId: number) => del<{ ok: boolean }>(`/tasks/${id}/scope/${scopeId}`),
|
||||
>>>>>>> Stashed changes
|
||||
// 某资产在本任务里关联的意图 / 事实 / 发现(覆盖图节点抽屉用)。
|
||||
taskAssetRefs: (id: string, assetId: number) =>
|
||||
get<CoverageAssetRefs>(`/tasks/${id}/asset-refs?asset_id=${assetId}`),
|
||||
@@ -621,6 +787,10 @@ export const api = {
|
||||
if (!r.ok) throw new Error(await r.text());
|
||||
},
|
||||
|
||||
// ---- intercept LLM judge (模型兜底审批,全局配置) ----
|
||||
interceptGetJudgeConfig: () => get<JudgeConfig>("/intercept/judge"),
|
||||
interceptSetJudgeConfig: (cfg: JudgeConfig) => put<{ ok: boolean }>("/intercept/judge", cfg),
|
||||
|
||||
// ---- commands (tool execution history, any tool) ----
|
||||
commands: (params?: { task?: string; q?: string; page?: number; size?: number }) => {
|
||||
const sp = new URLSearchParams();
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
import { getLocalStorageValue, setLocalStorageValue } from "@/lib/local-storage.client";
|
||||
|
||||
// 会话输入框的发送/换行键位。纯前端偏好:只落 localStorage,不入库、不随账号同步,
|
||||
// 因此换浏览器需要重设。见 issue #39——0.3.2 把 Ctrl+Enter 发送改成了 Enter 发送,
|
||||
// 这里把旧键位还回来作为可选项。
|
||||
export type ChatSendMode = "enter" | "ctrl-enter";
|
||||
|
||||
export const CHAT_SEND_MODE_KEY = "artex_chat_send_mode";
|
||||
export const DEFAULT_CHAT_SEND_MODE: ChatSendMode = "enter";
|
||||
|
||||
export const CHAT_SEND_MODE_OPTIONS: { value: ChatSendMode; label: string }[] = [
|
||||
{ value: "enter", label: "Enter 发送,Shift+Enter 换行" },
|
||||
{ value: "ctrl-enter", label: "Ctrl+Enter 发送,Enter 换行" },
|
||||
];
|
||||
|
||||
function parseMode(raw: string | null): ChatSendMode {
|
||||
return raw === "ctrl-enter" || raw === "enter" ? raw : DEFAULT_CHAT_SEND_MODE;
|
||||
}
|
||||
|
||||
// 同一标签页内的订阅者集合。localStorage 的 storage 事件只在「其他」标签页触发,
|
||||
// 本页在设置里改完后要靠 emit 通知同页的输入框,否则得刷新才生效。
|
||||
const listeners = new Set<() => void>();
|
||||
|
||||
function subscribe(listener: () => void) {
|
||||
listeners.add(listener);
|
||||
window.addEventListener("storage", listener);
|
||||
return () => {
|
||||
listeners.delete(listener);
|
||||
window.removeEventListener("storage", listener);
|
||||
};
|
||||
}
|
||||
|
||||
// 返回的是字符串字面量,Object.is 按值比较,不会让 useSyncExternalStore 陷入循环。
|
||||
function getSnapshot(): ChatSendMode {
|
||||
return parseMode(getLocalStorageValue(CHAT_SEND_MODE_KEY));
|
||||
}
|
||||
|
||||
// 服务端没有 localStorage,先渲染默认值,hydrate 后 getSnapshot 再纠正。
|
||||
function getServerSnapshot(): ChatSendMode {
|
||||
return DEFAULT_CHAT_SEND_MODE;
|
||||
}
|
||||
|
||||
export function useChatSendMode(): ChatSendMode {
|
||||
return React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
||||
}
|
||||
|
||||
export function setChatSendMode(mode: ChatSendMode) {
|
||||
setLocalStorageValue(CHAT_SEND_MODE_KEY, mode);
|
||||
for (const listener of listeners) listener();
|
||||
}
|
||||
|
||||
// shouldSubmitOnKey 判断一次按键是否应当发送。
|
||||
// isComposing / keyCode 229 是中文等输入法正在选字,必须放行,否则回车选词会误发送。
|
||||
// enter 模式只排除 Shift,与 0.3.2 的行为逐字保持一致——不改设置的用户手感不变。
|
||||
// ctrl-enter 模式同时接受 Ctrl 与 Cmd(macOS)。
|
||||
export function shouldSubmitOnKey(e: React.KeyboardEvent, mode: ChatSendMode): boolean {
|
||||
if (e.key !== "Enter") return false;
|
||||
if (e.nativeEvent.isComposing || e.nativeEvent.keyCode === 229) return false;
|
||||
if (mode === "ctrl-enter") return e.ctrlKey || e.metaKey;
|
||||
return !e.shiftKey;
|
||||
}
|
||||
@@ -698,14 +698,14 @@ export const interceptRules: InterceptRule[] = [
|
||||
];
|
||||
|
||||
export const interceptPending: InterceptPending[] = [
|
||||
{ id: 101, rule_id: 1, task_id: "t-acme-web", agent_name: "work#3", tool_name: "bash", tool_input: { command: "mysqldump -h 203.0.113.10 -uroot acme_prod > /tmp/dump.sql" }, status: "pending", created_at: T("2026-07-26T03:58:00Z") },
|
||||
{ id: 101, rule_id: 1, task_id: "t-acme-web", agent_name: "work#3", tool_name: "bash", tool_input: { command: "mysqldump -h 203.0.113.10 -uroot acme_prod > /tmp/dump.sql" }, status: "pending", reason: "数据外泄门控:整库导出需人工审批", created_at: T("2026-07-26T03:58:00Z") },
|
||||
];
|
||||
|
||||
export const interceptHistory: InterceptApprovalRow[] = [
|
||||
{ id: 90, rule_id: 1, task_id: "t-acme-web", agent_name: "work#1", tool_name: "bash", tool_input: { command: "dd if=/dev/zero of=/tmp/x" }, status: "denied", created_at: T("2026-07-25T20:00:00Z"), decided_at: T("2026-07-25T20:01:00Z"), conv_title: "", conv_agent_key: "", rule_name: "破坏性命令需审批" },
|
||||
{ id: 91, task_id: "t-acme-api", agent_name: "work#2", tool_name: "bash", tool_input: { command: "nmap -sV 203.0.113.11" }, status: "allowed", created_at: T("2026-07-25T18:00:00Z"), decided_at: T("2026-07-25T18:00:05Z"), conv_title: "", conv_agent_key: "", rule_name: "" },
|
||||
{ id: 92, rule_id: 2, task_id: "t-acme-web", agent_name: "work#13", tool_name: "bash", tool_input: { command: "impacket-secretsdump acme.local/[email protected] -just-dc" }, status: "allowed", created_at: T("2026-07-26T00:05:00Z"), decided_at: T("2026-07-26T00:06:00Z"), conv_title: "", conv_agent_key: "", rule_name: "外发流量直接拒绝" },
|
||||
{ id: 93, rule_id: 1, task_id: "t-acme-web", agent_name: "work#7", tool_name: "bash", tool_input: { command: "java -jar JNDIExploit.jar -i <vps> -l 1389" }, status: "allowed", created_at: T("2026-07-25T15:14:00Z"), decided_at: T("2026-07-25T15:15:00Z"), conv_title: "", conv_agent_key: "", rule_name: "破坏性命令需审批" },
|
||||
{ id: 90, rule_id: 1, task_id: "t-acme-web", agent_name: "work#1", tool_name: "bash", tool_input: { command: "dd if=/dev/zero of=/tmp/x" }, status: "denied", reason: "破坏性命令需审批", created_at: T("2026-07-25T20:00:00Z"), decided_at: T("2026-07-25T20:01:00Z"), conv_title: "", conv_agent_key: "", rule_name: "破坏性命令需审批" },
|
||||
{ id: 91, task_id: "t-acme-api", agent_name: "work#2", tool_name: "bash", tool_input: { command: "rm -rf /var/www/html" }, status: "denied", reason: "[模型] 删除目标生产文件(D4)", created_at: T("2026-07-25T18:00:00Z"), decided_at: T("2026-07-25T18:00:05Z"), conv_title: "", conv_agent_key: "", rule_name: "" },
|
||||
{ id: 92, rule_id: 2, task_id: "t-acme-web", agent_name: "work#13", tool_name: "bash", tool_input: { command: "impacket-secretsdump acme.local/[email protected] -just-dc" }, status: "allowed", reason: "外发流量直接拒绝", created_at: T("2026-07-26T00:05:00Z"), decided_at: T("2026-07-26T00:06:00Z"), conv_title: "", conv_agent_key: "", rule_name: "外发流量直接拒绝" },
|
||||
{ id: 93, task_id: "t-acme-web", agent_name: "work#7", tool_name: "bash", tool_input: { command: "mysql -e \"DROP TABLE users_bak_0921\"" }, status: "pending", reason: "[模型] 疑似备份表,无法确定是否生产数据", created_at: T("2026-07-25T15:14:00Z"), conv_title: "", conv_agent_key: "", rule_name: "" },
|
||||
];
|
||||
|
||||
// ── Conversations (chat) ─────────────────────────────────────────────────────
|
||||
@@ -753,6 +753,7 @@ export const conversationMessages: Record<number, Activity[]> = {
|
||||
|
||||
// ── 资产测试覆盖度(/tasks/{id}/coverage)──
|
||||
export const coverage = {
|
||||
enabled: true,
|
||||
scope_rows: 4,
|
||||
denominator: 26,
|
||||
tested: 11,
|
||||
|
||||
+474
-2
@@ -6,6 +6,189 @@ import * as D from "./data";
|
||||
|
||||
const delay = (ms = 120) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
<<<<<<< Updated upstream
|
||||
=======
|
||||
// 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 mockConversations = structuredClone(D.conversations);
|
||||
const mockIntents = structuredClone(D.intents);
|
||||
const mockCompanies = structuredClone(D.companies);
|
||||
const mockAssets = structuredClone(D.assets);
|
||||
let mockActiveTask = D.ACTIVE_TASK;
|
||||
|
||||
function mockAssetCounts(): Record<string, number> {
|
||||
return mockAssets.reduce<Record<string, number>>((counts, asset) => {
|
||||
counts[asset.type] = (counts[asset.type] ?? 0) + 1;
|
||||
return counts;
|
||||
}, {});
|
||||
}
|
||||
|
||||
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 normalizedTemplateName(value: unknown): string {
|
||||
return String(value ?? "")
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
>>>>>>> Stashed changes
|
||||
function parseBody(body?: BodyInit | null): Record<string, unknown> {
|
||||
if (typeof body !== "string") return {};
|
||||
try {
|
||||
@@ -34,11 +217,213 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
|
||||
if (path === "/auth/change-password") return { ok: true };
|
||||
|
||||
// ── tasks ──
|
||||
<<<<<<< Updated upstream
|
||||
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 (path === "/tasks" && m === "GET") 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 created: Task = {
|
||||
id,
|
||||
name: String(b.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,
|
||||
};
|
||||
for (const item of mockTasks) item.active = false;
|
||||
mockTasks.unshift(created);
|
||||
mockActiveTask = id;
|
||||
return created;
|
||||
}
|
||||
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 === "DELETE") {
|
||||
const id = seg[1];
|
||||
const index = mockTasks.findIndex((item) => item.id === id);
|
||||
if (index >= 0) mockTasks.splice(index, 1);
|
||||
|
||||
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 };
|
||||
>>>>>>> Stashed changes
|
||||
if (seg[0] === "tasks" && seg[2] === "chat" && seg[3] === "stop") return { status: "stopped" };
|
||||
if (path === "/active") return { active: String(b.id ?? D.ACTIVE_TASK) };
|
||||
|
||||
@@ -76,14 +461,20 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
|
||||
if (path === "/exploration/findings/stats") {
|
||||
const vulnclasses = Array.from(new Set(D.findings.map((f) => f.vulnclass))).sort();
|
||||
// 「按任务」下拉:有漏洞的任务 + 描述 + 条数(mock 任务 id 是字符串,直接当 id 用)。
|
||||
<<<<<<< Updated upstream
|
||||
const taskMap = new Map<string, { description: string; count: number }>();
|
||||
for (const f of D.findings) {
|
||||
=======
|
||||
const taskMap = new Map<string, { name: string; description: string; count: number }>();
|
||||
for (const f of mockFindings) {
|
||||
>>>>>>> Stashed changes
|
||||
if (!f.task_id) continue;
|
||||
const cur = taskMap.get(f.task_id) ?? { description: f.task_description ?? "", count: 0 };
|
||||
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, description: v.description, count: v.count }));
|
||||
const tasks = Array.from(taskMap, ([id, v]) => ({ id, name: v.name, description: v.description, count: v.count }));
|
||||
return {
|
||||
total: D.findings.length,
|
||||
pending: D.findings.filter((f) => f.status === "pending").length,
|
||||
@@ -95,6 +486,76 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
|
||||
tasks,
|
||||
};
|
||||
}
|
||||
<<<<<<< Updated upstream
|
||||
=======
|
||||
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,
|
||||
};
|
||||
}
|
||||
>>>>>>> Stashed changes
|
||||
// 单条 finding:GET 详情 / PATCH 改状态/严重度/名称/类别(demo 直接改内存对象)。
|
||||
if (
|
||||
seg[0] === "exploration" &&
|
||||
@@ -257,6 +718,17 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
|
||||
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 };
|
||||
|
||||
@@ -6,6 +6,7 @@ export type EngineMode = "exploring" | "paused" | "stalled" | "idle";
|
||||
|
||||
export interface Task {
|
||||
id: string;
|
||||
name?: string; // 可选任务名称;空/缺省=未命名,展示时回退到描述
|
||||
description: string;
|
||||
goal: string;
|
||||
status: TaskStatus;
|
||||
@@ -24,6 +25,44 @@ export interface Task {
|
||||
engine_mode?: EngineMode;
|
||||
tokens?: TokenTotal; // whole-task token consumption
|
||||
llm_profile_id?: number; // LLM profile used; absent = default profile
|
||||
<<<<<<< Updated upstream
|
||||
=======
|
||||
llm_profile_ids?: number[]; // ordered task-level failover chain
|
||||
active_llm_profile_id?: number; // profile used by the next LLM call
|
||||
llm_failover_state?: "default" | "ready" | "chain_exhausted" | string;
|
||||
llm_failover_reason?: string;
|
||||
source_task_ids?: string[]; // directly related tasks inherited as read-only context
|
||||
company_ids?: number[]; // associated company asset scopes available as task context
|
||||
coverage_enabled?: boolean; // 资产覆盖度功能开关(创建时定,默认开);false=不计算/不展示覆盖度
|
||||
}
|
||||
|
||||
export interface TaskTemplate {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
goal: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface DeleteTaskOptions {
|
||||
delete_assets: boolean;
|
||||
delete_traffic: boolean;
|
||||
delete_files: boolean;
|
||||
delete_findings: boolean;
|
||||
delete_llm_records: boolean;
|
||||
}
|
||||
|
||||
export interface DeleteTaskResult {
|
||||
deleted: string;
|
||||
assets_deleted: number;
|
||||
assets_detached: number;
|
||||
traffic_deleted: number;
|
||||
files_deleted: boolean;
|
||||
findings_deleted: number;
|
||||
llm_records_deleted: number;
|
||||
cleanup_warning?: string;
|
||||
>>>>>>> Stashed changes
|
||||
}
|
||||
|
||||
// ---- Asset graph (global, shared across tasks) ----
|
||||
@@ -198,6 +237,30 @@ export interface WorkspaceFile {
|
||||
content?: string;
|
||||
}
|
||||
|
||||
<<<<<<< Updated upstream
|
||||
=======
|
||||
// 任务测试范围的一条(覆盖度分母 + 授权边界)。
|
||||
export interface TaskScopeRow {
|
||||
id: number;
|
||||
task_id: number;
|
||||
kind: "company" | "root_domain" | "subdomain" | "ip" | "cidr";
|
||||
company_id?: number;
|
||||
company_name?: string; // 后端 JOIN companies 解析,仅 kind=company 有值
|
||||
domain?: string;
|
||||
net?: string;
|
||||
source: "auto" | "agent" | "manual";
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export type CompanyScopeKind = "domain" | "ip" | "cidr" | "icp" | "keyword";
|
||||
|
||||
// 新增企业时提交的结构化资产范围规则。
|
||||
export interface CompanyScopeRule {
|
||||
kind: CompanyScopeKind;
|
||||
value: string;
|
||||
}
|
||||
|
||||
>>>>>>> Stashed changes
|
||||
// 公司资产范围规则的一条(归属唯一真值来源)。
|
||||
export interface ScopeRow {
|
||||
id: number;
|
||||
@@ -236,6 +299,26 @@ export interface TaskNode {
|
||||
ts: string;
|
||||
}
|
||||
|
||||
// 目标管理卡片用的目标(后端已把 payload 拆成 text/vulnclass)。
|
||||
export interface TaskGoal {
|
||||
id: string;
|
||||
text: string;
|
||||
vulnclass?: string;
|
||||
state: string; // GoalState
|
||||
origin?: string;
|
||||
ts: string;
|
||||
}
|
||||
|
||||
// 约束管理卡片用的操作约束(allow=允许 / deny=禁止)。
|
||||
export type ConstraintKind = "allow" | "deny";
|
||||
export interface TaskConstraint {
|
||||
id: string;
|
||||
kind: ConstraintKind;
|
||||
text: string;
|
||||
origin?: string;
|
||||
ts?: string;
|
||||
}
|
||||
|
||||
// ---- Findings ----
|
||||
export type Severity = "critical" | "high" | "medium" | "low";
|
||||
|
||||
@@ -283,6 +366,37 @@ export interface FindingsPage {
|
||||
page_size: number;
|
||||
}
|
||||
|
||||
<<<<<<< Updated upstream
|
||||
=======
|
||||
export interface FindingGroup {
|
||||
task_id: string | number | null;
|
||||
task_name?: string; // 可选任务名称;空/缺省=未命名
|
||||
task_description: string;
|
||||
task_status: string;
|
||||
count: number;
|
||||
critical: number;
|
||||
high: number;
|
||||
medium: number;
|
||||
low: number;
|
||||
last_found_at: string;
|
||||
}
|
||||
|
||||
export interface FindingGroupsPage {
|
||||
items: FindingGroup[];
|
||||
total: number;
|
||||
finding_total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
}
|
||||
|
||||
export interface FindingDeepenResponse {
|
||||
task_id: string;
|
||||
intent_id: string;
|
||||
state: IntentState;
|
||||
queued: boolean;
|
||||
}
|
||||
|
||||
>>>>>>> Stashed changes
|
||||
// FindingStats 是发现全表聚合(统计卡 + 漏洞类型下拉),服务端计算,不受分页影响。
|
||||
export interface FindingStats {
|
||||
total: number;
|
||||
@@ -298,7 +412,12 @@ export interface FindingStats {
|
||||
// FindingTaskOption 是发现页「按任务」筛选下拉的一项:有漏洞的任务(描述为空表示任务已删除,
|
||||
// 前端回退展示 id)及其漏洞条数。
|
||||
export interface FindingTaskOption {
|
||||
<<<<<<< Updated upstream
|
||||
id: number;
|
||||
=======
|
||||
id: string | number;
|
||||
name?: string; // 可选任务名称;空/缺省=未命名
|
||||
>>>>>>> Stashed changes
|
||||
description: string;
|
||||
count: number;
|
||||
}
|
||||
@@ -498,6 +617,20 @@ export interface Settings {
|
||||
web_search_proxy?: string;
|
||||
python_interpreter?: string; // 自定义脚本工具的 python 解释器路径(空=运行时检测)
|
||||
workers?: number; // 并发工作 agent 数(默认3);对之后启动的任务生效
|
||||
<<<<<<< Updated upstream
|
||||
=======
|
||||
// 任务并发上限:同时「运行中」的任务数上限。关闭=不限;开启后新建任务超限则排队,有空位自动启动。
|
||||
task_concurrency_enabled?: boolean; // 默认 false
|
||||
task_concurrency_limit?: number; // 开启后默认 5
|
||||
// LLM 轮询(故障转移)。默认关;开启后「未指定模型」的 agent 在当前配置不可用
|
||||
// (余额不足/key 失效/限流/服务异常)时自动切到下一个配置。
|
||||
llm_pool_enabled?: boolean; // 默认 false
|
||||
// 绑定了指定配置的 agent/任务失败时是否也回落到轮询链。默认 false = 绑定即独占。
|
||||
llm_pool_bind_fallback?: boolean;
|
||||
// 操作约束注入范围(默认都开):把任务的 allow/deny 约束拼进对应 agent 的系统提示。
|
||||
constraints_inject_planner?: boolean;
|
||||
constraints_inject_worker?: boolean;
|
||||
>>>>>>> Stashed changes
|
||||
}
|
||||
|
||||
// ---- LLM config ----
|
||||
@@ -664,10 +797,22 @@ export interface InterceptPending {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
tool_input: Record<string, any>;
|
||||
status: "pending" | "allowed" | "denied" | "timeout";
|
||||
reason: string; // 规则 message 或模型判定理由(模型判定带 [模型] 前缀)
|
||||
decided_at?: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// JudgeConfig: 模型兜底审批(仅当没有任何拦截规则命中时由模型判断)的全局配置。
|
||||
export interface JudgeConfig {
|
||||
enabled: boolean;
|
||||
profile_id: number; // 0 = 跟随激活/默认配置
|
||||
prompt: string; // 判定提示词;GET 未设置时后端回填内置模板全文
|
||||
timeout_seconds: number; // 模型调用超时
|
||||
fail_action: "allow" | "ask" | "deny"; // 模型出错/超时/不可解析时的回退
|
||||
ask_timeout_seconds: number; // 模型判 ask 转人工后的审批等待超时
|
||||
ask_timeout_action: "allow" | "deny"; // 审批超时后的默认动作
|
||||
}
|
||||
|
||||
// InterceptApprovalRow enriches InterceptPending with conversation/task and rule context.
|
||||
export interface InterceptApprovalRow extends InterceptPending {
|
||||
conv_title: string; // "" if no linked conversation
|
||||
|
||||
Reference in new issue
Block a user