xiaozhou-chat 1.0.4 → 1.0.6

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/tools.js ADDED
@@ -0,0 +1,178 @@
1
+
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { exec, spawn } from "node:child_process";
5
+
6
+ // 扫描目录辅助函数
7
+ export function scanDir(dir, ignoreList = [], prefix = "", depth = 0) {
8
+ if (depth > 10) return ""; // Recursion limit
9
+
10
+ // 默认忽略列表
11
+ const defaultIgnore = ["node_modules", ".git", "dist", "coverage", ".DS_Store", ".env", ".next", "build"];
12
+ const ignore = [...defaultIgnore, ...ignoreList];
13
+
14
+ let output = "";
15
+ try {
16
+ const files = fs.readdirSync(dir, { withFileTypes: true });
17
+
18
+ // Sort: directories first, then files
19
+ files.sort((a, b) => {
20
+ if (a.isDirectory() && !b.isDirectory()) return -1;
21
+ if (!a.isDirectory() && b.isDirectory()) return 1;
22
+ return a.name.localeCompare(b.name);
23
+ });
24
+
25
+ for (const file of files) {
26
+ if (ignore.includes(file.name)) continue;
27
+ if (file.isDirectory()) {
28
+ output += `${prefix}- ${file.name}/\n`;
29
+ output += scanDir(path.join(dir, file.name), ignoreList, `${prefix} `, depth + 1);
30
+ } else {
31
+ output += `${prefix}- ${file.name}\n`;
32
+ }
33
+ }
34
+ } catch (e) {
35
+ // ignore access errors
36
+ }
37
+ return output;
38
+ }
39
+
40
+ export const builtInTools = [
41
+ {
42
+ type: "function",
43
+ function: {
44
+ name: "read_file",
45
+ description: "读取本地文件的内容。当用户要求查看文件、分析代码或重构文件时使用。",
46
+ parameters: {
47
+ type: "object",
48
+ properties: {
49
+ path: { type: "string", description: "文件的相对或绝对路径" }
50
+ },
51
+ required: ["path"]
52
+ }
53
+ }
54
+ },
55
+ {
56
+ type: "function",
57
+ function: {
58
+ name: "write_file",
59
+ description: "将内容写入本地文件。用于生成代码、重构文件或保存配置。会覆盖现有文件。",
60
+ parameters: {
61
+ type: "object",
62
+ properties: {
63
+ path: { type: "string", description: "文件的相对或绝对路径" },
64
+ content: { type: "string", description: "要写入的文件内容" }
65
+ },
66
+ required: ["path", "content"]
67
+ }
68
+ }
69
+ },
70
+ {
71
+ type: "function",
72
+ function: {
73
+ name: "list_dir",
74
+ description: "列出目录下的文件和子目录。用于探索项目结构。",
75
+ parameters: {
76
+ type: "object",
77
+ properties: {
78
+ path: { type: "string", description: "目录路径,默认为当前目录" }
79
+ }
80
+ }
81
+ }
82
+ },
83
+ {
84
+ type: "function",
85
+ function: {
86
+ name: "run_command",
87
+ description: "在终端中执行 Shell 命令。用于安装依赖、运行测试、构建项目等。请谨慎使用。",
88
+ parameters: {
89
+ type: "object",
90
+ properties: {
91
+ command: { type: "string", description: "要执行的 Shell 命令" }
92
+ },
93
+ required: ["command"]
94
+ }
95
+ }
96
+ }
97
+ ];
98
+
99
+ // Map-based handlers for better maintainability
100
+ const handlers = {
101
+ async read_file(args) {
102
+ const filepath = path.resolve(process.cwd(), args.path);
103
+ if (!fs.existsSync(filepath)) return `Error: File not found: ${filepath}`;
104
+
105
+ // 安全检查:防止读取敏感文件 (如 /etc/passwd 或 .env)
106
+ // 简单实现:检查文件名
107
+ if (path.basename(filepath) === ".env" || path.basename(filepath).includes("key")) {
108
+ // Warning but allow if user explicitly asked? Better strict for now.
109
+ // return `Error: Access to sensitive file ${args.path} is restricted.`;
110
+ }
111
+
112
+ const content = fs.readFileSync(filepath, "utf-8");
113
+ return `(File Content of ${args.path}):\n${content}`;
114
+ },
115
+
116
+ async write_file(args) {
117
+ const filepath = path.resolve(process.cwd(), args.path);
118
+ const dir = path.dirname(filepath);
119
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
120
+
121
+ // 自动备份机制
122
+ if (fs.existsSync(filepath)) {
123
+ const backupPath = `${filepath}.bak`;
124
+ try {
125
+ fs.copyFileSync(filepath, backupPath);
126
+ console.log(`📦 已创建备份: ${path.basename(backupPath)}`);
127
+ } catch (e) {
128
+ console.error(`⚠️ 备份失败: ${e.message}`);
129
+ }
130
+ }
131
+
132
+ fs.writeFileSync(filepath, args.content, "utf-8");
133
+ console.log(`💾 AI 已写入文件: ${args.path}`);
134
+ return `Success: File written to ${args.path}`;
135
+ },
136
+
137
+ async list_dir(args) {
138
+ const dirpath = path.resolve(process.cwd(), args.path || ".");
139
+ if (!fs.existsSync(dirpath)) return `Error: Directory not found: ${dirpath}`;
140
+ // Use scanDir logic but non-recursive or shallow?
141
+ // The original list_dir was just fs.readdirSync.
142
+ // Let's keep it simple for list_dir, maybe just 1 level.
143
+ const files = fs.readdirSync(dirpath);
144
+ return `(Directory Listing of ${args.path || "."}):\n${files.join("\n")}`;
145
+ },
146
+
147
+ async run_command(args, context) {
148
+ // Context needs to provide a way to ask user for confirmation
149
+ const cmd = args.command;
150
+
151
+ // Safety check: ask user
152
+ if (context && context.confirmCommand) {
153
+ const approved = await context.confirmCommand(cmd);
154
+ if (!approved) return "Error: User denied command execution.";
155
+ } else {
156
+ console.log(`⚠️ Warning: Executing command without confirmation mechanism: ${cmd}`);
157
+ }
158
+
159
+ console.log(`> Executing: ${cmd}`);
160
+ return new Promise((resolve) => {
161
+ exec(cmd, (error, stdout, stderr) => {
162
+ if (error) {
163
+ resolve(`Error: ${error.message}\nStderr: ${stderr}`);
164
+ } else {
165
+ resolve(`Output:\n${stdout}\n${stderr ? `Stderr: ${stderr}` : ""}`);
166
+ }
167
+ });
168
+ });
169
+ }
170
+ };
171
+
172
+ export async function handleBuiltInTool(name, args, context) {
173
+ const handler = handlers[name];
174
+ if (handler) {
175
+ return await handler(args, context);
176
+ }
177
+ return null;
178
+ }
package/lib/ui.js ADDED
@@ -0,0 +1,114 @@
1
+
2
+ import process from "node:process";
3
+
4
+ export class Spinner {
5
+ constructor(text = "思考中...") {
6
+ this.text = text;
7
+ this.chars = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
8
+ this.index = 0;
9
+ this.timer = null;
10
+ }
11
+ start() {
12
+ if (this.timer) return;
13
+ process.stdout.write("\x1b[?25l"); // Hide cursor
14
+
15
+ // 立即打印第一帧
16
+ process.stdout.write(`\r\x1b[36m${this.chars[this.index]} ${this.text}\x1b[0m`);
17
+ this.index = (this.index + 1) % this.chars.length;
18
+
19
+ this.timer = setInterval(() => {
20
+ process.stdout.write(`\r\x1b[36m${this.chars[this.index]} ${this.text}\x1b[0m`);
21
+ this.index = (this.index + 1) % this.chars.length;
22
+ }, 80);
23
+ }
24
+ stop() {
25
+ if (this.timer) {
26
+ clearInterval(this.timer);
27
+ this.timer = null;
28
+ process.stdout.write("\r\x1b[K"); // Clear line
29
+ process.stdout.write("\x1b[?25h"); // Show cursor
30
+ }
31
+ }
32
+ }
33
+
34
+ export class StreamPrinter {
35
+ constructor() {
36
+ this.buffer = "";
37
+ this.isPrinting = false;
38
+ this.resolveIdle = null;
39
+ this.timer = null;
40
+ }
41
+
42
+ add(text) {
43
+ if (!text) return;
44
+ this.buffer += text;
45
+ this.start();
46
+ }
47
+
48
+ start() {
49
+ if (this.isPrinting) return;
50
+ this.isPrinting = true;
51
+ this.printLoop();
52
+ }
53
+
54
+ printLoop() {
55
+ if (this.buffer.length === 0) {
56
+ this.isPrinting = false;
57
+ if (this.resolveIdle) {
58
+ this.resolveIdle();
59
+ this.resolveIdle = null;
60
+ }
61
+ return;
62
+ }
63
+
64
+ // 动态速度控制
65
+ const len = this.buffer.length;
66
+ let count = 1;
67
+ let delay = 5; // 基础 5ms/字
68
+
69
+ // 激进加速策略
70
+ if (len > 200) { count = 50; delay = 1; }
71
+ else if (len > 50) { count = 10; delay = 2; }
72
+ else if (len > 20) { count = 5; delay = 3; }
73
+ else if (len > 5) { count = 2; delay = 4; }
74
+
75
+ // 使用迭代器确保不切断 Unicode 字符
76
+ let charIndex = 0;
77
+ let charCount = 0;
78
+ for (const char of this.buffer) {
79
+ charIndex += char.length;
80
+ charCount++;
81
+ if (charCount >= count) break;
82
+ }
83
+
84
+ const splitIndex = charIndex;
85
+ const chunk = this.buffer.slice(0, splitIndex);
86
+ this.buffer = this.buffer.slice(splitIndex);
87
+
88
+ process.stdout.write(chunk);
89
+
90
+ this.timer = setTimeout(() => this.printLoop(), delay);
91
+ }
92
+
93
+ stop() {
94
+ if (this.isPrinting) {
95
+ this.isPrinting = false;
96
+ this.buffer = "";
97
+ if (this.timer) {
98
+ clearTimeout(this.timer);
99
+ this.timer = null;
100
+ }
101
+ if (this.resolveIdle) {
102
+ this.resolveIdle();
103
+ this.resolveIdle = null;
104
+ }
105
+ }
106
+ }
107
+
108
+ async waitIdle() {
109
+ if (!this.isPrinting && this.buffer.length === 0) return;
110
+ return new Promise(resolve => {
111
+ this.resolveIdle = resolve;
112
+ });
113
+ }
114
+ }
package/lib/utils.js ADDED
@@ -0,0 +1,21 @@
1
+
2
+ export function estimateTokens(str) {
3
+ // 简易 Token 估算策略
4
+ // ASCII 字符 (英文/数字/符号): 约 0.25 Token (4 chars ~= 1 token)
5
+ // CJK 字符 (中文/日文/韩文): 约 1.5 Token (1 char ~= 1.5 token)
6
+ // 这是一个保守估计,确保不会轻易超出 limits
7
+ let tokens = 0;
8
+ for (let i = 0; i < str.length; i++) {
9
+ const code = str.charCodeAt(i);
10
+ if (code <= 128) {
11
+ tokens += 0.25;
12
+ } else {
13
+ tokens += 1.5;
14
+ }
15
+ }
16
+ return Math.ceil(tokens);
17
+ }
18
+
19
+ export function sleep(ms) {
20
+ return new Promise((resolve) => setTimeout(resolve, ms));
21
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xiaozhou-chat",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "CLI chatbot based on NewAPI",
5
5
  "bin": {
6
6
  "xiaozhou-chat": "bin/cli.js"
@@ -19,7 +19,10 @@
19
19
  "pkg": "^5.8.1"
20
20
  },
21
21
  "dependencies": {
22
- "minimist": "^1.2.8"
22
+ "marked": "^15.0.12",
23
+ "marked-terminal": "^7.3.0",
24
+ "minimist": "^1.2.8",
25
+ "moment": "^2.30.1"
23
26
  },
24
27
  "pkg": {
25
28
  "assets": []
package/update_config.js DELETED
@@ -1,19 +0,0 @@
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
- }