dsh-connect-workbuddy 1.0.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/CHANGELOG.md +33 -0
- package/LICENSE +21 -0
- package/README.en.md +141 -0
- package/README.md +143 -0
- package/THIRD_PARTY_NOTICES.md +57 -0
- package/cordis.patch.yml +6 -0
- package/docs/assets/dsh-connect-workbuddy-usage-card.png +0 -0
- package/lib/bin.d.ts +19 -0
- package/lib/bin.js +202 -0
- package/lib/client.js +872 -0
- package/lib/host-heartbeat-DHfQtrSf.js +1118 -0
- package/lib/index.d.ts +651 -0
- package/lib/index.js +792 -0
- package/package.json +114 -0
- package/screenshots.json +3 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,792 @@
|
|
|
1
|
+
import { C as defaultDesktopAuthCandidates, D as workbuddyAccountId, E as parseWorkBuddyAuth, O as workbuddyOwnAuthPath, S as authFileName, T as defaultDesktopAuthPath, _ as WorkBuddyCatalog, a as readHostHeartbeat, b as WORKBUDDY_AUTH_FILE_ENV, c as WORKBUDDY_CONNECT_VERSION, d as parseCreditMultiplier, f as parseReasoning, g as FALLBACK_WORKBUDDY_MODELS, h as regionOf, i as processStartTimeMs, l as WorkBuddyUpstreamClient, m as prepareChatBody, n as clearHostHeartbeat, o as workbuddyHostHeartbeatPath, p as parseUpstreamModel, r as isHeartbeatProcessAlive, s as writeHostHeartbeat, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, u as classifyUpstreamError, v as deriveCatalog, w as defaultDesktopAuthDirs, x as WorkBuddyCredentialStore, y as WORKBUDDY_AUTH_FILENAME } from "./host-heartbeat-DHfQtrSf.js";
|
|
2
|
+
import z from "@deepseek-ai/schemastery";
|
|
3
|
+
import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
4
|
+
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
5
|
+
import { createProvider } from "@earendil-works/pi-ai";
|
|
6
|
+
import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
|
|
7
|
+
import { resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
|
|
8
|
+
import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
|
|
9
|
+
import { createServer } from "node:http";
|
|
10
|
+
import { Readable } from "node:stream";
|
|
11
|
+
//#region src/adapter.ts
|
|
12
|
+
/**
|
|
13
|
+
* The `workbuddy` pi-ai provider: one loopback-backed adapter registered
|
|
14
|
+
* into the Harness LLM seam, assembled from public `dsh-llm-pi-ai`
|
|
15
|
+
* extension points.
|
|
16
|
+
*
|
|
17
|
+
* 参考:corrinehu/dsh-workbuddy-connect(MIT,Copyright (c) 2026 Corrine Hu)
|
|
18
|
+
* — pi-ai provider 的装配方式(createProvider + openAICompletionsApi +
|
|
19
|
+
* inert auth plane + 用 shim 的进程内 secret 作为 apiKey)由该项目实现;
|
|
20
|
+
* DSH 插件结构与 provider 注册的思路参照
|
|
21
|
+
* franksong2702/dsh-codex-connect(Apache-2.0),经其转引。
|
|
22
|
+
* 改动:模型描述符补上 upstream 给出的多模态与推理档位信息(若有),
|
|
23
|
+
* 供 DSH 的能力判断使用。
|
|
24
|
+
*
|
|
25
|
+
* @module dsh-connect-workbuddy/adapter
|
|
26
|
+
*/
|
|
27
|
+
/** Provider route this bundle owns. */
|
|
28
|
+
const WORKBUDDY_PROVIDER = "workbuddy";
|
|
29
|
+
/** Provider idle ceiling while one stream read is outstanding. */
|
|
30
|
+
const WORKBUDDY_STREAM_IDLE_TIMEOUT_MS = 3e5;
|
|
31
|
+
/**
|
|
32
|
+
* Image-request budgets at the dsh-llm-pi-ai defaults; the profile type made
|
|
33
|
+
* them required in 0.1.1-rc.2.
|
|
34
|
+
*/
|
|
35
|
+
const REQUEST_IMAGE_BUDGETS = {
|
|
36
|
+
maxRequestImageBytes: 20971520,
|
|
37
|
+
requestImagePixelBudget: 4194304,
|
|
38
|
+
requestImageMaxBytes: 1048576
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Inert pi-ai auth plane. The workbuddy route authenticates only through the
|
|
42
|
+
* shim shared secret resolved per request by `resolveApiKey`, so pi-ai's own
|
|
43
|
+
* credential lifecycle and ambient discovery must never manufacture a
|
|
44
|
+
* credential for it. `PiAiAdapterOptions.auth` is required since 0.1.1-rc.2;
|
|
45
|
+
* every ambient question here answers "nothing stored, nothing set".
|
|
46
|
+
*/
|
|
47
|
+
const INERT_AUTH = {
|
|
48
|
+
credentials: {
|
|
49
|
+
async read() {},
|
|
50
|
+
async list() {
|
|
51
|
+
return [];
|
|
52
|
+
},
|
|
53
|
+
async modify() {
|
|
54
|
+
throw new Error("dsh-connect-workbuddy: the workbuddy route has no pi-ai credential lifecycle");
|
|
55
|
+
},
|
|
56
|
+
async delete() {}
|
|
57
|
+
},
|
|
58
|
+
authContext: {
|
|
59
|
+
async env() {},
|
|
60
|
+
async fileExists() {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
/** No per-token pricing is knowable for a subscription quota; report zero. */
|
|
66
|
+
const NO_COST = {
|
|
67
|
+
input: 0,
|
|
68
|
+
output: 0,
|
|
69
|
+
cacheRead: 0,
|
|
70
|
+
cacheWrite: 0
|
|
71
|
+
};
|
|
72
|
+
const THINKING_LEVELS = [
|
|
73
|
+
"minimal",
|
|
74
|
+
"low",
|
|
75
|
+
"medium",
|
|
76
|
+
"high",
|
|
77
|
+
"xhigh",
|
|
78
|
+
"max"
|
|
79
|
+
];
|
|
80
|
+
/** pi-ai input modalities: images only when WorkBuddy advertises them. */
|
|
81
|
+
function workBuddyModelInput(info) {
|
|
82
|
+
return info.multimodal === true ? ["text", "image"] : ["text"];
|
|
83
|
+
}
|
|
84
|
+
/** Map only levels advertised by WorkBuddy; undeclared DSH levels stay unavailable. */
|
|
85
|
+
function workBuddyThinkingLevelMap(info) {
|
|
86
|
+
const supported = info.reasoning?.supportedEfforts?.filter((effort) => THINKING_LEVELS.includes(effort));
|
|
87
|
+
if (supported === void 0 || supported.length === 0) return void 0;
|
|
88
|
+
const map = Object.fromEntries(THINKING_LEVELS.map((level) => [level, supported.includes(level) ? level : null]));
|
|
89
|
+
if (info.reasoning?.canDisableThinking !== true) map.off = null;
|
|
90
|
+
return map;
|
|
91
|
+
}
|
|
92
|
+
/** Build one pi-ai model descriptor pointing at the loopback shim. */
|
|
93
|
+
function toPiModel(info, baseUrl) {
|
|
94
|
+
const thinkingLevelMap = workBuddyThinkingLevelMap(info);
|
|
95
|
+
return {
|
|
96
|
+
id: info.id,
|
|
97
|
+
name: info.name,
|
|
98
|
+
api: "openai-completions",
|
|
99
|
+
provider: WORKBUDDY_PROVIDER,
|
|
100
|
+
baseUrl,
|
|
101
|
+
input: workBuddyModelInput(info),
|
|
102
|
+
cost: NO_COST,
|
|
103
|
+
contextWindow: info.contextWindow,
|
|
104
|
+
maxTokens: info.maxTokens,
|
|
105
|
+
reasoning: thinkingLevelMap !== void 0,
|
|
106
|
+
...thinkingLevelMap === void 0 ? {} : { thinkingLevelMap },
|
|
107
|
+
compat: { supportsReasoningEffort: thinkingLevelMap !== void 0 }
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Assemble the adapter. The provider's `getModels` reads the live catalog,
|
|
112
|
+
* and every model's `baseUrl` is re-resolved per read so the shim's
|
|
113
|
+
* ephemeral port applies from the first snapshot after startup.
|
|
114
|
+
*/
|
|
115
|
+
function createWorkBuddyAdapter(options) {
|
|
116
|
+
const { shim, store, catalog, resolveAttachments } = options;
|
|
117
|
+
const buildModels = () => {
|
|
118
|
+
const baseUrl = `${shim.baseUrl()}/v1`;
|
|
119
|
+
return catalog.current().map((info) => toPiModel(info, baseUrl));
|
|
120
|
+
};
|
|
121
|
+
const provider = {
|
|
122
|
+
...createProvider({
|
|
123
|
+
id: WORKBUDDY_PROVIDER,
|
|
124
|
+
name: "WorkBuddy",
|
|
125
|
+
auth: { apiKey: {
|
|
126
|
+
name: "WorkBuddy OAuth bearer token",
|
|
127
|
+
async resolve({ credential }) {
|
|
128
|
+
const apiKey = credential?.key;
|
|
129
|
+
return apiKey === void 0 || apiKey.length === 0 ? void 0 : {
|
|
130
|
+
auth: { apiKey },
|
|
131
|
+
source: "WorkBuddy"
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
} },
|
|
135
|
+
models: buildModels(),
|
|
136
|
+
api: openAICompletionsApi()
|
|
137
|
+
}),
|
|
138
|
+
getModels: () => buildModels()
|
|
139
|
+
};
|
|
140
|
+
const profile = {
|
|
141
|
+
provider: WORKBUDDY_PROVIDER,
|
|
142
|
+
displayName: "WorkBuddy",
|
|
143
|
+
streamIdleTimeoutMs: WORKBUDDY_STREAM_IDLE_TIMEOUT_MS,
|
|
144
|
+
retryPolicy: resolveRetryPolicy(void 0, "dsh-connect-workbuddy retryPolicy"),
|
|
145
|
+
configuredMaxTokens: /* @__PURE__ */ new Map(),
|
|
146
|
+
...REQUEST_IMAGE_BUDGETS,
|
|
147
|
+
piProvider: provider
|
|
148
|
+
};
|
|
149
|
+
let profiles = /* @__PURE__ */ new Map([[WORKBUDDY_PROVIDER, profile]]);
|
|
150
|
+
return {
|
|
151
|
+
adapter: new PiAiAdapter({
|
|
152
|
+
profiles: () => profiles,
|
|
153
|
+
auth: INERT_AUTH,
|
|
154
|
+
resolveApiKey: async () => shim.token(),
|
|
155
|
+
...resolveAttachments === void 0 ? {} : { resolveAttachments }
|
|
156
|
+
}),
|
|
157
|
+
invalidate: () => {
|
|
158
|
+
profiles = /* @__PURE__ */ new Map([[WORKBUDDY_PROVIDER, profile]]);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
//#endregion
|
|
163
|
+
//#region src/shim.ts
|
|
164
|
+
/**
|
|
165
|
+
* Loopback OpenAI-compatible endpoint. The pi-ai provider points here; the
|
|
166
|
+
* shim applies the WorkBuddy wire quirks (forced streaming, string
|
|
167
|
+
* `tool_choice`, CLI-shaped headers) and forwards to the real upstream.
|
|
168
|
+
* It binds 127.0.0.1 only and never serves another interface.
|
|
169
|
+
*
|
|
170
|
+
* 参考:corrinehu/dsh-workbuddy-connect(MIT,Copyright (c) 2026 Corrine Hu)
|
|
171
|
+
* — 入站加固的四重校验(Host 必须回环、Origin 必须回环、chat POST 必须
|
|
172
|
+
* JSON、bearer 必须匹配进程内随机 secret)、常量时间比对、
|
|
173
|
+
* 随机端口绑定、body 上限、上游错误分类到 HTTP 状态码的映射,
|
|
174
|
+
* 均由该项目设计并验证。
|
|
175
|
+
* 改动:无。安全相关代码不做「改善」,原样沿用。
|
|
176
|
+
|
|
177
|
+
* @module dsh-connect-workbuddy/shim
|
|
178
|
+
*/
|
|
179
|
+
const REQUEST_BODY_LIMIT = 67108864;
|
|
180
|
+
/** Loopback hostnames the shim's own in-process client uses. */
|
|
181
|
+
const LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
|
|
182
|
+
"127.0.0.1",
|
|
183
|
+
"localhost",
|
|
184
|
+
"[::1]"
|
|
185
|
+
]);
|
|
186
|
+
/** Strip the optional :port from a Host header value, IPv6-bracket aware. */
|
|
187
|
+
function hostnameOfHost(host) {
|
|
188
|
+
let hostname = host.trim().toLowerCase();
|
|
189
|
+
if (hostname.startsWith("[")) {
|
|
190
|
+
const end = hostname.indexOf("]");
|
|
191
|
+
return end === -1 ? hostname : hostname.slice(0, end + 1);
|
|
192
|
+
}
|
|
193
|
+
const colon = hostname.lastIndexOf(":");
|
|
194
|
+
if (colon !== -1 && /^\d+$/.test(hostname.slice(colon + 1))) hostname = hostname.slice(0, colon);
|
|
195
|
+
return hostname;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* The request's Host header must name the loopback interface. A DNS-rebinding
|
|
199
|
+
* page (attacker domain re-resolved to 127.0.0.1) sends its own domain in
|
|
200
|
+
* Host, so this check drops those before any routing happens.
|
|
201
|
+
*/
|
|
202
|
+
function hostIsLoopback(host) {
|
|
203
|
+
if (host === void 0 || host.trim() === "") return false;
|
|
204
|
+
return LOOPBACK_HOSTS.has(hostnameOfHost(host));
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* A browser-sent Origin (present header) must be loopback. Non-browser
|
|
208
|
+
* clients (the plugin's own fetch calls) send no Origin at all and pass.
|
|
209
|
+
*/
|
|
210
|
+
function originIsLoopback(origin) {
|
|
211
|
+
if (origin === void 0 || origin.trim() === "") return true;
|
|
212
|
+
try {
|
|
213
|
+
const { hostname } = new URL(origin);
|
|
214
|
+
return LOOPBACK_HOSTS.has(hostname) || hostname === "::1";
|
|
215
|
+
} catch {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/** Chat-completion POSTs must carry a JSON body type (simple-request CSRF drops here). */
|
|
220
|
+
function isJsonContentType(req) {
|
|
221
|
+
const type = req.headers["content-type"];
|
|
222
|
+
return typeof type === "string" && type.trim().toLowerCase().startsWith("application/json");
|
|
223
|
+
}
|
|
224
|
+
/** HTTP status each upstream failure class surfaces as. */
|
|
225
|
+
const KIND_STATUS = {
|
|
226
|
+
hard_credit: 402,
|
|
227
|
+
soft_rate: 429,
|
|
228
|
+
session_dead: 401,
|
|
229
|
+
not_found: 502,
|
|
230
|
+
server: 502,
|
|
231
|
+
client: 400
|
|
232
|
+
};
|
|
233
|
+
function writeJson(res, status, body) {
|
|
234
|
+
const payload = JSON.stringify(body);
|
|
235
|
+
res.writeHead(status, {
|
|
236
|
+
"Content-Type": "application/json",
|
|
237
|
+
"Content-Length": Buffer.byteLength(payload)
|
|
238
|
+
});
|
|
239
|
+
res.end(payload);
|
|
240
|
+
}
|
|
241
|
+
function writeOpenAIError(res, status, kind, message) {
|
|
242
|
+
writeJson(res, status, { error: {
|
|
243
|
+
message,
|
|
244
|
+
type: kind,
|
|
245
|
+
code: kind
|
|
246
|
+
} });
|
|
247
|
+
}
|
|
248
|
+
/** Read a request body with a size cap; over-limit bodies fail the request. */
|
|
249
|
+
function readBody(req) {
|
|
250
|
+
return new Promise((resolve, reject) => {
|
|
251
|
+
const chunks = [];
|
|
252
|
+
let size = 0;
|
|
253
|
+
req.on("data", (chunk) => {
|
|
254
|
+
size += chunk.length;
|
|
255
|
+
if (size > REQUEST_BODY_LIMIT) {
|
|
256
|
+
reject(/* @__PURE__ */ new Error("request body too large"));
|
|
257
|
+
req.destroy();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
chunks.push(chunk);
|
|
261
|
+
});
|
|
262
|
+
req.on("end", () => resolve(Buffer.concat(chunks)));
|
|
263
|
+
req.on("error", reject);
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Start the loopback endpoint. Requests must carry the shim's shared secret;
|
|
268
|
+
* the loopback bind alone is not a trust boundary.
|
|
269
|
+
*/
|
|
270
|
+
function createWorkBuddyShim(options) {
|
|
271
|
+
const { store, client, catalog } = options;
|
|
272
|
+
const logger = options.logger;
|
|
273
|
+
const SHARED_SECRET = randomBytes(32).toString("base64url");
|
|
274
|
+
/** Constant-time bearer check; absent or mismatched bearers are rejected. */
|
|
275
|
+
function bearerOk(req) {
|
|
276
|
+
const header = req.headers.authorization;
|
|
277
|
+
if (typeof header !== "string") return false;
|
|
278
|
+
const match = /^Bearer\s+(.+)$/i.exec(header.trim());
|
|
279
|
+
if (match === null) return false;
|
|
280
|
+
const presented = match[1];
|
|
281
|
+
const expected = SHARED_SECRET;
|
|
282
|
+
const a = Buffer.from(presented);
|
|
283
|
+
const b = Buffer.from(expected);
|
|
284
|
+
if (a.length !== b.length) return false;
|
|
285
|
+
return timingSafeEqual(a, b);
|
|
286
|
+
}
|
|
287
|
+
const server = createServer((req, res) => {
|
|
288
|
+
handle(req, res);
|
|
289
|
+
});
|
|
290
|
+
const ready = new Promise((resolve, reject) => {
|
|
291
|
+
server.once("listening", () => resolve());
|
|
292
|
+
server.once("error", reject);
|
|
293
|
+
});
|
|
294
|
+
server.listen(0, "127.0.0.1");
|
|
295
|
+
const baseUrl = () => {
|
|
296
|
+
const address = server.address();
|
|
297
|
+
if (address === null || typeof address === "string") throw new Error("workbuddy shim has no listening address");
|
|
298
|
+
return `http://127.0.0.1:${address.port}`;
|
|
299
|
+
};
|
|
300
|
+
async function handle(req, res) {
|
|
301
|
+
try {
|
|
302
|
+
if (!hostIsLoopback(req.headers.host)) {
|
|
303
|
+
writeOpenAIError(res, 403, "host_not_allowed", "Host header must name the loopback interface");
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (!originIsLoopback(req.headers.origin)) {
|
|
307
|
+
writeOpenAIError(res, 403, "origin_not_allowed", "Origin must be a loopback origin");
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (!bearerOk(req)) {
|
|
311
|
+
writeOpenAIError(res, 401, "unauthorized", "missing or invalid Authorization bearer");
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const url = req.url ?? "/";
|
|
315
|
+
if (req.method === "GET" && (url === "/healthz" || url === "/healthz/")) {
|
|
316
|
+
writeJson(res, 200, { ok: true });
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (req.method === "GET" && (url === "/v1/models" || url === "/v1/models/")) {
|
|
320
|
+
writeJson(res, 200, {
|
|
321
|
+
object: "list",
|
|
322
|
+
data: catalog.current().map((model) => ({
|
|
323
|
+
id: model.id,
|
|
324
|
+
object: "model",
|
|
325
|
+
created: 0,
|
|
326
|
+
owned_by: "workbuddy"
|
|
327
|
+
}))
|
|
328
|
+
});
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (req.method === "POST" && (url === "/v1/chat/completions" || url === "/v1/chat/completions/")) {
|
|
332
|
+
await chatCompletions(req, res);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
writeOpenAIError(res, 404, "not_found", `no such route: ${req.method} ${url}`);
|
|
336
|
+
} catch (error) {
|
|
337
|
+
if (!res.headersSent) writeOpenAIError(res, 500, "internal", String(error));
|
|
338
|
+
else res.end();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
async function chatCompletions(req, res) {
|
|
342
|
+
if (!isJsonContentType(req)) {
|
|
343
|
+
writeOpenAIError(res, 415, "unsupported_media_type", "Content-Type must be application/json");
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
let credential;
|
|
347
|
+
try {
|
|
348
|
+
credential = await store.resolve();
|
|
349
|
+
} catch (error) {
|
|
350
|
+
writeOpenAIError(res, 401, "not_signed_in", String(error));
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const raw = (await readBody(req)).toString("utf8");
|
|
354
|
+
const prepared = prepareChatBody(raw);
|
|
355
|
+
const controller = new AbortController();
|
|
356
|
+
req.on("close", () => controller.abort());
|
|
357
|
+
const result = await client.chatStream(credential, prepared, controller.signal);
|
|
358
|
+
if (!result.ok) {
|
|
359
|
+
writeOpenAIError(res, KIND_STATUS[result.kind], result.kind, `workbuddy upstream ${result.kind} (http ${result.status}): ${result.message.slice(0, 400)}`);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
res.writeHead(200, {
|
|
363
|
+
"Content-Type": "text/event-stream",
|
|
364
|
+
"Cache-Control": "no-cache",
|
|
365
|
+
"Connection": "keep-alive",
|
|
366
|
+
"X-Accel-Buffering": "no"
|
|
367
|
+
});
|
|
368
|
+
let sawDone = false;
|
|
369
|
+
const body = Readable.fromWeb(result.response.body);
|
|
370
|
+
body.on("data", (chunk) => {
|
|
371
|
+
if (chunk.includes("[DONE]")) sawDone = true;
|
|
372
|
+
});
|
|
373
|
+
body.on("error", (error) => {
|
|
374
|
+
logger?.warn("dsh-connect-workbuddy: upstream stream failed mid-flight", error);
|
|
375
|
+
if (!sawDone && res.writable) res.end("data: [DONE]\n\n");
|
|
376
|
+
});
|
|
377
|
+
body.pipe(res);
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
ready,
|
|
381
|
+
baseUrl,
|
|
382
|
+
token: () => SHARED_SECRET,
|
|
383
|
+
close: () => new Promise((resolve, reject) => {
|
|
384
|
+
server.close(() => resolve());
|
|
385
|
+
server.closeAllConnections();
|
|
386
|
+
server.once("error", reject);
|
|
387
|
+
})
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
//#endregion
|
|
391
|
+
//#region src/status-paths.ts
|
|
392
|
+
/**
|
|
393
|
+
* Node-free constants and types shared by the Host and browser halves.
|
|
394
|
+
*
|
|
395
|
+
* 参考:dingminhua/dsh-connect-trae(MIT,Copyright (c) 2026 LaoDing)
|
|
396
|
+
* — 「3 条同源只读路由(usage / models:refresh / accounts:refresh)+ 一份
|
|
397
|
+
* 与浏览器共享的 node-free 类型定义」的 host↔client 桥梁形态来自该项目
|
|
398
|
+
* (其 `status-paths.ts` 亦如此,并注明沿用
|
|
399
|
+
* corrinehu/dsh-workbuddy-connect 的 status-route 模式)。
|
|
400
|
+
* 改动:路由路径改用本插件 id;类型字段按 WorkBuddy 上游实际给出的能力
|
|
401
|
+
* (积分倍率、多模态、推理档位)调整,不保留 trae 的 1M 变体字段。
|
|
402
|
+
*
|
|
403
|
+
* @module dsh-connect-workbuddy/status-paths
|
|
404
|
+
*/
|
|
405
|
+
/** Plugin-owned usage endpoint consumed by its browser half. */
|
|
406
|
+
const WORKBUDDY_USAGE_PATH = "/plugins/dsh-connect-workbuddy/usage";
|
|
407
|
+
/** Plugin-owned live model refresh endpoint. */
|
|
408
|
+
const WORKBUDDY_MODELS_REFRESH_PATH = "/plugins/dsh-connect-workbuddy/models/refresh";
|
|
409
|
+
/** Plugin-owned local account rescan endpoint. */
|
|
410
|
+
const WORKBUDDY_ACCOUNTS_REFRESH_PATH = "/plugins/dsh-connect-workbuddy/accounts/refresh";
|
|
411
|
+
/** Plugin-owned daily check-in action endpoint. */
|
|
412
|
+
const WORKBUDDY_CHECKIN_PATH = "/plugins/dsh-connect-workbuddy/checkin";
|
|
413
|
+
//#endregion
|
|
414
|
+
//#region src/web-status.ts
|
|
415
|
+
/** Redact token-like content before it crosses to the browser. */
|
|
416
|
+
function safeMessage(error) {
|
|
417
|
+
return (error instanceof Error ? error.message : String(error)).replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/gu, "[redacted token]").replace(/(\b(?:code|token|refresh_token|access_token)=)[^&\s]+/giu, "$1[redacted]").slice(0, 500);
|
|
418
|
+
}
|
|
419
|
+
function json(res, status, body) {
|
|
420
|
+
const payload = JSON.stringify(body);
|
|
421
|
+
res.writeHead(status, {
|
|
422
|
+
"Content-Type": "application/json",
|
|
423
|
+
"Content-Length": Buffer.byteLength(payload)
|
|
424
|
+
});
|
|
425
|
+
res.end(payload);
|
|
426
|
+
}
|
|
427
|
+
/** Loopback browser origins only; other devices are refused until trusted origins exist. */
|
|
428
|
+
function loopbackOrigin(req) {
|
|
429
|
+
const origin = req.headers.origin;
|
|
430
|
+
if (origin === void 0) return true;
|
|
431
|
+
try {
|
|
432
|
+
const { hostname } = new URL(origin);
|
|
433
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname === "::1";
|
|
434
|
+
} catch {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
/** Map the credit answer to the card's compact document. */
|
|
439
|
+
function toCredits(answer) {
|
|
440
|
+
return {
|
|
441
|
+
total: answer.total,
|
|
442
|
+
packages: answer.packages.map((pack) => ({
|
|
443
|
+
packageName: pack.packageName,
|
|
444
|
+
remain: pack.remain,
|
|
445
|
+
size: pack.size,
|
|
446
|
+
monthly: pack.monthly,
|
|
447
|
+
...pack.refreshAtMs === void 0 ? {} : { cycleRefreshMs: pack.refreshAtMs },
|
|
448
|
+
...pack.expiresAtMs === void 0 ? {} : { expiresAtMs: pack.expiresAtMs }
|
|
449
|
+
})),
|
|
450
|
+
expiringSoon: answer.expiringSoon,
|
|
451
|
+
...answer.nearestExpiryMs === void 0 ? {} : { nearestExpiryMs: answer.nearestExpiryMs }
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
/** Project a model into the card's row, dropping empty optional fields. */
|
|
455
|
+
function toWebModel(model, budgets) {
|
|
456
|
+
return {
|
|
457
|
+
id: model.id,
|
|
458
|
+
name: model.name,
|
|
459
|
+
contextWindow: model.contextWindow > 2e5 ? Math.min(model.contextWindow, budgets[model.id] ?? 2e5) : model.contextWindow,
|
|
460
|
+
nativeContextWindow: model.contextWindow,
|
|
461
|
+
maxTokens: model.maxTokens,
|
|
462
|
+
...model.creditMultiplier === void 0 ? {} : { creditMultiplier: model.creditMultiplier },
|
|
463
|
+
...model.multimodal === void 0 ? {} : { multimodal: model.multimodal },
|
|
464
|
+
...model.reasoning === void 0 ? {} : { reasoning: {
|
|
465
|
+
...model.reasoning.supportedEfforts === void 0 ? {} : { supportedEfforts: [...model.reasoning.supportedEfforts] },
|
|
466
|
+
...model.reasoning.defaultEffort === void 0 ? {} : { defaultEffort: model.reasoning.defaultEffort }
|
|
467
|
+
} }
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
/** Project a store account into the card's token-free account row. */
|
|
471
|
+
function toWebAccount(account) {
|
|
472
|
+
return {
|
|
473
|
+
id: account.id,
|
|
474
|
+
accountName: account.accountName,
|
|
475
|
+
...account.uin === void 0 ? {} : { uin: account.uin },
|
|
476
|
+
domain: account.domain,
|
|
477
|
+
source: account.source,
|
|
478
|
+
tokenExpiresAtMs: account.tokenExpiresAtMs,
|
|
479
|
+
selected: account.selected
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Assemble the card's usage document. Sign-in state is read-only; credit is a
|
|
484
|
+
* live billing answer whose failure degrades to `creditsError` rather than
|
|
485
|
+
* failing the whole document.
|
|
486
|
+
*/
|
|
487
|
+
async function workBuddyWebStatus(deps) {
|
|
488
|
+
const accounts = await deps.store.accounts();
|
|
489
|
+
if ((await deps.store.status()).state !== "signed-out" && accounts.length === 0) return {
|
|
490
|
+
status: "signed-out",
|
|
491
|
+
accounts: []
|
|
492
|
+
};
|
|
493
|
+
let credential;
|
|
494
|
+
try {
|
|
495
|
+
credential = await deps.store.resolve();
|
|
496
|
+
} catch (error) {
|
|
497
|
+
return {
|
|
498
|
+
status: "signed-out",
|
|
499
|
+
accounts: accounts.map(toWebAccount),
|
|
500
|
+
message: safeMessage(error)
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
const account = {
|
|
504
|
+
accountId: accounts.find((account) => account.selected)?.id ?? "",
|
|
505
|
+
accountName: credential.nickname ?? credential.uin ?? credential.uid,
|
|
506
|
+
...credential.uin === void 0 ? {} : { uin: credential.uin },
|
|
507
|
+
...credential.domain === "" ? {} : { domain: credential.domain },
|
|
508
|
+
source: credential.source,
|
|
509
|
+
tokenExpiresAtMs: credential.expiresAtMs,
|
|
510
|
+
accounts: accounts.map(toWebAccount),
|
|
511
|
+
models: deps.displayModels().map((model) => toWebModel(model, deps.contextBudgets())),
|
|
512
|
+
enabledModelIds: [...deps.enabledModelIds()],
|
|
513
|
+
imageModelIds: [...deps.imageModelIds()]
|
|
514
|
+
};
|
|
515
|
+
const [creditsResult, checkinResult] = await Promise.allSettled([deps.client.fetchCredits(credential), deps.client.fetchCheckinStatus(credential)]);
|
|
516
|
+
return {
|
|
517
|
+
status: "signed-in",
|
|
518
|
+
...account,
|
|
519
|
+
...creditsResult.status === "fulfilled" ? { credits: toCredits(creditsResult.value) } : { creditsError: safeMessage(creditsResult.reason) },
|
|
520
|
+
...checkinResult.status === "fulfilled" ? { checkin: checkinResult.value } : { checkinError: safeMessage(checkinResult.reason) }
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Mount the read-only routes on a context where `webServer` is available.
|
|
525
|
+
* The caller uses `ctx.inject(['webServer'], ...)`, so Desktop startup order
|
|
526
|
+
* cannot make this registration disappear.
|
|
527
|
+
*/
|
|
528
|
+
function registerWorkBuddyStatusRoute(ctx, deps) {
|
|
529
|
+
ctx.effect(() => {
|
|
530
|
+
const disposeUsage = ctx.webServer.register({
|
|
531
|
+
kind: "exact",
|
|
532
|
+
path: WORKBUDDY_USAGE_PATH,
|
|
533
|
+
handler: async (req, res) => {
|
|
534
|
+
if (req.method !== "GET") {
|
|
535
|
+
json(res, 405, { error: "method not allowed" });
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
if (!loopbackOrigin(req)) {
|
|
539
|
+
json(res, 403, { error: "origin-not-trusted" });
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
try {
|
|
543
|
+
json(res, 200, await workBuddyWebStatus(deps));
|
|
544
|
+
} catch (error) {
|
|
545
|
+
json(res, 500, { error: safeMessage(error) });
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
const disposeAccounts = ctx.webServer.register({
|
|
550
|
+
kind: "exact",
|
|
551
|
+
path: WORKBUDDY_ACCOUNTS_REFRESH_PATH,
|
|
552
|
+
handler: async (req, res) => {
|
|
553
|
+
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
554
|
+
if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
|
|
555
|
+
try {
|
|
556
|
+
json(res, 200, { accounts: (await deps.store.accounts()).map(toWebAccount) });
|
|
557
|
+
} catch (error) {
|
|
558
|
+
json(res, 500, { error: safeMessage(error) });
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
const disposeCheckin = ctx.webServer.register({
|
|
563
|
+
kind: "exact",
|
|
564
|
+
path: WORKBUDDY_CHECKIN_PATH,
|
|
565
|
+
handler: async (req, res) => {
|
|
566
|
+
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
567
|
+
if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
|
|
568
|
+
try {
|
|
569
|
+
const credential = await deps.store.resolve();
|
|
570
|
+
const current = await deps.client.fetchCheckinStatus(credential);
|
|
571
|
+
if (!current.active) return json(res, 409, { error: "check-in activity is not active" });
|
|
572
|
+
if (current.todayCheckedIn) return json(res, 200, {
|
|
573
|
+
alreadyCheckedIn: true,
|
|
574
|
+
checkin: current
|
|
575
|
+
});
|
|
576
|
+
json(res, 200, {
|
|
577
|
+
alreadyCheckedIn: false,
|
|
578
|
+
claim: await deps.client.claimDailyCheckin(credential),
|
|
579
|
+
checkin: await deps.client.fetchCheckinStatus(credential)
|
|
580
|
+
});
|
|
581
|
+
} catch (error) {
|
|
582
|
+
json(res, 500, { error: safeMessage(error) });
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
const disposeRefresh = ctx.webServer.register({
|
|
587
|
+
kind: "exact",
|
|
588
|
+
path: WORKBUDDY_MODELS_REFRESH_PATH,
|
|
589
|
+
handler: async (req, res) => {
|
|
590
|
+
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
591
|
+
if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
|
|
592
|
+
if (deps.discoverModels === void 0) return json(res, 503, { error: "model refresh unavailable" });
|
|
593
|
+
try {
|
|
594
|
+
json(res, 200, { models: (await deps.discoverModels()).map((model) => toWebModel(model, deps.contextBudgets())) });
|
|
595
|
+
} catch (error) {
|
|
596
|
+
json(res, 500, { error: safeMessage(error) });
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
return () => {
|
|
601
|
+
disposeRefresh();
|
|
602
|
+
disposeCheckin();
|
|
603
|
+
disposeAccounts();
|
|
604
|
+
disposeUsage();
|
|
605
|
+
};
|
|
606
|
+
}, "dsh-connect-workbuddy: Web status route");
|
|
607
|
+
}
|
|
608
|
+
//#endregion
|
|
609
|
+
//#region src/index.ts
|
|
610
|
+
/** Stable Cordis plugin name. */
|
|
611
|
+
const name = "dsh-connect-workbuddy";
|
|
612
|
+
/** The model registry required before the provider can register. */
|
|
613
|
+
const inject = ["llm"];
|
|
614
|
+
/** Settings namespace for the plugin configuration card. */
|
|
615
|
+
const WORKBUDDY_SETTINGS_NS = settingsNamespace("workbuddy");
|
|
616
|
+
const modelConfig = z.object({
|
|
617
|
+
id: z.string().required(),
|
|
618
|
+
name: z.string().required(),
|
|
619
|
+
contextWindow: z.number().step(1).min(1),
|
|
620
|
+
maxTokens: z.number().step(1).min(1)
|
|
621
|
+
});
|
|
622
|
+
const Config = z.object({
|
|
623
|
+
authFile: z.string().description("WorkBuddy desktop auth file (defaults to the app's own location)"),
|
|
624
|
+
accountId: z.string().description("Selected local WorkBuddy account id (never a token)"),
|
|
625
|
+
lastCatalog: z.array(modelConfig).description("Last refreshed WorkBuddy model directory shown by the plugin card"),
|
|
626
|
+
enabledModelIds: z.array(z.string()).default([]).description("WorkBuddy model ids the user enabled"),
|
|
627
|
+
imageModelIds: z.array(z.string()).default([]).description("WorkBuddy model ids the user opted into image input"),
|
|
628
|
+
contextBudgets: z.dict(z.number().step(1).min(1)).default({}).description("Local DSH context budget per WorkBuddy model")
|
|
629
|
+
});
|
|
630
|
+
/**
|
|
631
|
+
* Start the loopback endpoint, register the `workbuddy` provider, and
|
|
632
|
+
* refresh the model catalog from the upstream once credentials allow it.
|
|
633
|
+
* The static fallback catalog serves from the first moment, so an offline
|
|
634
|
+
* upstream never leaves the provider empty.
|
|
635
|
+
*/
|
|
636
|
+
function apply(ctx, config) {
|
|
637
|
+
const client = new WorkBuddyUpstreamClient();
|
|
638
|
+
const store = new WorkBuddyCredentialStore({
|
|
639
|
+
...config.authFile === void 0 ? {} : { desktopPath: config.authFile },
|
|
640
|
+
refresh: (credential) => client.refreshToken(credential)
|
|
641
|
+
});
|
|
642
|
+
if (config.accountId !== void 0) store.selectAccount(config.accountId);
|
|
643
|
+
const catalog = new WorkBuddyCatalog();
|
|
644
|
+
const shim = createWorkBuddyShim({
|
|
645
|
+
store,
|
|
646
|
+
client,
|
|
647
|
+
catalog,
|
|
648
|
+
logger: ctx.logger
|
|
649
|
+
});
|
|
650
|
+
const enabledSet = (value) => new Set(value.enabledModelIds ?? []);
|
|
651
|
+
const imageSet = (value) => new Set(value.imageModelIds ?? []);
|
|
652
|
+
const withImageSelection = (models, images) => models.map((model) => ({
|
|
653
|
+
...model,
|
|
654
|
+
...images.has(model.id) ? { multimodal: true } : { multimodal: false }
|
|
655
|
+
}));
|
|
656
|
+
const configuredModels = (value) => withImageSelection(deriveCatalog(value.lastCatalog?.length ? value.lastCatalog : FALLBACK_WORKBUDDY_MODELS, enabledSet(value), value.contextBudgets ?? {}), imageSet(value));
|
|
657
|
+
const displayModels = (value) => value.lastCatalog?.length ? value.lastCatalog : FALLBACK_WORKBUDDY_MODELS;
|
|
658
|
+
let current = () => config;
|
|
659
|
+
const discoverModels = async (signal) => {
|
|
660
|
+
const credential = await store.resolve();
|
|
661
|
+
return client.fetchModels(credential, signal);
|
|
662
|
+
};
|
|
663
|
+
ctx.inject(["webServer"], (webCtx) => registerWorkBuddyStatusRoute(webCtx, {
|
|
664
|
+
store,
|
|
665
|
+
client,
|
|
666
|
+
displayModels: () => displayModels(current()),
|
|
667
|
+
enabledModelIds: () => current().enabledModelIds ?? [],
|
|
668
|
+
imageModelIds: () => current().imageModelIds ?? [],
|
|
669
|
+
contextBudgets: () => current().contextBudgets ?? {},
|
|
670
|
+
discoverModels
|
|
671
|
+
}));
|
|
672
|
+
installSettingsSection(ctx, WORKBUDDY_SETTINGS_NS, Config, config, {
|
|
673
|
+
setSource(source) {
|
|
674
|
+
current = source;
|
|
675
|
+
},
|
|
676
|
+
onChange() {
|
|
677
|
+
const next = current();
|
|
678
|
+
store.setDesktopPath(next.authFile);
|
|
679
|
+
store.selectAccount(next.accountId);
|
|
680
|
+
catalog.set(configuredModels(next));
|
|
681
|
+
invalidateCatalog();
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
let stopped = false;
|
|
685
|
+
let invalidateCatalog = () => {};
|
|
686
|
+
ctx.effect(() => () => {
|
|
687
|
+
stopped = true;
|
|
688
|
+
shim.close();
|
|
689
|
+
clearHostHeartbeat();
|
|
690
|
+
});
|
|
691
|
+
/**
|
|
692
|
+
* Prefer an account with usable credit. Every account yields the same
|
|
693
|
+
* catalog, but an account at zero credit fails each chat request, so the
|
|
694
|
+
* default must not land there.
|
|
695
|
+
*/
|
|
696
|
+
const selectPreferredCreditAccount = async () => {
|
|
697
|
+
try {
|
|
698
|
+
const accounts = await store.accounts();
|
|
699
|
+
const scored = [];
|
|
700
|
+
for (const account of accounts) try {
|
|
701
|
+
store.selectAccount(account.id);
|
|
702
|
+
const credential = await store.resolve();
|
|
703
|
+
const credits = await client.fetchCredits(credential);
|
|
704
|
+
scored.push({
|
|
705
|
+
id: account.id,
|
|
706
|
+
credits: credits.total
|
|
707
|
+
});
|
|
708
|
+
} catch (error) {
|
|
709
|
+
ctx.logger.warn(`dsh-connect-workbuddy: account ${account.id} unavailable for default selection`, error);
|
|
710
|
+
}
|
|
711
|
+
scored.sort((a, b) => b.credits - a.credits);
|
|
712
|
+
store.setPreferAccountIds(scored.filter((item) => item.credits > 0).map((item) => item.id));
|
|
713
|
+
store.selectAccount(current().accountId);
|
|
714
|
+
} catch (error) {
|
|
715
|
+
ctx.logger.warn("dsh-connect-workbuddy: default account selection failed; continuing", error);
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
shim.ready.then(async () => {
|
|
719
|
+
if (stopped) return;
|
|
720
|
+
let invalidate;
|
|
721
|
+
try {
|
|
722
|
+
const workbuddy = createWorkBuddyAdapter({
|
|
723
|
+
shim,
|
|
724
|
+
store,
|
|
725
|
+
catalog,
|
|
726
|
+
resolveAttachments: () => ctx.get("attachments")
|
|
727
|
+
});
|
|
728
|
+
invalidate = workbuddy.invalidate;
|
|
729
|
+
invalidateCatalog = () => {
|
|
730
|
+
workbuddy.invalidate();
|
|
731
|
+
};
|
|
732
|
+
let releaseAdapter;
|
|
733
|
+
let releaseDirectory;
|
|
734
|
+
try {
|
|
735
|
+
releaseAdapter = ctx.llm.registerAdapter([WORKBUDDY_PROVIDER], workbuddy.adapter);
|
|
736
|
+
releaseDirectory = ctx.llm.registerConfigurableProviders([{
|
|
737
|
+
provider: WORKBUDDY_PROVIDER,
|
|
738
|
+
displayName: "WorkBuddy",
|
|
739
|
+
settingsNs: WORKBUDDY_SETTINGS_NS,
|
|
740
|
+
settingsPath: [],
|
|
741
|
+
declared: false
|
|
742
|
+
}]);
|
|
743
|
+
} finally {
|
|
744
|
+
if (releaseAdapter === void 0 || releaseDirectory === void 0) {
|
|
745
|
+
releaseAdapter?.();
|
|
746
|
+
releaseDirectory?.();
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
try {
|
|
750
|
+
ctx.effect(() => () => {
|
|
751
|
+
releaseAdapter?.();
|
|
752
|
+
releaseDirectory?.();
|
|
753
|
+
});
|
|
754
|
+
} catch {
|
|
755
|
+
releaseAdapter?.();
|
|
756
|
+
releaseDirectory?.();
|
|
757
|
+
}
|
|
758
|
+
ctx.llm.registerModelDiscovery(WORKBUDDY_SETTINGS_NS, async (request) => {
|
|
759
|
+
if (request.provider !== "workbuddy") return [];
|
|
760
|
+
return withImageSelection(deriveCatalog(await discoverModels(request.signal), enabledSet(current()), current().contextBudgets ?? {}), imageSet(current())).map((model) => ({
|
|
761
|
+
id: model.id,
|
|
762
|
+
name: model.name,
|
|
763
|
+
contextWindow: model.contextWindow,
|
|
764
|
+
maxTokens: model.maxTokens,
|
|
765
|
+
inputModalities: workBuddyModelInput(model)
|
|
766
|
+
}));
|
|
767
|
+
});
|
|
768
|
+
writeHostHeartbeat();
|
|
769
|
+
} catch (error) {
|
|
770
|
+
ctx.logger.error("dsh-connect-workbuddy: provider registration failed", error);
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
await selectPreferredCreditAccount();
|
|
774
|
+
if (stopped) return;
|
|
775
|
+
(async () => {
|
|
776
|
+
try {
|
|
777
|
+
const credential = await store.resolve();
|
|
778
|
+
if (stopped) return;
|
|
779
|
+
const models = await client.fetchModels(credential);
|
|
780
|
+
if (stopped) return;
|
|
781
|
+
catalog.set(withImageSelection(deriveCatalog(models, enabledSet(current()), current().contextBudgets ?? {}), imageSet(current())));
|
|
782
|
+
invalidate?.();
|
|
783
|
+
} catch (error) {
|
|
784
|
+
ctx.logger.warn("dsh-connect-workbuddy: dynamic model catalog unavailable; serving the static fallback list", error);
|
|
785
|
+
}
|
|
786
|
+
})();
|
|
787
|
+
}).catch((error) => {
|
|
788
|
+
ctx.logger.error("dsh-connect-workbuddy: loopback endpoint failed to start; provider not registered", error);
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
//#endregion
|
|
792
|
+
export { Config, FALLBACK_WORKBUDDY_MODELS, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, WorkBuddyCatalog, WorkBuddyCredentialStore, WorkBuddyUpstreamClient, apply, authFileName, classifyUpstreamError, clearHostHeartbeat, createWorkBuddyAdapter, createWorkBuddyShim, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, deriveCatalog, inject, isHeartbeatProcessAlive, name, parseCreditMultiplier, parseReasoning, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, processStartTimeMs, readHostHeartbeat, regionOf, registerWorkBuddyStatusRoute, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
|