refactor(asl): ASL frontend architecture refactoring with left navigation

- feat: Create ASLLayout component with 7-module left navigation
- feat: Implement Title Screening Settings page with optimized PICOS layout
- feat: Add placeholder pages for Workbench and Results
- fix: Fix nested routing structure for React Router v6
- fix: Resolve Spin component warning in MainLayout
- fix: Add QueryClientProvider to App.tsx
- style: Optimize PICOS form layout (P+I left, C+O+S right)
- style: Align Inclusion/Exclusion criteria side-by-side
- docs: Add architecture refactoring and routing fix reports

Ref: Week 2 Frontend Development
Scope: ASL module MVP - Title Abstract Screening
This commit is contained in:
2025-11-18 21:51:51 +08:00
parent e3e7e028e8
commit 3634933ece
213 changed files with 20054 additions and 442 deletions

View File

@@ -406,3 +406,5 @@ npm run dev
**下一步安装winston依赖开始ASL模块开发** 🚀

View File

@@ -75,3 +75,5 @@ export interface CacheAdapter {
}

View File

@@ -98,3 +98,5 @@ export class CacheFactory {
}

View File

@@ -50,3 +50,5 @@ import { CacheFactory } from './CacheFactory.js'
export const cache = CacheFactory.getInstance()

View File

@@ -25,3 +25,5 @@ export { registerHealthRoutes } from './healthCheck.js'
export type { HealthCheckResponse } from './healthCheck.js'

View File

@@ -81,3 +81,5 @@ export class JobFactory {
}

View File

@@ -88,3 +88,5 @@ export interface JobQueue {
}

View File

@@ -0,0 +1,43 @@
import { CloseAIAdapter } from './CloseAIAdapter.js';
/**
* Claude-4.5-Sonnet适配器便捷封装
*
* 通过CloseAI代理访问Anthropic Claude-4.5-Sonnet模型
*
* 模型特点:
* - 准确率93%
* - 速度:中等
* - 成本¥0.021/1K tokens
* - 适用场景:第三方仲裁、结构化输出、高质量文本生成
*
* 使用场景:
* - 双模型对比筛选DeepSeek vs GPT-5
* - 三模型共识仲裁DeepSeek + GPT-5 + Claude
* - 作为独立裁判解决冲突决策
*
* 使用示例:
* ```typescript
* import { ClaudeAdapter } from '@/common/llm/adapters';
*
* const claude = new ClaudeAdapter();
* const response = await claude.chat([
* { role: 'user', content: '作为第三方仲裁,请判断文献是否应该纳入...' }
* ]);
* ```
*
* 参考文档docs/02-通用能力层/01-LLM大模型网关/03-CloseAI集成指南.md
*/
export class ClaudeAdapter extends CloseAIAdapter {
/**
* 构造函数
* @param modelName - 模型名称,默认 'claude-sonnet-4-5-20250929'
*/
constructor(modelName: string = 'claude-sonnet-4-5-20250929') {
super('claude', modelName);
console.log(`[ClaudeAdapter] 初始化完成,模型: ${modelName}`);
}
}

View File

