@raolin2025/claude-code-node 2.3.5 → 2.4.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.
@@ -1,61 +1,59 @@
1
- import { ChannelManager } from "./index.js";
2
1
  /**
3
- * cc-notify — 通知守护进程(C 方案:智能路由)
2
+ * cc-notify — 通知守护进程(v2.0 增强版)
4
3
  *
5
- * 核心逻辑:
6
- * 手机发消息 → cc-notify 收到
7
- * → 检查 cc-node 是否在运行
8
- * → 在运行:转发消息给已运行的 cc-node(通过 Unix socket)
9
- * → 没在运行:spawn 一个新的 cc-node 执行,完成后退出
4
+ * 架构:
5
+ * Telegram Bot API → cc-notify (长轮询) → cc-node
6
+ * QQ ← OpenClaw QQ Bot cc-notify (HTTP API) → cc-node
7
+ * HTTP API cc-notify cc-node
8
+ *
9
+ * 支持:
10
+ * - Telegram Bot 长轮询监听(增强版)
11
+ * - QQ Bot 消息(通过 OpenClaw 转发到 HTTP API /chat 端点)
12
+ * - HTTP API(带 API Key 认证)
13
+ * - C 方案智能路由(运行中 → socket 转发,未运行 → 启动新进程)
14
+ * - 多通道消息推送
15
+ * - API Key 自动生成 + 持久化
10
16
  *
11
17
  * 用法:
12
- * cc-notify # 前台运行
13
- * cc-notify --daemon # 后台守护进程
14
- * cc-notify --stop # 停止守护进程
15
- * cc-notify --status # 查看状态
18
+ * cc-notify # 前台运行
19
+ * cc-notify --daemon # 后台守护进程
20
+ * cc-notify --stop # 停止守护进程
21
+ * cc-notify --status # 查看状态
16
22
  */
17
- import { createServer } from "http";
18
- import {
19
- readFileSync,
20
- writeFileSync,
21
- unlinkSync,
22
- existsSync,
23
- appendFileSync,
24
- mkdirSync,
25
- openSync,
26
- closeSync,
27
- } from "fs";
28
- import { resolve, join } from "path";
29
- import { homedir } from "os";
30
- import { spawn } from "child_process";
31
- import { createConnection } from "net";
32
- import crypto from "crypto";
23
+ import { createServer } from 'node:http'
33
24
  import {
34
- SOCK_DIR,
35
- SOCK_PATH,
36
- CC_NODE_PID,
37
- CC_NOTIFY_PID,
38
- CC_NOTIFY_LOG,
39
- DEFAULT_HTTP_PORT,
40
- } from "../core/paths.js";
25
+ readFileSync, writeFileSync, unlinkSync, existsSync, appendFileSync,
26
+ mkdirSync, openSync, closeSync,
27
+ } from 'node:fs'
28
+ import { join } from 'node:path'
29
+ import { homedir } from 'node:os'
30
+ import { spawn, execSync } from 'node:child_process'
31
+ import { createConnection } from 'node:net'
32
+ import crypto from 'node:crypto'
33
+ import { SOCK_DIR, SOCK_PATH, CC_NODE_PID, CC_NOTIFY_PID, CC_NOTIFY_LOG, DEFAULT_HTTP_PORT } from '../core/paths.js'
34
+ import { ChannelManager } from './index.js'
41
35
 
42
36
  // ============================================================
43
37
  // 配置加载
44
38
  // ============================================================
39
+
45
40
  function loadConfig() {
46
41
  // 生成或加载 API Key
47
- let apiKey = process.env.CC_NOTIFY_API_KEY || "";
42
+ let apiKey = process.env.CC_NOTIFY_API_KEY || ''
48
43
  if (!apiKey) {
49
- // 自动生成并保存 API Key
50
- apiKey = crypto.randomBytes(32).toString("hex");
51
- const configDir = join(process.cwd(), ".claude-code");
52
- const configPath = join(configDir, "notify-api-key.txt");
44
+ const configDir = join(process.cwd(), '.claude-code')
45
+ const configPath = join(configDir, 'notify-api-key.txt')
53
46
  try {
54
- mkdirSync(configDir, { recursive: true });
55
- writeFileSync(configPath, apiKey, "utf8");
56
- console.log(`[notify] Generated API Key: ${apiKey} (saved to ${configPath})`);
47
+ if (existsSync(configPath)) {
48
+ apiKey = readFileSync(configPath, 'utf8').trim()
49
+ } else {
50
+ apiKey = crypto.randomBytes(32).toString('hex')
51
+ mkdirSync(configDir, { recursive: true })
52
+ writeFileSync(configPath, apiKey, 'utf8')
53
+ log(`[config] Generated API Key: ${apiKey.slice(0, 8)}... (saved to ${configPath})`)
54
+ }
57
55
  } catch (e) {
58
- console.warn("[notify] Failed to save API Key:", e.message);
56
+ log(`[config] API Key file error: ${e.message}`)
59
57
  }
60
58
  }
61
59
 
@@ -65,531 +63,753 @@ function loadConfig() {
65
63
  port: parseInt(process.env.CC_NOTIFY_PORT || String(DEFAULT_HTTP_PORT), 10),
66
64
  pidFile: process.env.CC_NOTIFY_CC_NODE_PID || CC_NOTIFY_PID,
67
65
  logFile: process.env.CC_NOTIFY_LOG_FILE || CC_NOTIFY_LOG,
68
- ccNodePath: process.env.CC_NODE_PATH || "cc-node",
66
+ ccNodePath: process.env.CC_NODE_PATH || 'cc-node',
69
67
  apiKey,
70
- };
71
68
 
69
+ }
70
+
71
+ // 从 .claude-code/config.json 加载
72
72
  for (const dir of [process.cwd(), homedir()]) {
73
- const cfgPath = join(dir, ".claude-code", "config.json");
73
+ const cfgPath = join(dir, '.claude-code', 'config.json')
74
74
  if (existsSync(cfgPath)) {
75
75
  try {
76
- const data = JSON.parse(readFileSync(cfgPath, "utf8"));
77
- if (data.channels) Object.assign(config.channels, data.channels);
78
- if (data.defaultChannel && !config.defaultChannel)
79
- config.defaultChannel = data.defaultChannel;
80
- if (data.notify?.port) config.port = data.notify.port;
81
- if (data.notify?.ccNodePath) config.ccNodePath = data.notify.ccNodePath;
76
+ const data = JSON.parse(readFileSync(cfgPath, 'utf8'))
77
+ if (data.channels) Object.assign(config.channels, data.channels)
78
+ if (data.defaultChannel && !config.defaultChannel) config.defaultChannel = data.defaultChannel
79
+ if (data.notify?.port) config.port = data.notify.port
80
+ if (data.notify?.ccNodePath) config.ccNodePath = data.notify.ccNodePath
81
+ if (data.notify?.apiKey) config.apiKey = data.notify.apiKey
82
+ // (QQ Bot config handled via channels.qqbot)
82
83
  } catch {}
83
84
  }
84
85
  }
