| import { get } from 'node:https'; | ||
| import { createLogger } from '../logger.js'; | ||
| const log = createLogger('UpdateCheck'); | ||
| const NPM_REGISTRY_URL = 'https://registry.npmjs.org/cc-im/latest'; | ||
| const FETCH_TIMEOUT_MS = 5000; | ||
| function fetchLatestVersion() { | ||
| return new Promise((resolve) => { | ||
| const req = get(NPM_REGISTRY_URL, { timeout: FETCH_TIMEOUT_MS }, (res) => { | ||
| if (res.statusCode !== 200) { | ||
| res.resume(); | ||
| resolve(null); | ||
| return; | ||
| } | ||
| let data = ''; | ||
| res.on('data', (chunk) => { data += chunk; }); | ||
| res.on('end', () => { | ||
| try { | ||
| const { version } = JSON.parse(data); | ||
| resolve(typeof version === 'string' ? version : null); | ||
| } | ||
| catch { | ||
| resolve(null); | ||
| } | ||
| }); | ||
| }); | ||
| req.on('error', () => resolve(null)); | ||
| req.on('timeout', () => { req.destroy(); resolve(null); }); | ||
| }); | ||
| } | ||
| /** | ||
| * 比较语义化版本,返回 true 表示 latest 比 current 更新 | ||
| */ | ||
| function isNewer(current, latest) { | ||
| const parse = (v) => v.replace(/^v/, '').split('.').map(Number); | ||
| const c = parse(current); | ||
| const l = parse(latest); | ||
| for (let i = 0; i < 3; i++) { | ||
| if ((l[i] ?? 0) > (c[i] ?? 0)) | ||
| return true; | ||
| if ((l[i] ?? 0) < (c[i] ?? 0)) | ||
| return false; | ||
| } | ||
| return false; | ||
| } | ||
| /** | ||
| * 异步检查是否有新版本,有则打印提示日志。不阻塞启动流程。 | ||
| */ | ||
| export async function checkForUpdate(currentVersion) { | ||
| try { | ||
| const latest = await fetchLatestVersion(); | ||
| if (latest && isNewer(currentVersion, latest)) { | ||
| log.info(`发现新版本 v${latest}(当前 v${currentVersion}),请运行: npx cc-im@latest 或 npm i -g cc-im@latest`); | ||
| } | ||
| } | ||
| catch { | ||
| // 静默忽略,不影响正常启动 | ||
| } | ||
| } |
@@ -216,2 +216,3 @@ import { spawn } from 'node:child_process'; | ||
| } | ||
| rl.close(); | ||
| if (!child.killed) { | ||
@@ -218,0 +219,0 @@ child.kill('SIGTERM'); |
| import { MAX_STREAMING_CONTENT_LENGTH, MAX_CARD_CONTENT_LENGTH } from '../constants.js'; | ||
| import { splitLongContent as sharedSplitLongContent, truncateText } from '../shared/utils.js'; | ||
| import { buildInputSummary } from '../shared/utils.js'; | ||
| import { splitLongContent as sharedSplitLongContent, truncateText, buildInputSummary } from '../shared/utils.js'; | ||
| const HEADER_TEMPLATES = { | ||
@@ -5,0 +4,0 @@ processing: 'blue', |
| import { getClient } from './client.js'; | ||
| import { createLogger } from '../logger.js'; | ||
| import { safeStringify } from '../shared/utils.js'; | ||
| import { withRetry } from '../shared/retry.js'; | ||
| import { withRetry, NonRetryableError } from '../shared/retry.js'; | ||
| const log = createLogger('CardKit'); | ||
@@ -56,2 +56,6 @@ const sessions = new Map(); | ||
| return withRetry(async () => { | ||
| // 限频错误不再重试,避免重试风暴 | ||
| const s = sessions.get(cardId); | ||
| if (s?.completed) | ||
| return; | ||
| const client = getClient(); | ||
@@ -66,6 +70,10 @@ const res = await client.cardkit.v1.card.settings({ | ||
| if (res?.code && res.code !== 0) { | ||
| // 限频错误不可重试,直接穿透 | ||
| if (res.code === 200400) { | ||
| log.warn(`enableStreaming rate limited: ${res.msg}`); | ||
| throw new NonRetryableError(`enableStreaming rate limited: code=${res.code}, msg=${res.msg}`); | ||
| } | ||
| log.error(`enableStreaming failed: code=${res.code}, msg=${res.msg}`); | ||
| throw new Error(`enableStreaming error: code=${res.code}, msg=${res.msg}`); | ||
| } | ||
| const s = sessions.get(cardId); | ||
| if (s) | ||
@@ -111,2 +119,4 @@ s.streamingEnabled = true; | ||
| return; // 更新过于频繁,等下次节流重试 | ||
| if (code === 200740) | ||
| return; // 查询结果为空(卡片已失效),静默忽略 | ||
| // 200850 / 300309: 流式超时或已关闭 → 重新启用后重试一次 | ||
@@ -120,2 +130,6 @@ if (code === 200850 || code === 300309) { | ||
| await enableStreaming(cardId); | ||
| // 启用后再次检查是否已完成 | ||
| const s2 = sessions.get(cardId); | ||
| if (!s2 || s2.completed) | ||
| return; | ||
| const retryRes = await call(nextSeq(cardId)); | ||
@@ -201,2 +215,5 @@ if (retryRes?.code && retryRes.code !== 0) { | ||
| return; | ||
| // 标记已完成,防止并发调用 | ||
| s.completed = true; | ||
| s.streamingEnabled = false; | ||
| const client = getClient(); | ||
@@ -215,3 +232,2 @@ try { | ||
| else { | ||
| s.streamingEnabled = false; | ||
| log.debug(`Streaming disabled for card ${cardId}`); | ||
@@ -218,0 +234,0 @@ } |
@@ -225,5 +225,5 @@ import { join } from 'node:path'; | ||
| const eventType = data?.header?.event_type || data?.type || 'unknown'; | ||
| log.info(`Received event: ${eventType}`, safeStringify(data).slice(0, 500)); | ||
| log.info(`Received event: ${eventType}`); | ||
| if (eventType.includes('card') || eventType.includes('action')) { | ||
| log.info('Card/Action event data:', safeStringify(data, 2)); | ||
| log.debug('Card/Action event data:', safeStringify(data, 2)); | ||
| } | ||
@@ -230,0 +230,0 @@ }, |
@@ -202,5 +202,5 @@ import { createServer } from 'node:http'; | ||
| log.info(`Permission server listening on 127.0.0.1:${port}`); | ||
| resolve({ close: () => server.close() }); | ||
| resolve({ close: () => new Promise((r) => server.close(() => r())) }); | ||
| }); | ||
| }); | ||
| } |
+7
-1
@@ -13,4 +13,8 @@ import { loadConfig } from './config.js'; | ||
| import { cleanOldImages } from './shared/utils.js'; | ||
| import { checkForUpdate } from './shared/update-check.js'; | ||
| import { initLogger, createLogger, closeLogger } from './logger.js'; | ||
| import { execFileSync } from 'node:child_process'; | ||
| import { createRequire } from 'node:module'; | ||
| const require = createRequire(import.meta.url); | ||
| const { version: APP_VERSION } = require('../package.json'); | ||
| const log = createLogger('Main'); | ||
@@ -114,3 +118,3 @@ function getClaudeVersion(cliPath) { | ||
| const startupMsg = [ | ||
| '🟢 cc-im 服务已启动', | ||
| `🟢 cc-im v${APP_VERSION} 服务已启动`, | ||
| '', | ||
@@ -125,2 +129,3 @@ `平台: ${activeBots.join(' + ')}`, | ||
| sendLifecycleNotification(activeBots, startupMsg).catch(() => { }); | ||
| checkForUpdate(APP_VERSION).catch(() => { }); | ||
| const imageCleanupTimer = setInterval(() => { | ||
@@ -138,2 +143,3 @@ cleanOldImages().then((n) => { if (n > 0) | ||
| log.info('Shutting down...'); | ||
| clearInterval(imageCleanupTimer); | ||
| // 发送关闭通知 | ||
@@ -140,0 +146,0 @@ const uptimeSec = Math.floor((Date.now() - startedAt) / 1000); |
| import { createLogger } from '../logger.js'; | ||
| const log = createLogger('Retry'); | ||
| /** 抛出此错误时 withRetry 不再重试,直接穿透 */ | ||
| export class NonRetryableError extends Error { | ||
| constructor(message) { | ||
| super(message); | ||
| this.name = 'NonRetryableError'; | ||
| } | ||
| } | ||
| export async function withRetry(fn, opts) { | ||
@@ -12,3 +19,3 @@ const maxRetries = opts?.maxRetries ?? 3; | ||
| catch (err) { | ||
| if (attempt >= maxRetries) | ||
| if (err instanceof NonRetryableError || attempt >= maxRetries) | ||
| throw err; | ||
@@ -15,0 +22,0 @@ const delay = Math.min(baseDelay * 2 ** attempt + Math.random() * 200, maxDelay); |
| /** | ||
| * 共享工具函数 | ||
| */ | ||
| import { readdir, stat, unlink } from 'node:fs/promises'; | ||
| import { readdir, stat, unlink, mkdir } from 'node:fs/promises'; | ||
| import { join } from 'node:path'; | ||
| import { IMAGE_DIR } from '../constants.js'; | ||
| import { createLogger } from '../logger.js'; | ||
| import { mkdir } from 'node:fs/promises'; | ||
| const log = createLogger('Utils'); | ||
@@ -10,0 +9,0 @@ /** |
+6
-6
| { | ||
| "name": "cc-im", | ||
| "version": "1.1.0", | ||
| "version": "1.1.1", | ||
| "description": "Multi-platform bot bridge (Feishu & Telegram) for Claude Code CLI", | ||
@@ -38,11 +38,11 @@ "repository": { | ||
| "dependencies": { | ||
| "@larksuiteoapi/node-sdk": "^1.45.0", | ||
| "dotenv": "^16.4.7", | ||
| "@larksuiteoapi/node-sdk": "^1.59.0", | ||
| "dotenv": "^17.3.1", | ||
| "telegraf": "^4.16.3" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/node": "^22.12.0", | ||
| "@types/node": "^25.3.0", | ||
| "@vitest/coverage-v8": "^4.0.18", | ||
| "tsx": "^4.19.0", | ||
| "typescript": "^5.7.0", | ||
| "tsx": "^4.21.0", | ||
| "typescript": "^5.9.3", | ||
| "vitest": "^4.0.18" | ||
@@ -49,0 +49,0 @@ }, |
+5
-3
@@ -26,2 +26,3 @@ # cc-im | ||
| - **守护进程模式**:支持 `-d` 后台运行和 `stop` 停止 | ||
| - **版本更新检查**:启动时自动检查 npm 最新版本,有更新时提示 | ||
| - **日志等级配置**:支持 DEBUG/INFO/WARN/ERROR 四级日志 | ||
@@ -41,3 +42,3 @@ | ||
| export TELEGRAM_BOT_TOKEN=your_bot_token | ||
| npx cc-im | ||
| npx cc-im@latest | ||
| ``` | ||
@@ -55,3 +56,3 @@ | ||
| export TELEGRAM_BOT_TOKEN=your_bot_token | ||
| npx cc-im | ||
| npx cc-im@latest | ||
@@ -83,3 +84,3 @@ # 方式二:从源码运行 | ||
| export FEISHU_APP_SECRET=your_app_secret | ||
| npx cc-im | ||
| npx cc-im@latest | ||
| ``` | ||
@@ -277,2 +278,3 @@ | ||
| │ ├── types.ts # 共享类型定义 | ||
| │ ├── update-check.ts # 启动时版本更新检查 | ||
| │ └── utils.ts # 共享工具函数 | ||
@@ -279,0 +281,0 @@ ├── session/ |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
191012
1.8%36
2.86%4568
1.92%285
0.71%+ Added
- Removed
Updated