my-ai-chat-framework 2.0.0 → 3.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.
@@ -0,0 +1,60 @@
1
+ /**
2
+ * 自定义错误类
3
+ * 用于区分不同类型的错误,方便用户通过 `error.name` 或 `instanceof` 处理
4
+ */
5
+
6
+ // API 错误 (如 401 认证失败、400 参数错误、429 限流等)
7
+ export class APIError extends Error {
8
+ /**
9
+ * @param {string} message - 错误消息(通常来自 API 响应)
10
+ * @param {number} statusCode - HTTP 状态码
11
+ * @param {any} originalError - 原始错误对象或相关信息
12
+ * @param {string} responseText - 原始响应文本(如果有)
13
+ */
14
+ constructor(message, statusCode, originalError, responseText) {
15
+ super(message)
16
+ this.name = 'APIError'
17
+ this.statusCode = statusCode
18
+ this.originalError = originalError
19
+ this.responseText = responseText
20
+ }
21
+ }
22
+
23
+ // 网络错误 (fetch 失败、连接超时等)
24
+ export class NetworkError extends Error {
25
+ /**
26
+ * @param {string} message - 错误消息
27
+ * @param {any} originalError - 原始错误对象或相关信息
28
+ */
29
+ constructor(message, originalError) {
30
+ super(message)
31
+ this.name = 'NetworkError'
32
+ this.originalError = originalError
33
+ }
34
+ }
35
+
36
+ // 配置错误 (缺少必要配置项、配置项类型错误等)
37
+ export class ConfigurationError extends Error {
38
+ /**
39
+ * @param {string} message - 错误消息
40
+ */
41
+ constructor(message) {
42
+ super(message)
43
+ this.name = 'ConfigurationError'
44
+ }
45
+ }
46
+
47
+ // 解析错误 (解析 API 响应失败、响应格式不正确,不符合预期等)
48
+ export class ParsingError extends Error {
49
+ /**
50
+ * @param {string} message - 错误消息
51
+ * @param {any} originalError - 原始错误对象或相关信息
52
+ * @param {string} responseText - 原始响应文本(如果有)
53
+ **/
54
+ constructor(message, originalError, responseText) {
55
+ super(message)
56
+ this.name = 'ParsingError'
57
+ this.originalError = originalError
58
+ this.responseText = responseText
59
+ }
60
+ }
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * 极简事件发射器
3
+ * 支持同步/异步事件,handler 返回 Promise 时自动 await
3
4
  */
4
5
  export class EventEmitter {
5
6
  constructor() {
@@ -19,6 +20,9 @@ export class EventEmitter {
19
20
  else this._events.delete(event);
20
21
  }
21
22
 
23
+ /**
24
+ * 同步触发事件
25
+ */
22
26
  emit(event, data) {
23
27
  if (!this._events.has(event)) return;
24
28
  for (const handler of this._events.get(event)) {
@@ -29,4 +33,19 @@ export class EventEmitter {
29
33
  }
30
34
  }
31
35
  }
36
+
37
+ /**
38
+ * 异步触发事件,逐个 await handler
39
+ * handler 返回 Promise 时自动等待
40
+ */
41
+ async emitAsync(event, data) {
42
+ if (!this._events.has(event)) return;
43
+ for (const handler of this._events.get(event)) {
44
+ try {
45
+ await handler(data);
46
+ } catch (err) {
47
+ console.error(`事件 ${event} 处理出错:`, err);
48
+ }
49
+ }
50
+ }
32
51
  }
@@ -5,6 +5,10 @@
5
5
  * id?: string,
6
6
  * role: 'user'|'assistant'|'system'|'tool',
7
7
  * content: string,
8
+ * images?: Array<string>, // 图片引用(URL / id),框架不存储图片数据
9
+ * _ephemeral?: boolean, // 临时消息:仅底部连续时参与请求,续写后转正
10
+ * _complete?: boolean, // 流式是否完成(false=未完成/中断)
11
+ * prefix?: boolean, // 前缀续写标记
8
12
  * toolCalls?: Array,
9
13
  * toolCallId?: string,
10
14
  * timestamp?: number,
@@ -43,6 +47,23 @@ export class MessageStore {
43
47
  return this.add({ role: 'tool', content, toolCallId, metadata });
44
48
  }
45
49
 
50
+ /**
51
+ * 快速添加一次性的、带续写标记的 assistant 消息
52
+ * 适用于思维链引导、临时注入等场景
53
+ * @param {string} content — 引导内容
54
+ * @param {Object} [options] — { reasoningContent, prefix }
55
+ * @returns {Object} 添加的消息
56
+ */
57
+ addOnceAssistant(content, options = {}) {
58
+ return this.add({
59
+ role: 'assistant',
60
+ content,
61
+ reasoningContent: options.reasoningContent,
62
+ prefix: options.prefix !== false, // 默认开启续写
63
+ _ephemeral: true
64
+ });
65
+ }
66
+
46
67
  getLast() {
47
68
  return this._messages[this._messages.length - 1] || null;
48
69
  }
@@ -70,4 +91,49 @@ export class MessageStore {
70
91
  }
71
92
  return [];
72
93
  }
