feat(app): 移除内置agents并添加CORS支持和技能上传优化
移除了agents/registry.json中的所有内置agents配置,将agents数组清空。 为web应用添加了CORS中间件支持,允许指定的前端地址跨域访问。 重构了技能上传功能,增加了LLM重写机制,自动规范化上传的技能格式。 新增了工具名称提取逻辑,从技能正文中自动识别Required Tools段落。 更新了技能学习候选者和草稿的载荷结构,添加评估报告统计信息。 修改了意图路由技能的说明,改进任务状态管理逻辑。
This commit is contained in:
@ -58,6 +58,7 @@ 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 = 45000;
|
||||
const SKILL_LEARNING_REQUEST_TIMEOUT_MS = 120000;
|
||||
@ -117,6 +118,34 @@ 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
|
||||
@ -163,6 +192,7 @@ 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 {
|
||||
@ -215,16 +245,11 @@ async function fetchJSON<T>(path: string, options?: FetchJsonOptions): Promise<T
|
||||
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}`);
|
||||
const detail = parseErrorDetail(text);
|
||||
throw new ApiError(`${pickAppText(locale, '接口错误', 'API error')} ${res.status}: ${detail}`, {
|
||||
status: res.status,
|
||||
detail,
|
||||
});
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
@ -1216,7 +1241,7 @@ export async function uploadSkill(file: File): Promise<Skill> {
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(`接口错误 ${res.status}: ${text}`);
|
||||
throw new Error(`接口错误 ${res.status}: ${parseErrorDetail(text)}`);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
8
app-instance/frontend/lib/user-file-paths.ts
Normal file
8
app-instance/frontend/lib/user-file-paths.ts
Normal file
@ -0,0 +1,8 @@
|
||||
const USER_FILE_MUTABLE_ROOTS = new Set(['uploads', 'outputs', 'shared', 'tasks']);
|
||||
|
||||
export function canMutateUserFilesPath(path: string): boolean {
|
||||
const cleaned = path.trim().replace(/^\/+|\/+$/g, '');
|
||||
if (!cleaned) return false;
|
||||
const [root] = cleaned.split('/');
|
||||
return USER_FILE_MUTABLE_ROOTS.has(root);
|
||||
}
|
||||
@ -3,9 +3,23 @@ import { resolve } from 'node:path';
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { canMutateUserFilesPath } from './user-file-paths';
|
||||
|
||||
const root = resolve(__dirname, '..');
|
||||
|
||||
describe('user file system frontend wiring', () => {
|
||||
it('only enables mutating file actions inside concrete user-file roots', () => {
|
||||
expect(canMutateUserFilesPath('')).toBe(false);
|
||||
expect(canMutateUserFilesPath('/')).toBe(false);
|
||||
expect(canMutateUserFilesPath('qa-folder')).toBe(false);
|
||||
|
||||
expect(canMutateUserFilesPath('uploads')).toBe(true);
|
||||
expect(canMutateUserFilesPath('uploads/qa-folder')).toBe(true);
|
||||
expect(canMutateUserFilesPath('outputs/report.md')).toBe(true);
|
||||
expect(canMutateUserFilesPath('shared')).toBe(true);
|
||||
expect(canMutateUserFilesPath('tasks/task-1')).toBe(true);
|
||||
});
|
||||
|
||||
it('routes API client helpers to user file endpoints', () => {
|
||||
const apiSource = readFileSync(resolve(root, 'lib/api.ts'), 'utf8');
|
||||
|
||||
@ -17,6 +31,13 @@ describe('user file system frontend wiring', () => {
|
||||
expect(apiSource).toContain('/api/user-files/mkdir');
|
||||
});
|
||||
|
||||
it('notifies the app shell when API auth is cleared', () => {
|
||||
const apiSource = readFileSync(resolve(root, 'lib/api.ts'), 'utf8');
|
||||
|
||||
expect(apiSource).toContain('AUTH_CLEARED_EVENT');
|
||||
expect(apiSource).toContain("window.dispatchEvent(new CustomEvent(AUTH_CLEARED_EVENT))");
|
||||
});
|
||||
|
||||
it('does not wire the Files page to workspace or MinIO management APIs', () => {
|
||||
const pageSource = readFileSync(resolve(root, 'app/(app)/files/page.tsx'), 'utf8');
|
||||
|
||||
@ -29,4 +50,18 @@ describe('user file system frontend wiring', () => {
|
||||
expect(pageSource).not.toContain('accessKey');
|
||||
expect(pageSource).not.toContain('secretKey');
|
||||
});
|
||||
|
||||
it('does not retry user-file loads after an auth failure', () => {
|
||||
const pageSource = readFileSync(resolve(root, 'app/(app)/files/page.tsx'), 'utf8');
|
||||
|
||||
expect(pageSource).toContain('isAuthError');
|
||||
expect(pageSource).toContain('if (isAuthError(err))');
|
||||
});
|
||||
|
||||
it('shows backend upload error details instead of raw JSON payloads', () => {
|
||||
const apiSource = readFileSync(resolve(root, 'lib/api.ts'), 'utf8');
|
||||
|
||||
expect(apiSource).toContain('function parseErrorDetail');
|
||||
expect(apiSource).toContain('throw new Error(`接口错误 ${res.status}: ${parseErrorDetail(text)}`)');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user