mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-29 04:10:00 +08:00
feat(hermes): add telegram runtime options
This commit is contained in:
@@ -3340,6 +3340,7 @@ const HERMES_PROVIDER_ROUTING_SORTS = new Set(['price', 'throughput', 'latency']
|
||||
const HERMES_PROVIDER_ROUTING_DATA_COLLECTION = new Set(['allow', 'deny'])
|
||||
const HERMES_DISPLAY_TOOL_PROGRESS_VALUES = new Set(['off', 'new', 'all', 'verbose'])
|
||||
const HERMES_DISPLAY_STREAMING_VALUES = new Set(['inherit', 'true', 'false'])
|
||||
const HERMES_TELEGRAM_REPLY_TO_MODE_VALUES = new Set(['off', 'first', 'all'])
|
||||
const HERMES_DISPLAY_RESUME_VALUES = new Set(['full', 'minimal'])
|
||||
const HERMES_DISPLAY_BUSY_INPUT_MODES = new Set(['interrupt', 'queue', 'steer'])
|
||||
const HERMES_DISPLAY_BACKGROUND_PROCESS_NOTIFICATIONS = new Set(['off', 'result', 'error', 'all'])
|
||||
@@ -5386,6 +5387,13 @@ function normalizeHermesGroupPolicy(raw) {
|
||||
return 'allowlist'
|
||||
}
|
||||
|
||||
function normalizeHermesTelegramReplyToMode(raw, strict = false) {
|
||||
const value = String(raw || '').trim().toLowerCase() || 'first'
|
||||
if (HERMES_TELEGRAM_REPLY_TO_MODE_VALUES.has(value)) return value
|
||||
if (strict) throw new Error('platforms.telegram.extra.reply_to_mode 必须是 off、first 或 all')
|
||||
return 'first'
|
||||
}
|
||||
|
||||
function readHermesPlatform(config, platform) {
|
||||
const platforms = config?.platforms && typeof config.platforms === 'object' ? config.platforms : {}
|
||||
const entry = platforms?.[platform] && typeof platforms[platform] === 'object' ? platforms[platform] : {}
|
||||
@@ -5400,6 +5408,12 @@ export function buildHermesChannelConfigValues(config = {}, envValues = {}) {
|
||||
const form = { enabled: entry.enabled === true }
|
||||
if (platform === 'telegram') {
|
||||
form.botToken = hermesEnvValue(envValues, 'TELEGRAM_BOT_TOKEN') || (typeof entry.token === 'string' ? entry.token : '')
|
||||
putHermesString(form, extra, 'reply_to_mode')
|
||||
putHermesBool(form, extra, 'guest_mode')
|
||||
putHermesBool(form, extra, 'disable_link_previews')
|
||||
form.replyToMode = normalizeHermesTelegramReplyToMode(hermesEnvValue(envValues, 'TELEGRAM_REPLY_TO_MODE') || form.replyToMode, false)
|
||||
putHermesEnvBool(form, envValues, 'TELEGRAM_GUEST_MODE', 'guestMode')
|
||||
putHermesEnvBool(form, envValues, 'TELEGRAM_DISABLE_LINK_PREVIEWS', 'disableLinkPreviews')
|
||||
} else if (platform === 'discord') {
|
||||
form.token = hermesEnvValue(envValues, 'DISCORD_BOT_TOKEN') || (typeof entry.token === 'string' ? entry.token : '')
|
||||
for (const [yamlKey, formKey] of [
|
||||
@@ -5611,6 +5625,11 @@ function normalizeHermesChannelForm(platform, form = {}) {
|
||||
normalized.historyBackfillLimit = String(normalized.historyBackfillLimit || '').trim()
|
||||
normalized.replyToMode = String(normalized.replyToMode || '').trim()
|
||||
}
|
||||
if (platform === 'telegram') {
|
||||
normalized.replyToMode = normalizeHermesTelegramReplyToMode(normalized.replyToMode, true)
|
||||
if (Object.hasOwn(normalized, 'guestMode')) normalized.guestMode = normalized.guestMode === true || normalized.guestMode === 'true' || normalized.guestMode === 'on'
|
||||
if (Object.hasOwn(normalized, 'disableLinkPreviews')) normalized.disableLinkPreviews = normalized.disableLinkPreviews === true || normalized.disableLinkPreviews === 'true' || normalized.disableLinkPreviews === 'on'
|
||||
}
|
||||
if (platform === 'irc') {
|
||||
if (Object.hasOwn(normalized, 'useTls')) normalized.useTls = normalized.useTls === true || normalized.useTls === 'true' || normalized.useTls === 'on'
|
||||
}
|
||||
@@ -5697,6 +5716,9 @@ export function mergeHermesChannelConfig(config = {}, platform, form = {}) {
|
||||
entry.enabled = normalized.enabled
|
||||
if (normalizedPlatform === 'telegram') {
|
||||
deleteHermesEntryKey(entry, 'token')
|
||||
setHermesExtra(entry, 'reply_to_mode', normalized.replyToMode)
|
||||
if (Object.hasOwn(normalized, 'guestMode')) setHermesExtra(entry, 'guest_mode', !!normalized.guestMode)
|
||||
if (Object.hasOwn(normalized, 'disableLinkPreviews')) setHermesExtra(entry, 'disable_link_previews', !!normalized.disableLinkPreviews)
|
||||
} else if (normalizedPlatform === 'discord') {
|
||||
deleteHermesEntryKey(entry, 'token')
|
||||
for (const [formKey, extraKey] of [
|
||||
@@ -5860,6 +5882,9 @@ export function buildHermesChannelEnvUpdates(platform, form = {}) {
|
||||
updates.TELEGRAM_ALLOWED_USERS = csvEnvValue(form.allowFrom)
|
||||
updates.TELEGRAM_GROUP_ALLOWED_USERS = csvEnvValue(form.groupAllowFrom)
|
||||
if (Object.hasOwn(form, 'requireMention')) updates.TELEGRAM_REQUIRE_MENTION = boolEnvValue(form.requireMention)
|
||||
updates.TELEGRAM_REPLY_TO_MODE = normalizeHermesTelegramReplyToMode(form.replyToMode, true)
|
||||
if (Object.hasOwn(form, 'guestMode')) updates.TELEGRAM_GUEST_MODE = boolEnvValue(form.guestMode)
|
||||
if (Object.hasOwn(form, 'disableLinkPreviews')) updates.TELEGRAM_DISABLE_LINK_PREVIEWS = boolEnvValue(form.disableLinkPreviews)
|
||||
} else if (platform === 'discord') {
|
||||
updates.DISCORD_BOT_TOKEN = String(form.token || '').trim()
|
||||
updates.DISCORD_ALLOWED_USERS = csvEnvValue(form.allowFrom)
|
||||
|
||||
@@ -2167,6 +2167,7 @@ const HERMES_CHANNEL_PLATFORMS: [&str; 10] = [
|
||||
|
||||
const HERMES_DISPLAY_TOOL_PROGRESS_VALUES: [&str; 4] = ["off", "new", "all", "verbose"];
|
||||
const HERMES_DISPLAY_STREAMING_VALUES: [&str; 3] = ["inherit", "true", "false"];
|
||||
const HERMES_TELEGRAM_REPLY_TO_MODE_VALUES: [&str; 3] = ["off", "first", "all"];
|
||||
const HERMES_PROMPT_CACHE_TTLS: [&str; 2] = ["5m", "1h"];
|
||||
const HERMES_PROVIDER_ROUTING_SORTS: [&str; 3] = ["price", "throughput", "latency"];
|
||||
const HERMES_PROVIDER_ROUTING_DATA_COLLECTION: [&str; 2] = ["allow", "deny"];
|
||||
@@ -2228,6 +2229,25 @@ fn normalize_hermes_display_streaming_text(
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_hermes_telegram_reply_to_mode(
|
||||
value: Option<String>,
|
||||
strict: bool,
|
||||
) -> Result<String, String> {
|
||||
let mode = value.unwrap_or_default().trim().to_ascii_lowercase();
|
||||
let mode = if mode.is_empty() {
|
||||
"first".to_string()
|
||||
} else {
|
||||
mode
|
||||
};
|
||||
if HERMES_TELEGRAM_REPLY_TO_MODE_VALUES.contains(&mode.as_str()) {
|
||||
Ok(mode)
|
||||
} else if strict {
|
||||
Err("platforms.telegram.extra.reply_to_mode 必须是 off、first 或 all".to_string())
|
||||
} else {
|
||||
Ok("first".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_hermes_display_streaming_yaml(
|
||||
value: Option<&serde_yaml::Value>,
|
||||
strict: bool,
|
||||
@@ -2712,6 +2732,27 @@ fn build_hermes_channel_config_values(
|
||||
.or_else(|| yaml_string_field(&entry, "token"))
|
||||
.unwrap_or_default();
|
||||
form.insert("botToken".to_string(), Value::String(token));
|
||||
let reply_to_mode = normalize_hermes_telegram_reply_to_mode(
|
||||
hermes_env_value(env_values, "TELEGRAM_REPLY_TO_MODE")
|
||||
.or_else(|| yaml_string_field(&extra, "reply_to_mode")),
|
||||
false,
|
||||
)
|
||||
.unwrap_or_else(|_| "first".to_string());
|
||||
form.insert("replyToMode".to_string(), Value::String(reply_to_mode));
|
||||
insert_json_bool_if_present(&mut form, &extra, "guest_mode", "guestMode");
|
||||
insert_json_bool_if_present(
|
||||
&mut form,
|
||||
&extra,
|
||||
"disable_link_previews",
|
||||
"disableLinkPreviews",
|
||||
);
|
||||
put_json_bool_from_env(&mut form, env_values, "TELEGRAM_GUEST_MODE", "guestMode");
|
||||
put_json_bool_from_env(
|
||||
&mut form,
|
||||
env_values,
|
||||
"TELEGRAM_DISABLE_LINK_PREVIEWS",
|
||||
"disableLinkPreviews",
|
||||
);
|
||||
}
|
||||
"discord" => {
|
||||
let token = hermes_env_value(env_values, "DISCORD_BOT_TOKEN")
|
||||
@@ -8296,7 +8337,23 @@ fn merge_hermes_channel_config(
|
||||
);
|
||||
|
||||
match platform {
|
||||
"telegram" => delete_yaml_key(entry, "token"),
|
||||
"telegram" => {
|
||||
delete_yaml_key(entry, "token");
|
||||
set_extra_string_if_present(
|
||||
entry,
|
||||
"reply_to_mode",
|
||||
Some(normalize_hermes_telegram_reply_to_mode(
|
||||
form_string(form, "replyToMode"),
|
||||
true,
|
||||
)?),
|
||||
);
|
||||
if let Some(value) = form_bool(form, "guestMode") {
|
||||
set_extra_bool(entry, "guest_mode", value);
|
||||
}
|
||||
if let Some(value) = form_bool(form, "disableLinkPreviews") {
|
||||
set_extra_bool(entry, "disable_link_previews", value);
|
||||
}
|
||||
}
|
||||
"discord" => {
|
||||
delete_yaml_key(entry, "token");
|
||||
for (form_key_name, extra_key_name) in [
|
||||
@@ -8544,6 +8601,17 @@ fn build_hermes_channel_env_updates(platform: &str, form: &Value) -> Vec<(String
|
||||
if let Some(value) = form_bool(form, "requireMention") {
|
||||
push("TELEGRAM_REQUIRE_MENTION", bool_env_value(value));
|
||||
}
|
||||
push(
|
||||
"TELEGRAM_REPLY_TO_MODE",
|
||||
normalize_hermes_telegram_reply_to_mode(form_string(form, "replyToMode"), true)
|
||||
.unwrap_or_else(|_| "first".to_string()),
|
||||
);
|
||||
if let Some(value) = form_bool(form, "guestMode") {
|
||||
push("TELEGRAM_GUEST_MODE", bool_env_value(value));
|
||||
}
|
||||
if let Some(value) = form_bool(form, "disableLinkPreviews") {
|
||||
push("TELEGRAM_DISABLE_LINK_PREVIEWS", bool_env_value(value));
|
||||
}
|
||||
}
|
||||
"discord" => {
|
||||
push(
|
||||
@@ -19280,6 +19348,9 @@ platforms:
|
||||
"groupPolicy": "allowlist",
|
||||
"allowFrom": "1001, 1002",
|
||||
"requireMention": true,
|
||||
"replyToMode": "off",
|
||||
"guestMode": true,
|
||||
"disableLinkPreviews": true,
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
@@ -19296,15 +19367,39 @@ platforms:
|
||||
config["platforms"]["telegram"]["extra"]["unknown_option"].as_str(),
|
||||
Some("keep-me")
|
||||
);
|
||||
assert_eq!(
|
||||
config["platforms"]["telegram"]["extra"]["reply_to_mode"].as_str(),
|
||||
Some("off")
|
||||
);
|
||||
assert_eq!(
|
||||
config["platforms"]["telegram"]["extra"]["guest_mode"].as_bool(),
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
config["platforms"]["telegram"]["extra"]["disable_link_previews"].as_bool(),
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(values["telegram"]["replyToMode"], "off");
|
||||
assert_eq!(values["telegram"]["guestMode"], true);
|
||||
assert_eq!(values["telegram"]["disableLinkPreviews"], true);
|
||||
let env = build_hermes_channel_env_updates(
|
||||
"telegram",
|
||||
&json!({
|
||||
"botToken": "123:token",
|
||||
"allowFrom": "1001, 1002",
|
||||
"requireMention": true,
|
||||
"replyToMode": "off",
|
||||
"guestMode": true,
|
||||
"disableLinkPreviews": true,
|
||||
}),
|
||||
);
|
||||
assert!(env.contains(&("TELEGRAM_BOT_TOKEN".to_string(), "123:token".to_string())));
|
||||
assert!(env.contains(&("TELEGRAM_REPLY_TO_MODE".to_string(), "off".to_string())));
|
||||
assert!(env.contains(&("TELEGRAM_GUEST_MODE".to_string(), "true".to_string())));
|
||||
assert!(env.contains(&(
|
||||
"TELEGRAM_DISABLE_LINK_PREVIEWS".to_string(),
|
||||
"true".to_string()
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -17,6 +17,13 @@ const CHANNELS = [
|
||||
fields: [
|
||||
{ key: 'botToken', labelKey: 'engine.hermesChannelBotToken', type: 'password', placeholder: '123456:ABC-DEF...' },
|
||||
],
|
||||
advancedFields: [
|
||||
{ key: 'replyToMode', labelKey: 'engine.hermesChannelTelegramReplyToMode', type: 'select', options: [['first', 'engine.hermesChannelTelegramReplyFirst'], ['all', 'engine.hermesChannelTelegramReplyAll'], ['off', 'engine.hermesChannelTelegramReplyOff']] },
|
||||
],
|
||||
advancedToggles: [
|
||||
{ key: 'guestMode', labelKey: 'engine.hermesChannelTelegramGuestMode' },
|
||||
{ key: 'disableLinkPreviews', labelKey: 'engine.hermesChannelTelegramDisableLinkPreviews' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'discord',
|
||||
@@ -271,6 +278,11 @@ function defaultForm(platform) {
|
||||
form.historyBackfill = false
|
||||
form.replyToMode = 'first'
|
||||
}
|
||||
if (platform === 'telegram') {
|
||||
form.replyToMode = 'first'
|
||||
form.guestMode = false
|
||||
form.disableLinkPreviews = false
|
||||
}
|
||||
if (platform === 'slack') form.webhookPath = '/slack/events'
|
||||
return form
|
||||
}
|
||||
|
||||
@@ -1522,6 +1522,12 @@ export default {
|
||||
hermesChannelDiscordReplyFirst: _('仅首条回复引用', 'Reply to first message', '僅首則回覆引用'),
|
||||
hermesChannelDiscordReplyAll: _('每条回复都引用', 'Reply to every message', '每則回覆都引用'),
|
||||
hermesChannelDiscordReplyOff: _('不引用回复', 'No reply reference', '不引用回覆'),
|
||||
hermesChannelTelegramReplyToMode: _('Telegram 回复引用', 'Telegram Reply Reference', 'Telegram 回覆引用'),
|
||||
hermesChannelTelegramReplyFirst: _('仅首条回复引用', 'Reply to first message', '僅首則回覆引用'),
|
||||
hermesChannelTelegramReplyAll: _('每条回复都引用', 'Reply to every message', '每則回覆都引用'),
|
||||
hermesChannelTelegramReplyOff: _('不引用回复', 'No reply reference', '不引用回覆'),
|
||||
hermesChannelTelegramGuestMode: _('允许被 @ 时临时响应非白名单群组', 'Allow explicit @mentions from non-allowlisted groups', '允許被 @ 時臨時回應非白名單群組'),
|
||||
hermesChannelTelegramDisableLinkPreviews: _('关闭链接预览', 'Disable link previews', '關閉連結預覽'),
|
||||
hermesChannelDiscordAutoThread: _('自动创建线程', 'Auto-create threads', '自動建立討論串'),
|
||||
hermesChannelDiscordReactions: _('启用表情反馈', 'Enable reactions', '啟用表情回饋'),
|
||||
hermesChannelDiscordThreadRequireMention: _('线程内也要求 @Bot', 'Require @mention in threads', '討論串內也要求 @Bot'),
|
||||
|
||||
@@ -18,6 +18,9 @@ test('Hermes 渠道读取会从 platforms 平台配置生成稳定表单值', ()
|
||||
group_policy: 'allowlist',
|
||||
allow_from: ['1001', '1002'],
|
||||
require_mention: true,
|
||||
reply_to_mode: 'all',
|
||||
guest_mode: true,
|
||||
disable_link_previews: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -29,6 +32,9 @@ test('Hermes 渠道读取会从 platforms 平台配置生成稳定表单值', ()
|
||||
assert.equal(values.telegram.groupPolicy, 'allowlist')
|
||||
assert.equal(values.telegram.allowFrom, '1001, 1002')
|
||||
assert.equal(values.telegram.requireMention, true)
|
||||
assert.equal(values.telegram.replyToMode, 'all')
|
||||
assert.equal(values.telegram.guestMode, true)
|
||||
assert.equal(values.telegram.disableLinkPreviews, true)
|
||||
})
|
||||
|
||||
test('Hermes 渠道读取会按运行时优先级合并 .env 凭证', () => {
|
||||
@@ -101,6 +107,9 @@ test('Hermes 渠道保存会写入 Hermes 最新 platforms 配置并保留无关
|
||||
groupPolicy: 'allowlist',
|
||||
allowFrom: '1001, 1002',
|
||||
requireMention: true,
|
||||
replyToMode: 'off',
|
||||
guestMode: true,
|
||||
disableLinkPreviews: true,
|
||||
})
|
||||
|
||||
assert.deepEqual(next.model, { provider: 'anthropic', default: 'claude-sonnet-4-6' })
|
||||
@@ -110,9 +119,19 @@ test('Hermes 渠道保存会写入 Hermes 最新 platforms 配置并保留无关
|
||||
assert.equal(next.platforms.telegram.extra.group_policy, 'allowlist')
|
||||
assert.deepEqual(next.platforms.telegram.extra.allow_from, ['1001', '1002'])
|
||||
assert.equal(next.platforms.telegram.extra.require_mention, true)
|
||||
assert.equal(next.platforms.telegram.extra.reply_to_mode, 'off')
|
||||
assert.equal(next.platforms.telegram.extra.guest_mode, true)
|
||||
assert.equal(next.platforms.telegram.extra.disable_link_previews, true)
|
||||
assert.equal(next.platforms.telegram.extra.unknown_option, 'keep-me')
|
||||
})
|
||||
|
||||
test('Hermes Telegram 保存会校验回复模式选项', () => {
|
||||
assert.throws(() => mergeHermesChannelConfig({}, 'telegram', {
|
||||
enabled: true,
|
||||
replyToMode: 'sometimes',
|
||||
}), /platforms\.telegram\.extra\.reply_to_mode/)
|
||||
})
|
||||
|
||||
test('Hermes 飞书保存会补齐可运行默认项并使用 Hermes snake_case 字段', () => {
|
||||
const next = mergeHermesChannelConfig({}, 'feishu', {
|
||||
enabled: true,
|
||||
@@ -143,12 +162,18 @@ test('Hermes 渠道保存会生成运行时仍会读取的环境变量', () => {
|
||||
allowFrom: '1001, 1002',
|
||||
groupAllowFrom: 'group-a\ngroup-b',
|
||||
requireMention: true,
|
||||
replyToMode: 'off',
|
||||
guestMode: true,
|
||||
disableLinkPreviews: true,
|
||||
})
|
||||
|
||||
assert.equal(telegramEnv.TELEGRAM_BOT_TOKEN, '123:token')
|
||||
assert.equal(telegramEnv.TELEGRAM_ALLOWED_USERS, '1001,1002')
|
||||
assert.equal(telegramEnv.TELEGRAM_GROUP_ALLOWED_USERS, 'group-a,group-b')
|
||||
assert.equal(telegramEnv.TELEGRAM_REQUIRE_MENTION, 'true')
|
||||
assert.equal(telegramEnv.TELEGRAM_REPLY_TO_MODE, 'off')
|
||||
assert.equal(telegramEnv.TELEGRAM_GUEST_MODE, 'true')
|
||||
assert.equal(telegramEnv.TELEGRAM_DISABLE_LINK_PREVIEWS, 'true')
|
||||
|
||||
const feishuEnv = buildHermesChannelEnvUpdates('feishu', {
|
||||
appId: 'cli_xxx',
|
||||
|
||||
Reference in New Issue
Block a user