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 CHANGED
@@ -53,12 +53,15 @@
53
53
 
54
54
  ## 前置条件(重要)
55
55
 
56
- 1. 代理必须跑在 **workbuddy2api 主分支源码**上,不能用 PyPI 的 2.0.3:
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
- 2. 登录请使用仓库里的 `login_workbuddy.py`(`platform=CLI` + codebuddy.cn
59
- 请求头,与官方插件一致),**不要**用代理自带的 `--login`(VSCode platform
61
+ 3. 登录使用**插件内置的 `login_workbuddy.py`**(本包自带,依赖系统 `python3`,
62
+ 纯标准库,Python 3.7+ 即可),**不需要**代理自带的 `--login`(VSCode platform
60
63
  会 401)。
61
- 3. 插件装入 profile 后需要**重启 `dsh web`** 才能加载新的 bundle(包括本小部件)。
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
- > 本插件**没有任何构建步骤、零运行时依赖**:`lib/client.js` 是浏览器原生 JS
78
- > 直接被 DSH serve,不需要 vite / react / TypeScript 编译。
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
- ./start-workbuddy.sh
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
- ./login-workbuddy.sh
149
+ python3 node_modules/dsh-llm-workbuddy/login_workbuddy.py --session-file ~/.codebuddy-session.json
125
150
  # 浏览器打开打印的链接,用 WorkBuddy/CodeBuddy 账号登录
126
- # 完成后自动保存会话到 .workbuddy/session.json(也提示已写入 ~/.codebuddy-session.json
151
+ # 完成后自动保存会话到 ~/.codebuddy-session.json
127
152
  ```
128
153
 
129
- 两种方式的产物是同一个 `session.json`,可混用:终端登录后 Web 胶囊会自动变绿,
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` | 读取 `.workbuddy/session.json` `auth.expiresAt` 判断会话是否有效,并 `fetch` 代理 `/health` 判断 `proxyUp`;返回 JSON:`{ sessionFile, authenticated, expiresAt, account, proxyUp, tokenValid, loginScriptAvailable }` |
144
- | `POST /api/workbuddy/login` | 若已有有效会话则直接返回 `alreadyLoggedIn`;否则 `spawn` `.tools/uv run login_workbuddy.py --session-file .workbuddy/session.json`,从子进程 stdout 解析出 `authUrl` 立即返回 `{ authUrl, pending:true }`(设备流在后台继续,前端轮询 status 感知完成) |
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
- > `dsh-llm-workbuddy/lib/index.js` 向上两级即仓库根 `dsh-workbuddy/`,其下的
148
- > `.workbuddy/session.json` `login_workbuddy.py` 即为目标。
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
- | 胶囊红 + `代理未运行` | `start-workbuddy.sh` 没起或挂了启动代理后再看 |
207
- | 点「登录」没反应 / 按钮灰 | `loginScriptAvailable:false` → `login_workbuddy.py` `.tools/uv` 不在仓库根;检查路径 |
208
- | 新标签页打开后登录完成,胶囊仍是红 | 会话文件 `.workbuddy/session.json` 未刷新或 `expiresAt` 已过期 → 刷新页面或重跑登录 |
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
- | `../login_workbuddy.py` | 设备流登录脚本(被后端路由 spawn) |
234
- | `../start-workbuddy.sh` | 启动 workbuddy2api 代理 |
235
- | `../.workbuddy/session.json` | 登录会话文件(胶囊与适配器共读) |
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
- /** Resolve the repo root that holds `login_workbuddy.py` and `.workbuddy/`. */
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
- // dsh-llm-workbuddy/lib/index.js -> dsh-workbuddy/ (two levels up).
665
- const REPO_ROOT = resolve(PLUGIN_DIR, "..", "..");
666
- const SESSION_FILE = resolve(REPO_ROOT, ".workbuddy", "session.json");
667
- const LOGIN_SCRIPT = resolve(REPO_ROOT, "login_workbuddy.py");
668
- const WORKBUDDY_UV = resolve(REPO_ROOT, ".tools", "uv");
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(SESSION_FILE)) {
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(SESSION_FILE, "utf-8"));
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(LOGIN_SCRIPT),
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(LOGIN_SCRIPT)) {
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
- const args = ["run", "--python", "3.12", "python", "-u", LOGIN_SCRIPT, "--session-file", SESSION_FILE];
760
- const child = spawn(WORKBUDDY_UV, args, { cwd: REPO_ROOT, env: { ...process.env, UV_PYTHON_INSTALL_DIR: resolve(REPO_ROOT, ".tools", "uv-python"), UV_CACHE_DIR: resolve(REPO_ROOT, ".tools", "uv-cache"), PYTHONUNBUFFERED: "1" } });
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.0",
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.8",
46
- "@deepseek-ai/dsh-settings": "^0.1.0-rc.8",
47
- "@deepseek-ai/dsh-timeout": "^0.1.0-rc.8",
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
  }