@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 +10 -9
- package/package.json +1 -1
- package/src/adapter.js +14 -8
- package/src/api.js +11 -1
- package/src/index.js +3 -0
- package/src/topology.js +56 -0
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
|
-
|
|
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
|
-
|
|
42
|
+
界面长什么样、每块显示什么,见下面的 [设置面板](#设置面板) 一节(含截图)。
|
|
38
43
|
|
|
39
|
-
|
|
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
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
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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')
|
|
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
|
})
|
package/src/topology.js
ADDED
|
@@ -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
|
+
}
|