feat: 新增工具插件、系统日志、workflow配置及前端优化

1. 新增工具插件(edit_file, python_executor, search_file, shell_executor, write_file)
2. 新增系统事件日志模块和API
3. 新增workflow配置文件和详情API
4. 前端增加SSE、错误边界、设置引导等组件
5. 优化认证加密、速率限制、配置加载等工具模块
6. 删除废弃的cluster和health API
7. 补充单元测试和集成测试

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-03 07:34:43 +00:00
parent f04fef916f
commit a53ffebe0e
57 changed files with 2804 additions and 271 deletions
+130
View File
@@ -0,0 +1,130 @@
// 基于 fetch + ReadableStream 的轻量 SSE 客户端,带指数退避自动重连。
//
// 原生 EventSource 无法携带自定义 header,只能把 token 放进 URL query
// 而 token 进 URL 会被网关/浏览器历史/Referer 记录,存在泄露风险。
// 这里用 fetch 手动读取 text/event-streamtoken 走标准 Authorization header。
export interface SSEHandlers {
onOpen?: () => void;
onMessage?: (data: string) => void;
onError?: (err: unknown) => void;
// 连接断开、准备重连时回调,附带本次退避延迟(毫秒)
onReconnect?: (delayMs: number) => void;
}
export interface SSEOptions {
// 初始重连延迟(毫秒),默认 1000
baseDelayMs?: number;
// 最大重连延迟(毫秒),默认 30000
maxDelayMs?: number;
// 鉴权失败(401/403)时是否停止重连,默认 true
stopOnAuthError?: boolean;
}
export interface SSEConnection {
close: () => void;
}
const AUTH_ERROR_STATUSES = new Set([401, 403]);
export function connectSSE(
url: string,
token: string,
handlers: SSEHandlers,
options: SSEOptions = {},
): SSEConnection {
const baseDelay = options.baseDelayMs ?? 1000;
const maxDelay = options.maxDelayMs ?? 30000;
const stopOnAuthError = options.stopOnAuthError ?? true;
let controller = new AbortController();
let closed = false;
let attempt = 0;
let retryTimer: ReturnType<typeof setTimeout> | null = null;
const scheduleReconnect = () => {
if (closed) return;
// 指数退避 + 抖动,封顶 maxDelay
const backoff = Math.min(baseDelay * 2 ** attempt, maxDelay);
const delay = backoff / 2 + Math.random() * (backoff / 2);
attempt += 1;
handlers.onReconnect?.(delay);
retryTimer = setTimeout(() => {
if (closed) return;
controller = new AbortController();
void run();
}, delay);
};
const run = async () => {
try {
const resp = await fetch(url, {
method: 'GET',
headers: {
Accept: 'text/event-stream',
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
signal: controller.signal,
});
if (!resp.ok || !resp.body) {
handlers.onError?.(new Error(`SSE connect failed: ${resp.status}`));
if (stopOnAuthError && AUTH_ERROR_STATUSES.has(resp.status)) {
closed = true;
return;
}
scheduleReconnect();
return;
}
// 连接成功,重置退避计数
attempt = 0;
handlers.onOpen?.();
const reader = resp.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 });
let sep: number;
while ((sep = buffer.indexOf('\n\n')) !== -1) {
const rawEvent = buffer.slice(0, sep);
buffer = buffer.slice(sep + 2);
const data = parseEventData(rawEvent);
if (data !== null) handlers.onMessage?.(data);
}
}
// 流正常结束(服务端关闭),非主动 close 则尝试重连
if (!closed) scheduleReconnect();
} catch (err) {
if (controller.signal.aborted || closed) return;
handlers.onError?.(err);
scheduleReconnect();
}
};
void run();
return {
close: () => {
closed = true;
if (retryTimer) clearTimeout(retryTimer);
controller.abort();
},
};
}
function parseEventData(rawEvent: string): string | null {
// 只关心 data: 行,多行 data 用 \n 拼接,忽略注释(:)与其他字段
const dataLines = rawEvent
.split('\n')
.filter((line) => line.startsWith('data:'))
.map((line) => line.slice(5).replace(/^ /, ''));
if (dataLines.length === 0) return null;
return dataLines.join('\n');
}