@lobehub/chat 1.84.24 → 1.84.26
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/CHANGELOG.md +50 -0
- package/apps/desktop/src/main/controllers/__tests__/BrowserWindowsCtr.test.ts +195 -0
- package/apps/desktop/src/main/controllers/__tests__/DevtoolsCtr.test.ts +44 -0
- package/apps/desktop/src/main/controllers/__tests__/MenuCtr.test.ts +82 -0
- package/apps/desktop/src/main/controllers/__tests__/ShortcutCtr.test.ts +64 -0
- package/apps/desktop/src/main/controllers/__tests__/TrayMenuCtr.test.ts +256 -0
- package/apps/desktop/src/main/controllers/__tests__/UpdaterCtr.test.ts +82 -0
- package/apps/desktop/src/main/services/fileSrv.ts +49 -10
- package/apps/desktop/vitest.config.ts +17 -0
- package/changelog/v1.json +18 -0
- package/package.json +3 -3
- package/packages/electron-server-ipc/package.json +3 -0
- package/packages/electron-server-ipc/src/ipcClient.ts +58 -21
- package/packages/electron-server-ipc/src/ipcServer.test.ts +417 -0
- package/packages/electron-server-ipc/src/ipcServer.ts +21 -16
- package/src/config/aiModels/ollama.ts +12 -2
- package/src/libs/langchain/loaders/epub/index.ts +4 -2
- package/src/libs/mcp/__tests__/__snapshots__/index.test.ts.snap +3 -1
- package/src/server/routers/async/file.ts +1 -1
- package/src/server/routers/lambda/file.ts +1 -1
- package/src/server/routers/lambda/importer.ts +4 -2
- package/src/server/routers/lambda/message.ts +2 -2
- package/src/server/routers/lambda/ragEval.ts +1 -1
- package/src/server/routers/lambda/user.ts +2 -2
- package/src/server/services/file/index.ts +46 -0
- package/src/server/utils/tempFileManager.ts +5 -8
- package/vitest.config.ts +3 -2
@@ -0,0 +1,82 @@
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
2
|
+
|
3
|
+
import type { App } from '@/core/App';
|
4
|
+
|
5
|
+
// 模拟 logger
|
6
|
+
vi.mock('@/utils/logger', () => ({
|
7
|
+
createLogger: () => ({
|
8
|
+
info: vi.fn(),
|
9
|
+
}),
|
10
|
+
}));
|
11
|
+
|
12
|
+
import UpdaterCtr from '../UpdaterCtr';
|
13
|
+
|
14
|
+
// 模拟 App 及其依赖项
|
15
|
+
const mockCheckForUpdates = vi.fn();
|
16
|
+
const mockDownloadUpdate = vi.fn();
|
17
|
+
const mockInstallNow = vi.fn();
|
18
|
+
const mockInstallLater = vi.fn();
|
19
|
+
|
20
|
+
const mockApp = {
|
21
|
+
updaterManager: {
|
22
|
+
checkForUpdates: mockCheckForUpdates,
|
23
|
+
downloadUpdate: mockDownloadUpdate,
|
24
|
+
installNow: mockInstallNow,
|
25
|
+
installLater: mockInstallLater,
|
26
|
+
},
|
27
|
+
} as unknown as App;
|
28
|
+
|
29
|
+
describe('UpdaterCtr', () => {
|
30
|
+
let updaterCtr: UpdaterCtr;
|
31
|
+
|
32
|
+
beforeEach(() => {
|
33
|
+
vi.clearAllMocks();
|
34
|
+
updaterCtr = new UpdaterCtr(mockApp);
|
35
|
+
});
|
36
|
+
|
37
|
+
describe('checkForUpdates', () => {
|
38
|
+
it('should call updaterManager.checkForUpdates', async () => {
|
39
|
+
await updaterCtr.checkForUpdates();
|
40
|
+
expect(mockCheckForUpdates).toHaveBeenCalled();
|
41
|
+
});
|
42
|
+
});
|
43
|
+
|
44
|
+
describe('downloadUpdate', () => {
|
45
|
+
it('should call updaterManager.downloadUpdate', async () => {
|
46
|
+
await updaterCtr.downloadUpdate();
|
47
|
+
expect(mockDownloadUpdate).toHaveBeenCalled();
|
48
|
+
});
|
49
|
+
});
|
50
|
+
|
51
|
+
describe('quitAndInstallUpdate', () => {
|
52
|
+
it('should call updaterManager.installNow', () => {
|
53
|
+
updaterCtr.quitAndInstallUpdate();
|
54
|
+
expect(mockInstallNow).toHaveBeenCalled();
|
55
|
+
});
|
56
|
+
});
|
57
|
+
|
58
|
+
describe('installLater', () => {
|
59
|
+
it('should call updaterManager.installLater', () => {
|
60
|
+
updaterCtr.installLater();
|
61
|
+
expect(mockInstallLater).toHaveBeenCalled();
|
62
|
+
});
|
63
|
+
});
|
64
|
+
|
65
|
+
// 测试错误处理
|
66
|
+
describe('error handling', () => {
|
67
|
+
it('should handle errors when checking for updates', async () => {
|
68
|
+
const error = new Error('Network error');
|
69
|
+
mockCheckForUpdates.mockRejectedValueOnce(error);
|
70
|
+
|
71
|
+
// 由于控制器并未明确处理并返回错误,这里我们只验证调用发生且错误正确冒泡
|
72
|
+
await expect(updaterCtr.checkForUpdates()).rejects.toThrow(error);
|
73
|
+
});
|
74
|
+
|
75
|
+
it('should handle errors when downloading updates', async () => {
|
76
|
+
const error = new Error('Download failed');
|
77
|
+
mockDownloadUpdate.mockRejectedValueOnce(error);
|
78
|
+
|
79
|
+
await expect(updaterCtr.downloadUpdate()).rejects.toThrow(error);
|
80
|
+
});
|
81
|
+
});
|
82
|
+
});
|
@@ -6,12 +6,16 @@ import { promisify } from 'node:util';
|
|
6
6
|
|
7
7
|
import { FILE_STORAGE_DIR } from '@/const/dir';
|
8
8
|
import { makeSureDirExist } from '@/utils/file-system';
|
9
|
+
import { createLogger } from '@/utils/logger';
|
9
10
|
|
10
11
|
import { ServiceModule } from './index';
|
11
12
|
|
12
13
|
const readFilePromise = promisify(fs.readFile);
|
13
14
|
const unlinkPromise = promisify(fs.unlink);
|
14
15
|
|
16
|
+
// Create logger
|
17
|
+
const logger = createLogger('services:FileService');
|
18
|
+
|
15
19
|
interface UploadFileParams {
|
16
20
|
content: ArrayBuffer;
|
17
21
|
filename: string;
|
@@ -35,8 +39,10 @@ export default class FileService extends ServiceModule {
|
|
35
39
|
constructor(app) {
|
36
40
|
super(app);
|
37
41
|
|
38
|
-
//
|
42
|
+
// Initialize file storage directory
|
43
|
+
logger.info('Initializing file storage directory');
|
39
44
|
makeSureDirExist(this.UPLOADS_DIR);
|
45
|
+
logger.debug(`Upload directory created: ${this.UPLOADS_DIR}`);
|
40
46
|
}
|
41
47
|
|
42
48
|
/**
|
@@ -48,19 +54,23 @@ export default class FileService extends ServiceModule {
|
|
48
54
|
hash,
|
49
55
|
type,
|
50
56
|
}: UploadFileParams): Promise<{ metadata: FileMetadata; success: boolean }> {
|
57
|
+
logger.info(`Starting to upload file: ${filename}, hash: ${hash}`);
|
51
58
|
try {
|
52
59
|
// 创建时间戳目录
|
53
60
|
const date = (Date.now() / 1000 / 60 / 60).toFixed(0);
|
54
61
|
const dirname = join(this.UPLOADS_DIR, date);
|
62
|
+
logger.debug(`Creating timestamp directory: ${dirname}`);
|
55
63
|
makeSureDirExist(dirname);
|
56
64
|
|
57
65
|
// 生成文件保存路径
|
58
66
|
const fileExt = filename.split('.').pop() || '';
|
59
67
|
const savedFilename = `${hash}${fileExt ? `.${fileExt}` : ''}`;
|
60
68
|
const savedPath = join(dirname, savedFilename);
|
69
|
+
logger.debug(`Generated file save path: ${savedPath}`);
|
61
70
|
|
62
71
|
// 写入文件内容
|
63
72
|
const buffer = Buffer.from(content);
|
73
|
+
logger.debug(`Writing file content, size: ${buffer.length} bytes`);
|
64
74
|
await writeFile(savedPath, buffer);
|
65
75
|
|
66
76
|
// 写入元数据文件
|
@@ -72,10 +82,12 @@ export default class FileService extends ServiceModule {
|
|
72
82
|
size: buffer.length,
|
73
83
|
type,
|
74
84
|
};
|
85
|
+
logger.debug(`Writing metadata file: ${metaFilePath}`);
|
75
86
|
await writeFile(metaFilePath, JSON.stringify(metadata, null, 2));
|
76
87
|
|
77
88
|
// 返回与S3兼容的元数据格式
|
78
89
|
const desktopPath = `desktop://${date}/${savedFilename}`;
|
90
|
+
logger.info(`File upload successful: ${desktopPath}`);
|
79
91
|
|
80
92
|
return {
|
81
93
|
metadata: {
|
@@ -87,7 +99,7 @@ export default class FileService extends ServiceModule {
|
|
87
99
|
success: true,
|
88
100
|
};
|
89
101
|
} catch (error) {
|
90
|
-
|
102
|
+
logger.error(`File upload failed:`, error);
|
91
103
|
throw new Error(`File upload failed: ${(error as Error).message}`);
|
92
104
|
}
|
93
105
|
}
|
@@ -96,35 +108,41 @@ export default class FileService extends ServiceModule {
|
|
96
108
|
* 获取文件内容
|
97
109
|
*/
|
98
110
|
async getFile(path: string): Promise<{ content: ArrayBuffer; mimeType: string }> {
|
111
|
+
logger.info(`Getting file content: ${path}`);
|
99
112
|
try {
|
100
113
|
// 处理desktop://路径
|
101
114
|
if (!path.startsWith('desktop://')) {
|
115
|
+
logger.error(`Invalid desktop file path: ${path}`);
|
102
116
|
throw new Error(`Invalid desktop file path: ${path}`);
|
103
117
|
}
|
104
118
|
|
105
119
|
// 标准化路径格式
|
106
120
|
// 可能收到的格式: desktop:/12345/file.png 或 desktop://12345/file.png
|
107
121
|
const normalizedPath = path.replace(/^desktop:\/+/, 'desktop://');
|
122
|
+
logger.debug(`Normalized path: ${normalizedPath}`);
|
108
123
|
|
109
124
|
// 解析路径
|
110
125
|
const relativePath = normalizedPath.replace('desktop://', '');
|
111
126
|
const filePath = join(this.UPLOADS_DIR, relativePath);
|
112
|
-
|
113
|
-
console.log('Reading file from:', filePath);
|
127
|
+
logger.debug(`Reading file from path: ${filePath}`);
|
114
128
|
|
115
129
|
// 读取文件内容
|
130
|
+
logger.debug(`Starting to read file content`);
|
116
131
|
const content = await readFilePromise(filePath);
|
132
|
+
logger.debug(`File content read complete, size: ${content.length} bytes`);
|
117
133
|
|
118
134
|
// 读取元数据获取MIME类型
|
119
135
|
const metaFilePath = `${filePath}.meta`;
|
120
136
|
let mimeType = 'application/octet-stream'; // 默认MIME类型
|
137
|
+
logger.debug(`Attempting to read metadata file: ${metaFilePath}`);
|
121
138
|
|
122
139
|
try {
|
123
140
|
const metaContent = await readFilePromise(metaFilePath, 'utf8');
|
124
141
|
const metadata = JSON.parse(metaContent);
|
125
142
|
mimeType = metadata.type || mimeType;
|
143
|
+
logger.debug(`Got MIME type from metadata: ${mimeType}`);
|
126
144
|
} catch (metaError) {
|
127
|
-
|
145
|
+
logger.warn(`Failed to read metadata file: ${(metaError as Error).message}, using default MIME type`);
|
128
146
|
// 如果元数据文件不存在,尝试从文件扩展名猜测MIME类型
|
129
147
|
const ext = path.split('.').pop()?.toLowerCase();
|
130
148
|
if (ext) {
|
@@ -155,15 +173,17 @@ export default class FileService extends ServiceModule {
|
|
155
173
|
break;
|
156
174
|
}
|
157
175
|
}
|
176
|
+
logger.debug(`Set MIME type based on file extension: ${mimeType}`);
|
158
177
|
}
|
159
178
|
}
|
160
179
|
|
180
|
+
logger.info(`File retrieval successful: ${path}`);
|
161
181
|
return {
|
162
182
|
content: content.buffer as ArrayBuffer,
|
163
183
|
mimeType,
|
164
184
|
};
|
165
185
|
} catch (error) {
|
166
|
-
|
186
|
+
logger.error(`File retrieval failed:`, error);
|
167
187
|
throw new Error(`File retrieval failed: ${(error as Error).message}`);
|
168
188
|
}
|
169
189
|
}
|
@@ -172,29 +192,37 @@ export default class FileService extends ServiceModule {
|
|
172
192
|
* 删除文件
|
173
193
|
*/
|
174
194
|
async deleteFile(path: string): Promise<{ success: boolean }> {
|
195
|
+
logger.info(`Deleting file: ${path}`);
|
175
196
|
try {
|
176
197
|
// 处理desktop://路径
|
177
198
|
if (!path.startsWith('desktop://')) {
|
199
|
+
logger.error(`Invalid desktop file path: ${path}`);
|
178
200
|
throw new Error(`Invalid desktop file path: ${path}`);
|
179
201
|
}
|
180
202
|
|
181
203
|
// 解析路径
|
182
204
|
const relativePath = path.replace('desktop://', '');
|
183
205
|
const filePath = join(this.UPLOADS_DIR, relativePath);
|
206
|
+
logger.debug(`File deletion path: ${filePath}`);
|
184
207
|
|
185
208
|
// 删除文件及其元数据
|
209
|
+
logger.debug(`Starting file deletion`);
|
186
210
|
await unlinkPromise(filePath);
|
211
|
+
logger.debug(`File deletion successful`);
|
187
212
|
|
188
213
|
// 尝试删除元数据文件,但不强制要求存在
|
189
214
|
try {
|
215
|
+
logger.debug(`Attempting to delete metadata file`);
|
190
216
|
await unlinkPromise(`${filePath}.meta`);
|
217
|
+
logger.debug(`Metadata file deletion successful`);
|
191
218
|
} catch (error) {
|
192
|
-
|
219
|
+
logger.warn(`Failed to delete metadata file: ${(error as Error).message}`);
|
193
220
|
}
|
194
221
|
|
222
|
+
logger.info(`File deletion operation complete: ${path}`);
|
195
223
|
return { success: true };
|
196
224
|
} catch (error) {
|
197
|
-
|
225
|
+
logger.error(`File deletion failed:`, error);
|
198
226
|
throw new Error(`File deletion failed: ${(error as Error).message}`);
|
199
227
|
}
|
200
228
|
}
|
@@ -203,15 +231,18 @@ export default class FileService extends ServiceModule {
|
|
203
231
|
* 批量删除文件
|
204
232
|
*/
|
205
233
|
async deleteFiles(paths: string[]): Promise<DeleteFilesResponse> {
|
234
|
+
logger.info(`Batch deleting files, count: ${paths.length}`);
|
206
235
|
const errors: { message: string; path: string }[] = [];
|
207
236
|
|
208
237
|
// 并行处理所有删除请求
|
238
|
+
logger.debug(`Starting parallel deletion requests`);
|
209
239
|
const results = await Promise.allSettled(
|
210
240
|
paths.map(async (path) => {
|
211
241
|
try {
|
212
242
|
await this.deleteFile(path);
|
213
243
|
return { path, success: true };
|
214
244
|
} catch (error) {
|
245
|
+
logger.warn(`Failed to delete file: ${path}, error: ${(error as Error).message}`);
|
215
246
|
return {
|
216
247
|
error: (error as Error).message,
|
217
248
|
path,
|
@@ -222,8 +253,10 @@ export default class FileService extends ServiceModule {
|
|
222
253
|
);
|
223
254
|
|
224
255
|
// 处理结果
|
256
|
+
logger.debug(`Processing batch deletion results`);
|
225
257
|
results.forEach((result) => {
|
226
258
|
if (result.status === 'rejected') {
|
259
|
+
logger.error(`Unexpected error: ${result.reason}`);
|
227
260
|
errors.push({
|
228
261
|
message: `Unexpected error: ${result.reason}`,
|
229
262
|
path: 'unknown',
|
@@ -236,20 +269,26 @@ export default class FileService extends ServiceModule {
|
|
236
269
|
}
|
237
270
|
});
|
238
271
|
|
272
|
+
const success = errors.length === 0;
|
273
|
+
logger.info(`Batch deletion operation complete, success: ${success}, error count: ${errors.length}`);
|
239
274
|
return {
|
240
|
-
success
|
275
|
+
success,
|
241
276
|
...(errors.length > 0 && { errors }),
|
242
277
|
};
|
243
278
|
}
|
244
279
|
|
245
280
|
async getFilePath(path: string): Promise<string> {
|
281
|
+
logger.debug(`Getting filesystem path: ${path}`);
|
246
282
|
// 处理desktop://路径
|
247
283
|
if (!path.startsWith('desktop://')) {
|
284
|
+
logger.error(`Invalid desktop file path: ${path}`);
|
248
285
|
throw new Error(`Invalid desktop file path: ${path}`);
|
249
286
|
}
|
250
287
|
|
251
288
|
// 解析路径
|
252
289
|
const relativePath = path.replace('desktop://', '');
|
253
|
-
|
290
|
+
const fullPath = join(this.UPLOADS_DIR, relativePath);
|
291
|
+
logger.debug(`Resolved filesystem path: ${fullPath}`);
|
292
|
+
return fullPath;
|
254
293
|
}
|
255
294
|
}
|
@@ -0,0 +1,17 @@
|
|
1
|
+
import { resolve } from 'node:path';
|
2
|
+
import { defineConfig } from 'vitest/config';
|
3
|
+
|
4
|
+
export default defineConfig({
|
5
|
+
test: {
|
6
|
+
alias: {
|
7
|
+
'@': resolve(__dirname, './src/main'),
|
8
|
+
},
|
9
|
+
coverage: {
|
10
|
+
all: false,
|
11
|
+
provider: 'v8',
|
12
|
+
reporter: ['text', 'json', 'lcov', 'text-summary'],
|
13
|
+
reportsDirectory: './coverage/app',
|
14
|
+
},
|
15
|
+
environment: 'node',
|
16
|
+
},
|
17
|
+
});
|
package/changelog/v1.json
CHANGED
@@ -1,4 +1,22 @@
|
|
1
1
|
[
|
2
|
+
{
|
3
|
+
"children": {
|
4
|
+
"improvements": [
|
5
|
+
"Add qwen3 for ollama."
|
6
|
+
]
|
7
|
+
},
|
8
|
+
"date": "2025-05-08",
|
9
|
+
"version": "1.84.26"
|
10
|
+
},
|
11
|
+
{
|
12
|
+
"children": {
|
13
|
+
"fixes": [
|
14
|
+
"Fix desktop upload image on macOS."
|
15
|
+
]
|
16
|
+
},
|
17
|
+
"date": "2025-05-08",
|
18
|
+
"version": "1.84.25"
|
19
|
+
},
|
2
20
|
{
|
3
21
|
"children": {
|
4
22
|
"fixes": [
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@lobehub/chat",
|
3
|
-
"version": "1.84.
|
3
|
+
"version": "1.84.26",
|
4
4
|
"description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
5
5
|
"keywords": [
|
6
6
|
"framework",
|
@@ -121,7 +121,7 @@
|
|
121
121
|
"dependencies": {
|
122
122
|
"@ant-design/icons": "^5.6.1",
|
123
123
|
"@ant-design/pro-components": "^2.8.7",
|
124
|
-
"@anthropic-ai/sdk": "^0.
|
124
|
+
"@anthropic-ai/sdk": "^0.40.1",
|
125
125
|
"@auth/core": "^0.38.0",
|
126
126
|
"@aws-sdk/client-bedrock-runtime": "^3.779.0",
|
127
127
|
"@aws-sdk/client-s3": "^3.779.0",
|
@@ -199,7 +199,7 @@
|
|
199
199
|
"langfuse": "^3.37.1",
|
200
200
|
"langfuse-core": "^3.37.1",
|
201
201
|
"lodash-es": "^4.17.21",
|
202
|
-
"lucide-react": "^0.
|
202
|
+
"lucide-react": "^0.508.0",
|
203
203
|
"mammoth": "^1.9.0",
|
204
204
|
"mdast-util-to-markdown": "^2.1.2",
|
205
205
|
"modern-screenshot": "^4.6.0",
|
@@ -192,39 +192,76 @@ export class ElectronIpcClient {
|
|
192
192
|
|
193
193
|
log('Sending request: %s %o', method, params);
|
194
194
|
return new Promise<T>((resolve, reject) => {
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
195
|
+
const id = Math.random().toString(36).slice(2, 15);
|
196
|
+
const request = { id, method, params };
|
197
|
+
log('Created request with ID: %s', id);
|
198
|
+
|
199
|
+
// eslint-disable-next-line no-undef
|
200
|
+
let requestTimeoutId: NodeJS.Timeout;
|
201
|
+
|
202
|
+
const cleanupAndResolve = (value: T) => {
|
203
|
+
clearTimeout(requestTimeoutId);
|
204
|
+
this.requestQueue.delete(id);
|
205
|
+
resolve(value);
|
206
|
+
};
|
207
|
+
|
208
|
+
const cleanupAndReject = (error: any) => {
|
209
|
+
clearTimeout(requestTimeoutId);
|
210
|
+
this.requestQueue.delete(id);
|
211
|
+
// 保留超时错误的 console.error 日志
|
212
|
+
if (
|
213
|
+
error &&
|
214
|
+
error.message &&
|
215
|
+
typeof error.message === 'string' &&
|
216
|
+
error.message.startsWith('Request timed out')
|
217
|
+
) {
|
208
218
|
console.error('Request timed out, ID: %s, method: %s', id, method);
|
209
|
-
|
210
|
-
|
219
|
+
}
|
220
|
+
reject(error);
|
221
|
+
};
|
222
|
+
|
223
|
+
this.requestQueue.set(id, { reject: cleanupAndReject, resolve: cleanupAndResolve });
|
224
|
+
log('Added request to queue, current queue size: %d', this.requestQueue.size);
|
225
|
+
|
226
|
+
requestTimeoutId = setTimeout(() => {
|
227
|
+
const pendingRequest = this.requestQueue.get(id);
|
228
|
+
if (pendingRequest) {
|
229
|
+
// 请求仍在队列中,表示超时
|
230
|
+
// 调用其专用的 reject处理器 (cleanupAndReject)
|
231
|
+
const errorMsg = `Request timed out, method: ${method}`;
|
232
|
+
// console.error 移至 cleanupAndReject 中处理
|
233
|
+
pendingRequest.reject(new Error(errorMsg));
|
234
|
+
}
|
235
|
+
// 如果 pendingRequest 不存在, 表示请求已被处理,其超时已清除
|
236
|
+
}, 5000);
|
211
237
|
|
238
|
+
try {
|
212
239
|
// 发送请求
|
213
|
-
const requestJson = JSON.stringify(request);
|
240
|
+
const requestJson = JSON.stringify(request) + '\n';
|
214
241
|
log('Writing request to socket, size: %d bytes', requestJson.length);
|
215
242
|
this.socket!.write(requestJson, (err) => {
|
216
243
|
if (err) {
|
217
|
-
|
218
|
-
|
244
|
+
// 写入失败,请求应被视为失败
|
245
|
+
// 调用其 reject 处理器 (cleanupAndReject)
|
246
|
+
const pending = this.requestQueue.get(id);
|
247
|
+
if (pending) {
|
248
|
+
pending.reject(err); // 这会调用 cleanupAndReject
|
249
|
+
} else {
|
250
|
+
// 理论上不应发生,因为写入失败通常很快
|
251
|
+
// 但为安全起见,确保原始 promise 被 reject
|
252
|
+
cleanupAndReject(err);
|
253
|
+
}
|
219
254
|
console.error('Failed to write request to socket: %o', err);
|
220
|
-
reject(err);
|
221
255
|
} else {
|
222
256
|
log('Request successfully written to socket, ID: %s', id);
|
223
257
|
}
|
224
258
|
});
|
225
259
|
} catch (err) {
|
226
|
-
console.error('Error sending request: %o', err);
|
227
|
-
|
260
|
+
console.error('Error sending request (during setup/write phase): %o', err);
|
261
|
+
// 发生在此处的错误意味着请求甚至没有机会进入队列或设置超时
|
262
|
+
// 直接调用 cleanupAndReject 以确保一致性,尽管此时 requestTimeoutId 可能未定义
|
263
|
+
// (clearTimeout(undefined)是安全的)
|
264
|
+
cleanupAndReject(err);
|
228
265
|
}
|
229
266
|
});
|
230
267
|
}
|