@mzzsfy/dsh-usage-panel 0.4.5 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +1 -1
- package/src/client.js +23 -11
- package/src/notify.mjs +2 -2
- package/test/notify.route.test.mjs +8 -7
- package/test/notify.test.mjs +4 -4
package/README.md
CHANGED
|
@@ -73,7 +73,7 @@ usage-panel:
|
|
|
73
73
|
balanceThreshold: null # 余额阈值,null 为不启用
|
|
74
74
|
resetNotice: true # 窗口重置时通知上一窗口峰值
|
|
75
75
|
toast: true # 页内 toast 通道
|
|
76
|
-
webhookUrl: '' #
|
|
76
|
+
webhookUrl: '' # webhook 目标 URL,留空禁用;面板原文回显
|
|
77
77
|
imTargets: [] # dsh-im 投递目标 [{botId, targetId}]
|
|
78
78
|
```
|
|
79
79
|
|
|
@@ -88,7 +88,7 @@ usage-panel:
|
|
|
88
88
|
### 通知接口
|
|
89
89
|
|
|
90
90
|
- `GET /api/usage-panel/notifications`:通知投影长轮询;`cursor` 缺省=首拉立即返回全量,`cursor` 等于当前版本时挂起至事件或超时,落后或超前(宿主重启版本回退)立即返回全量,客户端以响应 version 重置游标自愈;响应 `{units, version}`
|
|
91
|
-
- `GET|POST /api/usage-panel/notify-config`:全局通知规则;`webhookUrl`
|
|
91
|
+
- `GET|POST /api/usage-panel/notify-config`:全局通知规则;`webhookUrl` 原文随响应回显
|
|
92
92
|
- `POST /api/usage-panel/test-webhook` / `POST /api/usage-panel/test-im`:测试投递,返回真实结果
|
|
93
93
|
- `GET /api/usage-panel/im-targets?botId=`:列出 dsh-im 该 bot 已保存投递目标
|
|
94
94
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mzzsfy/dsh-usage-panel",
|
|
3
3
|
"description": "多平台 LLM 账号余额与额度面板:支持 DeepSeek/OpenRouter/Kimi/智谱/MiniMax/NewApi 及自定义端点,自动定期查询,趋势图与历史快照,用量/余额阈值与窗口重置多通道通知",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.5.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"exports": {
|
package/src/client.js
CHANGED
|
@@ -992,6 +992,7 @@ function NotifyConfigCard() {
|
|
|
992
992
|
.then((res) => {
|
|
993
993
|
if (!alive) return
|
|
994
994
|
setConfig(res && res.notify ? res.notify : {})
|
|
995
|
+
setWebhookUrl(res && res.notify && typeof res.notify.webhookUrl === 'string' ? res.notify.webhookUrl : '')
|
|
995
996
|
setImAvailable(res ? res.imAvailable === true : false)
|
|
996
997
|
})
|
|
997
998
|
.catch((err) => { if (alive) setError('读取通知配置失败:' + (err && err.message ? err.message : String(err))) })
|
|
@@ -1097,6 +1098,9 @@ function NotifyConfigCard() {
|
|
|
1097
1098
|
}
|
|
1098
1099
|
const targets = Array.isArray(config.imTargets) ? config.imTargets : []
|
|
1099
1100
|
const boundBots = imBoundBotIds(targets)
|
|
1101
|
+
// 已配置态与测试/保存分流均由 webhookUrl 派生;草稿编辑中不被其他 patch 回执重置
|
|
1102
|
+
const savedWebhookUrl = typeof config.webhookUrl === 'string' ? config.webhookUrl.trim() : ''
|
|
1103
|
+
const webhookDirty = webhookUrl.trim() !== savedWebhookUrl
|
|
1100
1104
|
// 通知配置一次即久,默认折叠:summary 摘要行常显状态,展开才是完整配置
|
|
1101
1105
|
return h('details', { className: 'up-card up-fold' },
|
|
1102
1106
|
h('summary', { className: 'up-fold__summary' },
|
|
@@ -1104,7 +1108,7 @@ function NotifyConfigCard() {
|
|
|
1104
1108
|
h('span', { className: 'up-dot' + (config.enabled === true ? '' : ' up-dot--off') }),
|
|
1105
1109
|
h('span', { className: 'up-meta' }, config.enabled === true ? '已启用' : '已关闭'),
|
|
1106
1110
|
targets.length > 0 ? h('span', { className: 'up-meta' }, targets.length + ' 个 IM 目标') : null,
|
|
1107
|
-
|
|
1111
|
+
savedWebhookUrl.length > 0 ? h('span', { className: 'up-meta' }, 'webhook 已配置') : null,
|
|
1108
1112
|
),
|
|
1109
1113
|
h('div', { className: 'up-card__row' },
|
|
1110
1114
|
h(Switch, {
|
|
@@ -1156,29 +1160,37 @@ function NotifyConfigCard() {
|
|
|
1156
1160
|
onChange: (checked) => patch({ toast: checked }),
|
|
1157
1161
|
}),
|
|
1158
1162
|
h('div', { className: 'up-card__row' },
|
|
1159
|
-
h('span', { className: 'up-field__label', title: 'host 直发的 Slack 兼容 JSON
|
|
1160
|
-
|
|
1163
|
+
h('span', { className: 'up-field__label', title: 'host 直发的 Slack 兼容 JSON 通知;URL 原文回显,改动保存后自动发送测试' }, 'Webhook'),
|
|
1164
|
+
savedWebhookUrl.length > 0 ? h('span', { className: 'up-dot', title: '已配置' }) : null,
|
|
1161
1165
|
h('span', { className: 'up-field' },
|
|
1162
1166
|
h('input', {
|
|
1163
|
-
type: '
|
|
1167
|
+
type: 'text', style: { width: '260px' },
|
|
1164
1168
|
value: webhookUrl,
|
|
1165
1169
|
onChange: (e) => setWebhookUrl(e.target.value),
|
|
1166
|
-
placeholder:
|
|
1170
|
+
placeholder: 'https://hooks.example.com/…',
|
|
1167
1171
|
})),
|
|
1168
1172
|
h('button', {
|
|
1169
1173
|
className: 'up-btn',
|
|
1170
1174
|
onClick: () => {
|
|
1171
|
-
if (
|
|
1172
|
-
if (
|
|
1175
|
+
if (!webhookDirty) {
|
|
1176
|
+
if (savedWebhookUrl.length === 0) { notify('请先填写 webhook URL', 'error'); return }
|
|
1173
1177
|
testWebhook()
|
|
1174
1178
|
return
|
|
1175
1179
|
}
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1180
|
+
// 草稿与已保存值不同即保存(含清空=禁用);清空保存后通道关闭,不再自动测试误导报错
|
|
1181
|
+
const disabling = webhookUrl.trim().length === 0
|
|
1182
|
+
api('/api/usage-panel/notify-config', { method: 'POST', body: JSON.stringify({ webhookUrl: webhookUrl.trim() }) })
|
|
1183
|
+
.then((res) => {
|
|
1184
|
+
if (res && res.notify) {
|
|
1185
|
+
setConfig(res.notify)
|
|
1186
|
+
setWebhookUrl(typeof res.notify.webhookUrl === 'string' ? res.notify.webhookUrl : '')
|
|
1187
|
+
}
|
|
1188
|
+
notify(disabling ? 'webhook 已禁用' : '通知配置已保存', 'ok')
|
|
1189
|
+
if (!disabling) testWebhook()
|
|
1190
|
+
})
|
|
1179
1191
|
.catch(fail)
|
|
1180
1192
|
},
|
|
1181
|
-
},
|
|
1193
|
+
}, webhookDirty ? '保存并测试' : '测试'),
|
|
1182
1194
|
),
|
|
1183
1195
|
// IM 通道:目标来自 dsh-im 已保存目录,勾选即自动保存;新建与平台测试在 dsh-im 设置页完成
|
|
1184
1196
|
imAvailable ? null : h('span', { className: 'up-meta' }, 'dsh-im 未安装,IM 通道不可用'),
|
package/src/notify.mjs
CHANGED
|
@@ -405,7 +405,7 @@ export function resolvedNotifySettings(raw) {
|
|
|
405
405
|
}
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
-
// 面板可见配置:webhookUrl
|
|
408
|
+
// 面板可见配置:webhookUrl 原文随响应回显,面板所见即所存。
|
|
409
409
|
export function publicNotify(resolved) {
|
|
410
410
|
return {
|
|
411
411
|
enabled: resolved.enabled,
|
|
@@ -414,7 +414,7 @@ export function publicNotify(resolved) {
|
|
|
414
414
|
resetNotice: resolved.resetNotice,
|
|
415
415
|
toast: resolved.toast,
|
|
416
416
|
imTargets: resolved.imTargets,
|
|
417
|
-
|
|
417
|
+
webhookUrl: resolved.webhookUrl,
|
|
418
418
|
}
|
|
419
419
|
}
|
|
420
420
|
|
|
@@ -176,22 +176,22 @@ test('评估接线: custom 账号 extract 规则驱动 balance 读数全链路(
|
|
|
176
176
|
}
|
|
177
177
|
})
|
|
178
178
|
|
|
179
|
-
test('notify-config: GET
|
|
179
|
+
test('notify-config: GET 回显 webhookUrl 原文', async () => {
|
|
180
180
|
// Given 已配置 webhook 的 settings
|
|
181
181
|
const { ctx, routes } = makeCtx({ settingsValue: { notify: { enabled: true, webhookUrl: 'https://hooks.example.com/private' } } })
|
|
182
182
|
apply(ctx)
|
|
183
183
|
// When 读取通知配置
|
|
184
184
|
const res = await call(routes, '/api/usage-panel/notify-config', makeReq('GET'))
|
|
185
|
-
// Then
|
|
185
|
+
// Then 原文随响应回显, webhookConfigured 已移除
|
|
186
186
|
assert.equal(res.status, 200)
|
|
187
|
-
assert.equal(res.payload.notify.webhookUrl,
|
|
188
|
-
assert.equal(res.payload.notify
|
|
187
|
+
assert.equal(res.payload.notify.webhookUrl, 'https://hooks.example.com/private')
|
|
188
|
+
assert.equal('webhookConfigured' in res.payload.notify, false)
|
|
189
189
|
assert.equal(res.payload.imAvailable, false)
|
|
190
190
|
})
|
|
191
191
|
|
|
192
192
|
test('notify-config: 非法补丁 400, 合法补丁合并生效', async () => {
|
|
193
|
-
// Given 合法 ctx
|
|
194
|
-
const { ctx, routes } = makeCtx({ settingsValue: { notify: { enabled: true } } })
|
|
193
|
+
// Given 合法 ctx,已配置 webhook
|
|
194
|
+
const { ctx, routes } = makeCtx({ settingsValue: { notify: { enabled: true, webhookUrl: 'https://hooks.example.com/keep' } } })
|
|
195
195
|
apply(ctx)
|
|
196
196
|
// When 提交越界阈值
|
|
197
197
|
const bad = await call(routes, '/api/usage-panel/notify-config', makeReq('POST', { quotaThresholdPct: 0 }))
|
|
@@ -199,12 +199,13 @@ test('notify-config: 非法补丁 400, 合法补丁合并生效', async () => {
|
|
|
199
199
|
assert.equal(bad.status, 400)
|
|
200
200
|
// When 提交合法补丁(仅改阈值与余额阈值)
|
|
201
201
|
const good = await call(routes, '/api/usage-panel/notify-config', makeReq('POST', { quotaThresholdPct: 80, balanceThreshold: 20 }))
|
|
202
|
-
// Then 生效且未提供的键保持原值
|
|
202
|
+
// Then 生效且未提供的键保持原值(部分补丁不得清掉已存 webhook)
|
|
203
203
|
assert.equal(good.status, 200)
|
|
204
204
|
const view = await call(routes, '/api/usage-panel/notify-config', makeReq('GET'))
|
|
205
205
|
assert.equal(view.payload.notify.quotaThresholdPct, 80)
|
|
206
206
|
assert.equal(view.payload.notify.balanceThreshold, 20)
|
|
207
207
|
assert.equal(view.payload.notify.enabled, true)
|
|
208
|
+
assert.equal(view.payload.notify.webhookUrl, 'https://hooks.example.com/keep')
|
|
208
209
|
})
|
|
209
210
|
|
|
210
211
|
test('test-webhook: 返回真实投递结果而非谎报成功', async () => {
|
package/test/notify.test.mjs
CHANGED
|
@@ -418,14 +418,14 @@ test('读侧归一: 残缺配置回退默认值', () => {
|
|
|
418
418
|
assert.equal(resolved.webhookUrl, '')
|
|
419
419
|
})
|
|
420
420
|
|
|
421
|
-
test('
|
|
421
|
+
test('面板回显: publicNotify 返回 webhookUrl 原文', () => {
|
|
422
422
|
// Given 已配置 webhook 的归一配置
|
|
423
423
|
const resolved = resolvedNotifySettings({ webhookUrl: 'https://hooks.example.com/private' })
|
|
424
424
|
// When 转面板可见形态
|
|
425
425
|
const view = publicNotify(resolved)
|
|
426
|
-
// Then
|
|
427
|
-
assert.equal(view.webhookUrl,
|
|
428
|
-
assert.equal(view
|
|
426
|
+
// Then 原文随响应回显, webhookConfigured 已移除
|
|
427
|
+
assert.equal(view.webhookUrl, 'https://hooks.example.com/private')
|
|
428
|
+
assert.equal('webhookConfigured' in view, false)
|
|
429
429
|
assert.equal(view.toast, true)
|
|
430
430
|
})
|
|
431
431
|
|