231 lines
6.0 KiB
TypeScript
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'
|
|
}
|