xiaozhou-chat 1.0.9 → 1.0.12
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/lib/chat.js +108 -52
- package/package.json +3 -2
package/lib/chat.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { Spinner, StreamPrinter } from "./ui.js";
|
|
3
3
|
import { sleep } from "./utils.js";
|
|
4
4
|
import { updateConfig, setProfileValue } from "./config.js";
|
|
5
|
+
import { builtInTools } from "./tools.js";
|
|
5
6
|
|
|
6
7
|
// 尝试加载 Markdown 渲染库
|
|
7
8
|
let marked;
|
|
@@ -57,43 +58,28 @@ export async function chatStream(context, userInput = null, options = {}) {
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
// 构造 Tools 定义
|
|
60
|
-
// TODO: 从 mcpClients 和 builtInTools 获取
|
|
61
|
-
// 这里我们假设 context 传入了完整的 tools 列表,或者我们在外部组装
|
|
62
|
-
// 为了解耦,建议外部传入 tools array
|
|
63
61
|
const tools = context.tools || [];
|
|
64
62
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
messages: messages,
|
|
68
|
-
stream: true,
|
|
69
|
-
max_tokens: 8192
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
if (tools.length > 0) {
|
|
73
|
-
body.tools = tools;
|
|
74
|
-
}
|
|
75
|
-
|
|
63
|
+
// 处理 System Prompt
|
|
64
|
+
let requestMessages = messages;
|
|
76
65
|
if (config.systemPrompt) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
// But standard is: messages[0] is system.
|
|
84
|
-
// We'll let the caller handle message structure, or handle it here.
|
|
85
|
-
// For CLI, we usually want to prepend system prompt if not present.
|
|
86
|
-
const sysMsg = { role: "system", content: config.systemPrompt };
|
|
87
|
-
if (messages.length > 0 && messages[0].role === "system") {
|
|
88
|
-
// Replace existing? Or assume caller synced it?
|
|
89
|
-
// Let's use a copy for the request to be safe
|
|
90
|
-
body.messages = [sysMsg, ...messages.filter(m => m.role !== "system")];
|
|
91
|
-
} else {
|
|
92
|
-
body.messages = [sysMsg, ...messages];
|
|
93
|
-
}
|
|
94
|
-
}
|
|
66
|
+
const sysMsg = { role: "system", content: config.systemPrompt };
|
|
67
|
+
if (messages.length > 0 && messages[0].role === "system") {
|
|
68
|
+
requestMessages = [sysMsg, ...messages.filter(m => m.role !== "system")];
|
|
69
|
+
} else {
|
|
70
|
+
requestMessages = [sysMsg, ...messages];
|
|
71
|
+
}
|
|
95
72
|
}
|
|
96
73
|
|
|
74
|
+
// 构造请求 Body
|
|
75
|
+
const createBody = (withTools = true) => ({
|
|
76
|
+
model: config.model,
|
|
77
|
+
messages: requestMessages,
|
|
78
|
+
stream: true,
|
|
79
|
+
max_tokens: 8192,
|
|
80
|
+
tools: (withTools && tools.length > 0) ? tools : undefined
|
|
81
|
+
});
|
|
82
|
+
|
|
97
83
|
const spinner = new Spinner(isRecursion ? "AI 正在分析工具结果..." : "AI 正在思考...");
|
|
98
84
|
spinner.start();
|
|
99
85
|
|
|
@@ -101,44 +87,82 @@ export async function chatStream(context, userInput = null, options = {}) {
|
|
|
101
87
|
|
|
102
88
|
let requestUrl = `${config.baseUrl}/chat/completions`;
|
|
103
89
|
|
|
104
|
-
//
|
|
105
|
-
|
|
90
|
+
// 自动修正 Base URL (如果缺少 /v1)
|
|
91
|
+
if (!config.baseUrl.endsWith("/v1") && !config.baseUrl.endsWith("/v1/")) {
|
|
92
|
+
// console.log(`\n⚠️ 自动追加 /v1 到 Base URL`);
|
|
93
|
+
requestUrl = `${config.baseUrl}/v1/chat/completions`;
|
|
94
|
+
}
|
|
106
95
|
|
|
96
|
+
let shouldRetryWithV1 = false;
|
|
97
|
+
|
|
107
98
|
try {
|
|
108
|
-
let res
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
99
|
+
let res;
|
|
100
|
+
let usedTools = true;
|
|
101
|
+
|
|
102
|
+
// 第一次尝试 (默认带 Tools)
|
|
103
|
+
try {
|
|
104
|
+
res = await requestWithRetry(requestUrl, {
|
|
105
|
+
method: "POST",
|
|
106
|
+
headers: {
|
|
107
|
+
"Content-Type": "application/json",
|
|
108
|
+
Authorization: `Bearer ${config.apiKey}`
|
|
109
|
+
},
|
|
110
|
+
body: JSON.stringify(createBody(true)),
|
|
111
|
+
signal: signal
|
|
112
|
+
}, 1); // 减少内部重试,由外层控制
|
|
113
|
+
} catch (e) {
|
|
114
|
+
// 如果是 400 错误,且我们用了 Tools,尝试降级
|
|
115
|
+
if (e.message.includes("400") || e.message.includes("Improperly formed request")) {
|
|
116
|
+
console.log("\n⚠️ API 不支持工具调用或参数格式错误,尝试自动降级为纯聊天模式...");
|
|
117
|
+
usedTools = false;
|
|
118
|
+
res = await requestWithRetry(requestUrl, {
|
|
119
|
+
method: "POST",
|
|
120
|
+
headers: {
|
|
121
|
+
"Content-Type": "application/json",
|
|
122
|
+
Authorization: `Bearer ${config.apiKey}`
|
|
123
|
+
},
|
|
124
|
+
body: JSON.stringify(createBody(false)),
|
|
125
|
+
signal: signal
|
|
126
|
+
});
|
|
127
|
+
} else {
|
|
128
|
+
throw e;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 智能检测 HTML (404/BaseUrl 错误)
|
|
119
133
|
const contentType = res.headers.get("content-type");
|
|
120
134
|
if (contentType && contentType.includes("text/html")) {
|
|
121
135
|
if (!config.baseUrl.endsWith("/v1") && !config.baseUrl.endsWith("/v1/")) {
|
|
122
|
-
// 静默重试,不打扰用户
|
|
123
136
|
requestUrl = `${config.baseUrl}/v1/chat/completions`;
|
|
124
137
|
shouldRetryWithV1 = true;
|
|
125
|
-
|
|
126
|
-
// 重试请求
|
|
138
|
+
// 重试 (保持降级状态)
|
|
127
139
|
res = await requestWithRetry(requestUrl, {
|
|
128
140
|
method: "POST",
|
|
129
141
|
headers: {
|
|
130
142
|
"Content-Type": "application/json",
|
|
131
143
|
Authorization: `Bearer ${config.apiKey}`
|
|
132
144
|
},
|
|
133
|
-
body: JSON.stringify(
|
|
145
|
+
body: JSON.stringify(createBody(usedTools)),
|
|
134
146
|
signal: signal
|
|
135
147
|
});
|
|
136
148
|
}
|
|
137
149
|
}
|
|
138
150
|
|
|
139
|
-
// 停止 Spinner
|
|
151
|
+
// 停止 Spinner
|
|
140
152
|
spinner.stop();
|
|
141
153
|
|
|
154
|
+
// 检查是否是非流式响应 (JSON)
|
|
155
|
+
if (contentType && contentType.includes("application/json") && !createBody().stream) {
|
|
156
|
+
const data = await res.json();
|
|
157
|
+
const content = data.choices?.[0]?.message?.content || "";
|
|
158
|
+
printer.print(content);
|
|
159
|
+
printer.stop();
|
|
160
|
+
return { content };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 准备流式输出
|
|
164
|
+
// spinner.stop(); // 移到上面了
|
|
165
|
+
|
|
142
166
|
if (shouldRetryWithV1 && res.ok && !res.headers.get("content-type")?.includes("text/html")) {
|
|
143
167
|
// 静默保存配置
|
|
144
168
|
try {
|
|
@@ -272,14 +296,46 @@ export async function chatStream(context, userInput = null, options = {}) {
|
|
|
272
296
|
|
|
273
297
|
// Handle Tool Calls
|
|
274
298
|
if (toolCalls.length > 0) {
|
|
299
|
+
// 提取已知工具名
|
|
300
|
+
const knownToolNames = builtInTools.map(t => t.function.name);
|
|
301
|
+
|
|
275
302
|
for (const tc of toolCalls) {
|
|
276
|
-
|
|
303
|
+
let funcName = tc.function.name;
|
|
304
|
+
|
|
305
|
+
// 自动修正工具名粘连 (例如: read_fileread_file -> read_file)
|
|
306
|
+
if (!knownToolNames.includes(funcName)) {
|
|
307
|
+
// 按长度降序排序,优先匹配更长的工具名
|
|
308
|
+
const matched = knownToolNames
|
|
309
|
+
.sort((a, b) => b.length - a.length)
|
|
310
|
+
.find(name => funcName.includes(name));
|
|
311
|
+
|
|
312
|
+
if (matched) {
|
|
313
|
+
console.log(`⚠️ 检测到工具名异常 "${funcName}",自动修正为 "${matched}"`);
|
|
314
|
+
funcName = matched;
|
|
315
|
+
tc.function.name = matched; // 修正原始对象,这对后续消息历史至关重要
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
277
319
|
const argsStr = tc.function.arguments;
|
|
278
320
|
console.log(`🛠️ 调用工具: ${funcName}(${argsStr})`);
|
|
279
321
|
|
|
280
322
|
let result = null;
|
|
281
323
|
try {
|
|
282
|
-
|
|
324
|
+
let args;
|
|
325
|
+
try {
|
|
326
|
+
args = JSON.parse(argsStr);
|
|
327
|
+
} catch (e) {
|
|
328
|
+
// 尝试修复常见的 JSON 粘连问题 (例如: {"a":1}{"b":2})
|
|
329
|
+
if (argsStr.includes("}{")) {
|
|
330
|
+
console.log("⚠️ 检测到 JSON 粘连,尝试修复...");
|
|
331
|
+
// 简单策略:只取第一个 JSON
|
|
332
|
+
const fixStr = argsStr.split("}{")[0] + "}";
|
|
333
|
+
args = JSON.parse(fixStr);
|
|
334
|
+
} else {
|
|
335
|
+
throw e;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
283
339
|
// 1. Try built-in
|
|
284
340
|
result = await toolHandlers(funcName, args);
|
|
285
341
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xiaozhou-chat",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.12",
|
|
4
4
|
"description": "CLI chatbot based on NewAPI",
|
|
5
5
|
"bin": {
|
|
6
6
|
"xiaozhou-chat": "bin/cli.js"
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"marked": "^15.0.12",
|
|
23
23
|
"marked-terminal": "^7.3.0",
|
|
24
24
|
"minimist": "^1.2.8",
|
|
25
|
-
"moment": "^2.30.1"
|
|
25
|
+
"moment": "^2.30.1",
|
|
26
|
+
"xiaozhou-chat": "^1.0.1-0.1"
|
|
26
27
|
},
|
|
27
28
|
"pkg": {
|
|
28
29
|
"assets": []
|