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/core/Errors.js
CHANGED
|
@@ -1,60 +1,73 @@
|
|
|
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
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
* @param {string}
|
|
53
|
-
|
|
54
|
-
constructor(message
|
|
55
|
-
super(message)
|
|
56
|
-
this.name = '
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
+
// 校验错误 (传参非法、索引越界、调用前提不满足等 —— 使用者传错了东西)
|
|
48
|
+
// 继承 ConfigurationError:老的 `catch (e) { if (e instanceof ConfigurationError) }` 仍然命中,
|
|
49
|
+
// 需要精细区分时用 `instanceof ValidationError`。
|
|
50
|
+
export class ValidationError extends ConfigurationError {
|
|
51
|
+
/**
|
|
52
|
+
* @param {string} message - 错误消息
|
|
53
|
+
*/
|
|
54
|
+
constructor(message) {
|
|
55
|
+
super(message)
|
|
56
|
+
this.name = 'ValidationError'
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 解析错误 (解析 API 响应失败、响应格式不正确,不符合预期等)
|
|
61
|
+
export class ParsingError extends Error {
|
|
62
|
+
/**
|
|
63
|
+
* @param {string} message - 错误消息
|
|
64
|
+
* @param {any} originalError - 原始错误对象或相关信息
|
|
65
|
+
* @param {string} responseText - 原始响应文本(如果有)
|
|
66
|
+
**/
|
|
67
|
+
constructor(message, originalError, responseText) {
|
|
68
|
+
super(message)
|
|
69
|
+
this.name = 'ParsingError'
|
|
70
|
+
this.originalError = originalError
|
|
71
|
+
this.responseText = responseText
|
|
72
|
+
}
|
|
60
73
|
}
|
package/src/core/MessageStore.js
CHANGED
|
@@ -91,6 +91,32 @@ export class MessageStore {
|
|
|
91
91
|
}
|
|
92
92
|
return [];
|
|
93
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
|
+
|
|
94
120
|
|
|
95
121
|
/**
|
|
96
122
|
* 更新消息:按 id 查找并合并 changes,不新增消息
|
|
@@ -109,4 +135,5 @@ export class MessageStore {
|
|
|
109
135
|
}
|
|
110
136
|
return null;
|
|
111
137
|
}
|
|
138
|
+
|
|
112
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;
|
|
@@ -1,118 +1,120 @@
|
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
*
|
|
26
|
-
* @
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
*
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
*
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
*
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
+
import { ValidationError } from './Errors.js';
|
|
17
|
+
|
|
18
|
+
export class SystemPromptStore {
|
|
19
|
+
constructor() {
|
|
20
|
+
this._prompts = [];
|
|
21
|
+
this._idCounter = 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 添加一条 system prompt
|
|
26
|
+
* @param {string} content — 提示词内容
|
|
27
|
+
* @param {boolean} [enabled=true] — 是否启用
|
|
28
|
+
* @returns {Object} 添加的记录
|
|
29
|
+
*/
|
|
30
|
+
add(content, enabled = true) {
|
|
31
|
+
if (!content || typeof content !== 'string' || !content.trim()) {
|
|
32
|
+
throw new ValidationError('[SystemPromptStore] content 必须是非空字符串');
|
|
33
|
+
}
|
|
34
|
+
const record = {
|
|
35
|
+
id: `sys_${Date.now()}_${++this._idCounter}`,
|
|
36
|
+
content: content.trim(),
|
|
37
|
+
enabled: Boolean(enabled),
|
|
38
|
+
timestamp: Date.now()
|
|
39
|
+
};
|
|
40
|
+
this._prompts.push(record);
|
|
41
|
+
return record;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 删除指定索引的 system prompt
|
|
46
|
+
* @param {number} index
|
|
47
|
+
* @returns {Object} 被删除的记录
|
|
48
|
+
*/
|
|
49
|
+
remove(index) {
|
|
50
|
+
if (index < 0 || index >= this._prompts.length) {
|
|
51
|
+
throw new ValidationError(`[SystemPromptStore] 索引越界: ${index}`);
|
|
52
|
+
}
|
|
53
|
+
return this._prompts.splice(index, 1)[0];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 切换指定索引的启用/禁用状态
|
|
58
|
+
* @param {number} index
|
|
59
|
+
* @returns {boolean} 切换后的状态
|
|
60
|
+
*/
|
|
61
|
+
toggle(index) {
|
|
62
|
+
if (index < 0 || index >= this._prompts.length) {
|
|
63
|
+
throw new ValidationError(`[SystemPromptStore] 索引越界: ${index}`);
|
|
64
|
+
}
|
|
65
|
+
this._prompts[index].enabled = !this._prompts[index].enabled;
|
|
66
|
+
return this._prompts[index].enabled;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 更新指定索引的 content
|
|
71
|
+
* @param {number} index
|
|
72
|
+
* @param {string} content
|
|
73
|
+
*/
|
|
74
|
+
update(index, content) {
|
|
75
|
+
if (index < 0 || index >= this._prompts.length) {
|
|
76
|
+
throw new ValidationError(`[SystemPromptStore] 索引越界: ${index}`);
|
|
77
|
+
}
|
|
78
|
+
if (!content || typeof content !== 'string' || !content.trim()) {
|
|
79
|
+
throw new ValidationError('[SystemPromptStore] content 必须是非空字符串');
|
|
80
|
+
}
|
|
81
|
+
this._prompts[index].content = content.trim();
|
|
82
|
+
this._prompts[index].timestamp = Date.now();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 清空并用一条 content 替换(便捷方法,常用于 config.system = '...')
|
|
87
|
+
* @param {string} content
|
|
88
|
+
*/
|
|
89
|
+
set(content) {
|
|
90
|
+
this._prompts = [];
|
|
91
|
+
if (content && typeof content === 'string' && content.trim()) {
|
|
92
|
+
this.add(content, true);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 按 enabled 筛选后,转为适配器可用的格式
|
|
98
|
+
* @returns {Array<{role:'system', content:string}>}
|
|
99
|
+
*/
|
|
100
|
+
getEnabled() {
|
|
101
|
+
return this._prompts
|
|
102
|
+
.filter(p => p.enabled)
|
|
103
|
+
.map(p => ({ role: 'system', content: p.content }));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 返回所有记录(含 enabled 状态,用于 UI 展示)
|
|
108
|
+
* @returns {Array}
|
|
109
|
+
*/
|
|
110
|
+
getAll() {
|
|
111
|
+
return [...this._prompts];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 清空所有 system prompt
|
|
116
|
+
*/
|
|
117
|
+
clear() {
|
|
118
|
+
this._prompts = [];
|
|
119
|
+
}
|
|
120
|
+
}
|
package/src/index.js
CHANGED
|
@@ -2,10 +2,13 @@ export { ChatService } from './core/ChatService.js';
|
|
|
2
2
|
export { MessageStore } from './core/MessageStore.js';
|
|
3
3
|
export { SystemPromptStore } from './core/SystemPromptStore.js';
|
|
4
4
|
export { EventEmitter } from './core/EventEmitter.js';
|
|
5
|
-
export { openaiAdapter } from './adapters/openai.js';
|
|
6
|
-
export { toolCallingPlugin } from './plugins/tool-calling.js';
|
|
7
|
-
export { modelRegistryPlugin } from './plugins/model-registry.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 { continuationPlugin, createContinuationPlugin } from './plugins/continuation.js';
|
|
8
9
|
export { MessageFormatter } from './utils/MessageFormatter.js';
|
|
9
10
|
export { assembleMessages } from './utils/MessageFormatter.js';
|
|
10
11
|
|
|
11
|
-
export { APIError, NetworkError, ConfigurationError, ParsingError } from './core/Errors.js';
|
|
12
|
+
export { APIError, NetworkError, ConfigurationError, ValidationError, ParsingError } from './core/Errors.js';
|
|
13
|
+
export { assertAdapter } from './core/ChatService.js';
|
|
14
|
+
export { Pipeline } from './core/Pipeline.js';
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* continuation 插件(v4.0)
|
|
3
|
+
*
|
|
4
|
+
* 「续写 / 临时消息」这套玩法住在这里 —— 核心不认识它,装上才有:
|
|
5
|
+
* 1. 续写检测(替换核心的 autoContinue 步骤):底部若有带 prefix 标记的消息,就把结果合并回它
|
|
6
|
+
* 2. ephemeral 过滤器:临时消息只有"位于底部连续段"时才发给 API
|
|
7
|
+
* 3. continueLast / continueLastStream 两个 API
|
|
8
|
+
* 4. uninstall():把自己装的东西全部还原(步骤、过滤器、方法)
|
|
9
|
+
*
|
|
10
|
+
* 用法:
|
|
11
|
+
* import { createContinuationPlugin } from 'my-ai-chat-framework';
|
|
12
|
+
* chat.use(createContinuationPlugin({ autoContinue: true }));
|
|
13
|
+
*
|
|
14
|
+
* // 模拟思维链:注入一条临时引导,结果合并回上一条 assistant,不留痕
|
|
15
|
+
* chat.messages.addOnceAssistant('(内心:他肯定又要熬夜)');
|
|
16
|
+
* await chat.stream('在吗');
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { MessageFormatter } from '../utils/MessageFormatter.js';
|
|
20
|
+
import { ValidationError } from '../core/Errors.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* ephemeral 规则:只有底部连续的临时消息保留,其余丢弃(与 v3.x 核心内置行为一致)。
|
|
24
|
+
* 导出以便使用者直接注册到 MessageFormatter(不使用插件时也能用这条规则)。
|
|
25
|
+
*/
|
|
26
|
+
export function ephemeralFilterRule(msg, { index, messages }) {
|
|
27
|
+
if (!msg._ephemeral) return msg;
|
|
28
|
+
let start = messages.length;
|
|
29
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
30
|
+
if (messages[i]._ephemeral) start = i;
|
|
31
|
+
else break;
|
|
32
|
+
}
|
|
33
|
+
return index >= start ? msg : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 找续写目标:最后一条必须是 assistant 或临时消息(否则抛错)。
|
|
38
|
+
* @param {object} chat ChatService 实例
|
|
39
|
+
*/
|
|
40
|
+
export function prepareContinue(chat) {
|
|
41
|
+
const msgs = chat.messages.getAll();
|
|
42
|
+
const last = msgs[msgs.length - 1];
|
|
43
|
+
if (last && (last.role === 'assistant' || last._ephemeral)) return last;
|
|
44
|
+
throw new ValidationError('最后一条消息不是 assistant,无法续写');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 续写结束后的"转正" */
|
|
48
|
+
function finalize(chat, targetId) {
|
|
49
|
+
chat.messages.update(targetId, { _complete: true, prefix: undefined, _ephemeral: false });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 创建续写插件实例(工厂)
|
|
54
|
+
* @param {Object} [options] — { autoContinue?: boolean } 是否自动识别底部 prefix 消息并合并(默认 false)
|
|
55
|
+
*/
|
|
56
|
+
export function createContinuationPlugin(options = {}) {
|
|
57
|
+
return {
|
|
58
|
+
name: 'continuation',
|
|
59
|
+
_options: { autoContinue: false, ...options },
|
|
60
|
+
_prevEphemeralFilter: undefined,
|
|
61
|
+
_installedOn: null,
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 安装:替换续写步骤 + 接管 ephemeral 过滤 + 挂续写 API
|
|
65
|
+
* @param {import('../core/ChatService.js').ChatService} chat
|
|
66
|
+
*/
|
|
67
|
+
install(chat) {
|
|
68
|
+
this._installedOn = chat;
|
|
69
|
+
|
|
70
|
+
// 0) 登记可运行时修改的配置字段(v4.0:插件自带配置,不用改核心白名单)
|
|
71
|
+
if (typeof chat.registerConfigKeys === 'function') {
|
|
72
|
+
chat.registerConfigKeys({ autoContinue: (v) => typeof v === 'boolean' });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 1) 续写检测(用 v3.1 的出口替换核心的 autoContinue 步骤)
|
|
76
|
+
chat.replaceStage('autoContinue', (ctx) => {
|
|
77
|
+
// 运行时 `chat.updateConfig({ autoContinue })` 优先,其次取安装时的工厂 options
|
|
78
|
+
const enabled = chat.config.autoContinue ?? this._options.autoContinue;
|
|
79
|
+
if (!enabled) return; // 默认关:不替用户做选择
|
|
80
|
+
if (ctx.mergeToEntry) return;
|
|
81
|
+
const last = ctx.messages.getLast();
|
|
82
|
+
if (last && last.prefix && (last.role === 'assistant' || last._ephemeral)) {
|
|
83
|
+
ctx.mergeToEntry = last.id;
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// 2) 接管 ephemeral 过滤(同名覆盖;保存旧函数,卸载时还原)
|
|
88
|
+
this._prevEphemeralFilter = MessageFormatter.registerFilter('ephemeral', ephemeralFilterRule, { priority: 100 });
|
|
89
|
+
|
|
90
|
+
// 3) 续写 API(通过 chat.plugins.continuation 暴露,v4.0 命名空间)
|
|
91
|
+
const continueLast = async () => {
|
|
92
|
+
const target = prepareContinue(chat);
|
|
93
|
+
chat.messages.update(target.id, { prefix: true });
|
|
94
|
+
try {
|
|
95
|
+
await chat.sendExisting(undefined, { mergeToEntry: target.id });
|
|
96
|
+
finalize(chat, target.id);
|
|
97
|
+
return target;
|
|
98
|
+
} catch (err) {
|
|
99
|
+
finalize(chat, target.id);
|
|
100
|
+
if (err.name === 'AbortError') {
|
|
101
|
+
chat.emit('aborted', { timestamp: Date.now() });
|
|
102
|
+
return target;
|
|
103
|
+
}
|
|
104
|
+
throw err;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const continueLastStream = async (onProgress, onDone) => {
|
|
109
|
+
const target = prepareContinue(chat);
|
|
110
|
+
chat.messages.update(target.id, { prefix: true });
|
|
111
|
+
const baseLen = (target.content || '').length;
|
|
112
|
+
try {
|
|
113
|
+
await chat.sendExistingStream(
|
|
114
|
+
undefined,
|
|
115
|
+
{ mergeToEntry: target.id },
|
|
116
|
+
(chunk) => { if (onProgress) onProgress({ ...chunk, content: (chunk.content || '').slice(baseLen) }); },
|
|
117
|
+
(final) => { if (onDone) onDone(final); }
|
|
118
|
+
);
|
|
119
|
+
finalize(chat, target.id);
|
|
120
|
+
return target;
|
|
121
|
+
} catch (err) {
|
|
122
|
+
finalize(chat, target.id);
|
|
123
|
+
if (err.name === 'AbortError') {
|
|
124
|
+
chat.emit('aborted', { timestamp: Date.now() });
|
|
125
|
+
return target;
|
|
126
|
+
}
|
|
127
|
+
throw err;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// 交给 use() 挂到 chat.plugins['continuation']
|
|
132
|
+
return { continueLast, continueLastStream, uninstall: () => this.uninstall(chat) };
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 卸载:恢复被替换的步骤、还原过滤器、移除挂载的方法
|
|
137
|
+
* @param {import('../core/ChatService.js').ChatService} [chat]
|
|
138
|
+
*/
|
|
139
|
+
uninstall(chat = this._installedOn) {
|
|
140
|
+
if (!chat) return chat;
|
|
141
|
+
if (typeof chat.restoreStage === 'function') chat.restoreStage('autoContinue');
|
|
142
|
+
if (this._prevEphemeralFilter) {
|
|
143
|
+
MessageFormatter.registerFilter('ephemeral', this._prevEphemeralFilter, { priority: 100 });
|
|
144
|
+
} else {
|
|
145
|
+
MessageFormatter.unregisterFilter('ephemeral');
|
|
146
|
+
}
|
|
147
|
+
if (chat.plugins && chat.plugins['continuation']) delete chat.plugins['continuation'];
|
|
148
|
+
if (typeof chat.unregisterConfigKeys === 'function') chat.unregisterConfigKeys(['autoContinue']);
|
|
149
|
+
this._installedOn = null;
|
|
150
|
+
return chat;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** 兼容用单例(多实例场景请用工厂) */
|
|
156
|
+
export const continuationPlugin = createContinuationPlugin();
|