@ww_nero/mini-cli 1.0.81 → 1.0.83

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/src/request.js CHANGED
@@ -1,328 +1,345 @@
1
- const fetch = require('node-fetch');
2
- const { createParser } = require('eventsource-parser');
3
- const { COMPACT_SUMMARY_PROMPT } = require('./config');
4
-
5
- const joinUrl = (base, pathname) => {
6
- const normalizedBase = String(base || '').replace(/\/$/, '');
7
- const normalizedPath = pathname.startsWith('/') ? pathname : `/${pathname}`;
8
- return `${normalizedBase}${normalizedPath}`;
9
- };
10
-
11
- const createAbortError = () => {
12
- if (typeof DOMException === 'function') {
13
- return new DOMException('Aborted', 'AbortError');
14
- }
15
- const error = new Error('Aborted');
16
- error.name = 'AbortError';
17
- return error;
18
- };
19
-
20
- const createFetchOptions = (endpoint, body, abortController) => ({
21
- method: 'POST',
22
- headers: {
23
- Authorization: `Bearer ${endpoint.key}`,
24
- 'Content-Type': 'application/json'
25
- },
26
- body: JSON.stringify(body),
27
- signal: abortController.signal
28
- });
29
-
30
- const makeRequestWithRetry = async (endpoint, requestBody, abortController, options = {}) => {
31
- const { maxRetries = 5, retryDelay = 3000, onRetry = null } = options;
32
- const url = joinUrl(endpoint.baseUrl, '/chat/completions');
33
-
34
- const attemptRequest = async () => {
35
- try {
36
- const response = await fetch(url, createFetchOptions(endpoint, requestBody, abortController));
37
- if (!response.ok) {
38
- throw new Error(`HTTP ${response.status}`);
39
- }
40
- return { response, error: null };
41
- } catch (error) {
42
- if (abortController.signal.aborted) {
43
- throw createAbortError();
44
- }
45
- return { response: null, error };
46
- }
47
- };
48
-
49
- let attempt = 0;
50
- let result = await attemptRequest();
51
-
52
- while (!result.response && attempt < maxRetries) {
53
- attempt += 1;
54
- if (typeof onRetry === 'function') {
55
- onRetry(attempt, result.error);
56
- }
57
- await new Promise(resolve => {
58
- const handleAbort = () => {
59
- abortController.signal.removeEventListener('abort', handleAbort);
60
- clearTimeout(timeout);
61
- resolve();
62
- };
63
- const timeout = setTimeout(() => {
64
- abortController.signal.removeEventListener('abort', handleAbort);
65
- resolve();
66
- }, retryDelay);
67
- abortController.signal.addEventListener('abort', handleAbort);
68
- });
69
- if (abortController.signal.aborted) {
70
- throw createAbortError();
71
- }
72
- result = await attemptRequest();
73
- }
74
-
75
- if (!result.response) {
76
- throw result.error;
77
- }
78
-
79
- return result.response;
80
- };
81
-
82
- const processStreamResponse = (response, options = {}) => {
83
- const {
84
- onContent = null,
85
- onReasoningContent = null,
86
- onComplete = null,
87
- abortController = null,
88
- includeToolCalls = false
89
- } = options;
90
-
91
- return new Promise((resolve, reject) => {
92
- let abortHandler = null;
93
- const cleanupAbortListener = () => {
94
- if (abortController && abortHandler) {
95
- abortController.signal.removeEventListener('abort', abortHandler);
96
- abortHandler = null;
97
- }
98
- };
99
-
100
- const rejectWithAbort = () => {
101
- cleanupAbortListener();
102
- if (response.body && typeof response.body.destroy === 'function') {
103
- response.body.destroy();
104
- }
105
- reject(createAbortError());
106
- };
107
-
108
- const checkAbort = () => {
109
- if (abortController && abortController.signal.aborted) {
110
- rejectWithAbort();
111
- return true;
112
- }
113
- return false;
114
- };
115
-
116
- try {
117
- let fullContent = '';
118
- let toolCalls = [];
119
- let currentMessage = null;
120
- let fullReasoningContent = '';
121
- let usage = null;
122
- const decoder = new TextDecoder('utf-8', { fatal: false });
123
-
124
- const handleEvent = (event) => {
125
- if (checkAbort()) return;
126
- if (!event || event.type !== 'event') {
127
- return;
128
- }
129
- if (!event.data || event.data === '[DONE]') {
130
- return;
131
- }
132
- try {
133
- const payload = JSON.parse(event.data);
134
- if (payload.usage && typeof payload.usage === 'object') {
135
- usage = payload.usage;
136
- }
137
- const choice = payload.choices && payload.choices[0];
138
- if (!choice || !choice.delta) return;
139
- const delta = choice.delta;
140
- const contentChunk = delta.content || '';
141
- if (contentChunk) {
142
- fullContent += contentChunk;
143
- if (onContent) {
144
- onContent(contentChunk, fullContent);
145
- }
146
- }
147
-
148
- const reasoningChunk = delta.reasoning_content || '';
149
- if (reasoningChunk) {
150
- fullReasoningContent += reasoningChunk;
151
- if (onReasoningContent) {
152
- onReasoningContent(reasoningChunk, fullReasoningContent);
153
- }
154
- }
155
-
156
- if (includeToolCalls && Array.isArray(delta.tool_calls)) {
157
- delta.tool_calls.forEach((call, index) => {
158
- if (!toolCalls[index]) {
159
- toolCalls[index] = {
160
- id: call.id || '',
161
- type: call.type || 'function',
162
- function: {
163
- name: (call.function && call.function.name) || '',
164
- arguments: ''
165
- }
166
- };
167
- }
168
-
169
- if (call.function && typeof call.function.arguments === 'string') {
170
- toolCalls[index].function.arguments += call.function.arguments;
171
- }
172
- });
173
- }
174
-
175
- if (includeToolCalls) {
176
- currentMessage = {
177
- role: 'assistant',
178
- content: fullContent || null,
179
- ...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
180
- ...(fullReasoningContent ? { reasoning_content: fullReasoningContent } : {})
181
- };
182
- }
183
- } catch (error) {
184
- console.warn('SSE parse error:', error.message);
185
- }
186
- };
187
-
188
- const parser = createParser(handleEvent);
189
-
190
- const safeFeed = (text) => {
191
- try {
192
- parser.feed(text);
193
- } catch (error) {
194
- console.warn('SSE error:', error.message);
195
- }
196
- };
197
-
198
- response.body.on('data', chunk => {
199
- if (checkAbort()) return;
200
- const text = decoder.decode(chunk, { stream: true });
201
- safeFeed(text);
202
- });
203
-
204
- response.body.on('end', () => {
205
- if (checkAbort()) return;
206
- cleanupAbortListener();
207
- const rest = decoder.decode();
208
- if (rest) {
209
- safeFeed(rest);
210
- }
211
- onComplete && onComplete();
212
- if (includeToolCalls) {
213
- const filteredToolCalls = toolCalls.filter(Boolean);
214
- const assistantMessage = currentMessage || {
215
- role: 'assistant',
216
- content: fullContent || null,
217
- ...(filteredToolCalls.length > 0 ? { tool_calls: filteredToolCalls } : {})
218
- };
219
-
220
- if (fullReasoningContent && !assistantMessage.reasoning_content) {
221
- assistantMessage.reasoning_content = fullReasoningContent;
222
- }
223
-
224
- resolve({
225
- content: fullContent,
226
- toolCalls: filteredToolCalls,
227
- message: assistantMessage,
228
- reasoningContent: fullReasoningContent,
229
- usage
230
- });
231
- } else {
232
- resolve({
233
- content: fullContent,
234
- reasoningContent: fullReasoningContent,
235
- usage
236
- });
237
- }
238
- });
239
-
240
- response.body.on('error', (error) => {
241
- cleanupAbortListener();
242
- reject(error);
243
- });
244
-
245
- if (abortController) {
246
- abortHandler = () => {
247
- rejectWithAbort();
248
- };
249
- abortController.signal.addEventListener('abort', abortHandler);
250
- }
251
- } catch (error) {
252
- reject(error);
253
- }
254
- });
255
- };
256
-
257
- const buildConversationForSummary = (messages) => {
258
- const parts = [];
259
- for (const msg of messages) {
260
- if (msg.role === 'system') continue;
261
-
262
- const role = msg.role === 'user' ? '用户' : msg.role === 'assistant' ? '助手' : '工具';
263
- let content = msg.content || '';
264
-
265
- if (msg.tool_call_id) {
266
- parts.push(`[工具返回 ${msg.tool_call_id}]: ${content}`);
267
- } else if (msg.tool_calls && msg.tool_calls.length > 0) {
268
- const toolNames = msg.tool_calls.map(tc => tc.function?.name || '未知工具').join(', ');
269
- parts.push(`[${role}]: ${content || ''}\n[调用工具: ${toolNames}]`);
270
- } else {
271
- parts.push(`[${role}]: ${content}`);
272
- }
273
- }
274
- return parts.join('\n\n');
275
- };
276
-
277
- const performCompactSummary = async (endpoint, messages, abortController) => {
278
- const conversationText = buildConversationForSummary(messages);
279
-
280
- const summaryMessages = [
281
- {
282
- role: 'system',
283
- content: COMPACT_SUMMARY_PROMPT
284
- },
285
- {
286
- role: 'user',
287
- content: `以下是需要总结的对话内容:\n\n${conversationText}`
288
- }
289
- ];
290
-
291
- const requestBody = {
292
- stream: false,
293
- messages: summaryMessages,
294
- model: endpoint.model
295
- };
296
-
297
- const url = joinUrl(endpoint.baseUrl, '/chat/completions');
298
-
299
- try {
300
- const response = await fetch(url, createFetchOptions(endpoint, requestBody, abortController));
301
-
302
- if (!response.ok) {
303
- throw new Error(`HTTP error! status: ${response.status}`);
304
- }
305
-
306
- const data = await response.json();
307
- const choice = data.choices && data.choices[0];
308
-
309
- if (choice && choice.message && choice.message.content) {
310
- return choice.message.content;
311
- }
312
-
313
- throw new Error('无法获取总结内容');
314
- } catch (error) {
315
- if (error.name === 'AbortError') {
316
- throw error;
317
- }
318
- console.error('Compact 总结请求失败:', error);
319
- throw error;
320
- }
321
- };
322
-
323
- module.exports = {
324
- makeRequestWithRetry,
325
- processStreamResponse,
326
- buildConversationForSummary,
327
- performCompactSummary
328
- };
1
+ const fetch = require('node-fetch');
2
+ const { createParser } = require('eventsource-parser');
3
+ const { COMPACT_SUMMARY_PROMPT } = require('./config');
4
+
5
+ const joinUrl = (base, pathname) => {
6
+ const normalizedBase = String(base || '').replace(/\/$/, '');
7
+ const normalizedPath = pathname.startsWith('/') ? pathname : `/${pathname}`;
8
+ return `${normalizedBase}${normalizedPath}`;
9
+ };
10
+
11
+ const createAbortError = () => {
12
+ if (typeof DOMException === 'function') {
13
+ return new DOMException('Aborted', 'AbortError');
14
+ }
15
+ const error = new Error('Aborted');
16
+ error.name = 'AbortError';
17
+ return error;
18
+ };
19
+
20
+ const createFetchOptions = (endpoint, body, abortController) => ({
21
+ method: 'POST',
22
+ headers: {
23
+ Authorization: `Bearer ${endpoint.key}`,
24
+ 'Content-Type': 'application/json'
25
+ },
26
+ body: JSON.stringify(body),
27
+ signal: abortController.signal
28
+ });
29
+
30
+ const makeRequestWithRetry = async (endpoint, requestBody, abortController, options = {}) => {
31
+ const { maxRetries = 5, retryDelay = 3000, onRetry = null } = options;
32
+ const url = joinUrl(endpoint.baseUrl, '/chat/completions');
33
+
34
+ const attemptRequest = async () => {
35
+ try {
36
+ const response = await fetch(url, createFetchOptions(endpoint, requestBody, abortController));
37
+ if (!response.ok) {
38
+ let detail = '';
39
+ try {
40
+ const body = await response.text();
41
+ detail = body ? `: ${body}` : '';
42
+ } catch (_) {}
43
+ throw new Error(`HTTP ${response.status}${detail}`);
44
+ }
45
+ return { response, error: null };
46
+ } catch (error) {
47
+ if (abortController.signal.aborted) {
48
+ throw createAbortError();
49
+ }
50
+ return { response: null, error };
51
+ }
52
+ };
53
+
54
+ let attempt = 0;
55
+ let result = await attemptRequest();
56
+
57
+ while (!result.response && attempt < maxRetries) {
58
+ attempt += 1;
59
+ if (typeof onRetry === 'function') {
60
+ onRetry(attempt, result.error);
61
+ }
62
+ await new Promise(resolve => {
63
+ const handleAbort = () => {
64
+ abortController.signal.removeEventListener('abort', handleAbort);
65
+ clearTimeout(timeout);
66
+ resolve();
67
+ };
68
+ const timeout = setTimeout(() => {
69
+ abortController.signal.removeEventListener('abort', handleAbort);
70
+ resolve();
71
+ }, retryDelay);
72
+ abortController.signal.addEventListener('abort', handleAbort);
73
+ });
74
+ if (abortController.signal.aborted) {
75
+ throw createAbortError();
76
+ }
77
+ result = await attemptRequest();
78
+ }
79
+
80
+ if (!result.response) {
81
+ throw result.error;
82
+ }
83
+
84
+ return result.response;
85
+ };
86
+
87
+ const processStreamResponse = (response, options = {}) => {
88
+ const {
89
+ onContent = null,
90
+ onReasoningContent = null,
91
+ onComplete = null,
92
+ abortController = null,
93
+ includeToolCalls = false
94
+ } = options;
95
+
96
+ return new Promise((resolve, reject) => {
97
+ let abortHandler = null;
98
+ const cleanupAbortListener = () => {
99
+ if (abortController && abortHandler) {
100
+ abortController.signal.removeEventListener('abort', abortHandler);
101
+ abortHandler = null;
102
+ }
103
+ };
104
+
105
+ const rejectWithAbort = () => {
106
+ cleanupAbortListener();
107
+ if (response.body && typeof response.body.destroy === 'function') {
108
+ response.body.destroy();
109
+ }
110
+ reject(createAbortError());
111
+ };
112
+
113
+ const checkAbort = () => {
114
+ if (abortController && abortController.signal.aborted) {
115
+ rejectWithAbort();
116
+ return true;
117
+ }
118
+ return false;
119
+ };
120
+
121
+ try {
122
+ let fullContent = '';
123
+ let toolCalls = [];
124
+ let currentMessage = null;
125
+ let fullReasoningContent = '';
126
+ let usage = null;
127
+ const decoder = new TextDecoder('utf-8', { fatal: false });
128
+
129
+ const handleEvent = (event) => {
130
+ if (checkAbort()) return;
131
+ if (!event || event.type !== 'event') {
132
+ return;
133
+ }
134
+ if (!event.data || event.data === '[DONE]') {
135
+ return;
136
+ }
137
+ try {
138
+ const payload = JSON.parse(event.data);
139
+ if (payload.usage && typeof payload.usage === 'object') {
140
+ usage = payload.usage;
141
+ }
142
+ const choice = payload.choices && payload.choices[0];
143
+ if (!choice || !choice.delta) return;
144
+ const delta = choice.delta;
145
+ const contentChunk = delta.content || '';
146
+ if (contentChunk) {
147
+ fullContent += contentChunk;
148
+ if (onContent) {
149
+ onContent(contentChunk, fullContent);
150
+ }
151
+ }
152
+
153
+ const reasoningChunk = delta.reasoning_content || '';
154
+ if (reasoningChunk) {
155
+ fullReasoningContent += reasoningChunk;
156
+ if (onReasoningContent) {
157
+ onReasoningContent(reasoningChunk, fullReasoningContent);
158
+ }
159
+ }
160
+
161
+ if (includeToolCalls && Array.isArray(delta.tool_calls)) {
162
+ delta.tool_calls.forEach((call) => {
163
+ const idx = typeof call.index === 'number' ? call.index : toolCalls.length;
164
+ if (!toolCalls[idx]) {
165
+ toolCalls[idx] = {
166
+ id: call.id || '',
167
+ type: call.type || 'function',
168
+ function: {
169
+ name: '',
170
+ arguments: ''
171
+ }
172
+ };
173
+ }
174
+
175
+ if (call.id) {
176
+ toolCalls[idx].id = call.id;
177
+ }
178
+ if (call.function && call.function.name) {
179
+ toolCalls[idx].function.name += call.function.name;
180
+ }
181
+ if (call.function && typeof call.function.arguments === 'string') {
182
+ toolCalls[idx].function.arguments += call.function.arguments;
183
+ }
184
+ });
185
+ }
186
+
187
+ if (includeToolCalls) {
188
+ currentMessage = {
189
+ role: 'assistant',
190
+ content: fullContent || null,
191
+ ...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
192
+ ...(fullReasoningContent ? { reasoning_content: fullReasoningContent } : {})
193
+ };
194
+ }
195
+ } catch (error) {
196
+ console.warn('SSE parse error:', error.message);
197
+ }
198
+ };
199
+
200
+ const parser = createParser(handleEvent);
201
+
202
+ const safeFeed = (text) => {
203
+ try {
204
+ parser.feed(text);
205
+ } catch (error) {
206
+ console.warn('SSE error:', error.message);
207
+ }
208
+ };
209
+
210
+ response.body.on('data', chunk => {
211
+ if (checkAbort()) return;
212
+ const text = decoder.decode(chunk, { stream: true });
213
+ safeFeed(text);
214
+ });
215
+
216
+ response.body.on('end', () => {
217
+ if (checkAbort()) return;
218
+ cleanupAbortListener();
219
+ const rest = decoder.decode();
220
+ if (rest) {
221
+ safeFeed(rest);
222
+ }
223
+ onComplete && onComplete();
224
+ if (includeToolCalls) {
225
+ const filteredToolCalls = toolCalls.filter(Boolean);
226
+ const assistantMessage = currentMessage || {
227
+ role: 'assistant',
228
+ content: fullContent || null,
229
+ ...(filteredToolCalls.length > 0 ? { tool_calls: filteredToolCalls } : {})
230
+ };
231
+
232
+ if (fullReasoningContent && !assistantMessage.reasoning_content) {
233
+ assistantMessage.reasoning_content = fullReasoningContent;
234
+ }
235
+
236
+ resolve({
237
+ content: fullContent,
238
+ toolCalls: filteredToolCalls,
239
+ message: assistantMessage,
240
+ reasoningContent: fullReasoningContent,
241
+ usage
242
+ });
243
+ } else {
244
+ resolve({
245
+ content: fullContent,
246
+ reasoningContent: fullReasoningContent,
247
+ usage
248
+ });
249
+ }
250
+ });
251
+
252
+ response.body.on('error', (error) => {
253
+ cleanupAbortListener();
254
+ reject(error);
255
+ });
256
+
257
+ if (abortController) {
258
+ abortHandler = () => {
259
+ rejectWithAbort();
260
+ };
261
+ abortController.signal.addEventListener('abort', abortHandler);
262
+ }
263
+ } catch (error) {
264
+ reject(error);
265
+ }
266
+ });
267
+ };
268
+
269
+ const buildConversationForSummary = (messages) => {
270
+ const parts = [];
271
+ for (const msg of messages) {
272
+ if (msg.role === 'system') continue;
273
+
274
+ const role = msg.role === 'user' ? '用户' : msg.role === 'assistant' ? '助手' : '工具';
275
+ let content = msg.content || '';
276
+
277
+ if (msg.tool_call_id) {
278
+ parts.push(`[工具返回 ${msg.tool_call_id}]: ${content}`);
279
+ } else if (msg.tool_calls && msg.tool_calls.length > 0) {
280
+ const toolNames = msg.tool_calls.map(tc => tc.function?.name || '未知工具').join(', ');
281
+ parts.push(`[${role}]: ${content || ''}\n[调用工具: ${toolNames}]`);
282
+ } else {
283
+ parts.push(`[${role}]: ${content}`);
284
+ }
285
+ }
286
+ return parts.join('\n\n');
287
+ };
288
+
289
+ const performCompactSummary = async (endpoint, messages, abortController) => {
290
+ const conversationText = buildConversationForSummary(messages);
291
+
292
+ const summaryMessages = [
293
+ {
294
+ role: 'system',
295
+ content: COMPACT_SUMMARY_PROMPT
296
+ },
297
+ {
298
+ role: 'user',
299
+ content: `以下是需要总结的对话内容:\n\n${conversationText}`
300
+ }
301
+ ];
302
+
303
+ const requestBody = {
304
+ stream: false,
305
+ messages: summaryMessages,
306
+ model: endpoint.model
307
+ };
308
+
309
+ const url = joinUrl(endpoint.baseUrl, '/chat/completions');
310
+
311
+ try {
312
+ const response = await fetch(url, createFetchOptions(endpoint, requestBody, abortController));
313
+
314
+ if (!response.ok) {
315
+ let detail = '';
316
+ try {
317
+ const body = await response.text();
318
+ detail = body ? `: ${body}` : '';
319
+ } catch (_) {}
320
+ throw new Error(`HTTP ${response.status}${detail}`);
321
+ }
322
+
323
+ const data = await response.json();
324
+ const choice = data.choices && data.choices[0];
325
+
326
+ if (choice && choice.message && choice.message.content) {
327
+ return choice.message.content;
328
+ }
329
+
330
+ throw new Error('无法获取总结内容');
331
+ } catch (error) {
332
+ if (error.name === 'AbortError') {
333
+ throw error;
334
+ }
335
+ console.error('Compact 总结请求失败:', error);
336
+ throw error;
337
+ }
338
+ };
339
+
340
+ module.exports = {
341
+ makeRequestWithRetry,
342
+ processStreamResponse,
343
+ buildConversationForSummary,
344
+ performCompactSummary
345
+ };