xiaozhou-chat 1.0.2 → 1.0.4
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/bin/cli.js +269 -16
- package/package.json +8 -2
- package/update_config.js +19 -0
package/bin/cli.js
CHANGED
|
@@ -33,9 +33,9 @@ function initConfigFile() {
|
|
|
33
33
|
if (fs.existsSync(homeConfigFile)) return;
|
|
34
34
|
|
|
35
35
|
const defaultConfig = {
|
|
36
|
-
apiKey: "",
|
|
37
|
-
baseUrl: "https://
|
|
38
|
-
model: "
|
|
36
|
+
apiKey: "sk-KUDRAZivzcKUGWnfxAjskgX15uFmNPcKgPvxSWc5pCRwobnK",
|
|
37
|
+
baseUrl: "https://paid.tribiosapi.top/v1",
|
|
38
|
+
model: "claude-sonnet-4-5-20250929"
|
|
39
39
|
};
|
|
40
40
|
|
|
41
41
|
fs.writeFileSync(homeConfigFile, JSON.stringify(defaultConfig, null, 2), "utf-8");
|
|
@@ -138,6 +138,12 @@ function showHelp() {
|
|
|
138
138
|
console.log(`
|
|
139
139
|
可用命令:
|
|
140
140
|
/help 显示帮助
|
|
141
|
+
/config [set] 查看或修改配置
|
|
142
|
+
/system [prompt] 设置系统提示词
|
|
143
|
+
/scan 扫描当前目录结构到上下文
|
|
144
|
+
/load <file> 加载文件内容到上下文
|
|
145
|
+
/save [index] <path> 保存 AI 回复中的代码块
|
|
146
|
+
/paste 进入多行粘贴模式
|
|
141
147
|
/history [N] 显示历史(可选最近 N 条)
|
|
142
148
|
/clear 清空历史
|
|
143
149
|
/export 导出历史为 Markdown
|
|
@@ -148,22 +154,49 @@ function showHelp() {
|
|
|
148
154
|
initConfigFile();
|
|
149
155
|
const config = loadConfig();
|
|
150
156
|
|
|
151
|
-
|
|
157
|
+
let API_KEY =
|
|
152
158
|
config.apiKey ||
|
|
153
159
|
args["api-key"] ||
|
|
154
|
-
process.env.NEWAPI_API_KEY
|
|
160
|
+
process.env.NEWAPI_API_KEY ||
|
|
161
|
+
"sk-KUDRAZivzcKUGWnfxAjskgX15uFmNPcKgPvxSWc5pCRwobnK";
|
|
155
162
|
|
|
156
|
-
|
|
163
|
+
let BASE_URL =
|
|
157
164
|
config.baseUrl ||
|
|
158
165
|
args["base-url"] ||
|
|
159
166
|
process.env.NEWAPI_BASE_URL ||
|
|
160
|
-
"https://
|
|
167
|
+
"https://paid.tribiosapi.top/v1";
|
|
161
168
|
|
|
162
|
-
|
|
169
|
+
let MODEL =
|
|
163
170
|
config.model ||
|
|
164
171
|
args["model"] ||
|
|
165
172
|
process.env.NEWAPI_MODEL ||
|
|
166
|
-
"
|
|
173
|
+
"claude-sonnet-4-5-20250929";
|
|
174
|
+
|
|
175
|
+
let SYSTEM_PROMPT =
|
|
176
|
+
config.systemPrompt ||
|
|
177
|
+
args["system-prompt"] ||
|
|
178
|
+
process.env.NEWAPI_SYSTEM_PROMPT ||
|
|
179
|
+
"";
|
|
180
|
+
|
|
181
|
+
function getWriteConfigFile() {
|
|
182
|
+
if (fs.existsSync(projectConfigFile)) return projectConfigFile;
|
|
183
|
+
if (fs.existsSync(projectAltConfigFile)) return projectAltConfigFile;
|
|
184
|
+
return homeConfigFile;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function updateConfig(key, value) {
|
|
188
|
+
const target = getWriteConfigFile();
|
|
189
|
+
const current = loadConfigFrom(target);
|
|
190
|
+
current[key] = value;
|
|
191
|
+
fs.writeFileSync(target, JSON.stringify(current, null, 2), "utf-8");
|
|
192
|
+
console.log(`✅ 已更新 ${key} 到配置文件: ${target}`);
|
|
193
|
+
|
|
194
|
+
// 更新运行时变量
|
|
195
|
+
if (key === "apiKey") API_KEY = value;
|
|
196
|
+
if (key === "baseUrl") BASE_URL = value;
|
|
197
|
+
if (key === "model") MODEL = value;
|
|
198
|
+
if (key === "systemPrompt") SYSTEM_PROMPT = value;
|
|
199
|
+
}
|
|
167
200
|
|
|
168
201
|
if (!API_KEY) {
|
|
169
202
|
console.error("❌ 请在 ~/.newapi-chat-config.json 或环境变量中配置 NEWAPI_API_KEY");
|
|
@@ -175,7 +208,7 @@ let messages = loadHistory();
|
|
|
175
208
|
const rl = readline.createInterface({
|
|
176
209
|
input: process.stdin,
|
|
177
210
|
output: process.stdout,
|
|
178
|
-
prompt: "
|
|
211
|
+
prompt: "小周> "
|
|
179
212
|
});
|
|
180
213
|
|
|
181
214
|
readline.emitKeypressEvents(process.stdin);
|
|
@@ -199,7 +232,23 @@ console.log("✅ NewAPI Chat CLI 已启动,输入 /help 查看命令");
|
|
|
199
232
|
async function chatStream(userInput) {
|
|
200
233
|
messages.push({ role: "user", content: userInput });
|
|
201
234
|
|
|
202
|
-
|
|
235
|
+
// 自动修正 URL:如果 Base URL 不包含 /v1,尝试追加
|
|
236
|
+
const url = BASE_URL.endsWith("/v1")
|
|
237
|
+
? `${BASE_URL}/chat/completions`
|
|
238
|
+
: `${BASE_URL}/v1/chat/completions`;
|
|
239
|
+
|
|
240
|
+
const apiMessages = [...messages];
|
|
241
|
+
if (SYSTEM_PROMPT) {
|
|
242
|
+
// 检查是否已经有 system prompt,如果没有则添加
|
|
243
|
+
if (!apiMessages.length || apiMessages[0].role !== "system") {
|
|
244
|
+
apiMessages.unshift({ role: "system", content: SYSTEM_PROMPT });
|
|
245
|
+
} else {
|
|
246
|
+
// 临时替换 system prompt 为当前配置的
|
|
247
|
+
apiMessages[0] = { role: "system", content: SYSTEM_PROMPT };
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const res = await fetch(url, {
|
|
203
252
|
method: "POST",
|
|
204
253
|
headers: {
|
|
205
254
|
"Content-Type": "application/json",
|
|
@@ -207,7 +256,7 @@ async function chatStream(userInput) {
|
|
|
207
256
|
},
|
|
208
257
|
body: JSON.stringify({
|
|
209
258
|
model: MODEL,
|
|
210
|
-
messages,
|
|
259
|
+
messages: apiMessages,
|
|
211
260
|
stream: true
|
|
212
261
|
})
|
|
213
262
|
});
|
|
@@ -220,22 +269,26 @@ async function chatStream(userInput) {
|
|
|
220
269
|
const reader = res.body.getReader();
|
|
221
270
|
const decoder = new TextDecoder("utf-8");
|
|
222
271
|
let reply = "";
|
|
272
|
+
let buffer = "";
|
|
223
273
|
|
|
224
274
|
while (true) {
|
|
225
275
|
const { done, value } = await reader.read();
|
|
226
276
|
if (done) break;
|
|
227
277
|
|
|
228
|
-
|
|
229
|
-
const lines =
|
|
278
|
+
buffer += decoder.decode(value, { stream: true });
|
|
279
|
+
const lines = buffer.split("\n");
|
|
280
|
+
buffer = lines.pop() || "";
|
|
230
281
|
|
|
231
282
|
for (const line of lines) {
|
|
232
283
|
if (!line.startsWith("data:")) continue;
|
|
233
|
-
const data = line.
|
|
284
|
+
const data = line.slice(5).trim();
|
|
234
285
|
if (data === "[DONE]") break;
|
|
235
286
|
|
|
236
287
|
try {
|
|
237
288
|
const json = JSON.parse(data);
|
|
238
|
-
const token =
|
|
289
|
+
const token =
|
|
290
|
+
json.choices?.[0]?.delta?.content ??
|
|
291
|
+
json.choices?.[0]?.message?.content;
|
|
239
292
|
if (token) {
|
|
240
293
|
process.stdout.write(token);
|
|
241
294
|
reply += token;
|
|
@@ -251,7 +304,28 @@ async function chatStream(userInput) {
|
|
|
251
304
|
|
|
252
305
|
rl.prompt();
|
|
253
306
|
|
|
307
|
+
let inputMode = "chat"; // chat | paste
|
|
308
|
+
let pasteBuffer = [];
|
|
309
|
+
|
|
254
310
|
rl.on("line", async (line) => {
|
|
311
|
+
if (inputMode === "paste") {
|
|
312
|
+
if (line.trim() === "---") {
|
|
313
|
+
inputMode = "chat";
|
|
314
|
+
const content = pasteBuffer.join("\n");
|
|
315
|
+
pasteBuffer = [];
|
|
316
|
+
console.log(`✅ 已接收 ${content.length} 字符,正在发送...`);
|
|
317
|
+
try {
|
|
318
|
+
await chatStream(content);
|
|
319
|
+
} catch (e) {
|
|
320
|
+
console.error("❌", e.message);
|
|
321
|
+
}
|
|
322
|
+
rl.setPrompt("小周> ");
|
|
323
|
+
return rl.prompt();
|
|
324
|
+
}
|
|
325
|
+
pasteBuffer.push(line);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
255
329
|
const input = line.trim();
|
|
256
330
|
if (!input) return rl.prompt();
|
|
257
331
|
|
|
@@ -260,6 +334,184 @@ rl.on("line", async (line) => {
|
|
|
260
334
|
return rl.prompt();
|
|
261
335
|
}
|
|
262
336
|
|
|
337
|
+
if (input.startsWith("/system")) {
|
|
338
|
+
const prompt = input.slice(7).trim();
|
|
339
|
+
if (!prompt) {
|
|
340
|
+
console.log(`当前 System Prompt: ${SYSTEM_PROMPT || "(空)"}`);
|
|
341
|
+
} else {
|
|
342
|
+
updateConfig("systemPrompt", prompt);
|
|
343
|
+
}
|
|
344
|
+
return rl.prompt();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (input === "/scan") {
|
|
348
|
+
console.log("🔍 正在扫描项目结构...");
|
|
349
|
+
const ignore = ["node_modules", ".git", "dist", "coverage", ".DS_Store", ".env"];
|
|
350
|
+
|
|
351
|
+
function scanDir(dir, prefix = "") {
|
|
352
|
+
let output = "";
|
|
353
|
+
try {
|
|
354
|
+
const files = fs.readdirSync(dir, { withFileTypes: true });
|
|
355
|
+
for (const file of files) {
|
|
356
|
+
if (ignore.includes(file.name)) continue;
|
|
357
|
+
if (file.isDirectory()) {
|
|
358
|
+
output += `${prefix}- ${file.name}/\n`;
|
|
359
|
+
output += scanDir(path.join(dir, file.name), `${prefix} `);
|
|
360
|
+
} else {
|
|
361
|
+
output += `${prefix}- ${file.name}\n`;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
} catch (e) {}
|
|
365
|
+
return output;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const structure = scanDir(process.cwd());
|
|
369
|
+
let contextMsg = `(Current Project Structure):\n\`\`\`\n${structure}\n\`\`\``;
|
|
370
|
+
|
|
371
|
+
// 尝试自动读取 package.json
|
|
372
|
+
try {
|
|
373
|
+
if (fs.existsSync("package.json")) {
|
|
374
|
+
const pkg = fs.readFileSync("package.json", "utf-8");
|
|
375
|
+
contextMsg += `\n\n(package.json content):\n\`\`\`json\n${pkg}\n\`\`\``;
|
|
376
|
+
console.log("📦 已自动包含 package.json 内容");
|
|
377
|
+
}
|
|
378
|
+
} catch {}
|
|
379
|
+
|
|
380
|
+
messages.push({ role: "user", content: contextMsg });
|
|
381
|
+
saveHistory(messages);
|
|
382
|
+
|
|
383
|
+
console.log(structure);
|
|
384
|
+
console.log("✅ 项目结构已发送给 AI,请继续提问(如:解释一下这个项目)。");
|
|
385
|
+
return rl.prompt();
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (input.startsWith("/load")) {
|
|
389
|
+
const file = input.slice(5).trim();
|
|
390
|
+
if (!file) {
|
|
391
|
+
console.log("❌ 用法: /load <file_path>");
|
|
392
|
+
return rl.prompt();
|
|
393
|
+
}
|
|
394
|
+
try {
|
|
395
|
+
const content = fs.readFileSync(file, "utf-8");
|
|
396
|
+
const userMsg = `(Context from ${path.basename(file)}):\n\`\`\`\n${content}\n\`\`\``;
|
|
397
|
+
messages.push({ role: "user", content: userMsg });
|
|
398
|
+
saveHistory(messages);
|
|
399
|
+
console.log(`✅ 已加载文件: ${file} (${content.length} chars)`);
|
|
400
|
+
console.log("👉 该文件内容将作为上下文发送给 AI,请继续提问。");
|
|
401
|
+
} catch (e) {
|
|
402
|
+
console.error(`❌ 读取文件失败: ${e.message}`);
|
|
403
|
+
}
|
|
404
|
+
return rl.prompt();
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (input === "/paste") {
|
|
408
|
+
inputMode = "paste";
|
|
409
|
+
console.log("📝 进入粘贴模式,请粘贴多行文本。");
|
|
410
|
+
console.log("👉 输入单独一行 '---' (三个减号) 结束并发送。");
|
|
411
|
+
rl.setPrompt("... ");
|
|
412
|
+
return rl.prompt();
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (input.startsWith("/save")) {
|
|
416
|
+
const parts = input.split(/\s+/);
|
|
417
|
+
// 用法: /save [index] <filename> 或 /save <filename> (默认最后一个代码块)
|
|
418
|
+
let blockIndex = -1;
|
|
419
|
+
let filename = "";
|
|
420
|
+
|
|
421
|
+
if (parts.length === 2) {
|
|
422
|
+
filename = parts[1];
|
|
423
|
+
} else if (parts.length === 3) {
|
|
424
|
+
blockIndex = parseInt(parts[1]) - 1; // 转换为 0-based
|
|
425
|
+
filename = parts[2];
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (!filename) {
|
|
429
|
+
console.log("❌ 用法: /save [index] <filename>");
|
|
430
|
+
return rl.prompt();
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// 查找最后一条 AI 消息
|
|
434
|
+
const lastAiMsg = [...messages].reverse().find(m => m.role === "assistant");
|
|
435
|
+
if (!lastAiMsg) {
|
|
436
|
+
console.log("❌ 没有找到 AI 回复历史");
|
|
437
|
+
return rl.prompt();
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// 提取代码块
|
|
441
|
+
const regex = /```[\w]*\n([\s\S]*?)```/g;
|
|
442
|
+
const blocks = [];
|
|
443
|
+
let match;
|
|
444
|
+
while ((match = regex.exec(lastAiMsg.content)) !== null) {
|
|
445
|
+
blocks.push(match[1]);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (blocks.length === 0) {
|
|
449
|
+
console.log("❌ 上一条回复中没有找到代码块");
|
|
450
|
+
return rl.prompt();
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// 确定要保存哪个块
|
|
454
|
+
let contentToSave = "";
|
|
455
|
+
if (blockIndex >= 0) {
|
|
456
|
+
if (blockIndex >= blocks.length) {
|
|
457
|
+
console.log(`❌ 索引超出范围 (共 ${blocks.length} 个代码块)`);
|
|
458
|
+
return rl.prompt();
|
|
459
|
+
}
|
|
460
|
+
contentToSave = blocks[blockIndex];
|
|
461
|
+
} else {
|
|
462
|
+
// 默认保存最后一个,或者如果只有一个就保存那个
|
|
463
|
+
contentToSave = blocks[blocks.length - 1];
|
|
464
|
+
if (blocks.length > 1) {
|
|
465
|
+
console.log(`ℹ️ 检测到 ${blocks.length} 个代码块,默认保存最后一个。`);
|
|
466
|
+
console.log("👉 使用 /save 1 <filename> 指定保存第一个。");
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
try {
|
|
471
|
+
const savePath = path.resolve(process.cwd(), filename);
|
|
472
|
+
fs.writeFileSync(savePath, contentToSave, "utf-8");
|
|
473
|
+
console.log(`✅ 已保存到: ${savePath}`);
|
|
474
|
+
} catch (e) {
|
|
475
|
+
console.error(`❌ 保存失败: ${e.message}`);
|
|
476
|
+
}
|
|
477
|
+
return rl.prompt();
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (input.startsWith("/config") || input.startsWith("/配置")) {
|
|
481
|
+
const parts = input.split(/\s+/);
|
|
482
|
+
const cmd = parts[1];
|
|
483
|
+
|
|
484
|
+
if (!cmd || cmd === "list") {
|
|
485
|
+
console.log(`
|
|
486
|
+
当前配置:
|
|
487
|
+
apiKey: ${API_KEY ? API_KEY.slice(0, 8) + "..." : "未设置"}
|
|
488
|
+
baseUrl: ${BASE_URL}
|
|
489
|
+
model: ${MODEL}
|
|
490
|
+
system: ${SYSTEM_PROMPT ? SYSTEM_PROMPT.slice(0, 20) + "..." : "默认"}
|
|
491
|
+
`);
|
|
492
|
+
return rl.prompt();
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (cmd === "set") {
|
|
496
|
+
const key = parts[2];
|
|
497
|
+
const value = parts[3];
|
|
498
|
+
if (!key || !value) {
|
|
499
|
+
console.log("❌ 用法: /config set <key> <value>");
|
|
500
|
+
return rl.prompt();
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (["apiKey", "baseUrl", "model", "systemPrompt"].includes(key)) {
|
|
504
|
+
updateConfig(key, value);
|
|
505
|
+
} else {
|
|
506
|
+
console.log("❌ 仅支持修改: apiKey, baseUrl, model, systemPrompt");
|
|
507
|
+
}
|
|
508
|
+
return rl.prompt();
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
console.log("❌ 未知命令,用法: /config [list|set]");
|
|
512
|
+
return rl.prompt();
|
|
513
|
+
}
|
|
514
|
+
|
|
263
515
|
if (input.startsWith("/history")) {
|
|
264
516
|
const n = Number(input.split(/\s+/)[1]);
|
|
265
517
|
showHistory(messages, Number.isFinite(n) && n > 0 ? n : undefined);
|
|
@@ -291,3 +543,4 @@ rl.on("line", async (line) => {
|
|
|
291
543
|
|
|
292
544
|
rl.prompt();
|
|
293
545
|
});
|
|
546
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xiaozhou-chat",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "CLI chatbot based on NewAPI",
|
|
5
5
|
"bin": {
|
|
6
6
|
"xiaozhou-chat": "bin/cli.js"
|
|
@@ -10,7 +10,13 @@
|
|
|
10
10
|
"node": ">=18"
|
|
11
11
|
},
|
|
12
12
|
"scripts": {
|
|
13
|
-
"build:win": "pkg . --targets node18-win-x64 --output dist/newapi-chat.exe"
|
|
13
|
+
"build:win": "pkg . --targets node18-win-x64 --output dist/newapi-chat.exe",
|
|
14
|
+
"build:mac": "pkg . --targets node18-macos-x64 --output dist/xiaozhou-chat",
|
|
15
|
+
"pack:dmg": "mkdir -p release && cp dist/xiaozhou-chat release/ && create-dmg release dist"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"create-dmg": "^6.0.0",
|
|
19
|
+
"pkg": "^5.8.1"
|
|
14
20
|
},
|
|
15
21
|
"dependencies": {
|
|
16
22
|
"minimist": "^1.2.8"
|
package/update_config.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
|
|
5
|
+
const homeConfigFile = path.join(os.homedir(), ".newapi-chat-config.json");
|
|
6
|
+
|
|
7
|
+
const config = {
|
|
8
|
+
apiKey: "sk-KUDRAZivzcKUGWnfxAjskgX15uFmNPcKgPvxSWc5pCRwobnK",
|
|
9
|
+
baseUrl: "https://paid.tribiosapi.top/v1",
|
|
10
|
+
model: "claude-sonnet-4-5-20250929"
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
fs.writeFileSync(homeConfigFile, JSON.stringify(config, null, 2), "utf-8");
|
|
15
|
+
console.log("Successfully updated config file at " + homeConfigFile);
|
|
16
|
+
} catch (e) {
|
|
17
|
+
console.error("Failed to update config:", e);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|