From 4e7805bc76835eeab69bef5d889e3382c7720def Mon Sep 17 00:00:00 2001 From: smail Date: Sun, 20 Sep 2026 07:53:09 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E9=BD=90=E6=8F=92=E4=BB=B6=E5=AF=86?= =?UTF-8?q?=E9=92=A5=E9=85=8D=E7=BD=AE=E5=85=A5=E5=8F=A3=E5=B9=B6=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=E6=95=8F=E6=84=9F=E5=80=BC=E6=8F=90=E4=BA=A4=E8=BE=B9?= =?UTF-8?q?=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ConfigSettings/ConfigSettings.js | 13 ++++ test/frontend/config-settings.test.mjs | 70 +++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/frontend/src/views/components/ConfigSettings/ConfigSettings.js b/frontend/src/views/components/ConfigSettings/ConfigSettings.js index 0696b2a..9315caa 100644 --- a/frontend/src/views/components/ConfigSettings/ConfigSettings.js +++ b/frontend/src/views/components/ConfigSettings/ConfigSettings.js @@ -158,6 +158,16 @@ export function usePage(pageKey = 'base') { if (selectedSection.value === 'base' && ['mysql', 'postgresql'].includes(formConfig.value.database?.type)) { return { label: '数据库密码', description: '只在本次请求内传输,保存后仅保留密文状态。留空表示保持原值。', placeholder: '输入新数据库密码(可选)' } } + const pluginSecrets = { telegram_push: 'Telegram Bot Token', wechat_push: '微信 Secret' } + const label = pluginSecrets[selectedSection.value] + if (label && formConfig.value.plugins?.[selectedSection.value]) { + const configured = readFormConfig(loadedText.value).plugins?.[selectedSection.value]?.secretStatus?.configured + return { + label, + description: `${configured ? '已设置' : '未设置'}。保存后加密存储,不回显;留空保持原值。`, + placeholder: `输入新的 ${label}(可选)` + } + } return null }) const sensitiveHint = computed(() => secretField.value ? '密码不会回填,修改请使用下方独立输入框,留空保持原值' : '') @@ -526,6 +536,9 @@ export function usePage(pageKey = 'base') { if (value && selectedSection.value === 'base' && secretField.value) { secretChanges['database.password'] = { operation: 'replace', value: { plaintext: value } } } + if (value && ['telegram_push', 'wechat_push'].includes(selectedSection.value) && secretField.value) { + secretChanges[`plugins.${selectedSection.value}.secret`] = { operation: 'replace', value: { plaintext: value } } + } const relayPassword = formConfig.value?.relay?.password if (relayPassword && selectedSection.value === 'mail') { secretChanges['relay.password'] = { operation: 'replace', value: { plaintext: relayPassword } } diff --git a/test/frontend/config-settings.test.mjs b/test/frontend/config-settings.test.mjs index 8fbae91..7fe3847 100644 --- a/test/frontend/config-settings.test.mjs +++ b/test/frontend/config-settings.test.mjs @@ -1147,6 +1147,76 @@ test('插件读取状态字段不传回严格解析接口,未展示字段和 assert.equal(page.formConfig.value.plugins.telegram_push.secretStatus.configured, true) }) +for (const key of ['telegram_push', 'wechat_push']) { + test(`${key} 密钥独立提交、留空保留,保存回读与离开后清空`, async () => { + let active = { meta: { generation: 1 }, plugins: { + telegram_push: { enabled: false, settings: { timeout: 30 }, secretStatus: { configured: false } }, + wechat_push: { enabled: false, settings: { appId: 'test-only', future: 'preserved' }, secretStatus: { configured: false } } + } } + const original = structuredClone(active) + let pending = null + const requests = [] + const http = { + get: async path => ({ errorNo: 0, data: path.endsWith('/status') ? { activeGeneration: active.meta.generation } + : path.endsWith('/pending') ? pending : active }), + put: async (path, body) => { + requests.push({ path, body }) + pending = { generation: 2, section: 'plugins', sameSession: true, candidateId: 'secret-test', payload: body.payload } + return { errorNo: 0, data: pending } + }, + post: async (path, body) => { + requests.push({ path, body }) + if (path.endsWith('/apply')) { + active = { ...pending.payload, meta: { generation: 2 } } + active.plugins[key].secretStatus = { configured: true } + pending = null + } + return { errorNo: 0, data: { valid: true } } + } + } + const { page, dispose } = fixture(() => {}, key, { http }) + await page.loadAll() + assert.match(page.secretField.value.description, /^未设置/) + await page.validateConfig() + assert.deepEqual(requests.at(-1).body.secretChanges, {}) + page.updateSecret(' test-only-plugin-secret ') + assert.equal(page.hasChanges.value, true) + assert.equal(page.editorText.value.includes('test-only-plugin-secret'), false) + await page.validateConfig() + assert.equal(requests.at(-1).path, '/api/v1/config/plugins/validate') + assert.deepEqual(requests.at(-1).body.secretChanges, { + [`plugins.${key}.secret`]: { operation: 'replace', value: { plaintext: ' test-only-plugin-secret ' } } + }) + for (const id of Object.keys(original.plugins)) { + assert.deepEqual(requests.at(-1).body.payload.plugins[id].settings, original.plugins[id].settings) + assert.equal(requests.at(-1).body.payload.plugins[id].secretStatus, undefined) + assert.equal(requests.at(-1).body.payload.plugins[id].enabled, false) + } + assert.equal(await page.saveConfig(), true) + assert.equal(page.secretValue.value, '') + const saved = requests.find(item => item.path === '/api/v1/config/plugins') + assert.equal(saved.body.secretChanges[`plugins.${key}.secret`].value.plaintext, ' test-only-plugin-secret ') + page.updateSecret('blocked-pending-edit') + assert.equal(page.secretValue.value, '') + assert.equal(await page.applyConfig(), true) + assert.equal(page.secretValue.value, '') + assert.match(page.secretField.value.description, /^已设置/) + await page.validateConfig() + assert.deepEqual(requests.at(-1).body.secretChanges, {}) + page.updateSecret('discard-on-leave') + dispose() + assert.equal(page.secretValue.value, '') + + const readonly = fixture(() => {}, key, { http, hasCapability: () => false }).page + await readonly.loadAll() + const previousRequests = requests.length + readonly.updateSecret('forbidden') + await readonly.saveConfig() + assert.equal(readonly.secretValue.value, '') + assert.equal(requests.length, previousRequests) + }) +} + test('SQLite 不显示或发送密码,网络数据库和中继密码留空保留且不裁剪空格', async () => { for (const type of ['sqlite', 'mysql', 'postgresql']) { let request