Files
beaver_project/app-instance/frontend/lib/api.ts
steven_li ebfa242862 feat(outlook): 添加Outlook集成功能支持
添加完整的Outlook MCP集成,包括邮件和日历功能,通过AuthZ模式进行认证和权限管理,
支持邮箱连接、断开、状态检查和数据同步等功能。

fix(config): 统一配置文件路径从.nanobot到.beaver

将配置文件路径从/root/.nanobot统一更改为/root/.beaver,更新Dockerfile中的环境变量定义,
确保所有组件使用一致的配置目录结构。

feat(agent): 添加代理删除功能和助手身份提示

为代理注册表添加delete_agent方法,实现代理的动态删除功能;同时添加海狸助手身份提示,
确保AI助手在交互中保持一致的身份认知。

feat(engine): 增强引擎循环并添加意图决策快照

扩展AgentLoop类,添加intent_agent_decision参数用于意图驱动的代理决策,并在会话中记录
决策快照,便于后续分析和调试。

feat(authz): 扩展认证客户端功能

为AuthzClient添加设置权限、用户注册、后端注册和Outlook设置管理等新方法,增强系统
的认证和授权能力。
2026-05-14 16:01:46 +08:00

1560 lines
46 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,
ActiveTask,
ChatLogsResponse,
BackendTask,
ChatMessage,
CronJob,
FileAttachment,
Marketplace,
MarketplacePlugin,
NotificationDetail,
NotificationRun,
PluginInfo,
ProviderConfigPayload,
Session,
SessionDetail,
Skill,
SkillDetailResponse,
SkillDraft,
SkillDraftEvalReport,
SkillDraftSafetyReport,
SkillFileContent,
SkillHubInstallResponse,
SkillHubSearchItem,
SkillHubSearchResponse,
SkillHubVersionResponse,
SkillHubVersionsResponse,
SkillLearningCandidate,
SkillReviewRecord,
SlashCommand,
SessionProcessProjection,
SystemStatus,
TokenResponse,
OutlookConnectionPayload,
OutlookConnectionTestResult,
OutlookConnectResult,
OutlookEventListResponse,
OutlookMessageDetail,
OutlookMessageListResponse,
OutlookOverview,
OutlookStatus,
UiAgentDescriptor,
UiSubagentDescriptor,
UiMcpServerDescriptor,
WsEvent,
} from '@/types';
import { getCurrentAppLocale, pickAppText } from '@/lib/i18n/core';
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;
const SKILL_LEARNING_REQUEST_TIMEOUT_MS = 120000;
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 locale = getCurrentAppLocale();
const controller = new AbortController();
const timeoutId = globalThis.setTimeout(() => {
controller.abort(new DOMException(pickAppText(locale, '请求超时', 'Request timed out'), '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 locale = getCurrentAppLocale();
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(pickAppText(locale, '请求超时', 'Request timed out'));
}
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(`${pickAppText(locale, '接口错误', 'API 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[],
options?: {
replyToScheduledRunId?: string;
scheduledReplyIntent?: 'revise_once' | 'update_future' | 'continue_task';
thinkingEnabled?: boolean;
}
): Promise<{
response?: string;
status?: string;
session_id: string;
run_id?: string;
task_id?: string | null;
task_status?: string | null;
validation_result?: Record<string, unknown> | null;
}> {
const body: Record<string, unknown> = { message, session_id: sessionId };
if (attachments && attachments.length > 0) {
body.attachments = attachments;
}
if (options?.replyToScheduledRunId) {
body.reply_to_scheduled_run_id = options.replyToScheduledRunId;
body.scheduled_reply_intent = options.scheduledReplyIntent || 'revise_once';
}
if (typeof options?.thinkingEnabled === 'boolean') {
body.thinking_enabled = options.thinkingEnabled;
}
const result = await fetchJSON<{
response?: string;
status?: string;
session_id: string;
run_id?: string;
output_text?: string;
finish_reason?: string;
task_id?: string | null;
task_status?: string | null;
validation_result?: Record<string, unknown> | null;
}>('/api/chat', {
method: 'POST',
body: JSON.stringify(body),
});
return {
response: result.response ?? result.output_text,
status: result.status ?? result.finish_reason,
session_id: result.session_id,
run_id: result.run_id,
task_id: result.task_id,
task_status: result.task_status,
validation_result: result.validation_result,
};
}
export async function submitChatFeedback(params: {
sessionId: string;
runId: string;
feedbackType: 'satisfied' | 'revise' | 'abandon';
comment?: string;
}): Promise<{
session_id: string;
run_id: string;
task_id: string;
task_status: string;
feedback_type: string;
}> {
return fetchJSON('/api/chat/feedback', {
method: 'POST',
body: JSON.stringify({
session_id: params.sessionId,
run_id: params.runId,
feedback_type: params.feedbackType,
comment: params.comment,
}),
});
}
export function streamMessage(
message: string,
sessionId: string,
onChunk: (content: string) => void,
onDone: () => void,
onError: (error: string) => void
): () => void {
const controller = new AbortController();
const locale = getCurrentAppLocale();
(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(`${pickAppText(locale, 'HTTP 错误', 'HTTP error')} ${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 || pickAppText(locale, '流式请求失败', 'Streaming request failed'));
}
}
})();
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 getSessionProcess(key: string): Promise<SessionProcessProjection> {
return fetchJSON(`/api/sessions/${encodeURIComponent(key)}/process`);
}
export async function getChatLogs(limit = 50): Promise<ChatLogsResponse> {
return fetchJSON(`/api/debug/chat-logs?limit=${encodeURIComponent(String(limit))}`, {
timeoutMs: 30000,
});
}
export async function archiveSession(key: string): Promise<void> {
await fetchJSON(`/api/sessions/${encodeURIComponent(key)}/archive`, { method: 'POST' });
}
// ---------------------------------------------------------------------------
// Status (proxied)
// ---------------------------------------------------------------------------
export async function getStatus(): Promise<SystemStatus> {
return fetchJSON('/api/status');
}
export async function updateProviderConfig(
providerId: string,
payload: ProviderConfigPayload
): Promise<{ ok: boolean; provider: string; enabled: boolean }> {
return fetchJSON(`/api/providers/${encodeURIComponent(providerId)}/config`, {
method: 'POST',
body: JSON.stringify(payload),
});
}
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;
tz?: string;
session_key?: string;
mode?: 'notification' | 'task';
requires_followup?: boolean;
}): 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 listNotifications(): Promise<NotificationRun[]> {
return fetchJSON('/api/notifications');
}
export async function getNotification(scheduledRunId: string): Promise<NotificationDetail> {
return fetchJSON(`/api/notifications/${encodeURIComponent(scheduledRunId)}`);
}
export async function engageNotification(
scheduledRunId: string,
intent: 'revise_once' | 'update_future' | 'continue_task'
): Promise<{ ok: boolean; task_id: string; intent: string }> {
return fetchJSON(`/api/notifications/${encodeURIComponent(scheduledRunId)}/engage`, {
method: 'POST',
body: JSON.stringify({ intent }),
});
}
export async function listBackendTasks(): Promise<BackendTask[]> {
return fetchJSON('/api/tasks');
}
export async function getBackendTask(taskId: string): Promise<BackendTask> {
return fetchJSON(`/api/tasks/${encodeURIComponent(taskId)}`);
}
export async function deleteBackendTask(taskId: string): Promise<void> {
await fetchJSON(`/api/tasks/${encodeURIComponent(taskId)}`, { method: 'DELETE' });
}
export async function getActiveTask(sessionId: string): Promise<ActiveTask | null> {
return fetchJSON(`/api/sessions/${encodeURIComponent(sessionId)}/active-task`);
}
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 getSkillDetail(skillName: string): Promise<SkillDetailResponse> {
return fetchJSON(`/api/skills/${encodeURIComponent(skillName)}/detail`);
}
export async function getSkillVersion(skillName: string, version: string): Promise<SkillDetailResponse> {
return fetchJSON(`/api/skills/${encodeURIComponent(skillName)}/versions/${encodeURIComponent(version)}`);
}
export async function getSkillFile(skillName: string, version: string, filePath: string): Promise<SkillFileContent> {
const search = new URLSearchParams({ path: filePath });
return fetchJSON(
`/api/skills/${encodeURIComponent(skillName)}/versions/${encodeURIComponent(version)}/file?${search.toString()}`
);
}
export async function listSkillCandidates(status?: string): Promise<SkillLearningCandidate[]> {
const query = status ? `?status=${encodeURIComponent(status)}` : '';
return fetchJSON(`/api/skills/candidates${query}`);
}
export async function synthesizeSkillDraft(candidateId: string): Promise<SkillDraft> {
return fetchJSON(`/api/skills/candidates/${encodeURIComponent(candidateId)}/draft`, {
method: 'POST',
body: JSON.stringify({}),
timeoutMs: SKILL_LEARNING_REQUEST_TIMEOUT_MS,
});
}
export async function regenerateSkillDraft(candidateId: string): Promise<SkillDraft> {
return fetchJSON(`/api/skills/candidates/${encodeURIComponent(candidateId)}/regenerate`, {
method: 'POST',
body: JSON.stringify({}),
timeoutMs: SKILL_LEARNING_REQUEST_TIMEOUT_MS,
});
}
export async function runSkillLearningOnce(): Promise<{
processed: number;
succeeded: number;
failed: number;
skipped: number;
failures: Array<Record<string, string>>;
}> {
return fetchJSON('/api/skills/learning/run-once', {
method: 'POST',
body: JSON.stringify({}),
timeoutMs: SKILL_LEARNING_REQUEST_TIMEOUT_MS,
});
}
export async function listSkillDrafts(): Promise<SkillDraft[]> {
return fetchJSON('/api/skills/drafts');
}
export async function getSkillDraft(skillName: string, draftId: string): Promise<SkillDraft> {
return fetchJSON(`/api/skills/${encodeURIComponent(skillName)}/drafts/${encodeURIComponent(draftId)}`);
}
export async function getSkillDraftSafety(skillName: string, draftId: string): Promise<SkillDraftSafetyReport> {
return fetchJSON(`/api/skills/${encodeURIComponent(skillName)}/drafts/${encodeURIComponent(draftId)}/safety`);
}
export async function getSkillDraftEval(skillName: string, draftId: string): Promise<SkillDraftEvalReport> {
return fetchJSON(`/api/skills/${encodeURIComponent(skillName)}/drafts/${encodeURIComponent(draftId)}/eval`);
}
export async function submitSkillDraft(
skillName: string,
draftId: string,
notes: string = ''
): Promise<SkillReviewRecord> {
return fetchJSON(`/api/skills/${encodeURIComponent(skillName)}/drafts/${encodeURIComponent(draftId)}/submit`, {
method: 'POST',
body: JSON.stringify({ notes }),
});
}
export async function approveSkillDraft(
skillName: string,
draftId: string,
notes: string = ''
): Promise<SkillReviewRecord> {
return fetchJSON(`/api/skills/${encodeURIComponent(skillName)}/drafts/${encodeURIComponent(draftId)}/approve`, {
method: 'POST',
body: JSON.stringify({ notes }),
});
}
export async function rejectSkillDraft(
skillName: string,
draftId: string,
notes: string = ''
): Promise<SkillReviewRecord> {
return fetchJSON(`/api/skills/${encodeURIComponent(skillName)}/drafts/${encodeURIComponent(draftId)}/reject`, {
method: 'POST',
body: JSON.stringify({ notes }),
});
}
export async function publishSkillDraft(
skillName: string,
draftId: string,
notes: string = '',
confirmHighRisk: boolean = false
): Promise<Record<string, unknown>> {
return fetchJSON(`/api/skills/${encodeURIComponent(skillName)}/drafts/${encodeURIComponent(draftId)}/publish`, {
method: 'POST',
body: JSON.stringify({ notes, confirm_high_risk: confirmHighRisk }),
});
}
export async function disablePublishedSkill(skillName: string, reason: string = ''): Promise<Record<string, unknown>> {
return fetchJSON(`/api/skills/${encodeURIComponent(skillName)}/disable`, {
method: 'POST',
body: JSON.stringify({ reason }),
});
}
export async function rollbackPublishedSkill(
skillName: string,
targetVersion: string,
reason: string = ''
): Promise<Record<string, unknown>> {
return fetchJSON(`/api/skills/${encodeURIComponent(skillName)}/rollback`, {
method: 'POST',
body: JSON.stringify({ target_version: targetVersion, reason }),
});
}
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 retryDelegation(runId: string): Promise<{ ok: boolean; run_id: string }> {
return fetchJSON(`/api/delegations/${encodeURIComponent(runId)}/retry`, {
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();
}
export async function migrateSkills(): Promise<{ included: Array<Record<string, unknown>>; skipped: Array<Record<string, unknown>> }> {
return fetchJSON('/api/skills/migrate', { method: 'POST', timeoutMs: 45000 });
}
// ---------------------------------------------------------------------------
// SkillHub marketplace
// ---------------------------------------------------------------------------
export async function searchSkillHubSkills(params: {
q?: string;
sort?: 'relevance' | 'downloads' | 'newest';
page?: number;
size?: number;
namespace?: string;
} = {}): Promise<SkillHubSearchResponse> {
const search = new URLSearchParams();
if (params.q) search.set('q', params.q);
if (params.sort) search.set('sort', params.sort);
if (typeof params.page === 'number') search.set('page', String(params.page));
if (typeof params.size === 'number') search.set('size', String(params.size));
if (params.namespace) search.set('namespace', params.namespace);
const suffix = search.toString();
return fetchJSON(`/api/marketplaces/skills/search${suffix ? `?${suffix}` : ''}`);
}
export async function getSkillHubDetail(namespace: string, slug: string): Promise<SkillHubSearchItem> {
return fetchJSON(
`/api/marketplaces/skills/${encodeURIComponent(namespace.replace(/^@/, ''))}/${encodeURIComponent(slug)}`
);
}
export async function getSkillHubVersion(
namespace: string,
slug: string,
version: string
): Promise<SkillHubVersionResponse> {
return fetchJSON(
`/api/marketplaces/skills/${encodeURIComponent(namespace.replace(/^@/, ''))}/${encodeURIComponent(slug)}/versions/${encodeURIComponent(version)}`
);
}
export async function getSkillHubVersions(namespace: string, slug: string): Promise<SkillHubVersionsResponse> {
return fetchJSON(
`/api/marketplaces/skills/${encodeURIComponent(namespace.replace(/^@/, ''))}/${encodeURIComponent(slug)}/versions`
);
}
export async function getSkillHubFile(
namespace: string,
slug: string,
version: string,
filePath: string
): Promise<SkillFileContent> {
const search = new URLSearchParams({ path: filePath });
return fetchJSON(
`/api/marketplaces/skills/${encodeURIComponent(namespace.replace(/^@/, ''))}/${encodeURIComponent(slug)}/versions/${encodeURIComponent(version)}/file?${search.toString()}`
);
}
export async function installSkillHubSkill(
namespace: string,
slug: string,
version?: string
): Promise<SkillHubInstallResponse> {
return fetchJSON(
`/api/marketplaces/skills/${encodeURIComponent(namespace.replace(/^@/, ''))}/${encodeURIComponent(slug)}/install`,
{
method: 'POST',
body: JSON.stringify({ version }),
timeoutMs: 45000,
}
);
}
// ---------------------------------------------------------------------------
// 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> {
const locale = getCurrentAppLocale();
if (file.size > MAX_FILE_SIZE) {
throw new Error(pickAppText(locale, '文件过大(最大 50MB', 'File is too large (max 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(`${pickAppText(locale, '上传失败', 'Upload failed')}: ${xhr.status}`));
}
};
xhr.onerror = () => reject(new Error(pickAppText(locale, '上传失败', 'Upload failed')));
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 interface WorkspaceFileContent {
name: string;
path: string;
size: number;
content_type: string;
modified: string;
is_binary: boolean;
is_truncated: boolean;
content: string | null;
}
export async function browseWorkspace(path: string = ''): Promise<BrowseResult> {
const params = path ? `?path=${encodeURIComponent(path)}` : '';
return fetchJSON(`/api/workspace/browse${params}`);
}
export async function getWorkspaceFile(path: string): Promise<WorkspaceFileContent> {
return fetchJSON(`/api/workspace/file?path=${encodeURIComponent(path)}`);
}
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> {
const locale = getCurrentAppLocale();
if (file.size > MAX_FILE_SIZE) {
throw new Error(pickAppText(locale, '文件过大(最大 50MB', 'File is too large (max 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(`${pickAppText(locale, '上传失败', 'Upload failed')}: ${xhr.status}`));
}
};
xhr.onerror = () => reject(new Error(pickAppText(locale, '上传失败', 'Upload failed')));
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',
});
}