mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-29 04:10:00 +08:00
feat(hermes): support Discord plugin runtime config
This commit is contained in:
@@ -3091,6 +3091,22 @@ function hermesEnvValue(envValues, key) {
|
||||
return typeof value === 'string' && value.trim() ? value.trim() : ''
|
||||
}
|
||||
|
||||
function hermesEnvBoolValue(envValues, key) {
|
||||
const value = hermesEnvValue(envValues, key)
|
||||
if (!value) return undefined
|
||||
return ['true', '1', 'yes', 'on'].includes(value.toLowerCase())
|
||||
}
|
||||
|
||||
function putHermesEnvString(form, envValues, envKey, formKey) {
|
||||
const value = hermesEnvValue(envValues, envKey)
|
||||
if (value) form[formKey] = value
|
||||
}
|
||||
|
||||
function putHermesEnvBool(form, envValues, envKey, formKey) {
|
||||
const value = hermesEnvBoolValue(envValues, envKey)
|
||||
if (value !== undefined) form[formKey] = value
|
||||
}
|
||||
|
||||
function readHermesEnvValues() {
|
||||
const envPath = path.join(hermesHome(), '.env')
|
||||
const values = {}
|
||||
@@ -3136,6 +3152,30 @@ export function buildHermesChannelConfigValues(config = {}, envValues = {}) {
|
||||
form.botToken = hermesEnvValue(envValues, 'TELEGRAM_BOT_TOKEN') || (typeof entry.token === 'string' ? entry.token : '')
|
||||
} else if (platform === 'discord') {
|
||||
form.token = hermesEnvValue(envValues, 'DISCORD_BOT_TOKEN') || (typeof entry.token === 'string' ? entry.token : '')
|
||||
for (const [yamlKey, formKey] of [
|
||||
['free_response_channels', 'freeResponseChannels'],
|
||||
['allowed_channels', 'allowedChannels'],
|
||||
['ignored_channels', 'ignoredChannels'],
|
||||
['no_thread_channels', 'noThreadChannels'],
|
||||
]) {
|
||||
putHermesCsv(form, extra, yamlKey)
|
||||
putHermesEnvString(form, envValues, `DISCORD_${yamlKey.toUpperCase()}`, formKey)
|
||||
}
|
||||
for (const [yamlKey, formKey] of [
|
||||
['auto_thread', 'autoThread'],
|
||||
['reactions', 'reactions'],
|
||||
['thread_require_mention', 'threadRequireMention'],
|
||||
['history_backfill', 'historyBackfill'],
|
||||
]) {
|
||||
putHermesBool(form, extra, yamlKey)
|
||||
putHermesEnvBool(form, envValues, `DISCORD_${yamlKey.toUpperCase()}`, formKey)
|
||||
}
|
||||
putHermesString(form, extra, 'history_backfill_limit')
|
||||
putHermesEnvString(form, envValues, 'DISCORD_HISTORY_BACKFILL_LIMIT', 'historyBackfillLimit')
|
||||
putHermesString(form, extra, 'reply_to_mode')
|
||||
putHermesEnvString(form, envValues, 'DISCORD_REPLY_TO_MODE', 'replyToMode')
|
||||
putHermesEnvString(form, envValues, 'DISCORD_HOME_CHANNEL', 'homeChannel')
|
||||
putHermesEnvString(form, envValues, 'DISCORD_HOME_CHANNEL_NAME', 'homeChannelName')
|
||||
} else if (platform === 'slack') {
|
||||
form.botToken = hermesEnvValue(envValues, 'SLACK_BOT_TOKEN') || (typeof entry.token === 'string' ? entry.token : '')
|
||||
putHermesString(form, extra, 'app_token')
|
||||
@@ -3214,6 +3254,16 @@ function normalizeHermesChannelForm(platform, form = {}) {
|
||||
if (platform === 'slack') {
|
||||
normalized.webhookPath = String(normalized.webhookPath || '').trim() || '/slack/events'
|
||||
}
|
||||
if (platform === 'discord') {
|
||||
for (const key of ['freeResponseChannels', 'allowedChannels', 'ignoredChannels', 'noThreadChannels']) {
|
||||
if (Object.hasOwn(normalized, key)) normalized[key] = csvToStringArray(normalized[key])
|
||||
}
|
||||
for (const key of ['autoThread', 'reactions', 'threadRequireMention', 'historyBackfill']) {
|
||||
if (Object.hasOwn(normalized, key)) normalized[key] = normalized[key] === true || normalized[key] === 'true' || normalized[key] === 'on'
|
||||
}
|
||||
normalized.historyBackfillLimit = String(normalized.historyBackfillLimit || '').trim()
|
||||
normalized.replyToMode = String(normalized.replyToMode || '').trim()
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
@@ -3232,6 +3282,24 @@ export function mergeHermesChannelConfig(config = {}, platform, form = {}) {
|
||||
deleteHermesEntryKey(entry, 'token')
|
||||
} else if (normalizedPlatform === 'discord') {
|
||||
deleteHermesEntryKey(entry, 'token')
|
||||
for (const [formKey, extraKey] of [
|
||||
['freeResponseChannels', 'free_response_channels'],
|
||||
['allowedChannels', 'allowed_channels'],
|
||||
['ignoredChannels', 'ignored_channels'],
|
||||
['noThreadChannels', 'no_thread_channels'],
|
||||
]) {
|
||||
if (Array.isArray(normalized[formKey])) setHermesExtra(entry, extraKey, normalized[formKey])
|
||||
}
|
||||
for (const [formKey, extraKey] of [
|
||||
['autoThread', 'auto_thread'],
|
||||
['reactions', 'reactions'],
|
||||
['threadRequireMention', 'thread_require_mention'],
|
||||
['historyBackfill', 'history_backfill'],
|
||||
]) {
|
||||
if (Object.hasOwn(normalized, formKey)) setHermesExtra(entry, extraKey, !!normalized[formKey])
|
||||
}
|
||||
setHermesExtra(entry, 'history_backfill_limit', normalized.historyBackfillLimit)
|
||||
setHermesExtra(entry, 'reply_to_mode', normalized.replyToMode)
|
||||
} else if (normalizedPlatform === 'slack') {
|
||||
deleteHermesEntryKey(entry, 'token')
|
||||
deleteHermesExtraKey(entry, 'app_token')
|
||||
@@ -3340,6 +3408,18 @@ export function buildHermesChannelEnvUpdates(platform, form = {}) {
|
||||
updates.DISCORD_BOT_TOKEN = String(form.token || '').trim()
|
||||
updates.DISCORD_ALLOWED_USERS = csvEnvValue(form.allowFrom)
|
||||
if (Object.hasOwn(form, 'requireMention')) updates.DISCORD_REQUIRE_MENTION = boolEnvValue(form.requireMention)
|
||||
updates.DISCORD_FREE_RESPONSE_CHANNELS = csvEnvValue(form.freeResponseChannels)
|
||||
updates.DISCORD_ALLOWED_CHANNELS = csvEnvValue(form.allowedChannels)
|
||||
updates.DISCORD_IGNORED_CHANNELS = csvEnvValue(form.ignoredChannels)
|
||||
updates.DISCORD_NO_THREAD_CHANNELS = csvEnvValue(form.noThreadChannels)
|
||||
if (Object.hasOwn(form, 'autoThread')) updates.DISCORD_AUTO_THREAD = boolEnvValue(form.autoThread)
|
||||
if (Object.hasOwn(form, 'reactions')) updates.DISCORD_REACTIONS = boolEnvValue(form.reactions)
|
||||
if (Object.hasOwn(form, 'threadRequireMention')) updates.DISCORD_THREAD_REQUIRE_MENTION = boolEnvValue(form.threadRequireMention)
|
||||
if (Object.hasOwn(form, 'historyBackfill')) updates.DISCORD_HISTORY_BACKFILL = boolEnvValue(form.historyBackfill)
|
||||
updates.DISCORD_HISTORY_BACKFILL_LIMIT = String(form.historyBackfillLimit || '').trim()
|
||||
updates.DISCORD_REPLY_TO_MODE = String(form.replyToMode || '').trim()
|
||||
updates.DISCORD_HOME_CHANNEL = String(form.homeChannel || '').trim()
|
||||
updates.DISCORD_HOME_CHANNEL_NAME = String(form.homeChannelName || '').trim()
|
||||
} else if (platform === 'slack') {
|
||||
updates.SLACK_BOT_TOKEN = String(form.botToken || '').trim()
|
||||
updates.SLACK_APP_TOKEN = String(form.appToken || '').trim()
|
||||
|
||||
@@ -2279,6 +2279,21 @@ fn put_json_string_from_env(
|
||||
}
|
||||
}
|
||||
|
||||
fn put_json_bool_from_env(
|
||||
form: &mut serde_json::Map<String, Value>,
|
||||
env_values: &std::collections::HashMap<String, String>,
|
||||
env_key: &str,
|
||||
json_key: &str,
|
||||
) {
|
||||
if let Some(value) = hermes_env_value(env_values, env_key) {
|
||||
let enabled = matches!(
|
||||
value.trim().to_ascii_lowercase().as_str(),
|
||||
"true" | "1" | "yes" | "on"
|
||||
);
|
||||
form.insert(json_key.to_string(), Value::Bool(enabled));
|
||||
}
|
||||
}
|
||||
|
||||
fn build_hermes_channel_config_values(
|
||||
config: &serde_yaml::Value,
|
||||
env_values: &std::collections::HashMap<String, String>,
|
||||
@@ -2313,6 +2328,79 @@ fn build_hermes_channel_config_values(
|
||||
.or_else(|| yaml_string_field(&entry, "token"))
|
||||
.unwrap_or_default();
|
||||
form.insert("token".to_string(), Value::String(token));
|
||||
for (yaml_key_name, json_key_name, env_key_name) in [
|
||||
(
|
||||
"free_response_channels",
|
||||
"freeResponseChannels",
|
||||
"DISCORD_FREE_RESPONSE_CHANNELS",
|
||||
),
|
||||
(
|
||||
"allowed_channels",
|
||||
"allowedChannels",
|
||||
"DISCORD_ALLOWED_CHANNELS",
|
||||
),
|
||||
(
|
||||
"ignored_channels",
|
||||
"ignoredChannels",
|
||||
"DISCORD_IGNORED_CHANNELS",
|
||||
),
|
||||
(
|
||||
"no_thread_channels",
|
||||
"noThreadChannels",
|
||||
"DISCORD_NO_THREAD_CHANNELS",
|
||||
),
|
||||
] {
|
||||
insert_json_csv_if_present(&mut form, &extra, yaml_key_name, json_key_name);
|
||||
put_json_string_from_env(&mut form, env_values, env_key_name, json_key_name);
|
||||
}
|
||||
for (yaml_key_name, json_key_name, env_key_name) in [
|
||||
("auto_thread", "autoThread", "DISCORD_AUTO_THREAD"),
|
||||
("reactions", "reactions", "DISCORD_REACTIONS"),
|
||||
(
|
||||
"thread_require_mention",
|
||||
"threadRequireMention",
|
||||
"DISCORD_THREAD_REQUIRE_MENTION",
|
||||
),
|
||||
(
|
||||
"history_backfill",
|
||||
"historyBackfill",
|
||||
"DISCORD_HISTORY_BACKFILL",
|
||||
),
|
||||
] {
|
||||
insert_json_bool_if_present(&mut form, &extra, yaml_key_name, json_key_name);
|
||||
put_json_bool_from_env(&mut form, env_values, env_key_name, json_key_name);
|
||||
}
|
||||
insert_json_string_if_present(
|
||||
&mut form,
|
||||
&extra,
|
||||
"history_backfill_limit",
|
||||
"historyBackfillLimit",
|
||||
);
|
||||
put_json_string_from_env(
|
||||
&mut form,
|
||||
env_values,
|
||||
"DISCORD_HISTORY_BACKFILL_LIMIT",
|
||||
"historyBackfillLimit",
|
||||
);
|
||||
insert_json_string_if_present(&mut form, &extra, "reply_to_mode", "replyToMode");
|
||||
put_json_string_from_env(
|
||||
&mut form,
|
||||
env_values,
|
||||
"DISCORD_REPLY_TO_MODE",
|
||||
"replyToMode",
|
||||
);
|
||||
put_json_string_from_env(
|
||||
&mut form,
|
||||
env_values,
|
||||
"DISCORD_HOME_CHANNEL",
|
||||
"homeChannel",
|
||||
);
|
||||
put_json_string_from_env(
|
||||
&mut form,
|
||||
env_values,
|
||||
"DISCORD_HOME_CHANNEL_NAME",
|
||||
"homeChannelName",
|
||||
);
|
||||
}
|
||||
"slack" => {
|
||||
let bot_token = hermes_env_value(env_values, "SLACK_BOT_TOKEN")
|
||||
@@ -2568,7 +2656,35 @@ fn merge_hermes_channel_config(
|
||||
|
||||
match platform {
|
||||
"telegram" => delete_yaml_key(entry, "token"),
|
||||
"discord" => delete_yaml_key(entry, "token"),
|
||||
"discord" => {
|
||||
delete_yaml_key(entry, "token");
|
||||
for (form_key_name, extra_key_name) in [
|
||||
("freeResponseChannels", "free_response_channels"),
|
||||
("allowedChannels", "allowed_channels"),
|
||||
("ignoredChannels", "ignored_channels"),
|
||||
("noThreadChannels", "no_thread_channels"),
|
||||
] {
|
||||
if let Some(values) = form_string_array(form, form_key_name) {
|
||||
set_extra_string_array(entry, extra_key_name, values);
|
||||
}
|
||||
}
|
||||
for (form_key_name, extra_key_name) in [
|
||||
("autoThread", "auto_thread"),
|
||||
("reactions", "reactions"),
|
||||
("threadRequireMention", "thread_require_mention"),
|
||||
("historyBackfill", "history_backfill"),
|
||||
] {
|
||||
if let Some(value) = form_bool(form, form_key_name) {
|
||||
set_extra_bool(entry, extra_key_name, value);
|
||||
}
|
||||
}
|
||||
set_extra_string_if_present(
|
||||
entry,
|
||||
"history_backfill_limit",
|
||||
form_string(form, "historyBackfillLimit"),
|
||||
);
|
||||
set_extra_string_if_present(entry, "reply_to_mode", form_string(form, "replyToMode"));
|
||||
}
|
||||
"slack" => {
|
||||
delete_yaml_key(entry, "token");
|
||||
delete_extra_key(entry, "app_token");
|
||||
@@ -2740,6 +2856,50 @@ fn build_hermes_channel_env_updates(platform: &str, form: &Value) -> Vec<(String
|
||||
if let Some(value) = form_bool(form, "requireMention") {
|
||||
push("DISCORD_REQUIRE_MENTION", bool_env_value(value));
|
||||
}
|
||||
push(
|
||||
"DISCORD_FREE_RESPONSE_CHANNELS",
|
||||
csv_env_value(form, "freeResponseChannels"),
|
||||
);
|
||||
push(
|
||||
"DISCORD_ALLOWED_CHANNELS",
|
||||
csv_env_value(form, "allowedChannels"),
|
||||
);
|
||||
push(
|
||||
"DISCORD_IGNORED_CHANNELS",
|
||||
csv_env_value(form, "ignoredChannels"),
|
||||
);
|
||||
push(
|
||||
"DISCORD_NO_THREAD_CHANNELS",
|
||||
csv_env_value(form, "noThreadChannels"),
|
||||
);
|
||||
if let Some(value) = form_bool(form, "autoThread") {
|
||||
push("DISCORD_AUTO_THREAD", bool_env_value(value));
|
||||
}
|
||||
if let Some(value) = form_bool(form, "reactions") {
|
||||
push("DISCORD_REACTIONS", bool_env_value(value));
|
||||
}
|
||||
if let Some(value) = form_bool(form, "threadRequireMention") {
|
||||
push("DISCORD_THREAD_REQUIRE_MENTION", bool_env_value(value));
|
||||
}
|
||||
if let Some(value) = form_bool(form, "historyBackfill") {
|
||||
push("DISCORD_HISTORY_BACKFILL", bool_env_value(value));
|
||||
}
|
||||
push(
|
||||
"DISCORD_HISTORY_BACKFILL_LIMIT",
|
||||
form_string(form, "historyBackfillLimit").unwrap_or_default(),
|
||||
);
|
||||
push(
|
||||
"DISCORD_REPLY_TO_MODE",
|
||||
form_string(form, "replyToMode").unwrap_or_default(),
|
||||
);
|
||||
push(
|
||||
"DISCORD_HOME_CHANNEL",
|
||||
form_string(form, "homeChannel").unwrap_or_default(),
|
||||
);
|
||||
push(
|
||||
"DISCORD_HOME_CHANNEL_NAME",
|
||||
form_string(form, "homeChannelName").unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
"slack" => {
|
||||
push(
|
||||
@@ -2837,6 +2997,18 @@ fn write_hermes_channel_env(platform: &str, form: &Value) -> Result<(), String>
|
||||
"DISCORD_BOT_TOKEN",
|
||||
"DISCORD_ALLOWED_USERS",
|
||||
"DISCORD_REQUIRE_MENTION",
|
||||
"DISCORD_FREE_RESPONSE_CHANNELS",
|
||||
"DISCORD_ALLOWED_CHANNELS",
|
||||
"DISCORD_IGNORED_CHANNELS",
|
||||
"DISCORD_NO_THREAD_CHANNELS",
|
||||
"DISCORD_AUTO_THREAD",
|
||||
"DISCORD_REACTIONS",
|
||||
"DISCORD_THREAD_REQUIRE_MENTION",
|
||||
"DISCORD_HISTORY_BACKFILL",
|
||||
"DISCORD_HISTORY_BACKFILL_LIMIT",
|
||||
"DISCORD_REPLY_TO_MODE",
|
||||
"DISCORD_HOME_CHANNEL",
|
||||
"DISCORD_HOME_CHANNEL_NAME",
|
||||
],
|
||||
"slack" => vec![
|
||||
"SLACK_BOT_TOKEN",
|
||||
@@ -7948,6 +8120,150 @@ platforms:
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discord_channel_supports_plugin_runtime_fields() {
|
||||
let mut config: serde_yaml::Value = serde_yaml::from_str(
|
||||
r#"
|
||||
platforms:
|
||||
discord:
|
||||
enabled: true
|
||||
token: old-token
|
||||
extra:
|
||||
unknown_option: keep-me
|
||||
free_response_channels: ["yaml-free"]
|
||||
auto_thread: true
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let mut env = HashMap::new();
|
||||
env.insert(
|
||||
"DISCORD_BOT_TOKEN".to_string(),
|
||||
"env-discord-token".to_string(),
|
||||
);
|
||||
env.insert(
|
||||
"DISCORD_FREE_RESPONSE_CHANNELS".to_string(),
|
||||
"env-free".to_string(),
|
||||
);
|
||||
env.insert("DISCORD_AUTO_THREAD".to_string(), "false".to_string());
|
||||
env.insert("DISCORD_HOME_CHANNEL".to_string(), "home-1".to_string());
|
||||
|
||||
let values = build_hermes_channel_config_values(&config, &env);
|
||||
assert_eq!(values["discord"]["token"], "env-discord-token");
|
||||
assert_eq!(values["discord"]["freeResponseChannels"], "env-free");
|
||||
assert_eq!(values["discord"]["autoThread"], false);
|
||||
assert_eq!(values["discord"]["homeChannel"], "home-1");
|
||||
|
||||
merge_hermes_channel_config(
|
||||
&mut config,
|
||||
"discord",
|
||||
&json!({
|
||||
"enabled": true,
|
||||
"token": "discord-token",
|
||||
"allowFrom": "1001, 1002",
|
||||
"requireMention": true,
|
||||
"freeResponseChannels": "free-a\nfree-b",
|
||||
"allowedChannels": "allow-a",
|
||||
"ignoredChannels": "ignore-a",
|
||||
"noThreadChannels": "plain-a",
|
||||
"autoThread": false,
|
||||
"reactions": true,
|
||||
"threadRequireMention": true,
|
||||
"historyBackfill": true,
|
||||
"historyBackfillLimit": "12",
|
||||
"replyToMode": "off",
|
||||
"homeChannel": "home-1",
|
||||
"homeChannelName": "ops-home",
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
config["platforms"]["discord"]["token"],
|
||||
serde_yaml::Value::Null
|
||||
);
|
||||
assert_eq!(
|
||||
config["platforms"]["discord"]["extra"]["free_response_channels"]
|
||||
.as_sequence()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter_map(|item| item.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["free-a", "free-b"]
|
||||
);
|
||||
assert_eq!(
|
||||
config["platforms"]["discord"]["extra"]["allowed_channels"]
|
||||
.as_sequence()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter_map(|item| item.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["allow-a"]
|
||||
);
|
||||
assert_eq!(
|
||||
config["platforms"]["discord"]["extra"]["auto_thread"].as_bool(),
|
||||
Some(false)
|
||||
);
|
||||
assert_eq!(
|
||||
config["platforms"]["discord"]["extra"]["reactions"].as_bool(),
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
config["platforms"]["discord"]["extra"]["thread_require_mention"].as_bool(),
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
config["platforms"]["discord"]["extra"]["history_backfill"].as_bool(),
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
config["platforms"]["discord"]["extra"]["history_backfill_limit"].as_str(),
|
||||
Some("12")
|
||||
);
|
||||
assert_eq!(
|
||||
config["platforms"]["discord"]["extra"]["reply_to_mode"].as_str(),
|
||||
Some("off")
|
||||
);
|
||||
assert_eq!(
|
||||
config["platforms"]["discord"]["extra"]["unknown_option"].as_str(),
|
||||
Some("keep-me")
|
||||
);
|
||||
|
||||
let env_updates = build_hermes_channel_env_updates(
|
||||
"discord",
|
||||
&json!({
|
||||
"token": "discord-token",
|
||||
"allowFrom": "1001, 1002",
|
||||
"requireMention": true,
|
||||
"freeResponseChannels": "free-a\nfree-b",
|
||||
"allowedChannels": "allow-a",
|
||||
"ignoredChannels": "ignore-a",
|
||||
"noThreadChannels": "plain-a",
|
||||
"autoThread": false,
|
||||
"reactions": true,
|
||||
"threadRequireMention": true,
|
||||
"historyBackfill": true,
|
||||
"historyBackfillLimit": "12",
|
||||
"replyToMode": "off",
|
||||
"homeChannel": "home-1",
|
||||
"homeChannelName": "ops-home",
|
||||
}),
|
||||
);
|
||||
|
||||
assert!(
|
||||
env_updates.contains(&("DISCORD_BOT_TOKEN".to_string(), "discord-token".to_string()))
|
||||
);
|
||||
assert!(env_updates.contains(&(
|
||||
"DISCORD_FREE_RESPONSE_CHANNELS".to_string(),
|
||||
"free-a,free-b".to_string()
|
||||
)));
|
||||
assert!(env_updates.contains(&("DISCORD_AUTO_THREAD".to_string(), "false".to_string())));
|
||||
assert!(env_updates.contains(&(
|
||||
"DISCORD_THREAD_REQUIRE_MENTION".to_string(),
|
||||
"true".to_string()
|
||||
)));
|
||||
assert!(env_updates.contains(&("DISCORD_HOME_CHANNEL".to_string(), "home-1".to_string())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_dingtalk_channel_uses_runtime_fields() {
|
||||
let mut config: serde_yaml::Value = serde_yaml::from_str(
|
||||
|
||||
@@ -26,6 +26,22 @@ const CHANNELS = [
|
||||
secretFields: ['token'],
|
||||
fields: [
|
||||
{ key: 'token', labelKey: 'engine.hermesChannelBotToken', type: 'password', placeholder: 'MTA...' },
|
||||
{ key: 'homeChannel', labelKey: 'engine.hermesChannelDiscordHomeChannel', type: 'text', placeholder: '123456789012345678' },
|
||||
{ key: 'homeChannelName', labelKey: 'engine.hermesChannelDiscordHomeChannelName', type: 'text', placeholder: 'ops' },
|
||||
],
|
||||
advancedFields: [
|
||||
{ key: 'freeResponseChannels', labelKey: 'engine.hermesChannelDiscordFreeResponseChannels', type: 'textarea', placeholderKey: 'engine.hermesChannelDiscordFreeResponseChannelsPh' },
|
||||
{ key: 'allowedChannels', labelKey: 'engine.hermesChannelDiscordAllowedChannels', type: 'textarea', placeholderKey: 'engine.hermesChannelDiscordChannelListPh' },
|
||||
{ key: 'ignoredChannels', labelKey: 'engine.hermesChannelDiscordIgnoredChannels', type: 'textarea', placeholderKey: 'engine.hermesChannelDiscordChannelListPh' },
|
||||
{ key: 'noThreadChannels', labelKey: 'engine.hermesChannelDiscordNoThreadChannels', type: 'textarea', placeholderKey: 'engine.hermesChannelDiscordChannelListPh' },
|
||||
{ key: 'historyBackfillLimit', labelKey: 'engine.hermesChannelDiscordHistoryBackfillLimit', type: 'text', placeholder: '12' },
|
||||
{ key: 'replyToMode', labelKey: 'engine.hermesChannelDiscordReplyToMode', type: 'select', options: [['first', 'engine.hermesChannelDiscordReplyFirst'], ['all', 'engine.hermesChannelDiscordReplyAll'], ['off', 'engine.hermesChannelDiscordReplyOff']] },
|
||||
],
|
||||
advancedToggles: [
|
||||
{ key: 'autoThread', labelKey: 'engine.hermesChannelDiscordAutoThread' },
|
||||
{ key: 'reactions', labelKey: 'engine.hermesChannelDiscordReactions' },
|
||||
{ key: 'threadRequireMention', labelKey: 'engine.hermesChannelDiscordThreadRequireMention' },
|
||||
{ key: 'historyBackfill', labelKey: 'engine.hermesChannelDiscordHistoryBackfill' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -109,6 +125,13 @@ function defaultForm(platform) {
|
||||
form.typingIndicator = true
|
||||
form.resolveSenderNames = true
|
||||
}
|
||||
if (platform === 'discord') {
|
||||
form.autoThread = true
|
||||
form.reactions = true
|
||||
form.threadRequireMention = false
|
||||
form.historyBackfill = false
|
||||
form.replyToMode = 'first'
|
||||
}
|
||||
if (platform === 'slack') form.webhookPath = '/slack/events'
|
||||
return form
|
||||
}
|
||||
@@ -284,6 +307,25 @@ export function render() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${(channel.advancedFields || []).length ? `
|
||||
<div class="hm-channel-section">
|
||||
<div class="hm-channel-section-title">${esc(t('engine.hermesChannelRuntimeBehavior'))}</div>
|
||||
${(channel.advancedToggles || []).length ? `
|
||||
<div class="hm-channel-toggle-grid">
|
||||
${channel.advancedToggles.map(toggle => `
|
||||
<label class="hm-channel-check">
|
||||
<input class="hm-channel-input" data-key="${esc(toggle.key)}" type="checkbox" ${form[toggle.key] ? 'checked' : ''} ${disabled ? 'disabled' : ''}>
|
||||
<span>${esc(t(toggle.labelKey))}</span>
|
||||
</label>
|
||||
`).join('')}
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="hm-field-row">
|
||||
${channel.advancedFields.map(field => renderField(field, form, disabled)).join('')}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="hm-channel-footnote">
|
||||
${icon('info', 14)}
|
||||
<span>${esc(t('engine.hermesChannelRestartHint'))}</span>
|
||||
|
||||
@@ -937,7 +937,25 @@ export default {
|
||||
hermesChannelSaveFailed: _('保存渠道配置失败', 'Failed to save channel configuration', '儲存頻道設定失敗'),
|
||||
hermesChannelCredentials: _('凭证', 'Credentials', '憑證'),
|
||||
hermesChannelAccessPolicy: _('访问策略', 'Access Policy', '存取策略'),
|
||||
hermesChannelRuntimeBehavior: _('运行行为', 'Runtime Behavior', '執行行為'),
|
||||
hermesChannelBotToken: _('Bot Token', 'Bot Token', 'Bot Token'),
|
||||
hermesChannelDiscordHomeChannel: _('默认频道 ID', 'Home Channel ID', '預設頻道 ID'),
|
||||
hermesChannelDiscordHomeChannelName: _('默认频道名称', 'Home Channel Name', '預設頻道名稱'),
|
||||
hermesChannelDiscordFreeResponseChannels: _('免 @ 响应频道', 'Free Response Channels', '免 @ 回應頻道'),
|
||||
hermesChannelDiscordAllowedChannels: _('允许频道', 'Allowed Channels', '允許頻道'),
|
||||
hermesChannelDiscordIgnoredChannels: _('忽略频道', 'Ignored Channels', '忽略頻道'),
|
||||
hermesChannelDiscordNoThreadChannels: _('不自动建线程频道', 'No-thread Channels', '不自動建立討論串頻道'),
|
||||
hermesChannelDiscordChannelListPh: _('每行或逗号分隔一个 Discord 频道 ID。', 'One Discord channel ID per line or comma-separated.', '每行或逗號分隔一個 Discord 頻道 ID。'),
|
||||
hermesChannelDiscordFreeResponseChannelsPh: _('这些频道内无需 @Bot 即可响应;支持频道或父频道 ID。', 'Messages in these channels do not require @mention; channel or parent channel IDs are supported.', '這些頻道內無需 @Bot 即可回應;支援頻道或父頻道 ID。'),
|
||||
hermesChannelDiscordHistoryBackfillLimit: _('上下文回填条数', 'History Backfill Limit', '上下文回填筆數'),
|
||||
hermesChannelDiscordReplyToMode: _('回复模式', 'Reply Mode', '回覆模式'),
|
||||
hermesChannelDiscordReplyFirst: _('仅首条回复引用', 'Reply to first message', '僅首則回覆引用'),
|
||||
hermesChannelDiscordReplyAll: _('每条回复都引用', 'Reply to every message', '每則回覆都引用'),
|
||||
hermesChannelDiscordReplyOff: _('不引用回复', 'No reply reference', '不引用回覆'),
|
||||
hermesChannelDiscordAutoThread: _('自动创建线程', 'Auto-create threads', '自動建立討論串'),
|
||||
hermesChannelDiscordReactions: _('启用表情反馈', 'Enable reactions', '啟用表情回饋'),
|
||||
hermesChannelDiscordThreadRequireMention: _('线程内也要求 @Bot', 'Require @mention in threads', '討論串內也要求 @Bot'),
|
||||
hermesChannelDiscordHistoryBackfill: _('触发时回填频道上下文', 'Backfill channel context on trigger', '觸發時回填頻道上下文'),
|
||||
hermesChannelSlackBotToken: _('Bot Token (xoxb)', 'Bot Token (xoxb)', 'Bot Token (xoxb)'),
|
||||
hermesChannelSlackAppToken: _('App Token (xapp)', 'App Token (xapp)', 'App Token (xapp)'),
|
||||
hermesChannelSigningSecret: _('Signing Secret', 'Signing Secret', 'Signing Secret'),
|
||||
|
||||
@@ -146,6 +146,19 @@ function bindClick(page) {
|
||||
if (option) chooseWithAnimation(page, panel, option, engine)
|
||||
})
|
||||
|
||||
// CTA 按钮本身位于内容层,不一定会命中背景三角形面板;
|
||||
// 单独绑定可以保证移动端和键盘操作都能真正完成引擎选择。
|
||||
page.querySelectorAll('[data-engine-cta]').forEach(btn => {
|
||||
btn.addEventListener('click', (event) => {
|
||||
event.stopPropagation()
|
||||
if (_busy) return
|
||||
const engine = btn.dataset.engineCta
|
||||
const option = PRIMARY_OPTIONS.find(o => o.id === engine)
|
||||
const panel = page.querySelector(`.es-panel[data-engine="${engine}"]`)
|
||||
if (option && panel) chooseWithAnimation(page, panel, option, engine)
|
||||
})
|
||||
})
|
||||
|
||||
// 次级链接:两个都要 / 稍后再说(无对角线动画,直接走选择)
|
||||
page.querySelectorAll('[data-secondary]').forEach(btn => {
|
||||
btn.addEventListener('click', async (event) => {
|
||||
|
||||
@@ -183,6 +183,129 @@ test('Hermes 渠道保存会生成运行时仍会读取的环境变量', () => {
|
||||
assert.equal(dingTalkEnv.DINGTALK_REQUIRE_MENTION, 'true')
|
||||
})
|
||||
|
||||
test('Hermes Discord 读取会回显新版插件运行字段并优先使用环境变量', () => {
|
||||
const values = buildHermesChannelConfigValues({
|
||||
platforms: {
|
||||
discord: {
|
||||
enabled: true,
|
||||
extra: {
|
||||
require_mention: true,
|
||||
thread_require_mention: true,
|
||||
free_response_channels: ['free-a', 'free-b'],
|
||||
allowed_channels: ['allow-a'],
|
||||
ignored_channels: ['ignore-a'],
|
||||
no_thread_channels: ['plain-a'],
|
||||
auto_thread: true,
|
||||
reactions: false,
|
||||
history_backfill: true,
|
||||
history_backfill_limit: '12',
|
||||
reply_to_mode: 'all',
|
||||
},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
DISCORD_BOT_TOKEN: 'env-discord-token',
|
||||
DISCORD_HOME_CHANNEL: 'home-1',
|
||||
DISCORD_HOME_CHANNEL_NAME: 'ops-home',
|
||||
DISCORD_FREE_RESPONSE_CHANNELS: 'env-free',
|
||||
DISCORD_AUTO_THREAD: 'false',
|
||||
})
|
||||
|
||||
assert.equal(values.discord.enabled, true)
|
||||
assert.equal(values.discord.token, 'env-discord-token')
|
||||
assert.equal(values.discord.freeResponseChannels, 'env-free')
|
||||
assert.equal(values.discord.allowedChannels, 'allow-a')
|
||||
assert.equal(values.discord.ignoredChannels, 'ignore-a')
|
||||
assert.equal(values.discord.noThreadChannels, 'plain-a')
|
||||
assert.equal(values.discord.autoThread, false)
|
||||
assert.equal(values.discord.reactions, false)
|
||||
assert.equal(values.discord.threadRequireMention, true)
|
||||
assert.equal(values.discord.historyBackfill, true)
|
||||
assert.equal(values.discord.historyBackfillLimit, '12')
|
||||
assert.equal(values.discord.replyToMode, 'all')
|
||||
assert.equal(values.discord.homeChannel, 'home-1')
|
||||
assert.equal(values.discord.homeChannelName, 'ops-home')
|
||||
})
|
||||
|
||||
test('Hermes Discord 保存会写入新版插件 YAML 字段和运行时环境变量', () => {
|
||||
const next = mergeHermesChannelConfig({
|
||||
platforms: {
|
||||
discord: {
|
||||
enabled: true,
|
||||
token: 'old-token',
|
||||
extra: {
|
||||
unknown_option: 'keep-me',
|
||||
},
|
||||
},
|
||||
},
|
||||
}, 'discord', {
|
||||
enabled: true,
|
||||
token: 'discord-token',
|
||||
allowFrom: '1001, 1002',
|
||||
requireMention: true,
|
||||
freeResponseChannels: 'free-a\nfree-b',
|
||||
allowedChannels: 'allow-a',
|
||||
ignoredChannels: 'ignore-a',
|
||||
noThreadChannels: 'plain-a',
|
||||
autoThread: false,
|
||||
reactions: true,
|
||||
threadRequireMention: true,
|
||||
historyBackfill: true,
|
||||
historyBackfillLimit: '12',
|
||||
replyToMode: 'off',
|
||||
homeChannel: 'home-1',
|
||||
homeChannelName: 'ops-home',
|
||||
})
|
||||
|
||||
assert.equal(next.platforms.discord.enabled, true)
|
||||
assert.equal(next.platforms.discord.token, undefined)
|
||||
assert.deepEqual(next.platforms.discord.extra.allow_from, ['1001', '1002'])
|
||||
assert.deepEqual(next.platforms.discord.extra.free_response_channels, ['free-a', 'free-b'])
|
||||
assert.deepEqual(next.platforms.discord.extra.allowed_channels, ['allow-a'])
|
||||
assert.deepEqual(next.platforms.discord.extra.ignored_channels, ['ignore-a'])
|
||||
assert.deepEqual(next.platforms.discord.extra.no_thread_channels, ['plain-a'])
|
||||
assert.equal(next.platforms.discord.extra.auto_thread, false)
|
||||
assert.equal(next.platforms.discord.extra.reactions, true)
|
||||
assert.equal(next.platforms.discord.extra.thread_require_mention, true)
|
||||
assert.equal(next.platforms.discord.extra.history_backfill, true)
|
||||
assert.equal(next.platforms.discord.extra.history_backfill_limit, '12')
|
||||
assert.equal(next.platforms.discord.extra.reply_to_mode, 'off')
|
||||
assert.equal(next.platforms.discord.extra.unknown_option, 'keep-me')
|
||||
|
||||
const env = buildHermesChannelEnvUpdates('discord', {
|
||||
token: 'discord-token',
|
||||
allowFrom: '1001, 1002',
|
||||
requireMention: true,
|
||||
freeResponseChannels: 'free-a\nfree-b',
|
||||
allowedChannels: 'allow-a',
|
||||
ignoredChannels: 'ignore-a',
|
||||
noThreadChannels: 'plain-a',
|
||||
autoThread: false,
|
||||
reactions: true,
|
||||
threadRequireMention: true,
|
||||
historyBackfill: true,
|
||||
historyBackfillLimit: '12',
|
||||
replyToMode: 'off',
|
||||
homeChannel: 'home-1',
|
||||
homeChannelName: 'ops-home',
|
||||
})
|
||||
|
||||
assert.equal(env.DISCORD_BOT_TOKEN, 'discord-token')
|
||||
assert.equal(env.DISCORD_ALLOWED_USERS, '1001,1002')
|
||||
assert.equal(env.DISCORD_FREE_RESPONSE_CHANNELS, 'free-a,free-b')
|
||||
assert.equal(env.DISCORD_ALLOWED_CHANNELS, 'allow-a')
|
||||
assert.equal(env.DISCORD_IGNORED_CHANNELS, 'ignore-a')
|
||||
assert.equal(env.DISCORD_NO_THREAD_CHANNELS, 'plain-a')
|
||||
assert.equal(env.DISCORD_AUTO_THREAD, 'false')
|
||||
assert.equal(env.DISCORD_REACTIONS, 'true')
|
||||
assert.equal(env.DISCORD_THREAD_REQUIRE_MENTION, 'true')
|
||||
assert.equal(env.DISCORD_HISTORY_BACKFILL, 'true')
|
||||
assert.equal(env.DISCORD_HISTORY_BACKFILL_LIMIT, '12')
|
||||
assert.equal(env.DISCORD_REPLY_TO_MODE, 'off')
|
||||
assert.equal(env.DISCORD_HOME_CHANNEL, 'home-1')
|
||||
assert.equal(env.DISCORD_HOME_CHANNEL_NAME, 'ops-home')
|
||||
})
|
||||
|
||||
test('Hermes 渠道保存会从 YAML 清理旧凭证,避免覆盖 .env 运行时值', () => {
|
||||
const next = mergeHermesChannelConfig({
|
||||
platforms: {
|
||||
|
||||
Reference in New Issue
Block a user