dsh-llm-workbuddy 0.1.2 → 0.1.4
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 +32 -5
- package/lib/index.js +78 -52
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,7 +42,8 @@
|
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
- **后端路由**(`lib/index.js` 的 `registerWorkbuddyRoutes`)运行在 `dsh web` 的
|
|
45
|
-
HTTP 服务上,通过 Cordis 的 `ctx.webServer`
|
|
45
|
+
HTTP 服务上,通过 Cordis 的 `webServer` 服务注册(用 `ctx.get("webServer")`
|
|
46
|
+
探测,`webServer` 不列入 `inject`),headless profile 下自动跳过。
|
|
46
47
|
- **前端胶囊**(`lib/client.js`)是零依赖的原生浏览器 JS,由 DSH 的 `dsh.client`
|
|
47
48
|
双端机制在 `window.__DSH_BOOT__` 中注入,serve 于
|
|
48
49
|
`/plugins/dsh-llm-workbuddy/client.js`。
|
|
@@ -163,13 +164,16 @@ Web 登录后终端脚本也读得到同一份会话。
|
|
|
163
164
|
|
|
164
165
|
### 后端路由(`lib/index.js`)
|
|
165
166
|
|
|
166
|
-
插件在 `apply(ctx, config)`
|
|
167
|
-
|
|
167
|
+
插件在 `apply(ctx, config)` 里用 `ctx.get("webServer")` 探测 HTTP 服务(不在
|
|
168
|
+
`inject` 里,headless profile 无 `webServer` 时返回 `undefined`、自动跳过),
|
|
169
|
+
然后通过 `webServer.register({ kind: 'exact', path, handler })` 注册两条精确
|
|
170
|
+
路由,并用 `ctx.effect` 包裹以便热重载自动清理。HTTP 方法过滤在 handler 内自行
|
|
171
|
+
判断(`req.method`),因为路由 API 本身不含方法字段:
|
|
168
172
|
|
|
169
173
|
| 方法 + 路径 | 行为 |
|
|
170
174
|
|---|---|
|
|
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
|
|
175
|
+
| `GET /api/workbuddy/status` | 读取会话文件(默认 `~/.codebuddy-session.json`,或配置的 `sessionFile`)的 `auth.expiresAt` 判断会话是否有效,并 `fetch` 代理 `/health` 判断 `proxyUp`;返回 JSON:`{ sessionFile, authenticated, expiresAt, account, proxyUp, tokenValid, loginScriptAvailable }`;非 GET 返回 405 |
|
|
176
|
+
| `POST /api/workbuddy/login` | 若已有有效会话则直接返回 `alreadyLoggedIn`;否则用系统 `python3` `spawn` 包内 `login_workbuddy.py --session-file <sessionFile>`,从子进程 stdout 解析出 `authUrl` 立即返回 `{ authUrl, pending:true }`(设备流在后台继续,前端轮询 status 感知完成);非 POST 返回 405 |
|
|
173
177
|
|
|
174
178
|
> 会话文件与登录脚本路径的解析顺序:
|
|
175
179
|
> 1. 配置里显式指定的 `sessionFile` / `loginScript`;
|
|
@@ -186,6 +190,29 @@ Web 登录后终端脚本也读得到同一份会话。
|
|
|
186
190
|
- `authenticated && proxyUp` → 🟢 绿,显示 `WorkBuddy · <昵称>`
|
|
187
191
|
- 否则 → 🔴 红,显示「登录」按钮;`proxyUp` 为 false 时额外提示 `代理未运行`
|
|
188
192
|
|
|
193
|
+
> **关于 UI 挂载位置(临时做法说明)**
|
|
194
|
+
>
|
|
195
|
+
> 当前状态胶囊用 `position:fixed` 直接注入 `document.body`。DSH 官方的 UI
|
|
196
|
+
> 组合机制是 **slot 系统**(`@deepseek-ai/dsh-client-ui-slots`,`ctx.slots.register`
|
|
197
|
+
> 注册 React 组件进声明的 slot)。按官方「组合优先、不要假设/覆盖其他插件内部实现」
|
|
198
|
+
> 的原则,理想做法是注册进官方 slot,而非直接改产品外壳。
|
|
199
|
+
>
|
|
200
|
+
> 评估结论:**当前 shell 声明的 slot 里没有专门的「右下角状态位」**;最接近的
|
|
201
|
+
> `sidebar.footer.action`、`conversation.session.header.actions`、
|
|
202
|
+
> `conversation.input.overlay` 都是 **React 组件位**,迁移意味着要把本插件从
|
|
203
|
+
> 「零依赖原生 JS」改造成 **React + 构建流程**,并依赖 shell 暴露 `ctx.slots`
|
|
204
|
+
> 服务。因此现阶段保留 DOM 注入(机制允许),**待官方 shell 提供合适的状态胶囊
|
|
205
|
+
> slot 后再迁移**。以下是已知的迁移路径(供后续参考):
|
|
206
|
+
>
|
|
207
|
+
> ```js
|
|
208
|
+
> ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
|
|
209
|
+
> name: "sidebar.footer.action",
|
|
210
|
+
> id: "workbuddy-status",
|
|
211
|
+
> locale: NS,
|
|
212
|
+
> inject: () => ({ /* 轮询句柄 / 登录回调 */ })
|
|
213
|
+
> }, WorkBuddyStatusPill /* React 组件 */));
|
|
214
|
+
> ```
|
|
215
|
+
|
|
189
216
|
### 为什么不需要构建
|
|
190
217
|
|
|
191
218
|
DSH 的 `dsh.client` 机制只要求 `package.json` 里:
|
package/lib/index.js
CHANGED
|
@@ -38,7 +38,11 @@ import { MAX_TIMER_DELAY_MS, idleWatchdog, timeoutOf } from "@deepseek-ai/dsh-ti
|
|
|
38
38
|
|
|
39
39
|
/** Plugin identity (Cordis convention). */
|
|
40
40
|
export const name = "llm-workbuddy";
|
|
41
|
-
|
|
41
|
+
// `llm` is a hard requirement (the adapter must always register). `webServer`
|
|
42
|
+
// is optional — headless profiles have no HTTP surface, so it must NOT be in
|
|
43
|
+
// `inject` (that would make the whole plugin PENDING forever in headless).
|
|
44
|
+
// We probe it at the use site instead. See cordis-tutorial/03-services.md.
|
|
45
|
+
export const inject = ["llm"];
|
|
42
46
|
|
|
43
47
|
/** The single provider route this plugin owns. */
|
|
44
48
|
export const PROVIDER = "workbuddy";
|
|
@@ -765,58 +769,80 @@ async function probeProxy(baseURL) {
|
|
|
765
769
|
* file appears and reports `authenticated: true`.
|
|
766
770
|
*/
|
|
767
771
|
function registerWorkbuddyRoutes(ctx, config) {
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
}
|
|
796
|
-
// Existing session is fine; no need to re-run the flow.
|
|
797
|
-
const existing = readSessionStatus(sessionFile);
|
|
798
|
-
if (existing.authenticated) {
|
|
772
|
+
// Delay registration until the webServer service exists. The plugin keeps
|
|
773
|
+
// `webServer` OUT of `inject` so headless profiles (no HTTP surface) still
|
|
774
|
+
// load — but a synchronous `ctx.get("webServer")` probe at apply time races
|
|
775
|
+
// the webserver plugin's own mount and can observe the service before it is
|
|
776
|
+
// provided, silently dropping the routes. `ctx.inject` runs this callback
|
|
777
|
+
// only once webServer is available, and skips it entirely when the service
|
|
778
|
+
// never appears (headless), preserving the original intent.
|
|
779
|
+
ctx.inject(["webServer"], (webCtx) => {
|
|
780
|
+
const webServer = webCtx.webServer;
|
|
781
|
+
const baseURL = config_baseURL();
|
|
782
|
+
const { sessionFile, loginScript } = resolveLoginPaths(config);
|
|
783
|
+
// The register() call returns a disposer. Wrapping it in ctx.effect ties
|
|
784
|
+
// it to the plugin fiber so config hot-edit / reload cleans the routes up
|
|
785
|
+
// instead of leaking them (a leaked route makes a duplicate re-register
|
|
786
|
+
// throw). Method filtering must be done inside the handler — the route API
|
|
787
|
+
// only accepts `{ kind, path, handler }`.
|
|
788
|
+
webCtx.effect(() => webServer.register({
|
|
789
|
+
kind: "exact",
|
|
790
|
+
path: "/api/workbuddy/status",
|
|
791
|
+
async handler(req, res) {
|
|
792
|
+
if (req.method !== "GET") {
|
|
793
|
+
res.writeHead(405, { "content-type": "application/json" });
|
|
794
|
+
res.end(JSON.stringify({ error: "method not allowed" }));
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
const session = readSessionStatus(sessionFile);
|
|
798
|
+
const proxy = await probeProxy(baseURL);
|
|
799
799
|
res.writeHead(200, { "content-type": "application/json" });
|
|
800
|
-
res.end(JSON.stringify({
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
800
|
+
res.end(JSON.stringify({
|
|
801
|
+
...session,
|
|
802
|
+
...proxy,
|
|
803
|
+
loginScriptAvailable: existsSync(loginScript),
|
|
804
|
+
}));
|
|
805
|
+
},
|
|
806
|
+
}));
|
|
807
|
+
webCtx.effect(() => webServer.register({
|
|
808
|
+
kind: "exact",
|
|
809
|
+
path: "/api/workbuddy/login",
|
|
810
|
+
async handler(req, res) {
|
|
811
|
+
if (req.method !== "POST") {
|
|
812
|
+
res.writeHead(405, { "content-type": "application/json" });
|
|
813
|
+
res.end(JSON.stringify({ error: "method not allowed" }));
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
if (!existsSync(loginScript)) {
|
|
817
|
+
res.writeHead(503, { "content-type": "application/json" });
|
|
818
|
+
res.end(JSON.stringify({ error: "login_workbuddy.py not found" }));
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
// Existing session is fine; no need to re-run the flow.
|
|
822
|
+
const existing = readSessionStatus(sessionFile);
|
|
823
|
+
if (existing.authenticated) {
|
|
824
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
825
|
+
res.end(JSON.stringify({ alreadyLoggedIn: true, authUrl: null, ...existing }));
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
// login_workbuddy.py is dependency-free (stdlib only), so we invoke it
|
|
829
|
+
// directly with the system Python — no uv runtime required.
|
|
830
|
+
const args = ["-u", loginScript, "--session-file", sessionFile];
|
|
831
|
+
const child = spawn("python3", args, { env: { ...process.env, PYTHONUNBUFFERED: "1" } });
|
|
832
|
+
let stdout = "";
|
|
833
|
+
let authUrl = null;
|
|
834
|
+
child.stdout.on("data", (chunk) => {
|
|
835
|
+
stdout += chunk.toString();
|
|
836
|
+
const m = stdout.match(/https?:\/\/\S+/);
|
|
837
|
+
if (m && authUrl === null) authUrl = m[0];
|
|
838
|
+
});
|
|
839
|
+
child.stderr.on("data", () => {});
|
|
840
|
+
// The device flow blocks until login or timeout; we hand back the URL
|
|
841
|
+
// immediately and let the widget poll /status for completion.
|
|
842
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
843
|
+
res.end(JSON.stringify({ authUrl, pending: true }));
|
|
844
|
+
},
|
|
845
|
+
}));
|
|
820
846
|
});
|
|
821
847
|
}
|
|
822
848
|
|
package/package.json
CHANGED