@mzzsfy/dsh-usage-panel 0.6.0 → 0.7.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/package.json +1 -1
- package/src/client.js +3 -3
- package/src/index.js +3 -0
- package/test/poller.test.mjs +51 -0
- package/test/route.test.mjs +12 -0
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.7.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"exports": {
|
package/src/client.js
CHANGED
|
@@ -181,9 +181,9 @@ window.__ModuleLoader__.load({
|
|
|
181
181
|
})()
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
// 导航图标声明:交给 dsh-settings-nav-icons
|
|
185
|
-
//
|
|
186
|
-
const NAV_ICON = { '账号余额': '
|
|
184
|
+
// 导航图标声明:交给 dsh-settings-nav-icons 统一渲染;双键 = 分区 label + 市场
|
|
185
|
+
// 短名(发现页收录显示形态);该插件未就绪时入队,由其启动时排空
|
|
186
|
+
const NAV_ICON = { '账号余额': 'wallet', 'dsh-usage-panel': 'wallet' }
|
|
187
187
|
if (window.__navicIcons !== undefined) window.__navicIcons.register(NAV_ICON)
|
|
188
188
|
else if (Array.isArray(window.__navicIconQueue)) window.__navicIconQueue.push(NAV_ICON)
|
|
189
189
|
else window.__navicIconQueue = [NAV_ICON]
|
package/src/index.js
CHANGED
|
@@ -731,6 +731,9 @@ export function apply(ctx) {
|
|
|
731
731
|
let pollArmed = false
|
|
732
732
|
ctx.inject(['timer'], (timerCtx) => {
|
|
733
733
|
if (typeof timerCtx.interval !== 'function') return
|
|
734
|
+
// 配置预载:启动即读盘,消除懒加载空窗——否则宿主重启后直到首个面板请求前,
|
|
735
|
+
// tick 回调因 config 为 null 空转,自动查询静默停摆,通知全部缺席
|
|
736
|
+
void ensureConfig()
|
|
734
737
|
const dispose = timerCtx.interval(() => {
|
|
735
738
|
if (pollInFlight) return
|
|
736
739
|
const current = config
|
package/test/poller.test.mjs
CHANGED
|
@@ -13,6 +13,10 @@ import {
|
|
|
13
13
|
isDue,
|
|
14
14
|
runLimited,
|
|
15
15
|
} from '../src/poller.mjs'
|
|
16
|
+
import { evaluateAccount, createNotifyState } from '../src/notify.mjs'
|
|
17
|
+
|
|
18
|
+
// 宿主 tick 周期(index.js TICK_SEC=30s)的测试替身:语义=tick 远小于轮询间隔
|
|
19
|
+
const TICK_SEC = 30
|
|
16
20
|
|
|
17
21
|
test('场景: 失败退避指数增长并封顶', () => {
|
|
18
22
|
const backoff = createBackoff({ baseSec: 600 })
|
|
@@ -120,3 +124,50 @@ test('场景: 受限并发执行——空列表与并发大于任务数', async
|
|
|
120
124
|
await runLimited([1, 2], 8, async (item) => processed.push(item))
|
|
121
125
|
assert.deepEqual(processed, [1, 2])
|
|
122
126
|
})
|
|
127
|
+
|
|
128
|
+
// 端到端时钟推演:自动轮询 tick 驱动(无任何手动刷新)下,读数新越阈值即产出通知。
|
|
129
|
+
// Given tick 周期远小于轮询间隔,阈值 91%,上游 utilization 按轮次爬升。
|
|
130
|
+
// When 宿主 tick 循环按 isDue 判定到点轮次并执行查询 + 沿触发评估。
|
|
131
|
+
// Then 通知在越线轮次由自动路径产出,armed 解除后不重发。
|
|
132
|
+
test('场景: 自动轮询 tick 驱动下阈值穿越产出通知(无手动刷新)', () => {
|
|
133
|
+
let nowSec = 0
|
|
134
|
+
const utilizationByRound = [40, 55, 70, 93, 95, 60]
|
|
135
|
+
let round = 0
|
|
136
|
+
const account = { id: 'acct-sim', name: '模拟账号', last: null, notifyState: createNotifyState() }
|
|
137
|
+
const backoff = createBackoff({ baseSec: POLL_INTERVAL_SEC })
|
|
138
|
+
const events = []
|
|
139
|
+
for (; round < utilizationByRound.length && nowSec <= POLL_INTERVAL_SEC * utilizationByRound.length + TICK_SEC; nowSec += TICK_SEC) {
|
|
140
|
+
const due = !backoff.isBlocked(nowSec) && isDue({
|
|
141
|
+
lastQuerySec: lastQuerySecOf(account.last),
|
|
142
|
+
nowSec,
|
|
143
|
+
intervalSec: POLL_INTERVAL_SEC,
|
|
144
|
+
})
|
|
145
|
+
if (!due) continue
|
|
146
|
+
// runQuery 成功路径:落读数记 queriedAt(成功失败均记),退避恢复
|
|
147
|
+
const queriedAt = nowSec * 1000
|
|
148
|
+
account.last = {
|
|
149
|
+
ok: true,
|
|
150
|
+
reading: {
|
|
151
|
+
kind: 'quota',
|
|
152
|
+
windows: [{ label: '5小时', utilization: utilizationByRound[round++], remaining: null, limit: null, resetsAt: '2026-01-01T00:00:00Z' }],
|
|
153
|
+
},
|
|
154
|
+
error: null,
|
|
155
|
+
queriedAt,
|
|
156
|
+
}
|
|
157
|
+
backoff.onSuccess()
|
|
158
|
+
// evaluateAndDispatch 评估段:通知开,阈值 91%
|
|
159
|
+
const outcome = evaluateAccount({
|
|
160
|
+
account,
|
|
161
|
+
rule: { quotaThresholdPct: 91, balanceThreshold: null, resetNotice: true },
|
|
162
|
+
state: account.notifyState,
|
|
163
|
+
seq: events.length,
|
|
164
|
+
ts: queriedAt,
|
|
165
|
+
})
|
|
166
|
+
account.notifyState = outcome.state
|
|
167
|
+
events.push(...outcome.events)
|
|
168
|
+
}
|
|
169
|
+
assert.equal(round, utilizationByRound.length, '每轮到点均被 tick 驱动查询')
|
|
170
|
+
assert.equal(events.length, 1, '自动路径恰好产出一次阈值通知')
|
|
171
|
+
assert.equal(events[0].detail.value, 93, '通知发生在越线轮次')
|
|
172
|
+
assert.equal(account.notifyState.windows['5小时'].armed, false, '越线后武装解除')
|
|
173
|
+
})
|
package/test/route.test.mjs
CHANGED
|
@@ -90,6 +90,18 @@ test('settings:timer 服务激活时 pollArmed 为 true', async () => {
|
|
|
90
90
|
assert.equal(res.payload.pollArmed, true)
|
|
91
91
|
})
|
|
92
92
|
|
|
93
|
+
// 启动即预载配置:tick 驱动自动查询的先决条件——懒加载形态下宿主重启后
|
|
94
|
+
// 到首个面板请求之间 config 为 null,tick 空转,自动查询静默停摆
|
|
95
|
+
test('settings:timer 激活即预载配置(tick 不因 config 空窗停摆)', async () => {
|
|
96
|
+
const { ctx, routes } = makeCtx({ timerAvailable: true })
|
|
97
|
+
apply(ctx)
|
|
98
|
+
// ensureConfig 异步读盘;await 一拍微任务让它完成
|
|
99
|
+
await new Promise((resolve) => setImmediate(resolve))
|
|
100
|
+
const res = await call(routes, '/api/usage-panel/accounts', makeReq('GET'))
|
|
101
|
+
assert.equal(res.status, 200)
|
|
102
|
+
assert.ok(Array.isArray(res.payload.accounts), '配置已载入内存,轮询具备调度对象')
|
|
103
|
+
})
|
|
104
|
+
|
|
93
105
|
test('settings:timer 服务缺失时 pollArmed 为 false(降级可见)', async () => {
|
|
94
106
|
const { ctx, routes } = makeCtx({ timerAvailable: false })
|
|
95
107
|
apply(ctx)
|