添加 RuntimeContext 类用于捕获模型运行时的日期时间信息, 包括UTC时间、本地时间和时区信息,并在系统提示中显示这些信息。 同时增加最大上下文消息数和工具迭代次数的配置选项, 将验证服务从引擎加载器中移除,并更新相关的数据结构和接口。 BREAKING CHANGE: 移除了验证服务,相关字段被替换为证据状态和接受状态。 - 添加 RuntimeContext 类和相关渲染方法 - 增加 max_context_messages 和 max_tool_iterations 配置 - 移除 ValidationService 相关代码 - 更新消息记录中的验证状态字段 - 添加原始工具调用检测和回退处理
1367 lines
40 KiB
TypeScript
1367 lines
40 KiB
TypeScript
// Beaver API client - single-user direct mode.
|
||
|
||
import type {
|
||
AuthzStatus,
|
||
AuthUser,
|
||
ActiveTask,
|
||
ChatLogsResponse,
|
||
BackendTask,
|
||
ChatMessage,
|
||
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 } 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';
|
||
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;
|
||
evidence_status?: string | 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;
|
||
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 }),
|
||
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),
|
||
});
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 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<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 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}: ${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',
|
||
});
|
||
}
|