@@ -0,0 +1,344 @@
import axios from 'axios';
import { ILLMAdapter, Message, LLMOptions, LLMResponse, StreamChunk } from './types.js';
import { config } from '../../../config/env.js';
/**
* CloseAI通用适配器
*
* 支持通过CloseAI代理访问
* - OpenAI GPT-5-Pro
* - Anthropic Claude-4.5-Sonnet
*
* 设计原则:
* - CloseAI提供OpenAI兼容的统一接口
* - 通过不同的Base URL区分供应商
* - 代码逻辑完全复用OpenAI标准格式
*
* 参考文档docs/02-通用能力层/01-LLM大模型网关/03-CloseAI集成指南.md
*/
export class CloseAIAdapter implements ILLMAdapter {
modelName: string;
private apiKey: string;
private baseURL: string;
private provider: 'openai' | 'claude';
/**
* 构造函数
* @param provider - 供应商类型:'openai' 或 'claude'
* @param modelName - 模型名称(如 'gpt-5-pro' 或 'claude-sonnet-4-5-20250929'
*/
constructor(provider: 'openai' | 'claude', modelName: string) {
this.provider = provider;
this.modelName = modelName;
this.apiKey = config.closeaiApiKey || '';
// 根据供应商选择对应的Base URL
this.baseURL = provider === 'openai'
? config.closeaiOpenaiBaseUrl // https://api.openai-proxy.org/v1
: config.closeaiClaudeBaseUrl; // https://api.openai-proxy.org/anthropic
// 验证API Key配置
if (!this.apiKey) {
throw new Error(
'CloseAI API key is not configured. Please set CLOSEAI_API_KEY in .env file.'
);
}
console.log(`[CloseAIAdapter] 初始化完成`, {
provider: this.provider,
model: this.modelName,
baseURL: this.baseURL,
});
}
/**
* 非流式调用
* - OpenAI系列使用chat.completions格式
* - Claude系列使用messages格式Anthropic SDK
*/
async chat(messages: Message[], options?: LLMOptions): Promise<LLMResponse> {
try {
// Claude使用不同的API格式
if (this.provider === 'claude') {
return await this.chatClaude(messages, options);
}
// OpenAI系列标准格式不包含temperature等可能不支持的参数
const requestBody: any = {
model: this.modelName,
messages: messages,
max_tokens: options?.maxTokens ?? 2000,
};
// 可选参数:只在提供时才添加
if (options?.temperature !== undefined) {
requestBody.temperature = options.temperature;
}
if (options?.topP !== undefined) {
requestBody.top_p = options.topP;
}
console.log(`[CloseAIAdapter] 发起非流式调用`, {
provider: this.provider,
model: this.modelName,
messagesCount: messages.length,
params: Object.keys(requestBody),
});
const response = await axios.post(
`${this.baseURL}/chat/completions`,
requestBody,
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.apiKey}`,
},
timeout: 180000, // 180秒超时3分钟- GPT-5和Claude可能需要更长时间
}
);
const choice = response.data.choices[0];
const result: LLMResponse = {
content: choice.message.content,
model: response.data.model,
usage: {
promptTokens: response.data.usage.prompt_tokens,
completionTokens: response.data.usage.completion_tokens,
totalTokens: response.data.usage.total_tokens,
},
finishReason: choice.finish_reason,
};
console.log(`[CloseAIAdapter] 调用成功`, {
provider: this.provider,
model: result.model,
tokens: result.usage?.totalTokens,
contentLength: result.content.length,
});
return result;
} catch (error: unknown) {
console.error(`[CloseAIAdapter] ${this.provider.toUpperCase()} API Error:`, error);
if (axios.isAxiosError(error)) {
const errorMessage = error.response?.data?.error?.message || error.message;
const statusCode = error.response?.status;
// 提供更友好的错误信息
if (statusCode === 401) {
throw new Error(
`CloseAI认证失败: API Key无效或已过期。请检查 CLOSEAI_API_KEY 配置。`
);
} else if (statusCode === 429) {
throw new Error(
`CloseAI速率限制: 请求过于频繁,请稍后重试。`
);
} else if (statusCode === 500 || statusCode === 502 || statusCode === 503) {
throw new Error(
`CloseAI服务异常: 代理服务暂时不可用,请稍后重试。`
);
}
throw new Error(
`CloseAI (${this.provider.toUpperCase()}) API调用失败: ${errorMessage}`
);
}
throw error;
}
}
/**
* Claude专用调用方法
* 使用Anthropic Messages API格式
*/
private async chatClaude(messages: Message[], options?: LLMOptions): Promise<LLMResponse> {
try {
const requestBody = {
model: this.modelName,
messages: messages,
max_tokens: options?.maxTokens ?? 2000,
};
console.log(`[CloseAIAdapter] 发起Claude调用`, {
model: this.modelName,
messagesCount: messages.length,
});
const response = await axios.post(
`${this.baseURL}/v1/messages`, // Anthropic使用 /v1/messages
requestBody,
{
headers: {
'Content-Type': 'application/json',
'x-api-key': this.apiKey, // Anthropic使用 x-api-key 而不是 Authorization
'anthropic-version': '2023-06-01', // Anthropic需要版本号
},
timeout: 180000,
}
);
// Anthropic的响应格式不同
const content = response.data.content[0].text;
const result: LLMResponse = {
content: content,
model: response.data.model,
usage: {
promptTokens: response.data.usage.input_tokens,
completionTokens: response.data.usage.output_tokens,
totalTokens: response.data.usage.input_tokens + response.data.usage.output_tokens,
},
finishReason: response.data.stop_reason,
};
console.log(`[CloseAIAdapter] Claude调用成功`, {
model: result.model,
tokens: result.usage?.totalTokens,
contentLength: result.content.length,
});
return result;
} catch (error: unknown) {
console.error(`[CloseAIAdapter] Claude API Error:`, error);
if (axios.isAxiosError(error)) {
const errorMessage = error.response?.data?.error?.message || error.message;
throw new Error(
`CloseAI (Claude) API调用失败: ${errorMessage}`
);
}
throw error;
}
}
/**
* 流式调用
* - OpenAI系列使用SSE格式
* - Claude系列暂不支持可后续实现
*/
async *chatStream(
messages: Message[],
options?: LLMOptions,
onChunk?: (chunk: StreamChunk) => void
): AsyncGenerator<StreamChunk, void, unknown> {
// Claude流式调用暂不支持
if (this.provider === 'claude') {
throw new Error('Claude流式调用暂未实现请使用非流式调用');
}
try {
// OpenAI系列标准SSE格式
const requestBody: any = {
model: this.modelName,
messages: messages,
max_tokens: options?.maxTokens ?? 2000,
stream: true,
};
// 可选参数:只在提供时才添加
if (options?.temperature !== undefined) {
requestBody.temperature = options.temperature;
}
if (options?.topP !== undefined) {
requestBody.top_p = options.topP;
}
console.log(`[CloseAIAdapter] 发起流式调用`, {
provider: this.provider,
model: this.modelName,
messagesCount: messages.length,
});
const response = await axios.post(
`${this.baseURL}/chat/completions`,
requestBody,
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.apiKey}`,
},
responseType: 'stream',
timeout: 180000, // 180秒超时
}
);
const stream = response.data;
let buffer = '';
let chunkCount = 0;
for await (const chunk of stream) {
buffer += chunk.toString();
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
const trimmedLine = line.trim();
// 跳过空行和结束标记
if (!trimmedLine || trimmedLine === 'data: [DONE]') {
continue;
}
// 解析SSE数据
if (trimmedLine.startsWith('data: ')) {
try {
const jsonStr = trimmedLine.slice(6);
const data = JSON.parse(jsonStr);
const choice = data.choices[0];
const content = choice.delta?.content || '';
const streamChunk: StreamChunk = {
content: content,
done: choice.finish_reason === 'stop',
model: data.model,
};
// 如果流结束附加usage信息
if (choice.finish_reason === 'stop' && data.usage) {
streamChunk.usage = {
promptTokens: data.usage.prompt_tokens,
completionTokens: data.usage.completion_tokens,
totalTokens: data.usage.total_tokens,
};
}
chunkCount++;
// 回调函数(可选)
if (onChunk) {
onChunk(streamChunk);
}
yield streamChunk;
} catch (parseError) {
console.error('[CloseAIAdapter] Failed to parse SSE data:', parseError);
// 继续处理下一个chunk不中断流
}
}
}
}
console.log(`[CloseAIAdapter] 流式调用完成`, {
provider: this.provider,
model: this.modelName,
chunksReceived: chunkCount,
});
} catch (error) {
console.error(`[CloseAIAdapter] ${this.provider.toUpperCase()} Stream Error:`, error);
if (axios.isAxiosError(error)) {
const errorMessage = error.response?.data?.error?.message || error.message;
throw new Error(
`CloseAI (${this.provider.toUpperCase()}) 流式调用失败: ${errorMessage}`
);
}
throw error;
}
}
}

