@raolin2025/claude-code-node 1.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raolin2025/claude-code-node",
3
- "version": "1.2.0",
3
+ "version": "2.0.0",
4
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",
@@ -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
- * - 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' })
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 url = `https://api.telegram.org/bot${this.token}/sendMessage`
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 fetch(url, {
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: options.msgType || 'text',
65
+ const body = JSON.stringify({
66
+ msgtype: 'text',
67
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, {
68
+ })
69
+ const r = await fetch(this.webhookUrl, {
75
70
  method: 'POST',
76
71
  headers: { 'Content-Type': 'application/json' },
77
- body: JSON.stringify(body),
78
- }).then(r => r.json())
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: options.msgType || 'text',
93
- content: options.parseMode === 'markdown'
94
- ? JSON.stringify({ text: text })
95
- : JSON.stringify({ text: text }),
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: JSON.stringify(body),
101
- }).then(r => r.json())
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
- content: text,
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: JSON.stringify(body),
122
- }).then(r => r.json())
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
- text,
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: JSON.stringify(body),
144
- }).then(r => r.text())
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
- }).then(r => r.text())
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 || text}`
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