@mzzsfy/dsh-usage-panel 0.4.4 → 0.4.5
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 +1 -1
- package/src/index.js +6 -5
- package/src/poller.mjs +13 -0
- package/test/poller.test.mjs +48 -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.
|
|
4
|
+
"version": "0.4.5",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"exports": {
|
package/src/client.js
CHANGED
|
@@ -898,7 +898,7 @@ function DetailDialog(props) {
|
|
|
898
898
|
ranges.map((r) => h('button', {
|
|
899
899
|
key: r, className: 'up-btn', disabled: r === range,
|
|
900
900
|
onClick: () => setRange(r),
|
|
901
|
-
}, r === 'all' ? '全部' : '近 ' + r)),
|
|
901
|
+
}, r === 'all' ? '全部' : '近 ' + ({ '7d': '7 天', '30d': '30 天' })[r])),
|
|
902
902
|
h('button', { className: 'up-btn', onClick: props.onClose }, '关闭'),
|
|
903
903
|
),
|
|
904
904
|
h(TrendChart, { label: props.label, points, onDetail: null }),
|
package/src/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
} from './parsers.mjs'
|
|
17
17
|
import { readingToSnapshots, appendPoint, buildMonthSequence, newSequenceStore } from './history.mjs'
|
|
18
18
|
import { createHistoryStore } from './historyStore.mjs'
|
|
19
|
-
import { createBackoff, isShortWindowTier, tierIntervalSec, lastQuerySecOf, isDue } from './poller.mjs'
|
|
19
|
+
import { createBackoff, isShortWindowTier, tierIntervalSec, lastQuerySecOf, isDue, runLimited } from './poller.mjs'
|
|
20
20
|
import {
|
|
21
21
|
DEFAULT_QUOTA_THRESHOLD_PCT,
|
|
22
22
|
WEBHOOK_TIMEOUT_MS,
|
|
@@ -39,6 +39,9 @@ import {
|
|
|
39
39
|
const FETCH_TIMEOUT_MS = 20 * 1000
|
|
40
40
|
const BODY_MAX_BYTES = 256 * 1024
|
|
41
41
|
const MAX_ACCOUNTS = 20
|
|
42
|
+
// 单轮轮询的受限并发:串行执行时单账号 20s 超时即可拖满整轮(pollInFlight 互斥
|
|
43
|
+
// 使后续 tick 全部跳过,轮询事实停摆),并发上限同时压住上游瞬时压力
|
|
44
|
+
const POLL_CONCURRENCY = 4
|
|
42
45
|
// 通知长轮询服务端挂起上限,与 turn-notify 同值;客户端超时须大于此值
|
|
43
46
|
const LONG_POLL_WAIT_MS = 25 * 1000
|
|
44
47
|
// 数据目录支持 env 注入(测试隔离);缺省落 ~/.dsh/dsh-usage-panel
|
|
@@ -756,10 +759,8 @@ export function apply(ctx) {
|
|
|
756
759
|
pollInFlight = true
|
|
757
760
|
ensureHistory()
|
|
758
761
|
.then(async () => {
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
await runQuery(account).catch(() => {})
|
|
762
|
-
}
|
|
762
|
+
// 受限并发执行:单账号失败/超时不拖满整轮,后续 tick 不因互斥停摆
|
|
763
|
+
await runLimited(due, POLL_CONCURRENCY, (account) => runQuery(account).catch(() => {}))
|
|
763
764
|
await persistConfig().catch(() => {})
|
|
764
765
|
})
|
|
765
766
|
.catch(() => {})
|
package/src/poller.mjs
CHANGED
|
@@ -58,3 +58,16 @@ export function isDue({ lastQuerySec, nowSec, intervalSec }) {
|
|
|
58
58
|
if (lastQuerySec === null) return true
|
|
59
59
|
return nowSec - lastQuerySec >= intervalSec
|
|
60
60
|
}
|
|
61
|
+
|
|
62
|
+
// 受限并发执行:worker 池共享游标逐项取号(取号同步完成,无交错),单任务
|
|
63
|
+
// 失败/慢速只占用一个 worker 位,不拖满整轮;每任务结果经 task 感知,失败由
|
|
64
|
+
// 调用方在 task 内消化(本函数恒 resolve)
|
|
65
|
+
export async function runLimited(items, limit, task) {
|
|
66
|
+
let cursor = 0
|
|
67
|
+
const worker = async () => {
|
|
68
|
+
while (cursor < items.length) {
|
|
69
|
+
await task(items[cursor++])
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker))
|
|
73
|
+
}
|
package/test/poller.test.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
lastQuerySecOf,
|
|
13
13
|
isDue,
|
|
14
14
|
isShortWindowTier,
|
|
15
|
+
runLimited,
|
|
15
16
|
} from '../src/poller.mjs'
|
|
16
17
|
|
|
17
18
|
test('场景: 失败退避指数增长并封顶', () => {
|
|
@@ -86,3 +87,50 @@ test('场景: 短窗口档判定,从未成功(含最近失败)按短档,失败
|
|
|
86
87
|
assert.equal(isShortWindowTier({ ok: false, reading: null }, readingHasShort), true, '失败不塌缩:含短窗账号保持短档节奏')
|
|
87
88
|
assert.equal(isShortWindowTier({ ok: false, reading: null }, false), true, '失败的长窗账号也归短档:10 分钟节奏 + 退避压制重试')
|
|
88
89
|
})
|
|
90
|
+
|
|
91
|
+
test('场景: 受限并发执行——全项被处理,慢任务不阻塞其他 worker', async () => {
|
|
92
|
+
// Given 6 项任务,并发 2:每个任务挂起直到放行;并发内慢任务(后放行)只占 1 个 worker 位
|
|
93
|
+
const processed = []
|
|
94
|
+
const release = []
|
|
95
|
+
const task = async (item) => {
|
|
96
|
+
await new Promise((resolve) => release.push(resolve))
|
|
97
|
+
processed.push(item)
|
|
98
|
+
}
|
|
99
|
+
const done = runLimited([1, 2, 3, 4, 5, 6], 2, task)
|
|
100
|
+
// When 恰有 2 个任务在途(worker 数 = 并发上限)
|
|
101
|
+
assert.equal(release.length, 2, '并发上限决定在途任务数')
|
|
102
|
+
// 放行在途任务使其完成并取号下一项
|
|
103
|
+
release.splice(0).forEach((resolve) => resolve())
|
|
104
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
105
|
+
assert.equal(release.length, 2, '完成后立即补位')
|
|
106
|
+
release.splice(0).forEach((resolve) => resolve())
|
|
107
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
108
|
+
release.splice(0).forEach((resolve) => resolve())
|
|
109
|
+
await done
|
|
110
|
+
// Then 全部 6 项被处理,无重复无遗漏
|
|
111
|
+
assert.equal(processed.length, 6)
|
|
112
|
+
assert.deepEqual([...processed].sort((a, b) => a - b), [1, 2, 3, 4, 5, 6])
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('场景: 受限并发执行——单任务失败不中断其余任务(失败由 task 内消化)', async () => {
|
|
116
|
+
// Given task 对偶数项抛错但就地消化(生产用法:runQuery().catch 口径),runLimited 恒 resolve
|
|
117
|
+
const processed = []
|
|
118
|
+
const task = async (item) => {
|
|
119
|
+
try {
|
|
120
|
+
if (item % 2 === 0) throw new Error('boom-' + item)
|
|
121
|
+
processed.push(item)
|
|
122
|
+
} catch { /* 单项失败只占自己的 worker 位 */ }
|
|
123
|
+
}
|
|
124
|
+
// When 5 项、并发 2
|
|
125
|
+
await runLimited([1, 2, 3, 4, 5], 2, task)
|
|
126
|
+
// Then 奇数项全被处理
|
|
127
|
+
assert.deepEqual([...processed].sort((a, b) => a - b), [1, 3, 5])
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test('场景: 受限并发执行——空列表与并发大于任务数', async () => {
|
|
131
|
+
const processed = []
|
|
132
|
+
await runLimited([], 4, async (item) => processed.push(item))
|
|
133
|
+
assert.deepEqual(processed, [])
|
|
134
|
+
await runLimited([1, 2], 8, async (item) => processed.push(item))
|
|
135
|
+
assert.deepEqual(processed, [1, 2])
|
|
136
|
+
})
|