@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 CHANGED
@@ -133,4 +133,4 @@ node scripts/dev-link.mjs dsh-usage-panel # 仓库根执行:归一 profile 依
133
133
 
134
134
  ## dsh 版本兼容
135
135
 
136
- 三版本全部通过:用量面板渲染、激活 live。
136
+ 三版本(0.1.2-rc.1 / 0.1.5-rc.3 / 0.1.7-rc.1)全部通过:用量面板渲染(账号卡/通知规则折叠卡全控件)、API 面(pollArmed/通知投影)、激活 live。
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.0",
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
- const value = settings ? settings.get(NAMESPACE) : undefined
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
- // 读出当前值做浅合并后整体写回,不依赖 settings.update 的嵌套合并语义
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
- await settings.update(NAMESPACE, { notify: merged })
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
+ })