dsh-workbuddy-connect 0.2.6 → 0.3.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.en.md +18 -6
- package/README.md +18 -6
- package/lib/bin.js +1 -1
- package/lib/client.js +12 -1
- package/lib/{host-heartbeat-sa98SyZ9.js → host-heartbeat-9FtwMcIF.js} +15 -4
- package/lib/index.d.ts +14 -2
- package/lib/index.js +97 -76
- package/package.json +29 -30
package/README.en.md
CHANGED
|
@@ -10,13 +10,13 @@ Brings every model in the WorkBuddy desktop app (GLM-5.3, GLM-5.2, DeepSeek-V4-P
|
|
|
10
10
|
|
|
11
11
|

|
|
12
12
|
|
|
13
|
-
- **Image input**:
|
|
13
|
+
- **Image input**: most models accept images — paste or drop one straight into the conversation (GLM-5.3-Flash, GLM-5.2, the DeepSeek-V4 series, and more); the few text-only models (e.g. GLM-5.1) clearly say so.
|
|
14
14
|
|
|
15
|
-
- **Thinking effort**: the model picker
|
|
15
|
+
- **Thinking effort**: the model picker lets you switch the thinking effort on models that support it — GLM-5.3 offers low / high / xhigh and GLM-5.3-Flash offers low / high / max. Models without the option cannot be adjusted and use WorkBuddy's default.
|
|
16
16
|
|
|
17
|
-
- **
|
|
17
|
+
- **Promo badges**: promo badges (`限时免费`, `夜间折扣`) ride the model name itself (e.g. `Hy4 preview · x0.00 · 限时免费`), visible wherever you pick a model; the status card also collects currently-discounted models. Per the WorkBuddy service data, synced each time DSH starts.
|
|
18
18
|
|
|
19
|
-
- **Rate
|
|
19
|
+
- **Rate**: every model name carries its credits multiplier (e.g. `GLM-5.2 · x0.79`, `Hy3 · x0.00`) in both the `/model` popup and the composer's model dropdown. The rate is display-only and never affects requests.
|
|
20
20
|
|
|
21
21
|
- **Info at a glance**: Settings → Plugins → DSH WorkBuddy Connect card
|
|
22
22
|
|
|
@@ -30,6 +30,16 @@ Expand the card to see the account, token validity, and remaining credit.
|
|
|
30
30
|
|
|
31
31
|
Prerequisite: the WorkBuddy desktop app is installed and signed in (the plugin reuses the app's sign-in state and follows account switches automatically).
|
|
32
32
|
|
|
33
|
+
**Match the plugin version to your DSH core** — a mismatched combination fails to start DSH:
|
|
34
|
+
|
|
35
|
+
| Plugin | Required DSH core | Desktop app |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| **0.3.0+** | `0.1.2-rc.1` or newer | `2.0.5`+ recommended |
|
|
38
|
+
| **0.2.6** | `0.1.1-rc.2` (older line) | `2.0.3` / `2.0.4` |
|
|
39
|
+
|
|
40
|
+
- On DSH `0.1.2-rc.1` or newer, just install the latest: `dsh plugin --profile web add dsh-workbuddy-connect`
|
|
41
|
+
- Still on DSH `0.1.1-rc.2`? Stay on the older release: `dsh plugin --profile web add dsh-workbuddy-connect@0.2.6`
|
|
42
|
+
|
|
33
43
|
The plugin runs under all three DSH interfaces: **Web**, **Desktop**, and **TUI**. Pick the install command that matches the profile you use.
|
|
34
44
|
|
|
35
45
|
```sh
|
|
@@ -54,7 +64,9 @@ dsh plugin --profile dsh-tui add dsh-workbuddy-connect
|
|
|
54
64
|
dsh --profile dsh-tui
|
|
55
65
|
```
|
|
56
66
|
|
|
57
|
-
>
|
|
67
|
+
> **TUI users should stay on 0.2.6**: in our testing, installing this plugin 0.3.0 on the TUI profile crashes startup with `events is not iterable` — the terminal UI package (`@deepseek-harness-tui/dsh-tui`) has not been adapted to the new DSH core yet (the fix is committed upstream, unreleased). We recommend TUI users keep DSH `0.1.1-rc.2` and plugin `0.2.6` for now, and upgrade to 0.3.0 once the terminal UI ships an adapted release.
|
|
68
|
+
|
|
69
|
+
> Note: the `dsh-tui` profile requires pnpm 11 to install packages (a different pnpm on PATH fails with `ERR_PNPM_UNEXPECTED_STORE` — use `npx pnpm@11`).
|
|
58
70
|
|
|
59
71
|
After installing, switch to a WorkBuddy model in the model picker of the interface you chose. On Web, the settings card (Settings → Plugins → DSH WorkBuddy Connect) shows the account, token validity, and remaining credit; on TUI, configure `authFile` in `/settings`.
|
|
60
72
|
|
|
@@ -64,7 +76,7 @@ After installing, switch to a WorkBuddy model in the model picker of the interfa
|
|
|
64
76
|
|
|
65
77
|
## Known limitations
|
|
66
78
|
|
|
67
|
-
- Verified on macOS with the DSH Web / Desktop
|
|
79
|
+
- Verified on macOS with the DSH Web / Desktop profiles (`0.1.2-rc.1`+, Node 22+); TUI pending a terminal-UI release adapted to 0.1.2 (see the Install section). Windows probes Local and Roaming AppData in order; WSL first reads credentials from the mounted Windows user profile. If the Windows and Linux user names differ and Windows environment variables are not forwarded into WSL, point `WORKBUDDY_AUTH_FILE` at the actual file.
|
|
68
80
|
- Relies on WorkBuddy client interfaces (not a public API); the plugin may need updates as WorkBuddy changes.
|
|
69
81
|
|
|
70
82
|
## Disclaimer
|
package/README.md
CHANGED
|
@@ -15,16 +15,16 @@
|
|
|
15
15
|

|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
-
|
|
18
|
+
- **图片输入**:大部分模型支持发图,在对话里直接粘贴或拖入图片即可(GLM-5.3-Flash、GLM-5.2、DeepSeek-V4 系列等);少数只支持文字的模型(如 GLM-5.1)会明确提示不支持。
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
-
|
|
21
|
+
- **思考强度**:模型选择器里可为支持的模型切换思考强度,例如 GLM-5.3 可选 low / high / xhigh,GLM-5.3-Flash 可选 low / high / max;没有出现选项的模型不支持调整,使用 WorkBuddy 的默认档位。
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
-
|
|
24
|
+
- **徽章展示**:促销徽章(限时免费、夜间折扣)直接跟在模型名后面(如 `Hy4 preview · x0.00 · 限时免费`),选模型时一眼可见;设置卡片里也会汇总当前有优惠的模型。以 WorkBuddy 服务端的数据为准,每次启动 DSH 时同步。
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
-
|
|
27
|
+
- **费率比例**:模型选择列表里每个模型名后直接显示积分倍率(如 `GLM-5.2 · x0.79`、`Hy3 · x0.00`),`/model` 弹窗与输入框的模型下拉都能看到。倍率只是显示,不影响实际请求。
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
- **信息查看**:设置 → 插件 → DSH WorkBuddy Connect 卡片
|
|
@@ -40,6 +40,16 @@
|
|
|
40
40
|
|
|
41
41
|
前置:已安装并登录 WorkBuddy 桌面 App(插件复用 App 的登录状态,账号切换自动跟随)。
|
|
42
42
|
|
|
43
|
+
**版本对应(重要)**:本插件与 DSH 核心版本一一对应,不可混用——不匹配的组合会导致 DSH 启动失败:
|
|
44
|
+
|
|
45
|
+
| 插件版本 | 要求的 DSH 核心 | 桌面 App |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| **0.3.0+** | `0.1.2-rc.1` 及以上 | 建议 `2.0.5`+ |
|
|
48
|
+
| **0.2.6** | `0.1.1-rc.2`(旧线) | `2.0.3` / `2.0.4` |
|
|
49
|
+
|
|
50
|
+
- DSH `0.1.2-rc.1` 及以上的用户,正常安装最新版即可:`dsh plugin --profile web add dsh-workbuddy-connect`
|
|
51
|
+
- 还在用 DSH `0.1.1-rc.2` 的用户,请安装旧版本并停留在 `0.2.6`:`dsh plugin --profile web add dsh-workbuddy-connect@0.2.6`
|
|
52
|
+
|
|
43
53
|
插件在三种 DSH 界面下均可运行:**Web**、**Desktop**、**TUI**。根据你使用的 profile 选对应命令安装。
|
|
44
54
|
|
|
45
55
|
```sh
|
|
@@ -64,7 +74,9 @@ dsh plugin --profile dsh-tui add dsh-workbuddy-connect
|
|
|
64
74
|
dsh --profile dsh-tui
|
|
65
75
|
```
|
|
66
76
|
|
|
67
|
-
>
|
|
77
|
+
> **TUI 用户请先留在 0.2.6**:实测在 TUI 上安装本插件 0.3.0 会导致启动崩溃(报 `events is not iterable`),原因是终端界面插件 `@deepseek-harness-tui/dsh-tui` 还没适配 DSH 新核心(修复已提交,尚未发版)。建议 TUI 用户暂时继续使用 DSH `0.1.1-rc.2` 和本插件 `0.2.6`,等终端界面插件发布适配版本后再升级 0.3.0。
|
|
78
|
+
|
|
79
|
+
> 提示:`dsh-tui` profile 需用 pnpm 11 安装(PATH 里是其他版本会报 `ERR_PNPM_UNEXPECTED_STORE`,用 `npx pnpm@11` 即可)。
|
|
68
80
|
|
|
69
81
|
安装后,在对应界面的模型选择器里切换到 WorkBuddy 模型即可使用;Web 下设置卡片(设置 → 插件 → DSH WorkBuddy Connect)可查看账号信息、令牌有效期与剩余积分,TUI 下可在 `/settings` 里配置 `authFile`。
|
|
70
82
|
|
|
@@ -74,7 +86,7 @@ dsh --profile dsh-tui
|
|
|
74
86
|
|
|
75
87
|
## 已知限制
|
|
76
88
|
|
|
77
|
-
- 在 macOS 的 DSH Web / Desktop
|
|
89
|
+
- 在 macOS 的 DSH Web / Desktop profile(`0.1.2-rc.1`+、Node 22+)下验证通过;TUI 待终端界面插件发布 0.1.2 适配版后验证(见安装章节说明)。Windows 会依次探测 Local 与 Roaming AppData;WSL 会优先从挂载的 Windows 用户目录读取登录凭据。若 Windows 与 Linux 用户名不同且 Windows 环境变量未传入 WSL,请通过 `WORKBUDDY_AUTH_FILE` 指定实际位置。
|
|
78
90
|
- 依赖 WorkBuddy 客户端接口(非官方开放 API),WorkBuddy 更新后插件可能需要随之调整。
|
|
79
91
|
|
|
80
92
|
## 免责声明
|
package/lib/bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { S as workbuddyOwnAuthPath, a as readHostHeartbeat, c as WORKBUDDY_CONNECT_VERSION, l as WorkBuddyUpstreamClient, m as FALLBACK_WORKBUDDY_MODELS, o as workbuddyHostHeartbeatPath, r as isHeartbeatProcessAlive, v as WorkBuddyCredentialStore } from "./host-heartbeat-
|
|
2
|
+
import { S as workbuddyOwnAuthPath, a as readHostHeartbeat, c as WORKBUDDY_CONNECT_VERSION, l as WorkBuddyUpstreamClient, m as FALLBACK_WORKBUDDY_MODELS, o as workbuddyHostHeartbeatPath, r as isHeartbeatProcessAlive, v as WorkBuddyCredentialStore } from "./host-heartbeat-9FtwMcIF.js";
|
|
3
3
|
import { realpathSync } from "node:fs";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
//#region src/bin.ts
|
package/lib/client.js
CHANGED
|
@@ -470,7 +470,18 @@ window.__ModuleLoader__.load({
|
|
|
470
470
|
//#region src/client/index.tsx
|
|
471
471
|
/** Stable browser-plugin name. */
|
|
472
472
|
const name = "dsh-workbuddy-connect-client";
|
|
473
|
-
/**
|
|
473
|
+
/**
|
|
474
|
+
* Client services required by the Plugin configuration contribution.
|
|
475
|
+
*
|
|
476
|
+
* DSH 0.1.2 removed `@deepseek-ai/dsh-client-runtime` (the package that used to
|
|
477
|
+
* hold the browser `ClientContext` alias and the `slots` service). The services
|
|
478
|
+
* this card relies on now come from narrower packages: the `slots` registry
|
|
479
|
+
* moved to `@deepseek-ai/dsh-client-ui-renderer`, `locale` stayed in
|
|
480
|
+
* `@deepseek-ai/dsh-client-locale`, and the `settings.plugin.item` slot is
|
|
481
|
+
* declared by `@deepseek-ai/dsh-client-ui-settings-plugins`. All three are
|
|
482
|
+
* named in the package's `dsh.client.inject` list, so cordis has activated
|
|
483
|
+
* them before this plugin's fiber starts.
|
|
484
|
+
*/
|
|
474
485
|
const inject = ["slots", "locale"];
|
|
475
486
|
/**
|
|
476
487
|
* Register card copy and the WorkBuddy card under Plugin configuration.
|
|
@@ -142,10 +142,21 @@ function parseOwnDocument(text) {
|
|
|
142
142
|
const document = parsed;
|
|
143
143
|
if (document["version"] !== OWN_FORMAT_VERSION) return void 0;
|
|
144
144
|
if (typeof document["credential"] !== "object" || document["credential"] === null) return void 0;
|
|
145
|
-
const
|
|
146
|
-
|
|
145
|
+
const stored = document["credential"];
|
|
146
|
+
const accessToken = typeof stored["accessToken"] === "string" ? stored["accessToken"] : "";
|
|
147
|
+
if (accessToken === "") return void 0;
|
|
148
|
+
const refreshExpiresAtMs = typeof stored["refreshExpiresAtMs"] === "number" ? stored["refreshExpiresAtMs"] : void 0;
|
|
149
|
+
const enterpriseId = optionalString(stored["enterpriseId"]);
|
|
150
|
+
const nickname = optionalString(stored["nickname"]);
|
|
147
151
|
return {
|
|
148
|
-
|
|
152
|
+
accessToken,
|
|
153
|
+
refreshToken: typeof stored["refreshToken"] === "string" ? stored["refreshToken"] : "",
|
|
154
|
+
expiresAtMs: typeof stored["expiresAtMs"] === "number" ? stored["expiresAtMs"] : 0,
|
|
155
|
+
...refreshExpiresAtMs === void 0 ? {} : { refreshExpiresAtMs },
|
|
156
|
+
domain: optionalString(stored["domain"]) ?? "",
|
|
157
|
+
uid: optionalString(stored["uid"]) ?? "",
|
|
158
|
+
...enterpriseId === void 0 ? {} : { enterpriseId },
|
|
159
|
+
...nickname === void 0 ? {} : { nickname },
|
|
149
160
|
source: "dsh"
|
|
150
161
|
};
|
|
151
162
|
}
|
|
@@ -1041,7 +1052,7 @@ var WorkBuddyUpstreamClient = class {
|
|
|
1041
1052
|
};
|
|
1042
1053
|
//#endregion
|
|
1043
1054
|
//#region src/version.ts
|
|
1044
|
-
const WORKBUDDY_CONNECT_VERSION = "0.
|
|
1055
|
+
const WORKBUDDY_CONNECT_VERSION = "0.3.1";
|
|
1045
1056
|
//#endregion
|
|
1046
1057
|
//#region src/host-heartbeat.ts
|
|
1047
1058
|
/**
|
package/lib/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import z from "@deepseek-ai/schemastery";
|
|
2
2
|
import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
|
|
3
3
|
import { Context } from "@deepseek-ai/cordis";
|
|
4
|
+
import { SettingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
4
5
|
import { AttachmentStore } from "@deepseek-ai/dsh-attachment";
|
|
5
6
|
//#region src/upstream.d.ts
|
|
6
7
|
/** WorkBuddy region selected by the credential's login domain. */
|
|
@@ -407,8 +408,19 @@ declare function isHeartbeatProcessAlive(heartbeat: WorkBuddyHostHeartbeat): boo
|
|
|
407
408
|
declare const name = "llm-workbuddy";
|
|
408
409
|
/** The model registry required before the provider can register. */
|
|
409
410
|
declare const inject: string[];
|
|
410
|
-
/**
|
|
411
|
-
|
|
411
|
+
/**
|
|
412
|
+
* Settings namespace owning the configuration card.
|
|
413
|
+
*
|
|
414
|
+
* DSH 0.1.2 dropped the `settingsNamespace()` branding function: a namespace is
|
|
415
|
+
* now a nominal string, validated by the type system where it is used rather
|
|
416
|
+
* than at runtime by a function call. The brand is compile-time only, so this
|
|
417
|
+
* stays the plain string it always was — every comparison, descriptor lookup,
|
|
418
|
+
* and `dsh` config file still sees `'workbuddy'`. It is cast once here so the
|
|
419
|
+
* public constant carries the seam's type without pulling the brand helper
|
|
420
|
+
* into this package (upstream DSH plugins, `dsh-llm-pi-ai` included, pass
|
|
421
|
+
* their namespaces as plain string literals).
|
|
422
|
+
*/
|
|
423
|
+
declare const WORKBUDDY_SETTINGS_NS: SettingsNamespace;
|
|
412
424
|
/** Plugin configuration. */
|
|
413
425
|
interface Config {
|
|
414
426
|
/** Explicit WorkBuddy desktop auth-file path, overriding env and platform defaults. */
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { S as workbuddyOwnAuthPath, _ as WORKBUDDY_AUTH_FILE_ENV, a as readHostHeartbeat, b as defaultDesktopAuthPath, d as normalizeCredits, f as prepareChatBody, g as WORKBUDDY_AUTH_FILENAME, h as WorkBuddyCatalog, i as processStartTimeMs, l as WorkBuddyUpstreamClient, m as FALLBACK_WORKBUDDY_MODELS, n as clearHostHeartbeat, o as workbuddyHostHeartbeatPath, p as regionOf, r as isHeartbeatProcessAlive, s as writeHostHeartbeat, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, u as classifyUpstreamError, v as WorkBuddyCredentialStore, x as parseWorkBuddyAuth, y as defaultDesktopAuthCandidates } from "./host-heartbeat-
|
|
1
|
+
import { S as workbuddyOwnAuthPath, _ as WORKBUDDY_AUTH_FILE_ENV, a as readHostHeartbeat, b as defaultDesktopAuthPath, d as normalizeCredits, f as prepareChatBody, g as WORKBUDDY_AUTH_FILENAME, h as WorkBuddyCatalog, i as processStartTimeMs, l as WorkBuddyUpstreamClient, m as FALLBACK_WORKBUDDY_MODELS, n as clearHostHeartbeat, o as workbuddyHostHeartbeatPath, p as regionOf, r as isHeartbeatProcessAlive, s as writeHostHeartbeat, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, u as classifyUpstreamError, v as WorkBuddyCredentialStore, x as parseWorkBuddyAuth, y as defaultDesktopAuthCandidates } from "./host-heartbeat-9FtwMcIF.js";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
|
-
import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
4
3
|
import { createProvider } from "@earendil-works/pi-ai";
|
|
5
4
|
import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
|
|
6
5
|
import { resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
|
|
@@ -74,11 +73,12 @@ const RATE_SEPARATOR = " · ";
|
|
|
74
73
|
/**
|
|
75
74
|
* Append the billing rate to one model's display name.
|
|
76
75
|
*
|
|
77
|
-
* The rate
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* `description`
|
|
76
|
+
* The rate AND the declared promo badges ride the *name* alone: since DSH
|
|
77
|
+
* 0.1.2 the composer's model seat (`ModelSelect`) renders `model.name` only —
|
|
78
|
+
* `description` is no longer read there at all (the 0.1.1-era client rendered
|
|
79
|
+
* it, which is why the badges used to be visible in the seat). The `/model`
|
|
80
|
+
* popup renders the name too, so a separate `description` copy would either
|
|
81
|
+
* duplicate (rate) or vanish (badges) depending on client generation.
|
|
82
82
|
*
|
|
83
83
|
* This is display-only and cannot affect routing: the wire request is built
|
|
84
84
|
* from `model.id` (pi-ai's completions API sets `model: model.id`), the
|
|
@@ -87,19 +87,20 @@ const RATE_SEPARATOR = " · ";
|
|
|
87
87
|
* contents. Nothing in the host resolves a model *by* name.
|
|
88
88
|
*/
|
|
89
89
|
/**
|
|
90
|
-
* The
|
|
91
|
-
*
|
|
92
|
-
* labels are the upstream's own spellings and the host seam has no
|
|
93
|
-
* service, so non-Chinese UIs see them verbatim — accepted until the
|
|
94
|
-
* grows a localized badge slot.
|
|
90
|
+
* The catalog display suffix: the billing rate followed by the declared promo
|
|
91
|
+
* badges (`限时免费`, `夜间折扣`), or undefined when the row carries neither.
|
|
92
|
+
* The badge labels are the upstream's own spellings and the host seam has no
|
|
93
|
+
* locale service, so non-Chinese UIs see them verbatim — accepted until the
|
|
94
|
+
* picker grows a localized badge slot.
|
|
95
95
|
*/
|
|
96
|
-
function
|
|
97
|
-
const
|
|
98
|
-
return
|
|
96
|
+
function displaySuffix(info) {
|
|
97
|
+
const parts = [normalizeCredits(info.billing?.credits), ...info.billing?.badges ?? []].filter((part) => part !== void 0 && part !== "");
|
|
98
|
+
return parts.length === 0 ? void 0 : parts.join(" · ");
|
|
99
99
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
/** Append the catalog display suffix to one model's display name. */
|
|
101
|
+
function withCatalogDisplay(name, info) {
|
|
102
|
+
const suffix = displaySuffix(info);
|
|
103
|
+
return suffix === void 0 ? name : `${name}${RATE_SEPARATOR}${suffix}`;
|
|
103
104
|
}
|
|
104
105
|
/**
|
|
105
106
|
* Resolve a WorkBuddy model's reasoning capability into pi-ai's
|
|
@@ -235,11 +236,9 @@ var WorkBuddyPiAiAdapter = class extends PiAiAdapter {
|
|
|
235
236
|
return (await super.listModels(provider)).map((model) => {
|
|
236
237
|
const info = this.infoFor(model.id);
|
|
237
238
|
if (info === void 0) return model;
|
|
238
|
-
const promo = promoDescription(info);
|
|
239
239
|
return {
|
|
240
240
|
...model,
|
|
241
|
-
name:
|
|
242
|
-
...promo === void 0 ? {} : { description: promo }
|
|
241
|
+
name: withCatalogDisplay(model.name, info)
|
|
243
242
|
};
|
|
244
243
|
});
|
|
245
244
|
}
|
|
@@ -247,34 +246,22 @@ var WorkBuddyPiAiAdapter = class extends PiAiAdapter {
|
|
|
247
246
|
const resolved = await super.resolveModel(provider, model, signal);
|
|
248
247
|
const info = this.infoFor(model);
|
|
249
248
|
if (info === void 0) return resolved;
|
|
250
|
-
const promo = promoDescription(info);
|
|
251
249
|
return {
|
|
252
250
|
...resolved,
|
|
253
|
-
name:
|
|
254
|
-
...promo === void 0 ? {} : { description: promo }
|
|
251
|
+
name: withCatalogDisplay(resolved.name, info)
|
|
255
252
|
};
|
|
256
253
|
}
|
|
257
254
|
};
|
|
258
255
|
//#endregion
|
|
259
|
-
//#region src/
|
|
256
|
+
//#region src/loopback.ts
|
|
260
257
|
/**
|
|
261
|
-
*
|
|
262
|
-
* shim
|
|
263
|
-
*
|
|
264
|
-
* It binds 127.0.0.1 only and never serves another interface.
|
|
265
|
-
*
|
|
266
|
-
* Inbound hardening: the loopback bind alone is not a trust boundary (any
|
|
267
|
-
* local process or a DNS-rebinding page can reach 127.0.0.1), so every
|
|
268
|
-
* request must carry a loopback Host header, browser-sent Origins must be
|
|
269
|
-
* loopback, chat POSTs must be application/json, and the Authorization
|
|
270
|
-
* header must carry the shim's per-process shared secret. The plugin's
|
|
271
|
-
* own client satisfies all four by construction; local attackers cannot
|
|
272
|
-
* read the secret out of the plugin process's memory.
|
|
258
|
+
* Shared loopback gates for the plugin's local HTTP surfaces: the loopback
|
|
259
|
+
* shim and the same-origin web-status route. Both are only ever meant to be
|
|
260
|
+
* addressed through the machine's loopback interface.
|
|
273
261
|
*
|
|
274
|
-
* @module dsh-workbuddy-connect/
|
|
262
|
+
* @module dsh-workbuddy-connect/loopback
|
|
275
263
|
*/
|
|
276
|
-
|
|
277
|
-
/** Loopback hostnames the shim's own in-process client uses. */
|
|
264
|
+
/** Loopback hostnames a local plugin surface may be addressed by. */
|
|
278
265
|
const LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
|
|
279
266
|
"127.0.0.1",
|
|
280
267
|
"localhost",
|
|
@@ -288,7 +275,7 @@ function hostnameOfHost(host) {
|
|
|
288
275
|
return end === -1 ? hostname : hostname.slice(0, end + 1);
|
|
289
276
|
}
|
|
290
277
|
const colon = hostname.lastIndexOf(":");
|
|
291
|
-
if (colon !== -1 && /^\d+$/.test(hostname.slice(colon + 1))) hostname = hostname.slice(0, colon);
|
|
278
|
+
if (colon !== -1 && !hostname.slice(0, colon).includes(":") && /^\d+$/.test(hostname.slice(colon + 1))) hostname = hostname.slice(0, colon);
|
|
292
279
|
return hostname;
|
|
293
280
|
}
|
|
294
281
|
/**
|
|
@@ -313,6 +300,25 @@ function originIsLoopback(origin) {
|
|
|
313
300
|
return false;
|
|
314
301
|
}
|
|
315
302
|
}
|
|
303
|
+
//#endregion
|
|
304
|
+
//#region src/shim.ts
|
|
305
|
+
/**
|
|
306
|
+
* Loopback OpenAI-compatible endpoint. The pi-ai provider points here; the
|
|
307
|
+
* shim applies the WorkBuddy wire quirks (forced streaming, string
|
|
308
|
+
* `tool_choice`, CLI-shaped headers) and forwards to the real upstream.
|
|
309
|
+
* It binds 127.0.0.1 only and never serves another interface.
|
|
310
|
+
*
|
|
311
|
+
* Inbound hardening: the loopback bind alone is not a trust boundary (any
|
|
312
|
+
* local process or a DNS-rebinding page can reach 127.0.0.1), so every
|
|
313
|
+
* request must carry a loopback Host header, browser-sent Origins must be
|
|
314
|
+
* loopback, chat POSTs must be application/json, and the Authorization
|
|
315
|
+
* header must carry the shim's per-process shared secret. The plugin's
|
|
316
|
+
* own client satisfies all four by construction; local attackers cannot
|
|
317
|
+
* read the secret out of the plugin process's memory.
|
|
318
|
+
*
|
|
319
|
+
* @module dsh-workbuddy-connect/shim
|
|
320
|
+
*/
|
|
321
|
+
const REQUEST_BODY_LIMIT = 67108864;
|
|
316
322
|
/** Chat-completion POSTs must carry a JSON body type (simple-request CSRF drops here). */
|
|
317
323
|
function isJsonContentType(req) {
|
|
318
324
|
const type = req.headers["content-type"];
|
|
@@ -503,16 +509,14 @@ function json(res, status, body) {
|
|
|
503
509
|
});
|
|
504
510
|
res.end(payload);
|
|
505
511
|
}
|
|
506
|
-
/**
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
return false;
|
|
515
|
-
}
|
|
512
|
+
/**
|
|
513
|
+
* The request must be addressed to the loopback interface, and a
|
|
514
|
+
* browser-attached Origin must be loopback too. The Host check drops
|
|
515
|
+
* DNS-rebinding pages (their Host is the attacker's domain, not loopback);
|
|
516
|
+
* the card's same-origin fetches carry no Origin and pass on Host alone.
|
|
517
|
+
*/
|
|
518
|
+
function loopbackRequest(req) {
|
|
519
|
+
return hostIsLoopback(req.headers.host) && originIsLoopback(req.headers.origin);
|
|
516
520
|
}
|
|
517
521
|
/**
|
|
518
522
|
* Assemble the card's status document. Sign-in state is read-only; credit is
|
|
@@ -560,27 +564,31 @@ async function workBuddyWebStatus(deps) {
|
|
|
560
564
|
}
|
|
561
565
|
return statusWithModels;
|
|
562
566
|
}
|
|
567
|
+
/** The status route's request handler, extracted so tests can mount it on a bare server. */
|
|
568
|
+
function workBuddyStatusHandler(deps) {
|
|
569
|
+
return async (req, res) => {
|
|
570
|
+
if (req.method !== "GET") {
|
|
571
|
+
json(res, 405, { error: "method not allowed" });
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
if (!loopbackRequest(req)) {
|
|
575
|
+
json(res, 403, { error: "request-not-trusted" });
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
try {
|
|
579
|
+
json(res, 200, await workBuddyWebStatus(deps));
|
|
580
|
+
} catch (error) {
|
|
581
|
+
json(res, 500, { error: safeMessage(error) });
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
}
|
|
563
585
|
/** Mount the GET status route on an optional webServer context. */
|
|
564
586
|
function registerWorkBuddyStatusRoute(ctx, deps) {
|
|
565
587
|
ctx.effect(() => {
|
|
566
588
|
const dispose = ctx.webServer.register({
|
|
567
589
|
kind: "exact",
|
|
568
590
|
path: WORKBUDDY_STATUS_PATH,
|
|
569
|
-
handler:
|
|
570
|
-
if (req.method !== "GET") {
|
|
571
|
-
json(res, 405, { error: "method not allowed" });
|
|
572
|
-
return;
|
|
573
|
-
}
|
|
574
|
-
if (!loopbackOrigin(req)) {
|
|
575
|
-
json(res, 403, { error: "origin-not-trusted" });
|
|
576
|
-
return;
|
|
577
|
-
}
|
|
578
|
-
try {
|
|
579
|
-
json(res, 200, await workBuddyWebStatus(deps));
|
|
580
|
-
} catch (error) {
|
|
581
|
-
json(res, 500, { error: safeMessage(error) });
|
|
582
|
-
}
|
|
583
|
-
}
|
|
591
|
+
handler: workBuddyStatusHandler(deps)
|
|
584
592
|
});
|
|
585
593
|
return () => {
|
|
586
594
|
dispose();
|
|
@@ -593,8 +601,19 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
|
|
|
593
601
|
const name = "llm-workbuddy";
|
|
594
602
|
/** The model registry required before the provider can register. */
|
|
595
603
|
const inject = ["llm"];
|
|
596
|
-
/**
|
|
597
|
-
|
|
604
|
+
/**
|
|
605
|
+
* Settings namespace owning the configuration card.
|
|
606
|
+
*
|
|
607
|
+
* DSH 0.1.2 dropped the `settingsNamespace()` branding function: a namespace is
|
|
608
|
+
* now a nominal string, validated by the type system where it is used rather
|
|
609
|
+
* than at runtime by a function call. The brand is compile-time only, so this
|
|
610
|
+
* stays the plain string it always was — every comparison, descriptor lookup,
|
|
611
|
+
* and `dsh` config file still sees `'workbuddy'`. It is cast once here so the
|
|
612
|
+
* public constant carries the seam's type without pulling the brand helper
|
|
613
|
+
* into this package (upstream DSH plugins, `dsh-llm-pi-ai` included, pass
|
|
614
|
+
* their namespaces as plain string literals).
|
|
615
|
+
*/
|
|
616
|
+
const WORKBUDDY_SETTINGS_NS = "workbuddy";
|
|
598
617
|
const Config = z.object({ authFile: z.string().description("WorkBuddy desktop auth file (defaults to the app's own location)") });
|
|
599
618
|
/**
|
|
600
619
|
* Start the loopback endpoint, register the `workbuddy` provider, and
|
|
@@ -621,14 +640,16 @@ function apply(ctx, config) {
|
|
|
621
640
|
models: () => catalog.current()
|
|
622
641
|
}));
|
|
623
642
|
let current = () => config;
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
643
|
+
ctx.inject(["settings"], (settingsCtx) => {
|
|
644
|
+
settingsCtx.settings.installSection(ctx, WORKBUDDY_SETTINGS_NS, Config, config, {
|
|
645
|
+
setSource(source) {
|
|
646
|
+
current = source;
|
|
647
|
+
},
|
|
648
|
+
onChange() {
|
|
649
|
+
const next = current().authFile;
|
|
650
|
+
store.setDesktopPath(next);
|
|
651
|
+
}
|
|
652
|
+
});
|
|
632
653
|
});
|
|
633
654
|
let stopped = false;
|
|
634
655
|
ctx.effect(() => () => {
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dsh-workbuddy-connect",
|
|
3
3
|
"displayName": "DSH WorkBuddy Connect",
|
|
4
4
|
"description": "将 WorkBuddy 桌面 App 包含的模型自动接入 DeepSeek Harness — bring WorkBuddy desktop app models into DeepSeek Harness with zero configuration.",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.3.1",
|
|
6
6
|
"author": "corrinehu",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"dsh-plugin",
|
|
@@ -56,8 +56,7 @@
|
|
|
56
56
|
},
|
|
57
57
|
"client": {
|
|
58
58
|
"inject": [
|
|
59
|
-
"@deepseek-ai/dsh-client-
|
|
60
|
-
"@deepseek-ai/dsh-client-ui-slots",
|
|
59
|
+
"@deepseek-ai/dsh-client-ui-renderer",
|
|
61
60
|
"@deepseek-ai/dsh-client-ui-settings-plugins",
|
|
62
61
|
"@deepseek-ai/dsh-client-locale"
|
|
63
62
|
],
|
|
@@ -65,39 +64,39 @@
|
|
|
65
64
|
}
|
|
66
65
|
},
|
|
67
66
|
"peerDependencies": {
|
|
68
|
-
"@earendil-works/pi-ai": "0.
|
|
69
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
70
|
-
"@deepseek-ai/dsh-atomic-write": "^0.1.
|
|
71
|
-
"@deepseek-ai/dsh-attachment": "^0.1.
|
|
72
|
-
"@deepseek-ai/dsh-home-paths": "^0.1.
|
|
73
|
-
"@deepseek-ai/dsh-host-webserver": "^0.1.
|
|
74
|
-
"@deepseek-ai/dsh-llm": "^0.1.
|
|
75
|
-
"@deepseek-ai/dsh-llm-pi-ai": "^0.1.
|
|
76
|
-
"@deepseek-ai/dsh-settings": "^0.1.
|
|
77
|
-
"@deepseek-ai/schemastery": "^3.18.
|
|
67
|
+
"@earendil-works/pi-ai": "^0.84.2",
|
|
68
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
69
|
+
"@deepseek-ai/dsh-atomic-write": "^0.1.2-rc.1",
|
|
70
|
+
"@deepseek-ai/dsh-attachment": "^0.1.2-rc.1",
|
|
71
|
+
"@deepseek-ai/dsh-home-paths": "^0.1.2-rc.1",
|
|
72
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.2-rc.1",
|
|
73
|
+
"@deepseek-ai/dsh-llm": "^0.1.2-rc.1",
|
|
74
|
+
"@deepseek-ai/dsh-llm-pi-ai": "^0.1.2-rc.1",
|
|
75
|
+
"@deepseek-ai/dsh-settings": "^0.1.2-rc.1",
|
|
76
|
+
"@deepseek-ai/schemastery": "^3.18.2",
|
|
78
77
|
"react": "^18.2.0"
|
|
79
78
|
},
|
|
80
79
|
"devDependencies": {
|
|
81
|
-
"@earendil-works/pi-ai": "0.
|
|
82
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
83
|
-
"@deepseek-ai/dsh-atomic-write": "^0.1.
|
|
84
|
-
"@deepseek-ai/dsh-attachment": "^0.1.
|
|
85
|
-
"@deepseek-ai/dsh-client-locale": "^0.1.
|
|
86
|
-
"@deepseek-ai/dsh-client-
|
|
87
|
-
"@deepseek-ai/dsh-client-ui-
|
|
88
|
-
"@deepseek-ai/dsh-
|
|
89
|
-
"@deepseek-ai/dsh-
|
|
90
|
-
"@deepseek-ai/dsh-
|
|
91
|
-
"@deepseek-ai/dsh-
|
|
92
|
-
"@deepseek-ai/dsh-llm": "^0.1.
|
|
93
|
-
"@deepseek-ai/dsh-
|
|
94
|
-
"@deepseek-ai/
|
|
95
|
-
"@deepseek-ai/schemastery": "3.18.1",
|
|
80
|
+
"@earendil-works/pi-ai": "^0.84.2",
|
|
81
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
82
|
+
"@deepseek-ai/dsh-atomic-write": "^0.1.2-rc.1",
|
|
83
|
+
"@deepseek-ai/dsh-attachment": "^0.1.2-rc.1",
|
|
84
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.2-rc.1",
|
|
85
|
+
"@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.2-rc.1",
|
|
86
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.2-rc.1",
|
|
87
|
+
"@deepseek-ai/dsh-credentials": "^0.1.2-rc.1",
|
|
88
|
+
"@deepseek-ai/dsh-home-paths": "^0.1.2-rc.1",
|
|
89
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.2-rc.1",
|
|
90
|
+
"@deepseek-ai/dsh-llm": "^0.1.2-rc.1",
|
|
91
|
+
"@deepseek-ai/dsh-llm-pi-ai": "^0.1.2-rc.1",
|
|
92
|
+
"@deepseek-ai/dsh-settings": "^0.1.2-rc.1",
|
|
93
|
+
"@deepseek-ai/schemastery": "^3.18.2",
|
|
96
94
|
"@types/node": "^22.20.0",
|
|
97
95
|
"@types/react": "~18.3.1",
|
|
98
96
|
"react": "^18.2.0",
|
|
99
97
|
"tsdown": "^0.22.2",
|
|
100
98
|
"typescript": "^5.9.3",
|
|
101
|
-
"vitest": "^4.1.8"
|
|
99
|
+
"vitest": "^4.1.8",
|
|
100
|
+
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.2-rc.1"
|
|
102
101
|
}
|
|
103
|
-
}
|
|
102
|
+
}
|