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:
@@ -406,3 +406,5 @@ npm run dev
|
||||
**下一步:安装winston依赖,开始ASL模块开发!** 🚀
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
2
backend/src/common/cache/CacheAdapter.ts
vendored
2
backend/src/common/cache/CacheAdapter.ts
vendored
@@ -75,3 +75,5 @@ export interface CacheAdapter {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
2
backend/src/common/cache/CacheFactory.ts
vendored
2
backend/src/common/cache/CacheFactory.ts
vendored
@@ -98,3 +98,5 @@ export class CacheFactory {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
2
backend/src/common/cache/index.ts
vendored
2
backend/src/common/cache/index.ts
vendored
@@ -50,3 +50,5 @@ import { CacheFactory } from './CacheFactory.js'
|
||||
export const cache = CacheFactory.getInstance()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -25,3 +25,5 @@ export { registerHealthRoutes } from './healthCheck.js'
|
||||
export type { HealthCheckResponse } from './healthCheck.js'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -81,3 +81,5 @@ export class JobFactory {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -88,3 +88,5 @@ export interface JobQueue {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
43
backend/src/common/llm/adapters/ClaudeAdapter.ts
Normal file
43
backend/src/common/llm/adapters/ClaudeAdapter.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
344
backend/src/common/llm/adapters/CloseAIAdapter.ts
Normal file
344
backend/src/common/llm/adapters/CloseAIAdapter.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
41
backend/src/common/llm/adapters/GPT5Adapter.ts
Normal file
41
backend/src/common/llm/adapters/GPT5Adapter.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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-max(Qwen最新最强模型)
|
||||
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'];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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-Pro(CloseAI代理)⭐ 新增
|
||||
| 'claude-4.5' // Claude-4.5-Sonnet(CloseAI代理)⭐ 新增
|
||||
| 'gemini-pro'; // Gemini-Pro(预留)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -36,3 +36,5 @@ export {
|
||||
export { default } from './logger.js'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -39,3 +39,5 @@
|
||||
export { Metrics, requestTimingHook, responseTimingHook } from './metrics.js'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -65,3 +65,5 @@ export interface StorageAdapter {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user