cc-haha/adapters/common/config.ts
程序员阿江(Relakkes) ddcfa5faae feat(adapters): add WhatsApp linked-device support (#573)
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
2026-06-09 21:31:46 +08:00

222 lines
6.3 KiB
TypeScript

/**
* Adapter 配置加载
*
* 优先级:环境变量 > ~/.claude/adapters.json > 默认值
*/
import * as fs from 'node:fs'
import * as os from 'node:os'
import * as path from 'node:path'
export type PairedUser = {
userId: string | number
displayName: string
pairedAt: number
}
export type PairingState = {
code: string | null
expiresAt: number | null
createdAt: number | null
}
export type TelegramConfig = {
botToken: string
allowedUsers: number[]
pairedUsers: PairedUser[]
defaultWorkDir: string
}
export type FeishuConfig = {
appId: string
appSecret: string
encryptKey: string
verificationToken: string
allowedUsers: string[]
pairedUsers: PairedUser[]
defaultWorkDir: string
streamingCard: boolean
}
export type WechatConfig = {
accountId: string
botToken: string
baseUrl: string
userId: string
allowedUsers: string[]
pairedUsers: PairedUser[]
defaultWorkDir: string
}
export type DingtalkConfig = {
clientId: string
clientSecret: string
allowedUsers: string[]
pairedUsers: PairedUser[]
defaultWorkDir: string
endpoint: string
permissionCardTemplateId: string
}
export type WhatsAppConfig = {
accountJid: string
authDir: string
allowedUsers: string[]
pairedUsers: PairedUser[]
defaultWorkDir: string
}
export type AdapterConfig = {
serverUrl: string
defaultProjectDir: string
pairing: PairingState
telegram: TelegramConfig
feishu: FeishuConfig
wechat: WechatConfig
dingtalk: DingtalkConfig
whatsapp: WhatsAppConfig
}
export type AdapterPlatformConfig =
| TelegramConfig
| FeishuConfig
| WechatConfig
| DingtalkConfig
| WhatsAppConfig
function getConfigPath(): string {
const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude')
return path.join(configDir, 'adapters.json')
}
function loadFile(): Record<string, any> {
try {
return JSON.parse(fs.readFileSync(getConfigPath(), 'utf-8'))
} catch (err: any) {
if (err?.code !== 'ENOENT') {
console.warn(`[Config] Failed to parse ${getConfigPath()}, using defaults`)
}
return {}
}
}
export function loadConfig(): AdapterConfig {
const file = loadFile()
const tg = file.telegram ?? {}
const fs_ = file.feishu ?? {}
const wc = file.wechat ?? {}
const dt = file.dingtalk ?? {}
const wa = file.whatsapp ?? {}
const pairing = file.pairing ?? {}
const fallbackWorkDir = resolveUserDefaultWorkDir()
const whatsappAuthDir = resolveConfiguredPath(
process.env.WHATSAPP_AUTH_DIR || wa.authDir || defaultWhatsAppAuthDir(),
)
return {
serverUrl: process.env.ADAPTER_SERVER_URL || file.serverUrl || 'ws://127.0.0.1:3456',
defaultProjectDir: file.defaultProjectDir || '',
pairing: {
code: pairing.code ?? null,
expiresAt: pairing.expiresAt ?? null,
createdAt: pairing.createdAt ?? null,
},
telegram: {
botToken: process.env.TELEGRAM_BOT_TOKEN || tg.botToken || '',
allowedUsers: tg.allowedUsers ?? [],
pairedUsers: tg.pairedUsers ?? [],
defaultWorkDir: tg.defaultWorkDir || fallbackWorkDir,
},
feishu: {
appId: process.env.FEISHU_APP_ID || fs_.appId || '',
appSecret: process.env.FEISHU_APP_SECRET || fs_.appSecret || '',
encryptKey: process.env.FEISHU_ENCRYPT_KEY || fs_.encryptKey || '',
verificationToken: process.env.FEISHU_VERIFICATION_TOKEN || fs_.verificationToken || '',
allowedUsers: fs_.allowedUsers ?? [],
pairedUsers: fs_.pairedUsers ?? [],
defaultWorkDir: fs_.defaultWorkDir || fallbackWorkDir,
streamingCard: fs_.streamingCard ?? false,
},
wechat: {
accountId: process.env.WECHAT_ACCOUNT_ID || wc.accountId || '',
botToken: process.env.WECHAT_BOT_TOKEN || wc.botToken || '',
baseUrl: process.env.WECHAT_BASE_URL || wc.baseUrl || 'https://ilinkai.weixin.qq.com',
userId: process.env.WECHAT_USER_ID || wc.userId || '',
allowedUsers: wc.allowedUsers ?? [],
pairedUsers: wc.pairedUsers ?? [],
defaultWorkDir: wc.defaultWorkDir || fallbackWorkDir,
},
dingtalk: {
clientId: process.env.DINGTALK_CLIENT_ID || dt.clientId || '',
clientSecret: process.env.DINGTALK_CLIENT_SECRET || dt.clientSecret || '',
allowedUsers: dt.allowedUsers ?? [],
pairedUsers: dt.pairedUsers ?? [],
defaultWorkDir: dt.defaultWorkDir || fallbackWorkDir,
endpoint: process.env.DINGTALK_STREAM_ENDPOINT || dt.endpoint || 'https://api.dingtalk.com',
permissionCardTemplateId: process.env.DINGTALK_PERMISSION_CARD_TEMPLATE_ID || dt.permissionCardTemplateId || '',
},
whatsapp: {
accountJid: process.env.WHATSAPP_ACCOUNT_JID || wa.accountJid || '',
authDir: whatsappAuthDir,
allowedUsers: wa.allowedUsers ?? [],
pairedUsers: wa.pairedUsers ?? [],
defaultWorkDir: wa.defaultWorkDir || fallbackWorkDir,
},
}
}
export function getConfiguredWorkDir(config: AdapterConfig, platformConfig: AdapterPlatformConfig): string {
return config.defaultProjectDir || platformConfig.defaultWorkDir
}
function resolveUserDefaultWorkDir(): string {
const candidates = [
process.env.ADAPTER_DEFAULT_PROJECT_DIR,
process.env.CLAUDE_ADAPTER_DEFAULT_WORK_DIR,
process.env.PWD,
process.cwd(),
os.homedir(),
]
for (const candidate of candidates) {
const resolved = resolveExistingDirectory(candidate)
if (resolved) return resolved
}
return os.homedir()
}
function resolveExistingDirectory(value: string | undefined): string | null {
const trimmed = value?.trim()
if (!trimmed) return null
const expanded = trimmed === '~'
? os.homedir()
: trimmed.startsWith('~/')
? path.join(os.homedir(), trimmed.slice(2))
: trimmed
try {
const realPath = fs.realpathSync(expanded)
return fs.statSync(realPath).isDirectory() ? realPath : null
} catch {
return null
}
}
function defaultWhatsAppAuthDir(): string {
const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude')
return path.join(configDir, 'whatsapp-auth', 'default')
}
function resolveConfiguredPath(value: string): string {
const trimmed = value.trim()
if (!trimmed) return defaultWhatsAppAuthDir()
const expanded = trimmed === '~'
? os.homedir()
: trimmed.startsWith('~/')
? path.join(os.homedir(), trimmed.slice(2))
: trimmed
return path.resolve(expanded)
}