dsh-llm-workbuddy 0.1.0 → 0.1.2
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 +56 -23
- package/lib/index.js +63 -17
- package/login_workbuddy.py +123 -0
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -53,12 +53,15 @@
|
|
|
53
53
|
|
|
54
54
|
## 前置条件(重要)
|
|
55
55
|
|
|
56
|
-
1.
|
|
56
|
+
1. **代理必须独立安装并运行**:本插件**不打包**第三方代理 workbuddy2api。
|
|
57
|
+
请在你的机器上单独安装并启动它(见下方「安装代理」),它把 WorkBuddy/CodeBuddy
|
|
58
|
+
的私有协议转成标准 OpenAI chat-completions 格式。
|
|
59
|
+
2. 代理必须跑在 **workbuddy2api 主分支源码**上,不能用 PyPI 的 2.0.3:
|
|
57
60
|
旧版缺少 `X-Product-Code` / Genie-IDE 等请求头,登录会在最后一步 401。
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
3. 登录使用**插件内置的 `login_workbuddy.py`**(本包自带,依赖系统 `python3`,
|
|
62
|
+
纯标准库,Python 3.7+ 即可),**不需要**代理自带的 `--login`(VSCode platform
|
|
60
63
|
会 401)。
|
|
61
|
-
|
|
64
|
+
4. 插件装入 profile 后需要**重启 `dsh web`** 才能加载新的 bundle(包括本小部件)。
|
|
62
65
|
|
|
63
66
|
---
|
|
64
67
|
|
|
@@ -70,24 +73,44 @@
|
|
|
70
73
|
dsh plugin --profile web add ./dsh-llm-workbuddy
|
|
71
74
|
```
|
|
72
75
|
|
|
76
|
+
从 npm 发布版安装(正式发布后,朋友或你自己在任何机器上):
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
dsh plugin add dsh-llm-workbuddy
|
|
80
|
+
```
|
|
81
|
+
|
|
73
82
|
CLI 会把依赖写进 profile 并把 `dsh-llm-workbuddy` 追加到 `dsh.profile.bundles`,
|
|
74
83
|
同时在 `package.json` 的 `dsh.client` 声明里登记浏览器入口(见下文「Web 小部件
|
|
75
84
|
工作原理」)。然后**重启** `dsh web`。
|
|
76
85
|
|
|
77
|
-
>
|
|
78
|
-
>
|
|
86
|
+
> 本插件**没有构建步骤**:`lib/client.js` 是浏览器原生 JS 直接被 serve;登录脚本
|
|
87
|
+
> `login_workbuddy.py` 已打进包内,用系统 `python3` 运行(纯标准库,无需 uv)。
|
|
88
|
+
> 唯一需要自己装的是第三方代理 workbuddy2api。
|
|
79
89
|
|
|
80
90
|
---
|
|
81
91
|
|
|
82
|
-
##
|
|
92
|
+
## 安装代理(先决条件,第三方)
|
|
83
93
|
|
|
84
|
-
|
|
94
|
+
插件只提供模型路由与登录;真正的协议转换由 **workbuddy2api** 完成。请单独安装并
|
|
95
|
+
启动它(以 127.0.0.1:8787 为例):
|
|
85
96
|
|
|
86
97
|
```sh
|
|
87
|
-
|
|
98
|
+
# 1. 安装 uv(本机 Python 工具)
|
|
99
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
100
|
+
|
|
101
|
+
# 2. 拉取 workbuddy2api 主分支源码
|
|
102
|
+
git clone https://github.com/hawklithm/workbuddy2api.git
|
|
103
|
+
cd workbuddy2api
|
|
104
|
+
|
|
105
|
+
# 3. 启动代理(先完成下方「登录」后再请求)
|
|
106
|
+
uv run python -u -m codebuddy_proxy --desensitize \
|
|
107
|
+
--session-file ~/.codebuddy-session.json \
|
|
108
|
+
--log-file ~/.codebuddy-proxy.jsonl
|
|
88
109
|
# 监听 http://127.0.0.1:8787
|
|
89
110
|
```
|
|
90
111
|
|
|
112
|
+
> 仓库根目录也有 `start-workbuddy.sh` 脚本(面向仓库本地开发),发布版插件不依赖它。
|
|
113
|
+
|
|
91
114
|
验证:
|
|
92
115
|
|
|
93
116
|
```sh
|
|
@@ -120,13 +143,18 @@ curl http://127.0.0.1:8787/v1/models # 模型列表(含 glm-5.2 / deepseek-v4-
|
|
|
120
143
|
|
|
121
144
|
### 方式 B(传统):在终端手动跑
|
|
122
145
|
|
|
146
|
+
使用本包内置的登录脚本(纯标准库,依赖系统 `python3`):
|
|
147
|
+
|
|
123
148
|
```sh
|
|
124
|
-
|
|
149
|
+
python3 node_modules/dsh-llm-workbuddy/login_workbuddy.py --session-file ~/.codebuddy-session.json
|
|
125
150
|
# 浏览器打开打印的链接,用 WorkBuddy/CodeBuddy 账号登录
|
|
126
|
-
# 完成后自动保存会话到
|
|
151
|
+
# 完成后自动保存会话到 ~/.codebuddy-session.json
|
|
127
152
|
```
|
|
128
153
|
|
|
129
|
-
|
|
154
|
+
> 仓库根目录也有 `./login-workbuddy.sh` 包装脚本(面向本地开发,走 `.tools/uv`);
|
|
155
|
+
> 发布版插件直接用系统 `python3` 运行同款脚本即可。
|
|
156
|
+
|
|
157
|
+
两种方式写的是同一个会话文件,可混用:终端登录后 Web 胶囊会自动变绿,
|
|
130
158
|
Web 登录后终端脚本也读得到同一份会话。
|
|
131
159
|
|
|
132
160
|
---
|
|
@@ -140,12 +168,13 @@ Web 登录后终端脚本也读得到同一份会话。
|
|
|
140
168
|
|
|
141
169
|
| 方法 + 路径 | 行为 |
|
|
142
170
|
|---|---|
|
|
143
|
-
| `GET /api/workbuddy/status` |
|
|
144
|
-
| `POST /api/workbuddy/login` | 若已有有效会话则直接返回 `alreadyLoggedIn
|
|
171
|
+
| `GET /api/workbuddy/status` | 读取会话文件(默认 `~/.codebuddy-session.json`,或配置的 `sessionFile`)的 `auth.expiresAt` 判断会话是否有效,并 `fetch` 代理 `/health` 判断 `proxyUp`;返回 JSON:`{ sessionFile, authenticated, expiresAt, account, proxyUp, tokenValid, loginScriptAvailable }` |
|
|
172
|
+
| `POST /api/workbuddy/login` | 若已有有效会话则直接返回 `alreadyLoggedIn`;否则用系统 `python3` `spawn` 包内 `login_workbuddy.py --session-file <sessionFile>`,从子进程 stdout 解析出 `authUrl` 立即返回 `{ authUrl, pending:true }`(设备流在后台继续,前端轮询 status 感知完成) |
|
|
145
173
|
|
|
146
|
-
>
|
|
147
|
-
>
|
|
148
|
-
>
|
|
174
|
+
> 会话文件与登录脚本路径的解析顺序:
|
|
175
|
+
> 1. 配置里显式指定的 `sessionFile` / `loginScript`;
|
|
176
|
+
> 2. 包内 `login_workbuddy.py` + `~/.codebuddy-session.json`;
|
|
177
|
+
> 3. 旧仓库布局(向上两级 `dsh-workbuddy/` 的 `.workbuddy/session.json`)自动兼容。
|
|
149
178
|
|
|
150
179
|
### 前端胶囊(`lib/client.js`)
|
|
151
180
|
|
|
@@ -189,6 +218,8 @@ DSH 的 `dsh.client` 机制只要求 `package.json` 里:
|
|
|
189
218
|
# defaultContextWindow: 200000 # 未在目录中标明容量的模型使用
|
|
190
219
|
# discovery: true # 实时拉取代理的 /v1/models(30s 缓存)
|
|
191
220
|
# models: [...] # 静态目录(代理不可达时的兜底)
|
|
221
|
+
# loginScript: '' # 登录脚本绝对/相对路径;默认用包内 login_workbuddy.py
|
|
222
|
+
# sessionFile: ~/.codebuddy-session.json # 会话文件路径;默认同上
|
|
192
223
|
```
|
|
193
224
|
|
|
194
225
|
模型列表默认取插件内置目录;代理可达时改为实时拉取 `/v1/models`(支持
|
|
@@ -203,9 +234,9 @@ DSH 的 `dsh.client` 机制只要求 `package.json` 里:
|
|
|
203
234
|
|---|---|
|
|
204
235
|
| 右下角没有胶囊 | `dsh web` 没重启加载新 bundle → 重启 `dsh web`;或 `curl /plugins/dsh-llm-workbuddy/client.js` 应返回 200 |
|
|
205
236
|
| 胶囊一直 `…`(加载中) | `GET /api/workbuddy/status` 失败 → 确认 `dsh web` 在跑、端口正确 |
|
|
206
|
-
| 胶囊红 + `代理未运行` |
|
|
207
|
-
| 点「登录」没反应 / 按钮灰 | `loginScriptAvailable:false` → `login_workbuddy.py`
|
|
208
|
-
| 新标签页打开后登录完成,胶囊仍是红 |
|
|
237
|
+
| 胶囊红 + `代理未运行` | workbuddy2api 代理没起或挂了 → 按「安装代理」章节启动 |
|
|
238
|
+
| 点「登录」没反应 / 按钮灰 | `loginScriptAvailable:false` → 包内 `login_workbuddy.py` 缺失或系统无 `python3`;检查安装 |
|
|
239
|
+
| 新标签页打开后登录完成,胶囊仍是红 | 会话文件(默认 `~/.codebuddy-session.json`)未刷新或 `expiresAt` 已过期 → 刷新页面或重跑登录 |
|
|
209
240
|
| 启动 `dsh web` 报 `EPERM ... cordis.yml` | `.dsh` 所在系统卷受保护(`/System/Volumes/Data` 带 `protect`)。解决:`sudo chown -R $(whoami) /Users/jiyunyang/.dsh`,或 `export DSH_HOME=$HOME/dsh-home` 后重新 `dsh plugin --profile web add` 并把插件链接进新 home |
|
|
210
241
|
| 模型请求 `TRANSPORT` 错误 | 代理未运行或端口不对(连接被拒绝) |
|
|
211
242
|
|
|
@@ -228,8 +259,10 @@ DSH 的 `dsh.client` 机制只要求 `package.json` 里:
|
|
|
228
259
|
|---|---|
|
|
229
260
|
| `lib/index.js` | Cordis 插件主体:LLM 适配器 `WorkBuddyAdapter` + `/api/workbuddy/*` 路由注册 |
|
|
230
261
|
| `lib/client.js` | 零依赖浏览器小部件(状态胶囊 + 登录流程),被 `dsh.client` 注入 |
|
|
262
|
+
| `login_workbuddy.py` | 设备流登录脚本(随包发布,被后端路由用系统 `python3` spawn) |
|
|
231
263
|
| `cordis.patch.yml` | 本包的 Cordis bundle 挂载声明(`id: llm-workbuddy`) |
|
|
232
264
|
| `package.json` | 包元数据、`dsh.client` 浏览器入口声明、`llm-workbuddy` peer 依赖 |
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
265
|
+
|
|
266
|
+
> 第三方 workbuddy2api 代理**不在包内**,需单独安装(见「安装代理」章节)。
|
|
267
|
+
> 仓库根目录的 `start-workbuddy.sh` / `login-workbuddy.sh` / `.workbuddy/` 是本地
|
|
268
|
+
> 开发用的配套文件,不随 npm 包发布。
|
package/lib/index.js
CHANGED
|
@@ -592,6 +592,11 @@ export const Config = z.object({
|
|
|
592
592
|
models: z.array(catalogModel).default(DEFAULT_MODELS),
|
|
593
593
|
streamIdleTimeoutMs: z.number().min(Number.MIN_VALUE).max(MAX_TIMER_DELAY_MS).default(DEFAULT_STREAM_IDLE_TIMEOUT_MS),
|
|
594
594
|
discovery: z.boolean().default(true),
|
|
595
|
+
// Optional path overrides for the login helper. When unset, the plugin uses
|
|
596
|
+
// the login_workbuddy.py bundled with this package and the default session
|
|
597
|
+
// file location (~/.codebuddy-session.json).
|
|
598
|
+
loginScript: z.string(),
|
|
599
|
+
sessionFile: z.string(),
|
|
595
600
|
});
|
|
596
601
|
|
|
597
602
|
/** Resolve, validate, and detach the advisory model catalog. */
|
|
@@ -652,6 +657,8 @@ export function resolveAdapterOptions(config) {
|
|
|
652
657
|
models: resolveModels(config.models),
|
|
653
658
|
streamIdleTimeoutMs,
|
|
654
659
|
discovery: config.discovery ?? true,
|
|
660
|
+
loginScript: config.loginScript,
|
|
661
|
+
sessionFile: config.sessionFile,
|
|
655
662
|
};
|
|
656
663
|
}
|
|
657
664
|
|
|
@@ -659,25 +666,61 @@ export function resolveAdapterOptions(config) {
|
|
|
659
666
|
|
|
660
667
|
// #region login status web API
|
|
661
668
|
|
|
662
|
-
/**
|
|
669
|
+
/**
|
|
670
|
+
* Resolve the runtime paths for the login helper and session file.
|
|
671
|
+
*
|
|
672
|
+
* Precedence:
|
|
673
|
+
* 1. explicit `loginScript` / `sessionFile` config (paths may be absolute
|
|
674
|
+
* or relative to the process CWD);
|
|
675
|
+
* 2. bundled `login_workbuddy.py` (this package ships a copy) and the
|
|
676
|
+
* conventional `~/.codebuddy-session.json`;
|
|
677
|
+
* 3. legacy fallback: the monorepo layout `dsh-workbuddy/` (two levels up
|
|
678
|
+
* from the plugin dir) that held `login_workbuddy.py` and
|
|
679
|
+
* `.workbuddy/session.json`.
|
|
680
|
+
*/
|
|
663
681
|
const PLUGIN_DIR = dirname(fileURLToPath(import.meta.url));
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
const
|
|
667
|
-
|
|
668
|
-
|
|
682
|
+
|
|
683
|
+
function defaultSessionFile() {
|
|
684
|
+
const home = process.env.HOME ?? process.env.USERPROFILE;
|
|
685
|
+
return home ? resolve(home, ".codebuddy-session.json") : resolve(PLUGIN_DIR, ".codebuddy-session.json");
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function resolveLoginPaths(config) {
|
|
689
|
+
const configSession = config?.sessionFile;
|
|
690
|
+
const configScript = config?.loginScript;
|
|
691
|
+
|
|
692
|
+
let sessionFile;
|
|
693
|
+
if (typeof configSession === "string" && configSession.length > 0) {
|
|
694
|
+
sessionFile = resolve(configSession);
|
|
695
|
+
} else {
|
|
696
|
+
// Legacy monorepo layout: dsh-workbuddy/.workbuddy/session.json
|
|
697
|
+
const legacyRoot = resolve(PLUGIN_DIR, "..", "..");
|
|
698
|
+
const legacySession = resolve(legacyRoot, ".workbuddy", "session.json");
|
|
699
|
+
sessionFile = existsSync(legacySession) ? legacySession : defaultSessionFile();
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
let loginScript;
|
|
703
|
+
if (typeof configScript === "string" && configScript.length > 0) {
|
|
704
|
+
loginScript = resolve(configScript);
|
|
705
|
+
} else {
|
|
706
|
+
// Bundled copy ships at the package root (dsh-llm-workbuddy/login_workbuddy.py).
|
|
707
|
+
loginScript = resolve(PLUGIN_DIR, "..", "login_workbuddy.py");
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
return { sessionFile, loginScript };
|
|
711
|
+
}
|
|
669
712
|
|
|
670
713
|
/**
|
|
671
714
|
* Read the local WorkBuddy session file and derive its validity. Returns a
|
|
672
715
|
* normalized status object the Web widget polls. Never throws — missing or
|
|
673
716
|
* malformed state is reported as `authenticated: false` so the UI can prompt.
|
|
674
717
|
*/
|
|
675
|
-
function readSessionStatus() {
|
|
676
|
-
if (!existsSync(
|
|
718
|
+
function readSessionStatus(sessionFile) {
|
|
719
|
+
if (!existsSync(sessionFile)) {
|
|
677
720
|
return { sessionFile: false, authenticated: false, expiresAt: null, account: null };
|
|
678
721
|
}
|
|
679
722
|
try {
|
|
680
|
-
const raw = JSON.parse(readFileSync(
|
|
723
|
+
const raw = JSON.parse(readFileSync(sessionFile, "utf-8"));
|
|
681
724
|
const expiresAt = raw?.auth?.expiresAt ?? null;
|
|
682
725
|
const now = Date.now();
|
|
683
726
|
const expired = expiresAt !== null && expiresAt <= now;
|
|
@@ -721,21 +764,22 @@ async function probeProxy(baseURL) {
|
|
|
721
764
|
* can open it in a new tab. The widget then polls `/status` until the session
|
|
722
765
|
* file appears and reports `authenticated: true`.
|
|
723
766
|
*/
|
|
724
|
-
function registerWorkbuddyRoutes(ctx) {
|
|
767
|
+
function registerWorkbuddyRoutes(ctx, config) {
|
|
725
768
|
if (ctx.webServer === undefined) return; // headless profile: no HTTP surface
|
|
726
769
|
const baseURL = config_baseURL();
|
|
770
|
+
const { sessionFile, loginScript } = resolveLoginPaths(config);
|
|
727
771
|
ctx.webServer.register({
|
|
728
772
|
path: "/api/workbuddy/status",
|
|
729
773
|
exact: true,
|
|
730
774
|
methods: ["GET"],
|
|
731
775
|
async handler(_req, res) {
|
|
732
|
-
const session = readSessionStatus();
|
|
776
|
+
const session = readSessionStatus(sessionFile);
|
|
733
777
|
const proxy = await probeProxy(baseURL);
|
|
734
778
|
res.writeHead(200, { "content-type": "application/json" });
|
|
735
779
|
res.end(JSON.stringify({
|
|
736
780
|
...session,
|
|
737
781
|
...proxy,
|
|
738
|
-
loginScriptAvailable: existsSync(
|
|
782
|
+
loginScriptAvailable: existsSync(loginScript),
|
|
739
783
|
}));
|
|
740
784
|
},
|
|
741
785
|
});
|
|
@@ -744,20 +788,22 @@ function registerWorkbuddyRoutes(ctx) {
|
|
|
744
788
|
exact: true,
|
|
745
789
|
methods: ["POST"],
|
|
746
790
|
async handler(_req, res) {
|
|
747
|
-
if (!existsSync(
|
|
791
|
+
if (!existsSync(loginScript)) {
|
|
748
792
|
res.writeHead(503, { "content-type": "application/json" });
|
|
749
793
|
res.end(JSON.stringify({ error: "login_workbuddy.py not found" }));
|
|
750
794
|
return;
|
|
751
795
|
}
|
|
752
796
|
// Existing session is fine; no need to re-run the flow.
|
|
753
|
-
const existing = readSessionStatus();
|
|
797
|
+
const existing = readSessionStatus(sessionFile);
|
|
754
798
|
if (existing.authenticated) {
|
|
755
799
|
res.writeHead(200, { "content-type": "application/json" });
|
|
756
800
|
res.end(JSON.stringify({ alreadyLoggedIn: true, authUrl: null, ...existing }));
|
|
757
801
|
return;
|
|
758
802
|
}
|
|
759
|
-
|
|
760
|
-
|
|
803
|
+
// login_workbuddy.py is dependency-free (stdlib only), so we invoke it
|
|
804
|
+
// directly with the system Python — no uv runtime required.
|
|
805
|
+
const args = ["-u", loginScript, "--session-file", sessionFile];
|
|
806
|
+
const child = spawn("python3", args, { env: { ...process.env, PYTHONUNBUFFERED: "1" } });
|
|
761
807
|
let stdout = "";
|
|
762
808
|
let authUrl = null;
|
|
763
809
|
child.stdout.on("data", (chunk) => {
|
|
@@ -821,7 +867,7 @@ export function apply(ctx, config) {
|
|
|
821
867
|
settingsPath: [],
|
|
822
868
|
}]);
|
|
823
869
|
ctx.llm.registerAdapter([PROVIDER], adapter);
|
|
824
|
-
registerWorkbuddyRoutes(ctx);
|
|
870
|
+
registerWorkbuddyRoutes(ctx, config);
|
|
825
871
|
installSettingsSection(ctx, NS, Config, config, {
|
|
826
872
|
setSource: (source) => {
|
|
827
873
|
current = source;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Standalone WorkBuddy/CodeBuddy CN OAuth login.
|
|
3
|
+
|
|
4
|
+
Implements the device flow exactly like the official CodeBuddy plugin
|
|
5
|
+
(platform=CLI, Origin/Referer codebuddy.cn, CLI User-Agent), as verified by
|
|
6
|
+
the Sliverkiss/workbuddy2api Go login tool. Writes a session file in the
|
|
7
|
+
format the hawklithm/workbuddy2api Python proxy reads:
|
|
8
|
+
{ "auth": {accessToken, refreshToken, expiresIn, domain}, "account": {...}, "machineId": "..." }
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
python3 login_workbuddy.py [--session-file PATH] [--timeout 300]
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import json
|
|
17
|
+
import pathlib
|
|
18
|
+
import sys
|
|
19
|
+
import time
|
|
20
|
+
import urllib.error
|
|
21
|
+
import urllib.request
|
|
22
|
+
import uuid
|
|
23
|
+
|
|
24
|
+
BASE = "https://copilot.tencent.com"
|
|
25
|
+
PLATFORM = "CLI"
|
|
26
|
+
CLIENT_UA = "CLI/2.63.2 CodeBuddy/2.63.2"
|
|
27
|
+
ORIGIN = "https://www.codebuddy.cn"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def common_headers(req: urllib.request.Request) -> None:
|
|
31
|
+
req.add_header("Content-Type", "application/json")
|
|
32
|
+
req.add_header("Accept", "application/json, text/plain, */*")
|
|
33
|
+
req.add_header("X-Requested-With", "XMLHttpRequest")
|
|
34
|
+
req.add_header("Origin", ORIGIN)
|
|
35
|
+
req.add_header("Referer", ORIGIN + "/")
|
|
36
|
+
req.add_header("User-Agent", CLIENT_UA)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def do(method: str, path: str, *, auth: str | None = None, body: dict | None = None) -> tuple[dict, int]:
|
|
40
|
+
data = json.dumps(body).encode("utf-8") if body is not None else None
|
|
41
|
+
url = BASE + path
|
|
42
|
+
req = urllib.request.Request(url, data=data, method=method)
|
|
43
|
+
common_headers(req)
|
|
44
|
+
if auth:
|
|
45
|
+
req.add_header("Authorization", f"Bearer {auth}")
|
|
46
|
+
try:
|
|
47
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
48
|
+
return json.loads(resp.read().decode("utf-8")), resp.status
|
|
49
|
+
except urllib.error.HTTPError as exc:
|
|
50
|
+
detail = exc.read().decode("utf-8", errors="replace")
|
|
51
|
+
raise RuntimeError(f"HTTP {exc.code} {path}: {detail[:400]}") from exc
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def unwrap(payload: dict) -> dict:
|
|
55
|
+
"""{code, msg, data} envelope."""
|
|
56
|
+
if payload.get("code") not in (0, None):
|
|
57
|
+
raise RuntimeError(f"business code={payload.get('code')} msg={payload.get('msg')}")
|
|
58
|
+
data = payload.get("data")
|
|
59
|
+
return data if isinstance(data, dict) else {}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def main() -> int:
|
|
63
|
+
ap = argparse.ArgumentParser()
|
|
64
|
+
ap.add_argument("--session-file", default=str(pathlib.Path.home() / ".codebuddy-session.json"))
|
|
65
|
+
ap.add_argument("--timeout", type=int, default=300)
|
|
66
|
+
args = ap.parse_args()
|
|
67
|
+
|
|
68
|
+
session_file = pathlib.Path(args.session_file)
|
|
69
|
+
|
|
70
|
+
state_payload, _ = do("POST", f"/v2/plugin/auth/state?platform={PLATFORM}", body={})
|
|
71
|
+
st = unwrap(state_payload)
|
|
72
|
+
state = st.get("state")
|
|
73
|
+
auth_url = st.get("authUrl")
|
|
74
|
+
if not state or not auth_url:
|
|
75
|
+
raise RuntimeError(f"auth/state missing state/authUrl: {st!r}")
|
|
76
|
+
|
|
77
|
+
print("请在浏览器中打开以下链接,用你的 WorkBuddy/CodeBuddy 账号完成登录:")
|
|
78
|
+
print(f"\n {auth_url}\n")
|
|
79
|
+
|
|
80
|
+
deadline = time.monotonic() + args.timeout
|
|
81
|
+
token = None
|
|
82
|
+
while time.monotonic() < deadline:
|
|
83
|
+
time.sleep(2)
|
|
84
|
+
try:
|
|
85
|
+
tok, _ = do("GET", f"/v2/plugin/auth/token?state={state}")
|
|
86
|
+
except RuntimeError:
|
|
87
|
+
continue
|
|
88
|
+
if isinstance(tok.get("data"), dict) and tok["data"].get("accessToken"):
|
|
89
|
+
token = tok["data"]
|
|
90
|
+
break
|
|
91
|
+
if token is None:
|
|
92
|
+
print("登录超时,请重试。", file=sys.stderr)
|
|
93
|
+
return 1
|
|
94
|
+
|
|
95
|
+
access = token.get("accessToken", "")
|
|
96
|
+
account: dict = {}
|
|
97
|
+
try:
|
|
98
|
+
acct, _ = do("GET", f"/v2/plugin/login/account?state={state}", auth=access)
|
|
99
|
+
if isinstance(acct.get("data"), dict):
|
|
100
|
+
account = acct["data"]
|
|
101
|
+
except RuntimeError as exc:
|
|
102
|
+
print(f"(warn) login/account skipped: {exc}", file=sys.stderr)
|
|
103
|
+
|
|
104
|
+
session = {
|
|
105
|
+
"auth": {
|
|
106
|
+
"accessToken": access,
|
|
107
|
+
"refreshToken": token.get("refreshToken", ""),
|
|
108
|
+
"expiresIn": token.get("expiresIn"),
|
|
109
|
+
"expiresAt": int(time.time() * 1000) + int(token.get("expiresIn", 0) or 0) * 1000,
|
|
110
|
+
"domain": token.get("domain", ""),
|
|
111
|
+
},
|
|
112
|
+
"account": account,
|
|
113
|
+
"machineId": str(uuid.uuid4()),
|
|
114
|
+
}
|
|
115
|
+
session_file.parent.mkdir(parents=True, exist_ok=True)
|
|
116
|
+
session_file.write_text(json.dumps(session, ensure_ascii=False, indent=2))
|
|
117
|
+
print(f"\n登录成功,会话已保存: {session_file}")
|
|
118
|
+
print(f"用户: {account.get('nickname') or account.get('uid') or '<unknown>'}")
|
|
119
|
+
return 0
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
if __name__ == "__main__":
|
|
123
|
+
raise SystemExit(main())
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-llm-workbuddy",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "WorkBuddy (via the local workbuddy2api proxy) LLM provider adapter for DeepSeek Harness, with a Web login-status widget",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -32,19 +32,21 @@
|
|
|
32
32
|
"default": "./lib/index.js"
|
|
33
33
|
},
|
|
34
34
|
"./client": "./lib/client.js",
|
|
35
|
+
"./login_workbuddy.py": "./login_workbuddy.py",
|
|
35
36
|
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
36
37
|
"./package.json": "./package.json"
|
|
37
38
|
},
|
|
38
39
|
"files": [
|
|
39
40
|
"lib",
|
|
41
|
+
"login_workbuddy.py",
|
|
40
42
|
"cordis.patch.yml",
|
|
41
43
|
"README.md"
|
|
42
44
|
],
|
|
43
45
|
"peerDependencies": {
|
|
44
46
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
45
|
-
"@deepseek-ai/dsh-llm": "^0.1.0-rc.
|
|
46
|
-
"@deepseek-ai/dsh-settings": "^0.1.0-rc.
|
|
47
|
-
"@deepseek-ai/dsh-timeout": "^0.1.0-rc.
|
|
47
|
+
"@deepseek-ai/dsh-llm": "^0.1.0-rc.7",
|
|
48
|
+
"@deepseek-ai/dsh-settings": "^0.1.0-rc.7",
|
|
49
|
+
"@deepseek-ai/dsh-timeout": "^0.1.0-rc.7",
|
|
48
50
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
49
51
|
}
|
|
50
52
|
}
|