@mzzsfy/dsh-usage-panel 0.7.1 → 0.8.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 +4 -0
- package/package.json +1 -1
- package/src/client.js +4 -0
- package/src/index.js +1 -1
- package/src/notify.mjs +13 -2
- package/test/notify.test.mjs +45 -0
package/README.md
CHANGED
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.8.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"exports": {
|
package/src/client.js
CHANGED
|
@@ -565,6 +565,9 @@ function AccountCard(props) {
|
|
|
565
565
|
const armed = props.deleteArmed === true
|
|
566
566
|
const reading = account.last && account.last.ok ? account.last.reading : null
|
|
567
567
|
const level = reading && (reading.level || reading.membership) ? levelLabel(reading.level || reading.membership) : null
|
|
568
|
+
const queriedAt = account.last && typeof account.last.queriedAt === 'number' && Number.isFinite(account.last.queriedAt)
|
|
569
|
+
? account.last.queriedAt
|
|
570
|
+
: null
|
|
568
571
|
|
|
569
572
|
return h('div', { className: 'up-card' },
|
|
570
573
|
h('div', { className: 'up-card__row' },
|
|
@@ -572,6 +575,7 @@ function AccountCard(props) {
|
|
|
572
575
|
h('span', { className: 'up-badge', 'data-type': account.type }, TYPE_LABELS[account.type] || account.type),
|
|
573
576
|
level ? h('span', { className: 'up-badge', 'data-type': 'custom' }, level) : null,
|
|
574
577
|
h('span', { className: 'up-spacer' }),
|
|
578
|
+
queriedAt !== null ? h('span', { className: 'up-meta', title: fmtTimeFull(queriedAt) }, '上次更新 ' + fmtTime(queriedAt)) : null,
|
|
575
579
|
h('button', { className: 'up-btn', disabled: busy, onClick: props.onRefresh }, busy ? '查询中…' : '刷新'),
|
|
576
580
|
h('button', { className: 'up-btn', disabled: busy, onClick: props.onEdit }, '编辑'),
|
|
577
581
|
h('button', {
|
package/src/index.js
CHANGED
|
@@ -269,7 +269,7 @@ function normalizeAccounts(input) {
|
|
|
269
269
|
? lastSource.reading
|
|
270
270
|
: null,
|
|
271
271
|
error: typeof lastSource.error === 'string' ? lastSource.error : null,
|
|
272
|
-
queriedAt: Number.isFinite(
|
|
272
|
+
queriedAt: typeof lastSource.queriedAt === 'number' && Number.isFinite(lastSource.queriedAt) ? lastSource.queriedAt : null,
|
|
273
273
|
}
|
|
274
274
|
: null
|
|
275
275
|
const id = typeof raw.id === 'string' && raw.id.length > 0 ? raw.id : 'acct-' + String(index)
|
package/src/notify.mjs
CHANGED
|
@@ -6,6 +6,9 @@ export const PROJECTION_CAPACITY = 20
|
|
|
6
6
|
export const PROJECTION_TTL_MS = 60 * 1000
|
|
7
7
|
export const CLAIM_LOCK_TTL_MS = 30 * 1000
|
|
8
8
|
export const WEBHOOK_TIMEOUT_MS = 10 * 1000
|
|
9
|
+
// 轮转判定容差:远小于最短真实窗口量级,远大于服务端毫秒级抖动;
|
|
10
|
+
// 容差内视为同窗口静默跟随
|
|
11
|
+
export const RESET_DRIFT_TOLERANCE_MS = 60 * 1000
|
|
9
12
|
|
|
10
13
|
export const KIND_QUOTA = 'quota'
|
|
11
14
|
export const KIND_BALANCE = 'balance'
|
|
@@ -63,6 +66,13 @@ export function normalizeNotifyState(raw) {
|
|
|
63
66
|
|
|
64
67
|
const PERCENT_BASE = 100
|
|
65
68
|
|
|
69
|
+
// 窗口轮转判定:resetsAt 数值差超容差才算轮转;parse 失败无法证伪,保守按轮转。
|
|
70
|
+
// 容差内静默跟随,峰值与武装状态不重建
|
|
71
|
+
function windowRotated(prevResetsAt, resetsAt) {
|
|
72
|
+
const driftMs = Math.abs(Date.parse(resetsAt) - Date.parse(prevResetsAt))
|
|
73
|
+
return !(Number.isFinite(driftMs) && driftMs < RESET_DRIFT_TOLERANCE_MS)
|
|
74
|
+
}
|
|
75
|
+
|
|
66
76
|
// 沿触发评估:刷新读数越过逻辑点(阈值穿越/窗口重置)时产出事件,返回新状态不改入参。
|
|
67
77
|
// 仅在 last.ok 时由 host 调用;reading 缺失按无读数处理。
|
|
68
78
|
export function evaluateAccount({ account, rule, state, seq, ts }) {
|
|
@@ -88,8 +98,9 @@ function evaluateQuota({ account, rule, state, reading, events, next }) {
|
|
|
88
98
|
const prev = state.windows[label] || null
|
|
89
99
|
let armed = prev === null ? true : prev.armed !== false
|
|
90
100
|
let peak = prev === null ? null : prev.peak
|
|
91
|
-
// 窗口轮转:两侧 resetsAt
|
|
92
|
-
if (prev !== null && resetsAt !== null && prev.resetsAt !== null && prev.resetsAt !== resetsAt
|
|
101
|
+
// 窗口轮转:两侧 resetsAt 均有效、字符串不等且数值差超容差;上一窗口峰值随 reset 事件上报
|
|
102
|
+
if (prev !== null && resetsAt !== null && prev.resetsAt !== null && prev.resetsAt !== resetsAt
|
|
103
|
+
&& windowRotated(prev.resetsAt, resetsAt)) {
|
|
93
104
|
if (rule.resetNotice !== false && prev.peak !== null) {
|
|
94
105
|
events.push({
|
|
95
106
|
kind: KIND_RESET,
|
package/test/notify.test.mjs
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
KIND_QUOTA,
|
|
24
24
|
KIND_BALANCE,
|
|
25
25
|
KIND_RESET,
|
|
26
|
+
RESET_DRIFT_TOLERANCE_MS,
|
|
26
27
|
} from '../src/notify.mjs'
|
|
27
28
|
|
|
28
29
|
test('规则合并: 账号覆盖键生效, 其余继承全局', () => {
|
|
@@ -106,6 +107,50 @@ test('窗口重置: resetsAt 轮转产生 reset 事件报上一窗口峰值并
|
|
|
106
107
|
assert.equal(again.events[0].kind, KIND_QUOTA)
|
|
107
108
|
})
|
|
108
109
|
|
|
110
|
+
test('窗口重置: 毫秒级服务端抖动视为同窗口不发事件且峰值保留', () => {
|
|
111
|
+
// Given 阈值 90, 首轮 utilization 40 以 ISO 时刻建立基线
|
|
112
|
+
const rule = { quotaThresholdPct: 90, balanceThreshold: null, resetNotice: true }
|
|
113
|
+
let state = createNotifyState()
|
|
114
|
+
state = evaluateAccount({ account: quotaAccount(quotaReading(40, '2026-10-17T09:10:04.999Z')), rule, state, seq: 1, ts: 1000 }).state
|
|
115
|
+
// When 次轮 resetsAt 仅漂移 1ms(智谱 TIME_LIMIT 实测抖动), utilization 回落
|
|
116
|
+
const next = evaluateAccount({ account: quotaAccount(quotaReading(30, '2026-10-17T09:10:04.998Z')), rule, state, seq: 2, ts: 2000 })
|
|
117
|
+
// Then 无事件, 峰值保留 40 不误报, 基线静默跟随新值
|
|
118
|
+
assert.equal(next.events.length, 0)
|
|
119
|
+
assert.equal(next.state.windows['5小时'].peak, 40)
|
|
120
|
+
assert.equal(next.state.windows['5小时'].resetsAt, '2026-10-17T09:10:04.998Z')
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('窗口重置: 超容差的 resetsAt 变化仍判定为轮转', () => {
|
|
124
|
+
// Given 阈值 90, 首轮 92 建立基线
|
|
125
|
+
const rule = { quotaThresholdPct: 90, balanceThreshold: null, resetNotice: true }
|
|
126
|
+
let state = createNotifyState()
|
|
127
|
+
state = evaluateAccount({ account: quotaAccount(quotaReading(92, '2026-10-17T09:10:04.999Z')), rule, state, seq: 1, ts: 1000 }).state
|
|
128
|
+
// When resetsAt 前移 1 小时(真实轮转)
|
|
129
|
+
const next = evaluateAccount({ account: quotaAccount(quotaReading(5, '2026-10-17T10:10:04.999Z')), rule, state, seq: 2, ts: 2000 })
|
|
130
|
+
// Then 产生 reset 事件报上一窗口峰值 92
|
|
131
|
+
assert.equal(next.events.length, 1)
|
|
132
|
+
assert.equal(next.events[0].kind, KIND_RESET)
|
|
133
|
+
assert.equal(next.events[0].text, '[dsh] 账号A 5小时窗口已重置,上一窗口峰值用量 92%')
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
test('窗口重置: 漂移恰好容差内静默, 达到容差即轮转', () => {
|
|
137
|
+
// Given 阈值 90, 基线 resetsAt 为固定时刻
|
|
138
|
+
const rule = { quotaThresholdPct: 90, balanceThreshold: null, resetNotice: true }
|
|
139
|
+
const baseMs = Date.parse('2026-10-17T09:10:04.999Z')
|
|
140
|
+
const isoOf = (ms) => new Date(ms).toISOString()
|
|
141
|
+
const baselineState = evaluateAccount({ account: quotaAccount(quotaReading(40, isoOf(baseMs))), rule, state: createNotifyState(), seq: 1, ts: 1000 }).state
|
|
142
|
+
// When 漂移为容差减 1ms(容差内)
|
|
143
|
+
const inside = evaluateAccount({ account: quotaAccount(quotaReading(40, isoOf(baseMs + RESET_DRIFT_TOLERANCE_MS - 1))), rule, state: baselineState, seq: 2, ts: 2000 })
|
|
144
|
+
// Then 无事件, 基线跟随
|
|
145
|
+
assert.equal(inside.events.length, 0)
|
|
146
|
+
assert.equal(inside.state.windows['5小时'].resetsAt, isoOf(baseMs + RESET_DRIFT_TOLERANCE_MS - 1))
|
|
147
|
+
// When 漂移恰好等于容差(边界,严格小于判定不吸收,独立从原始基线评估)
|
|
148
|
+
const boundary = evaluateAccount({ account: quotaAccount(quotaReading(40, isoOf(baseMs + RESET_DRIFT_TOLERANCE_MS))), rule, state: baselineState, seq: 3, ts: 3000 })
|
|
149
|
+
// Then 判定轮转产生 reset 事件
|
|
150
|
+
assert.equal(boundary.events.length, 1)
|
|
151
|
+
assert.equal(boundary.events[0].kind, KIND_RESET)
|
|
152
|
+
})
|
|
153
|
+
|
|
109
154
|
test('窗口重置: resetNotice 关闭时不产生 reset 事件但仍重建基线并重新武装', () => {
|
|
110
155
|
// Given 阈值 90 已触发, 重置通知关闭
|
|
111
156
|
const rule = { quotaThresholdPct: 90, balanceThreshold: null, resetNotice: false }
|