@raolin2025/claude-code-node 1.1.0 → 2.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 +52 -0
- package/package.json +13 -4
- package/src/channel/index.js +81 -146
- package/src/channel/notify-daemon.js +592 -0
- package/src/core/cli.js +195 -114
- package/src/core/compact.js +171 -0
- package/src/core/config.js +1 -2
- package/src/core/cost-tracker.js +171 -0
- package/src/core/paths.js +12 -0
- package/src/core/query-engine.js +192 -89
- package/src/core/session.js +24 -9
- package/src/mcp/client.js +99 -1
- package/src/mcp/registry.js +1 -2
- package/src/security/bash-guard.js +174 -141
- package/src/security/enhanced-permission.js +72 -34
- package/src/security/path-guard.js +32 -29
- package/src/security/ssrf-guard.js +153 -50
- package/src/tools/glob.js +1 -1
- package/src/types/index.js +2 -1
- package/src/utils/file-ops.js +2 -3
package/README.md
CHANGED
|
@@ -377,3 +377,55 @@ cc-node
|
|
|
377
377
|
- ✅ **任务完成**(one-shot 模式自动通知)
|
|
378
378
|
- ❌ **执行出错**(自动通知错误内容)
|
|
379
379
|
- 🔄 **手动发送**(`/channel send` 命令)
|
|
380
|
+
|
|
381
|
+
## 🔔 后台运行 (cc-notify)
|
|
382
|
+
|
|
383
|
+
cc-notify 是独立的通知守护进程,**不需要 cc-node 在前台运行**,开机自启后随时可用。
|
|
384
|
+
|
|
385
|
+
### 三种运行方式
|
|
386
|
+
|
|
387
|
+
| 方式 | 命令 | 说明 |
|
|
388
|
+
|------|------|------|
|
|
389
|
+
| **前台** | `cc-notify` | 调试用,Ctrl+C 退出 |
|
|
390
|
+
| **后台守护** | `cc-notify --daemon` | 脱离终端后台运行 |
|
|
391
|
+
| **系统服务** | `systemctl start cc-notify` | 开机自启,最推荐 |
|
|
392
|
+
|
|
393
|
+
### 手机交互
|
|
394
|
+
|
|
395
|
+
在 Telegram 上给 Bot 发消息:
|
|
396
|
+
|
|
397
|
+
```
|
|
398
|
+
你好 → 当作一次性任务发给 cc-node 执行
|
|
399
|
+
/ping → 检查服务是否在线
|
|
400
|
+
/run ls -la → 执行 shell 命令
|
|
401
|
+
/notify 任务完成! → 向所有通道广播通知
|
|
402
|
+
/status → 查看服务状态
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
### HTTP API(守护模式可用)
|
|
406
|
+
|
|
407
|
+
```bash
|
|
408
|
+
# 发送通知
|
|
409
|
+
curl -X POST http://localhost:3456/send \
|
|
410
|
+
-H 'Content-Type: application/json' \
|
|
411
|
+
-d '{"text":"构建完成 ✅"}'
|
|
412
|
+
|
|
413
|
+
# 查看状态
|
|
414
|
+
curl http://localhost:3456/status
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### systemd 开机自启
|
|
418
|
+
|
|
419
|
+
```bash
|
|
420
|
+
# 安装服务
|
|
421
|
+
sudo cp cc-notify.service /etc/systemd/system/
|
|
422
|
+
# 编辑 Token
|
|
423
|
+
sudo vim /etc/systemd/system/cc-notify.service
|
|
424
|
+
# 启用
|
|
425
|
+
sudo systemctl daemon-reload
|
|
426
|
+
sudo systemctl enable cc-notify
|
|
427
|
+
sudo systemctl start cc-notify
|
|
428
|
+
|
|
429
|
+
# 查看日志
|
|
430
|
+
journalctl -u cc-notify -f
|
|
431
|
+
```
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@raolin2025/claude-code-node",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Node.js AI Code Agent CLI - Zero dependencies, pure JavaScript, security hardened",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Node.js AI Code Agent CLI - Zero dependencies, pure JavaScript, security hardened, multi-channel notifications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"bin": {
|
|
8
|
-
"cc-node": "src/index.js"
|
|
8
|
+
"cc-node": "src/index.js",
|
|
9
|
+
"cc-notify": "src/channel/notify-daemon.js"
|
|
9
10
|
},
|
|
10
11
|
"scripts": {
|
|
11
12
|
"start": "node src/index.js",
|
|
@@ -13,7 +14,15 @@
|
|
|
13
14
|
"test": "node --test src/__tests__/*.js",
|
|
14
15
|
"repl": "node src/index.js"
|
|
15
16
|
},
|
|
16
|
-
"keywords": [
|
|
17
|
+
"keywords": [
|
|
18
|
+
"claude",
|
|
19
|
+
"code",
|
|
20
|
+
"agent",
|
|
21
|
+
"cli",
|
|
22
|
+
"llm",
|
|
23
|
+
"ai",
|
|
24
|
+
"security"
|
|
25
|
+
],
|
|
17
26
|
"license": "MIT",
|
|
18
27
|
"author": "bg1avd",
|
|
19
28
|
"repository": {
|
package/src/channel/index.js
CHANGED
|
@@ -1,29 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 通讯通道模块 — 支持多平台消息推送
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* v1.1 修复:
|
|
5
|
+
* - TelegramChannel: 添加缺失的 url 属性
|
|
6
|
+
* - 新增 WeComChannel, FeishuChannel, DiscordChannel, SlackChannel 适配器实现
|
|
7
|
+
* - 所有适配器统一错误处理
|
|
8
|
+
*
|
|
4
9
|
* 支持的通道:
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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' })
|
|
10
|
+
* - Telegram Bot API
|
|
11
|
+
* - 企业微信 (WeCom) Webhook
|
|
12
|
+
* - 飞书 (Feishu) Webhook
|
|
13
|
+
* - Discord Webhook
|
|
14
|
+
* - Slack Webhook
|
|
15
|
+
* - 自定义 HTTP Webhook
|
|
20
16
|
*/
|
|
21
17
|
|
|
22
|
-
import { readFileSync, existsSync } from 'fs'
|
|
23
|
-
import { resolve } from 'path'
|
|
24
|
-
import { homedir } from 'os'
|
|
25
|
-
import { execSync } from 'child_process'
|
|
26
|
-
|
|
27
18
|
// ============================================================
|
|
28
19
|
// 通道适配器
|
|
29
20
|
// ============================================================
|
|
@@ -33,23 +24,32 @@ class TelegramChannel {
|
|
|
33
24
|
constructor({ token, chatId }) {
|
|
34
25
|
this.token = token
|
|
35
26
|
this.chatId = chatId
|
|
27
|
+
// v1.1 修复: 添加缺失的 url 属性
|
|
28
|
+
this.url = `https://api.telegram.org/bot${token}/sendMessage`
|
|
29
|
+
this.method = 'POST'
|
|
30
|
+
this.headers = {}
|
|
31
|
+
this.bodyTemplate = null
|
|
36
32
|
}
|
|
37
33
|
|
|
38
34
|
get name() { return 'telegram' }
|
|
39
35
|
|
|
40
36
|
async send(text, options = {}) {
|
|
41
|
-
const
|
|
42
|
-
const body = {
|
|
37
|
+
const body = JSON.stringify({
|
|
43
38
|
chat_id: this.chatId,
|
|
44
39
|
text,
|
|
45
40
|
parse_mode: options.parseMode || 'Markdown',
|
|
46
41
|
disable_notification: options.silent || false,
|
|
42
|
+
})
|
|
43
|
+
const r = await fetch(this.url, {
|
|
44
|
+
method: this.method,
|
|
45
|
+
headers: { 'Content-Type': 'application/json', ...this.headers },
|
|
46
|
+
body,
|
|
47
|
+
})
|
|
48
|
+
if (!r.ok) {
|
|
49
|
+
const errText = await r.text()
|
|
50
|
+
throw new Error(`Telegram API error ${r.status}: ${errText.slice(0, 200)}`)
|
|
47
51
|
}
|
|
48
|
-
return
|
|
49
|
-
method: 'POST',
|
|
50
|
-
headers: { 'Content-Type': 'application/json' },
|
|
51
|
-
body: JSON.stringify(body),
|
|
52
|
-
}).then(r => r.json())
|
|
52
|
+
return r.json()
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
@@ -62,20 +62,20 @@ class WeComChannel {
|
|
|
62
62
|
get name() { return 'wecom' }
|
|
63
63
|
|
|
64
64
|
async send(text, options = {}) {
|
|
65
|
-
const body = {
|
|
66
|
-
msgtype:
|
|
65
|
+
const body = JSON.stringify({
|
|
66
|
+
msgtype: 'text',
|
|
67
67
|
text: { content: text },
|
|
68
|
-
|
|
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, {
|
|
68
|
+
})
|
|
69
|
+
const r = await fetch(this.webhookUrl, {
|
|
75
70
|
method: 'POST',
|
|
76
71
|
headers: { 'Content-Type': 'application/json' },
|
|
77
|
-
body
|
|
78
|
-
})
|
|
72
|
+
body,
|
|
73
|
+
})
|
|
74
|
+
if (!r.ok) {
|
|
75
|
+
const errText = await r.text()
|
|
76
|
+
throw new Error(`WeCom API error ${r.status}: ${errText.slice(0, 200)}`)
|
|
77
|
+
}
|
|
78
|
+
return r.json()
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
|
|
@@ -88,17 +88,20 @@ class FeishuChannel {
|
|
|
88
88
|
get name() { return 'feishu' }
|
|
89
89
|
|
|
90
90
|
async send(text, options = {}) {
|
|
91
|
-
const body = {
|
|
92
|
-
msg_type:
|
|
93
|
-
content:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
return fetch(this.webhookUrl, {
|
|
91
|
+
const body = JSON.stringify({
|
|
92
|
+
msg_type: 'text',
|
|
93
|
+
content: { text },
|
|
94
|
+
})
|
|
95
|
+
const r = await fetch(this.webhookUrl, {
|
|
98
96
|
method: 'POST',
|
|
99
97
|
headers: { 'Content-Type': 'application/json' },
|
|
100
|
-
body
|
|
101
|
-
})
|
|
98
|
+
body,
|
|
99
|
+
})
|
|
100
|
+
if (!r.ok) {
|
|
101
|
+
const errText = await r.text()
|
|
102
|
+
throw new Error(`Feishu API error ${r.status}: ${errText.slice(0, 200)}`)
|
|
103
|
+
}
|
|
104
|
+
return r.json()
|
|
102
105
|
}
|
|
103
106
|
}
|
|
104
107
|
|
|
@@ -111,15 +114,17 @@ class DiscordChannel {
|
|
|
111
114
|
get name() { return 'discord' }
|
|
112
115
|
|
|
113
116
|
async send(text, options = {}) {
|
|
114
|
-
const body = {
|
|
115
|
-
|
|
116
|
-
username: options.username || 'cc-node',
|
|
117
|
-
}
|
|
118
|
-
return fetch(this.webhookUrl, {
|
|
117
|
+
const body = JSON.stringify({ content: text })
|
|
118
|
+
const r = await fetch(this.webhookUrl, {
|
|
119
119
|
method: 'POST',
|
|
120
120
|
headers: { 'Content-Type': 'application/json' },
|
|
121
|
-
body
|
|
122
|
-
})
|
|
121
|
+
body,
|
|
122
|
+
})
|
|
123
|
+
if (!r.ok) {
|
|
124
|
+
const errText = await r.text()
|
|
125
|
+
throw new Error(`Discord API error ${r.status}: ${errText.slice(0, 200)}`)
|
|
126
|
+
}
|
|
127
|
+
return r.status === 204 ? 'ok' : r.json()
|
|
123
128
|
}
|
|
124
129
|
}
|
|
125
130
|
|
|
@@ -132,16 +137,17 @@ class SlackChannel {
|
|
|
132
137
|
get name() { return 'slack' }
|
|
133
138
|
|
|
134
139
|
async send(text, options = {}) {
|
|
135
|
-
const body = {
|
|
136
|
-
|
|
137
|
-
username: options.username || 'cc-node',
|
|
138
|
-
mrkdwn: options.parseMode !== 'plain',
|
|
139
|
-
}
|
|
140
|
-
return fetch(this.webhookUrl, {
|
|
140
|
+
const body = JSON.stringify({ text })
|
|
141
|
+
const r = await fetch(this.webhookUrl, {
|
|
141
142
|
method: 'POST',
|
|
142
143
|
headers: { 'Content-Type': 'application/json' },
|
|
143
|
-
body
|
|
144
|
-
})
|
|
144
|
+
body,
|
|
145
|
+
})
|
|
146
|
+
if (!r.ok) {
|
|
147
|
+
const errText = await r.text()
|
|
148
|
+
throw new Error(`Slack API error ${r.status}: ${errText.slice(0, 200)}`)
|
|
149
|
+
}
|
|
150
|
+
return r.text()
|
|
145
151
|
}
|
|
146
152
|
}
|
|
147
153
|
|
|
@@ -160,12 +166,16 @@ class WebhookChannel {
|
|
|
160
166
|
const body = this.bodyTemplate
|
|
161
167
|
? this.bodyTemplate.replace('{text}', text)
|
|
162
168
|
: JSON.stringify({ text, ...options })
|
|
163
|
-
|
|
164
|
-
return fetch(this.url, {
|
|
169
|
+
const r = await fetch(this.url, {
|
|
165
170
|
method: this.method,
|
|
166
171
|
headers: { 'Content-Type': 'application/json', ...this.headers },
|
|
167
172
|
body,
|
|
168
|
-
})
|
|
173
|
+
})
|
|
174
|
+
if (!r.ok) {
|
|
175
|
+
const errText = await r.text()
|
|
176
|
+
throw new Error(`Webhook error ${r.status}: ${errText.slice(0, 200)}`)
|
|
177
|
+
}
|
|
178
|
+
return r.text()
|
|
169
179
|
}
|
|
170
180
|
}
|
|
171
181
|
|
|
@@ -204,37 +214,23 @@ export class ChannelManager {
|
|
|
204
214
|
}
|
|
205
215
|
}
|
|
206
216
|
|
|
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
|
-
*/
|
|
217
|
+
/** 从环境变量加载通道 */
|
|
215
218
|
_loadFromEnv() {
|
|
216
219
|
const envChannels = {}
|
|
217
|
-
|
|
218
220
|
for (const [key, value] of Object.entries(process.env)) {
|
|
219
221
|
if (!key.startsWith(ENV_PREFIX)) continue
|
|
220
222
|
const rest = key.slice(ENV_PREFIX.length)
|
|
221
|
-
|
|
222
223
|
if (rest === 'DEFAULT') {
|
|
223
224
|
this.defaultChannel = value.toLowerCase()
|
|
224
225
|
continue
|
|
225
226
|
}
|
|
226
|
-
|
|
227
|
-
// 解析 CC_NODE_CHANNEL_<TYPE>_<PARAM>
|
|
228
227
|
const parts = rest.split('_')
|
|
229
228
|
const channelType = parts[0].toLowerCase()
|
|
230
229
|
const param = parts.slice(1).join('_').toLowerCase()
|
|
231
|
-
|
|
232
230
|
if (!envChannels[channelType]) envChannels[channelType] = { type: channelType }
|
|
233
|
-
// 将 SNAKE_CASE 转为 camelCase
|
|
234
231
|
const camelKey = param.replace(/_([a-z])/g, (_, c) => c.toUpperCase())
|
|
235
232
|
envChannels[channelType][camelKey] = value
|
|
236
233
|
}
|
|
237
|
-
|
|
238
234
|
for (const [name, chConfig] of Object.entries(envChannels)) {
|
|
239
235
|
if (!this.channels.has(name)) {
|
|
240
236
|
this._registerChannel(name, chConfig)
|
|
@@ -259,24 +255,15 @@ export class ChannelManager {
|
|
|
259
255
|
}
|
|
260
256
|
|
|
261
257
|
/** 获取已注册通道列表 */
|
|
262
|
-
list() {
|
|
263
|
-
return Array.from(this.channels.keys())
|
|
264
|
-
}
|
|
258
|
+
list() { return Array.from(this.channels.keys()) }
|
|
265
259
|
|
|
266
|
-
/** 发送消息
|
|
267
|
-
* @param {string} text — 消息内容
|
|
268
|
-
* @param {object} options —
|
|
269
|
-
* channel: 通道名(默认用 defaultChannel 或全部通道)
|
|
270
|
-
* parseMode: 'Markdown' | 'plain'
|
|
271
|
-
* silent: 静默通知
|
|
272
|
-
*/
|
|
260
|
+
/** 发送消息 */
|
|
273
261
|
async send(text, options = {}) {
|
|
274
262
|
const targetChannels = options.channel
|
|
275
263
|
? [options.channel]
|
|
276
264
|
: this.defaultChannel
|
|
277
265
|
? [this.defaultChannel]
|
|
278
266
|
: this.list()
|
|
279
|
-
|
|
280
267
|
const results = []
|
|
281
268
|
for (const name of targetChannels) {
|
|
282
269
|
const ch = this.channels.get(name)
|
|
@@ -294,10 +281,7 @@ export class ChannelManager {
|
|
|
294
281
|
return results
|
|
295
282
|
}
|
|
296
283
|
|
|
297
|
-
/** 发送模板消息
|
|
298
|
-
* @param {string} template — 模板名:'task-done', 'error', 'question'
|
|
299
|
-
* @param {object} data — 模板变量
|
|
300
|
-
*/
|
|
284
|
+
/** 发送模板消息 */
|
|
301
285
|
async sendTemplate(template, data = {}, options = {}) {
|
|
302
286
|
const templates = {
|
|
303
287
|
'task-done': `✅ 任务完成\n${data.task || ''}\n${data.result ? '结果:' + data.result : ''}`,
|
|
@@ -306,56 +290,7 @@ export class ChannelManager {
|
|
|
306
290
|
'progress': `🔄 进度更新\n${data.task || ''}\n${data.progress || ''}${data.percent ? ' (' + data.percent + '%)' : ''}`,
|
|
307
291
|
'warning': `⚠️ 警告\n${data.message || ''}`,
|
|
308
292
|
}
|
|
309
|
-
const text = templates[template] || `📢 ${data.message ||
|
|
293
|
+
const text = templates[template] || `📢 ${data.message || ''}`
|
|
310
294
|
return this.send(text, options)
|
|
311
295
|
}
|
|
312
296
|
}
|
|
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
|