fix: polish WeCom notification settings

This commit is contained in:
zAhYAng
2026-08-11 22:28:36 +08:00
parent 962c58fdd1
commit eab658dc90
6 changed files with 111 additions and 18 deletions
@@ -362,19 +362,19 @@ const DEFAULT_WECOM_PAYLOAD_TEMPLATE = `{
预期:所有修改的 Go 文件采用项目标准格式。
- [ ] **步骤 2:运行后端回归测试**
运行:`go test ./internal/server ./internal/store`
预期:所有目标包通过,无失败测试。
- [ ] **步骤 3:重新运行前端生产构建**
- [ ] **步骤 2:运行前端生产构建**
运行:`npm run build`
工作目录:`web`
预期:退出码 0。
预期:退出码 0,并生成 `web/dist` 供 Go 的嵌入资源使用
- [ ] **步骤 3:运行后端回归测试**
运行:`go test ./...`
预期:所有目标包通过,无失败测试;`cmd/vocat``web` 包从步骤 2 生成的 `web/dist` 读取嵌入资源。
- [ ] **步骤 4:检查最终变更**
+32 -3
View File
@@ -517,9 +517,7 @@ func (s *Server) resolveNotificationTestConfig(
}
for key, value := range overlay {
if _, secret := sensitive[key]; secret {
if text, ok := value.(string); !ok || text == "" || text == store.SecretMask {
continue
}
value = mergeNotificationTestSecretValue(value, resolved[key])
}
resolved[key] = value
}
@@ -545,6 +543,37 @@ func (s *Server) resolveNotificationTestConfig(
return resolved, provider, nil
}
// mergeNotificationTestSecretValue preserves masked values submitted by the
// settings form while allowing newly entered sensitive values in the same
// request. WeCom URLs are a sensitive list, unlike the string-based secrets
// used by the other notification channels.
func mergeNotificationTestSecretValue(incoming, existing any) any {
if incoming == nil {
return existing
}
switch next := incoming.(type) {
case string:
if next == "" || next == store.SecretMask {
return existing
}
case []any:
previous, ok := existing.([]any)
if !ok {
return incoming
}
merged := make([]any, len(next))
for index, value := range next {
if index < len(previous) {
merged[index] = mergeNotificationTestSecretValue(value, previous[index])
} else {
merged[index] = value
}
}
return merged
}
return incoming
}
func validateNotificationTestConfig(channel string, config map[string]any) error {
switch channel {
case "webhook":
+53
View File
@@ -179,6 +179,59 @@ func TestWecomNotificationSettingsPreserveWebhookURLs(t *testing.T) {
}
}
func TestResolveWecomNotificationTestConfigAcceptsUnsavedWebhookURLs(t *testing.T) {
test := newSettingsAPITest(t)
raw, err := json.Marshal(map[string]any{
"urls": []string{"https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=unsaved"},
"payload_template": `{"msgtype":"text","text":{"content":{{message}}}}`,
})
if err != nil {
t.Fatal(err)
}
resolved, _, err := test.server.resolveNotificationTestConfig(context.Background(), "wecom", raw)
if err != nil {
t.Fatal(err)
}
urls, ok := resolved["urls"].([]any)
if !ok || len(urls) != 1 || urls[0] != "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=unsaved" {
t.Fatalf("resolved URLs = %#v", resolved["urls"])
}
}
func TestResolveWecomNotificationTestConfigMergesMaskedAndUnsavedWebhookURLs(t *testing.T) {
test := newSettingsAPITest(t)
storedURL := "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=stored"
unsavedURL := "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=unsaved"
storedConfig, err := json.Marshal(map[string]any{
"urls": []string{storedURL},
"payload_template": `{"msgtype":"text","text":{"content":{{message}}}}`,
})
if err != nil {
t.Fatal(err)
}
if err := test.database.UpsertNotificationSetting(context.Background(), store.NotificationSetting{
Channel: "wecom",
Config: storedConfig,
}); err != nil {
t.Fatal(err)
}
raw, err := json.Marshal(map[string]any{
"urls": []string{store.SecretMask, unsavedURL},
"payload_template": `{"msgtype":"text","text":{"content":{{message}}}}`,
})
if err != nil {
t.Fatal(err)
}
resolved, _, err := test.server.resolveNotificationTestConfig(context.Background(), "wecom", raw)
if err != nil {
t.Fatal(err)
}
urls, ok := resolved["urls"].([]any)
if !ok || len(urls) != 2 || urls[0] != storedURL || urls[1] != unsavedURL {
t.Fatalf("resolved URLs = %#v", resolved["urls"])
}
}
func TestNotificationSettingsRejectsUnknownAndMalformedInput(t *testing.T) {
test := newSettingsAPITest(t)
cases := []struct {
+4 -4
View File
@@ -269,13 +269,13 @@ export function WebhookTab({ value, onChange, testing, onTest }: PushChannelProp
}
export function WecomTab({ value, onChange, testing, onTest }: PushChannelProps<WecomForm>) {
const { t } = useI18n();
const { t, lang } = useI18n();
const off = !value.enabled;
const complete = hasAnyUrl(value.urls) && !!value.payloadTemplate.trim();
return (
<div className="pt-2">
<ChannelHeader
title={t("启用企业微信推送")}
title={t("启用企业微信消息推送")}
enabled={value.enabled}
onToggle={(enabled) => onChange({ enabled })}
actions={
@@ -294,13 +294,13 @@ export function WecomTab({ value, onChange, testing, onTest }: PushChannelProps<
onChange={(urls) => onChange({ urls })}
enabled={value.enabled}
placeholder="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=..."
emptyText={t("尚未配置任何企业微信 Webhook URL,点击右侧添加按钮。")}
emptyText={t("尚未配置任何企业微信消息推送 Webhook URL,点击右侧添加按钮。")}
/>
<Field
label={t("JSON 请求体模板")}
hint={
<>
{t("支持完整企业微信消息 JSON。变量必须作为 JSON 值使用,例如")} <code>{"{{message}}"}</code>
{t("支持完整企业微信消息推送 JSON。变量必须作为 JSON 值使用,例如")} <code>{"{{message}}"}</code>{lang === "zh" ? "。" : "."}
{t("可用变量:{{event}}、{{title}}、{{message}}、{{timestamp}}、{{content}}、{{number}}、{{device_id}}、{{device_name}}、{{device_label}}、{{time}}。")}
</>
}
+11
View File
@@ -229,6 +229,7 @@ export const EN_DICT: Record<string, string> = {
"Webhook 测试失败": "Webhook test failed",
"Bark 测试失败": "Bark test failed",
"Email 测试失败": "Email test failed",
"企业微信消息推送测试失败": "WeCom message push test failed",
// ---- 设置页:安全卡 ----
: "Security",
@@ -326,10 +327,20 @@ export const EN_DICT: Record<string, string> = {
"启用 Bark 推送": "Enable Bark",
"启用 Email 推送": "Enable Email",
"启用 Webhook 推送": "Enable Webhook",
"企业微信消息推送": "WeCom Message Push",
"启用企业微信消息推送": "Enable WeCom Message Push",
"Telegram / Bark / Email / Pushplus / Webhook / 企业微信消息推送": "Telegram / Bark / Email / Pushplus / Webhook / WeCom Message Push",
"目标 URLs": "Target URLs",
"添加 URL": "Add URL",
"尚未配置任何 Bark URL,点击右侧添加按钮。": "No Bark URLs yet. Click the add button on the right.",
"尚未配置任何 Webhook URL,点击右侧添加按钮。": "No Webhook URLs yet. Click the add button on the right.",
"每个企业微信消息推送 Webhook URL 单独占一行,点击添加 URL 新增一行;不使用逗号、空格或换行分隔多个 URL。":
"Enter one WeCom message push Webhook URL per line. Use Add URL to add another row; do not separate URLs with commas, spaces, or line breaks.",
"尚未配置任何企业微信消息推送 Webhook URL,点击右侧添加按钮。": "No WeCom message push Webhook URLs yet. Click the add button on the right.",
"JSON 请求体模板": "JSON Request Body Template",
"支持完整企业微信消息推送 JSON。变量必须作为 JSON 值使用,例如": "Supports a complete WeCom message push JSON payload. Use variables as JSON values, for example",
"可用变量:{{event}}、{{title}}、{{message}}、{{timestamp}}、{{content}}、{{number}}、{{device_id}}、{{device_name}}、{{device_label}}、{{time}}。":
"Available variables: {{event}}, {{title}}, {{message}}, {{timestamp}}, {{content}}, {{number}}, {{device_id}}, {{device_name}}, {{device_label}}, {{time}}.",
"分组 (Group)": "Group",
"例如 vocat": "e.g. vocat",
"iOS 设备上的通知分组。": "Notification group on iOS devices.",
+3 -3
View File
@@ -35,7 +35,7 @@ const NOTIFY_TABS = [
{ key: "email", label: "Email" },
{ key: "pushplus", label: "Pushplus" },
{ key: "webhook", label: "Webhook" },
{ key: "wecom", label: "企业微信" },
{ key: "wecom", label: "企业微信消息推送" },
];
const EMPTY_SYSTEM_INFO: SystemInfo = { version: "", buildTime: "", config: "" };
@@ -332,7 +332,7 @@ export default function SettingsPage() {
});
message.success(t("测试通知已发送"));
} catch (error) {
message.error(apiMessage(error) || t("企业微信测试失败"));
message.error(apiMessage(error) || t("企业微信消息推送测试失败"));
} finally {
setTestingWecom(false);
}
@@ -466,7 +466,7 @@ export default function SettingsPage() {
<CardIcon>
<AlertRegular className="text-[24px]" />
</CardIcon>
<CardTitle title={t("通知")} subtitle={t("Telegram / Bark / Email / Pushplus / Webhook / 企业微信")} />
<CardTitle title={t("通知")} subtitle={t("Telegram / Bark / Email / Pushplus / Webhook / 企业微信消息推送")} />
</div>
<Button variant="primary" loading={savingNotif} disabled={loadingNotif} onClick={onSaveNotifications} className="!border-0" icon={<CheckmarkRegular />}>
{t("保存通知配置")}