Files
beaver_project/app-instance/frontend/lib/api.ts
steven_li 4b0bf65ace ```
feat(engine): 优化智能体循环中的助手消息处理逻辑

- 在没有工具调用时才添加助手消息到上下文
- 确保工具调用响应正确添加到消息上下文中
- 修复了消息构建的条件逻辑

fix(cron): 改进定时任务调度的时间解析功能

- 添加正则表达式导入用于时间显示解析
- 实现从显示文本中提取毫秒间隔的功能
- 增强整数转换的安全性,避免类型错误
- 优化定时任务配置的解析逻辑

feat(outlook): 增强Outlook集成的功能和稳定性

- 将默认超时时间从10秒增加到180秒
- 为状态检查函数添加可选的验证参数
- 串行执行邮件概览获取操作而非并行
- 改进连接状态验证逻辑

feat(channel): 添加设备名称作为会话标识的选项

- 为终端WebSocket适配器添加新的配置选项
- 实现基于设备名称生成会话对等ID的功能
- 记录原始对等ID和设备名称的元数据
- 支持从设备名称创建会话对等ID

feat(skills): 完善技能学习评估系统和进度跟踪

- 在应用启动时自动调度待评估的技能草稿
- 为技能评估工作创建独立的循环工厂
- 实现异步技能评估任务的取消和清理机制
- 添加技能评估进度报告和状态跟踪功能
- 扩展会话列表API以包含更多详细信息
- 防止对不存在的会话进行操作
- 优化技能草稿提交和评估的业务逻辑

perf(skills): 提升技能评估的并发性能

- 实现并行技能案例评估以提高效率
- 添加最大并行案例数的环境变量控制
- 实现实时评估进度更新和回调机制
- 优化评估过程中的资源管理和同步

refactor(services): 创建隔离的智能体循环实例

- 添加创建独立智能体循环的工厂方法
- 确保新循环继承运行时服务配置
- 支持技能评估等需要隔离环境的场景
```
2026-06-15 14:48:16 +08:00

1586 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.

