mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
The Feishu and Telegram IM adapters used to be standalone Bun processes that the user had to launch manually with bun + .ts source on disk — which meant they were effectively unreachable from the bundled desktop app, since the user wouldn't have bun or the source tree. This adds them as a third mode of the merged claude-sidecar binary: claude-sidecar adapters --app-root <path> [--feishu] [--telegram] The launcher pre-checks credentials via the same `loadConfig()` the adapters use internally, then conditionally `await import()`s each enabled adapter whose creds are present. Adapters with missing creds are warned and skipped, so a partially-configured user (e.g. only Feishu set up, no Telegram bot token) still gets the working adapter started cleanly instead of having Telegram's top-level `process.exit(1)` kill the whole process. Adapter source code is unchanged — the adapters still self-start at top-level via Lark.WSClient.start() / grammy bot.start(). Their SIGINT handlers also still register independently. The only thing gating runtime is whether bun's static-import follows the dynamic specifier into adapters/feishu/index.ts and adapters/telegram/index.ts, which it does. Bundle impact ============= metric P0+P2 only +adapters mode delta claude-sidecar 66 MB 68 MB +2 MB .app total 87 MB 88 MB +1 MB .dmg 37 MB 37 MB 0 MB Both adapter SDKs (@larksuiteoapi/node-sdk and grammy) statically inline into the binary at a +2 MB cost, fully absorbed by DMG compression. Compared to the alternative (a separate ~60 MB sidecar binary per adapter, or even one combined ~60 MB adapter binary) this is essentially free. Verification ============ * `claude-sidecar server` regression test still passes (boots, /api/sessions → 200, CronScheduler runs) * `claude-sidecar cli --version` returns 999.0.0-local * `claude-sidecar adapters` (no flags) → exit 2 with usage error * `claude-sidecar adapters --feishu --telegram` (no creds) → both warned and skipped, exit 1 * `claude-sidecar adapters --feishu` (FEISHU_APP_ID=cli_fake creds) → Feishu adapter boots, Lark client `client ready`, attempts API connect, fails with 400 from feishu API and gracefully retries (correct behavior — fake creds) * `claude-sidecar adapters --telegram` (TELEGRAM_BOT_TOKEN=fake:token) → grammy bot.start() called, getMe API hits with 404, throws GrammyError (correct — fake creds) * `bun test adapters/` → 299 pass / 0 fail * `bun test src/` → 358 pass / 45 fail / 2 errors, identical to baseline Scanner change ============== desktop/scripts/scan-missing-imports.ts now also walks adapters/ in addition to src/, so any future feature() gated stubs in the adapter tree get auto-stubbed. As of this commit, adapters/ has 0 missing imports — all clean. Next step (UI integration, not done here) ========================================== To actually wire this into the desktop UX, the Tauri main process needs: - A "Configure IM adapters" settings page (App ID/Secret, bot token, allowed users) that writes ~/.claude/adapters.json - A "Start/stop Feishu" / "Start/stop Telegram" toggle that spawns `claude-sidecar adapters --feishu` (or both) as a managed sidecar, monitors lifecycle, restarts on crash The runtime infrastructure is now in place — that work is purely UI + Rust spawn glue and can be done independently. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
157 lines
5.3 KiB
TypeScript
157 lines
5.3 KiB
TypeScript
/**
|
||
* Claude Code 桌面端合并 sidecar 入口。
|
||
*
|
||
* 历史上 server / cli / IM adapters 是各自独立的进程。每个 bun-compile
|
||
* 二进制都要带一份 ~55MB 的 bun runtime,光这一项就重复占了 100MB+。
|
||
* 把所有运行模式合并到同一个二进制里,runtime 只保留一份;调用方通过
|
||
* 第一个 positional 参数选择模式:
|
||
*
|
||
* claude-sidecar server --app-root <path> --host 127.0.0.1 --port 12345
|
||
* claude-sidecar cli --app-root <path> [其它 CLI 参数...]
|
||
* claude-sidecar adapters --app-root <path> [--feishu] [--telegram]
|
||
*
|
||
* 任何模式都必须先做 process.env / process.argv 设置,再 await 进入相应的
|
||
* 子模块树。原因:src/server/index.ts、src/entrypoints/cli.tsx、以及
|
||
* adapters/feishu/index.ts 等顶层都会立即读 process.argv / process.env,
|
||
* 必须在它们求值前 splice 掉 --app-root、mode、--feishu/--telegram 这些
|
||
* launcher-only 参数。
|
||
*/
|
||
|
||
const rawArgs = process.argv.slice(2)
|
||
if (rawArgs.length === 0) {
|
||
console.error('claude-sidecar: missing mode argument (expected "server", "cli" or "adapters")')
|
||
process.exit(2)
|
||
}
|
||
const mode = rawArgs[0]!
|
||
const restArgs = rawArgs.slice(1)
|
||
|
||
if (mode === 'adapters') {
|
||
await runAdapters(restArgs)
|
||
} else {
|
||
const { appRoot, args } = parseLauncherArgs(restArgs)
|
||
|
||
process.env.CLAUDE_APP_ROOT = appRoot
|
||
process.env.CALLER_DIR ||= process.cwd()
|
||
process.argv = [process.argv[0]!, process.argv[1]!, ...args]
|
||
|
||
await import('../../preload.ts')
|
||
|
||
if (mode === 'server') {
|
||
const { startServer } = await import('../../src/server/index.ts')
|
||
startServer()
|
||
} else if (mode === 'cli') {
|
||
await import('../../src/entrypoints/cli.tsx')
|
||
} else {
|
||
console.error(`claude-sidecar: unknown mode "${mode}" (expected "server", "cli" or "adapters")`)
|
||
process.exit(2)
|
||
}
|
||
}
|
||
|
||
async function runAdapters(rawArgs: string[]): Promise<void> {
|
||
// adapters 模式的参数解析独立于 server/cli —— 这里只接受 --feishu /
|
||
// --telegram 选择启用哪个适配器,再加可选的 --app-root(透传给
|
||
// adapters/common/config.ts 内的 process.env 读取)。
|
||
let appRoot: string | null = process.env.CLAUDE_APP_ROOT ?? null
|
||
let enableFeishu = false
|
||
let enableTelegram = false
|
||
|
||
for (let i = 0; i < rawArgs.length; i++) {
|
||
const arg = rawArgs[i]
|
||
if (arg === '--app-root') {
|
||
appRoot = rawArgs[i + 1] ?? null
|
||
i += 1
|
||
continue
|
||
}
|
||
if (arg === '--feishu') {
|
||
enableFeishu = true
|
||
continue
|
||
}
|
||
if (arg === '--telegram') {
|
||
enableTelegram = true
|
||
continue
|
||
}
|
||
console.warn(`claude-sidecar adapters: ignoring unknown arg "${arg}"`)
|
||
}
|
||
|
||
if (!enableFeishu && !enableTelegram) {
|
||
console.error(
|
||
'claude-sidecar adapters: must enable at least one of --feishu / --telegram',
|
||
)
|
||
process.exit(2)
|
||
}
|
||
|
||
if (appRoot) {
|
||
process.env.CLAUDE_APP_ROOT = appRoot
|
||
}
|
||
process.env.CALLER_DIR ||= process.cwd()
|
||
|
||
await import('../../preload.ts')
|
||
|
||
// 在 import adapter 之前先用同一份 loadConfig() 检查凭据。adapter 的
|
||
// top-level 代码里已经有 if (!cred) process.exit(1),但那会把整个
|
||
// 进程拖死 —— 包括另一个本来正常的 adapter。这里提前 gate 一下,
|
||
// 缺凭据的 adapter 直接跳过、不 import。
|
||
const { loadConfig } = await import('../../adapters/common/config.ts')
|
||
const config = loadConfig()
|
||
|
||
let started = 0
|
||
|
||
if (enableFeishu) {
|
||
if (!config.feishu.appId || !config.feishu.appSecret) {
|
||
console.warn(
|
||
'[claude-sidecar] --feishu requested but FEISHU_APP_ID / FEISHU_APP_SECRET missing in env or ~/.claude/adapters.json — skipping',
|
||
)
|
||
} else {
|
||
console.log('[claude-sidecar] starting Feishu adapter')
|
||
// 副作用 import:feishu/index.ts 顶层会自动 new WSClient + start()
|
||
await import('../../adapters/feishu/index.ts')
|
||
started += 1
|
||
}
|
||
}
|
||
|
||
if (enableTelegram) {
|
||
if (!config.telegram.botToken) {
|
||
console.warn(
|
||
'[claude-sidecar] --telegram requested but TELEGRAM_BOT_TOKEN missing in env or ~/.claude/adapters.json — skipping',
|
||
)
|
||
} else {
|
||
console.log('[claude-sidecar] starting Telegram adapter')
|
||
// 副作用 import:telegram/index.ts 顶层会自动 bot.start()
|
||
await import('../../adapters/telegram/index.ts')
|
||
started += 1
|
||
}
|
||
}
|
||
|
||
if (started === 0) {
|
||
console.error(
|
||
'[claude-sidecar] no adapter could be started — check credentials in env or ~/.claude/adapters.json',
|
||
)
|
||
process.exit(1)
|
||
}
|
||
|
||
// 让进程保持存活:两个 adapter 都通过 long-lived WebSocket(Lark WSClient
|
||
// / grammY long-polling)持有 event loop,自然不会退出。这里不需要额外
|
||
// setInterval 兜底。两个 adapter 自己注册的 SIGINT handler 都会触发。
|
||
}
|
||
|
||
function parseLauncherArgs(rawArgs: string[]): { appRoot: string; args: string[] } {
|
||
const nextArgs: string[] = []
|
||
let appRoot: string | null = process.env.CLAUDE_APP_ROOT ?? null
|
||
|
||
for (let index = 0; index < rawArgs.length; index++) {
|
||
const arg = rawArgs[index]
|
||
if (arg === '--app-root') {
|
||
appRoot = rawArgs[index + 1] ?? null
|
||
index += 1
|
||
continue
|
||
}
|
||
nextArgs.push(arg!)
|
||
}
|
||
|
||
if (!appRoot) {
|
||
throw new Error('Missing --app-root for claude-sidecar')
|
||
}
|
||
|
||
return { appRoot, args: nextArgs }
|
||
}
|