chore: ARTEX 初始化
ARTEX — AI 自主渗透测试系统(Go 后端 + Next.js 前端,单二进制内嵌)。 Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
commit
0ef8561411
287 files changed
+70178
No files matched your search
@@ -0,0 +1,443 @@
|
||||
// Real backend client. /api/* is proxied to the Go backend (next.config rewrites).
|
||||
// Returns the domain types in lib/types.ts. Shapes match the backend handlers;
|
||||
// a few fields the backend serializes differently (e.g. created_at as a unix int)
|
||||
// are passed through and formatted at the call site.
|
||||
|
||||
import { MOCK } from "@/lib/mock/enabled";
|
||||
import { mockHandle } from "@/lib/mock/handler";
|
||||
import type {
|
||||
Task, Stats, Asset, AssetNode, Edge, Company, TaskNode, Finding, Activity,
|
||||
Audit, TrafficResp, TrafficDetail, Settings, LLMProfile, Agent, AgentDetail, PromptVar,
|
||||
PromptVersion, MCPServer, MCPTool, SkillItem, TokenUsage, TokenTotal, DailyTokenBucket,
|
||||
Tool, Conversation, AgentTrigger,
|
||||
InterceptRule, InterceptPending, InterceptApprovalRow,
|
||||
TaskAssetView, SSProject, SSTask, ConvTokenSummary,
|
||||
} from "@/lib/types";
|
||||
|
||||
function getToken(): string | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
return localStorage.getItem("artex_token");
|
||||
}
|
||||
|
||||
async function http<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
if (MOCK) return mockHandle<T>(init?.method ?? "GET", path, init?.body ?? null);
|
||||
const token = getToken();
|
||||
const r = await fetch(`/api${path}`, {
|
||||
...init,
|
||||
headers: {
|
||||
...(init?.body ? { "Content-Type": "application/json" } : {}),
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
...(init?.headers as Record<string, string> | undefined),
|
||||
},
|
||||
});
|
||||
if (r.status === 401) {
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.removeItem("artex_token");
|
||||
document.cookie = "artex_token=; path=/; max-age=0";
|
||||
window.location.href = "/login";
|
||||
}
|
||||
throw new Error("未授权");
|
||||
}
|
||||
if (!r.ok) throw new Error(`${init?.method ?? "GET"} ${path}: ${r.status}`);
|
||||
if (r.status === 204) return undefined as T;
|
||||
return r.json();
|
||||
}
|
||||
|
||||
// sseUrl builds a URL for Server-Sent Events streams. SSE must NOT go through the
|
||||
// Next.js dev `/api` rewrite: that proxy buffers the streamed response, so event
|
||||
// frames never reach the browser (the EventSource opens but receives 0 messages).
|
||||
// We therefore connect straight to the Go backend, whose CORS is open. Override
|
||||
// with NEXT_PUBLIC_SSE_BASE; set it to "" to force same-origin (e.g. behind a
|
||||
// production reverse proxy that flushes SSE correctly).
|
||||
// Token is appended as ?token= because SSE bypasses the Next.js proxy and the
|
||||
// browser does not send cookies cross-port.
|
||||
// mockReport returns a canned Markdown report for the demo.
|
||||
function mockReport(_task?: string): string {
|
||||
return `# ARTEX 渗透测试报告 — Acme Corp
|
||||
|
||||
## 概览
|
||||
- 范围:acme.com(含 www / admin / api / shop / vpn 子域)
|
||||
- 已确认发现:6 项(高危 3 · 中危 3 · 低危 2)
|
||||
- 引擎模式:exploring
|
||||
|
||||
## 关键发现
|
||||
1. **[高] 后台默认口令** admin.acme.com admin/admin123 → 可完全接管后台。
|
||||
2. **[高] SQL 注入** www.acme.com/search?q= → 可读取 acme_prod 库。
|
||||
3. **[高] IDOR** api.acme.com/v1/orders?id= → 可越权读取他人订单(含手机号/地址)。
|
||||
4. **[中] 反射型 XSS**、**暴露 .git 源码**、**登录无速率限制**。
|
||||
|
||||
## 建议
|
||||
- 后台强制改密 + 启用 MFA、封禁默认口令。
|
||||
- search 接口参数化查询、输出编码。
|
||||
- API 增加对象级授权校验(IDOR)、更换强 JWT 密钥。
|
||||
|
||||
> (demo)本报告由 mock 数据生成,仅用于界面演示。`;
|
||||
}
|
||||
|
||||
export function sseUrl(path: string): string {
|
||||
const base =
|
||||
process.env.NEXT_PUBLIC_SSE_BASE ??
|
||||
(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) => http<T>(p, { method: "DELETE" });
|
||||
|
||||
// Go serializes nil slices as JSON null — coerce to [].
|
||||
const arr = <T>(x: T[] | null | undefined): T[] => x ?? [];
|
||||
const tq = (task?: string, sep: "?" | "&" = "?") => (task ? `${sep}task=${encodeURIComponent(task)}` : "");
|
||||
|
||||
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 }),
|
||||
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 ?? "" })),
|
||||
createTask: (description: string, goal: string, llmProfileId?: number, timeoutSeconds?: number) =>
|
||||
post<Task>("/tasks", { description, goal, llm_profile_id: llmProfileId ?? null, timeout_seconds: timeoutSeconds ?? 0 }),
|
||||
deleteTask: (id: string) => del<{ deleted: number }>(`/tasks/${id}`),
|
||||
controlTask: (id: string, action: "pause" | "resume") => post<{ id: string; paused: boolean }>(`/tasks/${id}/control`, { action }),
|
||||
setActive: (id: string) => post<{ active: string }>("/active", { id }),
|
||||
// ---- stats ----
|
||||
stats: (task?: string) => get<Stats>(`/stats${tq(task)}`),
|
||||
|
||||
// ---- 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 })),
|
||||
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}`,
|
||||
).then((r) => ({ assets: r?.assets ?? [], total: r?.total ?? r?.count ?? 0 })),
|
||||
assetCounts: () => get<Record<string, number>>("/assets/counts"),
|
||||
deleteAssets: (ids: number[]) =>
|
||||
http<{ deleted: number }>("/assets", { method: "DELETE", body: JSON.stringify({ ids }) }),
|
||||
// legacy — kept for task-specific views; hits the same endpoint with task_id filter
|
||||
taskAssets: (taskId: string, type = "") =>
|
||||
get<{ count: number; assets: Asset[] }>(`/assets?task_id=${taskId}&type=${type}`)
|
||||
.then((r) => r?.assets ?? []),
|
||||
|
||||
// ---- companies (企业 + 资产范围;归属唯一来源) ----
|
||||
companies: () => get<Company[]>("/companies").then(arr),
|
||||
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) : [] },
|
||||
),
|
||||
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 },
|
||||
),
|
||||
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 },
|
||||
),
|
||||
deleteCompany: (id: number, deleteAssets = false) =>
|
||||
http<{ deleted: number; assets_deleted: number }>(`/companies/${id}`, {
|
||||
method: "DELETE",
|
||||
body: JSON.stringify({ delete_assets: deleteAssets }),
|
||||
}),
|
||||
|
||||
// ---- exploration (per task) ----
|
||||
frontier: (task?: string) => get<TaskNode[]>(`/exploration/frontier${tq(task)}`).then(arr),
|
||||
findings: (task?: string) => get<Finding[]>(`/exploration/findings${tq(task)}`).then(arr),
|
||||
intents: (task?: string) => get<TaskNode[]>(`/exploration/intents${tq(task)}`).then(arr),
|
||||
tokenStats: (task?: string) =>
|
||||
get<{ workers: TokenUsage[]; total: TokenTotal }>(`/exploration/tokens${tq(task)}`).then((r) => ({
|
||||
workers: arr(r.workers),
|
||||
total: r.total,
|
||||
})),
|
||||
tokenDaily: (days = 30) =>
|
||||
get<DailyTokenBucket[]>(`/tokens/daily?days=${days}`).then(arr),
|
||||
conversationTokens: () =>
|
||||
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();
|
||||
if (task) q.set("task", task);
|
||||
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 }));
|
||||
},
|
||||
activityDetail: (id: number, task?: string) => get<{ detail: string }>(`/exploration/activity/${id}${tq(task)}`),
|
||||
|
||||
// ---- traffic / audit / report / chat ----
|
||||
audit: (task?: string) => get<Audit>(`/audit${tq(task)}`),
|
||||
traffic: (page = 0, size = 100, host = "", method = "", q = "") =>
|
||||
get<TrafficResp>(
|
||||
`/traffic?page=${page}&size=${size}` +
|
||||
(host ? `&host=${encodeURIComponent(host)}` : "") +
|
||||
(method && method !== "all" ? `&method=${encodeURIComponent(method)}` : "") +
|
||||
(q ? `&q=${encodeURIComponent(q)}` : ""),
|
||||
),
|
||||
trafficExchange: (id: string) =>
|
||||
get<TrafficDetail>(`/traffic/exchange?id=${encodeURIComponent(id)}`),
|
||||
|
||||
// ---- 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),
|
||||
report: async (task?: string) => {
|
||||
if (MOCK) return mockReport(task);
|
||||
const token = getToken();
|
||||
const r = await fetch(`/api/report${tq(task)}`, {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
});
|
||||
if (!r.ok) throw new Error(`report: ${r.status}`);
|
||||
return r.text();
|
||||
},
|
||||
chat: (message: string, task?: string) => post<{ reply: string; mode: string }>(`/chat${tq(task)}`, { message }),
|
||||
stopChat: (taskId: string) => post<{ status: string }>(`/tasks/${taskId}/chat/stop`, {}),
|
||||
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 }),
|
||||
testLLM: (
|
||||
provider: string,
|
||||
model: string,
|
||||
base_url: string,
|
||||
api_key: string,
|
||||
proxy = "",
|
||||
reasoning_effort = "",
|
||||
profile_id?: number,
|
||||
) =>
|
||||
post<{ ok: boolean; error?: string; latency_ms?: number; model?: string }>("/llm/test", {
|
||||
provider,
|
||||
model,
|
||||
base_url,
|
||||
proxy,
|
||||
api_key,
|
||||
reasoning_effort,
|
||||
profile_id,
|
||||
}),
|
||||
llmProfiles: () => get<{ profiles: LLMProfile[] }>("/llm/profiles").then((r) => arr(r.profiles)),
|
||||
saveLLMProfile: (p: {
|
||||
id?: number; // omit/0 = create; set = update that profile
|
||||
name: string;
|
||||
format: string;
|
||||
model: string;
|
||||
base_url?: string;
|
||||
proxy?: string;
|
||||
api_key?: string; // blank on update keeps the existing key
|
||||
rate_per_second?: number;
|
||||
rate_per_minute?: number;
|
||||
context_window_k?: number;
|
||||
reasoning_effort?: string; // ""|"off"|"low"|"medium"|"high"|"max"
|
||||
}) => post<{ id: number }>("/llm/profiles", p),
|
||||
deleteLLMProfile: (id: string) => del<{ deleted: number }>(`/llm/profiles/${id}`),
|
||||
activateLLMProfile: (id: string) => post<{ ok: boolean }>("/llm/profiles/active", { id: Number(id) }),
|
||||
|
||||
// ---- 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 }),
|
||||
updateAgent: (key: string, name: string, description = "") =>
|
||||
patch<{ ok: boolean }>(`/agents/${key}`, { name, description }),
|
||||
deleteAgent: (key: string) => del<{ deleted: string }>(`/agents/${key}`),
|
||||
|
||||
// ---- conversations (chat page) ----
|
||||
conversations: () => get<{ conversations: Conversation[] }>("/conversations").then((r) => arr(r.conversations)),
|
||||
createConversation: (agent_key: string, title = "", llm_profile_id?: number | null) =>
|
||||
post<Conversation>("/conversations", { agent_key, title, llm_profile_id: llm_profile_id ?? null }),
|
||||
renameConversation: (id: number, title: string) => patch<{ ok: boolean }>(`/conversations/${id}`, { title }),
|
||||
updateConversationProfile: (id: number, llm_profile_id: number | null) =>
|
||||
patch<{ ok: boolean }>(`/conversations/${id}/profile`, { llm_profile_id }),
|
||||
deleteConversation: (id: number) => del<{ deleted: number }>(`/conversations/${id}`),
|
||||
conversationMessages: (id: number, since = 0) =>
|
||||
get<{ items: Activity[]; cursor: number; running: boolean }>(`/conversations/${id}/messages?since=${since}`).then(
|
||||
(r) => ({ items: arr(r.items), cursor: r.cursor ?? 0, running: !!r.running }),
|
||||
),
|
||||
conversationMsgDetail: (id: number, seq: number) =>
|
||||
get<{ detail: string }>(`/conversations/${id}/messages/${seq}`).then((r) => r.detail ?? ""),
|
||||
sendConversationMessage: (id: number, message: string) =>
|
||||
post<{ status: string }>(`/conversations/${id}/messages`, { message }),
|
||||
stopConversation: (id: number) => post<{ status: string }>(`/conversations/${id}/stop`, {}),
|
||||
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=用内置默认轮数
|
||||
saveAgentWrapup: (key: string, prompt: string, maxTurns?: number) =>
|
||||
put<{ ok: boolean }>(`/agents/${key}/wrapup`, { prompt, max_turns: maxTurns }),
|
||||
resetAgentWrapup: (key: string) =>
|
||||
post<{ ok: boolean; wrapup_default: string; wrapup_max_turns_default: number }>(`/agents/${key}/wrapup/reset`, {}),
|
||||
// 任务级超时收尾词(仅 worker/planner);prompt 空=清除、用内置默认
|
||||
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`, {}),
|
||||
// P3 triggers (仅自定义 agent)
|
||||
agentTriggers: (key: string) =>
|
||||
get<{ triggers: AgentTrigger[] }>(`/agents/${key}/triggers`).then((r) => arr(r.triggers)),
|
||||
createTrigger: (key: string, t: Omit<AgentTrigger, "id" | "agent_key" | "last_fire">) =>
|
||||
post<AgentTrigger>(`/agents/${key}/triggers`, t),
|
||||
updateTrigger: (id: number, t: Omit<AgentTrigger, "id" | "agent_key" | "last_fire">) =>
|
||||
patch<{ ok: boolean }>(`/triggers/${id}`, t),
|
||||
deleteTrigger: (id: number) => del<{ deleted: number }>(`/triggers/${id}`),
|
||||
saveAgentConfig: (key: string, patch: { max_turns: number; run_seconds?: number; web_search?: boolean; interactive_shell?: boolean }) =>
|
||||
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)),
|
||||
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 }),
|
||||
|
||||
// ---- tools (内置工具目录) ----
|
||||
tools: () => get<{ tools: Tool[] }>("/tools").then((r) => arr(r.tools)),
|
||||
saveTool: (key: string, patch: Pick<Tool, "description" | "schema" | "agents" | "enabled">) =>
|
||||
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),
|
||||
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),
|
||||
detectPython: () => post<{ python_interpreter: string }>("/settings/python/detect", {}),
|
||||
|
||||
// ---- mcp ----
|
||||
mcpServers: () => get<{ servers: MCPServer[] }>("/mcp").then((r) => arr(r.servers)),
|
||||
saveMcpServer: (m: Partial<MCPServer>) => post<{ id: number }>("/mcp", m),
|
||||
deleteMcpServer: (id: number) => del<{ deleted: number }>(`/mcp/${id}`),
|
||||
mcpTools: (id: number) => get<{ tools: MCPTool[] }>(`/mcp/${id}/tools`).then((r) => arr(r.tools)),
|
||||
refreshMcpServer: (id: number) => post<{ tools: MCPTool[] }>(`/mcp/${id}/refresh`, {}).then((r) => arr(r.tools)),
|
||||
|
||||
// ---- 资产同步 (ScopeSentry 数据源) ----
|
||||
ssStatus: () =>
|
||||
get<{ exists: boolean; configured: boolean; enabled: boolean; reachable: boolean; url?: string; tools: string[] }>(
|
||||
"/sync/scopesentry/status",
|
||||
),
|
||||
ssDatasource: (body: { url?: string; api_key?: string }) =>
|
||||
post<{ id: number; enabled: boolean }>("/sync/scopesentry/datasource", body),
|
||||
ssProjects: (page = 1, size = 50, search = "") =>
|
||||
get<{ projects: SSProject[]; tag: Record<string, number> }>(
|
||||
`/sync/scopesentry/projects?page=${page}&size=${size}${search ? `&search=${encodeURIComponent(search)}` : ""}`,
|
||||
).then((r) => ({ projects: arr(r.projects), tag: r.tag ?? {} })),
|
||||
ssTasks: (page = 1, size = 50, search = "") =>
|
||||
get<{ tasks: SSTask[] }>(
|
||||
`/sync/scopesentry/tasks?page=${page}&size=${size}${search ? `&search=${encodeURIComponent(search)}` : ""}`,
|
||||
).then((r) => arr(r.tasks)),
|
||||
ssSync: (body: {
|
||||
dimension: "project" | "task";
|
||||
targets: string[];
|
||||
asset_types: string[];
|
||||
create_company?: boolean;
|
||||
page_size?: number;
|
||||
}) =>
|
||||
post<{
|
||||
synced: Record<string, number>;
|
||||
companies: string[] | null;
|
||||
warnings: string[] | null;
|
||||
errors: string[] | null;
|
||||
}>("/sync/scopesentry/sync", body),
|
||||
|
||||
// ---- 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),
|
||||
// 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 }> => {
|
||||
if (MOCK) return { name: file.name.replace(/\.zip$/i, ""), files: 1 };
|
||||
const fd = new FormData();
|
||||
fd.append("file", file);
|
||||
const token = getToken();
|
||||
const r = await fetch(`/api/skills/upload${overwrite ? "?overwrite=true" : ""}`, {
|
||||
method: "POST",
|
||||
body: fd,
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
});
|
||||
const body = await r.json().catch(() => ({}));
|
||||
if (!r.ok) throw new Error(body?.error || `上传失败(${r.status})`);
|
||||
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 }),
|
||||
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}`),
|
||||
|
||||
// ---- 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)),
|
||||
toggleVisibility: (agentId: string, kind: string, resourceId: number, visible: boolean) =>
|
||||
post<{ ok: boolean }>("/visibility/toggle", { agent_id: agentId, kind, resource_id: resourceId, visible }),
|
||||
|
||||
// ---- visibility (Skill,按名称) ----
|
||||
skillVisibility: (name: string) => get<{ agents: string[] }>(`/visibility/skill/${name}`).then((r) => arr(r.agents)),
|
||||
toggleSkillVisibility: (agentId: string, skillName: string, visible: boolean) =>
|
||||
post<{ ok: boolean }>("/visibility/skill/toggle", { agent_id: agentId, skill_name: skillName, visible }),
|
||||
|
||||
// ---- intercept rules ----
|
||||
interceptRules: () => get<{ rules: InterceptRule[] }>("/intercept/rules").then((r) => arr(r.rules)),
|
||||
createInterceptRule: (rule: Omit<InterceptRule, "id" | "created_at" | "updated_at">) =>
|
||||
post<InterceptRule>("/intercept/rules", rule),
|
||||
updateInterceptRule: (id: number, rule: Omit<InterceptRule, "id" | "created_at" | "updated_at">) =>
|
||||
put<InterceptRule>(`/intercept/rules/${id}`, rule),
|
||||
deleteInterceptRule: (id: number) => del<{ deleted: number }>(`/intercept/rules/${id}`),
|
||||
toggleInterceptRule: (id: number, enabled: boolean) =>
|
||||
post<{ ok: boolean; enabled: boolean }>(`/intercept/rules/${id}/toggle`, { enabled }),
|
||||
|
||||
// ---- intercept pending (ask) ----
|
||||
interceptPending: () => get<{ pending: InterceptPending[] }>("/intercept/pending").then((r) => arr(r.pending)),
|
||||
interceptGetOne: (id: number) => get<InterceptPending>(`/intercept/pending/${id}`),
|
||||
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)),
|
||||
|
||||
// ---- intercept tool-config (全局工具拦截范围) ----
|
||||
interceptGetToolConfig: async (): Promise<{ enabled_tools: string[] }> => {
|
||||
if (MOCK) return { enabled_tools: ["bash"] };
|
||||
const token = getToken();
|
||||
const r = await fetch("/api/intercept/tool-config", {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
});
|
||||
if (!r.ok) throw new Error(await r.text());
|
||||
return r.json();
|
||||
},
|
||||
interceptSetToolConfig: async (enabledTools: string[]): Promise<void> => {
|
||||
if (MOCK) return;
|
||||
const token = getToken();
|
||||
const r = await fetch("/api/intercept/tool-config", {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
body: JSON.stringify({ enabled_tools: enabledTools }),
|
||||
});
|
||||
if (!r.ok) throw new Error(await r.text());
|
||||
},
|
||||
};
|
||||
Reference in new issue
Block a user