// Beaver API client - single-user direct mode.
import type {
AuthzStatus,
AuthUser,
ActiveTask,
AgentConfigPayload,
ChatLogsResponse,
BackendTask,
ChatMessage,
ChannelConnectionView,
ChannelConfigDetail,
ChannelConfigPayload,
ChannelConnectorDescriptor,
ConnectorSessionResponse,
ConnectorSessionStartPayload,
ChannelEventRecord,
CronJob,
FileAttachment,
NotificationDetail,
NotificationRun,
ProviderConfigPayload,
Session,
SessionDetail,
Skill,
SkillDetailResponse,
SkillDraft,
SkillDraftEvalReport,
SkillDraftSafetyReport,
SkillFileContent,
SkillHubInstallResponse,
SkillHubSearchItem,
SkillHubSearchResponse,
SkillHubVersionResponse,
SkillHubVersionsResponse,
SkillLearningCandidate,
SkillReviewRecord,
SessionProcessProjection,
SystemStatus,
TokenResponse,
OutlookConnectionPayload,
OutlookConnectionTestResult,
OutlookConnectResult,
OutlookEventListResponse,
OutlookMessageDetail,
OutlookMessageListResponse,
OutlookOverview,
OutlookStatus,
UiAgentDescriptor,
UiSubagentDescriptor,
UiMcpServerDescriptor,
WsEvent,
} from '@/types';
import { getCurrentAppLocale, pickAppText, type AppLocale } 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 = 'beaver_access_token';
const REFRESH_TOKEN_KEY = 'beaver_refresh_token';
export const AUTH_CLEARED_EVENT = 'beaver-auth-cleared';
const REQUEST_TIMEOUT_MS = 8000;
const OUTLOOK_REQUEST_TIMEOUT_MS = 360000;
const SKILL_LEARNING_REQUEST_TIMEOUT_MS = 120000;
export type PromptLocale = 'zh-Hans' | 'zh-Hant' | 'en';
export function promptLocaleForAppLocale(locale: AppLocale): PromptLocale {
if (locale === 'zh-Hant') {
return 'zh-Hant';
}
return locale === 'en-US' ? 'en' : 'zh-Hans';
}
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;
};
export class ApiError extends Error {
status: number;
detail: string;
constructor(message: string, options: { status: number; detail: string }) {
super(message);
this.name = 'ApiError';
this.status = options.status;
this.detail = options.detail;
}
}
export function isApiError(error: unknown, status?: number): error is ApiError {
return error instanceof ApiError && (status === undefined || error.status === status);
}
function parseErrorDetail(text: string): string {
try {
const parsed = JSON.parse(text);
if (parsed && typeof parsed.detail === 'string') {
return parsed.detail;
}
} catch {
// keep raw text
}
return text;
}
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);
window.dispatchEvent(new CustomEvent(AUTH_CLEARED_EVENT));
}
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();
}
const detail = parseErrorDetail(text);
throw new ApiError(`${pickAppText(locale, '接口错误', 'API error')} ${res.status}: ${detail}`, {
status: 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;
promptLocale?: PromptLocale;
}
): Promise<{
response?: string;
status?: string;
session_id: string;
run_id?: string;
task_id?: string | null;
task_status?: string | null;
evidence_status?: string | null;
}> {
const body: Record<string, unknown> = {
message,
session_id: sessionId,
prompt_locale: options?.promptLocale || promptLocaleForAppLocale(getCurrentAppLocale()),
};
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;
evidence_status?: string | 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,
evidence_status: result.evidence_status,
};
}
export async function submitChatFeedback(params: {
sessionId: string;
runId: string;
feedbackType: 'accept' | 'revise' | 'abandon';
comment?: string;
}): Promise<{
session_id: string;
run_id: string;
task_id: string;
task_status: string;
acceptance_type: string;
feedback_type: string;
}> {
return fetchJSON('/api/chat/acceptance', {
method: 'POST',
body: JSON.stringify({
session_id: params.sessionId,
run_id: params.runId,
acceptance_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,
prompt_locale: promptLocaleForAppLocale(getCurrentAppLocale()),
}),
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 updateAgentConfig(payload: AgentConfigPayload): Promise<{ ok: boolean }> {
return fetchJSON('/api/agent-config', {
method: 'POST',
body: JSON.stringify(payload),
});
}
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 getChannelConfig(channelId: string): Promise<ChannelConfigDetail> {
return fetchJSON(`/api/channels/${encodeURIComponent(channelId)}/config`);
}
export async function updateChannelConfig(
channelId: string,
payload: ChannelConfigPayload
): Promise<{ ok: boolean; channel_id: string; restart_required: boolean; channel: ChannelConfigDetail }> {
return fetchJSON(`/api/channels/${encodeURIComponent(channelId)}/config`, {
method: 'POST',
body: JSON.stringify(payload),
});
}
export async function listChannelEvents(channelId: string, limit: number = 100): Promise<ChannelEventRecord[]> {
return fetchJSON(`/api/channels/${encodeURIComponent(channelId)}/events?limit=${limit}`);
}
export async function listChannelConnectors(): Promise<ChannelConnectorDescriptor[]> {
return fetchJSON('/api/channel-connectors');
}
export async function listChannelConnections(): Promise<ChannelConnectionView[]> {
return fetchJSON('/api/channel-connections');
}
export async function startChannelConnectorSession(
payload: ConnectorSessionStartPayload
): Promise<ConnectorSessionResponse> {
return fetchJSON('/api/channel-connector-sessions', {
method: 'POST',
body: JSON.stringify({
kind: payload.kind,
displayName: payload.displayName,
ownerUserId: payload.ownerUserId,
options: payload.options || {},
}),
});
}
export async function getChannelConnectorSession(sessionId: string): Promise<ConnectorSessionResponse> {
return fetchJSON(`/api/channel-connector-sessions/${encodeURIComponent(sessionId)}`);
}
export async function restartRuntime(): Promise<{ ok: boolean; restarting: boolean }> {
return fetchJSON('/api/runtime/restart', {
method: 'POST',
timeoutMs: 5000,
});
}
// ---------------------------------------------------------------------------
// 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 recheckSkillDraftSafety(skillName: string, draftId: string): Promise<SkillDraftSafetyReport> {
return fetchJSON(`/api/skills/${encodeURIComponent(skillName)}/drafts/${encodeURIComponent(draftId)}/safety`, {
method: 'POST',
body: JSON.stringify({}),
});
}
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<SkillDraft> {
return fetchJSON(`/api/skills/${encodeURIComponent(skillName)}/drafts/${encodeURIComponent(draftId)}/submit`, {
method: 'POST',
body: JSON.stringify({ notes }),
timeoutMs: SKILL_LEARNING_REQUEST_TIMEOUT_MS,
});
}
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 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 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 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}: ${parseErrorDetail(text)}`);
}
return res.json();
}
// ---------------------------------------------------------------------------
// 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,
}
);
}
// ---------------------------------------------------------------------------
// 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',
});
}
// ---------------------------------------------------------------------------
// User File System
// ---------------------------------------------------------------------------
export interface UserFileItem {
name: string;
path: string;
type: 'file' | 'directory';
size: number | null;
content_type?: string | null;
modified?: string | null;
}
export interface UserFileBrowseResult {
path: string;
items: UserFileItem[];
}
export interface UserFileContent {
name: string;
path: string;
size: number;
content_type: string;
modified: string | null;
is_binary: boolean;
is_truncated: boolean;
content: string | null;
}
export interface UserFilesStatus {
configured: boolean;
storage_mode: string;
roots: string[];
workspace_visible: boolean;
}
export async function getUserFilesStatus(): Promise<UserFilesStatus> {
return fetchJSON('/api/user-files/status');
}
export async function browseUserFiles(path: string = ''): Promise<UserFileBrowseResult> {
const params = path ? `?path=${encodeURIComponent(path)}` : '';
return fetchJSON(`/api/user-files/browse${params}`);
}
export async function getUserFile(path: string): Promise<UserFileContent> {
return fetchJSON(`/api/user-files/preview?path=${encodeURIComponent(path)}`);
}
export function getUserFileDownloadUrl(path: string): string {
return buildApiUrl(`/api/user-files/download?path=${encodeURIComponent(path)}`);
}
export async function uploadUserFile(
file: File,
dirPath: string = 'uploads',
onProgress?: (percent: number) => void
): Promise<UserFileItem> {
const locale = getCurrentAppLocale();
const formData = new FormData();
formData.append('file', file);
formData.append('path', dirPath);
return new Promise<UserFileItem>((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', buildApiUrl('/api/user-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) {
resolve(JSON.parse(xhr.responseText));
} else {
let detail = '';
try {
const data = JSON.parse(xhr.responseText);
detail = typeof data?.detail === 'string' ? data.detail : '';
} catch {
detail = '';
}
reject(new Error(detail || `${pickAppText(locale, '上传失败', 'Upload failed')}: ${xhr.status}`));
}
};
xhr.onerror = () => reject(new Error(pickAppText(locale, '上传失败', 'Upload failed')));
xhr.send(formData);
});
}
export async function deleteUserFile(path: string): Promise<void> {
await fetchJSON(`/api/user-files/delete?path=${encodeURIComponent(path)}`, {
method: 'DELETE',
});
}
export async function createUserFileDir(path: string): Promise<UserFileItem> {
return fetchJSON(`/api/user-files/mkdir?path=${encodeURIComponent(path)}`, {
method: 'POST',
});
}