my-ai-chat-framework 2.0.0 → 2.7.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.
@@ -1,110 +1,298 @@
1
- import { EventEmitter } from './EventEmitter.js';
2
- import { MessageStore } from './MessageStore.js';
3
-
4
- /**
5
- * 核心聊天服务
6
- * 极简设计:只负责消息存储、事件发射、插件管理、请求委托
7
- */
8
- export class ChatService extends EventEmitter {
9
- constructor(config = {}) {
10
- super();
11
- this.config = {
12
- apiKey: config.apiKey || '',
13
- model: config.model || 'gpt-3.5-turbo',
14
- ...config
15
- };
16
- this.messages = new MessageStore();
17
- this._plugins = [];
18
- this._adapter = null; // 当前使用的 API 适配器
19
- this._pendingRequest = null;
20
- }
21
-
22
- /**
23
- * 加载插件
24
- * @param {object} plugin - 必须包含 install 方法
25
- */
26
- use(plugin) {
27
- if (typeof plugin.install === 'function') {
28
- plugin.install(this);
29
- this._plugins.push(plugin);
30
- } else {
31
- throw new Error('插件必须提供 install 方法');
32
- }
33
- return this;
34
- }
35
-
36
- /**
37
- * 设置 API 适配器
38
- */
39
- setAdapter(adapter) {
40
- this._adapter = adapter;
41
- return this;
42
- }
43
-
44
- /**
45
- * 发送消息(非流式)
46
- * @param {string|object} input - 字符串或消息对象
47
- */
48
- async send(input) {
49
- const userMsg = typeof input === 'string'
50
- ? { role: 'user', content: input }
51
- : { role: 'user', ...input };
52
-
53
- this.messages.add(userMsg);
54
- this.emit('sending', userMsg);
55
-
56
- // 构建请求
57
- const requestBody = this._adapter.buildRequest(this.messages.getAll(), this.config);
58
- const response = await this._adapter.send(requestBody, this.config);
59
- const assistantMsg = this._adapter.parseResponse(response);
60
-
61
- this.messages.add(assistantMsg);
62
- this.emit('message', assistantMsg);
63
- return assistantMsg;
64
- }
65
-
66
- /**
67
- * 流式发送消息
68
- * @param {string|object} input
69
- * @param {function} onProgress - 每次收到数据块时调用,参数为累积的当前消息对象
70
- * @param {function} onDone - 完成时调用,参数为最终消息对象
71
- */
72
- async stream(input, onProgress, onDone) {
73
- const userMsg = typeof input === 'string'
74
- ? { role: 'user', content: input }
75
- : { role: 'user', ...input };
76
-
77
- this.messages.add(userMsg);
78
- this.emit('sending', userMsg);
79
-
80
- const requestBody = this._adapter.buildRequest(this.messages.getAll(), this.config);
81
-
82
- let accumulated = { role: 'assistant', content: '' };
83
- await this._adapter.stream(
84
- requestBody,
85
- this.config,
86
- (chunk) => {
87
- // 累积消息
88
- if (chunk.content) accumulated.content += chunk.content;
89
- if (chunk.toolCalls) accumulated.toolCalls = chunk.toolCalls;
90
- if (chunk.reasoningContent) accumulated.reasoningContent = chunk.reasoningContent;
91
- // 触发进度事件
92
- this.emit('stream-progress', { ...accumulated });
93
- if (onProgress) onProgress({ ...accumulated });
94
- },
95
- (finalMessage) => {
96
- this.messages.add(finalMessage);
97
- this.emit('message', finalMessage);
98
- if (onDone) onDone(finalMessage);
99
- }
100
- );
101
- }
102
-
103
- /**
104
- * 注册工具(由工具调用插件实现)
105
- * 这里只留一个空方法,插件会覆盖它
106
- */
107
- registerTool(name, description, executor) {
108
- throw new Error('工具调用插件未加载,请先使用 use(toolCallingPlugin)');
109
- }
110
- }
1
+ import { EventEmitter } from './EventEmitter.js';
2
+ import { MessageStore } from './MessageStore.js';
3
+ import { SystemPromptStore } from './SystemPromptStore.js';
4
+ import { ConfigurationError, NetworkError } from './Errors.js';
5
+
6
+ export class ChatService extends EventEmitter {
7
+ constructor(config = {}) {
8
+ super();
9
+ this.config = { ...config };
10
+ this.messages = new MessageStore();
11
+ this.systemPrompts = new SystemPromptStore();
12
+ this._adapter = null;
13
+ this._abortController = null;
14
+ this._isGenerating = false;
15
+ this._hooks = new EventEmitter();
16
+ this._processResponse = null;
17
+
18
+ const model = this.config.model || this.config.modelParams?.model;
19
+ if (!model || typeof model !== 'string' || !model.trim()) {
20
+ throw new ConfigurationError('缺少 model 配置');
21
+ }
22
+
23
+ const temperature = this.config.modelParams?.temperature ?? this.config.temperature;
24
+ if (temperature !== undefined && (typeof temperature !== 'number' || temperature < 0 || temperature > 2)) {
25
+ throw new ConfigurationError(`temperature 必须在 0-2 之间,当前值: ${temperature}`);
26
+ }
27
+
28
+ const maxTokens = this.config.modelParams?.maxTokens ?? this.config.maxTokens;
29
+ if (maxTokens !== undefined && (typeof maxTokens !== 'number' || maxTokens < 1 || !Number.isInteger(maxTokens))) {
30
+ throw new ConfigurationError(`maxTokens 必须为正整数,当前值: ${maxTokens}`);
31
+ }
32
+
33
+ if (typeof config.system === 'string' && config.system.trim()) {
34
+ this.systemPrompts.set(config.system);
35
+ }
36
+ }
37
+
38
+ use(plugin) {
39
+ plugin.install(this);
40
+ return this;
41
+ }
42
+
43
+ setAdapter(adapter) {
44
+ this._adapter = adapter;
45
+ }
46
+
47
+ abort() {
48
+ if (this._abortController) this._abortController.abort();
49
+ }
50
+
51
+ get isGenerating() { return this._isGenerating; }
52
+
53
+ async continueLast() {
54
+ const target = this._prepareContinue();
55
+ this.messages.update(target.id, { prefix: true });
56
+ try {
57
+ await this._request({ addUser: false, isStream: false, mergeToEntry: target.id });
58
+ this.messages.update(target.id, {
59
+ _complete: true, prefix: undefined, _ephemeral: undefined
60
+ });
61
+ return target;
62
+ } catch (err) {
63
+ this.messages.update(target.id, { prefix: undefined });
64
+ throw err;
65
+ }
66
+ }
67
+
68
+ async continueLastStream(onProgress, onDone) {
69
+ const target = this._prepareContinue();
70
+ console.log('[DEBUG] continueLastStream target.id:', target.id);
71
+ this.messages.update(target.id, { prefix: true });
72
+ const baseLen = (target.content || '').length;
73
+
74
+ try {
75
+ await this._request({
76
+ addUser: false, isStream: true, mergeToEntry: target.id,
77
+ onProgress: (chunk) => {
78
+ if (onProgress) {
79
+ onProgress({ ...chunk, content: (chunk.content || '').slice(baseLen) });
80
+ }
81
+ },
82
+ onDone: (final) => { if (onDone) onDone(final); }
83
+ });
84
+ this.messages.update(target.id, {
85
+ _complete: true, prefix: undefined, _ephemeral: undefined
86
+ });
87
+ return target;
88
+ } catch (err) {
89
+ this.messages.update(target.id, { prefix: undefined });
90
+ throw err;
91
+ }
92
+ }
93
+
94
+ _prepareContinue() {
95
+ const msgs = this.messages.getAll();
96
+ const last = msgs[msgs.length - 1];
97
+ if (last && (last.role === 'assistant' || last._ephemeral)) return last;
98
+ throw new Error('最后一条消息不是 assistant,无法续写');
99
+ }
100
+
101
+ updateConfig(partial) {
102
+ Object.assign(this.config, partial);
103
+ if (typeof partial.system === 'string') this.systemPrompts.set(partial.system);
104
+ this.emit('config-updated', { changes: partial, timestamp: Date.now() });
105
+ }
106
+
107
+ // ========== 编排角色 ==========
108
+ async _request(options) {
109
+ let { userInput, addUser = true, isStream = false, onProgress, onDone, mergeToEntry } = options;
110
+
111
+ this.emit('sending', { addUser, userInput, timestamp: Date.now() });
112
+ this._addUserMessage(userInput, addUser);
113
+
114
+ // 发送前钩子:在 adapter 转换 messages 之前执行,可修改 messages/config
115
+ await this._hooks.emitAsync('beforeRequest', {
116
+ messages: this.messages,
117
+ config: this.config,
118
+ options
119
+ });
120
+
121
+ // 自动续写:启用 autoContinue 时,检测底部是否有 prefix 标记的消息,自动设为续写目标
122
+ // 用于 ephemeral 注入场景:注入一条带 prefix 的引导消息后,自动合并结果回原消息
123
+ // 不启用时 prefix 仅透传给 API,结果仍作为新消息追加
124
+ if (this.config.ephemeralContinue && !mergeToEntry) {
125
+ const msgs = this.messages.getAll();
126
+ const last = msgs[msgs.length - 1];
127
+ if (last && last.prefix && (last.role === 'assistant' || last._ephemeral)) {
128
+ mergeToEntry = last.id;
129
+ }
130
+ }
131
+
132
+ const body = this._adapter.buildRequest(
133
+ this.messages.getAll(), this.config, this.systemPrompts.getEnabled()
134
+ );
135
+
136
+ const retryCfg = this.config.retry || {};
137
+ try {
138
+ return await this._withRetry(body, {
139
+ isStream, onProgress, onDone, mergeToEntry,
140
+ maxRetries: retryCfg.maxRetries ?? 0,
141
+ retryDelay: retryCfg.retryDelay ?? 1000
142
+ });
143
+ } catch (error) {
144
+ this.emit('error', { error, timestamp: Date.now() });
145
+ throw error;
146
+ }
147
+ }
148
+
149
+ _addUserMessage(userInput, addUser) {
150
+ if (addUser && userInput !== undefined) {
151
+ const msg = typeof userInput === 'string'
152
+ ? { role: 'user', content: userInput }
153
+ : { role: 'user', ...userInput };
154
+ this.messages.add(msg);
155
+ }
156
+ }
157
+
158
+ /** retry 循环,占位消息只 push 一次 */
159
+ async _withRetry(body, { isStream, onProgress, onDone, mergeToEntry, maxRetries, retryDelay }) {
160
+ let lastError = null;
161
+ const adapterOptions = { signal: this._abortController?.signal };
162
+
163
+ let placeholder = null;
164
+ let base = null;
165
+ if (isStream) {
166
+ if (mergeToEntry) {
167
+ placeholder = this.messages.getAll().find(m => m.id === mergeToEntry);
168
+ if (placeholder) base = { content: placeholder.content || '', reasoning: placeholder.reasoningContent || '' };
169
+ }
170
+ if (!placeholder) {
171
+ placeholder = this.messages.add({ role: 'assistant', content: '', _complete: false });
172
+ }
173
+ }
174
+
175
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
176
+ if (attempt > 0) {
177
+ this.emit('retry', { attempt, maxRetries, lastError, timestamp: Date.now() });
178
+ await new Promise(r => setTimeout(r, retryDelay));
179
+ }
180
+ try {
181
+ this._isGenerating = true;
182
+ if (isStream) {
183
+ return await this._stream(body, adapterOptions, { placeholder, base, mergeToEntry, onProgress, onDone });
184
+ } else {
185
+ const resp = await this._adapter.send(body, this.config, adapterOptions);
186
+ let result = this._handleResult(this._adapter.parseResponse(resp), mergeToEntry);
187
+ // 响应后处理钩子(tool-calling 用),仅非续写时
188
+ if (this._processResponse && !mergeToEntry) {
189
+ result = await this._processResponse(result, { isStream: false });
190
+ }
191
+ return result;
192
+ }
193
+ } catch (error) {
194
+ lastError = error;
195
+ if (error.name === 'AbortError' || this._abortController?.signal.aborted) {
196
+ this._isGenerating = false; throw error;
197
+ }
198
+ if (error instanceof NetworkError && attempt < maxRetries) continue;
199
+ this._isGenerating = false; throw error;
200
+ }
201
+ }
202
+ this._isGenerating = false;
203
+ throw lastError;
204
+ }
205
+
206
+ /** 流式:ChatService 维护占位,adapter 只管解析 */
207
+ async _stream(body, adapterOptions, { placeholder, base, mergeToEntry, onProgress, onDone }) {
208
+ let finalMsg = null;
209
+
210
+ await this._adapter.stream(body, this.config,
211
+ (snap) => {
212
+ placeholder.content = snap.content || '';
213
+ if (snap.reasoningContent) placeholder.reasoningContent = snap.reasoningContent;
214
+ if (snap.toolCalls) placeholder.toolCalls = [...snap.toolCalls];
215
+ this.emit('stream-progress', snap);
216
+ if (onProgress) onProgress(snap);
217
+ },
218
+ async (final) => {
219
+ finalMsg = final;
220
+
221
+ if (this._processResponse && !mergeToEntry) {
222
+ finalMsg = await this._processResponse(finalMsg, {
223
+ isStream: true, onProgress, onDone
224
+ });
225
+ }
226
+
227
+ if (mergeToEntry && base) {
228
+ const newContent = base.content + (final.content || '');
229
+ const changes = { content: newContent, _complete: true };
230
+ if (final.reasoningContent) changes.reasoningContent = base.reasoning + final.reasoningContent;
231
+ if (final.toolCalls) changes.toolCalls = final.toolCalls;
232
+ this.messages.update(mergeToEntry, changes);
233
+ const updated = this.messages.getAll().find(m => m.id === mergeToEntry);
234
+ this.emit('message', updated);
235
+ if (onDone) onDone(updated);
236
+ } else {
237
+ placeholder._complete = true;
238
+ this.emit('message', finalMsg);
239
+ if (onDone) onDone(finalMsg);
240
+ }
241
+ },
242
+ adapterOptions
243
+ );
244
+ return mergeToEntry || finalMsg;
245
+ }
246
+
247
+ /** 非流式结果落位 */
248
+ _handleResult(assistantMsg, mergeToEntry) {
249
+ if (mergeToEntry) {
250
+ const entry = this.messages.getAll().find(m => m.id === mergeToEntry);
251
+ if (!entry) {
252
+ this.messages.add(assistantMsg);
253
+ this.emit('message', assistantMsg);
254
+ return assistantMsg;
255
+ }
256
+ const changes = {
257
+ content: (entry.content || '') + (assistantMsg.content || ''),
258
+ _complete: true
259
+ };
260
+ if (assistantMsg.reasoningContent) {
261
+ changes.reasoningContent = (entry.reasoningContent || '') + assistantMsg.reasoningContent;
262
+ }
263
+ if (assistantMsg.toolCalls) changes.toolCalls = assistantMsg.toolCalls;
264
+ this.messages.update(mergeToEntry, changes);
265
+ const updated = this.messages.getAll().find(m => m.id === mergeToEntry);
266
+ this.emit('message', updated);
267
+ return updated;
268
+ }
269
+ this.messages.add(assistantMsg);
270
+ this.emit('message', assistantMsg);
271
+ return assistantMsg;
272
+ }
273
+
274
+ // ========== 公共 API ==========
275
+ async send(userInput) {
276
+ this._abortController = new AbortController();
277
+ try { return await this._request({ userInput, addUser: true, isStream: false }); }
278
+ finally { this._abortController = null; this._isGenerating = false; }
279
+ }
280
+
281
+ async stream(userInput, onProgress, onDone) {
282
+ this._abortController = new AbortController();
283
+ try { return await this._request({ userInput, addUser: true, isStream: true, onProgress, onDone }); }
284
+ finally { this._abortController = null; this._isGenerating = false; }
285
+ }
286
+
287
+ async sendExisting() {
288
+ this._abortController = new AbortController();
289
+ try { return await this._request({ addUser: false, isStream: false }); }
290
+ finally { this._abortController = null; this._isGenerating = false; }
291
+ }
292
+
293
+ async sendExistingStream(onProgress, onDone) {
294
+ this._abortController = new AbortController();
295
+ try { return await this._request({ addUser: false, isStream: true, onProgress, onDone }); }
296
+ finally { this._abortController = null; this._isGenerating = false; }
297
+ }
298
+ }
@@ -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,22 @@ export class MessageStore {
70
91
  }
71
92
  return [];
72
93
  }
94
+
95
+ /**
96
+ * 更新消息:按 id 查找并合并 changes,不新增消息
97
+ * @param {string} id — 消息 id
98
+ * @param {Object} changes — 要合并的字段
99
+ * @returns {Object|null} 更新后的消息,未找到返回 null
100
+ */
101
+ update(id, changes) {
102
+ for (const msg of this._messages) {
103
+ if (msg.id === id) {
104
+ Object.assign(msg, changes);
105
+ // 更新 timestamp 便于排查
106
+ msg.timestamp = changes.timestamp || Date.now();
107
+ return msg;
108
+ }
109
+ }
110
+ return null;
111
+ }
73
112
  }
@@ -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
+ }