94
+ /**
95
+ * 撤回到指定消息 id 的上一个用户消息,删除两者之间的所有消息
96
+ * 适用于重发
97
+ * @param {string} id — 目标消息 id
98
+ * @returns {Array} 被删除的消息列表
99
+ */
100
+ undoToPreviousUser(id) {
101
+ // 从后往前找到指定消息及其上一个用户消息,删除两者之间的所有消息
102
+ let targetIndex = -1;
103
+ let previousUserIndex = -1;
104
+ for (let i = this._messages.length - 1; i >= 0; i--) {
105
+ if (this._messages[i].id === id) {
106
+ targetIndex = i;
107
+ }
108
+ if (targetIndex >= 0 && this._messages[i].role === 'user') {
109
+ previousUserIndex = i;
110
+ break;
111
+ }
112
+ }
113
+ if (targetIndex >= 0 && previousUserIndex >= 0) {
114
+ const removed = this._messages.splice(previousUserIndex + 1, targetIndex - previousUserIndex);
115
+ return removed;
116
+ }
117
+ return [];
118
+ }
119
+
120
+
121
+ /**
122
+ * 更新消息:按 id 查找并合并 changes,不新增消息
123
+ * @param {string} id — 消息 id
124
+ * @param {Object} changes — 要合并的字段
125
+ * @returns {Object|null} 更新后的消息,未找到返回 null
126
+ */
127
+ update(id, changes) {
128
+ for (const msg of this._messages) {
129
+ if (msg.id === id) {
130
+ Object.assign(msg, changes);
131
+ // 更新 timestamp 便于排查
132
+ msg.timestamp = changes.timestamp || Date.now();
133
+ return msg;
134
+ }
135
+ }
136
+ return null;
137
+ }
138
+
73
139
  }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Pipeline —— 极简顺序管道(v3.0 提案 · 阶段 1)