85
86
 
87
+ // 从环境变量加载通道配置
86
88
  for (const [key, value] of Object.entries(process.env)) {
87
- if (!key.startsWith("CC_NODE_CHANNEL_")) continue;
88
- const rest = key.slice("CC_NODE_CHANNEL_".length);
89
- if (rest === "DEFAULT") continue;
90
- const parts = rest.split("_");
91
- const type = parts[0].toLowerCase();
92
- const param = parts.slice(1).join("_").toLowerCase();
93
- if (!config.channels[type]) config.channels[type] = { type };
94
- const camelKey = param.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
95
- config.channels[type][camelKey] = value;
89
+ if (!key.startsWith('CC_NODE_CHANNEL_')) continue
90
+ const rest = key.slice('CC_NODE_CHANNEL_'.length)
91
+ if (rest === 'DEFAULT') continue
92
+ const parts = rest.split('_')
93
+ const type = parts[0].toLowerCase()
94
+ const param = parts.slice(1).join('_').toLowerCase()
95
+ if (!config.channels[type]) config.channels[type] = { type }
96
+ const camelKey = param.replace(/_([a-z])/g, (_, c) => c.toUpperCase())
97
+ config.channels[type][camelKey] = value
96
98
  }
97
99
 
98
- return config;
100
+ log(`[config] Channels: ${Object.keys(config.channels).join(', ') || 'none'}`)
101
+ log(`[config] Default channel: ${config.defaultChannel || 'none'}`)
102
+ log(`[config] QQ Bot appId: ${config.channels?.qqbot?.appId || process.env.CC_NODE_CHANNEL_QQBOT_APPID ? 'configured' : 'not set'}`)
103
+ return config
99
104
  }
100
105
 
101
106
  // ============================================================
102
107
  // 通道发送
103
108
  // ============================================================
109
+
104
110
  async function sendToChannel(channels, defaultChannel, text) {
105
- const cm = new ChannelManager({ channels, defaultChannel });
106
- return await cm.send(text);
111
+ const cm = new ChannelManager({ channels }, defaultChannel)
112
+ return await cm.send(text)
107
113
  }
108
114
 
109
115
  // ============================================================
110
116
  // 进程发现 — cc-node 是否在跑?
111
117
  // ============================================================
118
+
112
119
  function findCcNode() {
113
120
  if (existsSync(SOCK_PATH)) {
114
121
  return new Promise((resolve) => {
115
122
  const client = createConnection(SOCK_PATH, () => {
116
- client.end();
117
- resolve({ running: true, socketPath: SOCK_PATH });
118
- });
119
- client.on("error", () => {
120
- try {
121
- unlinkSync(SOCK_PATH);
122
- } catch {}
123
- resolve({ running: false });
124
- });
123
+ client.end()
124
+ resolve({ running: true, socketPath: SOCK_PATH })
125
+ })
126
+ client.on('error', () => {
127
+ try { unlinkSync(SOCK_PATH) } catch {}
128
+ resolve({ running: false })
129
+ })
125
130
  setTimeout(() => {
126
- client.destroy();
127
- resolve({ running: false });
128
- }, 2000);
129
- });
131
+ client.destroy()
132
+ resolve({ running: false })
133
+ }, 2000)
134
+ })
130
135
  }
131
136
  if (existsSync(CC_NODE_PID)) {
132
- const pid = parseInt(readFileSync(CC_NODE_PID, "utf8").trim(), 10);
137
+ const pid = parseInt(readFileSync(CC_NODE_PID, 'utf8').trim(), 10)
133
138
  try {
134
- process.kill(pid, 0);
135
- return { running: true, pid };
139
+ process.kill(pid, 0)
140
+ return { running: true, pid }
136
141
  } catch {
137
- try {
138
- unlinkSync(CC_NODE_PID);
139
- } catch {}
142
+ try { unlinkSync(CC_NODE_PID) } catch {}
140
143
  }
141
144
  }
142
- return { running: false };
145
+ return { running: false }
143
146
  }
144
147
 
145
148
  // ============================================================
146
149
  // 消息路由 — C 方案核心
147
150
  // ============================================================
151
+
148
152
  function sendToExistingNode(socketPath, text) {
149
153
  return new Promise((resolve, reject) => {
150
154
  const client = createConnection(socketPath, () => {
151
- const msg = JSON.stringify({ type: "user_input", text });
152
- client.write(msg + "\n");
153
- });
154
- let buffer = "";
155
- client.on("data", (data) => {
156
- buffer += data.toString();
157
- const lines = buffer.split("\n");
155
+ const msg = JSON.stringify({ type: 'user_input', text })
156
+ client.write(msg + '\n')
157
+ })
158
+ let buffer = ''
159
+ client.on('data', (data) => {
160
+ buffer += data.toString()
161
+ const lines = buffer.split('\n')
158
162
  if (lines.length > 1) {
159
163
  try {
160
- const response = JSON.parse(lines[0]);
161
- client.end();
162
- resolve(response);
164
+ const response = JSON.parse(lines[0])
165
+ client.end()
166
+ resolve(response)
163
167
  } catch {
164
- client.end();
165
- resolve({ type: "reply", text: buffer.trim() });
168
+ client.end()
169
+ resolve({ type: 'reply', text: buffer.trim() })
166
170
  }
167
171
  }
168
- });
169
- client.on("error", (err) => reject(err));
172
+ })
173
+ client.on('error', (err) => reject(err))
170
174
  setTimeout(() => {
171
- client.destroy();
172
- reject(new Error("timeout waiting for cc-node reply"));
173
- }, 60000);
174
- });
175
+ client.destroy()
176
+ reject(new Error('timeout waiting for cc-node reply'))
177
+ }, 120000)
178
+ })
175
179
  }
176
180
 
