my-ai-chat-framework 2.7.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.
- package/CHANGELOG.md +65 -0
- package/LICENSE +20 -20
- package/README.md +100 -13
- package/README_ZH.md +375 -0
- package/dist/my-ai-chat-framework.browser.es.js +758 -248
- package/dist/my-ai-chat-framework.browser.es.js.map +1 -1
- package/dist/my-ai-chat-framework.browser.umd.js +762 -247
- package/dist/my-ai-chat-framework.browser.umd.js.map +1 -1
- package/dist/my-ai-chat-framework.node.cjs.js +762 -247
- package/dist/my-ai-chat-framework.node.cjs.js.map +1 -1
- package/docs/DEVELOPER.md +479 -0
- package/package.json +31 -8
- package/src/adapters/openai.js +82 -7
- package/src/core/ChatService.js +415 -61
- package/src/core/Errors.js +59 -59
- package/src/core/MessageStore.js +27 -0
- package/src/core/Pipeline.js +48 -0
- package/src/core/SystemPromptStore.js +118 -118
- package/src/index.js +6 -4
- package/src/plugins/model-registry.js +223 -187
- package/src/plugins/tool-calling.js +215 -185
- package/src/utils/MessageFormatter.js +204 -204
- package/src/utils/typeCheck.js +11 -11
- package/src/utils/url.js +17 -17
|
@@ -1,204 +1,204 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MessageFormatter — 可注册的消息格式转换器
|
|
3
|
-
*
|
|
4
|
-
* 职责:
|
|
5
|
-
* 1. 内置 OpenA I兼容格式的转换逻辑
|
|
6
|
-
* 2. 支持 register(name, fn) 注册自定义格式(如 Anthropic、Gemini 等)
|
|
7
|
-
* 3. 统一处理 capabilities 过滤(reasoning、vision 等)
|
|
8
|
-
* 4. 所有适配器通过此工具获取 API 消息数组,消除重复代码
|
|
9
|
-
*
|
|
10
|
-
* 用法:
|
|
11
|
-
* import { MessageFormatter } from 'my-ai-chat-framework';
|
|
12
|
-
*
|
|
13
|
-
* // 使用内置格式
|
|
14
|
-
* const msgs = MessageFormatter.format({
|
|
15
|
-
* messages, systemPrompts, capabilities, resolveImage
|
|
16
|
-
* }); // 默认 'openai'
|
|
17
|
-
*
|
|
18
|
-
* // 注册自定义格式
|
|
19
|
-
* MessageFormatter.register('anthropic', ({ messages, systemPrompts, capabilities }) => {
|
|
20
|
-
* // 返回 Anthropic 格式的消息数组
|
|
21
|
-
* });
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
import { isString } from './typeCheck.js';
|
|
25
|
-
|
|
26
|
-
// ====== 内置格式 ======
|
|
27
|
-
|
|
28
|
-
const IS_URL = /^https?:\/\//i;
|
|
29
|
-
|
|
30
|
-
function defaultResolveImage(imageId) {
|
|
31
|
-
if (IS_URL.test(imageId)) return { url: imageId };
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function buildMultimodalContent(textContent, images, resolveImage) {
|
|
36
|
-
const content = [{ type: 'text', text: textContent }];
|
|
37
|
-
for (const ref of images) {
|
|
38
|
-
const resolved = resolveImage(ref);
|
|
39
|
-
if (!resolved) continue;
|
|
40
|
-
if (resolved.url) {
|
|
41
|
-
content.push({ type: 'image_url', image_url: { url: resolved.url } });
|
|
42
|
-
} else if (resolved.data) {
|
|
43
|
-
const mime = resolved.mimeType || 'image/png';
|
|
44
|
-
content.push({ type: 'image_url', image_url: { url: `data:${mime};base64,${resolved.data}` } });
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return content;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** OpenAI 兼容格式 */
|
|
51
|
-
function toOpenAI({ messages, systemPrompts, capabilities, resolveImage }) {
|
|
52
|
-
const rImg = typeof resolveImage === 'function' ? resolveImage : defaultResolveImage;
|
|
53
|
-
const result = [];
|
|
54
|
-
|
|
55
|
-
// 1) system 置顶
|
|
56
|
-
for (const sp of systemPrompts) {
|
|
57
|
-
if (sp?.content && isString(sp.content) && sp.content.trim()) {
|
|
58
|
-
result.push({ role: 'system', content: sp.content.trim() });
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// 2) 识别底部连续 ephemeral(只保留这些临时消息,其他 ephemeral 忽略)
|
|
63
|
-
let ephemEnd = messages.length;
|
|
64
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
65
|
-
if (messages[i]._ephemeral) ephemEnd = i;
|
|
66
|
-
else break;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// 3) 过滤 + 字段映射
|
|
70
|
-
for (let i = 0; i < messages.length; i++) {
|
|
71
|
-
const msg = messages[i];
|
|
72
|
-
|
|
73
|
-
// ephemeral 消息:仅底部连续的保留,其余跳过
|
|
74
|
-
if (msg._ephemeral) {
|
|
75
|
-
if (i < ephemEnd) continue;
|
|
76
|
-
// 底部 ephemeral 按原始 role 处理,不加 _ephemeral 标记
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// 非底部 ephemeral 的正常消息仍保留原有过滤
|
|
80
|
-
if (msg.role === 'system' && !msg._ephemeral) continue;
|
|
81
|
-
|
|
82
|
-
if (msg.role === 'tool') {
|
|
83
|
-
result.push({ role: 'tool', content: msg.content || '', tool_call_id: msg.toolCallId });
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (msg.role === 'assistant' && msg.toolCalls?.length) {
|
|
88
|
-
const entry = { role: 'assistant', content: msg.content || '', tool_calls: msg.toolCalls };
|
|
89
|
-
if (msg.prefix) entry.prefix = true;
|
|
90
|
-
if (capabilities?.reasoning && msg.reasoningContent) {
|
|
91
|
-
entry.reasoning_content = msg.reasoningContent;
|
|
92
|
-
}
|
|
93
|
-
result.push(entry);
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// 常规消息
|
|
98
|
-
const hasText = msg.content && isString(msg.content) && msg.content.trim();
|
|
99
|
-
const hasImages = msg.images && Array.isArray(msg.images) && msg.images.length > 0;
|
|
100
|
-
if (!hasText && !hasImages) {
|
|
101
|
-
if (msg.role === 'assistant' && msg.prefix) {
|
|
102
|
-
const prefixEntry = { role: 'assistant', content: '', prefix: true };
|
|
103
|
-
if (capabilities?.reasoning && msg.reasoningContent) {
|
|
104
|
-
prefixEntry.reasoning_content = msg.reasoningContent;
|
|
105
|
-
}
|
|
106
|
-
result.push(prefixEntry);
|
|
107
|
-
}
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const entry = { role: msg.role };
|
|
112
|
-
|
|
113
|
-
if (hasImages) {
|
|
114
|
-
if (!capabilities?.vision) {
|
|
115
|
-
throw new Error(
|
|
116
|
-
'[MessageFormatter] 消息包含图片但模型不支持视觉(capabilities.vision=false)。' +
|
|
117
|
-
'请切换模型或移除图片。'
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
entry.content = buildMultimodalContent(msg.content || '', msg.images, rImg);
|
|
121
|
-
} else {
|
|
122
|
-
entry.content = msg.content;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (msg.prefix) entry.prefix = true;
|
|
126
|
-
// 常规 assistant 不回传 reasoningContent——API 自己会重新推理
|
|
127
|
-
result.push(entry);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return result;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// ====== 格式化器 ======
|
|
134
|
-
|
|
135
|
-
export const MessageFormatter = {
|
|
136
|
-
_formats: new Map([
|
|
137
|
-
['openai', toOpenAI]
|
|
138
|
-
]),
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* 注册自定义格式
|
|
142
|
-
* @param {string} name — 格式名称(如 'anthropic', 'gemini')
|
|
143
|
-
* @param {Function} fn — 转换函数,接收 { messages, systemPrompts, capabilities, resolveImage },返回消息数组
|
|
144
|
-
*/
|
|
145
|
-
register(name, fn) {
|
|
146
|
-
if (!name || typeof name !== 'string' || !name.trim()) {
|
|
147
|
-
throw new Error('[MessageFormatter] 格式名称必须是非空字符串');
|
|
148
|
-
}
|
|
149
|
-
if (typeof fn !== 'function') {
|
|
150
|
-
throw new Error('[MessageFormatter] 转换函数必须是 function');
|
|
151
|
-
}
|
|
152
|
-
this._formats.set(name.trim(), fn);
|
|
153
|
-
},
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* 移除自定义格式(内置格式不可移除)
|
|
157
|
-
* @param {string} name
|
|
158
|
-
*/
|
|
159
|
-
unregister(name) {
|
|
160
|
-
if (name === 'openai') {
|
|
161
|
-
throw new Error('[MessageFormatter] 内置格式 "openai" 不可移除');
|
|
162
|
-
}
|
|
163
|
-
this._formats.delete(name);
|
|
164
|
-
},
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* 列出所有已注册的格式名称
|
|
168
|
-
* @returns {Array<string>}
|
|
169
|
-
*/
|
|
170
|
-
listFormats() {
|
|
171
|
-
return [...this._formats.keys()];
|
|
172
|
-
},
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* 按指定格式转换消息
|
|
176
|
-
* @param {Object} options
|
|
177
|
-
* @param {Array} options.messages — 内部消息列表
|
|
178
|
-
* @param {Array} [options.systemPrompts=[]] — 启用的 system prompt
|
|
179
|
-
* @param {Object} [options.capabilities={}] — 模型能力标签
|
|
180
|
-
* @param {Function} [options.resolveImage] — 图片解析钩子
|
|
181
|
-
* @param {string} [options.format='openai'] — 目标格式名称
|
|
182
|
-
* @returns {Array<Object>}
|
|
183
|
-
*/
|
|
184
|
-
format(options = {}) {
|
|
185
|
-
const formatName = options.format || 'openai';
|
|
186
|
-
const fn = this._formats.get(formatName);
|
|
187
|
-
if (!fn) {
|
|
188
|
-
throw new Error(
|
|
189
|
-
`[MessageFormatter] 未知格式 "${formatName}"。可用格式: ${[...this._formats.keys()].join(', ')}`
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
return fn({
|
|
193
|
-
messages: options.messages || [],
|
|
194
|
-
systemPrompts: options.systemPrompts || [],
|
|
195
|
-
capabilities: options.capabilities || {},
|
|
196
|
-
resolveImage: options.resolveImage
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
// 向后兼容:保留 assembleMessages 函数
|
|
202
|
-
export function assembleMessages(options = {}) {
|
|
203
|
-
return MessageFormatter.format({ ...options, format: 'openai' });
|
|
204
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* MessageFormatter — 可注册的消息格式转换器
|
|
3
|
+
*
|
|
4
|
+
* 职责:
|
|
5
|
+
* 1. 内置 OpenA I兼容格式的转换逻辑
|
|
6
|
+
* 2. 支持 register(name, fn) 注册自定义格式(如 Anthropic、Gemini 等)
|
|
7
|
+
* 3. 统一处理 capabilities 过滤(reasoning、vision 等)
|
|
8
|
+
* 4. 所有适配器通过此工具获取 API 消息数组,消除重复代码
|
|
9
|
+
*
|
|
10
|
+
* 用法:
|
|
11
|
+
* import { MessageFormatter } from 'my-ai-chat-framework';
|
|
12
|
+
*
|
|
13
|
+
* // 使用内置格式
|
|
14
|
+
* const msgs = MessageFormatter.format({
|
|
15
|
+
* messages, systemPrompts, capabilities, resolveImage
|
|
16
|
+
* }); // 默认 'openai'
|
|
17
|
+
*
|
|
18
|
+
* // 注册自定义格式
|
|
19
|
+
* MessageFormatter.register('anthropic', ({ messages, systemPrompts, capabilities }) => {
|
|
20
|
+
* // 返回 Anthropic 格式的消息数组
|
|
21
|
+
* });
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { isString } from './typeCheck.js';
|
|
25
|
+
|
|
26
|
+
// ====== 内置格式 ======
|
|
27
|
+
|
|
28
|
+
const IS_URL = /^https?:\/\//i;
|
|
29
|
+
|
|
30
|
+
function defaultResolveImage(imageId) {
|
|
31
|
+
if (IS_URL.test(imageId)) return { url: imageId };
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function buildMultimodalContent(textContent, images, resolveImage) {
|
|
36
|
+
const content = [{ type: 'text', text: textContent }];
|
|
37
|
+
for (const ref of images) {
|
|
38
|
+
const resolved = resolveImage(ref);
|
|
39
|
+
if (!resolved) continue;
|
|
40
|
+
if (resolved.url) {
|
|
41
|
+
content.push({ type: 'image_url', image_url: { url: resolved.url } });
|
|
42
|
+
} else if (resolved.data) {
|
|
43
|
+
const mime = resolved.mimeType || 'image/png';
|
|
44
|
+
content.push({ type: 'image_url', image_url: { url: `data:${mime};base64,${resolved.data}` } });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return content;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** OpenAI 兼容格式 */
|
|
51
|
+
function toOpenAI({ messages, systemPrompts, capabilities, resolveImage }) {
|
|
52
|
+
const rImg = typeof resolveImage === 'function' ? resolveImage : defaultResolveImage;
|
|
53
|
+
const result = [];
|
|
54
|
+
|
|
55
|
+
// 1) system 置顶
|
|
56
|
+
for (const sp of systemPrompts) {
|
|
57
|
+
if (sp?.content && isString(sp.content) && sp.content.trim()) {
|
|
58
|
+
result.push({ role: 'system', content: sp.content.trim() });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 2) 识别底部连续 ephemeral(只保留这些临时消息,其他 ephemeral 忽略)
|
|
63
|
+
let ephemEnd = messages.length;
|
|
64
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
65
|
+
if (messages[i]._ephemeral) ephemEnd = i;
|
|
66
|
+
else break;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 3) 过滤 + 字段映射
|
|
70
|
+
for (let i = 0; i < messages.length; i++) {
|
|
71
|
+
const msg = messages[i];
|
|
72
|
+
|
|
73
|
+
// ephemeral 消息:仅底部连续的保留,其余跳过
|
|
74
|
+
if (msg._ephemeral) {
|
|
75
|
+
if (i < ephemEnd) continue;
|
|
76
|
+
// 底部 ephemeral 按原始 role 处理,不加 _ephemeral 标记
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 非底部 ephemeral 的正常消息仍保留原有过滤
|
|
80
|
+
if (msg.role === 'system' && !msg._ephemeral) continue;
|
|
81
|
+
|
|
82
|
+
if (msg.role === 'tool') {
|
|
83
|
+
result.push({ role: 'tool', content: msg.content || '', tool_call_id: msg.toolCallId });
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (msg.role === 'assistant' && msg.toolCalls?.length) {
|
|
88
|
+
const entry = { role: 'assistant', content: msg.content || '', tool_calls: msg.toolCalls };
|
|
89
|
+
if (msg.prefix) entry.prefix = true;
|
|
90
|
+
if (capabilities?.reasoning && msg.reasoningContent) {
|
|
91
|
+
entry.reasoning_content = msg.reasoningContent;
|
|
92
|
+
}
|
|
93
|
+
result.push(entry);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 常规消息
|
|
98
|
+
const hasText = msg.content && isString(msg.content) && msg.content.trim();
|
|
99
|
+
const hasImages = msg.images && Array.isArray(msg.images) && msg.images.length > 0;
|
|
100
|
+
if (!hasText && !hasImages) {
|
|
101
|
+
if (msg.role === 'assistant' && msg.prefix) {
|
|
102
|
+
const prefixEntry = { role: 'assistant', content: '', prefix: true };
|
|
103
|
+
if (capabilities?.reasoning && msg.reasoningContent) {
|
|
104
|
+
prefixEntry.reasoning_content = msg.reasoningContent;
|
|
105
|
+
}
|
|
106
|
+
result.push(prefixEntry);
|
|
107
|
+
}
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const entry = { role: msg.role };
|
|
112
|
+
|
|
113
|
+
if (hasImages) {
|
|
114
|
+
if (!capabilities?.vision) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
'[MessageFormatter] 消息包含图片但模型不支持视觉(capabilities.vision=false)。' +
|
|
117
|
+
'请切换模型或移除图片。'
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
entry.content = buildMultimodalContent(msg.content || '', msg.images, rImg);
|
|
121
|
+
} else {
|
|
122
|
+
entry.content = msg.content;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (msg.prefix) entry.prefix = true;
|
|
126
|
+
// 常规 assistant 不回传 reasoningContent——API 自己会重新推理
|
|
127
|
+
result.push(entry);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ====== 格式化器 ======
|
|
134
|
+
|
|
135
|
+
export const MessageFormatter = {
|
|
136
|
+
_formats: new Map([
|
|
137
|
+
['openai', toOpenAI]
|
|
138
|
+
]),
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 注册自定义格式
|
|
142
|
+
* @param {string} name — 格式名称(如 'anthropic', 'gemini')
|
|
143
|
+
* @param {Function} fn — 转换函数,接收 { messages, systemPrompts, capabilities, resolveImage },返回消息数组
|
|
144
|
+
*/
|
|
145
|
+
register(name, fn) {
|
|
146
|
+
if (!name || typeof name !== 'string' || !name.trim()) {
|
|
147
|
+
throw new Error('[MessageFormatter] 格式名称必须是非空字符串');
|
|
148
|
+
}
|
|
149
|
+
if (typeof fn !== 'function') {
|
|
150
|
+
throw new Error('[MessageFormatter] 转换函数必须是 function');
|
|
151
|
+
}
|
|
152
|
+
this._formats.set(name.trim(), fn);
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 移除自定义格式(内置格式不可移除)
|
|
157
|
+
* @param {string} name
|
|
158
|
+
*/
|
|
159
|
+
unregister(name) {
|
|
160
|
+
if (name === 'openai') {
|
|
161
|
+
throw new Error('[MessageFormatter] 内置格式 "openai" 不可移除');
|
|
162
|
+
}
|
|
163
|
+
this._formats.delete(name);
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 列出所有已注册的格式名称
|
|
168
|
+
* @returns {Array<string>}
|
|
169
|
+
*/
|
|
170
|
+
listFormats() {
|
|
171
|
+
return [...this._formats.keys()];
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 按指定格式转换消息
|
|
176
|
+
* @param {Object} options
|
|
177
|
+
* @param {Array} options.messages — 内部消息列表
|
|
178
|
+
* @param {Array} [options.systemPrompts=[]] — 启用的 system prompt
|
|
179
|
+
* @param {Object} [options.capabilities={}] — 模型能力标签
|
|
180
|
+
* @param {Function} [options.resolveImage] — 图片解析钩子
|
|
181
|
+
* @param {string} [options.format='openai'] — 目标格式名称
|
|
182
|
+
* @returns {Array<Object>}
|
|
183
|
+
*/
|
|
184
|
+
format(options = {}) {
|
|
185
|
+
const formatName = options.format || 'openai';
|
|
186
|
+
const fn = this._formats.get(formatName);
|
|
187
|
+
if (!fn) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`[MessageFormatter] 未知格式 "${formatName}"。可用格式: ${[...this._formats.keys()].join(', ')}`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
return fn({
|
|
193
|
+
messages: options.messages || [],
|
|
194
|
+
systemPrompts: options.systemPrompts || [],
|
|
195
|
+
capabilities: options.capabilities || {},
|
|
196
|
+
resolveImage: options.resolveImage
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// 向后兼容:保留 assembleMessages 函数
|
|
202
|
+
export function assembleMessages(options = {}) {
|
|
203
|
+
return MessageFormatter.format({ ...options, format: 'openai' });
|
|
204
|
+
}
|
package/src/utils/typeCheck.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 类型判断工具
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* 判断一个值是否为字符串
|
|
7
|
-
* @param {any} value - 要检查的值
|
|
8
|
-
* @returns {boolean} 如果是字符串则返回 true
|
|
9
|
-
*/
|
|
10
|
-
export function isString(value) {
|
|
11
|
-
return typeof value === 'string';
|
|
1
|
+
/**
|
|
2
|
+
* 类型判断工具
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 判断一个值是否为字符串
|
|
7
|
+
* @param {any} value - 要检查的值
|
|
8
|
+
* @returns {boolean} 如果是字符串则返回 true
|
|
9
|
+
*/
|
|
10
|
+
export function isString(value) {
|
|
11
|
+
return typeof value === 'string';
|
|
12
12
|
}
|
package/src/utils/url.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* URL 工具函数
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* 拼接 baseUrl 和 path
|
|
7
|
-
* 注意:path 应以 '/' 开头,否则会替换 baseUrl 的最后一段
|
|
8
|
-
* @param {string} baseUrl - 基础 URL(如 https://api.deepseek.com)
|
|
9
|
-
* @param {string} path - 路径(如 /v1/chat/completions)
|
|
10
|
-
* @returns {string} 完整的 URL
|
|
11
|
-
*/
|
|
12
|
-
export function joinUrl(baseUrl, path) {
|
|
13
|
-
// 移除 baseUrl 末尾的斜杠(如果有)
|
|
14
|
-
const base = baseUrl.replace(/\/$/, '');
|
|
15
|
-
// 确保 path 以斜杠开头
|
|
16
|
-
const p = path.startsWith('/') ? path : '/' + path;
|
|
17
|
-
return base + p;
|
|
1
|
+
/**
|
|
2
|
+
* URL 工具函数
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 拼接 baseUrl 和 path
|
|
7
|
+
* 注意:path 应以 '/' 开头,否则会替换 baseUrl 的最后一段
|
|
8
|
+
* @param {string} baseUrl - 基础 URL(如 https://api.deepseek.com)
|
|
9
|
+
* @param {string} path - 路径(如 /v1/chat/completions)
|
|
10
|
+
* @returns {string} 完整的 URL
|
|
11
|
+
*/
|
|
12
|
+
export function joinUrl(baseUrl, path) {
|
|
13
|
+
// 移除 baseUrl 末尾的斜杠(如果有)
|
|
14
|
+
const base = baseUrl.replace(/\/$/, '');
|
|
15
|
+
// 确保 path 以斜杠开头
|
|
16
|
+
const p = path.startsWith('/') ? path : '/' + path;
|
|
17
|
+
return base + p;
|
|
18
18
|
}
|