@mzzsfy/dsh-usage-panel 0.8.0 → 0.8.1
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 +1 -1
- package/package.json +1 -1
- package/src/index.js +39 -4
- package/test/notify.route.test.mjs +102 -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.8.
|
|
4
|
+
"version": "0.8.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"exports": {
|
package/src/index.js
CHANGED
|
@@ -66,6 +66,21 @@ const SETTINGS_SCHEMA = z.object({
|
|
|
66
66
|
}).default({}),
|
|
67
67
|
})
|
|
68
68
|
|
|
69
|
+
// 0.1.7 宿主以静态 Config 导出生成设置节表单(maintain 同构);legacy 宿主经
|
|
70
|
+
// settings.register 注册命名空间。根级 volatile 包装:整节 live 表单,节写经
|
|
71
|
+
// loader 原地热更;旧宿主 schemastery 无 volatile 方法,特性检测原样返回
|
|
72
|
+
export const Config = volatileWrap(SETTINGS_SCHEMA)
|
|
73
|
+
|
|
74
|
+
function volatileWrap(schema) {
|
|
75
|
+
return typeof schema.volatile === 'function' ? schema.volatile() : schema
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// volatile ref 动态解包(get 协议):0.1.7 下 apply 入参 config 为整节单 ref;
|
|
79
|
+
// legacy 宿主 config 为普通对象原样透传
|
|
80
|
+
function unwrapVolatile(value) {
|
|
81
|
+
return typeof value?.get === 'function' ? value.get() : value
|
|
82
|
+
}
|
|
83
|
+
|
|
69
84
|
const TYPE_DEEPSEEK = 'deepseek'
|
|
70
85
|
const TYPE_OPENROUTER = 'openrouter'
|
|
71
86
|
const TYPE_KIMI = 'kimi'
|
|
@@ -326,7 +341,7 @@ async function queryAccount(account) {
|
|
|
326
341
|
// timer 为软依赖:不进 inject 声明,服务缺失时仅停用自动轮询,面板手动查询不受影响
|
|
327
342
|
export const inject = ['webServer']
|
|
328
343
|
|
|
329
|
-
export function apply(ctx) {
|
|
344
|
+
export function apply(ctx, configRef) {
|
|
330
345
|
let config = null
|
|
331
346
|
let loadPromise = null
|
|
332
347
|
let writeChain = Promise.resolve()
|
|
@@ -340,7 +355,9 @@ export function apply(ctx) {
|
|
|
340
355
|
|
|
341
356
|
function readNotifySettings() {
|
|
342
357
|
const settings = ctx.get('settings')
|
|
343
|
-
|
|
358
|
+
// 双形读(maintain 同构):legacy settings 命名空间 get;0.1.7+ apply 入参
|
|
359
|
+
// config 整节解包。方法面守卫兼防:服务在但缺 get 时回落默认值,防路由 handler 抛错
|
|
360
|
+
const value = settings && typeof settings.get === 'function' ? settings.get(NAMESPACE) : unwrapVolatile(configRef)
|
|
344
361
|
return resolvedNotifySettings(value && typeof value === 'object' ? value.notify : undefined)
|
|
345
362
|
}
|
|
346
363
|
|
|
@@ -630,11 +647,21 @@ export function apply(ctx) {
|
|
|
630
647
|
const body = JSON.parse(await readBody(req))
|
|
631
648
|
const check = validateNotifyPatch(body)
|
|
632
649
|
requireOk(check.ok, check.reason)
|
|
633
|
-
//
|
|
650
|
+
// 读出当前值做浅合并后整体写回,不依赖宿主写入的嵌套合并语义
|
|
634
651
|
const merged = { ...readNotifySettings(), ...check.patch }
|
|
652
|
+
// 双形写(maintain 同构,判定延迟到使用点):legacy 方法面以 register 为准
|
|
653
|
+
// (0.1.7 的 settings.update 名义在场但按命名空间找条目必败,不可作判据);
|
|
654
|
+
// 0.1.7+ 走 configEditor.edit 落 profile 条目(fiber.entry,与命名空间解耦)
|
|
635
655
|
const settings = ctx.get('settings')
|
|
636
656
|
requireOk(settings !== undefined, 'settings 服务不可用')
|
|
637
|
-
|
|
657
|
+
if (typeof settings.register === 'function' && typeof settings.update === 'function') {
|
|
658
|
+
await settings.update(NAMESPACE, { notify: merged })
|
|
659
|
+
} else {
|
|
660
|
+
const editor = ctx.get('configEditor')
|
|
661
|
+
const entry = ctx.fiber?.entry
|
|
662
|
+
requireOk(editor && typeof editor.edit === 'function' && entry, 'configEditor 服务不可用')
|
|
663
|
+
await editor.edit(entry, (current) => ({ ...current, notify: merged }))
|
|
664
|
+
}
|
|
638
665
|
sendJson(res, 200, { ok: true, notify: publicNotify(readNotifySettings()) })
|
|
639
666
|
}),
|
|
640
667
|
}),
|
|
@@ -765,6 +792,14 @@ export function apply(ctx) {
|
|
|
765
792
|
})
|
|
766
793
|
|
|
767
794
|
ctx.inject(['settings'], (settingsCtx) => {
|
|
795
|
+
// 方法面守卫:settings 服务缺 register 面(0.1.7 已移除)即走新形态分支;
|
|
796
|
+
// 0.1.7 关闭原生自动设置页,设置面由本包自带分区承担(maintain 同构)
|
|
797
|
+
if (typeof settingsCtx.settings.register !== 'function') {
|
|
798
|
+
if (typeof settingsCtx.settings?.configure === 'function') {
|
|
799
|
+
settingsCtx.effect(() => settingsCtx.settings.configure({ auto: false }, ctx.fiber))
|
|
800
|
+
}
|
|
801
|
+
return
|
|
802
|
+
}
|
|
768
803
|
settingsCtx.settings.register(NAMESPACE, SETTINGS_SCHEMA)
|
|
769
804
|
})
|
|
770
805
|
}
|
|
@@ -446,3 +446,105 @@ test('长轮询: cursor 落后当前版本时立即返回不挂起', async () =>
|
|
|
446
446
|
assert.equal(res.payload.units.length, 2)
|
|
447
447
|
assert.ok(res.payload.version > 0)
|
|
448
448
|
})
|
|
449
|
+
|
|
450
|
+
// --- 0.1.7 settings 面双形适配(maintain a913c2c 同构) ---
|
|
451
|
+
|
|
452
|
+
test('Config 导出契约:根级 volatile 包装,validate 产整节单 ref,默认值对拍', async () => {
|
|
453
|
+
const mod = await import('../src/index.js')
|
|
454
|
+
assert.ok(mod.Config, 'Config 导出必须在场')
|
|
455
|
+
const resolved = mod.Config['~standard'].validate({})
|
|
456
|
+
assert.equal(resolved.issues, undefined)
|
|
457
|
+
const section = resolved.value.get()
|
|
458
|
+
assert.equal(section.notify.enabled, false)
|
|
459
|
+
assert.equal(section.notify.toast, true)
|
|
460
|
+
assert.equal(section.notify.imTargets.length, 0)
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
// 0.1.7 形态装配:settings 无 register/get(宿主已移除),configRef 为整节单 ref 桩
|
|
464
|
+
function makeRc17Ctx({ configStore = {}, configEditorAvailable = true } = {}) {
|
|
465
|
+
const routes = new Map()
|
|
466
|
+
const editCalls = []
|
|
467
|
+
const configureCalls = []
|
|
468
|
+
const disposers = []
|
|
469
|
+
const configRef = { get: () => ({ ...configStore }) }
|
|
470
|
+
const settingsService = { configure: (presentation, owner) => { configureCalls.push({ presentation, owner }); return () => {} } }
|
|
471
|
+
const configEditor = {
|
|
472
|
+
async edit(entry, change) {
|
|
473
|
+
editCalls.push(entry)
|
|
474
|
+
const next = change({ ...configStore }, {})
|
|
475
|
+
for (const key of Object.keys(next)) configStore[key] = next[key]
|
|
476
|
+
},
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const ctx = {
|
|
480
|
+
get(name) {
|
|
481
|
+
if (name === 'settings') return settingsService
|
|
482
|
+
if (name === 'configEditor') return configEditorAvailable ? configEditor : undefined
|
|
483
|
+
return undefined
|
|
484
|
+
},
|
|
485
|
+
fiber: { entry: { options: { id: 'usage-panel', name: '@mzzsfy/dsh-usage-panel' } } },
|
|
486
|
+
effect(fn) { fn() },
|
|
487
|
+
inject(_deps, fn) {
|
|
488
|
+
fn({
|
|
489
|
+
settings: settingsService,
|
|
490
|
+
interval: () => {},
|
|
491
|
+
effect(stubEffect) {
|
|
492
|
+
const disposer = stubEffect()
|
|
493
|
+
if (typeof disposer === 'function') disposers.push(disposer)
|
|
494
|
+
},
|
|
495
|
+
})
|
|
496
|
+
},
|
|
497
|
+
webServer: {
|
|
498
|
+
register(route) {
|
|
499
|
+
routes.set(route.path, route.handler)
|
|
500
|
+
return () => routes.delete(route.path)
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
}
|
|
504
|
+
return { ctx, routes, configRef, configStore, editCalls, configureCalls, disposers }
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
test('0.1.7 形态:notify-config GET 读 configRef 节值,POST 经 configEditor.edit 落 fiber.entry', async () => {
|
|
508
|
+
// Given configRef 携带用户值 + configEditor 桩
|
|
509
|
+
const { ctx, routes, configRef, configStore, editCalls } = makeRc17Ctx({
|
|
510
|
+
configStore: { notify: { enabled: true, quotaThresholdPct: 88 } },
|
|
511
|
+
})
|
|
512
|
+
apply(ctx, configRef)
|
|
513
|
+
// When GET notify-config
|
|
514
|
+
const read = await call(routes, '/api/usage-panel/notify-config', makeReq('GET'))
|
|
515
|
+
// Then 返回 configRef 节值
|
|
516
|
+
assert.equal(read.status, 200)
|
|
517
|
+
assert.equal(read.payload.notify.enabled, true)
|
|
518
|
+
assert.equal(read.payload.notify.quotaThresholdPct, 88)
|
|
519
|
+
// When POST 部分补丁
|
|
520
|
+
const saved = await call(routes, '/api/usage-panel/notify-config', makeReq('POST', { enabled: true, balanceThreshold: 12 }))
|
|
521
|
+
// Then 整节合并落本条目
|
|
522
|
+
assert.equal(saved.status, 200)
|
|
523
|
+
assert.equal(saved.payload.ok, true)
|
|
524
|
+
assert.equal(editCalls.length, 1)
|
|
525
|
+
assert.equal(editCalls[0].options.id, 'usage-panel')
|
|
526
|
+
assert.equal(configStore.notify.enabled, true)
|
|
527
|
+
assert.equal(configStore.notify.balanceThreshold, 12)
|
|
528
|
+
assert.equal(configStore.notify.quotaThresholdPct, 88, '未提交字段整节保留')
|
|
529
|
+
})
|
|
530
|
+
|
|
531
|
+
test('0.1.7 形态:configEditor 缺失即 400 拒写,不崩溃', async () => {
|
|
532
|
+
// Given configEditor 服务缺席
|
|
533
|
+
const { ctx, routes, configRef } = makeRc17Ctx({ configEditorAvailable: false })
|
|
534
|
+
apply(ctx, configRef)
|
|
535
|
+
// When POST 写配置
|
|
536
|
+
const denied = await call(routes, '/api/usage-panel/notify-config', makeReq('POST', { enabled: true }))
|
|
537
|
+
// Then 400 错误响应,进程存活(测试走完即证)
|
|
538
|
+
assert.equal(denied.status, 400)
|
|
539
|
+
assert.ok(denied.payload.error)
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
test('0.1.7 形态:settings.configure 在场即关闭原生自动页,register 缺失不抛错', () => {
|
|
543
|
+
// Given settingsForms 带 configure 无 register
|
|
544
|
+
const { ctx, configRef, configureCalls } = makeRc17Ctx()
|
|
545
|
+
// When apply
|
|
546
|
+
apply(ctx, configRef)
|
|
547
|
+
// Then 关闭自动页且 owner 为本插件 fiber
|
|
548
|
+
assert.equal(configureCalls.length, 1)
|
|
549
|
+
assert.deepEqual(configureCalls[0].presentation, { auto: false })
|
|
550
|
+
})
|