@huai_xin/dsh-workbuddy 0.1.0 → 0.1.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
@@ -22,11 +22,16 @@ WorkBuddy 模型接入插件(DeepSeek Harness / DSH),同时支持两个部
22
22
  # 从 npm 安装到某个 profile(会同时把 bundle 挂进该 profile)
23
23
  dsh plugin --profile <profile> add @huai_xin/dsh-workbuddy
24
24
 
25
- # 本地目录(未发布时)
25
+ # 直接从 GitHub 安装(npm 版本还没跟上改动时用它)
26
+ dsh plugin --profile <profile> add github:2424830124/dsh-workbuddy
27
+
28
+ # 本地目录(开发时)
26
29
  dsh plugin --profile <profile> add /path/to/dsh-workbuddy
27
30
  ```
28
31
 
29
- 也可以在 Web UI 的插件管理里安装(效果相同,都是往 profile 的 `dsh.profile.bundles` 里加一条并应用 patch)。
32
+ npm 页面:<https://www.npmjs.com/package/@huai_xin/dsh-workbuddy>
33
+
34
+ 也可以在 Web UI 的插件管理里安装(效果相同,都是往 profile 的依赖里加一条并应用 patch)。
30
35
 
31
36
  安装后需要重启 DSH(替换已加载的 JS 模块需要新的 generation),随后 `/workbuddy` 命令即可使用。
32
37
 
@@ -34,12 +39,9 @@ dsh plugin --profile <profile> add /path/to/dsh-workbuddy
34
39
 
35
40
  ### 设置页
36
41
 
37
- **设置 → WorkBuddy**(侧边栏齿轮 → WorkBuddy)是**一张卡片 + 账号标签页**:海外版与大陆版各自独立登录,点标签只切换查看,不影响已登录的账号。
42
+ 界面长什么样、每块显示什么,见下面的 [设置面板](#设置面板) 一节(含截图)。
38
43
 
39
- - 卡片头:状态圆点 + 部署名 + provider/端点 + 状态胶囊
40
- - 信息区:账号、到期时间、是否可自动续期、**积分**(总量 + 其中签到获得的)、**每日签到**(今日状态、连续天数、本周进度点)
41
- - 按钮:**登录**、**立即领取**(签到未完成时可点)、**续期**、**退出登录**
42
- - 卡片下方**常驻该账号的模型列表**,切标签即换列表
44
+ 面板通过宿主路由 `/workbuddy/{state,login,logout,refresh,models,checkin,availability,credits,busy,enabled,import}` 读写状态,这些路由走与宿主一致的连接信任栅栏,只对本机已被 GUI 接纳的来源开放。
43
45
 
44
46
  积分与签到:
45
47
 
@@ -48,8 +50,6 @@ dsh plugin --profile <profile> add /path/to/dsh-workbuddy
48
50
  - 登录成功后也会立刻补一次签到
49
51
  - 海外版后端没有签到接口,只显示积分、不显示领取按钮
50
52
 
51
- 面板通过宿主路由 `/workbuddy/{state,login,logout,refresh,models,checkin}` 读写状态,这些路由走与宿主一致的连接信任栅栏,只对本机已被 GUI 接纳的来源开放。
52
-
53
53
  ### 命令
54
54
 
55
55
  ```
@@ -163,6 +163,7 @@ dsh-workbuddy/
163
163
  - 繁忙程度**只有大陆版有**(`POST /v2/chat/busy`;海外版 404,那一列整个不显示):空闲 / 繁忙 / 拥堵 / 未知四档,用词与官方前端一致(官方把 `normal` 与 `idle` 并成「空闲」),悬停给出官方的完整说法(轻微拥堵 / 极度拥堵)。
164
164
  - 工具栏:搜索框、「全部启用 / 全部停用」(**只作用于可用分组**,不可用分组自己手动调)、「⟳ 刷新列表」、「检测可用性」(手动,立即执行)。这两个按钮宽度固定,「检测中…」不会把 ⟳ 挤走。
165
165
  - 打开面板时会自动检测一次(国内零消耗、可频繁;海外版每次探测一 token,每天最多一次)。
166
+ - 开关**立即生效**:翻转后插件会通知 harness「模型集变了」,模型选择器随即重读目录(它的目录是缓存的,不通知就会一直显示已关掉的模型)。可用性探测结果与真实调用中学到的判定也会即时反映到默认开关上。
166
167
 
167
168
  > 该预览由 `node test/live/ui-preview.mjs` 用真实样式表生成到 `%TEMP%/wb-ui-preview.html`,改版后可以直接截图对比。
168
169
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@huai_xin/dsh-workbuddy",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "WorkBuddy model provider for DeepSeek Harness (international + China deployments)",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
package/src/adapter.js CHANGED
@@ -24,6 +24,7 @@ import { readCatalogCache, writeCatalogCache } from './catalog-cache.js'
24
24
  import { readPrefs, isEnabled } from './model-prefs.js'