177
181
  function spawnNewNode(ccNodePath, text) {
178
182
  return new Promise((resolve) => {
179
- const timeout = 120000;
180
- const timer = setTimeout(() => {
181
- child.kill();
182
- resolve({ type: "reply", text: "⏰ 执行超时(2 分钟)" });
183
- }, timeout);
183
+ const timeout = 180000 // 3分钟超时
184
+ let timer = setTimeout(() => {
185
+ child.kill()
186
+ resolve({ type: 'reply', text: '⏰ 执行超时(3 分钟)' })
187
+ }, timeout)
184
188
  try {
185
189
  const child = spawn(ccNodePath, [text], {
186
- stdio: ["pipe", "pipe", "pipe"],
187
- });
188
- let stdout = "";
189
- let stderr = "";
190
- child.stdout.on("data", (d) => (stdout += d.toString()));
191
- child.stderr.on("data", (d) => (stderr += d.toString()));
192
- child.on("close", (code) => {
193
- clearTimeout(timer);
190
+ stdio: ['pipe', 'pipe', 'pipe'],
191
+ env: { ...process.env, CC_NODE_ONESHOT: '1' },
192
+ timeout,
193
+ })
194
+ let stdout = ''
195
+ let stderr = ''
196
+ child.stdout.on('data', (d) => (stdout += d.toString()))
197
+ child.stderr.on('data', (d) => (stderr += d.toString()))
198
+ child.on('close', (code) => {
199
+ clearTimeout(timer)
200
+ timer = null
194
201
  if (stdout.trim()) {
195
- resolve({ type: "reply", text: stdout.trim().slice(0, 4000) });
202
+ resolve({ type: 'reply', text: stdout.trim().slice(0, 4000) })
196
203
  } else if (stderr.trim()) {
197
- resolve({ type: "reply", text: `❌ Error: ${stderr.trim().slice(0, 1000)}` });
204
+ resolve({ type: 'reply', text: `❌ Error: ${stderr.trim().slice(0, 1000)}` })
198
205
  } else {
199
- resolve({ type: "reply", text: "(no output)" });
206
+ resolve({ type: 'reply', text: `(completed with code ${code})` })
200
207
  }
201
- });
208
+ })
209
+ child.on('error', (e) => {
210
+ clearTimeout(timer)
211
+ timer = null
212
+ resolve({ type: 'reply', text: `❌ Failed: ${e.message}` })
213
+ })
202
214
  } catch (e) {
203
- clearTimeout(timer);
204
- resolve({ type: "reply", text: `❌ Failed: ${e.message}` });
215
+ clearTimeout(timer)
216
+ timer = null
217
+ resolve({ type: 'reply', text: `❌ Failed: ${e.message}` })
205
218
  }
206
- });
219
+ })
207
220
  }
208
221
 
209
222
  async function routeMessage(text, config) {
210
- const nodeInfo = await findCcNode();
223
+ const nodeInfo = await findCcNode()
211
224
  if (nodeInfo.running && nodeInfo.socketPath) {
212
- log(`[route] cc-node running → forwarding via socket`);
225
+ log(`[route] cc-node running → forwarding via socket`)
213
226
  try {
214
- const reply = await sendToExistingNode(nodeInfo.socketPath, text);
215
- return reply.text || JSON.stringify(reply);
227
+ const reply = await sendToExistingNode(nodeInfo.socketPath, text)
228
+ return reply.text || JSON.stringify(reply)
216
229
  } catch (e) {
217
- log(`[route] socket forward failed: ${e.message} → spawning new`);
218
- return (await spawnNewNode(config.ccNodePath, text)).text;
230
+ log(`[route] socket forward failed: ${e.message} → spawning new`)
231
+ return (await spawnNewNode(config.ccNodePath, text)).text
219
232
  }
220
233
  } else if (nodeInfo.running && nodeInfo.pid) {
221
- log(`[route] cc-node running (PID ${nodeInfo.pid}) but no socket → spawning new (one-shot mode)`);
222
- return (await spawnNewNode(config.ccNodePath, text)).text;
234
+ log(`[route] cc-node running (PID ${nodeInfo.pid}) but no socket → spawning new (one-shot mode)`)
235
+ return (await spawnNewNode(config.ccNodePath, text)).text
223
236
  } else {
224
- log(`[route] cc-node not running → spawning new`);
225
- return (await spawnNewNode(config.ccNodePath, text)).text;
237
+ log(`[route] cc-node not running → spawning new`)
238
+ return (await spawnNewNode(config.ccNodePath, text)).text
226
239
  }
227
240
  }
228
241
 
229
242
  // ============================================================
230
- // Telegram Bot 长轮询
243
+ // Telegram 监听器(动态加载)
231
244
  // ============================================================
