mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-19 13:33:35 +08:00
Add a WhatsApp adapter backed by Baileys linked-device auth, plus desktop QR binding UI, server config endpoints, sidecar startup wiring, tests, and documentation. Constraint: Uses WhatsApp Web linked-device auth, not Meta WhatsApp Business Cloud API. Tested: - cd adapters && bun run check:adapters - bun test src/server/__tests__/adapters.test.ts - cd desktop && bun run check:desktop - bun run check:native - bun run check:persistence-upgrade - bun run check:docs Not-tested: - Live WhatsApp QR pairing, because no WhatsApp account/device was exercised here. - bun run check:server, because the existing src/server/__tests__/conversations.test.ts timeout still fails independently. Confidence: medium Scope-risk: moderate
191 lines
6.7 KiB
TypeScript
191 lines
6.7 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] [--wechat] [--dingtalk] [--whatsapp]
|
||
*
|
||
* 任何模式都必须先做 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 参数。
|
||
*/
|
||
|
||
import { parseLauncherArgs, resolveSidecarInvocation } from './launcherRouting'
|
||
|
||
const rawArgs = process.argv.slice(2)
|
||
const invocation = resolveSidecarInvocation(rawArgs)
|
||
if (!invocation.mode) {
|
||
console.error('claude-sidecar: missing mode argument (expected "server", "cli" or "adapters")')
|
||
process.exit(2)
|
||
}
|
||
const mode = invocation.mode
|
||
const restArgs = invocation.restArgs
|
||
|
||
if (mode === 'adapters') {
|
||
await runAdapters(restArgs)
|
||
} else {
|
||
const { appRoot, args } = parseLauncherArgs(restArgs, invocation.defaultAppRoot)
|
||
|
||
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 / --wechat / --dingtalk / --whatsapp 选择启用哪个适配器,再加可选的 --app-root(透传给
|
||
// adapters/common/config.ts 内的 process.env 读取)。
|
||
let appRoot: string | null = process.env.CLAUDE_APP_ROOT ?? null
|
||
let enableFeishu = false
|
||
let enableTelegram = false
|
||
let enableWechat = false
|
||
let enableDingtalk = false
|
||
let enableWhatsApp = 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
|
||
}
|
||
if (arg === '--wechat') {
|
||
enableWechat = true
|
||
continue
|
||
}
|
||
if (arg === '--dingtalk') {
|
||
enableDingtalk = true
|
||
continue
|
||
}
|
||
if (arg === '--whatsapp') {
|
||
enableWhatsApp = true
|
||
continue
|
||
}
|
||
console.warn(`claude-sidecar adapters: ignoring unknown arg "${arg}"`)
|
||
}
|
||
|
||
if (!enableFeishu && !enableTelegram && !enableWechat && !enableDingtalk && !enableWhatsApp) {
|
||
console.error(
|
||
'claude-sidecar adapters: must enable at least one of --feishu / --telegram / --wechat / --dingtalk / --whatsapp',
|
||
)
|
||
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 (enableWechat) {
|
||
if (!config.wechat.accountId || !config.wechat.botToken) {
|
||
console.warn(
|
||
'[claude-sidecar] --wechat requested but no QR-bound WeChat account found in env or ~/.claude/adapters.json — skipping',
|
||
)
|
||
} else {
|
||
console.log('[claude-sidecar] starting WeChat adapter')
|
||
await import('../../adapters/wechat/index.ts')
|
||
started += 1
|
||
}
|
||
}
|
||
|
||
if (enableDingtalk) {
|
||
if (!config.dingtalk.clientId || !config.dingtalk.clientSecret) {
|
||
console.warn(
|
||
'[claude-sidecar] --dingtalk requested but DINGTALK_CLIENT_ID / DINGTALK_CLIENT_SECRET missing in env or ~/.claude/adapters.json — skipping',
|
||
)
|
||
} else {
|
||
console.log('[claude-sidecar] starting DingTalk adapter')
|
||
await import('../../adapters/dingtalk/index.ts')
|
||
started += 1
|
||
}
|
||
}
|
||
|
||
if (enableWhatsApp) {
|
||
const { hasWhatsAppAuth } = await import('../../adapters/whatsapp/session.ts')
|
||
if (!hasWhatsAppAuth(config.whatsapp.authDir)) {
|
||
console.warn(
|
||
'[claude-sidecar] --whatsapp requested but no QR-linked WhatsApp account found in env or ~/.claude/adapters.json — skipping',
|
||
)
|
||
} else {
|
||
console.log('[claude-sidecar] starting WhatsApp adapter')
|
||
await import('../../adapters/whatsapp/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 都会触发。
|
||
}
|