Files
beaver_project/app-instance/frontend/lib/api.ts
steven_li 29dfd14aa6 ```
feat(agent): 添加对持久化子智能体的支持并增强委派管理

添加了持久化子智能体的完整生命周期管理功能,包括创建、更新、删除和查询API接口。
新增了子智能体的JSON-RPC通信协议支持,实现了远程调用和任务管理功能。

同时增强了委派管理器的功能:
- 添加了对本地委派、插件委派和本地回退的开关控制
- 实现了持久化子智能体任务的自动检测和本地执行保护
- 增加了对不同委派类型的权限验证机制

修改了智能体注册表以支持插件智能体的条件性包含,并更新了工具注册逻辑以支持可选工具。

BREAKING CHANGE: 委派管理器的构造函数签名已更改,添加了新的控制参数。
```
2026-03-27 10:15:35 +08:00

1191 lines
33 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Nanobot API client — single-user direct mode.
import type {
AuthzBackendRecord,
AuthzChannelSettings,
AuthzRegisterBackendResponse,
AuthzStatus,
AuthUser,
ChatMessage,
CronJob,
FileAttachment,
Marketplace,
MarketplacePlugin,
PluginInfo,
Session,
SessionDetail,
Skill,
SlashCommand,
SystemStatus,
TokenResponse,
OutlookConnectionPayload,
OutlookConnectionTestResult,
OutlookConnectResult,
OutlookEventListResponse,
OutlookMessageDetail,
OutlookMessageListResponse,
OutlookOverview,
OutlookStatus,
UiAgentDescriptor,
UiSubagentDescriptor,
UiMcpServerDescriptor,
WsEvent,
} from '@/types';
const API_URL = process.env.NEXT_PUBLIC_API_URL?.trim();
const WS_URL = process.env.NEXT_PUBLIC_WS_URL?.trim();
const DEFAULT_API_URL = 'http://127.0.0.1:18080';
const ACCESS_TOKEN_KEY = 'nanobot_access_token';
const REFRESH_TOKEN_KEY = 'nanobot_refresh_token';
const REQUEST_TIMEOUT_MS = 8000;
const OUTLOOK_REQUEST_TIMEOUT_MS = 45000;
function isBrowser(): boolean {
return typeof window !== 'undefined';
}
function normalizeBaseUrl(value?: string | null): string | null {
const trimmed = value?.trim();
if (!trimmed) return null;
return trimmed.replace(/\/+$/, '');
}
export function buildAuthHandoffUrl(response: TokenResponse, nextPath: string): string | null {
const targetBaseUrl = normalizeBaseUrl(
response.backend_connection?.frontend_base_url ||
response.backend_connection?.public_base_url ||
response.backend_connection?.api_base_url ||
response.local_backend?.public_base_url
);
if (!targetBaseUrl) return null;
const handoffCode = response.handoff_code?.trim();
if (!handoffCode) return null;
const target = new URL('/handoff', targetBaseUrl);
target.searchParams.set('code', handoffCode);
if (nextPath) {
target.searchParams.set('next', nextPath);
}
return target.toString();
}
function getApiBaseUrl(): string {
if (API_URL) return API_URL;
if (isBrowser()) return window.location.origin;
return DEFAULT_API_URL;
}
function buildApiUrl(path: string): string {
if (!API_URL && isBrowser()) {
return path;
}
return `${getApiBaseUrl()}${path}`;
}
type FetchJsonOptions = RequestInit & {
timeoutMs?: number;
};
function withTimeout(
signal?: AbortSignal,
timeoutMs: number = REQUEST_TIMEOUT_MS
): { signal: AbortSignal; cleanup: () => void } {
const controller = new AbortController();
const timeoutId = globalThis.setTimeout(() => {
controller.abort(new DOMException('请求超时', 'AbortError'));
}, timeoutMs);
const forwardAbort = () => controller.abort(signal?.reason);
signal?.addEventListener('abort', forwardAbort, { once: true });
return {
signal: controller.signal,
cleanup: () => {
globalThis.clearTimeout(timeoutId);
signal?.removeEventListener('abort', forwardAbort);
},
};
}
// ---------------------------------------------------------------------------
// Token management
// ---------------------------------------------------------------------------
export function getAccessToken(): string | null {
if (!isBrowser()) return null;
return localStorage.getItem(ACCESS_TOKEN_KEY);
}
export function getRefreshToken(): string | null {
if (!isBrowser()) return null;
return localStorage.getItem(REFRESH_TOKEN_KEY);
}
export function setTokens(access: string, refresh: string): void {
if (!isBrowser()) return;
localStorage.setItem(ACCESS_TOKEN_KEY, access);
localStorage.setItem(REFRESH_TOKEN_KEY, refresh);
}
export function clearTokens(): void {
if (!isBrowser()) return;
localStorage.removeItem(ACCESS_TOKEN_KEY);
localStorage.removeItem(REFRESH_TOKEN_KEY);
}
export function isLoggedIn(): boolean {
return !!getAccessToken();
}
// ---------------------------------------------------------------------------
// HTTP helpers
// ---------------------------------------------------------------------------
function authHeaders(includeJsonContentType: boolean = true): Record<string, string> {
const headers: Record<string, string> = {};
if (includeJsonContentType) {
headers['Content-Type'] = 'application/json';
}
const token = getAccessToken();
if (token) {
headers.Authorization = `Bearer ${token}`;
}
return headers;
}
async function fetchJSON<T>(path: string, options?: FetchJsonOptions): Promise<T> {
const mergedHeaders = {
...authHeaders(),
...(options?.headers as Record<string, string> | undefined),
};
const { signal, cleanup } = withTimeout(options?.signal ?? undefined, options?.timeoutMs);
let res: Response;
try {
res = await fetch(buildApiUrl(path), {
headers: mergedHeaders,
...options,
signal,
});
} catch (error) {
cleanup();
if (error instanceof DOMException && error.name === 'AbortError') {
throw new Error('请求超时');
}
throw error;
}
cleanup();
if (!res.ok) {
const text = await res.text();
if (res.status === 401) {
clearTokens();
}
let detail = text;
try {
const parsed = JSON.parse(text);
if (parsed && typeof parsed.detail === 'string') {
detail = parsed.detail;
}
} catch {
// keep raw text
}
throw new Error(`接口错误 ${res.status}: ${detail}`);
}
return res.json();
}
// ---------------------------------------------------------------------------
// Auth API
// ---------------------------------------------------------------------------
export async function register(username: string, email: string, password: string): Promise<TokenResponse> {
return fetchJSON('/api/auth/register', {
method: 'POST',
body: JSON.stringify({ username, email, password }),
});
}
export async function login(username: string, password: string): Promise<TokenResponse> {
return fetchJSON('/api/auth/login', {
method: 'POST',
body: JSON.stringify({ username, password }),
});
}
export async function consumeHandoffCode(code: string): Promise<TokenResponse> {
return fetchJSON('/api/auth/handoff/consume', {
method: 'POST',
body: JSON.stringify({ code }),
});
}
export async function logout(): Promise<void> {
try {
await fetchJSON('/api/auth/logout', { method: 'POST' });
} catch {
// ignore logout network failures
} finally {
clearTokens();
}
}
export async function getMe(): Promise<AuthUser> {
return fetchJSON('/api/auth/me');
}
// ---------------------------------------------------------------------------
// Chat (proxied via /api/)
// ---------------------------------------------------------------------------
export async function sendMessage(
message: string,
sessionId: string = 'web:default',
attachments?: FileAttachment[]
): Promise<{ response?: string; status?: string; session_id: string }> {
const body: Record<string, unknown> = { message, session_id: sessionId };
if (attachments && attachments.length > 0) {
body.attachments = attachments;
}
return fetchJSON('/api/chat', {
method: 'POST',
body: JSON.stringify(body),
});
}
export function streamMessage(
message: string,
sessionId: string,
onChunk: (content: string) => void,
onDone: () => void,
onError: (error: string) => void
): () => void {
const controller = new AbortController();
(async () => {
try {
const res = await fetch(buildApiUrl('/api/chat/stream'), {
method: 'POST',
headers: authHeaders(),
body: JSON.stringify({ message, session_id: sessionId }),
signal: controller.signal,
});
if (!res.ok || !res.body) {
onError(`HTTP 错误 ${res.status}`);
return;
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
const data = line.slice(6);
try {
const parsed = JSON.parse(data);
if (parsed.type === 'content') {
onChunk(parsed.content);
} else if (parsed.type === 'done') {
onDone();
} else if (parsed.type === 'error') {
onError(parsed.error);
}
} catch {
// skip parse errors
}
}
}
} catch (err: any) {
if (err.name !== 'AbortError') {
onError(err.message || '流式请求失败');
}
}
})();
return () => controller.abort();
}
// ---------------------------------------------------------------------------
// WebSocket Manager
// ---------------------------------------------------------------------------
export type WsStatus = 'disconnected' | 'connecting' | 'connected';
export type WsMessageHandler = (data: WsEvent | Record<string, unknown>) => void;
export type WsStatusListener = (status: WsStatus) => void;
function getWsUrl(): string {
const baseUrl =
WS_URL ||
(API_URL ? API_URL : isBrowser() ? window.location.origin : DEFAULT_API_URL);
const url = new URL(baseUrl);
let protocol: 'ws:' | 'wss:';
if (url.protocol === 'ws:' || url.protocol === 'wss:') {
protocol = url.protocol;
} else if (url.protocol === 'https:') {
protocol = 'wss:';
} else {
protocol = 'ws:';
}
return `${protocol}//${url.host}`;
}
class WebSocketManager {
private ws: WebSocket | null = null;
private sessionId: string | null = null;
private messageHandlers: WsMessageHandler[] = [];
private statusListeners: WsStatusListener[] = [];
private status: WsStatus = 'disconnected';
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
private pingTimer: ReturnType<typeof setInterval> | null = null;
private reconnectDelay = 1000;
private maxReconnectDelay = 30000;
private intentionalClose = false;
connect(sessionId: string): void {
if (this.sessionId === sessionId && this.ws?.readyState === globalThis.WebSocket?.OPEN) {
return;
}
this.intentionalClose = false;
this.sessionId = sessionId;
this.reconnectDelay = 1000;
this._connect();
}
disconnect(): void {
this.intentionalClose = true;
this._cleanup();
this._setStatus('disconnected');
}
sendMessage(content: string): void {
if (this.ws?.readyState === globalThis.WebSocket?.OPEN) {
this.ws.send(JSON.stringify({ type: 'message', content }));
}
}
sendRaw(payload: Record<string, unknown>): void {
if (this.ws?.readyState === globalThis.WebSocket?.OPEN) {
this.ws.send(JSON.stringify(payload));
}
}
onMessage(handler: WsMessageHandler): () => void {
this.messageHandlers.push(handler);
return () => {
this.messageHandlers = this.messageHandlers.filter((h) => h !== handler);
};
}
onStatusChange(listener: WsStatusListener): () => void {
this.statusListeners.push(listener);
listener(this.status);
return () => {
this.statusListeners = this.statusListeners.filter((l) => l !== listener);
};
}
getStatus(): WsStatus {
return this.status;
}
private _connect(): void {
this._cleanup();
if (!this.sessionId) return;
this._setStatus('connecting');
const wsUrl = getWsUrl();
const token = getAccessToken();
const query = token ? `?token=${encodeURIComponent(token)}` : '';
const ws = new globalThis.WebSocket(`${wsUrl}/ws/${this.sessionId}${query}`);
ws.onopen = () => {
this.reconnectDelay = 1000;
this._setStatus('connected');
this._startPing();
};
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
if (data.type === 'pong') return;
for (const handler of this.messageHandlers) {
handler(data);
}
} catch {
// ignore parse errors
}
};
ws.onclose = () => {
this._stopPing();
if (!this.intentionalClose) {
this._setStatus('disconnected');
this._scheduleReconnect();
}
};
ws.onerror = () => {
// onclose will fire after onerror
};
this.ws = ws;
}
private _cleanup(): void {
this._stopPing();
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
if (this.ws) {
this.ws.onopen = null;
this.ws.onmessage = null;
this.ws.onclose = null;
this.ws.onerror = null;
if (this.ws.readyState === globalThis.WebSocket?.OPEN ||
this.ws.readyState === globalThis.WebSocket?.CONNECTING) {
this.ws.close();
}
this.ws = null;
}
}
private _scheduleReconnect(): void {
if (this.reconnectTimer) return;
this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = null;
this._connect();
}, this.reconnectDelay);
this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
}
private _startPing(): void {
this._stopPing();
this.pingTimer = setInterval(() => {
if (this.ws?.readyState === globalThis.WebSocket?.OPEN) {
this.ws.send(JSON.stringify({ type: 'ping' }));
}
}, 30000);
}
private _stopPing(): void {
if (this.pingTimer) {
clearInterval(this.pingTimer);
this.pingTimer = null;
}
}
private _setStatus(status: WsStatus): void {
if (this.status === status) return;
this.status = status;
for (const listener of this.statusListeners) {
listener(status);
}
}
}
export const wsManager = new WebSocketManager();
// ---------------------------------------------------------------------------
// Sessions (proxied)
// ---------------------------------------------------------------------------
export async function listSessions(): Promise<Session[]> {
return fetchJSON('/api/sessions');
}
export async function createSession(key: string): Promise<SessionDetail> {
return fetchJSON(`/api/sessions/${encodeURIComponent(key)}`, { method: 'POST' });
}
export async function getSession(key: string): Promise<SessionDetail> {
return fetchJSON(`/api/sessions/${encodeURIComponent(key)}`);
}
export async function deleteSession(key: string): Promise<void> {
await fetchJSON(`/api/sessions/${encodeURIComponent(key)}`, { method: 'DELETE' });
}
// ---------------------------------------------------------------------------
// Status (proxied)
// ---------------------------------------------------------------------------
export async function getStatus(): Promise<SystemStatus> {
return fetchJSON('/api/status');
}
export async function restartSystem(): Promise<{
ok: boolean;
restarting: boolean;
detail: string;
}> {
return fetchJSON('/api/system/restart', {
method: 'POST',
});
}
// ---------------------------------------------------------------------------
// Cron (proxied)
// ---------------------------------------------------------------------------
export async function listCronJobs(includeDisabled: boolean = true): Promise<CronJob[]> {
return fetchJSON(`/api/cron/jobs?include_disabled=${includeDisabled}`);
}
export async function addCronJob(params: {
name: string;
message: string;
every_seconds?: number;
cron_expr?: string;
at_iso?: string;
session_key?: string;
}): Promise<CronJob> {
return fetchJSON('/api/cron/jobs', {
method: 'POST',
body: JSON.stringify(params),
});
}
export async function removeCronJob(jobId: string): Promise<void> {
await fetchJSON(`/api/cron/jobs/${jobId}`, { method: 'DELETE' });
}
export async function toggleCronJob(jobId: string, enabled: boolean): Promise<CronJob> {
return fetchJSON(`/api/cron/jobs/${jobId}/toggle`, {
method: 'PUT',
body: JSON.stringify({ enabled }),
});
}
export async function runCronJob(jobId: string): Promise<void> {
await fetchJSON(`/api/cron/jobs/${jobId}/run`, { method: 'POST' });
}
export async function ping(): Promise<{ message: string }> {
return fetchJSON('/api/ping');
}
// ---------------------------------------------------------------------------
// Skills (proxied)
// ---------------------------------------------------------------------------
export async function listSkills(): Promise<Skill[]> {
return fetchJSON('/api/skills');
}
export async function listCommands(): Promise<SlashCommand[]> {
return fetchJSON('/api/commands');
}
export async function listPlugins(): Promise<PluginInfo[]> {
return fetchJSON('/api/plugins');
}
export async function listAgents(): Promise<UiAgentDescriptor[]> {
return fetchJSON('/api/agents');
}
export async function addAgent(payload: {
id?: string;
name?: string;
description?: string;
protocol?: string;
base_url?: string;
endpoint?: string;
card_url?: string;
auth_env?: string;
auth_mode?: string;
auth_audience?: string;
auth_scopes?: string[];
enabled?: boolean;
tags?: string[];
aliases?: string[];
metadata?: Record<string, unknown>;
}): Promise<UiAgentDescriptor> {
return fetchJSON('/api/agents', {
method: 'POST',
body: JSON.stringify(payload),
});
}
export async function deleteAgent(agentId: string): Promise<void> {
await fetchJSON(`/api/agents/${encodeURIComponent(agentId)}`, { method: 'DELETE' });
}
export async function refreshAgents(): Promise<{ agents: UiAgentDescriptor[] }> {
return fetchJSON('/api/agents/refresh', { method: 'POST' });
}
export async function listSubagents(): Promise<UiSubagentDescriptor[]> {
return fetchJSON('/api/subagents');
}
export async function createSubagent(payload: {
id: string;
name?: string;
description?: string;
system_prompt?: string;
model?: string;
enabled?: boolean;
delegation_mode?: string;
allow_mcp?: boolean;
tags?: string[];
aliases?: string[];
mcp_servers?: Record<string, Record<string, unknown>>;
metadata?: Record<string, unknown>;
}): Promise<UiSubagentDescriptor> {
return fetchJSON('/api/subagents', {
method: 'POST',
body: JSON.stringify(payload),
});
}
export async function updateSubagent(
subagentId: string,
payload: {
id: string;
name?: string;
description?: string;
system_prompt?: string;
model?: string;
enabled?: boolean;
delegation_mode?: string;
allow_mcp?: boolean;
tags?: string[];
aliases?: string[];
mcp_servers?: Record<string, Record<string, unknown>>;
metadata?: Record<string, unknown>;
}
): Promise<UiSubagentDescriptor> {
return fetchJSON(`/api/subagents/${encodeURIComponent(subagentId)}`, {
method: 'PUT',
body: JSON.stringify(payload),
});
}
export async function deleteSubagent(subagentId: string): Promise<void> {
await fetchJSON(`/api/subagents/${encodeURIComponent(subagentId)}`, {
method: 'DELETE',
});
}
export async function cancelDelegation(runId: string): Promise<{ ok: boolean; run_id: string }> {
return fetchJSON(`/api/delegations/${encodeURIComponent(runId)}/cancel`, {
method: 'POST',
});
}
export async function listMcpServers(): Promise<UiMcpServerDescriptor[]> {
return fetchJSON('/api/mcp/servers');
}
export async function addMcpServer(payload: {
id: string;
command?: string;
args?: string[];
env?: Record<string, string>;
url?: string;
headers?: Record<string, string>;
auth_mode?: string;
auth_audience?: string;
auth_scopes?: string[];
tool_timeout?: number;
}): Promise<UiMcpServerDescriptor> {
return fetchJSON('/api/mcp/servers', {
method: 'POST',
body: JSON.stringify(payload),
});
}
export async function updateMcpServer(
serverId: string,
payload: {
id: string;
command?: string;
args?: string[];
env?: Record<string, string>;
url?: string;
headers?: Record<string, string>;
auth_mode?: string;
auth_audience?: string;
auth_scopes?: string[];
tool_timeout?: number;
}
): Promise<UiMcpServerDescriptor> {
return fetchJSON(`/api/mcp/servers/${encodeURIComponent(serverId)}`, {
method: 'PUT',
body: JSON.stringify(payload),
});
}
export async function deleteMcpServer(serverId: string): Promise<void> {
await fetchJSON(`/api/mcp/servers/${encodeURIComponent(serverId)}`, {
method: 'DELETE',
});
}
export async function getAuthzStatus(): Promise<AuthzStatus> {
return fetchJSON('/api/authz/status');
}
export async function bindLocalBackendIdentity(payload: {
backend_id: string;
client_id: string;
client_secret: string;
name?: string;
public_base_url?: string;
authz_base_url?: string;
authz_enabled?: boolean;
}): Promise<Record<string, unknown>> {
return fetchJSON('/api/authz/local-backend/bind', {
method: 'POST',
body: JSON.stringify(payload),
});
}
export async function listAuthzBackends(): Promise<AuthzBackendRecord[]> {
return fetchJSON('/api/authz/backends');
}
export async function registerAuthzBackend(payload: {
name?: string;
backend_id?: string;
base_url?: string;
save_to_backend?: boolean;
authz_base_url?: string;
}): Promise<AuthzRegisterBackendResponse> {
return fetchJSON('/api/authz/backends/register', {
method: 'POST',
body: JSON.stringify(payload),
});
}
export async function getAuthzBackend(backendId: string): Promise<AuthzBackendRecord> {
return fetchJSON(`/api/authz/backends/${encodeURIComponent(backendId)}`);
}
export async function enableAuthzBackend(backendId: string): Promise<AuthzBackendRecord> {
return fetchJSON(`/api/authz/backends/${encodeURIComponent(backendId)}/enable`, {
method: 'POST',
});
}
export async function disableAuthzBackend(backendId: string): Promise<AuthzBackendRecord> {
return fetchJSON(`/api/authz/backends/${encodeURIComponent(backendId)}/disable`, {
method: 'POST',
});
}
export async function rotateAuthzBackendSecret(backendId: string): Promise<Record<string, unknown>> {
return fetchJSON(`/api/authz/backends/${encodeURIComponent(backendId)}/rotate-secret`, {
method: 'POST',
});
}
export async function getAuthzBackendPermissions(backendId: string): Promise<Record<string, unknown>> {
return fetchJSON(`/api/authz/backends/${encodeURIComponent(backendId)}/permissions`);
}
export async function setAuthzBackendPermissions(
backendId: string,
payload: Record<string, unknown>
): Promise<Record<string, unknown>> {
return fetchJSON(`/api/authz/backends/${encodeURIComponent(backendId)}/permissions`, {
method: 'POST',
body: JSON.stringify(payload),
});
}
export async function getAuthzBackendOutlookSettings(backendId: string): Promise<Record<string, unknown>> {
return fetchJSON(`/api/authz/backends/${encodeURIComponent(backendId)}/settings/outlook`);
}
export async function setAuthzBackendOutlookSettings(
backendId: string,
payload: Record<string, unknown>
): Promise<Record<string, unknown>> {
return fetchJSON(`/api/authz/backends/${encodeURIComponent(backendId)}/settings/outlook`, {
method: 'POST',
body: JSON.stringify(payload),
});
}
export async function deleteAuthzBackendOutlookSettings(backendId: string): Promise<Record<string, unknown>> {
return fetchJSON(`/api/authz/backends/${encodeURIComponent(backendId)}/settings/outlook`, {
method: 'DELETE',
});
}
export async function listAuthzChannelSettings(): Promise<Record<string, AuthzChannelSettings>> {
return fetchJSON('/api/authz/channel-settings');
}
export async function getAuthzChannelSettings(channelId: string): Promise<AuthzChannelSettings> {
return fetchJSON(`/api/authz/channel-settings/${encodeURIComponent(channelId)}`);
}
export async function setAuthzChannelSettings(
channelId: string,
payload: {
configured?: boolean;
config?: Record<string, unknown>;
secrets?: Record<string, unknown>;
}
): Promise<AuthzChannelSettings> {
return fetchJSON(`/api/authz/channel-settings/${encodeURIComponent(channelId)}`, {
method: 'POST',
body: JSON.stringify(payload),
});
}
export async function deleteAuthzChannelSettings(channelId: string): Promise<Record<string, unknown>> {
return fetchJSON(`/api/authz/channel-settings/${encodeURIComponent(channelId)}`, {
method: 'DELETE',
});
}
export async function testMcpServer(serverId: string): Promise<Record<string, unknown>> {
return fetchJSON(`/api/mcp/servers/${encodeURIComponent(serverId)}/test`, {
method: 'POST',
});
}
export async function listMcpTools(): Promise<Array<{ server_id: string; tools: Array<Record<string, unknown>> }>> {
return fetchJSON('/api/mcp/tools');
}
export async function getOutlookStatus(): Promise<OutlookStatus> {
return fetchJSON('/api/integrations/outlook/status', {
timeoutMs: OUTLOOK_REQUEST_TIMEOUT_MS,
});
}
export async function testOutlookConnection(
payload: OutlookConnectionPayload
): Promise<OutlookConnectionTestResult> {
return fetchJSON('/api/integrations/outlook/test-connection', {
method: 'POST',
body: JSON.stringify(payload),
timeoutMs: OUTLOOK_REQUEST_TIMEOUT_MS,
});
}
export async function connectOutlook(
payload: OutlookConnectionPayload
): Promise<OutlookConnectResult> {
return fetchJSON('/api/integrations/outlook/connect', {
method: 'POST',
body: JSON.stringify(payload),
timeoutMs: OUTLOOK_REQUEST_TIMEOUT_MS,
});
}
export async function disconnectOutlook(): Promise<{ ok: boolean }> {
return fetchJSON('/api/integrations/outlook/disconnect', {
method: 'POST',
timeoutMs: OUTLOOK_REQUEST_TIMEOUT_MS,
});
}
export async function getOutlookOverview(): Promise<OutlookOverview> {
return fetchJSON('/api/integrations/outlook/overview', {
timeoutMs: OUTLOOK_REQUEST_TIMEOUT_MS,
});
}
export async function getOutlookMessages(
folder: string,
options?: {
top?: number;
skip?: number;
unreadOnly?: boolean;
}
): Promise<OutlookMessageListResponse> {
const params = new URLSearchParams({
folder,
top: String(options?.top ?? 20),
skip: String(options?.skip ?? 0),
});
if (options?.unreadOnly) {
params.set('unread_only', 'true');
}
return fetchJSON(`/api/integrations/outlook/messages?${params.toString()}`, {
timeoutMs: OUTLOOK_REQUEST_TIMEOUT_MS,
});
}
export async function getOutlookEvents(options: {
startTime: string;
endTime: string;
top?: number;
skip?: number;
}): Promise<OutlookEventListResponse> {
const params = new URLSearchParams({
start_time: options.startTime,
end_time: options.endTime,
top: String(options.top ?? 20),
skip: String(options.skip ?? 0),
});
return fetchJSON(`/api/integrations/outlook/events?${params.toString()}`, {
timeoutMs: OUTLOOK_REQUEST_TIMEOUT_MS,
});
}
export async function getOutlookMessageDetail(
messageId: string,
changekey?: string | null
): Promise<OutlookMessageDetail> {
const params = new URLSearchParams({ message_id: messageId });
if (changekey) {
params.set('changekey', changekey);
}
return fetchJSON(`/api/integrations/outlook/message-detail?${params.toString()}`, {
timeoutMs: OUTLOOK_REQUEST_TIMEOUT_MS,
});
}
export async function downloadSkill(name: string): Promise<void> {
const url = buildApiUrl(`/api/skills/${encodeURIComponent(name)}/download`);
const res = await fetch(url, { headers: authHeaders(false) });
if (!res.ok) {
const text = await res.text();
throw new Error(`下载失败:${text}`);
}
const blob = await res.blob();
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `${name}.zip`;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(a.href);
}
export async function deleteSkill(name: string): Promise<void> {
await fetchJSON(`/api/skills/${encodeURIComponent(name)}`, { method: 'DELETE' });
}
export async function uploadSkill(file: File): Promise<Skill> {
const formData = new FormData();
formData.append('file', file);
const token = getAccessToken();
const headers: Record<string, string> = {};
if (token) headers.Authorization = `Bearer ${token}`;
const url = buildApiUrl('/api/skills/upload');
const res = await fetch(url, {
method: 'POST',
headers,
body: formData,
});
if (!res.ok) {
const text = await res.text();
throw new Error(`接口错误 ${res.status}: ${text}`);
}
return res.json();
}
// ---------------------------------------------------------------------------
// Marketplace (proxied)
// ---------------------------------------------------------------------------
export async function listMarketplaces(): Promise<Marketplace[]> {
return fetchJSON('/api/marketplaces');
}
export async function addMarketplace(source: string): Promise<Marketplace> {
return fetchJSON('/api/marketplaces', {
method: 'POST',
body: JSON.stringify({ source }),
});
}
export async function removeMarketplace(name: string): Promise<void> {
await fetchJSON(`/api/marketplaces/${encodeURIComponent(name)}`, {
method: 'DELETE',
});
}
export async function updateMarketplace(name: string): Promise<Marketplace> {
return fetchJSON(`/api/marketplaces/${encodeURIComponent(name)}/update`, {
method: 'POST',
});
}
export async function listMarketplacePlugins(name: string): Promise<MarketplacePlugin[]> {
return fetchJSON(`/api/marketplaces/${encodeURIComponent(name)}/plugins`);
}
export async function installMarketplacePlugin(marketplaceName: string, pluginName: string): Promise<void> {
await fetchJSON(
`/api/marketplaces/${encodeURIComponent(marketplaceName)}/plugins/${encodeURIComponent(pluginName)}/install`,
{ method: 'POST' }
);
}
export async function uninstallPlugin(pluginName: string): Promise<void> {
await fetchJSON(`/api/plugins/${encodeURIComponent(pluginName)}`, {
method: 'DELETE',
});
}
// ---------------------------------------------------------------------------
// Files (proxied)
// ---------------------------------------------------------------------------
const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB
export async function uploadFile(
file: File,
sessionId: string = 'web:default',
onProgress?: (percent: number) => void
): Promise<FileAttachment> {
if (file.size > MAX_FILE_SIZE) {
throw new Error('文件过大(最大 50MB');
}
const formData = new FormData();
formData.append('file', file);
formData.append('session_id', sessionId);
const result = await new Promise<FileAttachment>((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', buildApiUrl('/api/files/upload'));
const token = getAccessToken();
if (token) {
xhr.setRequestHeader('Authorization', `Bearer ${token}`);
}
xhr.upload.onprogress = (e) => {
if (e.lengthComputable && onProgress) {
onProgress(Math.round((e.loaded / e.total) * 100));
}
};
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
const data = JSON.parse(xhr.responseText);
resolve(data);
} else {
reject(new Error(`上传失败:${xhr.status}`));
}
};
xhr.onerror = () => reject(new Error('上传失败'));
xhr.send(formData);
});
return result;
}
export async function listFiles(sessionId?: string): Promise<FileAttachment[]> {
const params = sessionId ? `?session_id=${encodeURIComponent(sessionId)}` : '';
return fetchJSON(`/api/files${params}`);
}
export async function deleteFile(fileId: string): Promise<void> {
await fetchJSON(`/api/files/${encodeURIComponent(fileId)}`, { method: 'DELETE' });
}
export function getFileUrl(fileId: string): string {
return buildApiUrl(`/api/files/${encodeURIComponent(fileId)}`);
}
// ---------------------------------------------------------------------------
// Workspace Browser
// ---------------------------------------------------------------------------
export interface WorkspaceItem {
name: string;
path: string;
type: 'file' | 'directory';
size: number | null;
content_type?: string;
modified: string;
}
export interface BrowseResult {
path: string;
items: WorkspaceItem[];
}
export async function browseWorkspace(path: string = ''): Promise<BrowseResult> {
const params = path ? `?path=${encodeURIComponent(path)}` : '';
return fetchJSON(`/api/workspace/browse${params}`);
}
export function getWorkspaceDownloadUrl(path: string): string {
return buildApiUrl(`/api/workspace/download?path=${encodeURIComponent(path)}`);
}
export async function uploadToWorkspace(
file: File,
dirPath: string = '',
onProgress?: (percent: number) => void
): Promise<WorkspaceItem> {
if (file.size > MAX_FILE_SIZE) {
throw new Error('文件过大(最大 50MB');
}
const formData = new FormData();
formData.append('file', file);
formData.append('path', dirPath);
return new Promise<WorkspaceItem>((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', buildApiUrl('/api/workspace/upload'));
const token = getAccessToken();
if (token) {
xhr.setRequestHeader('Authorization', `Bearer ${token}`);
}
xhr.upload.onprogress = (e) => {
if (e.lengthComputable && onProgress) {
onProgress(Math.round((e.loaded / e.total) * 100));
}
};
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error(`上传失败:${xhr.status}`));
}
};
xhr.onerror = () => reject(new Error('上传失败'));
xhr.send(formData);
});
}
export async function deleteWorkspacePath(path: string): Promise<void> {
await fetchJSON(`/api/workspace/delete?path=${encodeURIComponent(path)}`, {
method: 'DELETE',
});
}
export async function createWorkspaceDir(path: string): Promise<WorkspaceItem> {
return fetchJSON(`/api/workspace/mkdir?path=${encodeURIComponent(path)}`, {
method: 'POST',
});
}