Files
beaver_project/app-instance/frontend/lib/api.ts
steven_li b3767dd4ab feat(outlook): 添加 Outlook MCP 集成支持并优化分页功能
- 新增 NANO_OUTLOOK_MCP_URL 和 NANO_OUTLOOK_MCP_SERVER_ID 环境变量配置
- 实现 Outlook 邮件和日历的分页查询功能,添加安全参数验证
- 为 app-instance 创建脚本添加 Outlook MCP 服务器 ID 参数
- 更新前端 Outlook 页面实现邮件列表和日历事件的分页浏览
- 添加 Git 忽略文件配置和 Docker 挂载路径修复

BREAKING CHANGE: Outlook 集成现在需要配置 MCP URL 和服务器 ID 环境变量
2026-03-16 17:01:58 +08:00

1122 lines
31 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 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;
}): 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',
});
}