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 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://api.newapi.pro/v1",
38
- model: "gpt-3.5-turbo"
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
- const API_KEY =
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
- const BASE_URL =
163
+ let BASE_URL =
157
164
  config.baseUrl ||
158
165
  args["base-url"] ||
159
166
  process.env.NEWAPI_BASE_URL ||
160
- "https://api.newapi.pro/v1";
167
+ "https://paid.tribiosapi.top/v1";
161
168
 
162
- const MODEL =
169
+ let MODEL =
163
170
  config.model ||
164
171
  args["model"] ||
165
172
  process.env.NEWAPI_MODEL ||
166
- "gpt-3.5-turbo";
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
- const res = await fetch(`${BASE_URL}/chat/completions`, {
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
- const chunk = decoder.decode(value);
229
- const lines = chunk.split("\n").filter(Boolean);
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.replace("data: ", "").trim();
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 = json.choices?.[0]?.delta?.content;
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.2",
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"
@@ -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
+ }