mirror of
https://github.com/MengMengCode/VoCat.git
synced 2026-08-13 03:13:43 +08:00
fix: polish WeCom notification settings
This commit is contained in:
@@ -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:检查最终变更**
|
||||
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}}。")}
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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("保存通知配置")}
|
||||
|
||||
Reference in New Issue
Block a user