25
25
  import { applyOfficialList, readOfficialList } from './official-list.js'
26
26
  import { chatRoutes } from './regions.js'
27
+ import { notifyModelTopology } from './topology.js'
27
28
 
28
29
  const CATALOG_TTL_MS = 10 * 60_000
29
30
  const TOOL_RESULT_IMAGE_TEXT = 'Attached image(s) from tool result:'
@@ -450,12 +451,15 @@ export class WorkBuddyAdapter {
450
451
  if (/11102|service info not found/i.test(text)) {
451
452
  writeAvailability(this.region.id, [{ id: model, state: 'dead', detail: '模型不存在(11102 service info not found)', at: Date.now() }])
452
453
  this.invalidateCatalog()
454
+ // The verdict feeds the default on/off state, so the picker has to re-read.
455
+ notifyModelTopology(this.ctx)
453
456
  return
454
457
  }
455
458
  if (/11158|is not allowed for user type/i.test(text)) {
456
459
  const type = /user type \[([^\]]{0,40})\]/i.exec(text)?.[1] ?? ''
457
460
  writeAvailability(this.region.id, [{ id: model, state: 'denied', detail: `账号无权限(11158${type.length > 0 ? `,用户类型 ${type}` : ''})`, at: Date.now() }])
458
461
  this.invalidateCatalog()
462
+ notifyModelTopology(this.ctx)
459
463
  }
460
464
  }
461
465
 
@@ -538,14 +542,16 @@ export class WorkBuddyAdapter {
538
542
  // when the user asks for it, which is how one gets tried anyway.
539
543
  const prefs = readPrefs(this.region.id)
540
544
  const dead = unusableModelIds(this.region.id)
541
- return catalog
542
- .filter((entry) => isEnabled(prefs, entry.id, dead.has(entry.id)))
543
- .map((entry) => ({
544
- provider,
545
- id: entry.id,
546
- name: entry.name,
547
- inputModalities: [...entry.input]
548
- }))
545
+ const offered = catalog.filter((entry) => isEnabled(prefs, entry.id, dead.has(entry.id)))
546
+ // Always logged: one line per picker open, and the fastest way to answer
547
+ // "did the picker re-read the list after that switch was flipped?".
548
+ this.ctx.logger.info(`[workbuddy] ${this.region.id}: 选择器读取目录 → ${offered.length}/${catalog.length} 个可选`)
549
+ return offered.map((entry) => ({
550
+ provider,
551
+ id: entry.id,
552
+ name: entry.name,
553
+ inputModalities: [...entry.input]
554
+ }))
549
555
  }
550
556
 
