my-ai-chat-framework 2.7.0 → 4.0.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/CHANGELOG.md +117 -0
- package/LICENSE +20 -20
- package/README.md +103 -16
- package/README_ZH.md +392 -0
- package/dist/my-ai-chat-framework.browser.es.js +1238 -421
- package/dist/my-ai-chat-framework.browser.es.js.map +1 -1
- package/dist/my-ai-chat-framework.browser.umd.js +1245 -420
- package/dist/my-ai-chat-framework.browser.umd.js.map +1 -1
- package/dist/my-ai-chat-framework.node.cjs.js +1245 -420
- package/dist/my-ai-chat-framework.node.cjs.js.map +1 -1
- package/docs/DEVELOPER.md +569 -0
- package/docs/README.md +33 -0
- package/package.json +31 -8
- package/src/adapters/openai.js +124 -23
- package/src/core/ChatService.js +596 -133
- package/src/core/Errors.js +72 -59
- package/src/core/MessageStore.js +27 -0
- package/src/core/Pipeline.js +48 -0
- package/src/core/SystemPromptStore.js +120 -118
- package/src/index.js +7 -4
- package/src/plugins/continuation.js +156 -0
- package/src/plugins/model-registry.js +236 -187
- package/src/plugins/tool-calling.js +245 -187
- package/src/utils/MessageFormatter.js +296 -204
- package/src/utils/typeCheck.js +11 -11
- package/src/utils/url.js +17 -17
package/src/adapters/openai.js
CHANGED
|
@@ -5,32 +5,68 @@
|
|
|
5
5
|
|
|
6
6
|
import { joinUrl } from "../utils/url.js";
|
|
7
7
|
import { isString } from "../utils/typeCheck.js";
|
|
8
|
-
import { APIError, NetworkError, ParsingError } from '../core/Errors.js';
|
|
8
|
+
import { APIError, NetworkError, ParsingError, ConfigurationError } from '../core/Errors.js';
|
|
9
9
|
import { MessageFormatter } from '../utils/MessageFormatter.js';
|
|
10
10
|
|
|
11
|
+
// 属于"运输层"的配置键:工厂实例持有,请求时覆盖 config 中同名项
|
|
12
|
+
const TRANSPORT_KEYS = ['apiKey', 'baseUrl', 'apiUrl', 'path', 'headers'];
|
|
13
|
+
|
|
11
14
|
export const openaiAdapter = {
|
|
12
15
|
name: 'openai',
|
|
13
16
|
|
|
14
|
-
|
|
17
|
+
/**
|
|
18
|
+
* 安装适配器。
|
|
19
|
+
* - 单例(openaiAdapter):options 无意义,配置从 chat.config 读取(平铺兼容路径)
|
|
20
|
+
* - 工厂实例(createOpenAIAdapter):options 并入实例自持的运输配置
|
|
21
|
+
*/
|
|
22
|
+
install(chatService, options = {}) {
|
|
23
|
+
if (this._transport) this._transport = { ...this._transport, ...options };
|
|
15
24
|
chatService.setAdapter(this);
|
|
16
25
|
},
|
|
17
26
|
|
|
27
|
+
/**
|
|
28
|
+
* 实例自持配置(运输层)覆盖调用方传入的 config 同名项。
|
|
29
|
+
* modelParams 特殊处理:深度合并(config 优先,transport 兜底)——
|
|
30
|
+
* 避免工厂实例的默认参数覆盖掉会话/请求级覆盖。
|
|
31
|
+
*/
|
|
32
|
+
_resolveConfig(config) {
|
|
33
|
+
const transport = this._transport || {};
|
|
34
|
+
const resolved = { ...config, ...transport };
|
|
35
|
+
if (transport.modelParams) {
|
|
36
|
+
resolved.modelParams = { ...transport.modelParams, ...(config.modelParams || {}) };
|
|
37
|
+
}
|
|
38
|
+
return resolved;
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
/** 工厂实例可提供给会话层的"请求默认参数"(非运输键的部分) */
|
|
42
|
+
getRequestDefaults() {
|
|
43
|
+
if (!this._transport) return {};
|
|
44
|
+
const defaults = {};
|
|
45
|
+
for (const [key, value] of Object.entries(this._transport)) {
|
|
46
|
+
if (!TRANSPORT_KEYS.includes(key) && value !== undefined) defaults[key] = value;
|
|
47
|
+
}
|
|
48
|
+
return defaults;
|
|
49
|
+
},
|
|
50
|
+
|
|
18
51
|
/**
|
|
19
52
|
* 构建 OpenAI 格式的请求体
|
|
20
53
|
* @param {Array} messages - 内部消息列表
|
|
21
|
-
* @param {Object} config -
|
|
54
|
+
* @param {Object} config - 合并后的请求参数(含 model/temperature 等)
|
|
22
55
|
* @param {Array} [systemPrompts=[]] - 启用的 system prompt 列表
|
|
23
56
|
* @returns {Object} 请求体
|
|
24
57
|
*/
|
|
25
58
|
buildRequest(messages, config, systemPrompts = []) {
|
|
59
|
+
config = this._resolveConfig(config);
|
|
26
60
|
const model = config.model || config.modelParams?.model;
|
|
27
61
|
if (!model) {
|
|
28
|
-
throw new
|
|
62
|
+
throw new ConfigurationError('Missing required config: model (either at top level or in modelParams)');
|
|
29
63
|
}
|
|
30
64
|
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
const
|
|
65
|
+
const mp = config.modelParams || {};
|
|
66
|
+
// v4.0:**不再提供默认值** —— 没传就不放进请求体,由 API 用自己的默认
|
|
67
|
+
const temperature = mp.temperature ?? config.temperature;
|
|
68
|
+
const maxTokens = mp.maxTokens ?? config.maxTokens;
|
|
69
|
+
const reasoningEffort = mp.reasoningEffort ?? config.reasoningEffort;
|
|
34
70
|
|
|
35
71
|
// 用 MessageFormatter 统一处理:system 置顶、消息过滤、字段映射、图片解析
|
|
36
72
|
// format 由 config.messageFormat 指定(默认 'openai')
|
|
@@ -45,20 +81,42 @@ export const openaiAdapter = {
|
|
|
45
81
|
const requestBody = {
|
|
46
82
|
model,
|
|
47
83
|
messages: apiMessages,
|
|
48
|
-
temperature,
|
|
49
|
-
max_tokens: maxTokens,
|
|
50
84
|
stream: false
|
|
51
85
|
};
|
|
52
86
|
|
|
87
|
+
// v4.0:可选参数只在"显式传了"时才放进请求体(不替使用者决定要什么默认值)
|
|
88
|
+
if (temperature !== undefined) requestBody.temperature = temperature;
|
|
89
|
+
if (maxTokens !== undefined) requestBody.max_tokens = maxTokens;
|
|
90
|
+
|
|
91
|
+
// 硬编码常用参数:驼峰命名,适配器负责转 API 格式
|
|
92
|
+
if (mp.topP !== undefined) requestBody.top_p = mp.topP;
|
|
93
|
+
if (mp.frequencyPenalty !== undefined) requestBody.frequency_penalty = mp.frequencyPenalty;
|
|
94
|
+
if (mp.presencePenalty !== undefined) requestBody.presence_penalty = mp.presencePenalty;
|
|
95
|
+
if (mp.stop !== undefined) requestBody.stop = mp.stop;
|
|
96
|
+
if (mp.responseFormat !== undefined) requestBody.response_format = mp.responseFormat;
|
|
97
|
+
if (mp.seed !== undefined) requestBody.seed = mp.seed;
|
|
98
|
+
|
|
53
99
|
if (config.tools && Array.isArray(config.tools) && config.tools.length > 0) {
|
|
54
100
|
requestBody.tools = config.tools;
|
|
55
101
|
requestBody.tool_choice = 'auto';
|
|
56
102
|
}
|
|
57
103
|
|
|
58
|
-
if (reasoningEffort
|
|
104
|
+
if (reasoningEffort !== undefined) {
|
|
59
105
|
requestBody.reasoning_effort = reasoningEffort;
|
|
60
106
|
}
|
|
61
107
|
|
|
108
|
+
// modelParams 中未被硬编码处理的字段,透传到请求体
|
|
109
|
+
const consumedKeys = new Set([
|
|
110
|
+
'model', 'temperature', 'maxTokens', 'reasoningEffort',
|
|
111
|
+
'topP', 'frequencyPenalty', 'presencePenalty',
|
|
112
|
+
'stop', 'responseFormat', 'seed'
|
|
113
|
+
]);
|
|
114
|
+
for (const [key, value] of Object.entries(mp)) {
|
|
115
|
+
if (!consumedKeys.has(key) && value !== undefined) {
|
|
116
|
+
requestBody[key] = value;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
62
120
|
return requestBody;
|
|
63
121
|
},
|
|
64
122
|
|
|
@@ -93,6 +151,7 @@ export const openaiAdapter = {
|
|
|
93
151
|
*/
|
|
94
152
|
async send(requestBody, config, options = {}) {
|
|
95
153
|
try {
|
|
154
|
+
config = this._resolveConfig(config);
|
|
96
155
|
const url = this._getUrl(config);
|
|
97
156
|
|
|
98
157
|
const headers = {
|
|
@@ -156,6 +215,7 @@ export const openaiAdapter = {
|
|
|
156
215
|
* @param {Object} [options] — { signal, headers: {...} }
|
|
157
216
|
*/
|
|
158
217
|
async stream(requestBody, config, onProgress, onDone, options = {}) {
|
|
218
|
+
config = this._resolveConfig(config);
|
|
159
219
|
const streamBody = { ...requestBody, stream: true };
|
|
160
220
|
const url = this._getUrl(config);
|
|
161
221
|
let response;
|
|
@@ -205,20 +265,37 @@ export const openaiAdapter = {
|
|
|
205
265
|
const { done, value } = await reader.read();
|
|
206
266
|
if (done) break;
|
|
207
267
|
buffer += decoder.decode(value, { stream: true });
|
|
208
|
-
const lines = buffer.split(/\n\n/);
|
|
209
|
-
buffer = lines.pop();
|
|
210
268
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
269
|
+
// ===== v4.0:稳健的 SSE 事件解析 =====
|
|
270
|
+
// 支持:\r\n 换行、`data:`(无空格)、多行 data(按 SSE 规范用 \n 连接)、注释行
|
|
271
|
+
let sepMatch;
|
|
272
|
+
const EVENT_SEP = /\r?\n\r?\n/;
|
|
273
|
+
while ((sepMatch = EVENT_SEP.exec(buffer)) !== null) {
|
|
274
|
+
const rawEvent = buffer.slice(0, sepMatch.index);
|
|
275
|
+
buffer = buffer.slice(sepMatch.index + sepMatch[0].length);
|
|
276
|
+
|
|
277
|
+
const dataPayload = rawEvent
|
|
278
|
+
.split(/\r?\n/)
|
|
279
|
+
.filter(l => l.startsWith('data:'))
|
|
280
|
+
.map(l => l.slice(5).replace(/^ /, ''))
|
|
281
|
+
.join('\n');
|
|
282
|
+
if (!dataPayload) continue;
|
|
283
|
+
if (dataPayload.trim() === '[DONE]') { reachedDone = true; break; }
|
|
284
|
+
|
|
215
285
|
try {
|
|
216
|
-
const chunk = JSON.parse(
|
|
286
|
+
const chunk = JSON.parse(dataPayload);
|
|
217
287
|
const delta = chunk.choices?.[0]?.delta;
|
|
218
288
|
if (!delta) continue;
|
|
219
289
|
|
|
220
|
-
|
|
290
|
+
// v4.0:本次 chunk 的"增量"(onProgress 第二个参数)
|
|
291
|
+
const deltaInfo = { content: '', reasoningContent: '', toolCalls: null };
|
|
292
|
+
|
|
293
|
+
if (delta.content) {
|
|
294
|
+
accumulated.content += delta.content;
|
|
295
|
+
deltaInfo.content = delta.content;
|
|
296
|
+
}
|
|
221
297
|
if (delta.tool_calls) {
|
|
298
|
+
const newCalls = [];
|
|
222
299
|
if (!accumulated.toolCalls) accumulated.toolCalls = [];
|
|
223
300
|
for (const toolCallDelta of delta.tool_calls) {
|
|
224
301
|
const index = toolCallDelta.index;
|
|
@@ -237,16 +314,22 @@ export const openaiAdapter = {
|
|
|
237
314
|
if (toolCallDelta.function?.arguments) {
|
|
238
315
|
accumulated.toolCalls[index].function.arguments += toolCallDelta.function.arguments;
|
|
239
316
|
}
|
|
317
|
+
newCalls.push({ index, delta: toolCallDelta });
|
|
240
318
|
}
|
|
319
|
+
deltaInfo.toolCalls = newCalls;
|
|
320
|
+
}
|
|
321
|
+
if (delta.reasoning_content) {
|
|
322
|
+
accumulated.reasoningContent += delta.reasoning_content;
|
|
323
|
+
deltaInfo.reasoningContent = delta.reasoning_content;
|
|
241
324
|
}
|
|
242
|
-
if (delta.reasoning_content) accumulated.reasoningContent += delta.reasoning_content;
|
|
243
325
|
|
|
244
|
-
//
|
|
245
|
-
onProgress({ ...accumulated });
|
|
326
|
+
// onProgress(snap, delta):snap = 全量累积快照;delta = 本次新增片段
|
|
327
|
+
onProgress({ ...accumulated }, deltaInfo);
|
|
246
328
|
} catch (e) {
|
|
247
|
-
console.warn('stream parsing failed :', e,
|
|
329
|
+
console.warn('stream parsing failed :', e, dataPayload);
|
|
248
330
|
}
|
|
249
331
|
}
|
|
332
|
+
if (reachedDone) break;
|
|
250
333
|
}
|
|
251
334
|
|
|
252
335
|
const finalMessage = {
|
|
@@ -277,4 +360,22 @@ export const openaiAdapter = {
|
|
|
277
360
|
throw new APIError(errorMessage, response.status, null, errorText);
|
|
278
361
|
}
|
|
279
362
|
|
|
280
|
-
};
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* 创建 OpenAI 兼容适配器实例(工厂)。
|
|
367
|
+
*
|
|
368
|
+
* 与单例 openaiAdapter 的区别:每个实例自持一份运输配置(apiKey/baseUrl/apiUrl/path/headers),
|
|
369
|
+
* 互不干扰——解决"同一适配器装到多个 ChatService 互相覆盖"的单例陷阱。
|
|
370
|
+
*
|
|
371
|
+
* 用法:
|
|
372
|
+
* const adapter = createOpenAIAdapter({ apiKey, baseUrl, modelParams: { temperature: 0.8 } });
|
|
373
|
+
* const chat = new ChatService({ adapter, model: 'deepseek-chat' });
|
|
374
|
+
* chat.use(adapter); // 或直接 new ChatService({ adapter })
|
|
375
|
+
*
|
|
376
|
+
* 非运输键(model/modelParams/messageFormat/resolveImage/capabilities 等)会作为
|
|
377
|
+
* "请求默认参数"供会话层合并,单次请求仍可覆盖。
|
|
378
|
+
*/
|
|
379
|
+
export function createOpenAIAdapter(options = {}) {
|
|
380
|
+
return { ...openaiAdapter, _transport: { ...options } };
|
|
381
|
+
}
|