@raolin2025/claude-code-node 1.0.0
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 +294 -0
- package/package.json +24 -0
- package/src/core/cli.js +314 -0
- package/src/core/config.js +131 -0
- package/src/core/index.js +9 -0
- package/src/core/query-engine.js +344 -0
- package/src/core/session.js +120 -0
- package/src/core/streaming.js +119 -0
- package/src/core/token-budget.js +88 -0
- package/src/index.js +13 -0
- package/src/mcp/client.js +214 -0
- package/src/mcp/index.js +5 -0
- package/src/mcp/registry.js +176 -0
- package/src/permission/permission.js +37 -0
- package/src/security/bash-guard.js +279 -0
- package/src/security/enhanced-permission.js +310 -0
- package/src/security/index.js +7 -0
- package/src/security/path-guard.js +190 -0
- package/src/security/ssrf-guard.js +178 -0
- package/src/tools/ask-user.js +34 -0
- package/src/tools/bash.js +101 -0
- package/src/tools/file-edit.js +112 -0
- package/src/tools/file-read.js +105 -0
- package/src/tools/file-write.js +57 -0
- package/src/tools/glob.js +113 -0
- package/src/tools/grep.js +117 -0
- package/src/tools/index.js +110 -0
- package/src/tools/web-fetch.js +125 -0
- package/src/tools/web-search.js +75 -0
- package/src/types/index.js +126 -0
- package/src/utils/diff.js +181 -0
- package/src/utils/file-ops.js +124 -0
- package/src/utils/format.js +130 -0
- package/src/utils/index.js +7 -0
- package/src/utils/process.js +112 -0
package/README.md
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# AI Code Agent — Node.js Edition
|
|
2
|
+
|
|
3
|
+
> 基于 OpenAI 兼容协议的轻量级 AI 编程助手
|
|
4
|
+
> 零外部依赖 · 纯 JavaScript · DeepSeek 默认 · 安全加固
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 🚀 快速开始
|
|
9
|
+
|
|
10
|
+
### 前置要求
|
|
11
|
+
- Node.js ≥ 18.0.0
|
|
12
|
+
- 至少一个 API Key:`DEEPSEEK_API_KEY`(默认)或 `LLM_API_KEY`(通用)
|
|
13
|
+
|
|
14
|
+
### 启动
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# 进入项目目录
|
|
18
|
+
cd ~/.openclaw/workspace/claude-code-node
|
|
19
|
+
|
|
20
|
+
# 设置 API Key(DeepSeek 为默认)
|
|
21
|
+
export DEEPSEEK_API_KEY=***
|
|
22
|
+
# 或通用方式
|
|
23
|
+
export LLM_API_KEY=***
|
|
24
|
+
|
|
25
|
+
# 启动 REPL(默认使用 DeepSeek)
|
|
26
|
+
node src/index.js
|
|
27
|
+
|
|
28
|
+
# 一次性执行
|
|
29
|
+
node src/index.js "列出当前目录的文件"
|
|
30
|
+
|
|
31
|
+
# 指定模型
|
|
32
|
+
node src/index.js --model deepseek-reasoner
|
|
33
|
+
|
|
34
|
+
# 切换其他提供商
|
|
35
|
+
node src/index.js --model qwen-plus --api-base https://dashscope.aliyuncs.com/compatible-mode/v1
|
|
36
|
+
|
|
37
|
+
# 恢复上一次会话
|
|
38
|
+
node src/index.js --resume session-1747000000000-abc123
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 📖 命令行参数
|
|
44
|
+
|
|
45
|
+
| 参数 | 短写 | 说明 | 默认值 |
|
|
46
|
+
|------|------|------|--------|
|
|
47
|
+
| `--model` | `-m` | LLM 模型名 | `deepseek-chat` |
|
|
48
|
+
| `--system-prompt` | `-s` | 系统提示词 | `""` |
|
|
49
|
+
| `--permission-mode` | `-p` | 权限模式 | `ask` |
|
|
50
|
+
| `--max-turns` | `-t` | 最大工具循环轮数 | `100` |
|
|
51
|
+
| `--api-base` | | API 基础 URL | `https://api.deepseek.com/v1` |
|
|
52
|
+
| `--resume` | `-r` | 恢复会话 ID | |
|
|
53
|
+
| `--verbose` | `-v` | 详细输出 | `false` |
|
|
54
|
+
| `--no-stream` | | 禁用流式响应 | `false` |
|
|
55
|
+
| `--help` | `-h` | 显示帮助 | |
|
|
56
|
+
|
|
57
|
+
### 权限模式
|
|
58
|
+
|
|
59
|
+
| 模式 | 说明 |
|
|
60
|
+
|------|------|
|
|
61
|
+
| `ask` | 每次工具调用需确认(安全,推荐) |
|
|
62
|
+
| `always-allow` | 自动允许所有工具调用(仍受安全策略约束) |
|
|
63
|
+
| `deny` | 拒绝所有工具调用 |
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 💬 REPL 内置命令
|
|
68
|
+
|
|
69
|
+
进入 REPL 后,输入 `/` 开头的命令:
|
|
70
|
+
|
|
71
|
+
| 命令 | 说明 |
|
|
72
|
+
|------|------|
|
|
73
|
+
| `/help` | 显示帮助 |
|
|
74
|
+
| `/model NAME` | 切换模型 |
|
|
75
|
+
| `/tools` | 列出可用工具 |
|
|
76
|
+
| `/session` | 查看当前会话信息 |
|
|
77
|
+
| `/sessions` | 列出所有会话 |
|
|
78
|
+
| `/clear` | 清空当前对话 |
|
|
79
|
+
| `/config KEY` | 查看配置(支持点号路径如 `tools.bash.timeout`) |
|
|
80
|
+
| `/budget` | 查看 Token 预算使用情况 |
|
|
81
|
+
| `/exit` `/quit` | 退出(Ctrl+C 也可以) |
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## 🛠️ 内置工具(9 个)
|
|
86
|
+
|
|
87
|
+
| 工具 | 说明 | 权限级别 | 安全检查 |
|
|
88
|
+
|------|------|---------|---------|
|
|
89
|
+
| **Bash** | 执行 shell 命令 | `ask` | ✅ 命令安全扫描 |
|
|
90
|
+
| **Read** | 读取文件 | `always-allow` | ✅ 路径安全检查 |
|
|
91
|
+
| **Edit** | 精确文本替换编辑 | `ask` | ✅ 写入路径安全 |
|
|
92
|
+
| **Write** | 创建/覆盖文件 | `ask` | ✅ 写入路径安全 |
|
|
93
|
+
| **Glob** | 文件模式搜索 | `always-allow` | — |
|
|
94
|
+
| **Grep** | 内容搜索(rg/grep) | `always-allow` | — |
|
|
95
|
+
| **WebFetch** | 抓取网页内容 | `ask` | ✅ SSRF 防护 |
|
|
96
|
+
| **WebSearch** | 网页搜索 | `ask` | 需要 API Key |
|
|
97
|
+
| **AskUserQuestion** | 向用户提问 | `always-allow` | — |
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## 🔒 安全架构
|
|
102
|
+
|
|
103
|
+
本项目包含 **4 层安全防护**,总计 **964 行安全代码**:
|
|
104
|
+
|
|
105
|
+
### 1️⃣ SSRF 防护(178 行)
|
|
106
|
+
阻止 LLM 通过 WebFetch 访问内网和云元数据:
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
🚫 10.0.0.0/8 — 私有网络
|
|
110
|
+
🚫 172.16.0.0/12 — 私有网络
|
|
111
|
+
🚫 192.168.0.0/16 — 私有网络
|
|
112
|
+
🚫 169.254.0.0/16 — AWS/GCP 元数据
|
|
113
|
+
🚫 100.64.0.0/10 — 阿里云元数据 (100.100.100.200)
|
|
114
|
+
🚫 fc00::/7 — IPv6 唯一本地
|
|
115
|
+
🚫 fe80::/10 — IPv6 链路本地
|
|
116
|
+
✅ 127.0.0.0/8 — 回环(允许,本地开发)
|
|
117
|
+
✅ ::1 — IPv6 回环
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### 2️⃣ Bash 命令安全(279 行)
|
|
121
|
+
阻止 LLM 执行危险 shell 命令:
|
|
122
|
+
|
|
123
|
+
| 类别 | 示例 | 严重性 |
|
|
124
|
+
|------|------|--------|
|
|
125
|
+
| 破坏性操作 | `rm -rf /`, `dd of=/dev/sda`, `mkfs` | 🚫 CRITICAL |
|
|
126
|
+
| 敏感文件访问 | `cat /etc/shadow`, `~/.ssh/id_rsa` | 🚫 CRITICAL/HIGH |
|
|
127
|
+
| 远程执行 | `curl \| bash`, `wget \| sh` | 🚫 CRITICAL |
|
|
128
|
+
| 提权 | `sudo su`, `pkexec` | ⚠️ HIGH |
|
|
129
|
+
| 容器逃逸 | `nsenter --target 1`, 特权 docker | 🚫 CRITICAL |
|
|
130
|
+
| 内网数据外泄 | `curl http://192.168.x.x` | 🚫 CRITICAL |
|
|
131
|
+
| 系统重定向 | `> /etc/hosts` | 🚫 CRITICAL |
|
|
132
|
+
|
|
133
|
+
### 3️⃣ 路径安全防护(190 行)
|
|
134
|
+
防止 LLM 访问/修改敏感文件:
|
|
135
|
+
|
|
136
|
+
- **路径遍历检测** — `../../../etc/passwd` → 阻止
|
|
137
|
+
- **SSH 密钥保护** — 禁止读取 `~/.ssh/id_*` 私钥
|
|
138
|
+
- **系统目录写入保护** — 禁止写 `/etc/`, `/boot/`, `/usr/bin/`
|
|
139
|
+
- **敏感路径列表** — `/etc/shadow`, `/etc/sudoers` 等
|
|
140
|
+
|
|
141
|
+
### 4️⃣ 增强权限系统(310 行)
|
|
142
|
+
|
|
143
|
+
```mermaid
|
|
144
|
+
graph TD
|
|
145
|
+
A[工具调用请求] --> B{安全检查}
|
|
146
|
+
B -->|🚫 不安全| C[直接拒绝]
|
|
147
|
+
B -->|✅ 安全| D{规则匹配}
|
|
148
|
+
D -->|DENY 规则| C
|
|
149
|
+
D -->|ALLOW 规则| E[执行工具]
|
|
150
|
+
D -->|无匹配| F{权限模式}
|
|
151
|
+
F -->|ask| G[请求用户确认]
|
|
152
|
+
F -->|always-allow| E
|
|
153
|
+
F -->|deny| C
|
|
154
|
+
C --> H[审计日志]
|
|
155
|
+
E --> H
|
|
156
|
+
G --> H
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
特性:
|
|
160
|
+
- **规则持久化** — 保存到 `.claude-code/permissions.json`
|
|
161
|
+
- **审计日志** — 记录到 `.claude-code/audit.log`
|
|
162
|
+
- **安全一票否决** — 即使规则允许,安全检查不通过仍拒绝
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## 🌐 API — OpenAI 兼容协议(全行业通用)
|
|
167
|
+
|
|
168
|
+
### DeepSeek(默认)
|
|
169
|
+
```bash
|
|
170
|
+
export DEEPSEEK_API_KEY=***
|
|
171
|
+
node src/index.js
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### 其他 OpenAI 兼容提供商
|
|
175
|
+
```bash
|
|
176
|
+
# 通义千问
|
|
177
|
+
export LLM_API_KEY=***
|
|
178
|
+
node src/index.js --model qwen-plus --api-base https://dashscope.aliyuncs.com/compatible-mode/v1
|
|
179
|
+
|
|
180
|
+
# 智谱 GLM
|
|
181
|
+
node src/index.js --model glm-4-flash --api-base https://open.bigmodel.cn/api/paas/v4
|
|
182
|
+
|
|
183
|
+
# Moonshot Kimi
|
|
184
|
+
node src/index.js --model kimi-k2-0711 --api-base https://api.moonshot.cn/v1
|
|
185
|
+
|
|
186
|
+
# OpenAI
|
|
187
|
+
node src/index.js --model gpt-4o --api-base https://api.openai.com/v1
|
|
188
|
+
|
|
189
|
+
# Ollama 本地
|
|
190
|
+
node src/index.js --model qwen2.5 --api-base http://localhost:11434/v1
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## 📂 配置文件
|
|
196
|
+
|
|
197
|
+
### 项目级
|
|
198
|
+
`.claude-code/config.json` — 存放在项目根目录
|
|
199
|
+
|
|
200
|
+
### 用户级
|
|
201
|
+
`~/.claude-code/config.json` — 全局默认配置
|
|
202
|
+
|
|
203
|
+
### 配置项
|
|
204
|
+
|
|
205
|
+
```json
|
|
206
|
+
{
|
|
207
|
+
"model": "deepseek-chat",
|
|
208
|
+
"maxTurns": 100,
|
|
209
|
+
"maxBudgetTokens": 1000000,
|
|
210
|
+
"permissionMode": "ask",
|
|
211
|
+
"tools": {
|
|
212
|
+
"bash": { "timeout": 120 },
|
|
213
|
+
"fileRead": { "maxLines": 2000 },
|
|
214
|
+
"webFetch": { "timeout": 30 }
|
|
215
|
+
},
|
|
216
|
+
"mcp": {
|
|
217
|
+
"servers": {}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## 🔌 MCP 服务器
|
|
225
|
+
|
|
226
|
+
支持通过 Model Context Protocol 连接外部工具服务器:
|
|
227
|
+
|
|
228
|
+
```javascript
|
|
229
|
+
import { MCPRegistry } from './src/mcp/index.js'
|
|
230
|
+
|
|
231
|
+
const registry = new MCPRegistry()
|
|
232
|
+
registry.register('my-server', {
|
|
233
|
+
command: 'npx',
|
|
234
|
+
args: ['my-mcp-server'],
|
|
235
|
+
env: { API_KEY: 'xxx' }
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
await registry.connectAll()
|
|
239
|
+
const tools = registry.getAllTools()
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## 📊 项目结构
|
|
245
|
+
|
|
246
|
+
```
|
|
247
|
+
claude-code-node/ 33 文件 · 4307 行
|
|
248
|
+
├── src/
|
|
249
|
+
│ ├── core/ 1279 行 — 引擎、CLI、会话、配置
|
|
250
|
+
│ ├── tools/ 944 行 — 9 个内置工具
|
|
251
|
+
│ ├── security/ 964 行 — 4 层安全防护
|
|
252
|
+
│ ├── utils/ 544 行 — 差异、文件、进程、格式
|
|
253
|
+
│ ├── mcp/ 385 行 — MCP 客户端+注册表
|
|
254
|
+
│ ├── types/ 125 行 — 类型定义
|
|
255
|
+
│ ├── permission/ 37 行 — 基础权限(兼容)
|
|
256
|
+
│ └── index.js 8 行 — 入口
|
|
257
|
+
└── package.json
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## ⚠️ 安全注意事项
|
|
263
|
+
|
|
264
|
+
1. **始终使用 `ask` 权限模式** — 除非你完全信任 LLM 输出
|
|
265
|
+
2. **不要暴露 API Key** — 使用环境变量,不要硬编码
|
|
266
|
+
3. **WebSearch 需要单独配置** — 设置 `BRAVE_SEARCH_API_KEY` 或 `GOOGLE_SEARCH_API_KEY`
|
|
267
|
+
4. **审计日志定期审查** — 检查 `.claude-code/audit.log` 中的 DENY 记录
|
|
268
|
+
5. **安全规则可持久化** — 使用 `/allow` 命令添加会话级规则
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## 🔧 与原 Claude Code 的对比
|
|
273
|
+
|
|
274
|
+
| 特性 | 原版 (TypeScript/Bun) | 本版 (Node.js) |
|
|
275
|
+
|------|----------------------|----------------|
|
|
276
|
+
| 运行时 | Bun | Node.js ≥ 18 |
|
|
277
|
+
| 语言 | TypeScript | JavaScript (ESM) |
|
|
278
|
+
| 依赖 | ~200 npm 包 | **0 外部依赖** |
|
|
279
|
+
| 代码量 | 512,000+ 行 | 4,307 行 |
|
|
280
|
+
| 工具数 | ~40 | 9(核心) |
|
|
281
|
+
| API 协议 | Anthropic | **OpenAI 兼容(全行业通用)** |
|
|
282
|
+
| SSRF 防护 | ✅ | ✅ |
|
|
283
|
+
| 命令安全 | ✅ (2592行) | ✅ (279行) |
|
|
284
|
+
| 路径安全 | ✅ | ✅ |
|
|
285
|
+
| 审计日志 | ✅ | ✅ |
|
|
286
|
+
| MCP 支持 | ✅ 完整 | ✅ 简化版 |
|
|
287
|
+
| 流式响应 | ✅ | ✅ |
|
|
288
|
+
| 会话管理 | ✅ | ✅ |
|
|
289
|
+
| UI | Ink (React CLI) | 纯 readline |
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
*基于 Claude Code 架构的 OpenAI 兼容重构*
|
|
294
|
+
*零外部依赖 · DeepSeek 默认 · 安全加固 · MIT License*
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@raolin2025/claude-code-node",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Node.js 重构版 Claude Code CLI Agent 框架",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"cc-node": "src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node src/index.js",
|
|
12
|
+
"dev": "node --watch src/index.js",
|
|
13
|
+
"test": "node --test src/__tests__/*.js",
|
|
14
|
+
"repl": "node src/index.js"
|
|
15
|
+
},
|
|
16
|
+
"keywords": ["claude", "code", "agent", "cli", "llm"],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18.0.0"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"src/"
|
|
23
|
+
]
|
|
24
|
+
}
|
package/src/core/cli.js
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI 入口 — 命令行解析和 REPL 循环
|
|
3
|
+
* 对应原版: src/cli/ + src/entrypoints/
|
|
4
|
+
*/
|
|
5
|
+
import { createInterface } from 'readline'
|
|
6
|
+
import { QueryEngine, QueryEngineConfig } from './query-engine.js'
|
|
7
|
+
import { createDefaultRegistry } from '../tools/index.js'
|
|
8
|
+
import { SessionManager } from './session.js'
|
|
9
|
+
import { Config } from './config.js'
|
|
10
|
+
import { TokenBudget } from './token-budget.js'
|
|
11
|
+
import { PermissionChecker } from '../permission/permission.js'
|
|
12
|
+
|
|
13
|
+
const BANNER = `
|
|
14
|
+
╔═══════════════════════════════════════════════╗
|
|
15
|
+
║ AI Code Agent — Node.js Edition ║
|
|
16
|
+
║ OpenAI-Compatible · DeepSeek Default ║
|
|
17
|
+
║ Type '/help' for commands ║
|
|
18
|
+
║ Type '/exit' or Ctrl+C to quit ║
|
|
19
|
+
╚═══════════════════════════════════════════════╝
|
|
20
|
+
`.trim()
|
|
21
|
+
|
|
22
|
+
const HELP_TEXT = `
|
|
23
|
+
Commands:
|
|
24
|
+
/help — Show this help
|
|
25
|
+
/model NAME — Switch model
|
|
26
|
+
/tools — List available tools
|
|
27
|
+
/session — Show session info
|
|
28
|
+
/sessions — List all sessions
|
|
29
|
+
/clear — Clear conversation
|
|
30
|
+
/config KEY — Show config value
|
|
31
|
+
/budget — Show token budget
|
|
32
|
+
/exit — Exit (also Ctrl+C)
|
|
33
|
+
/quit — Same as /exit
|
|
34
|
+
`
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 解析命令行参数
|
|
38
|
+
*/
|
|
39
|
+
function parseArgs(argv) {
|
|
40
|
+
const args = {
|
|
41
|
+
model: 'deepseek-chat',
|
|
42
|
+
systemPrompt: '',
|
|
43
|
+
permissionMode: 'ask',
|
|
44
|
+
maxTurns: 100,
|
|
45
|
+
verbose: false,
|
|
46
|
+
apiBase: 'https://api.deepseek.com/v1',
|
|
47
|
+
resume: null,
|
|
48
|
+
noStream: false,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let i = 2 // skip node and script name
|
|
52
|
+
while (i < argv.length) {
|
|
53
|
+
const arg = argv[i]
|
|
54
|
+
switch (arg) {
|
|
55
|
+
case '--model':
|
|
56
|
+
case '-m':
|
|
57
|
+
args.model = argv[++i]
|
|
58
|
+
break
|
|
59
|
+
case '--system-prompt':
|
|
60
|
+
case '-s':
|
|
61
|
+
args.systemPrompt = argv[++i]
|
|
62
|
+
break
|
|
63
|
+
case '--permission-mode':
|
|
64
|
+
case '-p':
|
|
65
|
+
args.permissionMode = argv[++i]
|
|
66
|
+
break
|
|
67
|
+
case '--max-turns':
|
|
68
|
+
case '-t':
|
|
69
|
+
args.maxTurns = parseInt(argv[++i], 10)
|
|
70
|
+
break
|
|
71
|
+
case '--api-key':
|
|
72
|
+
args.apiKey = argv[++i]
|
|
73
|
+
break
|
|
74
|
+
case '--api-base':
|
|
75
|
+
args.apiBase = argv[++i]
|
|
76
|
+
break
|
|
77
|
+
case '--resume':
|
|
78
|
+
case '-r':
|
|
79
|
+
args.resume = argv[++i]
|
|
80
|
+
break
|
|
81
|
+
case '--verbose':
|
|
82
|
+
case '-v':
|
|
83
|
+
args.verbose = true
|
|
84
|
+
break
|
|
85
|
+
case '--no-stream':
|
|
86
|
+
args.noStream = true
|
|
87
|
+
break
|
|
88
|
+
case '--help':
|
|
89
|
+
case '-h':
|
|
90
|
+
console.log(`Usage: cc-node [options]
|
|
91
|
+
|
|
92
|
+
Options:
|
|
93
|
+
-m, --model NAME Model to use (required, e.g. deepseek-chat, qwen-plus, glm-4-flash)
|
|
94
|
+
-s, --system-prompt TEXT System prompt
|
|
95
|
+
-p, --permission-mode Permission mode: ask|always-allow|deny (default: ask)
|
|
96
|
+
-t, --max-turns N Max tool loop turns (default: 100)
|
|
97
|
+
--api-base URL API base URL (default: https://api.deepseek.com/v1)
|
|
98
|
+
--api-key KEY API key (or set LLM_API_KEY env)
|
|
99
|
+
-r, --resume ID Resume a session
|
|
100
|
+
-v, --verbose Verbose mode
|
|
101
|
+
--no-stream Disable streaming
|
|
102
|
+
-h, --help Show this help
|
|
103
|
+
|
|
104
|
+
Environment variables:
|
|
105
|
+
LLM_API_KEY Universal API key (recommended)
|
|
106
|
+
DEEPSEEK_API_KEY DeepSeek API key (default)
|
|
107
|
+
OPENAI_API_KEY OpenAI API key
|
|
108
|
+
QWEN_API_KEY Qwen (DashScope) API key
|
|
109
|
+
GLM_API_KEY Zhipu GLM API key
|
|
110
|
+
KIMI_API_KEY Moonshot Kimi API key
|
|
111
|
+
LLM_API_BASE API base URL (default: https://api.deepseek.com/v1)`)
|
|
112
|
+
process.exit(0)
|
|
113
|
+
default:
|
|
114
|
+
if (!arg.startsWith('-')) {
|
|
115
|
+
// 非选项参数视为一次性输入
|
|
116
|
+
args.oneShot = argv.slice(i).join(' ')
|
|
117
|
+
i = argv.length
|
|
118
|
+
}
|
|
119
|
+
break
|
|
120
|
+
}
|
|
121
|
+
i++
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return args
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 主入口
|
|
129
|
+
*/
|
|
130
|
+
export async function main() {
|
|
131
|
+
const cliArgs = parseArgs(process.argv)
|
|
132
|
+
|
|
133
|
+
// 加载配置
|
|
134
|
+
const config = new Config()
|
|
135
|
+
await config.load(process.cwd())
|
|
136
|
+
|
|
137
|
+
// 合并 CLI 参数 > 项目配置 > 用户配置 > 默认值
|
|
138
|
+
const model = cliArgs.model || config.get('model')
|
|
139
|
+
const systemPrompt = cliArgs.systemPrompt || ''
|
|
140
|
+
const permissionMode = cliArgs.permissionMode || config.get('permissionMode')
|
|
141
|
+
const maxTurns = cliArgs.maxTurns || config.get('maxTurns')
|
|
142
|
+
const apiBase = cliArgs.apiBase || config.get('apiBase') || process.env.LLM_API_BASE || ''
|
|
143
|
+
const apiKey = cliArgs.apiKey || config.get('apiKey') || ''
|
|
144
|
+
const verbose = cliArgs.verbose || config.get('verbose')
|
|
145
|
+
|
|
146
|
+
// 创建工具注册表
|
|
147
|
+
const registry = createDefaultRegistry()
|
|
148
|
+
|
|
149
|
+
// 创建会话管理器
|
|
150
|
+
const sessionManager = new SessionManager({
|
|
151
|
+
sessionsDir: config.get('sessionsDir'),
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
// 恢复或创建会话
|
|
155
|
+
let session
|
|
156
|
+
if (cliArgs.resume) {
|
|
157
|
+
session = await sessionManager.load(cliArgs.resume)
|
|
158
|
+
if (!session) {
|
|
159
|
+
console.error(`Session not found: ${cliArgs.resume}`)
|
|
160
|
+
process.exit(1)
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
session = await sessionManager.create()
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 创建查询引擎
|
|
167
|
+
const engineConfig = new QueryEngineConfig({
|
|
168
|
+
model,
|
|
169
|
+
systemPrompt,
|
|
170
|
+
permissionMode,
|
|
171
|
+
maxTurns,
|
|
172
|
+
apiBase,
|
|
173
|
+
apiKey,
|
|
174
|
+
verbose,
|
|
175
|
+
tools: registry.getAll(),
|
|
176
|
+
})
|
|
177
|
+
const engine = new QueryEngine(engineConfig)
|
|
178
|
+
|
|
179
|
+
// 恢复会话历史
|
|
180
|
+
if (session?.messages?.length) {
|
|
181
|
+
for (const msg of session.messages) {
|
|
182
|
+
if (msg.role === 'user') {
|
|
183
|
+
engine.state.messages.push({ role: 'user', content: msg.content })
|
|
184
|
+
} else if (msg.role === 'assistant') {
|
|
185
|
+
engine.state.messages.push({ role: 'assistant', content: msg.content })
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const tokenBudget = new TokenBudget({
|
|
191
|
+
maxTokens: config.get('maxBudgetTokens') || 200_000,
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
// 一次性输入模式
|
|
195
|
+
if (cliArgs.oneShot) {
|
|
196
|
+
const result = await engine.processMessage(cliArgs.oneShot)
|
|
197
|
+
console.log(result.response)
|
|
198
|
+
process.exit(0)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// REPL 模式
|
|
202
|
+
const rl = createInterface({
|
|
203
|
+
input: process.stdin,
|
|
204
|
+
output: process.stdout,
|
|
205
|
+
prompt: '> ',
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
console.log(BANNER)
|
|
209
|
+
console.log(`Model: ${model} | Permission: ${permissionMode} | Tools: ${registry.getNames().join(', ')}`)
|
|
210
|
+
console.log()
|
|
211
|
+
|
|
212
|
+
rl.prompt()
|
|
213
|
+
|
|
214
|
+
rl.on('line', async (line) => {
|
|
215
|
+
const input = line.trim()
|
|
216
|
+
if (!input) {
|
|
217
|
+
rl.prompt()
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// 命令处理
|
|
222
|
+
if (input.startsWith('/')) {
|
|
223
|
+
const [cmd, ...rest] = input.slice(1).split(' ')
|
|
224
|
+
switch (cmd) {
|
|
225
|
+
case 'help':
|
|
226
|
+
console.log(HELP_TEXT)
|
|
227
|
+
break
|
|
228
|
+
case 'model':
|
|
229
|
+
if (rest[0]) {
|
|
230
|
+
engine.config.model = rest.join(' ')
|
|
231
|
+
console.log(`Model switched to: ${engine.config.model}`)
|
|
232
|
+
} else {
|
|
233
|
+
console.log(`Current model: ${engine.config.model}`)
|
|
234
|
+
}
|
|
235
|
+
break
|
|
236
|
+
case 'tools':
|
|
237
|
+
console.log('Available tools:')
|
|
238
|
+
for (const name of registry.getNames()) {
|
|
239
|
+
const tool = registry.get(name)
|
|
240
|
+
console.log(` ${name} — ${tool.description.split('\n')[0]}`)
|
|
241
|
+
}
|
|
242
|
+
break
|
|
243
|
+
case 'session':
|
|
244
|
+
console.log(`Session: ${session.id}`)
|
|
245
|
+
console.log(`Title: ${session.title}`)
|
|
246
|
+
console.log(`Messages: ${session.messages?.length || 0}`)
|
|
247
|
+
console.log(`Turns: ${engine.state.turnCount}`)
|
|
248
|
+
break
|
|
249
|
+
case 'sessions': {
|
|
250
|
+
const sessions = await sessionManager.list()
|
|
251
|
+
if (sessions.length === 0) {
|
|
252
|
+
console.log('No sessions found')
|
|
253
|
+
} else {
|
|
254
|
+
for (const s of sessions) {
|
|
255
|
+
console.log(` ${s.id} — ${s.title} (${s.messageCount} msgs, ${s.updated})`)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
break
|
|
259
|
+
}
|
|
260
|
+
case 'clear':
|
|
261
|
+
engine.reset()
|
|
262
|
+
session = await sessionManager.create()
|
|
263
|
+
console.log('Conversation cleared')
|
|
264
|
+
break
|
|
265
|
+
case 'config':
|
|
266
|
+
if (rest[0]) {
|
|
267
|
+
const val = config.get(rest.join(' '))
|
|
268
|
+
console.log(`${rest.join(' ')} = ${JSON.stringify(val, null, 2)}`)
|
|
269
|
+
} else {
|
|
270
|
+
console.log(JSON.stringify(config.toJSON(), null, 2))
|
|
271
|
+
}
|
|
272
|
+
break
|
|
273
|
+
case 'budget':
|
|
274
|
+
console.log(tokenBudget.format())
|
|
275
|
+
break
|
|
276
|
+
case 'exit':
|
|
277
|
+
case 'quit':
|
|
278
|
+
console.log('Goodbye!')
|
|
279
|
+
process.exit(0)
|
|
280
|
+
default:
|
|
281
|
+
console.log(`Unknown command: /${cmd}. Type /help for available commands.`)
|
|
282
|
+
}
|
|
283
|
+
rl.prompt()
|
|
284
|
+
return
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// 发送到引擎
|
|
288
|
+
try {
|
|
289
|
+
const result = await engine.processMessage(input)
|
|
290
|
+
|
|
291
|
+
// 输出助手回复
|
|
292
|
+
console.log()
|
|
293
|
+
console.log(result.response)
|
|
294
|
+
console.log()
|
|
295
|
+
|
|
296
|
+
// 保存到会话
|
|
297
|
+
await sessionManager.appendMessage({ role: 'user', content: input })
|
|
298
|
+
await sessionManager.appendMessage({ role: 'assistant', content: result.response })
|
|
299
|
+
|
|
300
|
+
if (verbose) {
|
|
301
|
+
console.log(`[Turns: ${result.turns} | Tools: ${result.toolResults.length}]`)
|
|
302
|
+
}
|
|
303
|
+
} catch (err) {
|
|
304
|
+
console.error(`\nError: ${err.message}\n`)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
rl.prompt()
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
rl.on('close', () => {
|
|
311
|
+
console.log('\nGoodbye!')
|
|
312
|
+
process.exit(0)
|
|
313
|
+
})
|
|
314
|
+
}
|