View File

@@ -0,0 +1,41 @@
import { CloseAIAdapter } from './CloseAIAdapter.js';
/**
* GPT-4o适配器便捷封装
*
* 通过CloseAI代理访问OpenAI GPT-4o模型
*
* 模型特点:
* - 准确率与GPT-4同级
* - 速度1-2秒响应
* - 成本:适中
* - 适用场景:高质量文献筛选、复杂推理、结构化输出
*
* 性能对比:
* - gpt-4o: 1.5秒(推荐)✅
* - gpt-4o-mini: 0.7秒(经济版)
* - gpt-5-pro: 50秒CloseAI平台上过慢不推荐
*
* 使用示例:
* ```typescript
* import { GPT5Adapter } from '@/common/llm/adapters';
*
* const gpt = new GPT5Adapter(); // 默认使用 gpt-4o
* const response = await gpt.chat([
* { role: 'user', content: '根据PICO标准筛选文献...' }
* ]);
* ```
*
* 参考文档docs/02-通用能力层/01-LLM大模型网关/03-CloseAI集成指南.md
*/
export class GPT5Adapter extends CloseAIAdapter {
/**
* 构造函数
* @param modelName - 模型名称,默认 'gpt-4o'(经过性能测试优化)
*/
constructor(modelName: string = 'gpt-4o') {
super('openai', modelName);
console.log(`[GPT5Adapter] 初始化完成,模型: ${modelName}`);
}
}

View File

