mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-29 20:30:00 +08:00
feat(hermes): add bundled plugin channel configs
This commit is contained in:
@@ -3285,7 +3285,18 @@ export function buildMessagingPlatformFormValues(platform, saved = {}, options =
|
||||
return form
|
||||
}
|
||||
|
||||
const HERMES_CHANNEL_PLATFORMS = ['telegram', 'discord', 'slack', 'feishu', 'dingtalk']
|
||||
const HERMES_CHANNEL_PLATFORMS = [
|
||||
'telegram',
|
||||
'discord',
|
||||
'slack',
|
||||
'feishu',
|
||||
'dingtalk',
|
||||
'teams',
|
||||
'google_chat',
|
||||
'irc',
|
||||
'line',
|
||||
'simplex',
|
||||
]
|
||||
|
||||
function normalizeHermesPlatform(platform) {
|
||||
const p = String(platform || '').trim().toLowerCase()
|
||||
@@ -3301,6 +3312,11 @@ function putHermesString(form, source, key) {
|
||||
if (typeof value === 'string') form[toCamelCaseKey(key)] = value
|
||||
}
|
||||
|
||||
function putHermesScalarString(form, source, key) {
|
||||
const value = source?.[key]
|
||||
if (typeof value === 'string' || typeof value === 'number') form[toCamelCaseKey(key)] = String(value)
|
||||
}
|
||||
|
||||
function putHermesBool(form, source, key) {
|
||||
const value = source?.[key]
|
||||
if (typeof value === 'boolean') form[toCamelCaseKey(key)] = value
|
||||
@@ -3332,6 +3348,13 @@ function putHermesEnvBool(form, envValues, envKey, formKey) {
|
||||
if (value !== undefined) form[formKey] = value
|
||||
}
|
||||
|
||||
function putHermesHomeChannel(form, entry) {
|
||||
const home = entry?.home_channel && typeof entry.home_channel === 'object' ? entry.home_channel : null
|
||||
if (!home) return
|
||||
if (typeof home.chat_id === 'string') form.homeChannel = home.chat_id
|
||||
if (typeof home.name === 'string') form.homeChannelName = home.name
|
||||
}
|
||||
|
||||
function readHermesEnvValues() {
|
||||
const envPath = path.join(hermesHome(), '.env')
|
||||
const values = {}
|
||||
@@ -3424,6 +3447,79 @@ export function buildHermesChannelConfigValues(config = {}, envValues = {}) {
|
||||
putHermesString(form, extra, 'client_secret')
|
||||
form.clientId = hermesEnvValue(envValues, 'DINGTALK_CLIENT_ID') || form.clientId || ''
|
||||
form.clientSecret = hermesEnvValue(envValues, 'DINGTALK_CLIENT_SECRET') || form.clientSecret || ''
|
||||
} else if (platform === 'teams') {
|
||||
for (const key of ['client_id', 'client_secret', 'tenant_id', 'service_url']) putHermesString(form, extra, key)
|
||||
putHermesScalarString(form, extra, 'port')
|
||||
putHermesHomeChannel(form, entry)
|
||||
form.clientId = hermesEnvValue(envValues, 'TEAMS_CLIENT_ID') || form.clientId || ''
|
||||
form.clientSecret = hermesEnvValue(envValues, 'TEAMS_CLIENT_SECRET') || form.clientSecret || ''
|
||||
form.tenantId = hermesEnvValue(envValues, 'TEAMS_TENANT_ID') || form.tenantId || ''
|
||||
form.port = hermesEnvValue(envValues, 'TEAMS_PORT') || form.port || ''
|
||||
form.serviceUrl = hermesEnvValue(envValues, 'TEAMS_SERVICE_URL') || form.serviceUrl || ''
|
||||
putHermesEnvString(form, envValues, 'TEAMS_ALLOWED_USERS', 'allowFrom')
|
||||
putHermesEnvBool(form, envValues, 'TEAMS_ALLOW_ALL_USERS', 'allowAllUsers')
|
||||
putHermesEnvString(form, envValues, 'TEAMS_HOME_CHANNEL', 'homeChannel')
|
||||
putHermesEnvString(form, envValues, 'TEAMS_HOME_CHANNEL_NAME', 'homeChannelName')
|
||||
} else if (platform === 'google_chat') {
|
||||
for (const key of ['project_id', 'subscription_name', 'service_account_json']) putHermesString(form, extra, key)
|
||||
putHermesHomeChannel(form, entry)
|
||||
form.projectId = hermesEnvValue(envValues, 'GOOGLE_CHAT_PROJECT_ID') || hermesEnvValue(envValues, 'GOOGLE_CLOUD_PROJECT') || form.projectId || ''
|
||||
form.subscriptionName = hermesEnvValue(envValues, 'GOOGLE_CHAT_SUBSCRIPTION_NAME') || hermesEnvValue(envValues, 'GOOGLE_CHAT_SUBSCRIPTION') || form.subscriptionName || ''
|
||||
form.serviceAccountJson = hermesEnvValue(envValues, 'GOOGLE_CHAT_SERVICE_ACCOUNT_JSON') || hermesEnvValue(envValues, 'GOOGLE_APPLICATION_CREDENTIALS') || form.serviceAccountJson || ''
|
||||
putHermesEnvString(form, envValues, 'GOOGLE_CHAT_ALLOWED_USERS', 'allowFrom')
|
||||
putHermesEnvBool(form, envValues, 'GOOGLE_CHAT_ALLOW_ALL_USERS', 'allowAllUsers')
|
||||
putHermesEnvString(form, envValues, 'GOOGLE_CHAT_HOME_CHANNEL', 'homeChannel')
|
||||
putHermesEnvString(form, envValues, 'GOOGLE_CHAT_HOME_CHANNEL_NAME', 'homeChannelName')
|
||||
} else if (platform === 'irc') {
|
||||
for (const key of ['server', 'channel', 'nickname', 'server_password', 'nickserv_password']) putHermesString(form, extra, key)
|
||||
putHermesScalarString(form, extra, 'port')
|
||||
putHermesBool(form, extra, 'use_tls')
|
||||
putHermesCsv(form, extra, 'allowed_users')
|
||||
if (form.allowedUsers && !form.allowFrom) form.allowFrom = form.allowedUsers
|
||||
delete form.allowedUsers
|
||||
putHermesHomeChannel(form, entry)
|
||||
form.server = hermesEnvValue(envValues, 'IRC_SERVER') || form.server || ''
|
||||
form.channel = hermesEnvValue(envValues, 'IRC_CHANNEL') || form.channel || ''
|
||||
form.nickname = hermesEnvValue(envValues, 'IRC_NICKNAME') || form.nickname || ''
|
||||
form.port = hermesEnvValue(envValues, 'IRC_PORT') || form.port || ''
|
||||
putHermesEnvBool(form, envValues, 'IRC_USE_TLS', 'useTls')
|
||||
form.serverPassword = hermesEnvValue(envValues, 'IRC_SERVER_PASSWORD') || form.serverPassword || ''
|
||||
form.nickservPassword = hermesEnvValue(envValues, 'IRC_NICKSERV_PASSWORD') || form.nickservPassword || ''
|
||||
putHermesEnvString(form, envValues, 'IRC_ALLOWED_USERS', 'allowFrom')
|
||||
putHermesEnvBool(form, envValues, 'IRC_ALLOW_ALL_USERS', 'allowAllUsers')
|
||||
putHermesEnvString(form, envValues, 'IRC_HOME_CHANNEL', 'homeChannel')
|
||||
putHermesEnvString(form, envValues, 'IRC_HOME_CHANNEL_NAME', 'homeChannelName')
|
||||
} else if (platform === 'line') {
|
||||
for (const key of ['channel_access_token', 'channel_secret', 'host', 'public_url', 'slow_response_threshold']) putHermesString(form, extra, key)
|
||||
putHermesScalarString(form, extra, 'port')
|
||||
putHermesCsv(form, extra, 'allowed_users')
|
||||
if (form.allowedUsers && !form.allowFrom) form.allowFrom = form.allowedUsers
|
||||
delete form.allowedUsers
|
||||
putHermesCsv(form, extra, 'allowed_groups')
|
||||
putHermesCsv(form, extra, 'allowed_rooms')
|
||||
putHermesHomeChannel(form, entry)
|
||||
form.channelAccessToken = hermesEnvValue(envValues, 'LINE_CHANNEL_ACCESS_TOKEN') || form.channelAccessToken || ''
|
||||
form.channelSecret = hermesEnvValue(envValues, 'LINE_CHANNEL_SECRET') || form.channelSecret || ''
|
||||
form.port = hermesEnvValue(envValues, 'LINE_PORT') || form.port || ''
|
||||
form.host = hermesEnvValue(envValues, 'LINE_HOST') || form.host || ''
|
||||
form.publicUrl = hermesEnvValue(envValues, 'LINE_PUBLIC_URL') || form.publicUrl || ''
|
||||
putHermesEnvString(form, envValues, 'LINE_ALLOWED_USERS', 'allowFrom')
|
||||
putHermesEnvString(form, envValues, 'LINE_ALLOWED_GROUPS', 'allowedGroups')
|
||||
putHermesEnvString(form, envValues, 'LINE_ALLOWED_ROOMS', 'allowedRooms')
|
||||
putHermesEnvBool(form, envValues, 'LINE_ALLOW_ALL_USERS', 'allowAllUsers')
|
||||
putHermesEnvString(form, envValues, 'LINE_HOME_CHANNEL', 'homeChannel')
|
||||
form.slowResponseThreshold = hermesEnvValue(envValues, 'LINE_SLOW_RESPONSE_THRESHOLD') || form.slowResponseThreshold || ''
|
||||
} else if (platform === 'simplex') {
|
||||
putHermesString(form, extra, 'ws_url')
|
||||
putHermesCsv(form, extra, 'allowed_users')
|
||||
if (form.allowedUsers && !form.allowFrom) form.allowFrom = form.allowedUsers
|
||||
delete form.allowedUsers
|
||||
putHermesHomeChannel(form, entry)
|
||||
form.wsUrl = hermesEnvValue(envValues, 'SIMPLEX_WS_URL') || form.wsUrl || ''
|
||||
putHermesEnvString(form, envValues, 'SIMPLEX_ALLOWED_USERS', 'allowFrom')
|
||||
putHermesEnvBool(form, envValues, 'SIMPLEX_ALLOW_ALL_USERS', 'allowAllUsers')
|
||||
putHermesEnvString(form, envValues, 'SIMPLEX_HOME_CHANNEL', 'homeChannel')
|
||||
putHermesEnvString(form, envValues, 'SIMPLEX_HOME_CHANNEL_NAME', 'homeChannelName')
|
||||
}
|
||||
putHermesString(form, extra, 'dm_policy')
|
||||
putHermesString(form, extra, 'group_policy')
|
||||
@@ -3450,6 +3546,26 @@ function setHermesExtra(entry, key, value) {
|
||||
entry.extra[key] = value
|
||||
}
|
||||
|
||||
function setHermesExtraInteger(entry, key, value) {
|
||||
const raw = String(value ?? '').trim()
|
||||
if (!raw) return
|
||||
const parsed = Number.parseInt(raw, 10)
|
||||
if (Number.isFinite(parsed)) setHermesExtra(entry, key, parsed)
|
||||
}
|
||||
|
||||
function setHermesHomeChannel(entry, form = {}) {
|
||||
if (!Object.hasOwn(form, 'homeChannel')) return
|
||||
const chatId = String(form.homeChannel || '').trim()
|
||||
if (!chatId) {
|
||||
deleteHermesEntryKey(entry, 'home_channel')
|
||||
return
|
||||
}
|
||||
entry.home_channel = {
|
||||
chat_id: chatId,
|
||||
name: String(form.homeChannelName || '').trim() || chatId,
|
||||
}
|
||||
}
|
||||
|
||||
function deleteHermesEntryKey(entry, key) {
|
||||
if (entry && typeof entry === 'object') delete entry[key]
|
||||
}
|
||||
@@ -3468,6 +3584,9 @@ function normalizeHermesChannelForm(platform, form = {}) {
|
||||
if (Object.hasOwn(normalized, 'requireMention')) {
|
||||
normalized.requireMention = normalized.requireMention === true || normalized.requireMention === 'true' || normalized.requireMention === 'on'
|
||||
}
|
||||
if (Object.hasOwn(normalized, 'allowAllUsers')) {
|
||||
normalized.allowAllUsers = normalized.allowAllUsers === true || normalized.allowAllUsers === 'true' || normalized.allowAllUsers === 'on'
|
||||
}
|
||||
if (platform === 'feishu') {
|
||||
normalized.domain = String(normalized.domain || '').trim() || 'feishu'
|
||||
normalized.connectionMode = String(normalized.connectionMode || '').trim() || 'websocket'
|
||||
@@ -3489,6 +3608,14 @@ function normalizeHermesChannelForm(platform, form = {}) {
|
||||
normalized.historyBackfillLimit = String(normalized.historyBackfillLimit || '').trim()
|
||||
normalized.replyToMode = String(normalized.replyToMode || '').trim()
|
||||
}
|
||||
if (platform === 'irc') {
|
||||
if (Object.hasOwn(normalized, 'useTls')) normalized.useTls = normalized.useTls === true || normalized.useTls === 'true' || normalized.useTls === 'on'
|
||||
}
|
||||
if (platform === 'line') {
|
||||
for (const key of ['allowedGroups', 'allowedRooms']) {
|
||||
if (Object.hasOwn(normalized, key)) normalized[key] = csvToStringArray(normalized[key])
|
||||
}
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
@@ -3544,6 +3671,40 @@ export function mergeHermesChannelConfig(config = {}, platform, form = {}) {
|
||||
deleteHermesExtraKey(entry, 'client_secret')
|
||||
deleteHermesExtraKey(entry, 'allow_from')
|
||||
deleteHermesExtraKey(entry, 'group_allow_from')
|
||||
} else if (normalizedPlatform === 'teams') {
|
||||
deleteHermesExtraKey(entry, 'client_id')
|
||||
deleteHermesExtraKey(entry, 'client_secret')
|
||||
deleteHermesExtraKey(entry, 'tenant_id')
|
||||
setHermesExtraInteger(entry, 'port', normalized.port)
|
||||
setHermesExtra(entry, 'service_url', String(normalized.serviceUrl || '').trim())
|
||||
setHermesHomeChannel(entry, normalized)
|
||||
} else if (normalizedPlatform === 'google_chat') {
|
||||
setHermesExtra(entry, 'project_id', String(normalized.projectId || '').trim())
|
||||
setHermesExtra(entry, 'subscription_name', String(normalized.subscriptionName || '').trim())
|
||||
deleteHermesExtraKey(entry, 'service_account_json')
|
||||
setHermesHomeChannel(entry, normalized)
|
||||
} else if (normalizedPlatform === 'irc') {
|
||||
setHermesExtra(entry, 'server', String(normalized.server || '').trim())
|
||||
setHermesExtraInteger(entry, 'port', normalized.port)
|
||||
setHermesExtra(entry, 'nickname', String(normalized.nickname || '').trim())
|
||||
setHermesExtra(entry, 'channel', String(normalized.channel || '').trim())
|
||||
if (Object.hasOwn(normalized, 'useTls')) setHermesExtra(entry, 'use_tls', !!normalized.useTls)
|
||||
deleteHermesExtraKey(entry, 'server_password')
|
||||
deleteHermesExtraKey(entry, 'nickserv_password')
|
||||
setHermesHomeChannel(entry, normalized)
|
||||
} else if (normalizedPlatform === 'line') {
|
||||
deleteHermesExtraKey(entry, 'channel_access_token')
|
||||
deleteHermesExtraKey(entry, 'channel_secret')
|
||||
setHermesExtraInteger(entry, 'port', normalized.port)
|
||||
setHermesExtra(entry, 'host', String(normalized.host || '').trim())
|
||||
setHermesExtra(entry, 'public_url', String(normalized.publicUrl || '').trim())
|
||||
if (Array.isArray(normalized.allowedGroups)) setHermesExtra(entry, 'allowed_groups', normalized.allowedGroups)
|
||||
if (Array.isArray(normalized.allowedRooms)) setHermesExtra(entry, 'allowed_rooms', normalized.allowedRooms)
|
||||
setHermesExtra(entry, 'slow_response_threshold', String(normalized.slowResponseThreshold || '').trim())
|
||||
setHermesHomeChannel(entry, normalized)
|
||||
} else if (normalizedPlatform === 'simplex') {
|
||||
setHermesExtra(entry, 'ws_url', String(normalized.wsUrl || '').trim())
|
||||
setHermesHomeChannel(entry, normalized)
|
||||
}
|
||||
if (Object.hasOwn(normalized, 'dmPolicy')) setHermesExtra(entry, 'dm_policy', normalized.dmPolicy)
|
||||
if (Object.hasOwn(normalized, 'groupPolicy')) {
|
||||
@@ -3552,7 +3713,8 @@ export function mergeHermesChannelConfig(config = {}, platform, form = {}) {
|
||||
}
|
||||
if (Object.hasOwn(normalized, 'requireMention')) setHermesExtra(entry, 'require_mention', !!normalized.requireMention)
|
||||
if (Array.isArray(normalized.allowFrom)) {
|
||||
setHermesExtra(entry, normalizedPlatform === 'dingtalk' ? 'allowed_users' : 'allow_from', normalized.allowFrom)
|
||||
const allowKey = ['dingtalk', 'irc', 'line', 'simplex'].includes(normalizedPlatform) ? 'allowed_users' : 'allow_from'
|
||||
setHermesExtra(entry, allowKey, normalized.allowFrom)
|
||||
}
|
||||
if (Array.isArray(normalized.groupAllowFrom)) {
|
||||
setHermesExtra(entry, normalizedPlatform === 'dingtalk' ? 'allowed_chats' : 'group_allow_from', normalized.groupAllowFrom)
|
||||
@@ -3666,6 +3828,54 @@ export function buildHermesChannelEnvUpdates(platform, form = {}) {
|
||||
updates.DINGTALK_ALLOWED_USERS = csvEnvValue(form.allowFrom)
|
||||
updates.DINGTALK_ALLOWED_CHATS = csvEnvValue(form.groupAllowFrom)
|
||||
if (Object.hasOwn(form, 'requireMention')) updates.DINGTALK_REQUIRE_MENTION = boolEnvValue(form.requireMention)
|
||||
} else if (platform === 'teams') {
|
||||
updates.TEAMS_CLIENT_ID = String(form.clientId || '').trim()
|
||||
updates.TEAMS_CLIENT_SECRET = String(form.clientSecret || '').trim()
|
||||
updates.TEAMS_TENANT_ID = String(form.tenantId || '').trim()
|
||||
updates.TEAMS_PORT = String(form.port || '').trim()
|
||||
updates.TEAMS_SERVICE_URL = String(form.serviceUrl || '').trim()
|
||||
updates.TEAMS_ALLOWED_USERS = csvEnvValue(form.allowFrom)
|
||||
if (Object.hasOwn(form, 'allowAllUsers')) updates.TEAMS_ALLOW_ALL_USERS = boolEnvValue(form.allowAllUsers)
|
||||
updates.TEAMS_HOME_CHANNEL = String(form.homeChannel || '').trim()
|
||||
updates.TEAMS_HOME_CHANNEL_NAME = String(form.homeChannelName || '').trim()
|
||||
} else if (platform === 'google_chat') {
|
||||
updates.GOOGLE_CHAT_PROJECT_ID = String(form.projectId || '').trim()
|
||||
updates.GOOGLE_CHAT_SUBSCRIPTION_NAME = String(form.subscriptionName || '').trim()
|
||||
updates.GOOGLE_CHAT_SERVICE_ACCOUNT_JSON = String(form.serviceAccountJson || '').trim()
|
||||
updates.GOOGLE_CHAT_ALLOWED_USERS = csvEnvValue(form.allowFrom)
|
||||
if (Object.hasOwn(form, 'allowAllUsers')) updates.GOOGLE_CHAT_ALLOW_ALL_USERS = boolEnvValue(form.allowAllUsers)
|
||||
updates.GOOGLE_CHAT_HOME_CHANNEL = String(form.homeChannel || '').trim()
|
||||
updates.GOOGLE_CHAT_HOME_CHANNEL_NAME = String(form.homeChannelName || '').trim()
|
||||
} else if (platform === 'irc') {
|
||||
updates.IRC_SERVER = String(form.server || '').trim()
|
||||
updates.IRC_PORT = String(form.port || '').trim()
|
||||
updates.IRC_NICKNAME = String(form.nickname || '').trim()
|
||||
updates.IRC_CHANNEL = String(form.channel || '').trim()
|
||||
if (Object.hasOwn(form, 'useTls')) updates.IRC_USE_TLS = boolEnvValue(form.useTls)
|
||||
updates.IRC_SERVER_PASSWORD = String(form.serverPassword || '').trim()
|
||||
updates.IRC_NICKSERV_PASSWORD = String(form.nickservPassword || '').trim()
|
||||
updates.IRC_ALLOWED_USERS = csvEnvValue(form.allowFrom)
|
||||
if (Object.hasOwn(form, 'allowAllUsers')) updates.IRC_ALLOW_ALL_USERS = boolEnvValue(form.allowAllUsers)
|
||||
updates.IRC_HOME_CHANNEL = String(form.homeChannel || '').trim()
|
||||
updates.IRC_HOME_CHANNEL_NAME = String(form.homeChannelName || '').trim()
|
||||
} else if (platform === 'line') {
|
||||
updates.LINE_CHANNEL_ACCESS_TOKEN = String(form.channelAccessToken || '').trim()
|
||||
updates.LINE_CHANNEL_SECRET = String(form.channelSecret || '').trim()
|
||||
updates.LINE_PORT = String(form.port || '').trim()
|
||||
updates.LINE_HOST = String(form.host || '').trim()
|
||||
updates.LINE_PUBLIC_URL = String(form.publicUrl || '').trim()
|
||||
updates.LINE_ALLOWED_USERS = csvEnvValue(form.allowFrom)
|
||||
updates.LINE_ALLOWED_GROUPS = csvEnvValue(form.allowedGroups)
|
||||
updates.LINE_ALLOWED_ROOMS = csvEnvValue(form.allowedRooms)
|
||||
if (Object.hasOwn(form, 'allowAllUsers')) updates.LINE_ALLOW_ALL_USERS = boolEnvValue(form.allowAllUsers)
|
||||
updates.LINE_HOME_CHANNEL = String(form.homeChannel || '').trim()
|
||||
updates.LINE_SLOW_RESPONSE_THRESHOLD = String(form.slowResponseThreshold || '').trim()
|
||||
} else if (platform === 'simplex') {
|
||||
updates.SIMPLEX_WS_URL = String(form.wsUrl || '').trim()
|
||||
updates.SIMPLEX_ALLOWED_USERS = csvEnvValue(form.allowFrom)
|
||||
if (Object.hasOwn(form, 'allowAllUsers')) updates.SIMPLEX_ALLOW_ALL_USERS = boolEnvValue(form.allowAllUsers)
|
||||
updates.SIMPLEX_HOME_CHANNEL = String(form.homeChannel || '').trim()
|
||||
updates.SIMPLEX_HOME_CHANNEL_NAME = String(form.homeChannelName || '').trim()
|
||||
}
|
||||
return updates
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -869,7 +869,7 @@ fn put_number_value_if_present(entry: &mut Map<String, Value>, key: &str, value:
|
||||
}
|
||||
return;
|
||||
}
|
||||
put_number_from_form(entry, key, &value.and_then(|v| v.as_str()).unwrap_or(""));
|
||||
put_number_from_form(entry, key, value.and_then(|v| v.as_str()).unwrap_or(""));
|
||||
}
|
||||
|
||||
fn normalize_numeric_form_value(map: &mut Map<String, Value>, key: &str) {
|
||||
|
||||
@@ -87,11 +87,112 @@ const CHANNELS = [
|
||||
{ key: 'clientSecret', labelKey: 'engine.hermesChannelDingTalkClientSecret', type: 'password', placeholder: 'client secret' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'teams',
|
||||
icon: 'users',
|
||||
titleKey: 'engine.hermesChannelTeams',
|
||||
descKey: 'engine.hermesChannelTeamsDesc',
|
||||
secretFields: ['clientId', 'clientSecret', 'tenantId'],
|
||||
fields: [
|
||||
{ key: 'clientId', labelKey: 'engine.hermesChannelTeamsClientId', type: 'text', placeholder: '00000000-0000-0000-0000-000000000000' },
|
||||
{ key: 'clientSecret', labelKey: 'engine.hermesChannelTeamsClientSecret', type: 'password', placeholder: 'client secret' },
|
||||
{ key: 'tenantId', labelKey: 'engine.hermesChannelTeamsTenantId', type: 'text', placeholder: '00000000-0000-0000-0000-000000000000' },
|
||||
{ key: 'port', labelKey: 'engine.hermesChannelPort', type: 'number', placeholder: '3978' },
|
||||
{ key: 'serviceUrl', labelKey: 'engine.hermesChannelServiceUrl', type: 'url', placeholder: 'https://smba.trafficmanager.net/teams/' },
|
||||
],
|
||||
policyFields: [
|
||||
{ key: 'allowFrom', labelKey: 'engine.hermesChannelAllowedUsers', type: 'textarea', placeholderKey: 'engine.hermesChannelTeamsAllowedUsersPh' },
|
||||
{ key: 'allowAllUsers', labelKey: 'engine.hermesChannelAllowAllUsers', type: 'checkbox' },
|
||||
{ key: 'homeChannel', labelKey: 'engine.hermesChannelHomeChannel', type: 'text', placeholder: '19:xxx@thread.tacv2' },
|
||||
{ key: 'homeChannelName', labelKey: 'engine.hermesChannelHomeChannelName', type: 'text', placeholder: 'ops' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'google_chat',
|
||||
icon: 'message-square',
|
||||
titleKey: 'engine.hermesChannelGoogleChat',
|
||||
descKey: 'engine.hermesChannelGoogleChatDesc',
|
||||
secretFields: ['projectId', 'serviceAccountJson'],
|
||||
fields: [
|
||||
{ key: 'projectId', labelKey: 'engine.hermesChannelGoogleProjectId', type: 'text', placeholder: 'my-gcp-project' },
|
||||
{ key: 'subscriptionName', labelKey: 'engine.hermesChannelGoogleSubscriptionName', type: 'text', placeholder: 'projects/my-gcp-project/subscriptions/hermes' },
|
||||
{ key: 'serviceAccountJson', labelKey: 'engine.hermesChannelGoogleServiceAccount', type: 'password', placeholderKey: 'engine.hermesChannelGoogleServiceAccountPh' },
|
||||
],
|
||||
policyFields: [
|
||||
{ key: 'allowFrom', labelKey: 'engine.hermesChannelAllowedUsers', type: 'textarea', placeholderKey: 'engine.hermesChannelGoogleAllowedUsersPh' },
|
||||
{ key: 'allowAllUsers', labelKey: 'engine.hermesChannelAllowAllUsers', type: 'checkbox' },
|
||||
{ key: 'homeChannel', labelKey: 'engine.hermesChannelHomeChannel', type: 'text', placeholder: 'spaces/AAAA...' },
|
||||
{ key: 'homeChannelName', labelKey: 'engine.hermesChannelHomeChannelName', type: 'text', placeholder: 'ops-space' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'irc',
|
||||
icon: 'hash',
|
||||
titleKey: 'engine.hermesChannelIrc',
|
||||
descKey: 'engine.hermesChannelIrcDesc',
|
||||
secretFields: ['server', 'serverPassword', 'nickservPassword'],
|
||||
fields: [
|
||||
{ key: 'server', labelKey: 'engine.hermesChannelIrcServer', type: 'text', placeholder: 'irc.libera.chat' },
|
||||
{ key: 'port', labelKey: 'engine.hermesChannelPort', type: 'number', placeholder: '6697' },
|
||||
{ key: 'nickname', labelKey: 'engine.hermesChannelIrcNickname', type: 'text', placeholder: 'hermes-bot' },
|
||||
{ key: 'channel', labelKey: 'engine.hermesChannelIrcChannel', type: 'text', placeholder: '#hermes' },
|
||||
{ key: 'serverPassword', labelKey: 'engine.hermesChannelIrcServerPassword', type: 'password', placeholder: 'optional' },
|
||||
{ key: 'nickservPassword', labelKey: 'engine.hermesChannelIrcNickservPassword', type: 'password', placeholder: 'optional' },
|
||||
],
|
||||
toggles: [
|
||||
{ key: 'useTls', labelKey: 'engine.hermesChannelIrcUseTls' },
|
||||
],
|
||||
policyFields: [
|
||||
{ key: 'allowFrom', labelKey: 'engine.hermesChannelAllowedUsers', type: 'textarea', placeholderKey: 'engine.hermesChannelIrcAllowedUsersPh' },
|
||||
{ key: 'allowAllUsers', labelKey: 'engine.hermesChannelAllowAllUsers', type: 'checkbox' },
|
||||
{ key: 'homeChannel', labelKey: 'engine.hermesChannelHomeChannel', type: 'text', placeholder: '#reports' },
|
||||
{ key: 'homeChannelName', labelKey: 'engine.hermesChannelHomeChannelName', type: 'text', placeholder: 'reports' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'line',
|
||||
icon: 'message-circle',
|
||||
titleKey: 'engine.hermesChannelLine',
|
||||
descKey: 'engine.hermesChannelLineDesc',
|
||||
secretFields: ['channelAccessToken', 'channelSecret'],
|
||||
fields: [
|
||||
{ key: 'channelAccessToken', labelKey: 'engine.hermesChannelLineAccessToken', type: 'password', placeholder: 'LINE channel access token' },
|
||||
{ key: 'channelSecret', labelKey: 'engine.hermesChannelLineSecret', type: 'password', placeholder: 'LINE channel secret' },
|
||||
{ key: 'port', labelKey: 'engine.hermesChannelPort', type: 'number', placeholder: '8646' },
|
||||
{ key: 'host', labelKey: 'engine.hermesChannelHost', type: 'text', placeholder: '0.0.0.0' },
|
||||
{ key: 'publicUrl', labelKey: 'engine.hermesChannelPublicUrl', type: 'url', placeholder: 'https://line.example.com' },
|
||||
],
|
||||
policyFields: [
|
||||
{ key: 'allowFrom', labelKey: 'engine.hermesChannelAllowedUsers', type: 'textarea', placeholderKey: 'engine.hermesChannelLineAllowedUsersPh' },
|
||||
{ key: 'allowedGroups', labelKey: 'engine.hermesChannelLineAllowedGroups', type: 'textarea', placeholderKey: 'engine.hermesChannelLineAllowedGroupsPh' },
|
||||
{ key: 'allowedRooms', labelKey: 'engine.hermesChannelLineAllowedRooms', type: 'textarea', placeholderKey: 'engine.hermesChannelLineAllowedRoomsPh' },
|
||||
{ key: 'allowAllUsers', labelKey: 'engine.hermesChannelAllowAllUsers', type: 'checkbox' },
|
||||
{ key: 'homeChannel', labelKey: 'engine.hermesChannelHomeChannel', type: 'text', placeholder: 'Uxxxxxxxx' },
|
||||
{ key: 'slowResponseThreshold', labelKey: 'engine.hermesChannelLineSlowResponse', type: 'number', placeholder: '45' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'simplex',
|
||||
icon: 'radio',
|
||||
titleKey: 'engine.hermesChannelSimpleX',
|
||||
descKey: 'engine.hermesChannelSimpleXDesc',
|
||||
secretFields: ['wsUrl'],
|
||||
fields: [
|
||||
{ key: 'wsUrl', labelKey: 'engine.hermesChannelSimpleXWsUrl', type: 'url', placeholder: 'ws://127.0.0.1:5225' },
|
||||
],
|
||||
policyFields: [
|
||||
{ key: 'allowFrom', labelKey: 'engine.hermesChannelAllowedUsers', type: 'textarea', placeholderKey: 'engine.hermesChannelSimpleXAllowedUsersPh' },
|
||||
{ key: 'allowAllUsers', labelKey: 'engine.hermesChannelAllowAllUsers', type: 'checkbox' },
|
||||
{ key: 'homeChannel', labelKey: 'engine.hermesChannelHomeChannel', type: 'text', placeholder: 'group:ops' },
|
||||
{ key: 'homeChannelName', labelKey: 'engine.hermesChannelHomeChannelName', type: 'text', placeholder: 'Ops' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const COMMON_FIELDS = [
|
||||
const LEGACY_POLICY_FIELDS = [
|
||||
{ key: 'dmPolicy', labelKey: 'engine.hermesChannelDmPolicy', type: 'select', options: [['pair', 'engine.hermesChannelPolicyPair'], ['open', 'engine.hermesChannelPolicyOpen'], ['allowlist', 'engine.hermesChannelPolicyAllowlist'], ['disabled', 'engine.hermesChannelPolicyDisabled']] },
|
||||
{ key: 'groupPolicy', labelKey: 'engine.hermesChannelGroupPolicy', type: 'select', options: [['allowlist', 'engine.hermesChannelPolicyAllowlist'], ['open', 'engine.hermesChannelPolicyOpen'], ['disabled', 'engine.hermesChannelPolicyDisabled']] },
|
||||
{ key: 'requireMention', labelKey: 'engine.hermesChannelRequireMention', type: 'checkbox' },
|
||||
{ key: 'allowFrom', labelKey: 'engine.hermesChannelAllowFrom', type: 'textarea', placeholderKey: 'engine.hermesChannelAllowFromPlaceholder' },
|
||||
{ key: 'groupAllowFrom', labelKey: 'engine.hermesChannelGroupAllowFrom', type: 'textarea', placeholderKey: 'engine.hermesChannelGroupAllowFromPlaceholder' },
|
||||
]
|
||||
@@ -109,13 +210,14 @@ function channelMeta(id) {
|
||||
}
|
||||
|
||||
function defaultForm(platform) {
|
||||
const form = {
|
||||
enabled: false,
|
||||
dmPolicy: 'pair',
|
||||
groupPolicy: 'allowlist',
|
||||
allowFrom: '',
|
||||
groupAllowFrom: '',
|
||||
requireMention: true,
|
||||
const channel = channelMeta(platform)
|
||||
const form = { enabled: false }
|
||||
if (!channel.policyFields) {
|
||||
form.dmPolicy = 'pair'
|
||||
form.groupPolicy = 'allowlist'
|
||||
form.allowFrom = ''
|
||||
form.groupAllowFrom = ''
|
||||
form.requireMention = true
|
||||
}
|
||||
if (platform === 'feishu') {
|
||||
form.domain = 'feishu'
|
||||
@@ -152,6 +254,15 @@ function isConfigured(channel, form) {
|
||||
function renderField(field, form, disabled) {
|
||||
const value = valueOf(form, field.key)
|
||||
const label = esc(t(field.labelKey))
|
||||
const placeholder = field.placeholderKey ? t(field.placeholderKey) : (field.placeholder || '')
|
||||
if (field.type === 'checkbox') {
|
||||
return `
|
||||
<label class="hm-channel-check">
|
||||
<input class="hm-channel-input" data-key="${esc(field.key)}" type="checkbox" ${form[field.key] ? 'checked' : ''} ${disabled ? 'disabled' : ''}>
|
||||
<span>${label}</span>
|
||||
</label>
|
||||
`
|
||||
}
|
||||
if (field.type === 'select') {
|
||||
return `
|
||||
<label class="hm-field">
|
||||
@@ -168,14 +279,14 @@ function renderField(field, form, disabled) {
|
||||
return `
|
||||
<label class="hm-field">
|
||||
<span class="hm-field-label">${label}</span>
|
||||
<textarea class="hm-input hm-channel-input hm-channel-textarea" data-key="${esc(field.key)}" ${disabled ? 'disabled' : ''} placeholder="${esc(t(field.placeholderKey))}">${esc(value)}</textarea>
|
||||
<textarea class="hm-input hm-channel-input hm-channel-textarea" data-key="${esc(field.key)}" ${disabled ? 'disabled' : ''} placeholder="${esc(placeholder)}">${esc(value)}</textarea>
|
||||
</label>
|
||||
`
|
||||
}
|
||||
return `
|
||||
<label class="hm-field">
|
||||
<span class="hm-field-label">${label}</span>
|
||||
<input class="hm-input hm-channel-input" data-key="${esc(field.key)}" type="${esc(field.type || 'text')}" value="${esc(value)}" ${disabled ? 'disabled' : ''} placeholder="${esc(field.placeholder || '')}" autocomplete="off">
|
||||
<input class="hm-input hm-channel-input" data-key="${esc(field.key)}" type="${esc(field.type || 'text')}" value="${esc(value)}" ${disabled ? 'disabled' : ''} placeholder="${esc(placeholder)}" autocomplete="off">
|
||||
</label>
|
||||
`
|
||||
}
|
||||
@@ -208,6 +319,9 @@ export function render() {
|
||||
const channel = channelMeta(active)
|
||||
const form = normalizeForm(active, values[active])
|
||||
const disabled = loading || saving
|
||||
const policyFields = channel.policyFields || LEGACY_POLICY_FIELDS
|
||||
const policyInputs = policyFields.filter(field => field.type !== 'checkbox')
|
||||
const policyToggles = policyFields.filter(field => field.type === 'checkbox')
|
||||
const enabledCount = CHANNELS.filter(item => normalizeForm(item.id, values[item.id]).enabled).length
|
||||
const configuredCount = CHANNELS.filter(item => isConfigured(item, normalizeForm(item.id, values[item.id]))).length
|
||||
|
||||
@@ -295,16 +409,16 @@ export function render() {
|
||||
|
||||
<div class="hm-channel-section">
|
||||
<div class="hm-channel-section-title">${esc(t('engine.hermesChannelAccessPolicy'))}</div>
|
||||
<div class="hm-field-row">
|
||||
${COMMON_FIELDS.slice(0, 2).map(field => renderField(field, form, disabled)).join('')}
|
||||
</div>
|
||||
<label class="hm-channel-check hm-channel-check--wide">
|
||||
<input class="hm-channel-input" data-key="requireMention" type="checkbox" ${form.requireMention ? 'checked' : ''} ${disabled ? 'disabled' : ''}>
|
||||
<span>${esc(t('engine.hermesChannelRequireMention'))}</span>
|
||||
</label>
|
||||
<div class="hm-field-row">
|
||||
${COMMON_FIELDS.slice(2).map(field => renderField(field, form, disabled)).join('')}
|
||||
</div>
|
||||
${policyInputs.length ? `
|
||||
<div class="hm-field-row">
|
||||
${policyInputs.map(field => renderField(field, form, disabled)).join('')}
|
||||
</div>
|
||||
` : ''}
|
||||
${policyToggles.length ? `
|
||||
<div class="hm-channel-toggle-grid">
|
||||
${policyToggles.map(field => renderField(field, form, disabled)).join('')}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
${(channel.advancedFields || []).length ? `
|
||||
|
||||
@@ -558,7 +558,8 @@
|
||||
justify-content: center;
|
||||
gap: 7px;
|
||||
padding: 9px 18px;
|
||||
height: 38px;
|
||||
min-height: 44px;
|
||||
height: 44px;
|
||||
border-radius: var(--hm-radius-sm);
|
||||
border: 1px solid var(--hm-border-strong);
|
||||
background: transparent;
|
||||
@@ -585,8 +586,9 @@
|
||||
}
|
||||
|
||||
[data-engine="hermes"] .hm-btn--sm {
|
||||
height: 30px;
|
||||
padding: 4px 12px;
|
||||
min-height: 44px;
|
||||
height: 44px;
|
||||
padding: 8px 14px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@@ -742,7 +744,8 @@
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
padding: 10px 14px;
|
||||
height: 40px;
|
||||
min-height: 44px;
|
||||
height: 44px;
|
||||
border: 1px solid var(--hm-border);
|
||||
background: var(--hm-surface-0);
|
||||
color: var(--hm-text-primary);
|
||||
@@ -1061,6 +1064,8 @@ body[data-active-engine="hermes"] .sidebar-title {
|
||||
}
|
||||
body[data-active-engine="hermes"] .sidebar-collapse-btn,
|
||||
body[data-active-engine="hermes"] .sidebar-close-btn {
|
||||
min-width: 44px;
|
||||
min-height: 44px;
|
||||
color: #A8A29E;
|
||||
background: transparent;
|
||||
border: none;
|
||||
@@ -1089,6 +1094,7 @@ body[data-active-engine="hermes"] .engine-switcher-label {
|
||||
padding: 0 4px 8px;
|
||||
}
|
||||
body[data-active-engine="hermes"] .engine-current {
|
||||
min-height: 44px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid rgba(202, 138, 4, 0.25);
|
||||
border-radius: 6px;
|
||||
@@ -1142,6 +1148,7 @@ body[data-active-engine="hermes"] .sidebar-nav {
|
||||
padding: 4px 10px;
|
||||
}
|
||||
body[data-active-engine="hermes"] .nav-item {
|
||||
min-height: 44px;
|
||||
padding: 9px 14px;
|
||||
border-radius: 6px;
|
||||
color: #44403C;
|
||||
@@ -1190,6 +1197,7 @@ body[data-active-engine="hermes"] .sidebar-footer {
|
||||
}
|
||||
body[data-active-engine="hermes"] #btn-theme-toggle,
|
||||
body[data-active-engine="hermes"] .lang-trigger {
|
||||
min-height: 44px;
|
||||
padding: 8px 14px;
|
||||
border-radius: 6px;
|
||||
color: #78716C;
|
||||
@@ -1207,6 +1215,9 @@ body[data-active-engine="hermes"] .lang-dropdown {
|
||||
box-shadow: 0 -4px 16px rgba(28, 25, 23, 0.08);
|
||||
border-radius: 8px;
|
||||
}
|
||||
body[data-active-engine="hermes"] .lang-search {
|
||||
min-height: 44px;
|
||||
}
|
||||
body[data-active-engine="hermes"] .lang-option {
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
|
||||
@@ -923,11 +923,21 @@ export default {
|
||||
hermesChannelSlack: _('Slack', 'Slack', 'Slack'),
|
||||
hermesChannelFeishu: _('飞书 / Lark', 'Feishu / Lark', '飛書 / Lark'),
|
||||
hermesChannelDingTalk: _('钉钉', 'DingTalk', '釘釘'),
|
||||
hermesChannelTeams: _('Microsoft Teams', 'Microsoft Teams', 'Microsoft Teams'),
|
||||
hermesChannelGoogleChat: _('Google Chat', 'Google Chat', 'Google Chat'),
|
||||
hermesChannelIrc: _('IRC', 'IRC', 'IRC'),
|
||||
hermesChannelLine: _('LINE', 'LINE', 'LINE'),
|
||||
hermesChannelSimpleX: _('SimpleX', 'SimpleX', 'SimpleX'),
|
||||
hermesChannelTelegramDesc: _('通过 Telegram Bot 与 Hermes 对话,适合个人私聊和小群组。', 'Talk to Hermes through a Telegram bot for direct chats and small groups.', '透過 Telegram Bot 與 Hermes 對話,適合個人私聊和小群組。'),
|
||||
hermesChannelDiscordDesc: _('连接 Discord Bot,支持服务器频道和线程里的 Agent 会话。', 'Connect a Discord bot for server channels and threaded agent sessions.', '連接 Discord Bot,支援伺服器頻道和討論串裡的 Agent 會話。'),
|
||||
hermesChannelSlackDesc: _('连接 Slack Bot,可用于团队频道、私信和工作流通知。', 'Connect a Slack bot for team channels, direct messages, and workflow notifications.', '連接 Slack Bot,可用於團隊頻道、私訊和工作流通知。'),
|
||||
hermesChannelFeishuDesc: _('连接飞书或 Lark 应用,支持长连接和 Webhook 两种模式。', 'Connect a Feishu or Lark app with WebSocket or webhook mode.', '連接飛書或 Lark 應用,支援長連線和 Webhook 兩種模式。'),
|
||||
hermesChannelDingTalkDesc: _('连接钉钉机器人应用,支持群聊白名单、用户白名单和 @Bot 唤醒策略。', 'Connect a DingTalk robot app with group allowlists, user allowlists, and @mention wake rules.', '連接釘釘機器人應用,支援群聊白名單、使用者白名單和 @Bot 喚醒策略。'),
|
||||
hermesChannelTeamsDesc: _('连接 Microsoft Teams Bot,支持服务 URL、用户白名单和默认频道。', 'Connect a Microsoft Teams bot with service URL, user allowlists, and a home channel.', '連接 Microsoft Teams Bot,支援服務 URL、使用者白名單和預設頻道。'),
|
||||
hermesChannelGoogleChatDesc: _('连接 Google Chat Pub/Sub 应用,支持项目订阅、服务账号和空间白名单。', 'Connect a Google Chat Pub/Sub app with project subscriptions, service accounts, and space allowlists.', '連接 Google Chat Pub/Sub 應用,支援專案訂閱、服務帳號和空間白名單。'),
|
||||
hermesChannelIrcDesc: _('连接 IRC 网络,支持 TLS、NickServ 密码、频道和用户白名单。', 'Connect an IRC network with TLS, NickServ passwords, channels, and user allowlists.', '連接 IRC 網路,支援 TLS、NickServ 密碼、頻道和使用者白名單。'),
|
||||
hermesChannelLineDesc: _('连接 LINE Messaging API,支持 Webhook、用户、群组和聊天室白名单。', 'Connect LINE Messaging API with webhook, user, group, and room allowlists.', '連接 LINE Messaging API,支援 Webhook、使用者、群組和聊天室白名單。'),
|
||||
hermesChannelSimpleXDesc: _('连接 SimpleX WebSocket 网关,支持联系人白名单和默认会话。', 'Connect a SimpleX WebSocket gateway with contact allowlists and a home conversation.', '連接 SimpleX WebSocket 閘道,支援聯絡人白名單和預設會話。'),
|
||||
hermesChannelEnabled: _('已启用', 'Enabled', '已啟用'),
|
||||
hermesChannelDisabled: _('未启用', 'Disabled', '未啟用'),
|
||||
hermesChannelSave: _('保存渠道', 'Save Channel', '儲存頻道'),
|
||||
@@ -964,6 +974,40 @@ export default {
|
||||
hermesChannelFeishuAppSecret: _('App Secret', 'App Secret', 'App Secret'),
|
||||
hermesChannelDingTalkClientId: _('Client ID / App Key', 'Client ID / App Key', 'Client ID / App Key'),
|
||||
hermesChannelDingTalkClientSecret: _('Client Secret', 'Client Secret', 'Client Secret'),
|
||||
hermesChannelTeamsClientId: _('Client ID', 'Client ID', 'Client ID'),
|
||||
hermesChannelTeamsClientSecret: _('Client Secret', 'Client Secret', 'Client Secret'),
|
||||
hermesChannelTeamsTenantId: _('Tenant ID', 'Tenant ID', 'Tenant ID'),
|
||||
hermesChannelGoogleProjectId: _('Project ID', 'Project ID', 'Project ID'),
|
||||
hermesChannelGoogleSubscriptionName: _('订阅名称', 'Subscription Name', '訂閱名稱'),
|
||||
hermesChannelGoogleServiceAccount: _('服务账号 JSON / 文件路径', 'Service Account JSON / Path', '服務帳號 JSON / 檔案路徑'),
|
||||
hermesChannelGoogleServiceAccountPh: _('可填写 JSON 内容或凭证文件路径。', 'Enter JSON content or a credentials file path.', '可填寫 JSON 內容或憑證檔案路徑。'),
|
||||
hermesChannelIrcServer: _('服务器', 'Server', '伺服器'),
|
||||
hermesChannelIrcNickname: _('昵称', 'Nickname', '暱稱'),
|
||||
hermesChannelIrcChannel: _('频道', 'Channel', '頻道'),
|
||||
hermesChannelIrcServerPassword: _('服务器密码', 'Server Password', '伺服器密碼'),
|
||||
hermesChannelIrcNickservPassword: _('NickServ 密码', 'NickServ Password', 'NickServ 密碼'),
|
||||
hermesChannelIrcUseTls: _('启用 TLS', 'Use TLS', '啟用 TLS'),
|
||||
hermesChannelLineAccessToken: _('Channel Access Token', 'Channel Access Token', 'Channel Access Token'),
|
||||
hermesChannelLineSecret: _('Channel Secret', 'Channel Secret', 'Channel Secret'),
|
||||
hermesChannelLineAllowedGroups: _('允许群组', 'Allowed Groups', '允許群組'),
|
||||
hermesChannelLineAllowedRooms: _('允许聊天室', 'Allowed Rooms', '允許聊天室'),
|
||||
hermesChannelLineSlowResponse: _('慢响应阈值(秒)', 'Slow Response Threshold (seconds)', '慢回應閾值(秒)'),
|
||||
hermesChannelSimpleXWsUrl: _('WebSocket 地址', 'WebSocket URL', 'WebSocket 位址'),
|
||||
hermesChannelPort: _('端口', 'Port', '連接埠'),
|
||||
hermesChannelHost: _('监听地址', 'Host', '監聽位址'),
|
||||
hermesChannelPublicUrl: _('公开 URL', 'Public URL', '公開 URL'),
|
||||
hermesChannelServiceUrl: _('服务 URL', 'Service URL', '服務 URL'),
|
||||
hermesChannelAllowedUsers: _('允许用户', 'Allowed Users', '允許使用者'),
|
||||
hermesChannelAllowAllUsers: _('允许所有用户', 'Allow all users', '允許所有使用者'),
|
||||
hermesChannelHomeChannel: _('默认会话 / 频道', 'Home Conversation / Channel', '預設會話 / 頻道'),
|
||||
hermesChannelHomeChannelName: _('默认会话名称', 'Home Conversation Name', '預設會話名稱'),
|
||||
hermesChannelTeamsAllowedUsersPh: _('每行或逗号分隔一个 Microsoft Entra 用户 ID。', 'One Microsoft Entra user ID per line or comma-separated.', '每行或逗號分隔一個 Microsoft Entra 使用者 ID。'),
|
||||
hermesChannelGoogleAllowedUsersPh: _('每行或逗号分隔一个 Google Chat 用户或空间 ID。', 'One Google Chat user or space ID per line or comma-separated.', '每行或逗號分隔一個 Google Chat 使用者或空間 ID。'),
|
||||
hermesChannelIrcAllowedUsersPh: _('每行或逗号分隔一个 IRC nick。', 'One IRC nick per line or comma-separated.', '每行或逗號分隔一個 IRC nick。'),
|
||||
hermesChannelLineAllowedUsersPh: _('每行或逗号分隔一个 LINE 用户 ID。', 'One LINE user ID per line or comma-separated.', '每行或逗號分隔一個 LINE 使用者 ID。'),
|
||||
hermesChannelLineAllowedGroupsPh: _('每行或逗号分隔一个 LINE 群组 ID。', 'One LINE group ID per line or comma-separated.', '每行或逗號分隔一個 LINE 群組 ID。'),
|
||||
hermesChannelLineAllowedRoomsPh: _('每行或逗号分隔一个 LINE 聊天室 ID。', 'One LINE room ID per line or comma-separated.', '每行或逗號分隔一個 LINE 聊天室 ID。'),
|
||||
hermesChannelSimpleXAllowedUsersPh: _('每行或逗号分隔一个 SimpleX 联系人或群组标识。', 'One SimpleX contact or group identifier per line or comma-separated.', '每行或逗號分隔一個 SimpleX 聯絡人或群組識別。'),
|
||||
hermesChannelFeishuDomain: _('区域', 'Region', '區域'),
|
||||
hermesChannelFeishuDomainCn: _('中国大陆(feishu)', 'Mainland China (feishu)', '中國大陸(feishu)'),
|
||||
hermesChannelFeishuDomainIntl: _('国际版(lark)', 'International (lark)', '國際版(lark)'),
|
||||
|
||||
@@ -746,8 +746,8 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.mobile-hamburger {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 8px;
|
||||
background: none;
|
||||
border: 1px solid var(--border-primary);
|
||||
|
||||
@@ -366,3 +366,288 @@ test('Hermes 钉钉保存会使用运行时实际读取的字段', () => {
|
||||
assert.equal(next.platforms.dingtalk.extra.require_mention, true)
|
||||
assert.equal(next.platforms.dingtalk.extra.unknown_option, 'keep-me')
|
||||
})
|
||||
|
||||
test('Hermes 插件平台读取会回显上游运行字段并优先使用环境变量', () => {
|
||||
const values = buildHermesChannelConfigValues({
|
||||
platforms: {
|
||||
teams: {
|
||||
enabled: true,
|
||||
extra: {
|
||||
client_id: 'yaml-teams-client',
|
||||
client_secret: 'yaml-teams-secret',
|
||||
tenant_id: 'yaml-tenant',
|
||||
port: 3978,
|
||||
service_url: 'https://smba.trafficmanager.net/teams/',
|
||||
allow_from: ['aad-1'],
|
||||
},
|
||||
},
|
||||
google_chat: {
|
||||
enabled: true,
|
||||
extra: {
|
||||
project_id: 'yaml-project',
|
||||
subscription_name: 'projects/yaml-project/subscriptions/hermes',
|
||||
service_account_json: 'yaml-sa.json',
|
||||
allow_from: ['user@example.com'],
|
||||
},
|
||||
},
|
||||
irc: {
|
||||
enabled: true,
|
||||
extra: {
|
||||
server: 'irc.libera.chat',
|
||||
channel: '#hermes',
|
||||
nickname: 'hermes-bot',
|
||||
use_tls: true,
|
||||
allowed_users: ['alice'],
|
||||
},
|
||||
},
|
||||
line: {
|
||||
enabled: true,
|
||||
extra: {
|
||||
channel_access_token: 'yaml-line-token',
|
||||
channel_secret: 'yaml-line-secret',
|
||||
host: '0.0.0.0',
|
||||
port: 8646,
|
||||
public_url: 'https://line.example.com',
|
||||
allowed_users: ['U1'],
|
||||
allowed_groups: ['C1'],
|
||||
allowed_rooms: ['R1'],
|
||||
slow_response_threshold: '45',
|
||||
},
|
||||
},
|
||||
simplex: {
|
||||
enabled: true,
|
||||
extra: {
|
||||
ws_url: 'ws://127.0.0.1:5225',
|
||||
allowed_users: ['contact-1'],
|
||||
},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
TEAMS_CLIENT_ID: 'env-teams-client',
|
||||
TEAMS_CLIENT_SECRET: 'env-teams-secret',
|
||||
TEAMS_TENANT_ID: 'env-tenant',
|
||||
TEAMS_HOME_CHANNEL: 'teams-home',
|
||||
TEAMS_HOME_CHANNEL_NAME: 'Ops',
|
||||
GOOGLE_CHAT_PROJECT_ID: 'env-project',
|
||||
GOOGLE_CHAT_SUBSCRIPTION_NAME: 'projects/env-project/subscriptions/hermes',
|
||||
GOOGLE_CHAT_SERVICE_ACCOUNT_JSON: 'env-sa.json',
|
||||
GOOGLE_CHAT_HOME_CHANNEL: 'spaces/AAA',
|
||||
IRC_SERVER: 'irc.oftc.net',
|
||||
IRC_CHANNEL: '#ops',
|
||||
IRC_NICKNAME: 'ops-bot',
|
||||
IRC_HOME_CHANNEL: '#reports',
|
||||
LINE_CHANNEL_ACCESS_TOKEN: 'env-line-token',
|
||||
LINE_CHANNEL_SECRET: 'env-line-secret',
|
||||
LINE_HOME_CHANNEL: 'U-home',
|
||||
SIMPLEX_WS_URL: 'ws://127.0.0.1:5226',
|
||||
SIMPLEX_HOME_CHANNEL: 'contact-home',
|
||||
})
|
||||
|
||||
assert.equal(values.teams.clientId, 'env-teams-client')
|
||||
assert.equal(values.teams.clientSecret, 'env-teams-secret')
|
||||
assert.equal(values.teams.tenantId, 'env-tenant')
|
||||
assert.equal(values.teams.homeChannel, 'teams-home')
|
||||
assert.equal(values.teams.allowFrom, 'aad-1')
|
||||
assert.equal(values.google_chat.projectId, 'env-project')
|
||||
assert.equal(values.google_chat.subscriptionName, 'projects/env-project/subscriptions/hermes')
|
||||
assert.equal(values.google_chat.serviceAccountJson, 'env-sa.json')
|
||||
assert.equal(values.google_chat.homeChannel, 'spaces/AAA')
|
||||
assert.equal(values.irc.server, 'irc.oftc.net')
|
||||
assert.equal(values.irc.channel, '#ops')
|
||||
assert.equal(values.irc.nickname, 'ops-bot')
|
||||
assert.equal(values.irc.homeChannel, '#reports')
|
||||
assert.equal(values.irc.useTls, true)
|
||||
assert.equal(values.irc.allowFrom, 'alice')
|
||||
assert.equal(values.line.channelAccessToken, 'env-line-token')
|
||||
assert.equal(values.line.channelSecret, 'env-line-secret')
|
||||
assert.equal(values.line.homeChannel, 'U-home')
|
||||
assert.equal(values.line.allowedGroups, 'C1')
|
||||
assert.equal(values.line.allowedRooms, 'R1')
|
||||
assert.equal(values.simplex.wsUrl, 'ws://127.0.0.1:5226')
|
||||
assert.equal(values.simplex.homeChannel, 'contact-home')
|
||||
assert.equal(values.simplex.allowFrom, 'contact-1')
|
||||
})
|
||||
|
||||
test('Hermes 插件平台保存会写入运行时读取的 YAML 字段和环境变量', () => {
|
||||
const teams = mergeHermesChannelConfig({
|
||||
platforms: {
|
||||
teams: {
|
||||
enabled: true,
|
||||
extra: {
|
||||
client_id: 'old-client',
|
||||
client_secret: 'old-secret',
|
||||
tenant_id: 'old-tenant',
|
||||
unknown_option: 'keep-me',
|
||||
},
|
||||
},
|
||||
},
|
||||
}, 'teams', {
|
||||
enabled: true,
|
||||
clientId: 'teams-client',
|
||||
clientSecret: 'teams-secret',
|
||||
tenantId: 'tenant-1',
|
||||
port: '3978',
|
||||
serviceUrl: 'https://smba.trafficmanager.net/teams/',
|
||||
allowFrom: 'aad-1, aad-2',
|
||||
allowAllUsers: false,
|
||||
homeChannel: '19:abc@thread.tacv2',
|
||||
homeChannelName: 'Ops',
|
||||
})
|
||||
|
||||
assert.equal(teams.platforms.teams.extra.client_id, undefined)
|
||||
assert.equal(teams.platforms.teams.extra.client_secret, undefined)
|
||||
assert.equal(teams.platforms.teams.extra.tenant_id, undefined)
|
||||
assert.equal(teams.platforms.teams.extra.port, 3978)
|
||||
assert.equal(teams.platforms.teams.extra.service_url, 'https://smba.trafficmanager.net/teams/')
|
||||
assert.deepEqual(teams.platforms.teams.extra.allow_from, ['aad-1', 'aad-2'])
|
||||
assert.equal(teams.platforms.teams.extra.unknown_option, 'keep-me')
|
||||
|
||||
const googleChat = mergeHermesChannelConfig({}, 'google_chat', {
|
||||
enabled: true,
|
||||
projectId: 'project-1',
|
||||
subscriptionName: 'projects/project-1/subscriptions/hermes',
|
||||
serviceAccountJson: 'C:\\keys\\sa.json',
|
||||
allowFrom: 'user@example.com',
|
||||
allowAllUsers: true,
|
||||
homeChannel: 'spaces/AAA',
|
||||
homeChannelName: 'Ops Space',
|
||||
})
|
||||
|
||||
assert.equal(googleChat.platforms.google_chat.enabled, true)
|
||||
assert.equal(googleChat.platforms.google_chat.extra.project_id, 'project-1')
|
||||
assert.equal(googleChat.platforms.google_chat.extra.subscription_name, 'projects/project-1/subscriptions/hermes')
|
||||
assert.equal(googleChat.platforms.google_chat.extra.service_account_json, undefined)
|
||||
assert.deepEqual(googleChat.platforms.google_chat.extra.allow_from, ['user@example.com'])
|
||||
|
||||
const irc = mergeHermesChannelConfig({}, 'irc', {
|
||||
enabled: true,
|
||||
server: 'irc.libera.chat',
|
||||
port: '6697',
|
||||
nickname: 'hermes-bot',
|
||||
channel: '#hermes',
|
||||
useTls: true,
|
||||
serverPassword: 'server-secret',
|
||||
nickservPassword: 'nick-secret',
|
||||
allowFrom: 'alice, bob',
|
||||
allowAllUsers: false,
|
||||
homeChannel: '#reports',
|
||||
homeChannelName: 'reports',
|
||||
})
|
||||
|
||||
assert.equal(irc.platforms.irc.extra.server, 'irc.libera.chat')
|
||||
assert.equal(irc.platforms.irc.extra.port, 6697)
|
||||
assert.equal(irc.platforms.irc.extra.nickname, 'hermes-bot')
|
||||
assert.equal(irc.platforms.irc.extra.channel, '#hermes')
|
||||
assert.equal(irc.platforms.irc.extra.use_tls, true)
|
||||
assert.equal(irc.platforms.irc.extra.server_password, undefined)
|
||||
assert.equal(irc.platforms.irc.extra.nickserv_password, undefined)
|
||||
assert.deepEqual(irc.platforms.irc.extra.allowed_users, ['alice', 'bob'])
|
||||
|
||||
const line = mergeHermesChannelConfig({}, 'line', {
|
||||
enabled: true,
|
||||
channelAccessToken: 'line-token',
|
||||
channelSecret: 'line-secret',
|
||||
port: '8646',
|
||||
host: '0.0.0.0',
|
||||
publicUrl: 'https://line.example.com',
|
||||
allowFrom: 'U1',
|
||||
allowedGroups: 'C1',
|
||||
allowedRooms: 'R1',
|
||||
allowAllUsers: false,
|
||||
homeChannel: 'U-home',
|
||||
slowResponseThreshold: '45',
|
||||
})
|
||||
|
||||
assert.equal(line.platforms.line.extra.channel_access_token, undefined)
|
||||
assert.equal(line.platforms.line.extra.channel_secret, undefined)
|
||||
assert.equal(line.platforms.line.extra.port, 8646)
|
||||
assert.equal(line.platforms.line.extra.host, '0.0.0.0')
|
||||
assert.equal(line.platforms.line.extra.public_url, 'https://line.example.com')
|
||||
assert.deepEqual(line.platforms.line.extra.allowed_users, ['U1'])
|
||||
assert.deepEqual(line.platforms.line.extra.allowed_groups, ['C1'])
|
||||
assert.deepEqual(line.platforms.line.extra.allowed_rooms, ['R1'])
|
||||
assert.equal(line.platforms.line.extra.slow_response_threshold, '45')
|
||||
|
||||
const simplex = mergeHermesChannelConfig({}, 'simplex', {
|
||||
enabled: true,
|
||||
wsUrl: 'ws://127.0.0.1:5225',
|
||||
allowFrom: 'contact-1',
|
||||
allowAllUsers: true,
|
||||
homeChannel: 'group:ops',
|
||||
homeChannelName: 'Ops',
|
||||
})
|
||||
|
||||
assert.equal(simplex.platforms.simplex.extra.ws_url, 'ws://127.0.0.1:5225')
|
||||
assert.deepEqual(simplex.platforms.simplex.extra.allowed_users, ['contact-1'])
|
||||
|
||||
const env = {
|
||||
...buildHermesChannelEnvUpdates('teams', {
|
||||
clientId: 'teams-client',
|
||||
clientSecret: 'teams-secret',
|
||||
tenantId: 'tenant-1',
|
||||
port: '3978',
|
||||
serviceUrl: 'https://smba.trafficmanager.net/teams/',
|
||||
allowFrom: 'aad-1, aad-2',
|
||||
allowAllUsers: false,
|
||||
homeChannel: '19:abc@thread.tacv2',
|
||||
homeChannelName: 'Ops',
|
||||
}),
|
||||
...buildHermesChannelEnvUpdates('google_chat', {
|
||||
projectId: 'project-1',
|
||||
subscriptionName: 'projects/project-1/subscriptions/hermes',
|
||||
serviceAccountJson: 'C:\\keys\\sa.json',
|
||||
allowFrom: 'user@example.com',
|
||||
allowAllUsers: true,
|
||||
homeChannel: 'spaces/AAA',
|
||||
homeChannelName: 'Ops Space',
|
||||
}),
|
||||
...buildHermesChannelEnvUpdates('irc', {
|
||||
server: 'irc.libera.chat',
|
||||
port: '6697',
|
||||
nickname: 'hermes-bot',
|
||||
channel: '#hermes',
|
||||
useTls: true,
|
||||
serverPassword: 'server-secret',
|
||||
nickservPassword: 'nick-secret',
|
||||
allowFrom: 'alice, bob',
|
||||
allowAllUsers: false,
|
||||
homeChannel: '#reports',
|
||||
homeChannelName: 'reports',
|
||||
}),
|
||||
...buildHermesChannelEnvUpdates('line', {
|
||||
channelAccessToken: 'line-token',
|
||||
channelSecret: 'line-secret',
|
||||
port: '8646',
|
||||
host: '0.0.0.0',
|
||||
publicUrl: 'https://line.example.com',
|
||||
allowFrom: 'U1',
|
||||
allowedGroups: 'C1',
|
||||
allowedRooms: 'R1',
|
||||
allowAllUsers: false,
|
||||
homeChannel: 'U-home',
|
||||
slowResponseThreshold: '45',
|
||||
}),
|
||||
...buildHermesChannelEnvUpdates('simplex', {
|
||||
wsUrl: 'ws://127.0.0.1:5225',
|
||||
allowFrom: 'contact-1',
|
||||
allowAllUsers: true,
|
||||
homeChannel: 'group:ops',
|
||||
homeChannelName: 'Ops',
|
||||
}),
|
||||
}
|
||||
|
||||
assert.equal(env.TEAMS_CLIENT_ID, 'teams-client')
|
||||
assert.equal(env.TEAMS_CLIENT_SECRET, 'teams-secret')
|
||||
assert.equal(env.TEAMS_TENANT_ID, 'tenant-1')
|
||||
assert.equal(env.TEAMS_ALLOWED_USERS, 'aad-1,aad-2')
|
||||
assert.equal(env.TEAMS_ALLOW_ALL_USERS, 'false')
|
||||
assert.equal(env.GOOGLE_CHAT_SERVICE_ACCOUNT_JSON, 'C:\\keys\\sa.json')
|
||||
assert.equal(env.GOOGLE_CHAT_ALLOW_ALL_USERS, 'true')
|
||||
assert.equal(env.IRC_USE_TLS, 'true')
|
||||
assert.equal(env.IRC_SERVER_PASSWORD, 'server-secret')
|
||||
assert.equal(env.IRC_NICKSERV_PASSWORD, 'nick-secret')
|
||||
assert.equal(env.LINE_CHANNEL_ACCESS_TOKEN, 'line-token')
|
||||
assert.equal(env.LINE_ALLOWED_GROUPS, 'C1')
|
||||
assert.equal(env.SIMPLEX_WS_URL, 'ws://127.0.0.1:5225')
|
||||
assert.equal(env.SIMPLEX_ALLOW_ALL_USERS, 'true')
|
||||
})
|
||||
|
||||
70
tests/hermes-channel-ui-registry.test.js
Normal file
70
tests/hermes-channel-ui-registry.test.js
Normal file
@@ -0,0 +1,70 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { t } from '../src/lib/i18n.js'
|
||||
|
||||
const source = readFileSync(new URL('../src/engines/hermes/pages/channels.js', import.meta.url), 'utf8')
|
||||
|
||||
function getChannelBlock(channelId) {
|
||||
const start = source.indexOf(`id: '${channelId}'`)
|
||||
assert.notEqual(start, -1, `未找到 Hermes ${channelId} 渠道入口`)
|
||||
const objectStart = source.lastIndexOf('{', start)
|
||||
let depth = 0
|
||||
for (let index = objectStart; index < source.length; index += 1) {
|
||||
const char = source[index]
|
||||
if (char === '{') depth += 1
|
||||
if (char === '}') depth -= 1
|
||||
if (depth === 0) return source.slice(objectStart, index + 1)
|
||||
}
|
||||
assert.fail(`未找到 Hermes ${channelId} 渠道入口结束位置`)
|
||||
}
|
||||
|
||||
function extractEngineKeys(block) {
|
||||
return [...block.matchAll(/['"](engine\.[A-Za-z0-9_.-]+)['"]/g)].map(match => match[1])
|
||||
}
|
||||
|
||||
test('Hermes bundled plugin 渠道页会暴露上游平台配置入口', () => {
|
||||
for (const channelId of ['teams', 'google_chat', 'irc', 'line', 'simplex']) {
|
||||
getChannelBlock(channelId)
|
||||
}
|
||||
})
|
||||
|
||||
test('Hermes bundled plugin 渠道页会暴露运行时真实读取字段', () => {
|
||||
const expectedFields = {
|
||||
teams: ['clientId', 'clientSecret', 'tenantId', 'port', 'serviceUrl', 'allowFrom', 'allowAllUsers', 'homeChannel', 'homeChannelName'],
|
||||
google_chat: ['projectId', 'subscriptionName', 'serviceAccountJson', 'allowFrom', 'allowAllUsers', 'homeChannel', 'homeChannelName'],
|
||||
irc: ['server', 'port', 'nickname', 'channel', 'useTls', 'serverPassword', 'nickservPassword', 'allowFrom', 'allowAllUsers', 'homeChannel', 'homeChannelName'],
|
||||
line: ['channelAccessToken', 'channelSecret', 'port', 'host', 'publicUrl', 'allowFrom', 'allowedGroups', 'allowedRooms', 'allowAllUsers', 'homeChannel', 'slowResponseThreshold'],
|
||||
simplex: ['wsUrl', 'allowFrom', 'allowAllUsers', 'homeChannel', 'homeChannelName'],
|
||||
}
|
||||
|
||||
for (const [channelId, fields] of Object.entries(expectedFields)) {
|
||||
const block = getChannelBlock(channelId)
|
||||
for (const field of fields) {
|
||||
assert.match(block, new RegExp(`key:\\s*'${field}'`), `${channelId} 缺少 ${field} 字段`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('Hermes bundled plugin 渠道页不会对新增平台渲染旧通用策略字段', () => {
|
||||
for (const channelId of ['teams', 'google_chat', 'irc', 'line', 'simplex']) {
|
||||
const block = getChannelBlock(channelId)
|
||||
assert.match(block, /policyFields:\s*\[/, `${channelId} 应显式声明访问策略字段`)
|
||||
assert.doesNotMatch(block, /key:\s*'dmPolicy'/, `${channelId} 不应显示旧私聊策略`)
|
||||
assert.doesNotMatch(block, /key:\s*'groupPolicy'/, `${channelId} 不应显示旧群组策略`)
|
||||
assert.doesNotMatch(block, /key:\s*'groupAllowFrom'/, `${channelId} 不应显示旧群组白名单`)
|
||||
assert.doesNotMatch(block, /key:\s*'requireMention'/, `${channelId} 不应显示旧 @Bot 唤醒开关`)
|
||||
}
|
||||
})
|
||||
|
||||
test('Hermes bundled plugin 渠道页新增平台不会暴露翻译 key', () => {
|
||||
const keys = new Set()
|
||||
for (const channelId of ['teams', 'google_chat', 'irc', 'line', 'simplex']) {
|
||||
for (const key of extractEngineKeys(getChannelBlock(channelId))) keys.add(key)
|
||||
}
|
||||
|
||||
assert.ok(keys.size > 0, '应能提取新增平台用到的 engine 翻译 key')
|
||||
for (const key of keys) {
|
||||
assert.notEqual(t(key), key, `${key} 缺少运行时翻译`)
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user