3
+ *
4
+ * 三个概念(对照 pipeline-demo.html):
5
+ * - 小车 ctx —— 本次请求的全部"状态",车间之间只通过它交接
6
+ * - 车间 stage —— { name, run(ctx) },只干一件事,不认别的车间
7
+ * - 调度 run() —— for + await,顺序执行(线性,无"回程";洋葱能力留给阶段 2 的 after 车间)
8
+ */
9
+
10
+ export class Pipeline {
11
+ constructor() {
12
+ this._stages = [];
13
+ }
14
+
15
+ /**
16
+ * 注册一个车间(追加到末尾)。
17
+ * @param {{name: string, run: (ctx: object) => void | Promise<void>}} stage
18
+ * @returns {Pipeline} this,支持链式
19
+ */
20
+ register(stage) {
21
+ if (!stage || typeof stage.run !== 'function') {
22
+ throw new TypeError('Pipeline.register: stage 需要 { name, run(ctx) },运行 run 必须是函数');
23
+ }
24
+ this._stages.push(stage);
25
+ return this;
26
+ }
27
+
28
+ /** 按名字移除车间(移除不存在的名字是安全的) */
29
+ unregister(name) {
30
+ this._stages = this._stages.filter(s => s.name !== name);
31
+ return this;
32
+ }
33
+
34
+ /** 列出当前车间名(调试用) */
35
+ names() {
36
+ return this._stages.map(s => s.name);
37
+ }
38
+
39
+ /** 把小车开过所有车间;返回 ctx(车间可改 ctx 上的字段) */
40
+ async run(ctx) {
41
+ for (const stage of this._stages) {
42
+ await stage.run(ctx);
43
+ }
44
+ return ctx;
45
+ }
46
+ }
47
+
48
+ export default Pipeline;
@@ -0,0 +1,118 @@
1
+ /**
2
+ * SystemPromptStore — 系统提示词存储
3
+ *
4
+ * 职责:管理多条 system prompt 的增删改查与开关
5
+ * 与 MessageStore 分离,因为 system prompt 是"AI 行为准则",不是对话事件
6
+ *
7
+ * 每条记录格式:
8
+ * {
9
+ * id: string,
10
+ * content: string,
11
+ * enabled: boolean,
12
+ * timestamp: number
13
+ * }
14
+ */
15
+
16
+ export class SystemPromptStore {
17
+ constructor() {
18
+ this._prompts = [];
19
+ this._idCounter = 0;
20
+ }
21
+
22
+ /**
23
+ * 添加一条 system prompt
24
+ * @param {string} content — 提示词内容
25
+ * @param {boolean} [enabled=true] — 是否启用
26
+ * @returns {Object} 添加的记录
27
+ */
28
+ add(content, enabled = true) {
29
+ if (!content || typeof content !== 'string' || !content.trim()) {
30
+ throw new Error('[SystemPromptStore] content 必须是非空字符串');
31
+ }
32
+ const record = {
33
+ id: `sys_${Date.now()}_${++this._idCounter}`,
34
+ content: content.trim(),
35
+ enabled: Boolean(enabled),
36
+ timestamp: Date.now()
37
+ };
38
+ this._prompts.push(record);
39
+ return record;
40
+ }
41
+
42
+ /**
43
+ * 删除指定索引的 system prompt
44
+ * @param {number} index
45
+ * @returns {Object} 被删除的记录
46
+ */
47
+ remove(index) {
48
+ if (index < 0 || index >= this._prompts.length) {
49
+ throw new Error(`[SystemPromptStore] 索引越界: ${index}`);
50
+ }
51
+ return this._prompts.splice(index, 1)[0];
52
+ }
53
+
54
+ /**
55
+ * 切换指定索引的启用/禁用状态
56
+ * @param {number} index
57
+ * @returns {boolean} 切换后的状态
58
+ */
59
+ toggle(index) {
60
+ if (index < 0 || index >= this._prompts.length) {
61
+ throw new Error(`[SystemPromptStore] 索引越界: ${index}`);
62
+ }
63
+ this._prompts[index].enabled = !this._prompts[index].enabled;
64
+ return this._prompts[index].enabled;
65
+ }
66
+
67
+ /**
68
+ * 更新指定索引的 content
69
+ * @param {number} index
70
+ * @param {string} content
71
+ */
72
+ update(index, content) {
73
+ if (index < 0 || index >= this._prompts.length) {
74
+ throw new Error(`[SystemPromptStore] 索引越界: ${index}`);
75
+ }
76
+ if (!content || typeof content !== 'string' || !content.trim()) {
77
+ throw new Error('[SystemPromptStore] content 必须是非空字符串');
78
+ }
79
+ this._prompts[index].content = content.trim();
80
+ this._prompts[index].timestamp = Date.now();
81
+ }
82
+
83
+ /**
84
+ * 清空并用一条 content 替换(便捷方法,常用于 config.system = '...')
85
+ * @param {string} content
86
+ */
87
+ set(content) {
88
+ this._prompts = [];
89
+ if (content && typeof content === 'string' && content.trim()) {
90
+ this.add(content, true);
91
+ }
92
+ }
93
+
94
+ /**
95
+ * 按 enabled 筛选后,转为适配器可用的格式
96
+ * @returns {Array<{role:'system', content:string}>}
97
+ */
98
+ getEnabled() {
99
+ return this._prompts
100
+ .filter(p => p.enabled)
101
+ .map(p => ({ role: 'system', content: p.content }));
102
+ }
103
+
104
+ /**
105
+ * 返回所有记录(含 enabled 状态,用于 UI 展示)
106
+ * @returns {Array}
107
+ */
108
+ getAll() {
109
+ return [...this._prompts];
110
+ }
111
+
112
+ /**
113
+ * 清空所有 system prompt
114
+ */
115
+ clear() {
116
+ this._prompts = [];
117
+ }
118
+ }
package/src/index.js CHANGED
@@ -1,15 +1,13 @@
1
- import { ChatService } from './core/ChatService.js';
2
- import { MessageStore } from './core/MessageStore.js';
3
- import { EventEmitter } from './core/EventEmitter.js';
4
- import { openaiAdapter } from './adapters/openai.js';
5
- import { toolCallingPlugin } from './plugins/tool-calling.js';
1
+ export { ChatService } from './core/ChatService.js';
2
+ export { MessageStore } from './core/MessageStore.js';
3
+ export { SystemPromptStore } from './core/SystemPromptStore.js';
4
+ export { EventEmitter } from './core/EventEmitter.js';
5
+ export { openaiAdapter, createOpenAIAdapter } from './adapters/openai.js';
6
+ export { toolCallingPlugin, createToolCallingPlugin } from './plugins/tool-calling.js';
7
+ export { modelRegistryPlugin, createModelRegistryPlugin } from './plugins/model-registry.js';
8
+ export { MessageFormatter } from './utils/MessageFormatter.js';
9
+ export { assembleMessages } from './utils/MessageFormatter.js';
6
10
 
