Files
goodbuddy/src/main/channels/channel-env.ts
T

231 lines
6.0 KiB
TypeScript

import type { ChannelInboundText } from '../../shared/channel-contracts'
import type { ChannelExecutor } from './channel-driver'
import { ChannelService } from './channel-service'
import {
DingTalkChannelDriver,
type DingTalkChannelDriverOptions
} from './dingtalk-channel-driver'
import {
normalizeDingTalkStaffId,
type DingTalkTransportFactory
} from './dingtalk-driver'
import {
WeComChannelDriver,
type WeComChannelDriverOptions
} from './wecom-channel-driver'
import type { WeComTransportFactory } from './wecom-driver'
type ChannelEnvironmentConfig =
| {
channel: 'dingtalk'
clientId: string
clientSecret: string
allowedSenderIds: readonly string[]
allowGroupMessages: boolean
}
| {
channel: 'wecom'
botId: string
secret: string
allowedSenderIds: readonly string[]
allowGroupMessages: boolean
}
export type EnvironmentChannelService = Pick<
ChannelService,
'start' | 'stop'
>
export type EnvironmentChannelBootstrapOptions = {
executor: ChannelExecutor
env?: NodeJS.ProcessEnv
dingtalkTransportFactory?: DingTalkTransportFactory
wecomTransportFactory?: WeComTransportFactory
createDingTalkDriver?: (
options: DingTalkChannelDriverOptions
) => DingTalkChannelDriver
createWeComDriver?: (
options: WeComChannelDriverOptions
) => WeComChannelDriver
createService?: (
driver: DingTalkChannelDriver | WeComChannelDriver,
executor: ChannelExecutor,
options: {
allowedSenderIds: readonly string[]
allowGroupMessages: boolean
}
) => EnvironmentChannelService
onStartError?: (channel: string, error: string) => void
}
function optionalCredential(
env: NodeJS.ProcessEnv,
name: string
): string | undefined {
const value = env[name]
if (value === undefined || value.trim() === '') {
return undefined
}
return value.trim()
}
function parseBoolean(
env: NodeJS.ProcessEnv,
name: string
): boolean {
const raw = env[name]
if (raw === undefined || raw === '') {
return false
}
if (raw === 'true') {
return true
}
if (raw === 'false') {
return false
}
throw new Error(`${name} 必须是 true 或 false`)
}
function parseList(
env: NodeJS.ProcessEnv,
name: string
): readonly string[] {
const raw = env[name]
if (raw === undefined || raw === '') {
return []
}
const values = raw.split(',').map((value) => value.trim())
if (values.some((value) => value === '')) {
throw new Error(`${name} 包含空白身份`)
}
return [...new Set(values)]
}
export function parseChannelEnvironment(
env: NodeJS.ProcessEnv
): readonly ChannelEnvironmentConfig[] {
const configs: ChannelEnvironmentConfig[] = []
const dingTalkClientId = optionalCredential(
env,
'GOODBUDDY_DINGTALK_CLIENT_ID'
)
const dingTalkClientSecret = optionalCredential(
env,
'GOODBUDDY_DINGTALK_CLIENT_SECRET'
)
const dingTalkAllowedSenderIds = parseList(
env,
'GOODBUDDY_DINGTALK_ALLOWED_SENDERS'
).map(normalizeDingTalkStaffId)
const dingTalkAllowGroupMessages = parseBoolean(
env,
'GOODBUDDY_DINGTALK_ALLOW_GROUPS'
)
if (
dingTalkClientId &&
dingTalkClientSecret &&
dingTalkAllowedSenderIds.length > 0
) {
configs.push({
channel: 'dingtalk',
clientId: dingTalkClientId,
clientSecret: dingTalkClientSecret,
allowedSenderIds: dingTalkAllowedSenderIds,
allowGroupMessages: dingTalkAllowGroupMessages
})
}
const weComBotId = optionalCredential(
env,
'GOODBUDDY_WECOM_BOT_ID'
)
const weComSecret = optionalCredential(
env,
'GOODBUDDY_WECOM_SECRET'
)
const weComAllowedSenderIds = parseList(
env,
'GOODBUDDY_WECOM_ALLOWED_SENDERS'
)
const weComAllowGroupMessages = parseBoolean(
env,
'GOODBUDDY_WECOM_ALLOW_GROUPS'
)
if (
weComBotId &&
weComSecret &&
weComAllowedSenderIds.length > 0
) {
configs.push({
channel: 'wecom',
botId: weComBotId,
secret: weComSecret,
allowedSenderIds: weComAllowedSenderIds,
allowGroupMessages: weComAllowGroupMessages
})
}
return configs
}
export function startEnvironmentChannels(
options: EnvironmentChannelBootstrapOptions
): readonly EnvironmentChannelService[] {
let configs: readonly ChannelEnvironmentConfig[]
try {
configs = parseChannelEnvironment(options.env ?? process.env)
} catch {
options.onStartError?.('environment', '通道环境变量配置无效')
return []
}
const services = configs.map((config) => {
const driver =
config.channel === 'dingtalk'
? (options.createDingTalkDriver ??
((driverOptions) =>
new DingTalkChannelDriver(driverOptions)))({
clientId: config.clientId,
clientSecret: config.clientSecret,
allowedSenderIds: config.allowedSenderIds,
...(options.dingtalkTransportFactory
? {
transportFactory:
options.dingtalkTransportFactory
}
: {})
})
: (options.createWeComDriver ??
((driverOptions) =>
new WeComChannelDriver(driverOptions)))({
botId: config.botId,
secret: config.secret,
...(options.wecomTransportFactory
? { transportFactory: options.wecomTransportFactory }
: {})
})
const service = (
options.createService ??
((channelDriver, executor, serviceOptions) =>
new ChannelService(channelDriver, executor, serviceOptions))
)(driver, options.executor, {
allowedSenderIds: config.allowedSenderIds,
allowGroupMessages: config.allowGroupMessages
})
void Promise.resolve()
.then(() => service.start())
.catch(() => {
options.onStartError?.(
config.channel,
`${config.channel} 通道启动失败`
)
})
return service
})
return services
}
export function isReadOnlyChannelMessage(
message: ChannelInboundText
): boolean {
return message.workMode === 'ask'
}