xiaozhou-chat 1.0.10 → 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.
Files changed (2) hide show
  1. package/lib/chat.js +73 -50
  2. package/package.json +3 -2
package/lib/chat.js CHANGED
@@ -58,43 +58,28 @@ export async function chatStream(context, userInput = null, options = {}) {
58
58
  }
59
59
 
60
60
  // 构造 Tools 定义
61
- // TODO: 从 mcpClients 和 builtInTools 获取
62
- // 这里我们假设 context 传入了完整的 tools 列表,或者我们在外部组装
63
- // 为了解耦,建议外部传入 tools array
64
61
  const tools = context.tools || [];
65
62
 
66
- const body = {
67
- model: config.model,
68
- messages: messages,
69
- stream: true,
70
- max_tokens: 8192
71
- };
72
-
73
- if (tools.length > 0) {
74
- body.tools = tools;
75
- }
76
-
63
+ // 处理 System Prompt
64
+ let requestMessages = messages;
77
65
  if (config.systemPrompt) {
78
- // Ensure system prompt is at the beginning
79
- if (messages.length === 0 || messages[0].role !== "system") {
80
- // Check if we should insert or replace?
81
- // Simplest: just don't mutate history permanently if it's transient?
82
- // But usually system prompt is part of config.
83
- // Let's assume messages already handles system prompt or we unshift it here for the request ONLY.
84
- // But standard is: messages[0] is system.
85
- // We'll let the caller handle message structure, or handle it here.
86
- // For CLI, we usually want to prepend system prompt if not present.
87
- const sysMsg = { role: "system", content: config.systemPrompt };
88
- if (messages.length > 0 && messages[0].role === "system") {
89
- // Replace existing? Or assume caller synced it?
90
- // Let's use a copy for the request to be safe
91
- body.messages = [sysMsg, ...messages.filter(m => m.role !== "system")];
92
- } else {
93
- body.messages = [sysMsg, ...messages];
94
- }
95
- }
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
+ }
96
72
  }
97
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
+
98
83
  const spinner = new Spinner(isRecursion ? "AI 正在分析工具结果..." : "AI 正在思考...");
99
84
  spinner.start();
100
85
 
@@ -102,44 +87,82 @@ export async function chatStream(context, userInput = null, options = {}) {
102
87
 
103
88
  let requestUrl = `${config.baseUrl}/chat/completions`;
104
89
 
105
- // 自动尝试逻辑
106
- let shouldRetryWithV1 = false;
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
+ }
107
95
 
96
+ let shouldRetryWithV1 = false;
97
+
108
98
  try {
109
- let res = await requestWithRetry(requestUrl, {
110
- method: "POST",
111
- headers: {
112
- "Content-Type": "application/json",
113
- Authorization: `Bearer ${config.apiKey}`
114
- },
115
- body: JSON.stringify(body),
116
- signal: signal
117
- });
118
-
119
- // 智能检测:如果返回的是 HTML (通常是 404 页或首页),且 URL 没带 v1,可能是用户配错了
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 错误)
120
133
  const contentType = res.headers.get("content-type");
121
134
  if (contentType && contentType.includes("text/html")) {
122
135
  if (!config.baseUrl.endsWith("/v1") && !config.baseUrl.endsWith("/v1/")) {
123
- // 静默重试,不打扰用户
124
136
  requestUrl = `${config.baseUrl}/v1/chat/completions`;
125
137
  shouldRetryWithV1 = true;
126
-
127
- // 重试请求
138
+ // 重试 (保持降级状态)
128
139
  res = await requestWithRetry(requestUrl, {
129
140
  method: "POST",
130
141
  headers: {
131
142
  "Content-Type": "application/json",
132
143
  Authorization: `Bearer ${config.apiKey}`
133
144
  },
134
- body: JSON.stringify(body),
145
+ body: JSON.stringify(createBody(usedTools)),
135
146
  signal: signal
136
147
  });
137
148
  }
138
149
  }
139
150
 
140
- // 停止 Spinner,准备流式输出
151
+ // 停止 Spinner
141
152
  spinner.stop();
142
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
+
143
166
  if (shouldRetryWithV1 && res.ok && !res.headers.get("content-type")?.includes("text/html")) {
144
167
  // 静默保存配置
145
168
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xiaozhou-chat",
3
- "version": "1.0.10",
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": []