dsh-model 0.0.1 → 0.15.0
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 +127 -0
- package/README.md +96 -43
- package/bin/dsh-model.js +8 -0
- package/dist/bridge/aggregate.js +91 -0
- package/dist/bridge/login.js +127 -0
- package/dist/bridge/runtime.js +136 -0
- package/dist/bridge/server.js +183 -0
- package/dist/bridge/workbuddy/_compat.js +52 -0
- package/dist/bridge/workbuddy/app-version.js +141 -0
- package/dist/bridge/workbuddy/auth.js +548 -0
- package/dist/bridge/workbuddy/catalog.js +154 -0
- package/dist/bridge/workbuddy/client-identity.js +234 -0
- package/dist/bridge/workbuddy/desktop-credential-protection.js +992 -0
- package/dist/bridge/workbuddy/loopback.js +52 -0
- package/dist/bridge/workbuddy/probe.js +153 -0
- package/dist/bridge/workbuddy/status-paths.js +49 -0
- package/dist/bridge/workbuddy/upstream.js +1180 -0
- package/dist/bridge/workbuddy/variants.js +101 -0
- package/dist/cli.js +215 -0
- package/dist/commands/bridge.js +68 -0
- package/dist/commands/connect.js +67 -0
- package/dist/commands/engine.js +122 -0
- package/dist/commands/key.js +75 -0
- package/dist/commands/login.js +113 -0
- package/dist/commands/logs.js +19 -0
- package/dist/commands/opencode.js +83 -0
- package/dist/commands/remote.js +129 -0
- package/dist/commands/self.js +116 -0
- package/dist/commands/service.js +72 -0
- package/dist/commands/setup.js +219 -0
- package/dist/commands/sources.js +196 -0
- package/dist/commands/status.js +216 -0
- package/dist/commands/uninstall.js +97 -0
- package/dist/commands/workbuddy.js +117 -0
- package/dist/context.js +86 -0
- package/dist/daemon/client.js +27 -0
- package/dist/daemon/core.js +850 -0
- package/dist/daemon/service.js +33 -0
- package/dist/daemon/stats.js +126 -0
- package/dist/daemon/usage.js +213 -0
- package/dist/dsh/cli.js +90 -0
- package/dist/dsh/connect.js +257 -0
- package/dist/dsh/credentials.js +55 -0
- package/dist/dsh/locate.js +129 -0
- package/dist/dsh/patch.js +106 -0
- package/dist/dsh/pick.js +59 -0
- package/dist/engine/auth.js +40 -0
- package/dist/engine/client.js +104 -0
- package/dist/engine/compat.js +94 -0
- package/dist/engine/config.js +47 -0
- package/dist/engine/install.js +118 -0
- package/dist/engine/mgmt.js +99 -0
- package/dist/engine/sourcemap.js +48 -0
- package/dist/errors.js +14 -0
- package/dist/i18n.js +39 -0
- package/dist/integrations/dshplugin.js +89 -0
- package/dist/integrations/opencode.js +101 -0
- package/dist/integrations/workbuddy.js +114 -0
- package/dist/keys.js +54 -0
- package/dist/ops.js +178 -0
- package/dist/remote/public.js +109 -0
- package/dist/secrets.js +13 -0
- package/dist/service/index.js +48 -0
- package/dist/service/launchd.js +89 -0
- package/dist/service/systemd.js +83 -0
- package/dist/sources.js +36 -0
- package/dist/state.js +90 -0
- package/dist/upstreams.js +23 -0
- package/dist/util/diff.js +30 -0
- package/dist/util/exec.js +42 -0
- package/dist/util/fs.js +107 -0
- package/dist/util/output.js +50 -0
- package/dist/util/pkg.js +13 -0
- package/dist/util/port.js +16 -0
- package/dist/util/prompt.js +40 -0
- package/dist/util/proxy.js +73 -0
- package/dist/util/redact.js +7 -0
- package/engine-manifest.json +28 -0
- package/package.json +77 -7
- package/plugin/client.js +688 -0
- package/plugin/cordis.patch.yml +4 -0
- package/plugin/index.js +231 -0
- package/docs/DESIGN.md +0 -610
package/README.en.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# dsh-model
|
|
4
|
+
|
|
5
|
+
**Many model sources behind one OpenAI-compatible endpoint — for dsh and every other tool you use.**
|
|
6
|
+
|
|
7
|
+
<p>
|
|
8
|
+
<a href="LICENSE"><img src="https://img.shields.io/github/license/AIcivilization/dsh-model" alt="MIT license"></a>
|
|
9
|
+
<a href="https://www.npmjs.com/package/dsh-model"><img src="https://img.shields.io/npm/v/dsh-model?color=cb3837&logo=npm" alt="npm version"></a>
|
|
10
|
+
<img src="https://img.shields.io/badge/DeepSeek%20Harness-%E2%89%A5%200.2.0--rc.2-4176E6" alt="DeepSeek Harness ≥ 0.2.0-rc.2">
|
|
11
|
+
<img src="https://img.shields.io/badge/platform-macOS%20%7C%20Linux-blue" alt="Platform: macOS / Linux">
|
|
12
|
+
<img src="https://img.shields.io/badge/node-%E2%89%A5%2020-339933?logo=node.js&logoColor=white" alt="Node ≥ 20">
|
|
13
|
+
<a href="https://github.com/router-for-me/CLIProxyAPI"><img src="https://img.shields.io/badge/engine-CLIProxyAPI%208.0.13-555" alt="Engine: CLIProxyAPI 8.0.13"></a>
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
English · [中文](README.md)
|
|
17
|
+
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
## What it is
|
|
21
|
+
|
|
22
|
+
You may have several model sources at once: free WorkBuddy credits, an OpenCode Zen key, paid Codex or Kimi subscriptions. Each has its own API and sign-in, and every tool needs its own setup.
|
|
23
|
+
|
|
24
|
+
dsh-model turns them into **one address and one kind of key**:
|
|
25
|
+
|
|
26
|
+
- **In dsh**: the models show up in dsh's model list automatically, grouped by source. Nothing to configure.
|
|
27
|
+
- **Everywhere else**: any editor or script that speaks the OpenAI API uses the same address and key to reach every model.
|
|
28
|
+
- **Low maintenance**: one command to install; a key is always required; uninstall restores dsh's config exactly.
|
|
29
|
+
- **On a server**: runs next to [dsh-vps](https://github.com/AIcivilization/dsh-vps) on the same VPS and opens a public address on dsh's own domain, so your laptop and phone can use it too.
|
|
30
|
+
|
|
31
|
+
> A personal tool: one person, one account per provider, not a public service.
|
|
32
|
+
|
|
33
|
+
## Quick start
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install -g dsh-model
|
|
37
|
+
dsh-model setup
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
On a dsh-vps server, use `sudo dsh-model setup`.
|
|
41
|
+
|
|
42
|
+
`setup` is safe to re-run. It:
|
|
43
|
+
|
|
44
|
+
1. downloads and verifies the engine and runs it on `127.0.0.1:8317/v1`, with key auth enforced;
|
|
45
|
+
2. connects OpenCode Zen (enter your key; it is tested before being saved) and WorkBuddy (dsh-model's own bridge);
|
|
46
|
+
3. installs the management page into dsh and syncs the models into dsh;
|
|
47
|
+
4. on a VPS, also opens a public address `https://<dsh's domain>:9443/v1` (or `<IP>.sslip.io` when there is no domain).
|
|
48
|
+
|
|
49
|
+
## Manage it in dsh: Settings → dsh-model
|
|
50
|
+
|
|
51
|
+
| Section | What it holds |
|
|
52
|
+
|---|---|
|
|
53
|
+
| **Endpoint** | Local and public addresses, each with a Copy button |
|
|
54
|
+
| **Sources** | One switch per provider: on connects (and signs in if needed), off pauses and keeps the sign-in. Each row is tagged Free / Free · limited / Paid and shows plan usage or remaining credits |
|
|
55
|
+
| **Models** | Pick which models of each source appear in dsh (5 suggested per source by default); the rest stay available through the endpoint |
|
|
56
|
+
| **API keys** | OpenAI-style, one per device. Requests, success rate, latency and speed over 24 hours; copy, rotate, revoke |
|
|
57
|
+
| **Model stats** | Success rate, latency and speed per model |
|
|
58
|
+
| **About** | Current and latest npm version; one-click Update and Uninstall (on a VPS these need root, so the commands are shown instead) |
|
|
59
|
+
|
|
60
|
+
If the service is not set up on this machine yet, the page says what is missing and offers a "Set up now" button, so you can also start by installing dsh-model from the dsh plugin marketplace and finish from this page.
|
|
61
|
+
|
|
62
|
+
dsh-model does every sign-in itself. No provider CLI is installed and the server needs no browser:
|
|
63
|
+
|
|
64
|
+
- **WorkBuddy, Codex, Kimi, Grok, Muse**: open the link and approve; if a code is needed, the dialog shows it;
|
|
65
|
+
- **Devin, Claude, Antigravity**: these send the browser to a fixed local address that will not load. That is expected: bring the address back and press "Paste from clipboard".
|
|
66
|
+
|
|
67
|
+
If a source is signed in but has no usable plan (say, a Kimi account without Kimi Code), dsh-model detects it, hides its models in dsh and shows where to subscribe.
|
|
68
|
+
|
|
69
|
+
> After updating dsh-model, restart dsh once so the management page picks up the new code. Model and switch changes need no restart.
|
|
70
|
+
|
|
71
|
+
## Sources and cost
|
|
72
|
+
|
|
73
|
+
| Source | Cost | Notes |
|
|
74
|
+
|---|---|---|
|
|
75
|
+
| WorkBuddy / WorkBuddy AI | Free | Credits come with the account |
|
|
76
|
+
| Codex | Paid | ChatGPT Plus / Pro |
|
|
77
|
+
| Kimi | Paid | Kimi Code plan |
|
|
78
|
+
| Grok | Paid | SuperGrok / X Premium+ |
|
|
79
|
+
| Muse | Paid | Muse Code plan |
|
|
80
|
+
| Devin | Paid | The free tier is refused in testing |
|
|
81
|
+
| OpenCode Zen | Paid | Pay as you go after a top-up; its free models only work inside the OpenCode app |
|
|
82
|
+
| Claude, Antigravity | High risk, hidden by default | The providers have banned third-party use before |
|
|
83
|
+
|
|
84
|
+
dsh-model only connects; it provides no quota of its own.
|
|
85
|
+
|
|
86
|
+
## Commands
|
|
87
|
+
|
|
88
|
+
| Command | What it does |
|
|
89
|
+
|---|---|
|
|
90
|
+
| `setup` | Install / repair everything (idempotent) |
|
|
91
|
+
| `sources` | Sources: switch, cost, sign-in state, usage |
|
|
92
|
+
| `source enable\|disable\|logout <source>` | Turn on (signing in if needed) / off / sign out |
|
|
93
|
+
| `key list\|add\|revoke\|rotate <name>` | Access keys, one per device |
|
|
94
|
+
| `stats` | Success rate, latency and speed by key, source and model |
|
|
95
|
+
| `status` / `doctor [--e2e]` | Overview / health checks |
|
|
96
|
+
| `models [sync]` | List models / sync them to dsh |
|
|
97
|
+
| `remote enable --via caddy\|ssh\|tailscale` / `remote disable` | Remote access (on a VPS, setup already enables caddy) |
|
|
98
|
+
| `engine version\|upgrade\|rollback` | Engine version |
|
|
99
|
+
| `logs [--bridge]` / `repair` | Logs / repair |
|
|
100
|
+
| `update [--to ver]` | Update to the latest npm version: program, dsh plugin and services together |
|
|
101
|
+
| `uninstall` | Clean uninstall |
|
|
102
|
+
|
|
103
|
+
Every command accepts `--lang zh|en` and `--json`.
|
|
104
|
+
|
|
105
|
+
## Architecture
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
dsh · editors · scripts ──Bearer key──▶ engine 127.0.0.1:8317/v1 (the only entry)
|
|
109
|
+
│ ▲ VPS: Caddy https://<dsh domain>:9443
|
|
110
|
+
┌────────────────────────────┼─────────────────────────┐
|
|
111
|
+
▼ ▼ ▼
|
|
112
|
+
subscription OAuth (Codex, Kimi…) opencode/* (your key) workbuddy/* → dsh-model's bridge
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
- The engine is [CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI) (MIT), version-pinned and sha256-verified. The WorkBuddy protocol layer is ported from [dsh-workbuddy-connect](https://github.com/corrinehu/dsh-workbuddy-connect) (MIT); the usage endpoints follow [CodexBar](https://github.com/steipete/CodexBar) (MIT).
|
|
116
|
+
- dsh-model writes only two things into dsh: the model providers and `refs.DSH_MODEL_API_KEY`. On uninstall they are restored byte for byte if nobody changed them.
|
|
117
|
+
|
|
118
|
+
See the [design doc](docs/DESIGN.md) (Chinese).
|
|
119
|
+
|
|
120
|
+
## Notes
|
|
121
|
+
|
|
122
|
+
- Using subscription and WorkBuddy credentials outside the official clients may break the providers' terms and put the account at risk. Decide for yourself.
|
|
123
|
+
- macOS and Linux, Node ≥ 20.
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
|
|
127
|
+
MIT
|
package/README.md
CHANGED
|
@@ -1,73 +1,126 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
1
3
|
# dsh-model
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
**把多家模型汇成一个 OpenAI 兼容地址,dsh 和你的其他软件都能直接用。**
|
|
6
|
+
|
|
7
|
+
<p>
|
|
8
|
+
<a href="LICENSE"><img src="https://img.shields.io/github/license/AIcivilization/dsh-model" alt="MIT license"></a>
|
|
9
|
+
<a href="https://www.npmjs.com/package/dsh-model"><img src="https://img.shields.io/npm/v/dsh-model?color=cb3837&logo=npm" alt="npm version"></a>
|
|
10
|
+
<img src="https://img.shields.io/badge/DeepSeek%20Harness-%E2%89%A5%200.2.0--rc.2-4176E6" alt="DeepSeek Harness ≥ 0.2.0-rc.2">
|
|
11
|
+
<img src="https://img.shields.io/badge/platform-macOS%20%7C%20Linux-blue" alt="Platform: macOS / Linux">
|
|
12
|
+
<img src="https://img.shields.io/badge/node-%E2%89%A5%2020-339933?logo=node.js&logoColor=white" alt="Node ≥ 20">
|
|
13
|
+
<a href="https://github.com/router-for-me/CLIProxyAPI"><img src="https://img.shields.io/badge/engine-CLIProxyAPI%208.0.13-555" alt="Engine: CLIProxyAPI 8.0.13"></a>
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
[English](README.en.md) · 中文
|
|
17
|
+
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
## 它是什么
|
|
21
|
+
|
|
22
|
+
你可能同时有好几个模型来源:WorkBuddy 的免费积分、OpenCode Zen 的 key,还有已经付费的 Codex、Kimi 订阅。它们的接口和登录方式各不相同,每个软件都要分别配置。
|
|
4
23
|
|
|
5
|
-
|
|
24
|
+
dsh-model 把它们合成**一个地址、一种 key**:
|
|
6
25
|
|
|
7
|
-
|
|
26
|
+
- **dsh 里**:装好后自动出现在 dsh 的模型列表里,按来源分组,不用手动配置。
|
|
27
|
+
- **其他软件**:编辑器、脚本,凡是支持 OpenAI 接口的,填上同一个地址和 key 就能用全部模型。
|
|
28
|
+
- **省心**:一条命令装好;始终要求 key,不会出现谁都能用的情况;卸载时把 dsh 的配置原样还原。
|
|
29
|
+
- **服务器上**:可以和 [dsh-vps](https://github.com/AIcivilization/dsh-vps) 装在同一台 VPS 上,自动用 dsh 的域名开一个对外地址,你的电脑、手机都能访问。
|
|
8
30
|
|
|
9
|
-
|
|
10
|
-
- 想在编辑器插件、脚本、其他 Agent 框架里用订阅模型,到处买 API key;
|
|
11
|
-
- 国内连不上国际模型:海外 VPS 部署 dsh-model 作中继,国内直连 VPS 即可;
|
|
12
|
-
- 一个 key 都没有:内置免费模型池(DeepSeek V4 Flash、MiMo、Hy3、Muse Spark 等),开箱即聊。
|
|
31
|
+
> 个人自用工具:一个人、每家一个账号,不对外提供服务。
|
|
13
32
|
|
|
14
33
|
## 快速开始
|
|
15
34
|
|
|
16
35
|
```bash
|
|
17
36
|
npm install -g dsh-model
|
|
18
|
-
dsh-model setup
|
|
37
|
+
dsh-model setup
|
|
19
38
|
```
|
|
20
39
|
|
|
21
|
-
|
|
40
|
+
服务器上(装了 dsh-vps 的 VPS)用 `sudo dsh-model setup`。
|
|
22
41
|
|
|
23
|
-
|
|
24
|
-
dsh-model login codex # device-code 登录,按提示在浏览器完成
|
|
25
|
-
```
|
|
42
|
+
`setup` 可以重复执行,它会依次:
|
|
26
43
|
|
|
27
|
-
|
|
44
|
+
1. 下载并校验引擎,在本机 `127.0.0.1:8317/v1` 上运行,强制 key 鉴权;
|
|
45
|
+
2. 接入 OpenCode Zen(输入你的 key,先实测再保存)和 WorkBuddy(dsh-model 自己的 bridge);
|
|
46
|
+
3. 把管理页装进 dsh,把模型同步进 dsh;
|
|
47
|
+
4. 在 VPS 上,额外开通对外地址 `https://<dsh 的域名>:9443/v1`(没有域名时用 `<IP>.sslip.io`)。
|
|
28
48
|
|
|
29
|
-
|
|
30
|
-
# 在海外 VPS 上
|
|
31
|
-
dsh-model setup && dsh-model relay enable
|
|
32
|
-
# 国内任何软件填 https://<vps>/v1 + token
|
|
33
|
-
```
|
|
49
|
+
## 在 dsh 里管理:设置 → dsh-model
|
|
34
50
|
|
|
35
|
-
|
|
51
|
+
| 区块 | 内容 |
|
|
52
|
+
|---|---|
|
|
53
|
+
| **访问地址** | 本机地址和外网地址,各带「复制」按钮 |
|
|
54
|
+
| **来源** | 每家一个开关:打开就接入(没登录会弹出登录框),关闭只停用、保留登录。每行标出「免费 / 免费·额度少 / 需付费」,下面显示套餐用量或剩余积分 |
|
|
55
|
+
| **选模型** | 每个来源选哪些模型显示在 dsh 里,默认每家推荐 5 个,其余的经统一地址照样可用 |
|
|
56
|
+
| **API key** | OpenAI 格式,每台设备一把。显示 24 小时内的请求数、成功率、延迟和速度,可复制、轮换、吊销 |
|
|
57
|
+
| **模型统计** | 按模型显示成功率、延迟和速度 |
|
|
58
|
+
| **关于** | 当前版本和 npm 上的最新版本;「更新」「卸载」一键完成(VPS 上要 root,这里给出命令) |
|
|
36
59
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
60
|
+
还没在这台机器上装好服务时,这一页会说明缺什么,并提供「一键安装」按钮。所以也可以先从 dsh 的插件市场装 dsh-model,再在这一页里点安装。
|
|
61
|
+
|
|
62
|
+
登录全由 dsh-model 自己完成,服务器上不装各家的 CLI,也不需要浏览器:
|
|
63
|
+
|
|
64
|
+
- **WorkBuddy、Codex、Kimi、Grok、Muse**:打开链接授权即可,要验证码的弹窗里会给出;
|
|
65
|
+
- **Devin、Claude、Antigravity**:这几家会把浏览器跳到一个固定的本机地址,页面打不开是正常的。把地址栏里的地址带回来,点「从剪贴板粘贴」即可。
|
|
66
|
+
|
|
67
|
+
已登录但没有可用套餐的来源(比如 Kimi 账号没有开通 Kimi Code),dsh-model 会实测出来,把它的模型从 dsh 里隐藏,并给出开通链接。
|
|
68
|
+
|
|
69
|
+
> 更新 dsh-model 后,要重启一次 dsh,管理页的新代码才会生效。模型和开关的变化不需要重启。
|
|
46
70
|
|
|
47
|
-
|
|
48
|
-
- dsh-model 自身只做安装编排、dsh 接线(`connect-dsh` / 干净卸载)、已装探测、海外中继产品化、免费模型池;
|
|
49
|
-
- 不自建界面——模型选择与使用全部发生在 dsh 的 Web UI(VPS 场景配合 [dsh-vps](https://github.com/AIcivilization/dsh-vps))。
|
|
71
|
+
## 来源与费用
|
|
50
72
|
|
|
51
|
-
|
|
73
|
+
| 来源 | 费用 | 说明 |
|
|
74
|
+
|---|---|---|
|
|
75
|
+
| WorkBuddy / WorkBuddy AI | 免费 | 账号自带积分 |
|
|
76
|
+
| Codex | 需付费 | ChatGPT Plus / Pro |
|
|
77
|
+
| Kimi | 需付费 | Kimi Code 套餐 |
|
|
78
|
+
| Grok | 需付费 | SuperGrok / X Premium+ |
|
|
79
|
+
| Muse | 需付费 | Muse Code 套餐 |
|
|
80
|
+
| Devin | 需付费 | 免费档实测调用会被拒 |
|
|
81
|
+
| OpenCode Zen | 需付费 | 充值后按量计费;它的免费模型只能在 OpenCode 软件里用 |
|
|
82
|
+
| Claude、Antigravity | 高风险,默认隐藏 | 服务商有封禁第三方使用的先例 |
|
|
52
83
|
|
|
53
|
-
|
|
84
|
+
dsh-model 只负责转接,不提供额度。
|
|
85
|
+
|
|
86
|
+
## 常用命令
|
|
54
87
|
|
|
55
88
|
| 命令 | 作用 |
|
|
56
89
|
|---|---|
|
|
57
|
-
| `
|
|
58
|
-
| `
|
|
59
|
-
| `
|
|
60
|
-
| `
|
|
61
|
-
| `
|
|
62
|
-
| `
|
|
90
|
+
| `setup` | 安装 / 修复全部(可重复执行) |
|
|
91
|
+
| `sources` | 来源列表:开关、费用、登录状态、用量 |
|
|
92
|
+
| `source enable\|disable\|logout <来源>` | 打开(需要时登录)/ 关闭 / 退出登录 |
|
|
93
|
+
| `key list\|add\|revoke\|rotate <名称>` | 访问 key,每台设备一把 |
|
|
94
|
+
| `stats` | 按 key、来源、模型的成功率、延迟和速度 |
|
|
95
|
+
| `status` / `doctor [--e2e]` | 总览 / 逐项自检 |
|
|
96
|
+
| `models [sync]` | 列出模型 / 同步到 dsh |
|
|
97
|
+
| `remote enable --via caddy\|ssh\|tailscale` / `remote disable` | 远程访问(VPS 上 setup 已自动开通 caddy) |
|
|
98
|
+
| `engine version\|upgrade\|rollback` | 引擎版本 |
|
|
99
|
+
| `logs [--bridge]` / `repair` | 日志 / 修复 |
|
|
100
|
+
| `update [--to 版本]` | 更新到 npm 上的最新版:程序、dsh 插件和服务一起更新 |
|
|
101
|
+
| `uninstall` | 干净卸载 |
|
|
102
|
+
|
|
103
|
+
所有命令都支持 `--lang zh|en` 和 `--json`。
|
|
104
|
+
|
|
105
|
+
## 架构
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
dsh · 编辑器 · 脚本 ──Bearer key──▶ 引擎 127.0.0.1:8317/v1(唯一入口)
|
|
109
|
+
│ ▲ VPS:Caddy https://<dsh 域名>:9443
|
|
110
|
+
┌────────────────────────┼────────────────────────┐
|
|
111
|
+
▼ ▼ ▼
|
|
112
|
+
订阅 OAuth(Codex、Kimi…) opencode/*(你的 key) workbuddy/* → dsh-model 的 bridge
|
|
113
|
+
```
|
|
63
114
|
|
|
64
|
-
|
|
115
|
+
- 引擎是 [CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI)(MIT),版本锁定并校验 sha256。WorkBuddy 协议层移植自 [dsh-workbuddy-connect](https://github.com/corrinehu/dsh-workbuddy-connect)(MIT),订阅用量的查询接口参考了 [CodexBar](https://github.com/steipete/CodexBar)(MIT)。
|
|
116
|
+
- dsh 里只写两样东西:模型 provider 和 `refs.DSH_MODEL_API_KEY`。卸载时,如果这两处没被改过,就逐字节还原。
|
|
65
117
|
|
|
66
|
-
|
|
118
|
+
详见 [设计文档](docs/DESIGN.md)。
|
|
67
119
|
|
|
68
|
-
##
|
|
120
|
+
## 注意
|
|
69
121
|
|
|
70
|
-
|
|
122
|
+
- 订阅和 WorkBuddy 的凭据在官方客户端之外使用,可能不符合服务商的条款,账号有风险,请自行判断。
|
|
123
|
+
- 支持 macOS 和 Linux,需要 Node ≥ 20。
|
|
71
124
|
|
|
72
125
|
## License
|
|
73
126
|
|
package/bin/dsh-model.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// bridge/aggregate.ts — 把 OpenAI chat.completion.chunk 的 SSE 流拼成一个非流式 chat.completion
|
|
2
|
+
//
|
|
3
|
+
// WorkBuddy 上游只给流式(prepareChatBody 强制 stream:true),而引擎对非流式客户端会发 stream:false,
|
|
4
|
+
// 期待一个完整的 JSON。这里按 OpenAI 的增量规则累积:content / reasoning_content 拼接,
|
|
5
|
+
// tool_calls 按 index 合并 id、name 与 arguments 片段,finish_reason 与 usage 取最后出现的值。
|
|
6
|
+
export function aggregateSse(text) {
|
|
7
|
+
let id = '';
|
|
8
|
+
let model = '';
|
|
9
|
+
let created = Math.floor(Date.now() / 1000);
|
|
10
|
+
let usage;
|
|
11
|
+
const choices = new Map();
|
|
12
|
+
for (const rawLine of text.split('\n')) {
|
|
13
|
+
const line = rawLine.trim();
|
|
14
|
+
if (!line.startsWith('data:'))
|
|
15
|
+
continue;
|
|
16
|
+
const data = line.slice(5).trim();
|
|
17
|
+
if (!data || data === '[DONE]')
|
|
18
|
+
continue;
|
|
19
|
+
let chunk;
|
|
20
|
+
try {
|
|
21
|
+
chunk = JSON.parse(data);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (typeof chunk.id === 'string' && chunk.id)
|
|
27
|
+
id = chunk.id;
|
|
28
|
+
if (typeof chunk.model === 'string' && chunk.model)
|
|
29
|
+
model = chunk.model;
|
|
30
|
+
if (typeof chunk.created === 'number')
|
|
31
|
+
created = chunk.created;
|
|
32
|
+
if (chunk.usage && typeof chunk.usage === 'object')
|
|
33
|
+
usage = chunk.usage;
|
|
34
|
+
for (const c of (Array.isArray(chunk.choices) ? chunk.choices : [])) {
|
|
35
|
+
const index = typeof c.index === 'number' ? c.index : 0;
|
|
36
|
+
let acc = choices.get(index);
|
|
37
|
+
if (!acc) {
|
|
38
|
+
acc = { index, content: '', reasoning: '', toolCalls: new Map(), finishReason: null, role: 'assistant' };
|
|
39
|
+
choices.set(index, acc);
|
|
40
|
+
}
|
|
41
|
+
const delta = (c.delta ?? c.message ?? {});
|
|
42
|
+
if (typeof delta.role === 'string')
|
|
43
|
+
acc.role = delta.role;
|
|
44
|
+
if (typeof delta.content === 'string')
|
|
45
|
+
acc.content += delta.content;
|
|
46
|
+
const reasoning = delta.reasoning_content ?? delta.reasoning;
|
|
47
|
+
if (typeof reasoning === 'string')
|
|
48
|
+
acc.reasoning += reasoning;
|
|
49
|
+
for (const tc of (Array.isArray(delta.tool_calls) ? delta.tool_calls : [])) {
|
|
50
|
+
const ti = typeof tc.index === 'number' ? tc.index : acc.toolCalls.size;
|
|
51
|
+
let t = acc.toolCalls.get(ti);
|
|
52
|
+
if (!t) {
|
|
53
|
+
t = { type: 'function', function: { name: '', arguments: '' } };
|
|
54
|
+
acc.toolCalls.set(ti, t);
|
|
55
|
+
}
|
|
56
|
+
if (typeof tc.id === 'string' && tc.id)
|
|
57
|
+
t.id = tc.id;
|
|
58
|
+
const fn = (tc.function ?? {});
|
|
59
|
+
if (typeof fn.name === 'string')
|
|
60
|
+
t.function.name += fn.name;
|
|
61
|
+
if (typeof fn.arguments === 'string')
|
|
62
|
+
t.function.arguments += fn.arguments;
|
|
63
|
+
}
|
|
64
|
+
if (typeof c.finish_reason === 'string' && c.finish_reason)
|
|
65
|
+
acc.finishReason = c.finish_reason;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const ordered = [...choices.values()].sort((a, b) => a.index - b.index);
|
|
69
|
+
if (!ordered.length)
|
|
70
|
+
ordered.push({ index: 0, content: '', reasoning: '', toolCalls: new Map(), finishReason: 'stop', role: 'assistant' });
|
|
71
|
+
return {
|
|
72
|
+
id: id || `chatcmpl-bridge-${Date.now()}`,
|
|
73
|
+
object: 'chat.completion',
|
|
74
|
+
created,
|
|
75
|
+
model,
|
|
76
|
+
choices: ordered.map((c) => {
|
|
77
|
+
const tools = [...c.toolCalls.entries()].sort(([a], [b]) => a - b).map(([i, t]) => ({ ...t, id: t.id ?? `call_${i}` }));
|
|
78
|
+
return {
|
|
79
|
+
index: c.index,
|
|
80
|
+
message: {
|
|
81
|
+
role: c.role,
|
|
82
|
+
content: c.content || (tools.length ? null : ''),
|
|
83
|
+
...(c.reasoning ? { reasoning_content: c.reasoning } : {}),
|
|
84
|
+
...(tools.length ? { tool_calls: tools } : {}),
|
|
85
|
+
},
|
|
86
|
+
finish_reason: c.finishReason ?? (tools.length ? 'tool_calls' : 'stop'),
|
|
87
|
+
};
|
|
88
|
+
}),
|
|
89
|
+
...(usage ? { usage } : {}),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// bridge/login.ts — dsh-model 自己完成 WorkBuddy 登录(不需要桌面 App,也不需要 CodeBuddy CLI)
|
|
2
|
+
//
|
|
3
|
+
// 流程与 WorkBuddy 内置 CLI 的 cli-external-link 相同(读其 product.json 与登录源码确认,2026-10-08):
|
|
4
|
+
// POST {base}/v2/plugin/auth/state?platform=<workbuddy|workbuddy-ai> → { state, authUrl }
|
|
5
|
+
// 用户在任意设备的浏览器打开 authUrl 并授权(服务器上不需要浏览器,也没有本地回调)
|
|
6
|
+
// 轮询 GET {base}/v2/plugin/auth/token?state=… → 令牌
|
|
7
|
+
// GET {base}/v2/plugin/login/account?state=…(Bearer) → 账号(uid / 企业 / 昵称)
|
|
8
|
+
// 令牌按移植凭据库的"自有副本"格式写进 $DSH_MODEL_HOME/workbuddy/<ownFilename>(0600),bridge 直接用。
|
|
9
|
+
// 网络请求走 curl:它认 -x 代理(Node 的 fetch 不走代理);国内版域名强制直连。
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { DshModelError } from '../errors.js';
|
|
12
|
+
import { L } from '../i18n.js';
|
|
13
|
+
import { run } from '../util/exec.js';
|
|
14
|
+
import { atomicWrite, ensureDir } from '../util/fs.js';
|
|
15
|
+
import { bridgeDir } from './runtime.js';
|
|
16
|
+
export const LOGIN_ENDPOINTS = {
|
|
17
|
+
workbuddy: { base: 'https://www.workbuddy.cn', platform: 'workbuddy' },
|
|
18
|
+
'workbuddy-ai': { base: 'https://www.workbuddy.ai', platform: 'workbuddy-ai' },
|
|
19
|
+
};
|
|
20
|
+
const NO_AUTH_HEADERS = ['X-No-Authorization: true', 'X-No-User-Id: true', 'X-No-Enterprise-Id: true', 'X-No-Department-Info: true'];
|
|
21
|
+
const POLL_INTERVAL_MS = 2000;
|
|
22
|
+
/** 一次 JSON 请求。headers 经 stdin 的 curl 配置传入,令牌不出现在进程参数里 */
|
|
23
|
+
async function curlJson(method, url, headers, proxy) {
|
|
24
|
+
const direct = /\.workbuddy\.cn$|\.codebuddy\.cn$|\.tencent\.com$|^127\.0\.0\.1$|^localhost$/.test(new URL(url).hostname);
|
|
25
|
+
const cfg = [...headers, 'Content-Type: application/json', 'Accept: application/json'].map((h) => `header = "${h.replace(/"/g, '')}"`).join('\n') + '\n';
|
|
26
|
+
const args = ['-s', '-m', '20', '-w', '\n%{http_code}', '-X', method, '-K', '-', ...(proxy && !direct ? ['-x', proxy] : ['--noproxy', '*']), ...(method === 'POST' ? ['-d', '{}'] : []), url];
|
|
27
|
+
const r = await run('curl', args, { input: cfg, timeoutMs: 30_000 });
|
|
28
|
+
if (r.code !== 0 && !r.stdout) {
|
|
29
|
+
throw new DshModelError('workbuddy_network', L(`连不上 ${new URL(url).host}(curl 退出码 ${r.code})${proxy ? '' : ';海外 / 国际版可能需要代理:dsh-model setup --proxy <地址>'}`, `Cannot reach ${new URL(url).host} (curl exit ${r.code})${proxy ? '' : '; the international site may need a proxy: dsh-model setup --proxy <url>'}`));
|
|
30
|
+
}
|
|
31
|
+
const lines = r.stdout.trimEnd().split('\n');
|
|
32
|
+
const status = Number(lines.pop()) || 0;
|
|
33
|
+
let body = {};
|
|
34
|
+
try {
|
|
35
|
+
body = JSON.parse(lines.join('\n'));
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// 非 JSON(网关错误页等)
|
|
39
|
+
}
|
|
40
|
+
return { status, body };
|
|
41
|
+
}
|
|
42
|
+
const str = (v) => (typeof v === 'string' && v ? v : undefined);
|
|
43
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : undefined);
|
|
44
|
+
/** 秒或毫秒的时间戳统一成毫秒 */
|
|
45
|
+
const toMs = (v) => (v < 1e12 ? v * 1000 : v);
|
|
46
|
+
export async function workbuddyLogin(variant, home, opts) {
|
|
47
|
+
const known = LOGIN_ENDPOINTS[variant.id];
|
|
48
|
+
if (!known)
|
|
49
|
+
throw new DshModelError('workbuddy_unknown_variant', `unknown WorkBuddy product: ${variant.id}`);
|
|
50
|
+
// 测试用:DSH_MODEL_WORKBUDDY_BASE 指向本机假服务
|
|
51
|
+
const ep = process.env.DSH_MODEL_WORKBUDDY_BASE ? { ...known, base: process.env.DSH_MODEL_WORKBUDDY_BASE } : known;
|
|
52
|
+
const st = await curlJson('POST', `${ep.base}/v2/plugin/auth/state?platform=${ep.platform}`, NO_AUTH_HEADERS, opts.proxy);
|
|
53
|
+
const state = str(st.body.data?.state);
|
|
54
|
+
const authUrl = str(st.body.data?.authUrl);
|
|
55
|
+
if (!state || !authUrl) {
|
|
56
|
+
throw new DshModelError('workbuddy_auth_state_failed', L(`${variant.displayName} 没有返回授权链接(HTTP ${st.status}${st.body.msg ? `:${st.body.msg}` : ''})`, `${variant.displayName} returned no authorization link (HTTP ${st.status}${st.body.msg ? `: ${st.body.msg}` : ''})`));
|
|
57
|
+
}
|
|
58
|
+
opts.onUrl(authUrl);
|
|
59
|
+
// 轮询令牌:授权前服务端返回"等待中",授权后返回令牌
|
|
60
|
+
const deadline = Date.now() + (opts.timeoutMs ?? 10 * 60_000);
|
|
61
|
+
let token;
|
|
62
|
+
while (Date.now() < deadline) {
|
|
63
|
+
if (opts.signal?.aborted)
|
|
64
|
+
throw new DshModelError('workbuddy_login_cancelled', L('已取消登录', 'Login cancelled'));
|
|
65
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
66
|
+
let res;
|
|
67
|
+
try {
|
|
68
|
+
res = await curlJson('GET', `${ep.base}/v2/plugin/auth/token?state=${encodeURIComponent(state)}`, NO_AUTH_HEADERS, opts.proxy);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
continue; // 偶发网络抖动:继续等
|
|
72
|
+
}
|
|
73
|
+
if (str(res.body.data?.accessToken)) {
|
|
74
|
+
token = res.body.data;
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (!token)
|
|
79
|
+
throw new DshModelError('workbuddy_login_timeout', L('等待授权超时(10 分钟)', 'Timed out waiting for authorization (10 minutes)'), L('重新执行 dsh-model workbuddy login', 'Run dsh-model workbuddy login again'));
|
|
80
|
+
const accessToken = str(token.accessToken);
|
|
81
|
+
// 账号信息:uid / 企业 / 昵称决定请求头(X-User-Id 等),拿不到就用 X-No-* 头
|
|
82
|
+
let account = {};
|
|
83
|
+
for (let i = 0; i < 5; i++) {
|
|
84
|
+
try {
|
|
85
|
+
const a = await curlJson('GET', `${ep.base}/v2/plugin/login/account?state=${encodeURIComponent(state)}`, [`Authorization: Bearer ${accessToken}`, 'X-No-User-Id: true', 'X-No-Enterprise-Id: true', 'X-No-Department-Info: true'], opts.proxy);
|
|
86
|
+
if (a.body.data && typeof a.body.data === 'object') {
|
|
87
|
+
account = a.body.data;
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// 重试
|
|
93
|
+
}
|
|
94
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
95
|
+
}
|
|
96
|
+
const expiresIn = num(token.expiresIn);
|
|
97
|
+
const expiresAt = num(token.expiresAt);
|
|
98
|
+
const refreshExpiresAt = num(token.refreshExpiresAt);
|
|
99
|
+
const enterpriseId = str(account.enterpriseId) ?? str(token.enterpriseId);
|
|
100
|
+
const nickname = str(account.nickname) ?? str(account.userNickname) ?? str(account.name);
|
|
101
|
+
const credential = {
|
|
102
|
+
accessToken,
|
|
103
|
+
refreshToken: str(token.refreshToken) ?? '',
|
|
104
|
+
expiresAtMs: expiresAt ? toMs(expiresAt) : expiresIn ? Date.now() + expiresIn * 1000 : 0,
|
|
105
|
+
...(refreshExpiresAt ? { refreshExpiresAtMs: toMs(refreshExpiresAt) } : {}),
|
|
106
|
+
// 凭据库按 domain 判断国内 / 国际:令牌没给就用登录站点的域名
|
|
107
|
+
domain: str(token.domain) ?? str(account.domain) ?? new URL(ep.base).hostname,
|
|
108
|
+
uid: str(account.uid) ?? str(account.userId) ?? str(token.uid) ?? '',
|
|
109
|
+
...(enterpriseId ? { enterpriseId } : {}),
|
|
110
|
+
...(nickname ? { nickname } : {}),
|
|
111
|
+
source: 'dsh',
|
|
112
|
+
};
|
|
113
|
+
const dir = bridgeDir(home);
|
|
114
|
+
await ensureDir(dir, { owner: opts.owner });
|
|
115
|
+
await atomicWrite(join(dir, variant.ownFilename), JSON.stringify({ version: 1, credential }, null, 2) + '\n', { mode: 0o600, owner: opts.owner });
|
|
116
|
+
return credential;
|
|
117
|
+
}
|
|
118
|
+
/** 退出登录:删掉 dsh-model 保存的那份(App 的文件不动) */
|
|
119
|
+
export async function workbuddyLogout(variant, home) {
|
|
120
|
+
const { rm } = await import('node:fs/promises');
|
|
121
|
+
const { exists } = await import('../util/fs.js');
|
|
122
|
+
const file = join(bridgeDir(home), variant.ownFilename);
|
|
123
|
+
if (!(await exists(file)))
|
|
124
|
+
return false;
|
|
125
|
+
await rm(file, { force: true });
|
|
126
|
+
return true;
|
|
127
|
+
}
|