Files
beaver_project/app-instance/frontend/lib/api.ts
steven_li 0c180f48f2 feat(delegation): 添加直连模式下的委托公告回调机制
- 引入 DirectAnnouncementCallback 类型用于处理直连模式下的公告
- 在 DelegationManager 中添加 _direct_announcement_callback 属性和设置方法
- 实现 _notify_direct_announcement 方法用于在非总线模式下将公告回写到本地会话
- 在委托取消、完成和分组完成时添加对直连公告的通知逻辑

feat(web): 增加 WebSocket 广播器支持实时会话更新通知

- 创建 WebSocketBroadcaster 类用于跟踪认证的 WebSocket 连接并广播 JSON 事件
- 在应用启动时初始化 websocket_broadcaster 实例
- 实现连接注册、注销和消息广播功能
- 添加过期连接清理机制

feat(agent): 新增系统公告处理方法支持本地处理

- 在 AgentLoop 中添加 process_system_announcement 方法用于在无常驻 run() 场景下处理系统公告
- 创建 InboundMessage 并通过 _process_message 进行处理

feat(cron): 改进定时任务的会话路由解析和实时更新

- 添加 _resolve_cron_session_key 和 _infer_cron_route_from_session_key 辅助函数
- 在 cron 任务执行完成后通过 WebSocket 广播会话更新事件
- 在添加定时任务时自动推断目标会话的渠道和聊天 ID

refactor: 项目名称从 Boardware Genius 统一改为 Boardware Agent Sandbox

- 更新前端页面标题和描述文本中的产品名称
- 添加新的品牌 Logo 图片资源
- 在前端布局中使用新的 Logo 显示
- 更新授权门户中的品牌信息和 Logo 显示

feat(frontend): 添加会话更新事件监听实现消息自动刷新

- 定义 SessionUpdatedEvent 类型接口
- 在 ChatPage 中添加会话更新事件的处理逻辑
- 当收到会话更新事件时自动重新加载会话列表和当前会话消息

feat(api): 扩展定时任务 API 支持会话键参数

- 在 addCronJob API 参数中添加 session_key 字段
- 更新前端 Cron 页面的表单处理以传递当前会话键
2026-03-18 14:34:25 +08:00

1127 lines
32 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,
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');
}
// ---------------------------------------------------------------------------
// 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 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',
});
}