feat(tasks): 总览页支持任务测试范围的查看/增删
- db: 新增 DeleteTaskScope;AddAgentScope 加 source 参数区分 agent/manual
- server: 新增 POST/DELETE /api/tasks/{id}/scope 端点
- web: 总览页新增「测试范围」卡片,可按类型手动增删,继承范围只读
Claude-Session: https://claude.ai/code/session_014TfZWeVGhjXqZD3vcj44mz
This commit is contained in:
1 parent
56101b46e8
commit
6b79043d82
8 files changed
+488
-193
No files matched your search
+207
-93
@@ -6,15 +6,59 @@
|
||||
import { MOCK } from "@/lib/mock/enabled";
|
||||
import { mockHandle } from "@/lib/mock/handler";
|
||||
import type {
|
||||
Task, DeleteTaskOptions, DeleteTaskResult, Stats, Asset, AssetNode, Edge, Company, TaskNode, Finding, Severity, FindingStatus,
|
||||
FindingsPage, FindingStats, FindingQuery, Activity,
|
||||
Audit, TrafficResp, TrafficDetail, TrafficHost, Settings, LLMProfile, Agent, AgentDetail, PromptVar,
|
||||
PromptVersion, MCPServer, MCPTool, SkillItem, TokenUsage, TokenTotal, DailyTokenBucket,
|
||||
Tool, Conversation, AgentTrigger, ChatAttachment,
|
||||
InterceptRule, InterceptPending, InterceptApprovalRow,
|
||||
TaskAssetView, SSProject, SSTask, ConvTokenSummary, CoverageGraphData, CoverageAssetRefs,
|
||||
WorkspaceListing, WorkspaceFile,
|
||||
CommandRecord, LLMRecordItem, LLMRecordDetail, LLMTask, LLMPoolStatus,
|
||||
Activity,
|
||||
Agent,
|
||||
AgentDetail,
|
||||
AgentTrigger,
|
||||
Asset,
|
||||
AssetNode,
|
||||
Audit,
|
||||
ChatAttachment,
|
||||
CommandRecord,
|
||||
Company,
|
||||
Conversation,
|
||||
ConvTokenSummary,
|
||||
CoverageAssetRefs,
|
||||
CoverageGraphData,
|
||||
DailyTokenBucket,
|
||||
DeleteTaskOptions,
|
||||
DeleteTaskResult,
|
||||
Edge,
|
||||
Finding,
|
||||
FindingQuery,
|
||||
FindingStats,
|
||||
FindingStatus,
|
||||
FindingsPage,
|
||||
InterceptApprovalRow,
|
||||
InterceptPending,
|
||||
InterceptRule,
|
||||
LLMPoolStatus,
|
||||
LLMProfile,
|
||||
LLMRecordDetail,
|
||||
LLMRecordItem,
|
||||
LLMTask,
|
||||
MCPServer,
|
||||
MCPTool,
|
||||
PromptVar,
|
||||
PromptVersion,
|
||||
Settings,
|
||||
Severity,
|
||||
SkillItem,
|
||||
SSProject,
|
||||
SSTask,
|
||||
Stats,
|
||||
Task,
|
||||
TaskAssetView,
|
||||
TaskNode,
|
||||
TaskScopeRow,
|
||||
TokenTotal,
|
||||
TokenUsage,
|
||||
Tool,
|
||||
TrafficDetail,
|
||||
TrafficHost,
|
||||
TrafficResp,
|
||||
WorkspaceFile,
|
||||
WorkspaceListing,
|
||||
} from "@/lib/types";
|
||||
|
||||
function getToken(): string | null {
|
||||
@@ -92,19 +136,21 @@ function mockReport(_task?: string): string {
|
||||
export function sseUrl(path: string): string {
|
||||
const base =
|
||||
process.env.NEXT_PUBLIC_SSE_BASE ??
|
||||
(typeof window !== "undefined"
|
||||
? `${window.location.protocol}//${window.location.hostname}:8787`
|
||||
: "");
|
||||
(typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8787` : "");
|
||||
const token = getToken();
|
||||
const sep = path.includes("?") ? "&" : "?";
|
||||
return token ? `${base}${path}${sep}token=${encodeURIComponent(token)}` : `${base}${path}`;
|
||||
}
|
||||
|
||||
const get = <T>(p: string) => http<T>(p);
|
||||
const post = <T>(p: string, body?: unknown) => http<T>(p, { method: "POST", body: body ? JSON.stringify(body) : undefined });
|
||||
const put = <T>(p: string, body?: unknown) => http<T>(p, { method: "PUT", body: body ? JSON.stringify(body) : undefined });
|
||||
const patch = <T>(p: string, body?: unknown) => http<T>(p, { method: "PATCH", body: body ? JSON.stringify(body) : undefined });
|
||||
const del = <T>(p: string, body?: unknown) => http<T>(p, { method: "DELETE", body: body ? JSON.stringify(body) : undefined });
|
||||
const post = <T>(p: string, body?: unknown) =>
|
||||
http<T>(p, { method: "POST", body: body ? JSON.stringify(body) : undefined });
|
||||
const put = <T>(p: string, body?: unknown) =>
|
||||
http<T>(p, { method: "PUT", body: body ? JSON.stringify(body) : undefined });
|
||||
const patch = <T>(p: string, body?: unknown) =>
|
||||
http<T>(p, { method: "PATCH", body: body ? JSON.stringify(body) : undefined });
|
||||
const del = <T>(p: string, body?: unknown) =>
|
||||
http<T>(p, { method: "DELETE", body: body ? JSON.stringify(body) : undefined });
|
||||
|
||||
// Go serializes nil slices as JSON null — coerce to [].
|
||||
const arr = <T>(x: T[] | null | undefined): T[] => x ?? [];
|
||||
@@ -116,15 +162,14 @@ export const api = {
|
||||
|
||||
// ---- auth ----
|
||||
authStatus: () => get<{ initialized: boolean }>("/auth/status"),
|
||||
login: (username: string, password: string) =>
|
||||
post<{ token: string }>("/auth/login", { username, password }),
|
||||
initPassword: (password: string) =>
|
||||
post<{ token: string }>("/auth/init", { password }),
|
||||
login: (username: string, password: string) => post<{ token: string }>("/auth/login", { username, password }),
|
||||
initPassword: (password: string) => post<{ token: string }>("/auth/init", { password }),
|
||||
changePassword: (oldPassword: string, newPassword: string) =>
|
||||
post<{ ok: boolean }>("/auth/change-password", { old_password: oldPassword, new_password: newPassword }),
|
||||
|
||||
// ---- tasks ----
|
||||
tasks: () => get<{ tasks: Task[]; active: string }>("/tasks").then((r) => ({ tasks: arr(r.tasks), active: r.active ?? "" })),
|
||||
tasks: () =>
|
||||
get<{ tasks: Task[]; active: string }>("/tasks").then((r) => ({ tasks: arr(r.tasks), active: r.active ?? "" })),
|
||||
createTask: (input: {
|
||||
description: string;
|
||||
goal: string;
|
||||
@@ -155,9 +200,9 @@ export const api = {
|
||||
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 }>(`/tasks/${id}/control`, { action }),
|
||||
deleteTask: (id: string, options: DeleteTaskOptions) => del<DeleteTaskResult>(`/tasks/${id}`, options),
|
||||
controlTask: (id: string, action: "pause" | "resume") =>
|
||||
post<{ id: string; paused: boolean }>(`/tasks/${id}/control`, { action }),
|
||||
controlIntent: (taskId: string, intentId: string, action: "pause" | "resume" | "cancel") =>
|
||||
post<{
|
||||
id: number;
|
||||
@@ -165,7 +210,8 @@ export const api = {
|
||||
deleted?: { intents: number; facts: number; findings: number; activities: number };
|
||||
}>(`/tasks/${taskId}/intents/${intentId}/control`, { action }),
|
||||
// 重跑一条没跑成功的意图(blocked/exhausted/stopped):置回 open,worker 会重新认领、从头再跑。
|
||||
rerunIntent: (taskId: string, intentId: string) => post<{ id: string; reopened: number }>(`/tasks/${taskId}/intents/${intentId}/rerun`),
|
||||
rerunIntent: (taskId: string, intentId: string) =>
|
||||
post<{ id: string; reopened: number }>(`/tasks/${taskId}/intents/${intentId}/rerun`),
|
||||
// 批量重跑本任务全部 blocked 意图(一次网络/LLM 断连导致多条 blocked 时一键全部重试)。
|
||||
rerunBlocked: (taskId: string) => post<{ id: string; reopened: number }>(`/tasks/${taskId}/intents/rerun-blocked`),
|
||||
setActive: (id: string) => post<{ active: string }>("/active", { id }),
|
||||
@@ -181,23 +227,24 @@ export const api = {
|
||||
by_type: { type: string; total: number; tested: number }[];
|
||||
}>(`/tasks/${id}/coverage`),
|
||||
// 资产覆盖图:范围内全部资产 + 连接用的根域名/公司节点,含 tested/in_scope。
|
||||
taskCoverageGraph: (id: string) =>
|
||||
get<CoverageGraphData>(`/tasks/${id}/coverage-graph`),
|
||||
taskCoverageGraph: (id: string) => get<CoverageGraphData>(`/tasks/${id}/coverage-graph`),
|
||||
// 本任务测试范围列表(含继承自来源任务的范围)。
|
||||
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}`),
|
||||
// 某资产在本任务里关联的意图 / 事实 / 发现(覆盖图节点抽屉用)。
|
||||
taskAssetRefs: (id: string, assetId: number) =>
|
||||
get<CoverageAssetRefs>(`/tasks/${id}/asset-refs?asset_id=${assetId}`),
|
||||
taskAssetRefs: (id: string, assetId: number) => get<CoverageAssetRefs>(`/tasks/${id}/asset-refs?asset_id=${assetId}`),
|
||||
|
||||
// ---- workspace file manager (workDir) ----
|
||||
workspaceList: (path = "") =>
|
||||
get<WorkspaceListing>(`/workspace/list?path=${encodeURIComponent(path)}`),
|
||||
workspaceRead: (path: string) =>
|
||||
get<WorkspaceFile>(`/workspace/read?path=${encodeURIComponent(path)}`),
|
||||
workspaceList: (path = "") => get<WorkspaceListing>(`/workspace/list?path=${encodeURIComponent(path)}`),
|
||||
workspaceRead: (path: string) => get<WorkspaceFile>(`/workspace/read?path=${encodeURIComponent(path)}`),
|
||||
workspaceWrite: (path: string, content: string) =>
|
||||
post<{ ok: boolean; path: string }>(`/workspace/write`, { path, content }),
|
||||
workspaceMkdir: (path: string) =>
|
||||
post<{ ok: boolean; path: string }>(`/workspace/mkdir`, { path }),
|
||||
workspaceDelete: (path: string) =>
|
||||
del<{ ok: boolean }>(`/workspace/delete?path=${encodeURIComponent(path)}`),
|
||||
workspaceMkdir: (path: string) => post<{ ok: boolean; path: string }>(`/workspace/mkdir`, { path }),
|
||||
workspaceDelete: (path: string) => del<{ ok: boolean }>(`/workspace/delete?path=${encodeURIComponent(path)}`),
|
||||
workspaceUpload: async (dir: string, files: File[]) => {
|
||||
if (MOCK) return { uploaded: files.length };
|
||||
const fd = new FormData();
|
||||
@@ -236,8 +283,9 @@ export const api = {
|
||||
// ---- assets ----
|
||||
// Server-side paginated: pass limit/offset, get back the page + full match total.
|
||||
assets: (type = "", limit = 50, offset = 0) =>
|
||||
get<{ count: number; total: number; assets: Asset[] }>(`/assets?type=${type}&limit=${limit}&offset=${offset}`)
|
||||
.then((r) => ({ assets: r?.assets ?? [], total: r?.total ?? r?.count ?? 0 })),
|
||||
get<{ count: number; total: number; assets: Asset[] }>(`/assets?type=${type}&limit=${limit}&offset=${offset}`).then(
|
||||
(r) => ({ assets: r?.assets ?? [], total: r?.total ?? r?.count ?? 0 }),
|
||||
),
|
||||
searchAssets: (dsl: string, type = "", limit = 50, offset = 0) =>
|
||||
get<{ count: number; total: number; assets: Asset[] }>(
|
||||
`/assets?dsl=${encodeURIComponent(dsl)}${type ? `&type=${encodeURIComponent(type)}` : ""}&limit=${limit}&offset=${offset}`,
|
||||
@@ -257,18 +305,38 @@ export const api = {
|
||||
createCompany: (name: string, logo: string, scope: string) =>
|
||||
post<{ id: number; created: boolean; scope_added?: number; scope_invalid?: number; scope_errors?: string[] }>(
|
||||
"/companies",
|
||||
{ name, logo, scope: scope.trim() ? scope.split("\n").map((s) => s.trim()).filter(Boolean) : [] },
|
||||
{
|
||||
name,
|
||||
logo,
|
||||
scope: scope.trim()
|
||||
? scope
|
||||
.split("\n")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
: [],
|
||||
},
|
||||
),
|
||||
addCompanyScope: (id: number, scope: string, reason = "") =>
|
||||
post<{ added: number; skipped: number; invalid: number; errors?: string[] }>(
|
||||
`/companies/${id}/scope`,
|
||||
{ scope: scope.trim() ? scope.split("\n").map((s) => s.trim()).filter(Boolean) : [], reason },
|
||||
),
|
||||
post<{ added: number; skipped: number; invalid: number; errors?: string[] }>(`/companies/${id}/scope`, {
|
||||
scope: scope.trim()
|
||||
? scope
|
||||
.split("\n")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
: [],
|
||||
reason,
|
||||
}),
|
||||
updateCompanyScope: (id: number, scope: string, reason = "") =>
|
||||
post<{ added: number; skipped: number; invalid: number; errors?: string[] }>(
|
||||
`/companies/${id}/scope`,
|
||||
{ scope: scope.trim() ? scope.split("\n").map((s) => s.trim()).filter(Boolean) : [], reason, reset: true },
|
||||
),
|
||||
post<{ added: number; skipped: number; invalid: number; errors?: string[] }>(`/companies/${id}/scope`, {
|
||||
scope: scope.trim()
|
||||
? scope
|
||||
.split("\n")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
: [],
|
||||
reason,
|
||||
reset: true,
|
||||
}),
|
||||
deleteCompany: (id: number, deleteAssets = false) =>
|
||||
http<{ deleted: number; assets_deleted: number }>(`/companies/${id}`, {
|
||||
method: "DELETE",
|
||||
@@ -293,12 +361,9 @@ export const api = {
|
||||
`/exploration/findings/${id}${contextTaskId ? `?context_task=${encodeURIComponent(contextTaskId)}` : ""}`,
|
||||
),
|
||||
// 漏洞链路:该漏洞节点回溯到任务初始节点的子图(节点 + 关系)。
|
||||
findingLineage: (id: string) =>
|
||||
get<{ nodes: TaskNode[]; edges: Edge[] }>(`/exploration/findings/${id}/lineage`),
|
||||
setFindingStatus: (id: string, status: FindingStatus) =>
|
||||
patch<Finding>(`/exploration/findings/${id}`, { status }),
|
||||
setFindingSeverity: (id: string, severity: Severity) =>
|
||||
patch<Finding>(`/exploration/findings/${id}`, { severity }),
|
||||
findingLineage: (id: string) => get<{ nodes: TaskNode[]; edges: Edge[] }>(`/exploration/findings/${id}/lineage`),
|
||||
setFindingStatus: (id: string, status: FindingStatus) => patch<Finding>(`/exploration/findings/${id}`, { status }),
|
||||
setFindingSeverity: (id: string, severity: Severity) => patch<Finding>(`/exploration/findings/${id}`, { severity }),
|
||||
// 一次保存漏洞的名称/类别/严重等级(发现列表行内编辑用),只传出现的字段。
|
||||
updateFinding: (
|
||||
id: string,
|
||||
@@ -312,10 +377,11 @@ export const api = {
|
||||
workers: arr(r.workers),
|
||||
total: r.total,
|
||||
})),
|
||||
tokenDaily: (days = 30) =>
|
||||
get<DailyTokenBucket[]>(`/tokens/daily?days=${days}`).then(arr),
|
||||
tokenDaily: (days = 30) => get<DailyTokenBucket[]>(`/tokens/daily?days=${days}`).then(arr),
|
||||
conversationTokens: () =>
|
||||
get<ConvTokenSummary[]>("/tokens/conversations").then(arr).catch(() => [] as ConvTokenSummary[]),
|
||||
get<ConvTokenSummary[]>("/tokens/conversations")
|
||||
.then(arr)
|
||||
.catch(() => [] as ConvTokenSummary[]),
|
||||
explorationGraph: (task?: string) => get<{ nodes: TaskNode[]; edges: Edge[] }>(`/exploration/graph${tq(task)}`),
|
||||
activity: (task?: string, opts?: { intent?: string; since?: number; limit?: number }) => {
|
||||
const q = new URLSearchParams();
|
||||
@@ -323,7 +389,10 @@ export const api = {
|
||||
if (opts?.intent) q.set("intent", opts.intent);
|
||||
if (opts?.since) q.set("since", String(opts.since));
|
||||
if (opts?.limit) q.set("limit", String(opts.limit));
|
||||
return get<{ items: Activity[]; cursor: number }>(`/exploration/activity?${q.toString()}`).then((r) => ({ items: arr(r.items), cursor: r.cursor ?? 0 }));
|
||||
return get<{ items: Activity[]; cursor: number }>(`/exploration/activity?${q.toString()}`).then((r) => ({
|
||||
items: arr(r.items),
|
||||
cursor: r.cursor ?? 0,
|
||||
}));
|
||||
},
|
||||
activityDetail: (id: number, task?: string) => get<{ detail: string }>(`/exploration/activity/${id}${tq(task)}`),
|
||||
// Reverse-paginated session history. session = "main" | "plan" | "intent:<id>".
|
||||
@@ -366,20 +435,21 @@ export const api = {
|
||||
(method && method !== "all" ? `&method=${encodeURIComponent(method)}` : "") +
|
||||
(q ? `&q=${encodeURIComponent(q)}` : ""),
|
||||
),
|
||||
trafficExchange: (id: string) =>
|
||||
get<TrafficDetail>(`/traffic/exchange?id=${encodeURIComponent(id)}`),
|
||||
trafficExchange: (id: string) => get<TrafficDetail>(`/traffic/exchange?id=${encodeURIComponent(id)}`),
|
||||
trafficHosts: () => get<{ hosts: TrafficHost[] }>(`/traffic/hosts`),
|
||||
trafficDeleteHost: (host: string) =>
|
||||
del<{ deleted: number }>(`/traffic?host=${encodeURIComponent(host)}`),
|
||||
trafficDeleteHosts: (hosts: string[]) =>
|
||||
del<{ deleted: number }>(`/traffic/hosts`, { hosts }),
|
||||
trafficDeleteHost: (host: string) => del<{ deleted: number }>(`/traffic?host=${encodeURIComponent(host)}`),
|
||||
trafficDeleteHosts: (hosts: string[]) => del<{ deleted: number }>(`/traffic/hosts`, { hosts }),
|
||||
|
||||
// ---- app settings (runtime toggles) ----
|
||||
settings: () => get<Settings>(`/settings`),
|
||||
setSettings: (patch: Partial<Settings>) => put<Settings>(`/settings`, patch),
|
||||
// Run a real "test" search with the given (or saved) config to verify it works.
|
||||
testWebSearch: (patch: { web_search_backend?: string; web_search_proxy?: string; brave_search_api_key?: string; tavily_search_api_key?: string }) =>
|
||||
post<{ ok: boolean; error?: string; count?: number; backend?: string }>(`/settings/web-search/test`, patch),
|
||||
testWebSearch: (patch: {
|
||||
web_search_backend?: string;
|
||||
web_search_proxy?: string;
|
||||
brave_search_api_key?: string;
|
||||
tavily_search_api_key?: string;
|
||||
}) => post<{ ok: boolean; error?: string; count?: number; backend?: string }>(`/settings/web-search/test`, patch),
|
||||
report: async (task?: string) => {
|
||||
if (MOCK) return mockReport(task);
|
||||
const token = getToken();
|
||||
@@ -393,7 +463,15 @@ export const api = {
|
||||
post<{ reply: string; mode: string }>(`/chat${tq(task)}`, { message, attachments }),
|
||||
// 方式1 文件上传:落到会话/任务工作目录 uploads/,返回可供 agent Read 的相对路径。
|
||||
chatUpload: async (scope: "task" | "session" | "staging", id: string, files: File[]) => {
|
||||
if (MOCK) return { attachments: files.map((f) => ({ name: f.name, path: `uploads/${f.name}`, size: f.size, abs: `/mock/${f.name}` })) };
|
||||
if (MOCK)
|
||||
return {
|
||||
attachments: files.map((f) => ({
|
||||
name: f.name,
|
||||
path: `uploads/${f.name}`,
|
||||
size: f.size,
|
||||
abs: `/mock/${f.name}`,
|
||||
})),
|
||||
};
|
||||
const fd = new FormData();
|
||||
for (const f of files) fd.append("file", f);
|
||||
const token = getToken();
|
||||
@@ -409,9 +487,29 @@ export const api = {
|
||||
gc: (ttl = 86400) => post<{ removed: number }>(`/gc?ttl=${ttl}`, {}),
|
||||
|
||||
// ---- LLM ----
|
||||
getLLM: () => get<{ configured: boolean; provider: string; model: string; base_url: string; proxy?: string; key_set: boolean; rate_per_second?: number; rate_per_minute?: number; context_window_k?: number; reasoning_effort?: string }>("/llm"),
|
||||
setLLM: (provider: string, model: string, base_url: string, api_key: string, rate_per_second = 0, rate_per_minute = 0, proxy = "", context_window_k = 0) =>
|
||||
post("/llm", { provider, model, base_url, proxy, api_key, rate_per_second, rate_per_minute, context_window_k }),
|
||||
getLLM: () =>
|
||||
get<{
|
||||
configured: boolean;
|
||||
provider: string;
|
||||
model: string;
|
||||
base_url: string;
|
||||
proxy?: string;
|
||||
key_set: boolean;
|
||||
rate_per_second?: number;
|
||||
rate_per_minute?: number;
|
||||
context_window_k?: number;
|
||||
reasoning_effort?: string;
|
||||
}>("/llm"),
|
||||
setLLM: (
|
||||
provider: string,
|
||||
model: string,
|
||||
base_url: string,
|
||||
api_key: string,
|
||||
rate_per_second = 0,
|
||||
rate_per_minute = 0,
|
||||
proxy = "",
|
||||
context_window_k = 0,
|
||||
) => post("/llm", { provider, model, base_url, proxy, api_key, rate_per_second, rate_per_minute, context_window_k }),
|
||||
testLLM: (
|
||||
provider: string,
|
||||
model: string,
|
||||
@@ -464,8 +562,7 @@ export const api = {
|
||||
// ---- agents ----
|
||||
agents: () => get<{ agents: Agent[] }>("/agents").then((r) => arr(r.agents)),
|
||||
getAgent: (key: string) => get<AgentDetail>(`/agents/${key}`),
|
||||
createAgent: (key: string, name: string, description = "") =>
|
||||
post<Agent>("/agents", { key, name, description }),
|
||||
createAgent: (key: string, name: string, description = "") => post<Agent>("/agents", { key, name, description }),
|
||||
updateAgent: (key: string, name: string, description = "") =>
|
||||
patch<{ ok: boolean }>(`/agents/${key}`, { name, description }),
|
||||
deleteAgent: (key: string) => del<{ deleted: string }>(`/agents/${key}`),
|
||||
@@ -497,7 +594,8 @@ export const api = {
|
||||
sendConversationMessage: (id: number, message: string, attachments?: ChatAttachment[]) =>
|
||||
post<{ status: string }>(`/conversations/${id}/messages`, { message, attachments }),
|
||||
stopConversation: (id: number) => post<{ status: string }>(`/conversations/${id}/stop`, {}),
|
||||
saveAgentPrompt: (key: string, template: string, note = "") => put<{ version: number }>(`/agents/${key}/prompt`, { template, note }),
|
||||
saveAgentPrompt: (key: string, template: string, note = "") =>
|
||||
put<{ version: number }>(`/agents/${key}/prompt`, { template, note }),
|
||||
resetAgentPrompt: (key: string) => post<{ version: number }>(`/agents/${key}/prompt/reset`, {}),
|
||||
// 收尾提示词(超时/步数耗尽的 settlement 提示);prompt 空串=清除覆盖、用内置默认;
|
||||
// max_turns 省略则不动、传 0=用内置默认轮数
|
||||
@@ -509,7 +607,10 @@ export const api = {
|
||||
saveAgentTaskTimeoutWrapup: (key: string, prompt: string, maxTurns?: number) =>
|
||||
put<{ ok: boolean }>(`/agents/${key}/wrapup/task-timeout`, { prompt, max_turns: maxTurns }),
|
||||
resetAgentTaskTimeoutWrapup: (key: string) =>
|
||||
post<{ ok: boolean; task_timeout_wrapup_default: string; task_timeout_wrapup_max_turns_default: number }>(`/agents/${key}/wrapup/task-timeout/reset`, {}),
|
||||
post<{ ok: boolean; task_timeout_wrapup_default: string; task_timeout_wrapup_max_turns_default: number }>(
|
||||
`/agents/${key}/wrapup/task-timeout/reset`,
|
||||
{},
|
||||
),
|
||||
// P3 triggers (仅自定义 agent)
|
||||
agentTriggers: (key: string) =>
|
||||
get<{ triggers: AgentTrigger[] }>(`/agents/${key}/triggers`).then((r) => arr(r.triggers)),
|
||||
@@ -531,12 +632,15 @@ export const api = {
|
||||
trigger_max_parallel?: number;
|
||||
},
|
||||
) => put<{ ok: boolean }>(`/agents/${key}/config`, patch),
|
||||
agentPromptVersions: (key: string) => get<{ versions: PromptVersion[] }>(`/agents/${key}/prompts`).then((r) => arr(r.versions)),
|
||||
agentVariables: (key: string) => get<{ variables: PromptVar[] }>(`/agents/${key}/variables`).then((r) => arr(r.variables)),
|
||||
agentPromptVersions: (key: string) =>
|
||||
get<{ versions: PromptVersion[] }>(`/agents/${key}/prompts`).then((r) => arr(r.versions)),
|
||||
agentVariables: (key: string) =>
|
||||
get<{ variables: PromptVar[] }>(`/agents/${key}/variables`).then((r) => arr(r.variables)),
|
||||
previewAgentPrompt: (key: string, template: string, sample?: Record<string, string>) =>
|
||||
post<{ rendered: string; error?: string }>(`/agents/${key}/prompt/preview`, { template, sample }),
|
||||
getAgentVisibility: (key: string) => get<{ mcp: number[]; skill: string[] }>(`/agents/${key}/visibility`),
|
||||
setAgentVisibility: (key: string, mcp: number[], skill: string[]) => put<{ ok: boolean }>(`/agents/${key}/visibility`, { mcp, skill }),
|
||||
setAgentVisibility: (key: string, mcp: number[], skill: string[]) =>
|
||||
put<{ ok: boolean }>(`/agents/${key}/visibility`, { mcp, skill }),
|
||||
|
||||
// ---- tools (内置工具目录) ----
|
||||
tools: () => get<{ tools: Tool[] }>("/tools").then((r) => arr(r.tools)),
|
||||
@@ -544,10 +648,13 @@ export const api = {
|
||||
put<{ ok: boolean }>(`/tools/${key}`, patch),
|
||||
resetTool: (key: string) => post<{ ok: boolean }>(`/tools/${key}/reset`, {}),
|
||||
// custom tools (自定义工具)
|
||||
createCustomTool: (t: Pick<Tool, "key" | "description" | "schema" | "agents" | "enabled" | "kind" | "exec" | "deferred">) =>
|
||||
post<{ key: string }>("/tools/custom", t),
|
||||
updateCustomTool: (key: string, t: Pick<Tool, "description" | "schema" | "agents" | "enabled" | "kind" | "exec" | "deferred">) =>
|
||||
put<{ ok: boolean }>(`/tools/custom/${key}`, t),
|
||||
createCustomTool: (
|
||||
t: Pick<Tool, "key" | "description" | "schema" | "agents" | "enabled" | "kind" | "exec" | "deferred">,
|
||||
) => post<{ key: string }>("/tools/custom", t),
|
||||
updateCustomTool: (
|
||||
key: string,
|
||||
t: Pick<Tool, "description" | "schema" | "agents" | "enabled" | "kind" | "exec" | "deferred">,
|
||||
) => put<{ ok: boolean }>(`/tools/custom/${key}`, t),
|
||||
deleteCustomTool: (key: string) => del<{ deleted: string }>(`/tools/custom/${key}`),
|
||||
testCustomTool: (body: { kind: string; exec: Record<string, unknown>; params: Record<string, unknown> }) =>
|
||||
post<{ output: string; is_error: boolean }>("/tools/custom/test", body),
|
||||
@@ -591,8 +698,14 @@ export const api = {
|
||||
|
||||
// ---- skills (文件系统) ----
|
||||
skills: () => get<{ skills: SkillItem[] }>("/skills").then((r) => arr(r.skills)),
|
||||
createSkill: (s: { name: string; description: string; license?: string; compatibility?: string; mcps?: string[]; instructions?: string }) =>
|
||||
post<{ name: string }>("/skills", s),
|
||||
createSkill: (s: {
|
||||
name: string;
|
||||
description: string;
|
||||
license?: string;
|
||||
compatibility?: string;
|
||||
mcps?: string[];
|
||||
instructions?: string;
|
||||
}) => post<{ name: string }>("/skills", s),
|
||||
// uploadSkill installs a skill from a .zip (multipart). Surfaces the backend
|
||||
// error text (e.g. 已存在 / 缺少 SKILL.md) so the UI can show a precise message.
|
||||
uploadSkill: async (file: File, overwrite = false): Promise<{ name: string; files: number }> => {
|
||||
@@ -610,20 +723,21 @@ export const api = {
|
||||
return body;
|
||||
},
|
||||
deleteSkill: (name: string) => del<{ deleted: string }>(`/skills/${name}`),
|
||||
updateSkillMeta: (name: string, meta: { mcps?: string[]; description?: string; license?: string; compatibility?: string }) =>
|
||||
put<{ ok: boolean }>(`/skills/${name}/meta`, meta),
|
||||
createSkillDir: (skill: string, path: string) =>
|
||||
post<{ dir: string }>(`/skills/${skill}/dirs`, { path }),
|
||||
updateSkillMeta: (
|
||||
name: string,
|
||||
meta: { mcps?: string[]; description?: string; license?: string; compatibility?: string },
|
||||
) => put<{ ok: boolean }>(`/skills/${name}/meta`, meta),
|
||||
createSkillDir: (skill: string, path: string) => post<{ dir: string }>(`/skills/${skill}/dirs`, { path }),
|
||||
skillFiles: (name: string) => get<{ files: string[] }>(`/skills/${name}/files`).then((r) => r.files),
|
||||
readSkillFile: (name: string, file: string) =>
|
||||
get<{ content: string; file: string }>(`/skills/${name}/files/${file}`).then((r) => r.content),
|
||||
writeSkillFile: (name: string, file: string, content: string) =>
|
||||
put<{ ok: boolean }>(`/skills/${name}/files/${file}`, { content }),
|
||||
deleteSkillPath: (skill: string, path: string) =>
|
||||
del<{ deleted: string }>(`/skills/${skill}/files/${path}`),
|
||||
deleteSkillPath: (skill: string, path: string) => del<{ deleted: string }>(`/skills/${skill}/files/${path}`),
|
||||
|
||||
// ---- visibility (MCP resource side) ---- (agent ids are strings per spec)
|
||||
resourceVisibility: (kind: string, id: number) => get<{ agents: string[] }>(`/visibility/${kind}/${id}`).then((r) => arr(r.agents)),
|
||||
resourceVisibility: (kind: string, id: number) =>
|
||||
get<{ agents: string[] }>(`/visibility/${kind}/${id}`).then((r) => arr(r.agents)),
|
||||
toggleVisibility: (agentId: string, kind: string, resourceId: number, visible: boolean) =>
|
||||
post<{ ok: boolean }>("/visibility/toggle", { agent_id: agentId, kind, resource_id: resourceId, visible }),
|
||||
|
||||
@@ -648,7 +762,8 @@ export const api = {
|
||||
interceptDecide: (id: number, decision: "allowed" | "denied") =>
|
||||
post<{ ok: boolean }>(`/intercept/pending/${id}/decide`, { decision }),
|
||||
interceptHistory: () => get<{ items: InterceptApprovalRow[] }>("/intercept/history").then((r) => arr(r.items)),
|
||||
interceptTask: (taskId: string) => get<{ items: InterceptApprovalRow[] }>(`/intercept/task/${taskId}`).then((r) => arr(r.items)),
|
||||
interceptTask: (taskId: string) =>
|
||||
get<{ items: InterceptApprovalRow[] }>(`/intercept/task/${taskId}`).then((r) => arr(r.items)),
|
||||
|
||||
// ---- intercept tool-config (全局工具拦截范围) ----
|
||||
interceptGetToolConfig: async (): Promise<{ enabled_tools: string[] }> => {
|
||||
@@ -696,6 +811,5 @@ export const api = {
|
||||
},
|
||||
llmRecordDetail: (id: number) => get<LLMRecordDetail>(`/llm/records/${id}`),
|
||||
llmTasks: () => get<{ tasks: LLMTask[] }>(`/llm/records/tasks`),
|
||||
llmRecordsDeleteTask: (task: string) =>
|
||||
del<{ deleted: number }>(`/llm/records?task=${encodeURIComponent(task)}`),
|
||||
llmRecordsDeleteTask: (task: string) => del<{ deleted: number }>(`/llm/records?task=${encodeURIComponent(task)}`),
|
||||
};
|
||||
Reference in new issue
Block a user