xiaozhou-chat 1.0.1 → 1.0.3
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/README.md +36 -11
- package/bin/cli.js +283 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
```markdown
|
|
2
2
|
# xiaozhou-chat
|
|
3
3
|
|
|
4
4
|
CLI chatbot based on NewAPI.
|
|
@@ -8,7 +8,7 @@ CLI chatbot based on NewAPI.
|
|
|
8
8
|
## ✨ 特性
|
|
9
9
|
- 基于 NewAPI 的命令行聊天工具
|
|
10
10
|
- 支持自定义模型与 Base URL
|
|
11
|
-
-
|
|
11
|
+
- 自动保存历史记录(项目根目录优先)
|
|
12
12
|
- Node.js 18+ 支持
|
|
13
13
|
|
|
14
14
|
---
|
|
@@ -46,6 +46,40 @@ export NEWAPI_MODEL="gpt-3.5-turbo"
|
|
|
46
46
|
|
|
47
47
|
---
|
|
48
48
|
|
|
49
|
+
## 📁 配置与历史记录位置
|
|
50
|
+
|
|
51
|
+
**项目根目录判定规则:**
|
|
52
|
+
从当前目录向上查找最近的 `package.json`,其所在目录为项目根目录。
|
|
53
|
+
|
|
54
|
+
### ✅ 配置文件优先级
|
|
55
|
+
1. 项目根目录:
|
|
56
|
+
- `.newapi-chat-config.json`
|
|
57
|
+
- `newapi-chat.config.json`
|
|
58
|
+
2. 用户目录:
|
|
59
|
+
- `~/.newapi-chat-config.json`
|
|
60
|
+
|
|
61
|
+
### ✅ 历史记录保存位置
|
|
62
|
+
- 优先保存到项目根目录:
|
|
63
|
+
```
|
|
64
|
+
<项目根>/.newapi-chat-history.json
|
|
65
|
+
```
|
|
66
|
+
- 无写入权限时回退:
|
|
67
|
+
```
|
|
68
|
+
~/.newapi-chat-history.json
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### ✅ 历史导出(Markdown)
|
|
72
|
+
- 优先导出到项目根目录:
|
|
73
|
+
```
|
|
74
|
+
<项目根>/newapi-chat-history.md
|
|
75
|
+
```
|
|
76
|
+
- 无写入权限时回退:
|
|
77
|
+
```
|
|
78
|
+
~/newapi-chat-history.md
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
49
83
|
## 🚀 使用
|
|
50
84
|
|
|
51
85
|
### 基础启动
|
|
@@ -87,15 +121,6 @@ NewAPI 是一个兼容 OpenAI 接口的网关服务...
|
|
|
87
121
|
|
|
88
122
|
---
|
|
89
123
|
|
|
90
|
-
## 📝 历史记录
|
|
91
|
-
|
|
92
|
-
默认保存在:
|
|
93
|
-
```
|
|
94
|
-
~/.newapi-chat-history.json
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
---
|
|
98
|
-
|
|
99
124
|
## 🪟 Windows 打包
|
|
100
125
|
|
|
101
126
|
安装 pkg:
|
package/bin/cli.js
CHANGED
|
@@ -7,60 +7,193 @@ import minimist from "minimist";
|
|
|
7
7
|
|
|
8
8
|
const args = minimist(process.argv.slice(2));
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
function findProjectRoot(startDir = process.cwd()) {
|
|
11
|
+
let dir = startDir;
|
|
12
|
+
while (true) {
|
|
13
|
+
if (fs.existsSync(path.join(dir, "package.json"))) {
|
|
14
|
+
return dir;
|
|
15
|
+
}
|
|
16
|
+
const parent = path.dirname(dir);
|
|
17
|
+
if (parent === dir) break;
|
|
18
|
+
dir = parent;
|
|
19
|
+
}
|
|
20
|
+
return startDir;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const projectRoot = findProjectRoot();
|
|
24
|
+
|
|
25
|
+
const homeConfigFile = path.join(os.homedir(), ".newapi-chat-config.json");
|
|
26
|
+
const projectConfigFile = path.join(projectRoot, ".newapi-chat-config.json");
|
|
27
|
+
const projectAltConfigFile = path.join(projectRoot, "newapi-chat.config.json");
|
|
28
|
+
|
|
29
|
+
const homeHistoryFile = path.join(os.homedir(), ".newapi-chat-history.json");
|
|
30
|
+
const projectHistoryFile = path.join(projectRoot, ".newapi-chat-history.json");
|
|
12
31
|
|
|
13
32
|
function initConfigFile() {
|
|
14
|
-
if (fs.existsSync(
|
|
33
|
+
if (fs.existsSync(homeConfigFile)) return;
|
|
15
34
|
|
|
16
35
|
const defaultConfig = {
|
|
17
|
-
apiKey: "",
|
|
18
|
-
baseUrl: "https://
|
|
19
|
-
model: "
|
|
36
|
+
apiKey: "sk-KUDRAZivzcKUGWnfxAjskgX15uFmNPcKgPvxSWc5pCRwobnK",
|
|
37
|
+
baseUrl: "https://paid.tribiosapi.top/v1",
|
|
38
|
+
model: "claude-sonnet-4-5-20250929"
|
|
20
39
|
};
|
|
21
40
|
|
|
22
|
-
fs.writeFileSync(
|
|
23
|
-
console.log(`✅ 已创建配置文件: ${
|
|
41
|
+
fs.writeFileSync(homeConfigFile, JSON.stringify(defaultConfig, null, 2), "utf-8");
|
|
42
|
+
console.log(`✅ 已创建配置文件: ${homeConfigFile}`);
|
|
24
43
|
console.log("👉 请编辑该文件填入 apiKey");
|
|
25
44
|
}
|
|
26
45
|
|
|
27
|
-
function
|
|
28
|
-
if (!fs.existsSync(
|
|
46
|
+
function loadConfigFrom(file) {
|
|
47
|
+
if (!fs.existsSync(file)) return {};
|
|
29
48
|
try {
|
|
30
|
-
return JSON.parse(fs.readFileSync(
|
|
49
|
+
return JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
31
50
|
} catch {
|
|
32
51
|
return {};
|
|
33
52
|
}
|
|
34
53
|
}
|
|
35
54
|
|
|
55
|
+
function loadConfig() {
|
|
56
|
+
const home = loadConfigFrom(homeConfigFile);
|
|
57
|
+
const project =
|
|
58
|
+
loadConfigFrom(projectConfigFile) ||
|
|
59
|
+
loadConfigFrom(projectAltConfigFile);
|
|
60
|
+
|
|
61
|
+
return { ...home, ...project };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function canWriteProjectDir() {
|
|
65
|
+
try {
|
|
66
|
+
fs.accessSync(projectRoot, fs.constants.W_OK);
|
|
67
|
+
return true;
|
|
68
|
+
} catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getReadHistoryFile() {
|
|
74
|
+
return fs.existsSync(projectHistoryFile)
|
|
75
|
+
? projectHistoryFile
|
|
76
|
+
: homeHistoryFile;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function getWriteHistoryFile() {
|
|
80
|
+
return canWriteProjectDir()
|
|
81
|
+
? projectHistoryFile
|
|
82
|
+
: homeHistoryFile;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getExportFile() {
|
|
86
|
+
const dir = canWriteProjectDir() ? projectRoot : os.homedir();
|
|
87
|
+
return path.join(dir, "newapi-chat-history.md");
|
|
88
|
+
}
|
|
89
|
+
|
|
36
90
|
function loadHistory() {
|
|
37
|
-
|
|
91
|
+
const target = getReadHistoryFile();
|
|
92
|
+
if (!fs.existsSync(target)) return [];
|
|
38
93
|
try {
|
|
39
|
-
return JSON.parse(fs.readFileSync(
|
|
94
|
+
return JSON.parse(fs.readFileSync(target, "utf-8"));
|
|
40
95
|
} catch {
|
|
41
96
|
return [];
|
|
42
97
|
}
|
|
43
98
|
}
|
|
44
99
|
|
|
45
100
|
function saveHistory(messages) {
|
|
46
|
-
|
|
101
|
+
const target = getWriteHistoryFile();
|
|
102
|
+
fs.writeFileSync(target, JSON.stringify(messages, null, 2));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function clearHistory() {
|
|
106
|
+
const target = getWriteHistoryFile();
|
|
107
|
+
fs.writeFileSync(target, JSON.stringify([], null, 2));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function showHistory(messages, limit) {
|
|
111
|
+
if (!messages.length) {
|
|
112
|
+
console.log("(暂无历史)");
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const list = limit ? messages.slice(-limit) : messages;
|
|
116
|
+
list.forEach((m, i) => {
|
|
117
|
+
const role = m.role === "user" ? "你" : "助手";
|
|
118
|
+
console.log(`[${i + 1}] ${role}: ${m.content}`);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function exportHistory(messages) {
|
|
123
|
+
if (!messages.length) {
|
|
124
|
+
console.log("(暂无历史)");
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const md = messages.map((m) => {
|
|
128
|
+
const role = m.role === "user" ? "你" : "助手";
|
|
129
|
+
return `### ${role}\n\n${m.content}\n`;
|
|
130
|
+
}).join("\n");
|
|
131
|
+
|
|
132
|
+
const file = getExportFile();
|
|
133
|
+
fs.writeFileSync(file, md, "utf-8");
|
|
134
|
+
console.log(`✅ 已导出: ${file}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function showHelp() {
|
|
138
|
+
console.log(`
|
|
139
|
+
可用命令:
|
|
140
|
+
/help 显示帮助
|
|
141
|
+
/config [set] 查看或修改配置
|
|
142
|
+
/system [prompt] 设置系统提示词
|
|
143
|
+
/load <file> 加载文件内容到上下文
|
|
144
|
+
/history [N] 显示历史(可选最近 N 条)
|
|
145
|
+
/clear 清空历史
|
|
146
|
+
/export 导出历史为 Markdown
|
|
147
|
+
exit 退出(或按 ESC)
|
|
148
|
+
`);
|
|
47
149
|
}
|
|
48
150
|
|
|
49
151
|
initConfigFile();
|
|
50
152
|
const config = loadConfig();
|
|
51
153
|
|
|
52
|
-
|
|
53
|
-
|
|
154
|
+
let API_KEY =
|
|
155
|
+
config.apiKey ||
|
|
156
|
+
args["api-key"] ||
|
|
157
|
+
process.env.NEWAPI_API_KEY;
|
|
158
|
+
|
|
159
|
+
let BASE_URL =
|
|
54
160
|
config.baseUrl ||
|
|
55
161
|
args["base-url"] ||
|
|
56
162
|
process.env.NEWAPI_BASE_URL ||
|
|
57
163
|
"https://api.newapi.pro/v1";
|
|
58
|
-
|
|
164
|
+
|
|
165
|
+
let MODEL =
|
|
59
166
|
config.model ||
|
|
60
167
|
args["model"] ||
|
|
61
168
|
process.env.NEWAPI_MODEL ||
|
|
62
169
|
"gpt-3.5-turbo";
|
|
63
170
|
|
|
171
|
+
let SYSTEM_PROMPT =
|
|
172
|
+
config.systemPrompt ||
|
|
173
|
+
args["system-prompt"] ||
|
|
174
|
+
process.env.NEWAPI_SYSTEM_PROMPT ||
|
|
175
|
+
"";
|
|
176
|
+
|
|
177
|
+
function getWriteConfigFile() {
|
|
178
|
+
if (fs.existsSync(projectConfigFile)) return projectConfigFile;
|
|
179
|
+
if (fs.existsSync(projectAltConfigFile)) return projectAltConfigFile;
|
|
180
|
+
return homeConfigFile;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function updateConfig(key, value) {
|
|
184
|
+
const target = getWriteConfigFile();
|
|
185
|
+
const current = loadConfigFrom(target);
|
|
186
|
+
current[key] = value;
|
|
187
|
+
fs.writeFileSync(target, JSON.stringify(current, null, 2), "utf-8");
|
|
188
|
+
console.log(`✅ 已更新 ${key} 到配置文件: ${target}`);
|
|
189
|
+
|
|
190
|
+
// 更新运行时变量
|
|
191
|
+
if (key === "apiKey") API_KEY = value;
|
|
192
|
+
if (key === "baseUrl") BASE_URL = value;
|
|
193
|
+
if (key === "model") MODEL = value;
|
|
194
|
+
if (key === "systemPrompt") SYSTEM_PROMPT = value;
|
|
195
|
+
}
|
|
196
|
+
|
|
64
197
|
if (!API_KEY) {
|
|
65
198
|
console.error("❌ 请在 ~/.newapi-chat-config.json 或环境变量中配置 NEWAPI_API_KEY");
|
|
66
199
|
process.exit(1);
|
|
@@ -71,15 +204,47 @@ let messages = loadHistory();
|
|
|
71
204
|
const rl = readline.createInterface({
|
|
72
205
|
input: process.stdin,
|
|
73
206
|
output: process.stdout,
|
|
74
|
-
prompt: "
|
|
207
|
+
prompt: "小周> "
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
readline.emitKeypressEvents(process.stdin);
|
|
211
|
+
if (process.stdin.isTTY) {
|
|
212
|
+
process.stdin.setRawMode(true);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
process.stdin.on("keypress", (_, key) => {
|
|
216
|
+
if (key && key.name === "escape") {
|
|
217
|
+
rl.close();
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
rl.on("close", () => {
|
|
222
|
+
console.log("\n👋 已退出");
|
|
223
|
+
process.exit(0);
|
|
75
224
|
});
|
|
76
225
|
|
|
77
|
-
console.log("✅ NewAPI Chat CLI 已启动,输入
|
|
226
|
+
console.log("✅ NewAPI Chat CLI 已启动,输入 /help 查看命令");
|
|
78
227
|
|
|
79
228
|
async function chatStream(userInput) {
|
|
80
229
|
messages.push({ role: "user", content: userInput });
|
|
81
230
|
|
|
82
|
-
|
|
231
|
+
// 自动修正 URL:如果 Base URL 不包含 /v1,尝试追加
|
|
232
|
+
const url = BASE_URL.endsWith("/v1")
|
|
233
|
+
? `${BASE_URL}/chat/completions`
|
|
234
|
+
: `${BASE_URL}/v1/chat/completions`;
|
|
235
|
+
|
|
236
|
+
const apiMessages = [...messages];
|
|
237
|
+
if (SYSTEM_PROMPT) {
|
|
238
|
+
// 检查是否已经有 system prompt,如果没有则添加
|
|
239
|
+
if (!apiMessages.length || apiMessages[0].role !== "system") {
|
|
240
|
+
apiMessages.unshift({ role: "system", content: SYSTEM_PROMPT });
|
|
241
|
+
} else {
|
|
242
|
+
// 临时替换 system prompt 为当前配置的
|
|
243
|
+
apiMessages[0] = { role: "system", content: SYSTEM_PROMPT };
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const res = await fetch(url, {
|
|
83
248
|
method: "POST",
|
|
84
249
|
headers: {
|
|
85
250
|
"Content-Type": "application/json",
|
|
@@ -87,7 +252,7 @@ async function chatStream(userInput) {
|
|
|
87
252
|
},
|
|
88
253
|
body: JSON.stringify({
|
|
89
254
|
model: MODEL,
|
|
90
|
-
messages,
|
|
255
|
+
messages: apiMessages,
|
|
91
256
|
stream: true
|
|
92
257
|
})
|
|
93
258
|
});
|
|
@@ -100,22 +265,26 @@ async function chatStream(userInput) {
|
|
|
100
265
|
const reader = res.body.getReader();
|
|
101
266
|
const decoder = new TextDecoder("utf-8");
|
|
102
267
|
let reply = "";
|
|
268
|
+
let buffer = "";
|
|
103
269
|
|
|
104
270
|
while (true) {
|
|
105
271
|
const { done, value } = await reader.read();
|
|
106
272
|
if (done) break;
|
|
107
273
|
|
|
108
|
-
|
|
109
|
-
const lines =
|
|
274
|
+
buffer += decoder.decode(value, { stream: true });
|
|
275
|
+
const lines = buffer.split("\n");
|
|
276
|
+
buffer = lines.pop() || "";
|
|
110
277
|
|
|
111
278
|
for (const line of lines) {
|
|
112
279
|
if (!line.startsWith("data:")) continue;
|
|
113
|
-
const data = line.
|
|
280
|
+
const data = line.slice(5).trim();
|
|
114
281
|
if (data === "[DONE]") break;
|
|
115
282
|
|
|
116
283
|
try {
|
|
117
284
|
const json = JSON.parse(data);
|
|
118
|
-
const token =
|
|
285
|
+
const token =
|
|
286
|
+
json.choices?.[0]?.delta?.content ??
|
|
287
|
+
json.choices?.[0]?.message?.content;
|
|
119
288
|
if (token) {
|
|
120
289
|
process.stdout.write(token);
|
|
121
290
|
reply += token;
|
|
@@ -134,6 +303,94 @@ rl.prompt();
|
|
|
134
303
|
rl.on("line", async (line) => {
|
|
135
304
|
const input = line.trim();
|
|
136
305
|
if (!input) return rl.prompt();
|
|
306
|
+
|
|
307
|
+
if (input === "/help") {
|
|
308
|
+
showHelp();
|
|
309
|
+
return rl.prompt();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (input.startsWith("/system")) {
|
|
313
|
+
const prompt = input.slice(7).trim();
|
|
314
|
+
if (!prompt) {
|
|
315
|
+
console.log(`当前 System Prompt: ${SYSTEM_PROMPT || "(空)"}`);
|
|
316
|
+
} else {
|
|
317
|
+
updateConfig("systemPrompt", prompt);
|
|
318
|
+
}
|
|
319
|
+
return rl.prompt();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (input.startsWith("/load")) {
|
|
323
|
+
const file = input.slice(5).trim();
|
|
324
|
+
if (!file) {
|
|
325
|
+
console.log("❌ 用法: /load <file_path>");
|
|
326
|
+
return rl.prompt();
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
const content = fs.readFileSync(file, "utf-8");
|
|
330
|
+
const userMsg = `(Context from ${path.basename(file)}):\n\`\`\`\n${content}\n\`\`\``;
|
|
331
|
+
messages.push({ role: "user", content: userMsg });
|
|
332
|
+
saveHistory(messages);
|
|
333
|
+
console.log(`✅ 已加载文件: ${file} (${content.length} chars)`);
|
|
334
|
+
console.log("👉 该文件内容将作为上下文发送给 AI,请继续提问。");
|
|
335
|
+
} catch (e) {
|
|
336
|
+
console.error(`❌ 读取文件失败: ${e.message}`);
|
|
337
|
+
}
|
|
338
|
+
return rl.prompt();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (input.startsWith("/config")) {
|
|
342
|
+
const parts = input.split(/\s+/);
|
|
343
|
+
const cmd = parts[1];
|
|
344
|
+
|
|
345
|
+
if (!cmd || cmd === "list") {
|
|
346
|
+
console.log(`
|
|
347
|
+
当前配置:
|
|
348
|
+
apiKey: ${API_KEY ? API_KEY.slice(0, 8) + "..." : "未设置"}
|
|
349
|
+
baseUrl: ${BASE_URL}
|
|
350
|
+
model: ${MODEL}
|
|
351
|
+
system: ${SYSTEM_PROMPT ? SYSTEM_PROMPT.slice(0, 20) + "..." : "默认"}
|
|
352
|
+
`);
|
|
353
|
+
return rl.prompt();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (cmd === "set") {
|
|
357
|
+
const key = parts[2];
|
|
358
|
+
const value = parts[3];
|
|
359
|
+
if (!key || !value) {
|
|
360
|
+
console.log("❌ 用法: /config set <key> <value>");
|
|
361
|
+
return rl.prompt();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (["apiKey", "baseUrl", "model", "systemPrompt"].includes(key)) {
|
|
365
|
+
updateConfig(key, value);
|
|
366
|
+
} else {
|
|
367
|
+
console.log("❌ 仅支持修改: apiKey, baseUrl, model, systemPrompt");
|
|
368
|
+
}
|
|
369
|
+
return rl.prompt();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
console.log("❌ 未知命令,用法: /config [list|set]");
|
|
373
|
+
return rl.prompt();
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (input.startsWith("/history")) {
|
|
377
|
+
const n = Number(input.split(/\s+/)[1]);
|
|
378
|
+
showHistory(messages, Number.isFinite(n) && n > 0 ? n : undefined);
|
|
379
|
+
return rl.prompt();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (input === "/clear") {
|
|
383
|
+
clearHistory();
|
|
384
|
+
messages = [];
|
|
385
|
+
console.log("✅ 历史已清空");
|
|
386
|
+
return rl.prompt();
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (input === "/export") {
|
|
390
|
+
exportHistory(messages);
|
|
391
|
+
return rl.prompt();
|
|
392
|
+
}
|
|
393
|
+
|
|
137
394
|
if (input === "exit" || input === "quit") {
|
|
138
395
|
rl.close();
|
|
139
396
|
return;
|
|
@@ -147,3 +404,4 @@ rl.on("line", async (line) => {
|
|
|
147
404
|
|
|
148
405
|
rl.prompt();
|
|
149
406
|
});
|
|
407
|
+
|