mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
DingTalk uses QR registration to store client credentials, then reuses the existing IM pairing and session bridge. The merged main implementation keeps the existing WeChat QR binding path intact while adding DingTalk as a peer IM platform. The default IM workdir now falls back to the local user working directory so a newly bound chat can start immediately even when recent-project history is empty. Constraint: Local main already carries WeChat IM, so the merge keeps WeChat config, QR binding, sidecar args, and unbind behavior intact while adding DingTalk. Rejected: Keep empty defaultProjectDir as project-picker-only | newly bound IM users can hit a dead end with no recent projects. Confidence: high Scope-risk: moderate Directive: Keep IM platform unions synchronized across config, pairing, sidecar args, desktop settings, and docs. Tested: bun test common/ dingtalk/ wechat/; bun test src/server/__tests__/adapters.test.ts; cd adapters && bunx tsc --noEmit; bun run check:policy; bun run check:server; bun run check:desktop; bun run check:adapters; bun run check:native; bun run check:docs Not-tested: quality:pr full gate was blocked by existing local main CLI-core diff requiring allow-cli-core-change maintainer approval; live post-fix DingTalk second-message delivery was not repeated after the merge.
173 lines
6.0 KiB
TypeScript
173 lines
6.0 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]
|
||
*
|
||
* 任何模式都必须先做 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 选择启用哪个适配器,再加可选的 --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
|
||
|
||
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
|
||
}
|
||
console.warn(`claude-sidecar adapters: ignoring unknown arg "${arg}"`)
|
||
}
|
||
|
||
if (!enableFeishu && !enableTelegram && !enableWechat && !enableDingtalk) {
|
||
console.error(
|
||
'claude-sidecar adapters: must enable at least one of --feishu / --telegram / --wechat / --dingtalk',
|
||
)
|
||
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 (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 都会触发。
|
||
}
|