dsh-llm-workbuddy 0.1.3 → 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 +23 -0
- package/lib/index.js +74 -70
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -190,6 +190,29 @@ Web 登录后终端脚本也读得到同一份会话。
|
|
|
190
190
|
- `authenticated && proxyUp` → 🟢 绿,显示 `WorkBuddy · <昵称>`
|
|
191
191
|
- 否则 → 🔴 红,显示「登录」按钮;`proxyUp` 为 false 时额外提示 `代理未运行`
|
|
192
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
|
+
|
|
193
216
|
### 为什么不需要构建
|
|
194
217
|
|
|
195
218
|
DSH 的 `dsh.client` 机制只要求 `package.json` 里:
|
package/lib/index.js
CHANGED
|
@@ -769,77 +769,81 @@ async function probeProxy(baseURL) {
|
|
|
769
769
|
* file appears and reports `authenticated: true`.
|
|
770
770
|
*/
|
|
771
771
|
function registerWorkbuddyRoutes(ctx, config) {
|
|
772
|
-
//
|
|
773
|
-
//
|
|
774
|
-
//
|
|
775
|
-
//
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
path
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
...proxy,
|
|
800
|
-
loginScriptAvailable: existsSync(loginScript),
|
|
801
|
-
}));
|
|
802
|
-
},
|
|
803
|
-
}));
|
|
804
|
-
ctx.effect(() => webServer.register({
|
|
805
|
-
kind: "exact",
|
|
806
|
-
path: "/api/workbuddy/login",
|
|
807
|
-
async handler(req, res) {
|
|
808
|
-
if (req.method !== "POST") {
|
|
809
|
-
res.writeHead(405, { "content-type": "application/json" });
|
|
810
|
-
res.end(JSON.stringify({ error: "method not allowed" }));
|
|
811
|
-
return;
|
|
812
|
-
}
|
|
813
|
-
if (!existsSync(loginScript)) {
|
|
814
|
-
res.writeHead(503, { "content-type": "application/json" });
|
|
815
|
-
res.end(JSON.stringify({ error: "login_workbuddy.py not found" }));
|
|
816
|
-
return;
|
|
817
|
-
}
|
|
818
|
-
// Existing session is fine; no need to re-run the flow.
|
|
819
|
-
const existing = readSessionStatus(sessionFile);
|
|
820
|
-
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);
|
|
821
799
|
res.writeHead(200, { "content-type": "application/json" });
|
|
822
|
-
res.end(JSON.stringify({
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
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
|
+
}));
|
|
846
|
+
});
|
|
843
847
|
}
|
|
844
848
|
|
|
845
849
|
/** Lazily resolve the configured proxy base URL for the health probe. */
|
package/package.json
CHANGED