551
557
  /**
package/src/api.js CHANGED
@@ -22,6 +22,7 @@ import { localDayKey } from './http.js'
22
22
  import { isEnabled, readPrefs, setEnabled } from './model-prefs.js'
23
23
  import { readOfficialList, writeOfficialList } from './official-list.js'
24
24
  import { REGIONS, REGION_IDS } from './regions.js'
25
+ import { notifyModelTopology } from './topology.js'
25
26
 
26
27
  const API_PREFIX = '/workbuddy'
27
28
  const MAX_BODY_BYTES = 16 * 1024
@@ -219,7 +220,12 @@ export function registerWorkbuddyApi(ctx, adapters) {
219
220
  // The adapter's catalog is the merged one the model picker sees:
220
221
  // both live endpoints merged when they answer, otherwise the last
221
222
  // successful read from disk. `source` lets the panel say which.
222
- if (url.searchParams.get('refresh') === '1') adapter.invalidateCatalog()
223
+ if (url.searchParams.get('refresh') === '1') {
224
+ adapter.invalidateCatalog()
225
+ // A manual refresh can change what the picker may offer, so the
226
+ // client's cached directory has to be told to re-read.
227
+ notifyModelTopology(ctx)
228
+ }
223
229
  const models = await adapter.models()
224
230
  const ledger = readAvailability(region.id)
225
231
  const imported = readOfficialList(region.id)
@@ -292,6 +298,9 @@ export function registerWorkbuddyApi(ctx, adapters) {
292
298
  const models = adapter === undefined ? [] : await adapter.models()
293
299
  const dead = unusableModelIds(region.id)
294
300
  const prefs = readPrefs(region.id)
301
+ // The picker caches its catalog until it hears this, so without the
302
+ // notification a model switched off here stays selectable there.
303
+ notifyModelTopology(ctx)
295
304
  ctx.logger.info(`[workbuddy] ${region.id}: ${reset ? '恢复默认' : (enabled ? '启用' : '停用')} ${ids.length} 个模型(${ids.length <= 3 ? ids.join(', ') : `${ids.slice(0, 3).join(', ')}…`}),手工设定 开${result.on}/关${result.off}`)
296
305
  sendJson(res, 200, {
297
306
  region: region.id,
@@ -440,6 +449,7 @@ export function registerWorkbuddyApi(ctx, adapters) {
440
449
  const result = writeOfficialList(region.id, body.models, { from: source })
441
450
  adapter?.invalidateCatalog()
442
451
  const models = adapter === undefined ? [] : await adapter.models()
452
+ notifyModelTopology(ctx)
443
453
  ctx.logger.info(`[workbuddy] ${region.id}: 账号名单${result.cleared === true ? '已清除' : `已导入 ${result.count} 个(其中 ${result.withMeta ?? 0} 个带元数据)`}(${source}),当前列表 ${models.length} 个`)
444
454
  sendJson(res, 200, {
445
455
  region: region.id,
package/src/index.js CHANGED
@@ -23,6 +23,7 @@ import { claimDailyCheckin, runAutoClaim } from './credits.js'
23
23
  import { msUntilNextMidnight } from './http.js'
24
24
  import { readPrefs } from './model-prefs.js'
25
25
  import { REGIONS, REGION_IDS, regionList } from './regions.js'
26
+ import { notifyModelTopology } from './topology.js'
26
27
 
27
28
  export const name = 'dsh-workbuddy'
28
29
  export const inject = ['commands', 'credentials', 'llm']
@@ -257,6 +258,8 @@ export function apply(ctx) {
257
258
  for (const verdict of result.results) counts.set(verdict.state, (counts.get(verdict.state) ?? 0) + 1)
258
259
  ctx.logger.info(`[workbuddy] ${id}: 启动可用性检测完成 ${result.results.length} 个${region.freeProbe === true ? '(零消耗)' : '(每个模型一个 token)'} — ${[...counts.entries()].map(([state, count]) => `${state} ${count}`).join(' / ')}`)
259
260
  adapter.invalidateCatalog()
261
+ // Verdicts decide the default on/off state, so tell the picker to re-read.
262
+ notifyModelTopology(ctx)
260
263
  })().catch((error) => {
261
264
  ctx.logger.warn(`[workbuddy] ${id}: 启动可用性检测失败 — ${error instanceof Error ? error.message : String(error)}`)
262
265
  })
@@ -0,0 +1,56 @@
1
+ /**
2
+ * The one place that tells the harness its model list changed.
3
+ *
4
+ * The Web client's model directory caches the catalog and only re-reads it when
5
+ * it hears a forwarded host event — `llm/adapters-updated` among them. Its
6
+ * declared contract says exactly what this needs: a payload-free notification
7
+ * after which "consumers re-read `listProviders()`, `listModels()`, or
8
+ * `listConfigurableProviders()` for the new state". Nothing else re-lists on a
9
+ * timer, so a switch the user just flipped, an imported account list, or a probe
10
+ * that retired a model stays invisible: the picker keeps offering models the panel
11
+ * has already switched off.
12
+ *
13
+ * `llm/adapters-updated` is on the app's forwarded-event allowlist and the
14
+ * forwarding layer subscribes with a plain `ctx.on(event, …)`, so emitting it
15
+ * reaches host listeners and every connected client. `ctx.emit` is preferred
16
+ * because the event name is a declared contract; the `llm` service's own
17
+ * `emitAdaptersUpdated()` (same dispatch, absent from the published contract) is
18
+ * the fallback.
19
+ *
20
+ * Everything is duck-typed, and the outcome is logged: when neither channel
21
+ * exists the panel still works, and the log says so instead of leaving a silent
22
+ * "the picker didn't refresh" mystery.
23
+ */
24
+
25
+ /**
26
+ * @param {object} ctx - host plugin context.
27
+ * @returns {boolean} whether a notification was delivered.
28
+ */
29
+ export function notifyModelTopology(ctx) {
30
+ let route = 'none'
31
+ let delivered = false
32
+ try {
33
+ if (typeof ctx?.emit === 'function') {
34
+ ctx.emit('llm/adapters-updated')
35
+ route = 'ctx.emit'
36
+ delivered = true
37
+ } else {
38
+ const llm = ctx?.llm ?? ctx?.get?.('llm')
39
+ if (llm !== undefined && llm !== null && typeof llm.emitAdaptersUpdated === 'function') {
40
+ llm.emitAdaptersUpdated()
41
+ route = 'llm.emitAdaptersUpdated'
42
+ delivered = true
43
+ }
44
+ }
45
+ } catch (error) {
46
+ ctx?.logger?.warn?.(`[workbuddy] 通知模型拓扑变化失败(不影响本次操作):${error instanceof Error ? error.message : String(error)}`)
47
+ return false
48
+ }
49
+
50
+ ctx?.logger?.info?.(
51
+ delivered
52
+ ? `[workbuddy] 已通知模型拓扑变化(${route})`
53
+ : '[workbuddy] 无法通知模型拓扑变化:当前上下文既没有 ctx.emit 也没有 llm.emitAdaptersUpdated,模型选择器要等它自己重读才会更新'
54
+ )
55
+ return delivered
56
+ }