@raolin2025/claude-code-node 1.0.0 → 1.1.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 CHANGED
@@ -1,10 +1,25 @@
1
1
  # AI Code Agent — Node.js Edition
2
2
 
3
+
3
4
  > 基于 OpenAI 兼容协议的轻量级 AI 编程助手
4
5
  > 零外部依赖 · 纯 JavaScript · DeepSeek 默认 · 安全加固
5
6
 
7
+ [![npm version](https://img.shields.io/npm/v/@raolin2025/claude-code-node.svg)](https://www.npmjs.com/package/@raolin2025/claude-code-node) [![GitHub](https://img.shields.io/badge/GitHub-bg1avd%2Fclaude--code--node-blue)](https://github.com/bg1avd/claude-code-node) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
8
  ---
7
9
 
10
+ ## 📦 安装
11
+
12
+ ```bash
13
+ # npm 全局安装
14
+ npm install -g @raolin2025/claude-code-node
15
+
16
+ # 或 npx 直接运行(无需安装)
17
+ npx @raolin2025/claude-code-node
18
+
19
+ # 安装后使用 cc-node 命令
20
+ cc-node
21
+ ```
22
+
8
23
  ## 🚀 快速开始
9
24
 
10
25
  ### 前置要求
@@ -15,29 +30,29 @@
15
30
 
16
31
  ```bash
17
32
  # 进入项目目录
18
- cd ~/.openclaw/workspace/claude-code-node
19
33
 
20
34
  # 设置 API Key(DeepSeek 为默认)
21
- export DEEPSEEK_API_KEY=***
35
+ export DEEPSEEK_API_KEY=your_key_here
22
36
  # 或通用方式
23
- export LLM_API_KEY=***
37
+ export LLM_API_KEY=your_key_here
24
38
 
25
39
  # 启动 REPL(默认使用 DeepSeek)
26
- node src/index.js
40
+ cc-node
27
41
 
28
42
  # 一次性执行
29
- node src/index.js "列出当前目录的文件"
43
+ cc-node "列出当前目录的文件"
30
44
 
31
45
  # 指定模型
32
- node src/index.js --model deepseek-reasoner
46
+ cc-node --model deepseek-reasoner
33
47
 
34
48
  # 切换其他提供商
35
- node src/index.js --model qwen-plus --api-base https://dashscope.aliyuncs.com/compatible-mode/v1
49
+ cc-node --model qwen-plus --api-base https://dashscope.aliyuncs.com/compatible-mode/v1
36
50
 
37
51
  # 恢复上一次会话
38
- node src/index.js --resume session-1747000000000-abc123
52
+ cc-node --resume session-1747000000000-abc123
39
53
  ```
40
54
 
55
+ [![npm version](https://img.shields.io/npm/v/@raolin2025/claude-code-node.svg)](https://www.npmjs.com/package/@raolin2025/claude-code-node) [![GitHub](https://img.shields.io/badge/GitHub-bg1avd%2Fclaude--code--node-blue)](https://github.com/bg1avd/claude-code-node) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
41
56
  ---
42
57
 
43
58
  ## 📖 命令行参数
@@ -62,6 +77,7 @@ node src/index.js --resume session-1747000000000-abc123
62
77
  | `always-allow` | 自动允许所有工具调用(仍受安全策略约束) |
63
78
  | `deny` | 拒绝所有工具调用 |
64
79
 
80
+ [![npm version](https://img.shields.io/npm/v/@raolin2025/claude-code-node.svg)](https://www.npmjs.com/package/@raolin2025/claude-code-node) [![GitHub](https://img.shields.io/badge/GitHub-bg1avd%2Fclaude--code--node-blue)](https://github.com/bg1avd/claude-code-node) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
65
81
  ---
66
82
 
67
83
  ## 💬 REPL 内置命令
@@ -80,6 +96,7 @@ node src/index.js --resume session-1747000000000-abc123
80
96
  | `/budget` | 查看 Token 预算使用情况 |
81
97
  | `/exit` `/quit` | 退出(Ctrl+C 也可以) |
82
98
 
99
+ [![npm version](https://img.shields.io/npm/v/@raolin2025/claude-code-node.svg)](https://www.npmjs.com/package/@raolin2025/claude-code-node) [![GitHub](https://img.shields.io/badge/GitHub-bg1avd%2Fclaude--code--node-blue)](https://github.com/bg1avd/claude-code-node) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
83
100
  ---
84
101
 
85
102
  ## 🛠️ 内置工具(9 个)
@@ -96,6 +113,7 @@ node src/index.js --resume session-1747000000000-abc123
96
113
  | **WebSearch** | 网页搜索 | `ask` | 需要 API Key |
97
114
  | **AskUserQuestion** | 向用户提问 | `always-allow` | — |
98
115
 
116
+ [![npm version](https://img.shields.io/npm/v/@raolin2025/claude-code-node.svg)](https://www.npmjs.com/package/@raolin2025/claude-code-node) [![GitHub](https://img.shields.io/badge/GitHub-bg1avd%2Fclaude--code--node-blue)](https://github.com/bg1avd/claude-code-node) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
99
117
  ---
100
118
 
101
119
  ## 🔒 安全架构
@@ -161,35 +179,37 @@ graph TD
161
179
  - **审计日志** — 记录到 `.claude-code/audit.log`
162
180
  - **安全一票否决** — 即使规则允许,安全检查不通过仍拒绝
163
181
 
182
+ [![npm version](https://img.shields.io/npm/v/@raolin2025/claude-code-node.svg)](https://www.npmjs.com/package/@raolin2025/claude-code-node) [![GitHub](https://img.shields.io/badge/GitHub-bg1avd%2Fclaude--code--node-blue)](https://github.com/bg1avd/claude-code-node) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
164
183
  ---
165
184
 
166
185
  ## 🌐 API — OpenAI 兼容协议(全行业通用)
167
186
 
168
187
  ### DeepSeek(默认)
169
188
  ```bash
170
- export DEEPSEEK_API_KEY=***
171
- node src/index.js
189
+ export DEEPSEEK_API_KEY=your_key_here
190
+ cc-node
172
191
  ```
173
192
 
174
193
  ### 其他 OpenAI 兼容提供商
175
194
  ```bash
176
195
  # 通义千问
177
- export LLM_API_KEY=***
178
- node src/index.js --model qwen-plus --api-base https://dashscope.aliyuncs.com/compatible-mode/v1
196
+ export LLM_API_KEY=your_key_here
197
+ cc-node --model qwen-plus --api-base https://dashscope.aliyuncs.com/compatible-mode/v1
179
198
 
180
199
  # 智谱 GLM
181
- node src/index.js --model glm-4-flash --api-base https://open.bigmodel.cn/api/paas/v4
200
+ cc-node --model glm-4-flash --api-base https://open.bigmodel.cn/api/paas/v4
182
201
 
183
202
  # Moonshot Kimi
184
- node src/index.js --model kimi-k2-0711 --api-base https://api.moonshot.cn/v1
203
+ cc-node --model kimi-k2-0711 --api-base https://api.moonshot.cn/v1
185
204
 
186
205
  # OpenAI
187
- node src/index.js --model gpt-4o --api-base https://api.openai.com/v1
206
+ cc-node --model gpt-4o --api-base https://api.openai.com/v1
188
207
 
189
208
  # Ollama 本地
190
- node src/index.js --model qwen2.5 --api-base http://localhost:11434/v1
209
+ cc-node --model qwen2.5 --api-base http://localhost:11434/v1
191
210
  ```
192
211
 
212
+ [![npm version](https://img.shields.io/npm/v/@raolin2025/claude-code-node.svg)](https://www.npmjs.com/package/@raolin2025/claude-code-node) [![GitHub](https://img.shields.io/badge/GitHub-bg1avd%2Fclaude--code--node-blue)](https://github.com/bg1avd/claude-code-node) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
193
213
  ---
194
214
 
195
215
  ## 📂 配置文件
@@ -219,6 +239,7 @@ node src/index.js --model qwen2.5 --api-base http://localhost:11434/v1
219
239
  }
220
240
  ```
221
241
 
242
+ [![npm version](https://img.shields.io/npm/v/@raolin2025/claude-code-node.svg)](https://www.npmjs.com/package/@raolin2025/claude-code-node) [![GitHub](https://img.shields.io/badge/GitHub-bg1avd%2Fclaude--code--node-blue)](https://github.com/bg1avd/claude-code-node) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
222
243
  ---
223
244
 
224
245
  ## 🔌 MCP 服务器
@@ -239,6 +260,7 @@ await registry.connectAll()
239
260
  const tools = registry.getAllTools()
240
261
  ```
241
262
 
263
+ [![npm version](https://img.shields.io/npm/v/@raolin2025/claude-code-node.svg)](https://www.npmjs.com/package/@raolin2025/claude-code-node) [![GitHub](https://img.shields.io/badge/GitHub-bg1avd%2Fclaude--code--node-blue)](https://github.com/bg1avd/claude-code-node) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
242
264
  ---
243
265
 
244
266
  ## 📊 项目结构
@@ -257,6 +279,7 @@ claude-code-node/ 33 文件 · 4307 行
257
279
  └── package.json
258
280
  ```
259
281
 
282
+ [![npm version](https://img.shields.io/npm/v/@raolin2025/claude-code-node.svg)](https://www.npmjs.com/package/@raolin2025/claude-code-node) [![GitHub](https://img.shields.io/badge/GitHub-bg1avd%2Fclaude--code--node-blue)](https://github.com/bg1avd/claude-code-node) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
260
283
  ---
261
284
 
262
285
  ## ⚠️ 安全注意事项
@@ -267,6 +290,7 @@ claude-code-node/ 33 文件 · 4307 行
267
290
  4. **审计日志定期审查** — 检查 `.claude-code/audit.log` 中的 DENY 记录
268
291
  5. **安全规则可持久化** — 使用 `/allow` 命令添加会话级规则
269
292
 
293
+ [![npm version](https://img.shields.io/npm/v/@raolin2025/claude-code-node.svg)](https://www.npmjs.com/package/@raolin2025/claude-code-node) [![GitHub](https://img.shields.io/badge/GitHub-bg1avd%2Fclaude--code--node-blue)](https://github.com/bg1avd/claude-code-node) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
270
294
  ---
271
295
 
272
296
  ## 🔧 与原 Claude Code 的对比
@@ -288,7 +312,68 @@ claude-code-node/ 33 文件 · 4307 行
288
312
  | 会话管理 | ✅ | ✅ |
289
313
  | UI | Ink (React CLI) | 纯 readline |
290
314
 
315
+ [![npm version](https://img.shields.io/npm/v/@raolin2025/claude-code-node.svg)](https://www.npmjs.com/package/@raolin2025/claude-code-node) [![GitHub](https://img.shields.io/badge/GitHub-bg1avd%2Fclaude--code--node-blue)](https://github.com/bg1avd/claude-code-node) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
291
316
  ---
292
317
 
293
318
  *基于 Claude Code 架构的 OpenAI 兼容重构*
294
319
  *零外部依赖 · DeepSeek 默认 · 安全加固 · MIT License*
320
+
321
+ ## 📡 通讯通道 (Notification Channels)
322
+
323
+ cc-node 支持多平台消息推送,任务完成或出错时自动通知到手机。
324
+
325
+ ### 支持的通道
326
+
327
+ | 通道 | 配置方式 | 说明 |
328
+ |------|----------|------|
329
+ | **Telegram** | Bot Token + Chat ID | 最推荐,支持 Markdown |
330
+ | **企业微信** | Webhook URL | 群机器人 |
331
+ | **飞书** | Webhook URL | 群机器人 |
332
+ | **Discord** | Webhook URL | 服务器频道 |
333
+ | **Slack** | Webhook URL | Incoming Webhook |
334
+ | **自定义** | HTTP URL + Method | 任意 Webhook |
335
+
336
+ ### 快速配置 (Telegram 为例)
337
+
338
+ ```bash
339
+ # 1. 在 Telegram 找 @BotFather 创建 Bot,拿到 Token
340
+ # 2. 获取你的 Chat ID(给 Bot 发消息后访问 https://api.telegram.org/bot<TOKEN>/getUpdates)
341
+ # 3. 设置环境变量
342
+ export CC_NODE_CHANNEL_TELEGRAM_TOKEN=123456:ABC-DEF
343
+ export CC_NODE_CHANNEL_TELEGRAM_CHAT_ID=78901234
344
+ export CC_NODE_CHANNEL_DEFAULT=telegram
345
+
346
+ # 4. 启动 cc-node,会自动加载
347
+ cc-node
348
+ ```
349
+
350
+ ### 配置文件方式
351
+
352
+ 在 `.claude-code/config.json` 中:
353
+
354
+ ```json
355
+ {
356
+ "channels": {
357
+ "telegram": {
358
+ "type": "telegram",
359
+ "token": "123456:ABC-DEF",
360
+ "chatId": "78901234"
361
+ }
362
+ },
363
+ "defaultChannel": "telegram"
364
+ }
365
+ ```
366
+
367
+ ### REPL 命令
368
+
369
+ ```
370
+ /channel list — 列出已配置通道
371
+ /channel test — 测试通道连通性
372
+ /channel send hello — 手动发送消息
373
+ ```
374
+
375
+ ### 通知时机
376
+
377
+ - ✅ **任务完成**(one-shot 模式自动通知)
378
+ - ❌ **执行出错**(自动通知错误内容)
379
+ - 🔄 **手动发送**(`/channel send` 命令)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@raolin2025/claude-code-node",
3
- "version": "1.0.0",
4
- "description": "Node.js 重构版 Claude Code CLI Agent 框架",
3
+ "version": "1.1.0",
4
+ "description": "Node.js AI Code Agent CLI - Zero dependencies, pure JavaScript, security hardened",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
7
7
  "bin": {
@@ -13,8 +13,17 @@
13
13
  "test": "node --test src/__tests__/*.js",
14
14
  "repl": "node src/index.js"
15
15
  },
16
- "keywords": ["claude", "code", "agent", "cli", "llm"],
16
+ "keywords": ["claude", "code", "agent", "cli", "llm", "ai", "security"],
17
17
  "license": "MIT",
18
+ "author": "bg1avd",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/bg1avd/claude-code-node.git"
22
+ },
23
+ "homepage": "https://github.com/bg1avd/claude-code-node#readme",
24
+ "bugs": {
25
+ "url": "https://github.com/bg1avd/claude-code-node/issues"
26
+ },
18
27
  "engines": {
19
28
  "node": ">=18.0.0"
20
29
  },
@@ -0,0 +1,361 @@
1
+ /**
2
+ * 通讯通道模块 — 支持多平台消息推送
3
+ *
4
+ * 支持的通道:
5
+ * - Telegram Bot API
6
+ * - 企业微信 (WeCom) Webhook
7
+ * - 飞书 (Feishu) Webhook
8
+ * - Discord Webhook
9
+ * - Slack Webhook
10
+ * - 自定义 HTTP Webhook
11
+ *
12
+ * 配置方式:
13
+ * 环境变量或 .claude-code/config.json 中的 channels 字段
14
+ *
15
+ * 用法:
16
+ * import { ChannelManager } from './channel.js'
17
+ * const cm = new ChannelManager()
18
+ * await cm.send('任务完成!结果:xxx')
19
+ * await cm.send('⚠️ 警告', { channel: 'telegram' })
20
+ */
21
+
22
+ import { readFileSync, existsSync } from 'fs'
23
+ import { resolve } from 'path'
24
+ import { homedir } from 'os'
25
+ import { execSync } from 'child_process'
26
+
27
+ // ============================================================
28
+ // 通道适配器
29
+ // ============================================================
30
+
31
+ /** Telegram Bot API 适配器 */
32
+ class TelegramChannel {
33
+ constructor({ token, chatId }) {
34
+ this.token = token
35
+ this.chatId = chatId
36
+ }
37
+
38
+ get name() { return 'telegram' }
39
+
40
+ async send(text, options = {}) {
41
+ const url = `https://api.telegram.org/bot${this.token}/sendMessage`
42
+ const body = {
43
+ chat_id: this.chatId,
44
+ text,
45
+ parse_mode: options.parseMode || 'Markdown',
46
+ disable_notification: options.silent || false,
47
+ }
48
+ return fetch(url, {
49
+ method: 'POST',
50
+ headers: { 'Content-Type': 'application/json' },
51
+ body: JSON.stringify(body),
52
+ }).then(r => r.json())
53
+ }
54
+ }
55
+
56
+ /** 企业微信 Webhook 适配器 */
57
+ class WeComChannel {
58
+ constructor({ webhookUrl }) {
59
+ this.webhookUrl = webhookUrl
60
+ }
61
+
62
+ get name() { return 'wecom' }
63
+
64
+ async send(text, options = {}) {
65
+ const body = {
66
+ msgtype: options.msgType || 'text',
67
+ text: { content: text },
68
+ markdown: options.parseMode === 'markdown' ? { content: text } : undefined,
69
+ }
70
+ // 清理 undefined 字段
71
+ if (body.markdown === undefined) delete body.markdown
72
+ if (body.msgtype === 'markdown') delete body.text
73
+
74
+ return fetch(this.webhookUrl, {
75
+ method: 'POST',
76
+ headers: { 'Content-Type': 'application/json' },
77
+ body: JSON.stringify(body),
78
+ }).then(r => r.json())
79
+ }
80
+ }
81
+
82
+ /** 飞书 Webhook 适配器 */
83
+ class FeishuChannel {
84
+ constructor({ webhookUrl }) {
85
+ this.webhookUrl = webhookUrl
86
+ }
87
+
88
+ get name() { return 'feishu' }
89
+
90
+ async send(text, options = {}) {
91
+ const body = {
92
+ msg_type: options.msgType || 'text',
93
+ content: options.parseMode === 'markdown'
94
+ ? JSON.stringify({ text: text })
95
+ : JSON.stringify({ text: text }),
96
+ }
97
+ return fetch(this.webhookUrl, {
98
+ method: 'POST',
99
+ headers: { 'Content-Type': 'application/json' },
100
+ body: JSON.stringify(body),
101
+ }).then(r => r.json())
102
+ }
103
+ }
104
+
105
+ /** Discord Webhook 适配器 */
106
+ class DiscordChannel {
107
+ constructor({ webhookUrl }) {
108
+ this.webhookUrl = webhookUrl
109
+ }
110
+
111
+ get name() { return 'discord' }
112
+
113
+ async send(text, options = {}) {
114
+ const body = {
115
+ content: text,
116
+ username: options.username || 'cc-node',
117
+ }
118
+ return fetch(this.webhookUrl, {
119
+ method: 'POST',
120
+ headers: { 'Content-Type': 'application/json' },
121
+ body: JSON.stringify(body),
122
+ }).then(r => r.json())
123
+ }
124
+ }
125
+
126
+ /** Slack Webhook 适配器 */
127
+ class SlackChannel {
128
+ constructor({ webhookUrl }) {
129
+ this.webhookUrl = webhookUrl
130
+ }
131
+
132
+ get name() { return 'slack' }
133
+
134
+ async send(text, options = {}) {
135
+ const body = {
136
+ text,
137
+ username: options.username || 'cc-node',
138
+ mrkdwn: options.parseMode !== 'plain',
139
+ }
140
+ return fetch(this.webhookUrl, {
141
+ method: 'POST',
142
+ headers: { 'Content-Type': 'application/json' },
143
+ body: JSON.stringify(body),
144
+ }).then(r => r.text())
145
+ }
146
+ }
147
+
148
+ /** 通用 HTTP Webhook 适配器 */
149
+ class WebhookChannel {
150
+ constructor({ url, method = 'POST', headers = {}, bodyTemplate }) {
151
+ this.url = url
152
+ this.method = method
153
+ this.headers = headers
154
+ this.bodyTemplate = bodyTemplate
155
+ }
156
+
157
+ get name() { return 'webhook' }
158
+
159
+ async send(text, options = {}) {
160
+ const body = this.bodyTemplate
161
+ ? this.bodyTemplate.replace('{text}', text)
162
+ : JSON.stringify({ text, ...options })
163
+
164
+ return fetch(this.url, {
165
+ method: this.method,
166
+ headers: { 'Content-Type': 'application/json', ...this.headers },
167
+ body,
168
+ }).then(r => r.text())
169
+ }
170
+ }
171
+
172
+ // ============================================================
173
+ // 通道管理器
174
+ // ============================================================
175
+
176
+ const CHANNEL_ADAPTERS = {
177
+ telegram: TelegramChannel,
178
+ wecom: WeComChannel,
179
+ feishu: FeishuChannel,
180
+ discord: DiscordChannel,
181
+ slack: SlackChannel,
182
+ webhook: WebhookChannel,
183
+ }
184
+
185
+ const ENV_PREFIX = 'CC_NODE_CHANNEL_'
186
+
187
+ export class ChannelManager {
188
+ constructor(config = {}) {
189
+ this.channels = new Map()
190
+ this.defaultChannel = config.defaultChannel || null
191
+ this._loadFromConfig(config)
192
+ this._loadFromEnv()
193
+ }
194
+
195
+ /** 从配置对象加载通道 */
196
+ _loadFromConfig(config) {
197
+ if (!config.channels) return
198
+ for (const [name, chConfig] of Object.entries(config.channels)) {
199
+ if (chConfig.enabled === false) continue
200
+ this._registerChannel(name, chConfig)
201
+ }
202
+ if (config.defaultChannel) {
203
+ this.defaultChannel = config.defaultChannel
204
+ }
205
+ }
206
+
207
+ /** 从环境变量加载通道
208
+ *
209
+ * 环境变量格式:
210
+ * CC_NODE_CHANNEL_TELEGRAM_TOKEN=xxx
211
+ * CC_NODE_CHANNEL_TELEGRAM_CHAT_ID=xxx
212
+ * CC_NODE_CHANNEL_WECOM_WEBHOOK_URL=xxx
213
+ * CC_NODE_CHANNEL_DEFAULT=telegram
214
+ */
215
+ _loadFromEnv() {
216
+ const envChannels = {}
217
+
218
+ for (const [key, value] of Object.entries(process.env)) {
219
+ if (!key.startsWith(ENV_PREFIX)) continue
220
+ const rest = key.slice(ENV_PREFIX.length)
221
+
222
+ if (rest === 'DEFAULT') {
223
+ this.defaultChannel = value.toLowerCase()
224
+ continue
225
+ }
226
+
227
+ // 解析 CC_NODE_CHANNEL_<TYPE>_<PARAM>
228
+ const parts = rest.split('_')
229
+ const channelType = parts[0].toLowerCase()
230
+ const param = parts.slice(1).join('_').toLowerCase()
231
+
232
+ if (!envChannels[channelType]) envChannels[channelType] = { type: channelType }
233
+ // 将 SNAKE_CASE 转为 camelCase
234
+ const camelKey = param.replace(/_([a-z])/g, (_, c) => c.toUpperCase())
235
+ envChannels[channelType][camelKey] = value
236
+ }
237
+
238
+ for (const [name, chConfig] of Object.entries(envChannels)) {
239
+ if (!this.channels.has(name)) {
240
+ this._registerChannel(name, chConfig)
241
+ }
242
+ }
243
+ }
244
+
245
+ /** 注册一个通道 */
246
+ _registerChannel(name, config) {
247
+ const type = config.type || name
248
+ const Adapter = CHANNEL_ADAPTERS[type]
249
+ if (!Adapter) {
250
+ console.warn(`[channel] Unknown channel type: ${type}`)
251
+ return
252
+ }
253
+ try {
254
+ const instance = new Adapter(config)
255
+ this.channels.set(name, instance)
256
+ } catch (e) {
257
+ console.warn(`[channel] Failed to register ${name}: ${e.message}`)
258
+ }
259
+ }
260
+
261
+ /** 获取已注册通道列表 */
262
+ list() {
263
+ return Array.from(this.channels.keys())
264
+ }
265
+
266
+ /** 发送消息
267
+ * @param {string} text — 消息内容
268
+ * @param {object} options —
269
+ * channel: 通道名(默认用 defaultChannel 或全部通道)
270
+ * parseMode: 'Markdown' | 'plain'
271
+ * silent: 静默通知
272
+ */
273
+ async send(text, options = {}) {
274
+ const targetChannels = options.channel
275
+ ? [options.channel]
276
+ : this.defaultChannel
277
+ ? [this.defaultChannel]
278
+ : this.list()
279
+
280
+ const results = []
281
+ for (const name of targetChannels) {
282
+ const ch = this.channels.get(name)
283
+ if (!ch) {
284
+ results.push({ channel: name, ok: false, error: 'not registered' })
285
+ continue
286
+ }
287
+ try {
288
+ const result = await ch.send(text, options)
289
+ results.push({ channel: name, ok: true, result })
290
+ } catch (e) {
291
+ results.push({ channel: name, ok: false, error: e.message })
292
+ }
293
+ }
294
+ return results
295
+ }
296
+
297
+ /** 发送模板消息
298
+ * @param {string} template — 模板名:'task-done', 'error', 'question'
299
+ * @param {object} data — 模板变量
300
+ */
301
+ async sendTemplate(template, data = {}, options = {}) {
302
+ const templates = {
303
+ 'task-done': `✅ 任务完成\n${data.task || ''}\n${data.result ? '结果:' + data.result : ''}`,
304
+ 'error': `❌ 错误\n${data.task || ''}\n${data.error || ''}`,
305
+ 'question': `❓ 需要确认\n${data.question || ''}\n${data.options ? '选项:' + data.options.join(' / ') : ''}`,
306
+ 'progress': `🔄 进度更新\n${data.task || ''}\n${data.progress || ''}${data.percent ? ' (' + data.percent + '%)' : ''}`,
307
+ 'warning': `⚠️ 警告\n${data.message || ''}`,
308
+ }
309
+ const text = templates[template] || `📢 ${data.message || text}`
310
+ return this.send(text, options)
311
+ }
312
+ }
313
+
314
+ // ============================================================
315
+ // REPL 命令集成
316
+ // ============================================================
317
+
318
+ /** 注册 /channel 命令到 REPL */
319
+ export function registerChannelCommands(repl, channelManager) {
320
+ if (!repl || !channelManager) return
321
+
322
+ repl.addCommand('/channel', {
323
+ description: '管理通讯通道',
324
+ handler: async (args) => {
325
+ const sub = args.trim()
326
+ if (sub === 'list' || sub === '') {
327
+ const channels = channelManager.list()
328
+ if (channels.length === 0) {
329
+ console.log('📭 没有配置通讯通道')
330
+ console.log(' 设置方法:')
331
+ console.log(' 1. 环境变量: CC_NODE_CHANNEL_TELEGRAM_TOKEN=xxx')
332
+ console.log(' 2. 配置文件: .claude-code/config.json -> channels')
333
+ } else {
334
+ console.log('📬 已配置通道:')
335
+ channels.forEach(ch => {
336
+ const isDefault = channelManager.defaultChannel === ch ? ' (默认)' : ''
337
+ console.log(` - ${ch}${isDefault}`)
338
+ })
339
+ }
340
+ } else if (sub.startsWith('send ')) {
341
+ const text = sub.slice(5)
342
+ const results = await channelManager.send(text)
343
+ results.forEach(r => {
344
+ console.log(r.ok ? `✅ ${r.channel}: 发送成功` : `❌ ${r.channel}: ${r.error}`)
345
+ })
346
+ } else if (sub.startsWith('test')) {
347
+ const results = await channelManager.send('📡 cc-node 通道测试消息')
348
+ results.forEach(r => {
349
+ console.log(r.ok ? `✅ ${r.channel}: 测试成功` : `❌ ${r.channel}: ${r.error}`)
350
+ })
351
+ } else {
352
+ console.log('用法:')
353
+ console.log(' /channel list — 列出通道')
354
+ console.log(' /channel send <msg> — 发送消息')
355
+ console.log(' /channel test — 测试通道')
356
+ }
357
+ }
358
+ })
359
+ }
360
+
361
+ export default ChannelManager
package/src/core/cli.js CHANGED
@@ -9,28 +9,30 @@ import { SessionManager } from './session.js'
9
9
  import { Config } from './config.js'
10
10
  import { TokenBudget } from './token-budget.js'
11
11
  import { PermissionChecker } from '../permission/permission.js'
12
+ import { ChannelManager } from '../channel/index.js'
12
13
 
13
14
  const BANNER = `
14
15
  ╔═══════════════════════════════════════════════╗
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
16
+ AI Code Agent — Node.js Edition
17
+ OpenAI-Compatible · DeepSeek Default
18
+ Type '/help' for commands
19
+ Type '/exit' or Ctrl+C to quit
19
20
  ╚═══════════════════════════════════════════════╝
20
21
  `.trim()
21
22
 
22
23
  const HELP_TEXT = `
23
24
  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
25
+ /help — Show this help
26
+ /model NAME — Switch model
27
+ /tools — List available tools
28
+ /session — Show session info
29
+ /sessions — List all sessions
30
+ /clear — Clear conversation
31
+ /config KEY — Show config value
32
+ /budget — Show token budget
33
+ /channel CMD Manage notification channels (list|send|test)
34
+ /exit Exit (also Ctrl+C)
35
+ /quit — Same as /exit
34
36
  `
35
37
 
36
38
  /**
@@ -52,41 +54,16 @@ function parseArgs(argv) {
52
54
  while (i < argv.length) {
53
55
  const arg = argv[i]
54
56
  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':
57
+ case '--model': case '-m': args.model = argv[++i]; break
58
+ case '--system-prompt': case '-s': args.systemPrompt = argv[++i]; break
59
+ case '--permission-mode': case '-p': args.permissionMode = argv[++i]; break
60
+ case '--max-turns': case '-t': args.maxTurns = parseInt(argv[++i], 10); break
61
+ case '--api-key': args.apiKey = argv[++i]; break
62
+ case '--api-base': args.apiBase = argv[++i]; break
63
+ case '--resume': case '-r': args.resume = argv[++i]; break
64
+ case '--verbose': case '-v': args.verbose = true; break
65
+ case '--no-stream': args.noStream = true; break
66
+ case '--help': case '-h':
90
67
  console.log(`Usage: cc-node [options]
91
68
 
92
69
  Options:
@@ -95,24 +72,33 @@ Options:
95
72
  -p, --permission-mode Permission mode: ask|always-allow|deny (default: ask)
96
73
  -t, --max-turns N Max tool loop turns (default: 100)
97
74
  --api-base URL API base URL (default: https://api.deepseek.com/v1)
98
- --api-key KEY API key (or set LLM_API_KEY env)
75
+ --api-key *** API key (or set LLM_API_KEY env)
99
76
  -r, --resume ID Resume a session
100
77
  -v, --verbose Verbose mode
101
78
  --no-stream Disable streaming
102
79
  -h, --help Show this help
103
80
 
104
81
  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)`)
82
+ LLM_API_KEY Universal API key (recommended)
83
+ DEEPSEEK_API_KEY DeepSeek API key (default)
84
+ OPENAI_API_KEY OpenAI API key
85
+ QWEN_API_KEY Qwen (DashScope) API key
86
+ GLM_API_KEY Zhipu GLM API key
87
+ KIMI_API_KEY Moonshot Kimi API key
88
+ LLM_API_BASE API base URL (default: https://api.deepseek.com/v1)
89
+
90
+ Channel environment variables:
91
+ CC_NODE_CHANNEL_DEFAULT Default channel name
92
+ CC_NODE_CHANNEL_TELEGRAM_TOKEN Telegram bot token
93
+ CC_NODE_CHANNEL_TELEGRAM_CHAT_ID Telegram chat ID
94
+ CC_NODE_CHANNEL_WECOM_WEBHOOK_URL WeCom webhook URL
95
+ CC_NODE_CHANNEL_FEISHU_WEBHOOK_URL Feishu webhook URL
96
+ CC_NODE_CHANNEL_DISCORD_WEBHOOK_URL Discord webhook URL
97
+ CC_NODE_CHANNEL_SLACK_WEBHOOK_URL Slack webhook URL
98
+ `)
112
99
  process.exit(0)
113
100
  default:
114
101
  if (!arg.startsWith('-')) {
115
- // 非选项参数视为一次性输入
116
102
  args.oneShot = argv.slice(i).join(' ')
117
103
  i = argv.length
118
104
  }
@@ -120,7 +106,6 @@ Environment variables:
120
106
  }
121
107
  i++
122
108
  }
123
-
124
109
  return args
125
110
  }
126
111
 
@@ -191,10 +176,23 @@ export async function main() {
191
176
  maxTokens: config.get('maxBudgetTokens') || 200_000,
192
177
  })
193
178
 
179
+ // 初始化通讯通道
180
+ const channelManager = new ChannelManager({
181
+ channels: config.get('channels') || {},
182
+ defaultChannel: config.get('defaultChannel') || null,
183
+ })
184
+
194
185
  // 一次性输入模式
195
186
  if (cliArgs.oneShot) {
196
187
  const result = await engine.processMessage(cliArgs.oneShot)
197
188
  console.log(result.response)
189
+ // 一次性模式结束后发通知
190
+ if (channelManager.list().length > 0) {
191
+ await channelManager.sendTemplate('task-done', {
192
+ task: cliArgs.oneShot.slice(0, 80),
193
+ result: result.response.slice(0, 200),
194
+ })
195
+ }
198
196
  process.exit(0)
199
197
  }
200
198
 
@@ -207,16 +205,17 @@ export async function main() {
207
205
 
208
206
  console.log(BANNER)
209
207
  console.log(`Model: ${model} | Permission: ${permissionMode} | Tools: ${registry.getNames().join(', ')}`)
208
+ if (channelManager.list().length > 0) {
209
+ const chList = channelManager.list().join(', ')
210
+ const def = channelManager.defaultChannel ? ` (default: ${channelManager.defaultChannel})` : ''
211
+ console.log(`Channels: ${chList}${def}`)
212
+ }
210
213
  console.log()
211
-
212
214
  rl.prompt()
213
215
 
214
216
  rl.on('line', async (line) => {
215
217
  const input = line.trim()
216
- if (!input) {
217
- rl.prompt()
218
- return
219
- }
218
+ if (!input) { rl.prompt(); return }
220
219
 
221
220
  // 命令处理
222
221
  if (input.startsWith('/')) {
@@ -273,6 +272,41 @@ export async function main() {
273
272
  case 'budget':
274
273
  console.log(tokenBudget.format())
275
274
  break
275
+ case 'channel': {
276
+ const subCmd = rest.join(' ')
277
+ if (subCmd === 'list' || subCmd === '') {
278
+ const channels = channelManager.list()
279
+ if (channels.length === 0) {
280
+ console.log('No channels configured')
281
+ console.log('Setup options:')
282
+ console.log(' 1. Environment: CC_NODE_CHANNEL_TELEGRAM_TOKEN=xxx CC_NODE_CHANNEL_TELEGRAM_CHAT_ID=xxx')
283
+ console.log(' 2. Config: .claude-code/config.json -> { "channels": { "telegram": { ... } } }')
284
+ } else {
285
+ console.log('Channels:')
286
+ for (const ch of channels) {
287
+ const isDefault = channelManager.defaultChannel === ch ? ' (default)' : ''
288
+ console.log(` - ${ch}${isDefault}`)
289
+ }
290
+ }
291
+ } else if (subCmd.startsWith('send ')) {
292
+ const text = subCmd.slice(5)
293
+ const results = await channelManager.send(text)
294
+ for (const r of results) {
295
+ console.log(r.ok ? `✅ ${r.channel}: sent` : `❌ ${r.channel}: ${r.error}`)
296
+ }
297
+ } else if (subCmd.startsWith('test')) {
298
+ const results = await channelManager.send('📡 cc-node channel test')
299
+ for (const r of results) {
300
+ console.log(r.ok ? `✅ ${r.channel}: test OK` : `❌ ${r.channel}: ${r.error}`)
301
+ }
302
+ } else {
303
+ console.log('Usage:')
304
+ console.log(' /channel list — List configured channels')
305
+ console.log(' /channel send <msg> — Send message to channels')
306
+ console.log(' /channel test — Test channel connectivity')
307
+ }
308
+ break
309
+ }
276
310
  case 'exit':
277
311
  case 'quit':
278
312
  console.log('Goodbye!')
@@ -302,8 +336,14 @@ export async function main() {
302
336
  }
303
337
  } catch (err) {
304
338
  console.error(`\nError: ${err.message}\n`)
339
+ // 错误也通知
340
+ if (channelManager.list().length > 0) {
341
+ await channelManager.sendTemplate('error', {
342
+ task: input.slice(0, 80),
343
+ error: err.message.slice(0, 200),
344
+ }).catch(() => {}) // 通知失败不影响主流程
345
+ }
305
346
  }
306
-
307
347
  rl.prompt()
308
348
  })
309
349
 
@@ -27,6 +27,8 @@ const DEFAULTS = {
27
27
  fileRead: { maxLines: 2000, maxSizeKB: 256 },
28
28
  webFetch: { timeout: 30, maxChars: 100000 },
29
29
  },
30
+ channels: {},
31
+ defaultChannel: null,
30
32
  mcp: {
31
33
  servers: {},
32
34
  },