7
- export { ChatService, MessageStore, EventEmitter, openaiAdapter, toolCallingPlugin };
8
-
9
- export default {
10
- ChatService,
11
- MessageStore,
12
- EventEmitter,
13
- openaiAdapter,
14
- toolCallingPlugin
15
- };
11
+ export { APIError, NetworkError, ConfigurationError, ParsingError } from './core/Errors.js';
12
+ export { assertAdapter } from './core/ChatService.js';
13
+ export { Pipeline } from './core/Pipeline.js';
@@ -0,0 +1,223 @@
1
+ /**
2
+ * model-registry 插件
3
+ *
4
+ * 职责:
5
+ * 1. 内置常用模型的能力标签表(model → capabilities 映射)
6
+ * 2. 安装时自动根据 config.model 查表,写入 config.capabilities
7
+ * 3. 监听 config-updated 事件,切换模型时自动同步能力
8
+ * 4. 提供 chat.registerModel(),允许用户追加/覆盖自定义模型
9
+ *
10
+ * 能力标签字段(全部可选,缺失视为 false / 未知):
11
+ * {
12
+ * reasoning: boolean — 是否返回 reasoning_content(思维链)
13
+ * vision: boolean — 是否支持图片输入
14
+ * toolCalling: boolean — 是否支持 function calling
15
+ * streaming: boolean — 是否支持流式传输
16
+ * maxInputTokens: number — 最大输入 token 数
17
+ * maxOutputTokens: number — 最大输出 token 数
18
+ * }
19
+ *
20
+ * 使用:
21
+ * chat.use(modelRegistryPlugin);
22
+ * // config.capabilities 现在已自动填充
23
+ * chat.registerModel('my-custom-model', { toolCalling: true });
24
+ */
25
+
26
+ // ====== 内置模型能力表 ======
27
+ // 数据来源:各厂商官方文档(2026-05 快照),用户可通过 registerModel() 覆盖
28
+ const BUILTIN_MODELS = {
29
+ // ---- DeepSeek ----
30
+ 'deepseek-chat': {
31
+ reasoning: false,
32
+ vision: false,
33
+ toolCalling: true,
34
+ streaming: true,
35
+ maxInputTokens: 128000,
36
+ maxOutputTokens: 8192
37
+ },
38
+ 'deepseek-reasoner': {
39
+ reasoning: true,
40
+ vision: false,
41
+ toolCalling: true,
42
+ streaming: true,
43
+ maxInputTokens: 128000,
44
+ maxOutputTokens: 8192
45
+ },
46
+
47
+ // ---- OpenAI ----
48
+ 'gpt-4o': {
49
+ reasoning: false,
50
+ vision: true,
51
+ toolCalling: true,
52
+ streaming: true,
53
+ maxInputTokens: 128000,
54
+ maxOutputTokens: 16384
55
+ },
56
+ 'gpt-4o-mini': {
57
+ reasoning: false,
58
+ vision: true,
59
+ toolCalling: true,
60
+ streaming: true,
61
+ maxInputTokens: 128000,
62
+ maxOutputTokens: 16384
63
+ },
64
+ 'gpt-3.5-turbo': {
65
+ reasoning: false,
66
+ vision: false,
67
+ toolCalling: true,
68
+ streaming: true,
69
+ maxInputTokens: 16385,
70
+ maxOutputTokens: 4096
71
+ },
72
+ 'o1': {
73
+ reasoning: true,
74
+ vision: false,
75
+ toolCalling: false,
76
+ streaming: false,
77
+ maxInputTokens: 200000,
78
+ maxOutputTokens: 100000
79
+ },
80
+ 'o3-mini': {
81
+ reasoning: true,
82
+ vision: false,
83
+ toolCalling: true,
84
+ streaming: true,
85
+ maxInputTokens: 200000,
86
+ maxOutputTokens: 100000
87
+ },
88
+
89
+ // ---- Anthropic ----
90
+ 'claude-3.5-sonnet': {
91
+ reasoning: false,
92
+ vision: true,
93
+ toolCalling: true,
94
+ streaming: true,
95
+ maxInputTokens: 200000,
96
+ maxOutputTokens: 8192
97
+ },
98
+ 'claude-3.5-haiku': {
99
+ reasoning: false,
100
+ vision: true,
101
+ toolCalling: true,
102
+ streaming: true,
103
+ maxInputTokens: 200000,
104
+ maxOutputTokens: 8192
105
+ }
106
+ };
107
+
108
+ const DEFAULT_CAPABILITIES = {
109
+ streaming: true,
110
+ toolCalling: false,
111
+ reasoning: false,
112
+ vision: false
113
+ };
114
+
115
+ /**
116
+ * 注意:请使用 createModelRegistryPlugin() 工厂创建实例。
117
+ * 默认导出的 modelRegistryPlugin 是兼容用的模块级单例,装到多个 ChatService
118
+ * 实例会互相覆盖(注册表、实例引用),新代码不要直接用单例。
119
+ */
120
+ export const modelRegistryPlugin = createModelRegistryPlugin();
121
+
122
+ /**
123
+ * 创建模型能力注册表插件实例(工厂)。
124
+ * @param {Object} [options] — { models: { '模型名': capabilities } }(合并进内置表)
125
+ */
126
+ export function createModelRegistryPlugin(options = {}) {
127
+ return {
128
+ name: 'model-registry',
129
+
130
+ _options: { ...options },
131
+ _registry: new Map([
132
+ ...Object.entries(BUILTIN_MODELS),
133
+ ...Object.entries(options.models || {})
134
+ ]),
135
+ chat: null,
136
+
137
+ /**
138
+ * 安装插件
139
+ * @param {ChatService} chatService - ChatService 实例
140
+ * @param {Object} [options] - 安装时配置(合并进 _options)
141
+ */
142
+ install(chatService, options = {}) {
143
+ this._options = { ...this._options, ...options };
144
+ this.chat = chatService;
145
+
146
+ /**
147
+ * 注册/覆盖一个模型的能力标签
148
+ * @param {string} name — 模型名称
149
+ * @param {Object} capabilities — 能力标签对象(部分字段即可,未提供的取默认值)
150
+ * @returns {ChatService}
151
+ */
152
+ chatService.registerModel = (name, capabilities = {}) => {
153
+ if (!name || typeof name !== 'string' || !name.trim()) {
154
+ throw new Error('[model-registry] 模型名称必须是非空字符串');
155
+ }
156
+ const merged = { ...DEFAULT_CAPABILITIES, ...capabilities };
157
+ this._registry.set(name.trim(), merged);
158
+ // 如果注册的模型与当前使用的模型同名,立即同步
159
+ if (chatService.config.model === name.trim()) {
160
+ this._syncCapabilities();
161
+ }
162
+ return chatService;
163
+ };
164
+
165
+ /**
166
+ * 列出所有已注册的模型名称
167
+ * @returns {Array<string>}
168
+ */
169
+ chatService.listModels = () => {
170
+ return [...this._registry.keys()];
171
+ };
172
+
173
+ // 安装时立即同步一次
174
+ this._syncCapabilities();
175
+
176
+ // 每次请求时按"本次生效的 model"(含请求级覆盖)解析 capabilities
177
+ // v3.0:beforeSend 车间,不再用 _hooks 钩子(时机与之前一致)
178
+ chatService.pipe({
179
+ name: 'model-registry-caps',
180
+ phase: 'beforeSend',
181
+ run: ({ config }) => {
182
+ if (!config.model) return;
183
+ const caps = this._registry.get(config.model);
184
+ if (caps) config.capabilities = { ...caps };
185
+ }
186
+ });
187
+
188
+ // 监听配置变更:model 变了就重新同步(兼容路径;请求时按生效 model 实时查表)
189
+ chatService.on('config-updated', ({ changes }) => {
190
+ if ('model' in changes) {
191
+ this._syncCapabilities();
192
+ }
193
+ // 如果用户手动改了 capabilities,用 merge 保留未指定的字段
194
+ if ('capabilities' in changes && !('model' in changes)) {
195
+ const current = this._lookupCapabilities();
196
+ if (current) {
197
+ chatService.config.capabilities = { ...current, ...changes.capabilities };
198
+ }
199
+ }
200
+ });
201
+ },
202
+
203
+ // ---- 内部方法 ----
204
+
205
+ /** 查表获取模型能力,未注册返回 null */
206
+ _lookupCapabilities() {
207
+ const model = this.chat.config.model;
208
+ if (!model) return null;
209
+ return this._registry.get(model) || null;
210
+ },
211
+
212
+ /** 将查到的能力写入 config.capabilities */
213
+ _syncCapabilities() {
214
+ const caps = this._lookupCapabilities();
215
+ if (caps) {
216
+ // 若用户已手动设了 capabilities,用模型的合并(用户值优先?还是模型优先?)
217
+ // 策略:模型优先,因为换了模型能力应该重置
218
+ this.chat.config.capabilities = { ...caps };
219
+ }
220
+ // 未注册的模型:不清除已有 capabilities,保留用户手动设的值
221
+ }
222
+ };
223
+ }