232
- class TelegramListener {
233
- constructor(config) {
234
- this.config = config;
235
- this.lastUpdateId = 0;
236
- this.running = false;
245
+
246
+ async function createTelegramListener(config) {
247
+ try {
248
+ const { TelegramListener } = await import('./tg-listener.js')
249
+ const listener = new TelegramListener(config)
250
+ return listener
251
+ } catch (e) {
252
+ log(`[TG] Failed to load tg-listener: ${e.message}`)
253
+ return null
237
254
  }
238
- async start(onMessage) {
239
- const ch = this.config.channels.telegram;
240
- if (!ch?.token) {
241
- log("Telegram: no token, skipping");
242
- return;
255
+ }
256
+
257
+ // ============================================================
258
+ // ============================================================
259
+
260
+
261
+
262
+ // ============================================================
263
+ // 统一消息处理器
264
+ // ============================================================
265
+
266
+ function createMessageHandler(config) {
267
+ return async (msg) => {
268
+ const { text, channel, chatId, from, replyTo, messageId } = msg
269
+
270
+ if (!text) return
271
+
272
+ log(`[msg] ${channel} ← ${from || '?'}: ${text.slice(0, 60)}`)
273
+
274
+ const isTelegram = channel === 'telegram' || channel === 'telegram_callback'
275
+ const isQQBot = channel === 'qqbot'
276
+
277
+ const lowerText = text.trim().toLowerCase()
278
+
279
+ // /ping
280
+ if (lowerText === '/ping' || lowerText === 'ping') {
281
+ const reply = '🏓 pong! cc-notify is alive.'
282
+ if (isTelegram) {
283
+ const { TelegramListener } = await import('./tg-listener.js')
284
+ const tl = new TelegramListener(config)
285
+ if (tl.bot) await tl.bot.sendMessage(chatId, reply, { replyTo })
286
+ }
287
+ if (isQQBot) {
288
+ await sendToChannel(config.channels, 'qqbot', reply)
289
+ }
290
+ return
291
+ }
292
+
293
+ // /status
294
+ if (lowerText === '/status' || lowerText === 'status') {
295
+ const nodeInfo = await findCcNode()
296
+ const chNames = Object.keys(config.channels || {})
297
+ const reply = [
298
+ '📊 cc-notify status',
299
+ `• Uptime: ${Math.floor(process.uptime())}s`,
300
+ `• Channels: ${chNames.join(', ') || 'none'}`,
301
+ `• cc-node: ${nodeInfo.running ? '✅ running' : '❌ not running'}`,
302
+ `• PID: ${process.pid}`,
303
+ ].join('\n')
304
+
305
+ if (isTelegram) {
306
+ const { TelegramListener } = await import('./tg-listener.js')
307
+ const tl = new TelegramListener(config)
308
+ if (tl.bot) await tl.bot.sendMessage(chatId, reply, { replyTo })
309
+ }
310
+ if (isQQBot) {
311
+ await sendToChannel(config.channels, 'qqbot', reply)
312
+ }
313
+ return
243
314
  }
244
- this.running = true;
245
- log("Telegram: started (long polling)");
246
- while (this.running) {
315
+
316
+ // /help
317
+ if (lowerText === '/help' || lowerText === 'help' || lowerText === '/start') {
318
+ const reply = [
319
+ '🤖 cc-notify — Remote AI Code Agent',
320
+ '',
321
+ 'Send any message → AI processes it as a programming task.',
322
+ '',
323
+ 'Commands:',
324
+ ' /ping — Check service status',
325
+ ' /status — View detailed status',
326
+ ' /run cmd — Execute shell command directly',
327
+ ' /notify — Broadcast notification to all channels',
328
+ ' /cancel — Cancel current operation',
329
+ ].join('\n')
330
+
331
+ if (isTelegram) {
332
+ const { TelegramListener } = await import('./tg-listener.js')
333
+ const tl = new TelegramListener(config)
334
+ if (tl.bot) await tl.bot.sendMessage(chatId, reply, { replyTo })
335
+ }
336
+ if (isQQBot) {
337
+ await sendToChannel(config.channels, 'qqbot', reply)
338
+ }
339
+ return
340
+ }
341
+
342
+ // /run — 直接执行命令
343
+ if (lowerText.startsWith('/run ') || lowerText.startsWith('run ')) {
344
+ const cmd = text.replace(/^\/(run|run)\s+/i, '').trim()
247
345
  try {
248
- const url = `https://api.telegram.org/bot${ch.token}/getUpdates`;
249
- const res = await fetch(url, {
250
- method: "POST",
251
- headers: { "Content-Type": "application/json" },
252
- body: JSON.stringify({ offset: this.lastUpdateId + 1, timeout: 30, allowed_updates: ["message"] }),
253
- });
254
- const data = await res.json();
255
- if (data.ok && data.result?.length) {
256
- for (const update of data.result) {
257
- this.lastUpdateId = update.update_id;
258
- if (update.message?.text) {
259
- const msg = {
260
- text: update.message.text,
261
- chatId: update.message.chat.id,
262
- from: update.message.from?.username || update.message.from?.first_name || "?",
263
- };
264
- log(`TG ← ${msg.from}: ${msg.text.slice(0, 60)}`);
265
- try {
266
- await onMessage(msg);
267
- } catch (e) {
268
- log(`handler error: ${e.message}`);
269
- }
270
- }
346
+ const output = execSync(cmd, { timeout: 30000, encoding: 'utf8', maxBuffer: 1024 * 1024 })
347
+ const reply = `💻 $ ${cmd}\n\n${output.trim().slice(0, 3500)}`
348
+
349
+ if (isTelegram) {
350
+ const { TelegramListener } = await import('./tg-listener.js')
351
+ const tl = new TelegramListener(config)
352
+ if (tl.bot) {
353
+ const escaped = '```\n' + output.trim().slice(0, 3500) + '\n```'
354
+ await tl.bot.sendMessage(chatId, `💻 $ ${cmd}\n${escaped}`, { replyTo })
271
355
  }
272
356
  }
357
+ if (isQQBot) {
358
+ await sendToChannel(config.channels, 'qqbot', reply.slice(0, 2000))
359
+ }
273
360
  } catch (e) {
274
- log(`TG poll error: ${e.message}`);
275
- await sleep(5000);
361
+ const reply = `❌ Command failed:\n${e.message}`
362
+ if (isTelegram) {
363
+ const { TelegramListener } = await import('./tg-listener.js')
364
+ const tl = new TelegramListener(config)
365
+ if (tl.bot) await tl.bot.sendMessage(chatId, reply, { replyTo })
366
+ }
367
+ if (isQQBot) {
368
+ await sendToChannel(config.channels, 'qqbot', reply)
369
+ }
370
+ }
371
+ return
372
+ }
373
+
374
+ // /notify — 广播
375
+ if (lowerText.startsWith('/notify ')) {
376
+ const notifyText = text.replace('/notify ', '')
377
+ try {
378
+ const results = await sendToChannel(config.channels, null, notifyText)
379
+ const reply = results.map(r => r.ok ? `✅ ${r.channel}` : `❌ ${r.channel}: ${r.error}`).join('\n')
380
+ if (isTelegram) {
381
+ const { TelegramListener } = await import('./tg-listener.js')
382
+ const tl = new TelegramListener(config)
383
+ if (tl.bot) await tl.bot.sendMessage(chatId, reply, { replyTo })
384
+ }
385
+ if (isQQBot) {
386
+ await sendToChannel(config.channels, 'qqbot', reply)
387
+ }
388
+ } catch (e) {
389
+ log(`[notify] broadcast error: ${e.message}`)
390
+ }
391
+ return
392
+ }
393
+
394
+ // /cancel
395
+ if (lowerText === '/cancel') {
396
+ const reply = '🚫 Cancelled.'
397
+ if (isTelegram) {
398
+ const { TelegramListener } = await import('./tg-listener.js')
399
+ const tl = new TelegramListener(config)
400
+ if (tl.bot) await tl.bot.sendMessage(chatId, reply, { replyTo })
401
+ }
402
+ if (isQQBot) {
403
+ await sendToChannel(config.channels, 'qqbot', reply)
404
+ }
405
+ return
406
+ }
407
+
408
+ // ============================================================
409
+ // 普通消息 → C 方案路由:发给 cc-node 处理
410
+ // ============================================================
411
+ log(`[route] processing: "${text.slice(0, 50)}${text.length > 50 ? '...' : ''}"`)
412
+
413
+ // 发送"处理中"提示
414
+ if (isTelegram && config.channels.telegram?.token) {
415
+ try {
416
+ await fetch(`https://api.telegram.org/bot${config.channels.telegram.token}/sendChatAction`, {
417
+ method: 'POST',
418
+ headers: { 'Content-Type': 'application/json' },
419
+ body: JSON.stringify({ chat_id: chatId, action: 'typing' }),
420
+ })
421
+ } catch {}
422
+ }
423
+ if (isQQBot) {
424
+ await sendToChannel(config.channels, 'qqbot', '🤖 收到,正在处理...')
425
+ }
426
+
427
+ try {
428
+ const result = await routeMessage(text, config)
429
+ const reply = result || '(no response)'
430
+
431
+ if (isTelegram && config.channels.telegram?.token) {
432
+ const { TelegramListener } = await import('./tg-listener.js')
433
+ const tl = new TelegramListener(config)
434
+ if (tl.bot) {
435
+ if (reply.length > 4000) {
436
+ const parts = []
437
+ let cur = ''
438
+ for (const line of reply.split('\n')) {
439
+ if (cur.length + line.length > 3800) { parts.push(cur); cur = line }
440
+ else { cur += (cur ? '\n' : '') + line }
441
+ }
442
+ if (cur) parts.push(cur)
443
+ for (let i = 0; i < parts.length; i++) {
444
+ const header = i > 0 ? `📎 (${i + 1}/${parts.length})\n` : ''
445
+ await tl.bot.sendMessage(chatId, header + parts[i], { replyTo: i === 0 ? replyTo : undefined })
446
+ await new Promise(r => setTimeout(r, 300))
447
+ }
448
+ } else {
449
+ await tl.bot.sendMessage(chatId, reply, { replyTo })
450
+ }
451
+ }
452
+ }
453
+
454
+ if (isQQBot) {
455
+ await sendToChannel(config.channels, 'qqbot', reply.slice(0, 2000))
456
+ }
457
+
458
+ log(`[route] done (${reply.length} chars)`)
459
+
460
+ } catch (e) {
461
+ log(`[route] error: ${e.message}`)
462
+ const errMsg = `❌ Error processing: ${e.message}`
463
+ if (isTelegram && config.channels.telegram?.token) {
464
+ try {
465
+ const { TelegramListener } = await import('./tg-listener.js')
466
+ const tl = new TelegramListener(config)
467
+ if (tl.bot) await tl.bot.sendMessage(chatId, errMsg, { replyTo })
468
+ } catch {}
469
+ }
470
+ if (isQQBot) {
471
+ try { await sendToChannel(config.channels, 'qqbot', errMsg) } catch {}
276
472
  }
277
473
  }
278
- }
279
- stop() {
280
- this.running = false;
281
474
  }
282
475
  }
283
476
 
284
477
  // ============================================================
285
478
  // HTTP API — 带 API Key 认证
286
479
  // ============================================================
480
+
287
481
  class HttpServer {
288
- constructor(config, channels) {
289
- this.config = config;
290
- this.channels = channels;
291
- this.server = null;
482
+ constructor(config) {
483
+ this.config = config
484
+ this.server = null
292
485
  }
293
486
 
294
- /** 验证 API Key */
295
487
  _validateApiKey(req) {
296
- const authHeader = req.headers["x-api-key"] || "";
297
- const url = new URL(req.url, `http://localhost`);
298
- const queryKey = url.searchParams.get("api_key") || "";
299
- const providedKey = authHeader || queryKey;
488
+ const authHeader = req.headers['x-api-key'] || ''
489
+ const url = new URL(req.url, `http://localhost`)
490
+ const queryKey = url.searchParams.get('api_key') || ''
491
+ const providedKey = authHeader || queryKey
300
492
 
301
493
  if (!providedKey) {
302
- return { valid: false, error: "API Key required. Use X-API-Key header or ?api_key=xxx" };
494
+ return { valid: false, error: 'API Key required. Use X-API-Key header or ?api_key=xxx' }
303
495
  }
304
496
  if (providedKey !== this.config.apiKey) {
305
- return { valid: false, error: "Invalid API Key" };
497
+ return { valid: false, error: 'Invalid API Key' }
306
498
  }
307
- return { valid: true };
499
+ return { valid: true }
308
500
  }
309
501
 
310
502
  start(onMessage) {
311
503
  this.server = createServer(async (req, res) => {
312
- const url = new URL(req.url, `http://localhost:${this.config.port}`);
313
- res.setHeader("Access-Control-Allow-Origin", "*");
314
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
315
- res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-API-Key");
316
-
317
- if (req.method === "OPTIONS") {
318
- res.writeHead(204);
319
- res.end();
320
- return;
504
+ const url = new URL(req.url, `http://localhost:${this.config.port}`)
505
+ res.setHeader('Access-Control-Allow-Origin', '*')
506
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
507
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-API-Key')
508
+
509
+ if (req.method === 'OPTIONS') {
510
+ res.writeHead(204)
511
+ res.end()
512
+ return
321
513
  }
322
514
 
323
- // API Key 认证(/status 端点不需要认证)
324
- if (req.method !== "GET" || url.pathname !== "/status") {
325
- const authResult = this._validateApiKey(req);
515
+ // /status 端点无需认证
516
+ const needsAuth = !(req.method === 'GET' && url.pathname === '/status')
517
+ if (needsAuth) {
518
+ const authResult = this._validateApiKey(req)
326
519
  if (!authResult.valid) {
327
- res.writeHead(401, { "Content-Type": "application/json" });
328
- res.end(JSON.stringify({ error: authResult.error }));
329
- return;
520
+ res.writeHead(401, { 'Content-Type': 'application/json' })
521
+ res.end(JSON.stringify({ error: authResult.error }))
522
+ return
330
523
  }
331
524
  }
332
525
 
333
526
  try {
334
- if (req.method === "GET" && url.pathname === "/status") {
335
- const nodeInfo = await findCcNode();
336
- res.writeHead(200, { "Content-Type": "application/json" });
337
- res.end(
338
- JSON.stringify({
339
- status: "running",
340
- channels: Object.keys(this.channels),
341
- defaultChannel: this.config.defaultChannel,
342
- uptime: Math.floor(process.uptime()),
343
- ccNodeRunning: nodeInfo.running,
344
- }),
345
- );
346
- } else if (req.method === "POST" && url.pathname === "/send") {
347
- const body = JSON.parse(await readBody(req));
348
- const { text, channel } = body;
527
+ if (req.method === 'GET' && url.pathname === '/status') {
528
+ const nodeInfo = await findCcNode()
529
+ res.writeHead(200, { 'Content-Type': 'application/json' })
530
+ res.end(JSON.stringify({
531
+ status: 'running',
532
+ version: '2.0',
533
+ channels: Object.keys(this.config.channels),
534
+ defaultChannel: this.config.defaultChannel,
535
+ uptime: Math.floor(process.uptime()),
536
+ ccNodeRunning: nodeInfo.running,
537
+ pid: process.pid,
538
+ apiKeyPrefix: this.config.apiKey.slice(0, 8) + '...',
539
+ qqbot: this.config.channels?.qqbot?.appId ? 'configured' : 'not set',
540
+ }))
541
+
542
+ } else if (req.method === 'POST' && url.pathname === '/send') {
543
+ const body = JSON.parse(await readBody(req))
544
+ const { text, channel } = body
349
545
  if (!text) {
350
- res.writeHead(400, { "Content-Type": "application/json" });
351
- res.end(JSON.stringify({ error: "text is required" }));
352
- return;
546
+ res.writeHead(400, { 'Content-Type': 'application/json' })
547
+ res.end(JSON.stringify({ error: 'text is required' }))
548
+ return
353
549
  }
354
- const results = await sendToChannel(this.channels, channel || this.config.defaultChannel, text);
355
- res.writeHead(200, { "Content-Type": "application/json" });
356
- res.end(JSON.stringify({ results }));
357
- } else if (req.method === "POST" && url.pathname === "/chat") {
358
- const body = JSON.parse(await readBody(req));
359
- const { text } = body;
550
+ const results = await sendToChannel(this.config.channels, channel || this.config.defaultChannel, text)
551
+ res.writeHead(200, { 'Content-Type': 'application/json' })
552
+ res.end(JSON.stringify({ results }))
553
+
554
+ } else if (req.method === 'POST' && url.pathname === '/chat') {
555
+ const body = JSON.parse(await readBody(req))
556
+ const { text, channel, from, replyTo, messageId, target } = body
360
557
  if (!text) {
361
- res.writeHead(400, { "Content-Type": "application/json" });
362
- res.end(JSON.stringify({ error: "text is required" }));
363
- return;
558
+ res.writeHead(400, { 'Content-Type': 'application/json' })
559
+ res.end(JSON.stringify({ error: 'text is required' }))
560
+ return
561
+ }
562
+
563
+ // 构建消息对象
564
+ const msg = {
565
+ text,
566
+ channel: channel || 'http',
567
+ chatId: target || '',
568
+ from: from || 'API',
569
+ replyTo: replyTo || undefined,
570
+ messageId: messageId || undefined,
364
571
  }
365
- const reply = await routeMessage(text, this.config);
366
- res.writeHead(200, { "Content-Type": "application/json" });
367
- res.end(JSON.stringify({ reply }));
572
+
573
+ await onMessage(msg)
574
+ // onMessage 自己发送回复,这里只返回 ack
575
+ res.writeHead(200, { 'Content-Type': 'application/json' })
576
+ res.end(JSON.stringify({ status: 'processing' }))
577
+
368
578
  } else {
369
- res.writeHead(404, { "Content-Type": "application/json" });
370
- res.end(JSON.stringify({ error: "not found" }));
579
+ res.writeHead(404, { 'Content-Type': 'application/json' })
580
+ res.end(JSON.stringify({ error: 'not found' }))
371
581
  }
372
582
  } catch (e) {
373
- res.writeHead(500, { "Content-Type": "application/json" });
374
- res.end(JSON.stringify({ error: e.message }));
583
+ res.writeHead(500, { 'Content-Type': 'application/json' })
584
+ res.end(JSON.stringify({ error: e.message }))
375
585
  }
376
- });
586
+ })
587
+
377
588
  this.server.listen(this.config.port, () => {
378
- log(`HTTP API: http://localhost:${this.config.port} (API Key protected)`);
379
- });
589
+ log(`[http] API: http://localhost:${this.config.port} (API Key protected)`)
590
+ })
380
591
  }
381
592
 
382
593
  stop() {
383
- this.server?.close();
594
+ this.server?.close()
384
595
  }
385
596
  }
386
597
 
387
598
  // ============================================================
388
599
  // 守护进程管理
389
600
  // ============================================================
601
+
390
602
  function startDaemon(config) {
391
603
  if (existsSync(config.pidFile)) {
392
- const pid = parseInt(readFileSync(config.pidFile, "utf8").trim(), 10);
604
+ const pid = parseInt(readFileSync(config.pidFile, 'utf8').trim(), 10)
393
605
  try {
394
- process.kill(pid, 0);
395
- console.error(`cc-notify already running (PID ${pid})`);
396
- process.exit(1);
606
+ process.kill(pid, 0)
607
+ console.error(`cc-notify already running (PID ${pid})`)
608
+ process.exit(1)
397
609
  } catch {
398
- try {
399
- unlinkSync(config.pidFile);
400
- } catch {}
610
+ try { unlinkSync(config.pidFile) } catch {}
401
611
  }
402
612
  }
403
613
  const child = spawn(process.execPath, [import.meta.url], {
404
614
  detached: true,
405
- stdio: "ignore",
406
- env: { ...process.env, CC_NOTIFY_DAEMON: "1" },
407
- });
408
- child.unref();
409
- console.log(`cc-notify daemon started (PID ${child.pid})`);
410
- console.log(`PID: ${config.pidFile}`);
411
- console.log(`Log: ${config.logFile}`);
412
- console.log(`HTTP: http://localhost:${config.port}`);
413
- console.log(`API Key: ${config.apiKey}`);
414
- process.exit(0);
615
+ stdio: 'ignore',
616
+ env: { ...process.env, CC_NOTIFY_DAEMON: '1' },
617
+ })
618
+ child.unref()
619
+ console.log(`cc-notify daemon started (PID ${child.pid})`)
620
+ console.log(`PID: ${config.pidFile}`)
621
+ console.log(`Log: ${config.logFile}`)
622
+ console.log(`HTTP: http://localhost:${config.port}`)
623
+ console.log(`API Key: ${config.apiKey.slice(0, 8)}...`)
624
+ process.exit(0)
415
625
  }
416
626
 
417
627
  function stopDaemon(config) {
418
628
  if (!existsSync(config.pidFile)) {
419
- console.log("cc-notify not running");
420
- process.exit(0);
629
+ console.log('cc-notify not running')
630
+ process.exit(0)
421
631
  }
422
- const pid = parseInt(readFileSync(config.pidFile, "utf8").trim(), 10);
632
+ const pid = parseInt(readFileSync(config.pidFile, 'utf8').trim(), 10)
423
633
  try {
424
- process.kill(pid, "SIGTERM");
425
- console.log(`cc-notify stopped (PID ${pid})`);
634
+ process.kill(pid, 'SIGTERM')
635
+ console.log(`cc-notify stopped (PID ${pid})`)
426
636
  } catch {
427
- console.log(`PID ${pid} not found`);
637
+ console.log(`PID ${pid} not found`)
428
638
  }
429
- try {
430
- unlinkSync(config.pidFile);
431
- } catch {}
432
- process.exit(0);
639
+ try { unlinkSync(config.pidFile) } catch {}
640
+ process.exit(0)
433
641
  }
434
642
 
435
643
  function showStatus(config) {
436
644
  if (!existsSync(config.pidFile)) {
437
- console.log("cc-notify not running");
438
- process.exit(0);
645
+ console.log('cc-notify not running')
646
+ process.exit(0)
439
647
  }
440
- const pid = parseInt(readFileSync(config.pidFile, "utf8").trim(), 10);
648
+ const pid = parseInt(readFileSync(config.pidFile, 'utf8').trim(), 10)
441
649
  try {
442
- process.kill(pid, 0);
443
- console.log(`cc-notify running (PID ${pid})`);
650
+ process.kill(pid, 0)
651
+ console.log(`cc-notify running (PID ${pid})`)
444
652
  fetch(`http://localhost:${config.port}/status`)
445
- .then((r) => r.json())
446
- .then((d) => console.log(JSON.stringify(d, null, 2)))
447
- .catch(() => console.log("(HTTP API not responding)"));
653
+ .then(r => r.json())
654
+ .then(d => console.log(JSON.stringify(d, null, 2)))
655
+ .catch(() => console.log('(HTTP API not responding)'))
448
656
  } catch {
449
- console.log(`PID ${pid} is dead`);
450
- try {
451
- unlinkSync(config.pidFile);
452
- } catch {}
657
+ console.log(`PID ${pid} is dead`)
658
+ try { unlinkSync(config.pidFile) } catch {}
453
659
  }
454
660
  }
455
661
 
456
662
  // ============================================================
457
- // 工具
663
+ // 工具函数
458
664
  // ============================================================
459
- function sleep(ms) {
460
- return new Promise((r) => setTimeout(r, ms));
461
- }
665
+
666
+ function sleep(ms) { return new Promise(r => setTimeout(r, ms)) }
462
667
 
463
668
  function readBody(req) {
464
669
  return new Promise((r) => {
465
- let b = "";
466
- req.on("data", (d) => (b += d));
467
- req.on("end", () => r(b));
468
- });
670
+ let b = ''
671
+ req.on('data', (d) => (b += d))
672
+ req.on('end', () => r(b))
673
+ })
469
674
  }
470
675
 
471
676
  function log(msg) {
472
- const ts = new Date().toISOString().slice(11, 19);
473
- const line = `[${ts}] ${msg}\n`;
474
- process.stdout.write(line);
475
- try {
476
- appendFileSync(CC_NOTIFY_LOG, line);
477
- } catch {}
677
+ const ts = new Date().toISOString().slice(11, 19)
678
+ const line = `[${ts}] ${msg}\n`
679
+ process.stdout.write(line)
680
+ try { appendFileSync(CC_NOTIFY_LOG, line) } catch {}
478
681
  }
479
682
 
480
683
  // ============================================================
481
684
  // 主入口
482
685
  // ============================================================
686
+
483
687
  async function main() {
484
- const config = loadConfig();
485
- const args = process.argv.slice(2);
688
+ const config = loadConfig()
689
+ const args = process.argv.slice(2)
486
690
 
487
- if (args.includes("--stop")) return stopDaemon(config);
488
- if (args.includes("--status")) return showStatus(config);
489
- if (args.includes("--daemon")) return startDaemon(config);
691
+ if (args.includes('--stop')) return stopDaemon(config)
692
+ if (args.includes('--status')) return showStatus(config)
693
+ if (args.includes('--daemon')) return startDaemon(config)
490
694
 
491
695
  // 确保 socket 目录存在
492
- mkdirSync(SOCK_DIR, { recursive: true });
696
+ mkdirSync(SOCK_DIR, { recursive: true })
493
697
 
494
- // M9 fix: PID file lock atomic create with retry, no unlink+write race
495
- let pidAcquired = false;
698
+ // PID 文件原子锁
699
+ let pidAcquired = false
496
700
  for (let attempt = 0; attempt < 3; attempt++) {
497
701
  try {
498
- const fd = openSync(config.pidFile, "wx");
499
- writeFileSync(fd, String(process.pid));
500
- closeSync(fd);
501
- pidAcquired = true;
502
- break;
702
+ const fd = openSync(config.pidFile, 'wx')
703
+ writeFileSync(fd, String(process.pid))
704
+ closeSync(fd)
705
+ pidAcquired = true
706
+ break
503
707
  } catch (err) {
504
- if (err.code !== "EEXIST") throw err;
505
- const oldPid = parseInt(readFileSync(config.pidFile, "utf8").trim(), 10);
708
+ if (err.code !== 'EEXIST') throw err
709
+ const oldPid = parseInt(readFileSync(config.pidFile, 'utf8').trim(), 10)
506
710
  try {
507
- process.kill(oldPid, 0);
508
- console.error("cc-notify already running (PID " + oldPid + "). Use --stop first.");
509
- process.exit(1);
711
+ process.kill(oldPid, 0)
712
+ console.error(`cc-notify already running (PID ${oldPid}). Use --stop first.`)
713
+ process.exit(1)
510
714
  } catch {
511
- try { unlinkSync(config.pidFile); } catch {}
512
- if (attempt < 2) await sleep(100);
715
+ try { unlinkSync(config.pidFile) } catch {}
716
+ if (attempt < 2) await sleep(100)
513
717
  }
514
718
  }
515
719
  }
516
- if (!pidAcquired) {
517
- writeFileSync(config.pidFile, String(process.pid));
518
- }
720
+ if (!pidAcquired) writeFileSync(config.pidFile, String(process.pid))
721
+
722
+ // 创建统一消息处理器
723
+ const onMessage = createMessageHandler(config)
519
724
 
725
+ // 清理函数
520
726
  const cleanup = () => {
521
- log("Shutting down...");
522
- try {
523
- unlinkSync(config.pidFile);
524
- } catch {}
525
- process.exit(0);
526
- };
527
- process.on("SIGTERM", cleanup);
528
- process.on("SIGINT", cleanup);
529
-
530
- log("cc-notify starting...");
531
- log(`Channels: ${Object.keys(config.channels).join(", ") || "none"}`);
532
- log(`API Key: ${config.apiKey}`);
533
-
534
- // Telegram 监听
535
- const tg = new TelegramListener(config);
536
- tg.start(async (msg) => {
537
- const text = msg.text;
538
- // 内部命令
539
- if (text.startsWith("/")) {
540
- const [cmd, ...rest] = text.split(" ");
541
- let reply;
542
- switch (cmd) {
543
- case "/start":
544
- case "/help":
545
- reply = "🤖 *cc-notify* AI Code Agent 通知服务\n\nCommands:\n/ping — 检查服务\n/status — 状态\n/notify <text> — 广播通知\n其他消息 → 自动发给 cc-node 处理";
546
- break;
547
- case "/ping":
548
- reply = "🏓 pong!";
549
- break;
550
- case "/status": {
551
- const nodeInfo = await findCcNode();
552
- reply = `📊 cc-notify\nChannels: ${Object.keys(config.channels).join(", ")}\ncc-node: ${nodeInfo.running ? "✅ running" : "❌ not running"}\nUptime: ${Math.floor(process.uptime())}s`;
553
- break;
554
- }
555
- case "/notify": {
556
- const notifyText = rest.join(" ");
557
- if (!notifyText) {
558
- reply = "Usage: /notify <text>";
559
- break;
560
- }
561
- const results = await sendToChannel(config.channels, config.defaultChannel, notifyText);
562
- reply = results
563
- .map((r) => (r.ok ? `✅ ${r.channel}` : `❌ ${r.channel}: ${r.error}`))
564
- .join("\n");
565
- break;
566
- }
567
- default:
568
- reply = await routeMessage(text, config);
569
- break;
570
- }
571
- if (config.channels.telegram?.token) {
572
- await sendToChannel(config.channels, "telegram", reply);
573
- }
574
- return;
575
- }
576
- // 普通消息 → C 方案路由
577
- log(`[route] processing: "${text.slice(0, 50)}"`);
578
- const reply = await routeMessage(text, config);
579
- if (config.channels.telegram?.token) {
580
- await sendToChannel(config.channels, "telegram", reply);
727
+ log('[main] Shutting down...')
728
+ tgListener?.stop()
729
+ httpServer?.stop()
730
+ try { unlinkSync(config.pidFile) } catch {}
731
+ log('[main] Goodbye!')
732
+ process.exit(0)
733
+ }
734
+ process.on('SIGTERM', cleanup)
735
+ process.on('SIGINT', cleanup)
736
+
737
+ log('╔══════════════════════════════════════╗')
738
+ log('║ cc-notify v2.0 — 启动中... ║')
739
+ log('╚══════════════════════════════════════╝')
740
+
741
+ // ============================================================
742
+ // 启动 Telegram 监听器
743
+ // ============================================================
744
+ let tgListener = null
745
+ if (config.channels.telegram?.token) {
746
+ tgListener = await createTelegramListener(config)
747
+ if (tgListener) {
748
+ tgListener.start(async (msg) => {
749
+ msg.channel = 'telegram'
750
+ await onMessage(msg)
751
+ }).catch(e => log(`[main] TG listener error: ${e.message}`))
752
+ log('[main] ✅ Telegram listener started')
581
753
  }
582
- });
754
+ }
583
755
 
584
- // HTTP API
585
- const http = new HttpServer(config, config.channels);
586
- http.start();
756
+ // ============================================================
757
+ // 启动 QQ Bot 监听器
758
+ // ============================================================
759
+ let qqBot = null
760
+ const qqAppId = (
761
+ config.channels?.qqbot?.appId
762
+ || process.env.CC_NODE_CHANNEL_QQBOT_APPID
763
+ || ''
764
+ )
765
+ const qqSecret = (
766
+ config.channels?.qqbot?.secret
767
+ || config.channels?.qqbot?.clientSecret
768
+ || process.env.CC_NODE_CHANNEL_QQBOT_SECRET
769
+ || ''
770
+ )
771
+
772
+ if (qqAppId && qqSecret) {
773
+ try {
774
+ const { QQBot } = await import('./qqbot-listener.js')
775
+ qqBot = new QQBot({ appId: qqAppId, clientSecret: qqSecret })
776
+ qqBot.listen(async (msg) => {
777
+ await onMessage(msg)
778
+ }).catch(e => log('[main] QQ listener error: ' + e.message))
779
+ log('[main] ✅ QQ Bot WebSocket listener started')
780
+ } catch (e) {
781
+ log('[main] ⚠️ QQ Bot load failed: ' + e.message)
782
+ }
783
+ } else {
784
+ log('[main] ℹ️ QQ Bot not configured (set CC_NODE_CHANNEL_QQBOT_APPID + CC_NODE_CHANNEL_QQBOT_SECRET)')
785
+ }
587
786
 
588
- log("cc-notify ready ✅");
589
- setInterval(() => {}, 60000); // keep alive
787
+ // 启动 HTTP API
788
+ // ============================================================
789
+ const httpServer = new HttpServer(config)
790
+ httpServer.start(onMessage)
791
+
792
+ // ============================================================
793
+ // 状态报告
794
+ // ============================================================
795
+ const activeListeners = []
796
+ if (tgListener) activeListeners.push('Telegram')
797
+ if (qqBot) activeListeners.push('QQBot')
798
+
799
+ log('╔══════════════════════════════════════╗')
800
+ log('║ cc-notify v2.0 READY ✅ ║')
801
+ log('╠══════════════════════════════════════╣')
802
+ log(`║ Listeners: ${activeListeners.join(', ') || 'HTTP only'}`)
803
+ log(`║ HTTP API: http://localhost:${config.port}`)
804
+ log(`║ API Key: ${config.apiKey.slice(0, 8)}...`)
805
+ log(`║ PID: ${process.pid}`)
806
+ log('╚══════════════════════════════════════╝')
807
+
808
+ // Keep alive
809
+ setInterval(() => {}, 60000)
590
810
  }
591
811
 
592
812
  main().catch((err) => {
593
- console.error("Fatal:", err);
594
- process.exit(1);
595
- });
813
+ console.error('Fatal error:', err.message)
814
+ process.exit(1)
815
+ })