File size: 3,858 Bytes
73bc263 44e0492 73bc263 44e0492 73bc263 44e0492 73bc263 44e0492 73bc263 44e0492 73bc263 1caee20 53e0f9f 1caee20 53e0f9f 73bc263 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | /**
* Feishu Config — read/write ~/.claude/adapters.json feishu section
*/
import * as fs from 'node:fs'
import * as path from 'node:path'
import * as os from 'node:os'
export type PairedUser = {
userId: string | number
displayName: string
pairedAt: number
}
export type AuthorizedUser = {
userId: string
displayName: string
authorizedAt: number
}
export type FeishuRuntimeConfig = {
appId?: string
appSecret?: string
/** 'feishu' | 'lark' — saved from registerApp result.user_info?.tenant_brand */
tenant?: string
encryptKey?: string
verificationToken?: string
/** DM 白名单用户 open_id 列表(空 = 所有人可私聊) */
allowedUsers?: string[]
/** 管理员 open_id 列表 */
admins?: string[]
/** 允许响应的群聊 chat_id 列表(空 = 所有群都不响应) */
allowedChats?: string[]
/** 群聊中是否要求 @bot 才响应(默认 true) */
requireMentionInGroup?: boolean
/** 已配对的用户 */
pairedUsers?: PairedUser[]
/** 已授权的用户 */
authorizedUsers?: AuthorizedUser[]
/** 是否使用卡片回复模式 */
streamingCard?: boolean
/** TTS 语音回复开关 — 发送消息时同时发送语音 */
ttsEnabled?: boolean
/** TTS 引擎: 'edge' | 'voxcpm' */
ttsProvider?: 'edge' | 'voxcpm'
/** TTS 语音名称(edge-tts 格式,默认 zh-CN-XiaoxiaoNeural) */
ttsVoice?: string
/** VoxCPM 参考音频路径(用于 voxtts 模式) */
ttsReferenceAudio?: string
}
function getConfigPath(): string {
const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude')
return path.join(configDir, 'adapters.json')
}
function maskSecret(value: string | undefined): string | undefined {
if (!value) return value
if (value.length <= 4) return '****'
return '****' + value.slice(-4)
}
export function getFeishuConfig(): FeishuRuntimeConfig {
try {
const raw = fs.readFileSync(getConfigPath(), 'utf-8')
const config = JSON.parse(raw) as { feishu?: FeishuRuntimeConfig }
return config.feishu ?? {}
} catch {
return {}
}
}
export function saveFeishuConfig(patch: Partial<FeishuRuntimeConfig>): void {
const filePath = getConfigPath()
const dir = path.dirname(filePath)
fs.mkdirSync(dir, { recursive: true, mode: 0o700 })
let existing: Record<string, unknown> = {}
try {
const raw = fs.readFileSync(filePath, 'utf-8')
existing = JSON.parse(raw)
} catch {
// File doesn't exist yet
}
const current = ((existing.feishu as Record<string, unknown> | undefined) ?? {}) as Record<string, unknown>
// Don't overwrite masked secrets (frontend sends back masked values for unchanged fields)
const merged: Record<string, unknown> = { ...current, ...patch }
for (const key of Object.keys(merged)) {
const val = merged[key]
if (typeof val === 'string' && val.startsWith('****')) {
merged[key] = current[key]
}
}
const finalConfig = { ...existing, feishu: merged }
fs.writeFileSync(filePath, JSON.stringify(finalConfig, null, 2) + '\n', { mode: 0o600 })
}
export function clearFeishuConfig(): void {
try {
const filePath = getConfigPath()
const raw = fs.readFileSync(filePath, 'utf-8')
const config = JSON.parse(raw) as Record<string, unknown>
delete config.feishu
fs.writeFileSync(filePath, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 })
} catch {
// ignore
}
}
export function maskFeishuAppId(appId?: string): string {
if (!appId) return '未配置'
if (appId.length <= 6) return '****'
return appId.slice(0, 3) + '****' + appId.slice(-3)
}
export function maskFeishuAppSecret(secret?: string): string {
return maskSecret(secret) ?? '未配置'
}
export function isFeishuConfigured(): boolean {
const config = getFeishuConfig()
return Boolean(config.appId && config.appSecret)
} |