@@ -1,6 +1,8 @@
import { ILLMAdapter, ModelType } from './types.js';
import { DeepSeekAdapter } from './DeepSeekAdapter.js';
import { QwenAdapter } from './QwenAdapter.js';
import { GPT5Adapter } from './GPT5Adapter.js';
import { ClaudeAdapter } from './ClaudeAdapter.js';
/**
* LLM工厂类
@@ -29,13 +31,21 @@ export class LLMFactory {
break;
case 'qwen3-72b':
adapter = new QwenAdapter('qwen-plus'); // Qwen3-72B对应的模型
adapter = new QwenAdapter('qwen-max'); // ⭐ 使用 qwen-maxQwen最新最强模型
break;
case 'qwen-long':
adapter = new QwenAdapter('qwen-long'); // 1M上下文超长文本模型
break;
case 'gpt-5':
adapter = new GPT5Adapter(); // ⭐ 通过CloseAI代理默认使用 gpt-5-pro
break;
case 'claude-4.5':
adapter = new ClaudeAdapter('claude-sonnet-4-5-20250929'); // ⭐ 通过CloseAI代理
break;
case 'gemini-pro':
// TODO: 实现Gemini适配器
throw new Error('Gemini adapter is not implemented yet');
@@ -67,7 +77,7 @@ export class LLMFactory {
* @returns 是否支持
*/
static isSupported(modelType: string): boolean {
return ['deepseek-v3', 'qwen3-72b', 'qwen-long', 'gemini-pro'].includes(modelType);
return ['deepseek-v3', 'qwen3-72b', 'qwen-long', 'gpt-5', 'claude-4.5', 'gemini-pro'].includes(modelType);
}
/**
@@ -75,7 +85,7 @@ export class LLMFactory {
* @returns 支持的模型列表
*/
static getSupportedModels(): ModelType[] {
return ['deepseek-v3', 'qwen3-72b', 'qwen-long', 'gemini-pro'];
return ['deepseek-v3', 'qwen3-72b', 'qwen-long', 'gpt-5', 'claude-4.5', 'gemini-pro'];
}
}

View File

@@ -51,7 +51,13 @@ export interface ILLMAdapter {
}
// 支持的模型类型
export type ModelType = 'deepseek-v3' | 'qwen3-72b' | 'qwen-long' | 'gemini-pro';
export type ModelType =
| 'deepseek-v3' // DeepSeek-V3直连
| 'qwen3-72b' // Qwen3-72B阿里云
| 'qwen-long' // Qwen-Long 1M上下文阿里云
| 'gpt-5' // GPT-5-ProCloseAI代理⭐ 新增
| 'claude-4.5' // Claude-4.5-SonnetCloseAI代理⭐ 新增
| 'gemini-pro'; // Gemini-Pro预留

View File

@@ -36,3 +36,5 @@ export {
export { default } from './logger.js'

View File

@@ -39,3 +39,5 @@
export { Metrics, requestTimingHook, responseTimingHook } from './metrics.js'

View File

@@ -65,3 +65,5 @@ export interface StorageAdapter {
}

View File

@@ -20,10 +20,37 @@ export interface ParseResult<T = any> {
* 3. 带后缀:{ "key": "value" }\n\n以上是提取结果
* 4. 代码块:```json\n{ "key": "value" }\n```
*/
/**
* 清理JSON字符串修复常见格式问题
* @param text - 原始文本
* @returns 清理后的文本
*/
function cleanJSONString(text: string): string {
let cleaned = text;
// 1. 替换中文引号为ASCII引号国际模型常见问题
cleaned = cleaned.replace(/"/g, '"'); // 中文左引号
cleaned = cleaned.replace(/"/g, '"'); // 中文右引号
cleaned = cleaned.replace(/'/g, "'"); // 中文左单引号
cleaned = cleaned.replace(/'/g, "'"); // 中文右单引号
// 2. 替换全角逗号、冒号为半角
cleaned = cleaned.replace(//g, ',');
cleaned = cleaned.replace(//g, ':');
// 3. 移除零宽字符和不可见字符
cleaned = cleaned.replace(/[\u200B-\u200D\uFEFF]/g, '');
return cleaned;
}
export function extractJSON(text: string): string | null {
// 预处理:清理常见格式问题
const cleanedText = cleanJSONString(text);
// 尝试1直接查找 {...} 或 [...]
const jsonPattern = /(\{[\s\S]*\}|\[[\s\S]*\])/;
const match = text.match(jsonPattern);
const match = cleanedText.match(jsonPattern);
if (match) {
return match[1];
@@ -31,7 +58,7 @@ export function extractJSON(text: string): string | null {
// 尝试2查找代码块中的JSON
const codeBlockPattern = /```(?:json)?\s*\n?([\s\S]*?)\n?```/;
const codeMatch = text.match(codeBlockPattern);
const codeMatch = cleanedText.match(codeBlockPattern);
if (codeMatch) {
return codeMatch[1].trim();