repowiki-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analyzer/api-analyzer.d.ts +22 -0
- package/dist/analyzer/api-analyzer.d.ts.map +1 -0
- package/dist/analyzer/api-analyzer.js +272 -0
- package/dist/analyzer/api-analyzer.js.map +1 -0
- package/dist/analyzer/config-analyzer.d.ts +18 -0
- package/dist/analyzer/config-analyzer.d.ts.map +1 -0
- package/dist/analyzer/config-analyzer.js +200 -0
- package/dist/analyzer/config-analyzer.js.map +1 -0
- package/dist/analyzer/database-analyzer.d.ts +24 -0
- package/dist/analyzer/database-analyzer.d.ts.map +1 -0
- package/dist/analyzer/database-analyzer.js +391 -0
- package/dist/analyzer/database-analyzer.js.map +1 -0
- package/dist/analyzer/index.d.ts +10 -0
- package/dist/analyzer/index.d.ts.map +1 -0
- package/dist/analyzer/index.js +10 -0
- package/dist/analyzer/index.js.map +1 -0
- package/dist/analyzer/module-analyzer.d.ts +20 -0
- package/dist/analyzer/module-analyzer.d.ts.map +1 -0
- package/dist/analyzer/module-analyzer.js +252 -0
- package/dist/analyzer/module-analyzer.js.map +1 -0
- package/dist/analyzer/workflow-analyzer.d.ts +19 -0
- package/dist/analyzer/workflow-analyzer.d.ts.map +1 -0
- package/dist/analyzer/workflow-analyzer.js +165 -0
- package/dist/analyzer/workflow-analyzer.js.map +1 -0
- package/dist/detector/dependency-detector.d.ts +50 -0
- package/dist/detector/dependency-detector.d.ts.map +1 -0
- package/dist/detector/dependency-detector.js +326 -0
- package/dist/detector/dependency-detector.js.map +1 -0
- package/dist/detector/entrypoint-detector.d.ts +30 -0
- package/dist/detector/entrypoint-detector.d.ts.map +1 -0
- package/dist/detector/entrypoint-detector.js +240 -0
- package/dist/detector/entrypoint-detector.js.map +1 -0
- package/dist/detector/index.d.ts +10 -0
- package/dist/detector/index.d.ts.map +1 -0
- package/dist/detector/index.js +10 -0
- package/dist/detector/index.js.map +1 -0
- package/dist/detector/tech-stack-detector.d.ts +41 -0
- package/dist/detector/tech-stack-detector.d.ts.map +1 -0
- package/dist/detector/tech-stack-detector.js +300 -0
- package/dist/detector/tech-stack-detector.js.map +1 -0
- package/dist/generator/index.d.ts +9 -0
- package/dist/generator/index.d.ts.map +1 -0
- package/dist/generator/index.js +9 -0
- package/dist/generator/index.js.map +1 -0
- package/dist/generator/markdown-generator.d.ts +71 -0
- package/dist/generator/markdown-generator.d.ts.map +1 -0
- package/dist/generator/markdown-generator.js +235 -0
- package/dist/generator/markdown-generator.js.map +1 -0
- package/dist/generator/mermaid-generator.d.ts +30 -0
- package/dist/generator/mermaid-generator.d.ts.map +1 -0
- package/dist/generator/mermaid-generator.js +297 -0
- package/dist/generator/mermaid-generator.js.map +1 -0
- package/dist/generator/sidebar-generator.d.ts +10 -0
- package/dist/generator/sidebar-generator.d.ts.map +1 -0
- package/dist/generator/sidebar-generator.js +120 -0
- package/dist/generator/sidebar-generator.js.map +1 -0
- package/dist/generator/wiki-generator.d.ts +45 -0
- package/dist/generator/wiki-generator.d.ts.map +1 -0
- package/dist/generator/wiki-generator.js +217 -0
- package/dist/generator/wiki-generator.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/llm/auth-manager.d.ts +50 -0
- package/dist/llm/auth-manager.d.ts.map +1 -0
- package/dist/llm/auth-manager.js +172 -0
- package/dist/llm/auth-manager.js.map +1 -0
- package/dist/llm/index.d.ts +10 -0
- package/dist/llm/index.d.ts.map +1 -0
- package/dist/llm/index.js +9 -0
- package/dist/llm/index.js.map +1 -0
- package/dist/llm/llm-client.d.ts +132 -0
- package/dist/llm/llm-client.d.ts.map +1 -0
- package/dist/llm/llm-client.js +308 -0
- package/dist/llm/llm-client.js.map +1 -0
- package/dist/llm/prompt-manager.d.ts +67 -0
- package/dist/llm/prompt-manager.d.ts.map +1 -0
- package/dist/llm/prompt-manager.js +283 -0
- package/dist/llm/prompt-manager.js.map +1 -0
- package/dist/models/analysis-result.d.ts +425 -0
- package/dist/models/analysis-result.d.ts.map +1 -0
- package/dist/models/analysis-result.js +34 -0
- package/dist/models/analysis-result.js.map +1 -0
- package/dist/models/analysis-types.d.ts +223 -0
- package/dist/models/analysis-types.d.ts.map +1 -0
- package/dist/models/analysis-types.js +95 -0
- package/dist/models/analysis-types.js.map +1 -0
- package/dist/models/file-reference.d.ts +62 -0
- package/dist/models/file-reference.d.ts.map +1 -0
- package/dist/models/file-reference.js +34 -0
- package/dist/models/file-reference.js.map +1 -0
- package/dist/models/index.d.ts +10 -0
- package/dist/models/index.d.ts.map +1 -0
- package/dist/models/index.js +10 -0
- package/dist/models/index.js.map +1 -0
- package/dist/models/project-profile.d.ts +48 -0
- package/dist/models/project-profile.d.ts.map +1 -0
- package/dist/models/project-profile.js +26 -0
- package/dist/models/project-profile.js.map +1 -0
- package/dist/models/wiki-page.d.ts +57 -0
- package/dist/models/wiki-page.d.ts.map +1 -0
- package/dist/models/wiki-page.js +19 -0
- package/dist/models/wiki-page.js.map +1 -0
- package/dist/pipeline.d.ts +30 -0
- package/dist/pipeline.d.ts.map +1 -0
- package/dist/pipeline.js +159 -0
- package/dist/pipeline.js.map +1 -0
- package/dist/scanner/file-scanner.d.ts +27 -0
- package/dist/scanner/file-scanner.d.ts.map +1 -0
- package/dist/scanner/file-scanner.js +149 -0
- package/dist/scanner/file-scanner.js.map +1 -0
- package/dist/scanner/ignore-rules.d.ts +31 -0
- package/dist/scanner/ignore-rules.d.ts.map +1 -0
- package/dist/scanner/ignore-rules.js +98 -0
- package/dist/scanner/ignore-rules.js.map +1 -0
- package/dist/scanner/index.d.ts +8 -0
- package/dist/scanner/index.d.ts.map +1 -0
- package/dist/scanner/index.js +8 -0
- package/dist/scanner/index.js.map +1 -0
- package/dist/scanner/tree-builder.d.ts +20 -0
- package/dist/scanner/tree-builder.d.ts.map +1 -0
- package/dist/scanner/tree-builder.js +118 -0
- package/dist/scanner/tree-builder.js.map +1 -0
- package/package.json +34 -0
- package/src/analyzer/api-analyzer.ts +324 -0
- package/src/analyzer/config-analyzer.ts +209 -0
- package/src/analyzer/database-analyzer.ts +468 -0
- package/src/analyzer/index.ts +26 -0
- package/src/analyzer/module-analyzer.ts +308 -0
- package/src/analyzer/workflow-analyzer.ts +190 -0
- package/src/detector/dependency-detector.ts +390 -0
- package/src/detector/entrypoint-detector.ts +270 -0
- package/src/detector/index.ts +21 -0
- package/src/detector/tech-stack-detector.ts +377 -0
- package/src/generator/index.ts +36 -0
- package/src/generator/markdown-generator.ts +277 -0
- package/src/generator/mermaid-generator.ts +340 -0
- package/src/generator/sidebar-generator.ts +134 -0
- package/src/generator/wiki-generator.ts +281 -0
- package/src/index.ts +12 -0
- package/src/llm/auth-manager.ts +207 -0
- package/src/llm/index.ts +21 -0
- package/src/llm/llm-client.ts +417 -0
- package/src/llm/prompt-manager.ts +325 -0
- package/src/models/analysis-result.ts +44 -0
- package/src/models/analysis-types.ts +121 -0
- package/src/models/file-reference.ts +41 -0
- package/src/models/index.ts +44 -0
- package/src/models/project-profile.ts +29 -0
- package/src/models/wiki-page.ts +23 -0
- package/src/pipeline.ts +225 -0
- package/src/scanner/file-scanner.ts +192 -0
- package/src/scanner/ignore-rules.ts +112 -0
- package/src/scanner/index.ts +19 -0
- package/src/scanner/tree-builder.ts +156 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module llm-client
|
|
3
|
+
* @description OpenAI 兼容的 LLM API 客户端。
|
|
4
|
+
*
|
|
5
|
+
* 特性:
|
|
6
|
+
* - 使用原生 fetch (Node 18+)
|
|
7
|
+
* - 指数退避重试 (429 / 5xx)
|
|
8
|
+
* - AbortController 超时控制
|
|
9
|
+
* - chatJSON: 自动解析 JSON 响应并可选 Zod 校验
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
import type { LLMConfig } from './auth-manager.js';
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// 类型定义
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
/** 聊天消息 */
|
|
20
|
+
export interface ChatMessage {
|
|
21
|
+
role: 'system' | 'user' | 'assistant';
|
|
22
|
+
content: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** LLM 客户端选项 */
|
|
26
|
+
export interface LLMClientOptions {
|
|
27
|
+
/** LLM 服务配置 */
|
|
28
|
+
config: LLMConfig;
|
|
29
|
+
/** 最大重试次数 (默认 3) */
|
|
30
|
+
maxRetries?: number;
|
|
31
|
+
/** 基础重试延迟,毫秒 (默认 1000) */
|
|
32
|
+
retryDelayMs?: number;
|
|
33
|
+
/** 请求超时,毫秒 (默认 120000,即 2 分钟) */
|
|
34
|
+
timeoutMs?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** LLM 响应 */
|
|
38
|
+
export interface LLMResponse {
|
|
39
|
+
/** 模型回复的文本内容 */
|
|
40
|
+
content: string;
|
|
41
|
+
/** Token 使用统计 */
|
|
42
|
+
usage?: {
|
|
43
|
+
promptTokens: number;
|
|
44
|
+
completionTokens: number;
|
|
45
|
+
totalTokens: number;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** chat 方法可选参数 */
|
|
50
|
+
export interface ChatOptions {
|
|
51
|
+
/** 温度,控制随机性 (默认 0.3) */
|
|
52
|
+
temperature?: number;
|
|
53
|
+
/** 最大生成 token 数 */
|
|
54
|
+
maxTokens?: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ============================================================================
|
|
58
|
+
// 错误类
|
|
59
|
+
// ============================================================================
|
|
60
|
+
|
|
61
|
+
/** LLM 请求错误基类 */
|
|
62
|
+
export class LLMError extends Error {
|
|
63
|
+
constructor(
|
|
64
|
+
message: string,
|
|
65
|
+
public readonly statusCode?: number,
|
|
66
|
+
public readonly responseBody?: string,
|
|
67
|
+
) {
|
|
68
|
+
super(message);
|
|
69
|
+
this.name = 'LLMError';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 认证错误 (401 / 403) */
|
|
74
|
+
export class LLMAuthError extends LLMError {
|
|
75
|
+
constructor(message: string, statusCode: number, responseBody?: string) {
|
|
76
|
+
super(message, statusCode, responseBody);
|
|
77
|
+
this.name = 'LLMAuthError';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 速率限制错误 (429) */
|
|
82
|
+
export class LLMRateLimitError extends LLMError {
|
|
83
|
+
constructor(message: string, responseBody?: string) {
|
|
84
|
+
super(message, 429, responseBody);
|
|
85
|
+
this.name = 'LLMRateLimitError';
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** JSON 解析错误 */
|
|
90
|
+
export class LLMJsonParseError extends LLMError {
|
|
91
|
+
constructor(
|
|
92
|
+
message: string,
|
|
93
|
+
public readonly rawContent: string,
|
|
94
|
+
) {
|
|
95
|
+
super(message);
|
|
96
|
+
this.name = 'LLMJsonParseError';
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ============================================================================
|
|
101
|
+
// 内部工具函数
|
|
102
|
+
// ============================================================================
|
|
103
|
+
|
|
104
|
+
/** 可重试的 HTTP 状态码 */
|
|
105
|
+
const RETRYABLE_STATUS_CODES = new Set([429, 500, 502, 503]);
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 延迟指定毫秒
|
|
109
|
+
*/
|
|
110
|
+
function sleep(ms: number): Promise<void> {
|
|
111
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 尝试从 Markdown 代码块中提取 JSON 字符串。
|
|
116
|
+
* 匹配 ```json ... ``` 或 ``` ... ``` 格式。
|
|
117
|
+
*/
|
|
118
|
+
function extractJsonFromMarkdown(text: string): string | null {
|
|
119
|
+
// 匹配 ```json\n...\n``` 或 ```\n...\n```
|
|
120
|
+
const fencePattern = /```(?:json)?\s*\n([\s\S]*?)\n\s*```/;
|
|
121
|
+
const match = text.match(fencePattern);
|
|
122
|
+
if (match?.[1]) {
|
|
123
|
+
return match[1].trim();
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 尝试解析 JSON 字符串,失败时尝试从 Markdown 代码块中提取再解析。
|
|
130
|
+
*/
|
|
131
|
+
function parseJsonSafe(text: string): unknown {
|
|
132
|
+
// 首先直接尝试解析
|
|
133
|
+
try {
|
|
134
|
+
return JSON.parse(text);
|
|
135
|
+
} catch {
|
|
136
|
+
// 尝试从代码块提取
|
|
137
|
+
const extracted = extractJsonFromMarkdown(text);
|
|
138
|
+
if (extracted) {
|
|
139
|
+
return JSON.parse(extracted);
|
|
140
|
+
}
|
|
141
|
+
throw new Error(`无法解析 JSON 内容`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ============================================================================
|
|
146
|
+
// LLMClient 类
|
|
147
|
+
// ============================================================================
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* OpenAI 兼容的 LLM 客户端。
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* ```ts
|
|
154
|
+
* const client = new LLMClient({ config });
|
|
155
|
+
* const response = await client.chat([
|
|
156
|
+
* { role: 'system', content: '你是一个助手。' },
|
|
157
|
+
* { role: 'user', content: '你好' },
|
|
158
|
+
* ]);
|
|
159
|
+
* console.log(response.content);
|
|
160
|
+
* ```
|
|
161
|
+
*/
|
|
162
|
+
export class LLMClient {
|
|
163
|
+
private readonly config: LLMConfig;
|
|
164
|
+
private readonly maxRetries: number;
|
|
165
|
+
private readonly retryDelayMs: number;
|
|
166
|
+
private readonly timeoutMs: number;
|
|
167
|
+
|
|
168
|
+
constructor(options: LLMClientOptions) {
|
|
169
|
+
this.config = options.config;
|
|
170
|
+
this.maxRetries = options.maxRetries ?? 3;
|
|
171
|
+
this.retryDelayMs = options.retryDelayMs ?? 1000;
|
|
172
|
+
this.timeoutMs = options.timeoutMs ?? 120_000;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 发送聊天补全请求。
|
|
177
|
+
*
|
|
178
|
+
* @param messages - 聊天消息列表
|
|
179
|
+
* @param options - 可选参数(温度、最大 token 数)
|
|
180
|
+
* @returns LLM 响应
|
|
181
|
+
* @throws {LLMAuthError} 认证失败
|
|
182
|
+
* @throws {LLMRateLimitError} 速率限制(重试耗尽后)
|
|
183
|
+
* @throws {LLMError} 其他请求错误
|
|
184
|
+
*/
|
|
185
|
+
async chat(messages: ChatMessage[], options?: ChatOptions): Promise<LLMResponse> {
|
|
186
|
+
const url = `${this.config.apiEndpoint.replace(/\/+$/, '')}/chat/completions`;
|
|
187
|
+
const body = this.buildRequestBody(messages, options);
|
|
188
|
+
|
|
189
|
+
let lastError: Error | null = null;
|
|
190
|
+
|
|
191
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
192
|
+
// 非首次请求时等待(指数退避)
|
|
193
|
+
if (attempt > 0) {
|
|
194
|
+
const delay = this.retryDelayMs * Math.pow(2, attempt - 1);
|
|
195
|
+
await sleep(delay);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
const response = await this.fetchWithTimeout(url, body);
|
|
200
|
+
return this.parseResponse(response);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
lastError = error as Error;
|
|
203
|
+
|
|
204
|
+
// 认证错误不重试
|
|
205
|
+
if (error instanceof LLMAuthError) {
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 非可重试错误不重试
|
|
210
|
+
if (error instanceof LLMError && error.statusCode && !RETRYABLE_STATUS_CODES.has(error.statusCode)) {
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// 最后一次重试仍然失败
|
|
215
|
+
if (attempt === this.maxRetries) {
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// 重试耗尽
|
|
222
|
+
throw lastError ?? new LLMError('LLM 请求失败: 未知错误');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* 发送聊天请求并将响应解析为 JSON。
|
|
227
|
+
*
|
|
228
|
+
* 工作流程:
|
|
229
|
+
* 1. 调用 chat() 获取文本响应
|
|
230
|
+
* 2. 尝试解析 JSON(支持从 Markdown 代码块中提取)
|
|
231
|
+
* 3. 若提供了 Zod schema,进行运行时校验
|
|
232
|
+
* 4. 若 JSON 解析失败,追加修正提示后重试一次
|
|
233
|
+
*
|
|
234
|
+
* @param messages - 聊天消息列表
|
|
235
|
+
* @param schema - 可选的 Zod 校验 schema
|
|
236
|
+
* @returns 解析后的 JSON 对象
|
|
237
|
+
* @throws {LLMJsonParseError} JSON 解析或校验失败
|
|
238
|
+
*/
|
|
239
|
+
async chatJSON<T = unknown>(messages: ChatMessage[], schema?: z.ZodType<T>): Promise<T> {
|
|
240
|
+
// 第一次尝试
|
|
241
|
+
const response = await this.chat(messages);
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
return this.validateJsonResponse<T>(response.content, schema);
|
|
245
|
+
} catch (firstError) {
|
|
246
|
+
// JSON 解析或校验失败,追加修正提示后重试
|
|
247
|
+
const fixMessages: ChatMessage[] = [
|
|
248
|
+
...messages,
|
|
249
|
+
{ role: 'assistant', content: response.content },
|
|
250
|
+
{
|
|
251
|
+
role: 'user',
|
|
252
|
+
content:
|
|
253
|
+
'你上次的回复不是合法的 JSON 格式。请严格按照要求只输出合法的 JSON,' +
|
|
254
|
+
'不要在 JSON 前后添加任何额外文字或 Markdown 格式标记。' +
|
|
255
|
+
(firstError instanceof Error ? `\n错误信息: ${firstError.message}` : ''),
|
|
256
|
+
},
|
|
257
|
+
];
|
|
258
|
+
|
|
259
|
+
const retryResponse = await this.chat(fixMessages);
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
return this.validateJsonResponse<T>(retryResponse.content, schema);
|
|
263
|
+
} catch (secondError) {
|
|
264
|
+
throw new LLMJsonParseError(
|
|
265
|
+
`JSON 解析失败(已重试): ${secondError instanceof Error ? secondError.message : '未知错误'}`,
|
|
266
|
+
retryResponse.content,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ========================================================================
|
|
273
|
+
// 私有方法
|
|
274
|
+
// ========================================================================
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* 构建 OpenAI 兼容的请求体
|
|
278
|
+
*/
|
|
279
|
+
private buildRequestBody(messages: ChatMessage[], options?: ChatOptions): string {
|
|
280
|
+
const payload: Record<string, unknown> = {
|
|
281
|
+
model: this.config.modelName,
|
|
282
|
+
messages: messages.map((m) => ({ role: m.role, content: m.content })),
|
|
283
|
+
temperature: options?.temperature ?? 0.3,
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
if (options?.maxTokens !== undefined) {
|
|
287
|
+
payload['max_tokens'] = options.maxTokens;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return JSON.stringify(payload);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* 带超时控制的 fetch 请求
|
|
295
|
+
*/
|
|
296
|
+
private async fetchWithTimeout(url: string, body: string): Promise<Response> {
|
|
297
|
+
const controller = new AbortController();
|
|
298
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
const response = await fetch(url, {
|
|
302
|
+
method: 'POST',
|
|
303
|
+
headers: {
|
|
304
|
+
'Authorization': `Bearer ${this.config.apiKey}`,
|
|
305
|
+
'Content-Type': 'application/json',
|
|
306
|
+
},
|
|
307
|
+
body,
|
|
308
|
+
signal: controller.signal,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// 处理错误状态码
|
|
312
|
+
if (!response.ok) {
|
|
313
|
+
const responseBody = await response.text().catch(() => '');
|
|
314
|
+
this.handleErrorStatus(response.status, responseBody);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return response;
|
|
318
|
+
} catch (error) {
|
|
319
|
+
if (error instanceof LLMError) {
|
|
320
|
+
throw error;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
324
|
+
throw new LLMError(`请求超时 (${this.timeoutMs}ms)`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
throw new LLMError(
|
|
328
|
+
`网络请求失败: ${error instanceof Error ? error.message : '未知错误'}`,
|
|
329
|
+
);
|
|
330
|
+
} finally {
|
|
331
|
+
clearTimeout(timer);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* 根据 HTTP 状态码抛出对应错误
|
|
337
|
+
*/
|
|
338
|
+
private handleErrorStatus(status: number, responseBody: string): never {
|
|
339
|
+
// 尝试从响应体中提取错误消息
|
|
340
|
+
let detail = '';
|
|
341
|
+
try {
|
|
342
|
+
const parsed = JSON.parse(responseBody) as { error?: { message?: string } };
|
|
343
|
+
detail = parsed?.error?.message ?? '';
|
|
344
|
+
} catch {
|
|
345
|
+
detail = responseBody.slice(0, 200);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (status === 401 || status === 403) {
|
|
349
|
+
throw new LLMAuthError(
|
|
350
|
+
`认证失败 (HTTP ${status}): ${detail || 'API 密钥无效或已过期'}`,
|
|
351
|
+
status,
|
|
352
|
+
responseBody,
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (status === 429) {
|
|
357
|
+
throw new LLMRateLimitError(
|
|
358
|
+
`请求速率超限 (HTTP 429): ${detail || '请稍后重试'}`,
|
|
359
|
+
responseBody,
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
throw new LLMError(
|
|
364
|
+
`API 请求失败 (HTTP ${status}): ${detail || '未知错误'}`,
|
|
365
|
+
status,
|
|
366
|
+
responseBody,
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* 从 OpenAI 兼容响应中解析内容和 usage
|
|
372
|
+
*/
|
|
373
|
+
private async parseResponse(response: Response): Promise<LLMResponse> {
|
|
374
|
+
const json = (await response.json()) as {
|
|
375
|
+
choices?: Array<{ message?: { content?: string } }>;
|
|
376
|
+
usage?: {
|
|
377
|
+
prompt_tokens?: number;
|
|
378
|
+
completion_tokens?: number;
|
|
379
|
+
total_tokens?: number;
|
|
380
|
+
};
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const content = json.choices?.[0]?.message?.content ?? '';
|
|
384
|
+
|
|
385
|
+
const result: LLMResponse = { content };
|
|
386
|
+
|
|
387
|
+
if (json.usage) {
|
|
388
|
+
result.usage = {
|
|
389
|
+
promptTokens: json.usage.prompt_tokens ?? 0,
|
|
390
|
+
completionTokens: json.usage.completion_tokens ?? 0,
|
|
391
|
+
totalTokens: json.usage.total_tokens ?? 0,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return result;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* 验证 JSON 响应:解析 + 可选 Zod 校验
|
|
400
|
+
*/
|
|
401
|
+
private validateJsonResponse<T>(content: string, schema?: z.ZodType<T>): T {
|
|
402
|
+
const parsed = parseJsonSafe(content);
|
|
403
|
+
|
|
404
|
+
if (schema) {
|
|
405
|
+
const result = schema.safeParse(parsed);
|
|
406
|
+
if (!result.success) {
|
|
407
|
+
const issues = result.error.issues
|
|
408
|
+
.map((i) => ` - ${i.path.join('.')}: ${i.message}`)
|
|
409
|
+
.join('\n');
|
|
410
|
+
throw new Error(`JSON 数据校验失败:\n${issues}`);
|
|
411
|
+
}
|
|
412
|
+
return result.data;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return parsed as T;
|
|
416
|
+
}
|
|
417
|
+
}
|