dsh-connect-workbuddy 3.6.0 → 4.2.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 +78 -0
- package/README.en.md +4 -0
- package/README.md +4 -0
- package/docs/assets/dsh-connect-workbuddy-composer-credits.png +0 -0
- package/lib/bin.js +1 -1
- package/lib/client.js +1754 -27
- package/lib/{host-heartbeat-Cg2ahl1n.js → host-heartbeat-C4TXu8ew.js} +367 -25
- package/lib/index.d.ts +263 -2
- package/lib/index.js +895 -10
- package/package.json +1 -1
- package/screenshots.json +2 -1
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as isWorkbuddyBundle, A as workbuddyAccountId, B as parseCreditMultiplier, C as defaultDesktopAuthDirs, D as isVaultAccountId, E as isEncryptedCredentialError, F as WorkBuddyUpstreamClient, G as WORKBUDDY_APP_EXECUTABLE_ENV, H as parseUpstreamModel, I as classifyUpstreamError, J as deriveAtRestKeyId, K as clearAtRestKeyCache, L as declaredTools, M as CREDENTIAL_REJECTED_CODE, N as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, O as legacyWorkbuddyOwnAuthPath, P as WorkBuddyCredentialRejectedError, Q as isEncryptedFieldWrapper, R as isCredentialRejectedError, S as defaultDesktopAuthCandidates, T as hasEncryptedCredentialFields, U as prepareChatBody, V as parseReasoning, W as regionOf, X as findWorkbuddyAppExecutable, Y as fetchAtRestKeyPayload, _ as WORKBUDDY_AUTH_FILENAME, a as readHostHeartbeat, b as WorkBuddyEncryptedCredentialError, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, et as macosBundleExecutable, f as WorkBuddyCatalog, g as ENCRYPTED_CREDENTIAL_CODE, h as fallbackModelsFor, i as processStartTimeMs, it as workbuddyAppExecutableCandidates, j as workbuddyOwnAuthPath, k as parseWorkBuddyAuth, l as piAiRuntimeInfo, m as deriveCatalog, n as clearHostHeartbeat, nt as openEncryptedField, o as workbuddyHostHeartbeatPath, p as applyContextBudgets, q as deriveAtRestKey, r as isHeartbeatProcessAlive, rt as readAtRestKey, s as writeHostHeartbeat, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as macosNestedAppBundles, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, w as defaultDesktopAuthPath, x as authFileName, y as WorkBuddyCredentialStore, z as knownRegionOf } from "./host-heartbeat-C4TXu8ew.js";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
4
|
import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
@@ -2337,7 +2337,7 @@ async function checkinOne(target, deps) {
|
|
|
2337
2337
|
return {
|
|
2338
2338
|
...base,
|
|
2339
2339
|
status: "failed",
|
|
2340
|
-
message: messageOf(error)
|
|
2340
|
+
message: messageOf$1(error)
|
|
2341
2341
|
};
|
|
2342
2342
|
}
|
|
2343
2343
|
if (credential === void 0) return {
|
|
@@ -2367,7 +2367,7 @@ async function checkinOne(target, deps) {
|
|
|
2367
2367
|
return {
|
|
2368
2368
|
...base,
|
|
2369
2369
|
status: "failed",
|
|
2370
|
-
message: messageOf(error)
|
|
2370
|
+
message: messageOf$1(error)
|
|
2371
2371
|
};
|
|
2372
2372
|
}
|
|
2373
2373
|
}
|
|
@@ -2413,7 +2413,7 @@ async function testOne(target, modelId, deps) {
|
|
|
2413
2413
|
result: {
|
|
2414
2414
|
modelId,
|
|
2415
2415
|
outcome: "credential-rejected",
|
|
2416
|
-
message: messageOf(error)
|
|
2416
|
+
message: messageOf$1(error)
|
|
2417
2417
|
}
|
|
2418
2418
|
};
|
|
2419
2419
|
}
|
|
@@ -2477,7 +2477,7 @@ function defaultWait(ms) {
|
|
|
2477
2477
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2478
2478
|
}
|
|
2479
2479
|
/** A safe message from an unknown throwable, bounded in length. */
|
|
2480
|
-
function messageOf(error) {
|
|
2480
|
+
function messageOf$1(error) {
|
|
2481
2481
|
return (error instanceof Error ? error.message : String(error)).slice(0, 300);
|
|
2482
2482
|
}
|
|
2483
2483
|
/**
|
|
@@ -2689,6 +2689,469 @@ function resolveTargetModel(catalog, preferredId) {
|
|
|
2689
2689
|
};
|
|
2690
2690
|
}
|
|
2691
2691
|
//#endregion
|
|
2692
|
+
//#region src/oauth.ts
|
|
2693
|
+
/**
|
|
2694
|
+
* OAuth QR sign-in: add a WorkBuddy account by scanning a code, without the
|
|
2695
|
+
* desktop app.
|
|
2696
|
+
*
|
|
2697
|
+
* 参考:WorkBuddy OAuth 采集的通行实现(对照官网 cockpit 的
|
|
2698
|
+
* `oauth_start` / `oauth_poll`):
|
|
2699
|
+
* 向 `{billing_base}{API_PREFIX}/auth/state?platform={platform}` 申请一个
|
|
2700
|
+
* state,用户在浏览器打开授权页完成登录,客户端轮询
|
|
2701
|
+
* `{billing_base}{API_PREFIX}/auth/token?state={state}` 换取凭据,再以
|
|
2702
|
+
* Bearer 调 `{API_PREFIX}/login/account` 拉取账号资料。三步的 URL 形态、
|
|
2703
|
+
* 响应字段双拼写(camelCase/snake_case)容错、以及「区域必须绑定会话」的
|
|
2704
|
+
* 约定均沿用该实现。
|
|
2705
|
+
* 改动:端点按本插件 `upstream.ts` 的区域约定路由 —— CN 为
|
|
2706
|
+
* `https://www.codebuddy.cn`;国际版按凭据域分家(`workbuddy.ai` 与
|
|
2707
|
+
* `codebuddy.ai` 两把钥匙各自签发,混用会被网关拒绝),插件发起登录时
|
|
2708
|
+
* 还没有凭据,因此由用户显式传基址,缺省 `https://www.workbuddy.ai`。
|
|
2709
|
+
* 登录结果不落独立账号库,而是交给宿主写入统一凭据库(`src/auth.ts` 的
|
|
2710
|
+
* vault),与桌面端扫描到的账号共用一套发现/选择/刷新机制。
|
|
2711
|
+
*
|
|
2712
|
+
* 会话状态只在进程内(`LoginSession`):PKCE 类 state 与登录进度不落盘,
|
|
2713
|
+
* 拿到凭据即终结。刷新凭据仍走 `refreshToken`(上游 refresh 端点),此处
|
|
2714
|
+
* 只负责首次取凭据。
|
|
2715
|
+
*
|
|
2716
|
+
* @module dsh-connect-workbuddy/oauth
|
|
2717
|
+
*/
|
|
2718
|
+
/** The CN billing base; matches `upstream.ts`'s CN_BILLING_BASE. */
|
|
2719
|
+
const CN_BILLING_BASE = "https://www.codebuddy.cn";
|
|
2720
|
+
/** Default international base; the desktop app's gateway. */
|
|
2721
|
+
const GLOBAL_BILLING_BASE = "https://www.workbuddy.ai";
|
|
2722
|
+
/** Upstream plugin API prefix shared by both regions. */
|
|
2723
|
+
const API_PREFIX = "/v2/plugin";
|
|
2724
|
+
/** In-process session map. Deliberately not persisted: a restart cancels. */
|
|
2725
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
2726
|
+
/**
|
|
2727
|
+
* The billing base a login for `region` should start from.
|
|
2728
|
+
*
|
|
2729
|
+
* CN is single-homed. The international product signs in through either of
|
|
2730
|
+
* two brand domains (see `upstream.ts` `globalBase`); the caller may pin
|
|
2731
|
+
* one with `explicitBase` — a token issued at one domain is refused by the
|
|
2732
|
+
* other, so the choice must be made BEFORE the state is issued, not after.
|
|
2733
|
+
*/
|
|
2734
|
+
function oauthBillingBase(region, explicitBase) {
|
|
2735
|
+
if (region === "cn") return CN_BILLING_BASE;
|
|
2736
|
+
const trimmed = explicitBase?.trim();
|
|
2737
|
+
if (trimmed !== void 0 && trimmed !== "") return normalizeBase(trimmed);
|
|
2738
|
+
return GLOBAL_BILLING_BASE;
|
|
2739
|
+
}
|
|
2740
|
+
/** trim + strip one trailing slash; no scheme invented (see upstream.ts). */
|
|
2741
|
+
function normalizeBase(base) {
|
|
2742
|
+
return base.trim().replace(/\/+$/, "");
|
|
2743
|
+
}
|
|
2744
|
+
/**
|
|
2745
|
+
* Start one login: ask the upstream for a state and return the URL the user
|
|
2746
|
+
* opens to scan/confirm. The session is bound to `region` AND `base`;
|
|
2747
|
+
* both are checked on every poll.
|
|
2748
|
+
*
|
|
2749
|
+
* Never throws for an upstream refusal — the error becomes the returned
|
|
2750
|
+
* `error` — because the card renders the reason inline rather than through
|
|
2751
|
+
* an exception path.
|
|
2752
|
+
*/
|
|
2753
|
+
async function oauthStart(region, options) {
|
|
2754
|
+
const base = normalizeBase(oauthBillingBase(region, options?.base));
|
|
2755
|
+
const doFetch = options?.fetchImpl ?? fetch;
|
|
2756
|
+
let response;
|
|
2757
|
+
try {
|
|
2758
|
+
response = await doFetch(`${base}${API_PREFIX}/auth/state?platform=workbuddy`, {
|
|
2759
|
+
method: "POST",
|
|
2760
|
+
headers: {
|
|
2761
|
+
"Content-Type": "application/json",
|
|
2762
|
+
"Accept": "application/json"
|
|
2763
|
+
},
|
|
2764
|
+
body: "{}",
|
|
2765
|
+
signal: AbortSignal.timeout(3e4)
|
|
2766
|
+
});
|
|
2767
|
+
} catch (error) {
|
|
2768
|
+
return { error: `could not reach ${base} to start the sign-in (${messageOf(error)})` };
|
|
2769
|
+
}
|
|
2770
|
+
const document = await readJson(response);
|
|
2771
|
+
if (document === void 0) return { error: `the sign-in service answered with a non-JSON body (http ${response.status})` };
|
|
2772
|
+
const data = envelopeData(document);
|
|
2773
|
+
const state = firstString(data, ["state", "State"]);
|
|
2774
|
+
if (state === "") return { error: `the sign-in service did not issue a state (http ${response.status})` };
|
|
2775
|
+
const authUrl = firstString(data, [
|
|
2776
|
+
"authUrl",
|
|
2777
|
+
"auth_url",
|
|
2778
|
+
"url",
|
|
2779
|
+
"Url"
|
|
2780
|
+
]) || `${base}/login?state=${encodeURIComponent(state)}`;
|
|
2781
|
+
const loginId = `wbo_${randomUUID().replaceAll("-", "")}`;
|
|
2782
|
+
sessions.set(loginId, {
|
|
2783
|
+
region,
|
|
2784
|
+
base,
|
|
2785
|
+
state,
|
|
2786
|
+
expiresAtMs: Date.now() + 6e5,
|
|
2787
|
+
done: false
|
|
2788
|
+
});
|
|
2789
|
+
return {
|
|
2790
|
+
loginId,
|
|
2791
|
+
verificationUri: authUrl,
|
|
2792
|
+
expiresIn: 600
|
|
2793
|
+
};
|
|
2794
|
+
}
|
|
2795
|
+
/**
|
|
2796
|
+
* Poll once for a login's outcome.
|
|
2797
|
+
*
|
|
2798
|
+
* Ordering matters and mirrors the reference implementation: the session's
|
|
2799
|
+
* region/base binding is checked BEFORE any I/O, so a poll that names the
|
|
2800
|
+
* wrong tab never reaches the network, and an unknown id fails without a
|
|
2801
|
+
* request too.
|
|
2802
|
+
*/
|
|
2803
|
+
async function oauthPoll(loginId, region, options) {
|
|
2804
|
+
const session = sessions.get(loginId);
|
|
2805
|
+
if (session === void 0) return {
|
|
2806
|
+
done: true,
|
|
2807
|
+
error: "sign-in session not found; start a new one"
|
|
2808
|
+
};
|
|
2809
|
+
if (session.region !== region) return {
|
|
2810
|
+
done: true,
|
|
2811
|
+
error: "sign-in session belongs to another region tab"
|
|
2812
|
+
};
|
|
2813
|
+
if (session.done) return terminalOf(session);
|
|
2814
|
+
if (Date.now() > session.expiresAtMs) {
|
|
2815
|
+
session.done = true;
|
|
2816
|
+
session.error = "the sign-in timed out; start again";
|
|
2817
|
+
sessions.delete(loginId);
|
|
2818
|
+
return {
|
|
2819
|
+
done: true,
|
|
2820
|
+
error: session.error
|
|
2821
|
+
};
|
|
2822
|
+
}
|
|
2823
|
+
const doFetch = options?.fetchImpl ?? fetch;
|
|
2824
|
+
let response;
|
|
2825
|
+
try {
|
|
2826
|
+
response = await doFetch(`${session.base}${API_PREFIX}/auth/token?state=${encodeURIComponent(session.state)}`, {
|
|
2827
|
+
method: "GET",
|
|
2828
|
+
headers: { "Accept": "application/json" },
|
|
2829
|
+
signal: AbortSignal.timeout(3e4)
|
|
2830
|
+
});
|
|
2831
|
+
} catch {
|
|
2832
|
+
return { done: false };
|
|
2833
|
+
}
|
|
2834
|
+
const document = await readJson(response);
|
|
2835
|
+
if (document === void 0) return { done: false };
|
|
2836
|
+
const code = typeof document["code"] === "number" ? document["code"] : -1;
|
|
2837
|
+
if (code !== 0 && code !== 200) return { done: false };
|
|
2838
|
+
const data = envelopeData(document);
|
|
2839
|
+
const accessToken = firstString(data, [
|
|
2840
|
+
"accessToken",
|
|
2841
|
+
"access_token",
|
|
2842
|
+
"AccessToken"
|
|
2843
|
+
]);
|
|
2844
|
+
if (accessToken === "") return { done: false };
|
|
2845
|
+
session.done = true;
|
|
2846
|
+
try {
|
|
2847
|
+
const account = await collectAccount(session, accessToken, data, doFetch);
|
|
2848
|
+
sessions.delete(loginId);
|
|
2849
|
+
return {
|
|
2850
|
+
done: true,
|
|
2851
|
+
account
|
|
2852
|
+
};
|
|
2853
|
+
} catch (error) {
|
|
2854
|
+
session.error = messageOf(error);
|
|
2855
|
+
sessions.delete(loginId);
|
|
2856
|
+
return {
|
|
2857
|
+
done: true,
|
|
2858
|
+
error: session.error
|
|
2859
|
+
};
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
/** Terminal view of a session, for a poll that arrives after completion. */
|
|
2863
|
+
function terminalOf(session) {
|
|
2864
|
+
if (session.result !== void 0) return {
|
|
2865
|
+
done: true,
|
|
2866
|
+
account: session.result
|
|
2867
|
+
};
|
|
2868
|
+
return {
|
|
2869
|
+
done: true,
|
|
2870
|
+
error: session.error ?? "the sign-in ended without a result"
|
|
2871
|
+
};
|
|
2872
|
+
}
|
|
2873
|
+
/** Fetch the account profile and assemble the success value. */
|
|
2874
|
+
async function collectAccount(session, accessToken, tokenData, doFetch) {
|
|
2875
|
+
const headers = {
|
|
2876
|
+
"Authorization": `Bearer ${accessToken}`,
|
|
2877
|
+
"Accept": "application/json"
|
|
2878
|
+
};
|
|
2879
|
+
const domain = firstString(tokenData, ["domain", "Domain"]);
|
|
2880
|
+
if (domain !== "") headers["X-Domain"] = domain;
|
|
2881
|
+
let profile = {};
|
|
2882
|
+
try {
|
|
2883
|
+
const document = await readJson(await doFetch(`${session.base}${API_PREFIX}/login/account`, {
|
|
2884
|
+
method: "GET",
|
|
2885
|
+
headers,
|
|
2886
|
+
signal: AbortSignal.timeout(3e4)
|
|
2887
|
+
}));
|
|
2888
|
+
if (document !== void 0) profile = envelopeData(document);
|
|
2889
|
+
} catch {}
|
|
2890
|
+
const uid = firstString(profile, [
|
|
2891
|
+
"uid",
|
|
2892
|
+
"Uid",
|
|
2893
|
+
"userId",
|
|
2894
|
+
"UserID"
|
|
2895
|
+
]);
|
|
2896
|
+
const nickname = optionalString(profile, [
|
|
2897
|
+
"nickname",
|
|
2898
|
+
"NickName",
|
|
2899
|
+
"nickname",
|
|
2900
|
+
"ScreenName"
|
|
2901
|
+
]);
|
|
2902
|
+
const uin = optionalString(profile, ["uin", "Uin"]);
|
|
2903
|
+
const enterpriseId = optionalString(profile, ["enterpriseId", "EnterpriseId"]);
|
|
2904
|
+
const expiresAtMs = expiryMs(tokenData, ["expiresAt", "expires_at"], ["expiresIn", "expires_in"]) ?? 0;
|
|
2905
|
+
const refreshExpiresAtMs = expiryMs(tokenData, ["refreshExpiresAt", "refresh_expires_at"], ["refreshExpiresIn", "refresh_expires_in"]);
|
|
2906
|
+
return {
|
|
2907
|
+
accessToken,
|
|
2908
|
+
refreshToken: firstString(tokenData, [
|
|
2909
|
+
"refreshToken",
|
|
2910
|
+
"refresh_token",
|
|
2911
|
+
"RefreshToken"
|
|
2912
|
+
]),
|
|
2913
|
+
expiresAtMs,
|
|
2914
|
+
...refreshExpiresAtMs === void 0 ? {} : { refreshExpiresAtMs },
|
|
2915
|
+
domain: domain !== "" ? domain : firstString(profile, ["domain", "Domain"]),
|
|
2916
|
+
uid,
|
|
2917
|
+
...nickname === void 0 ? {} : { nickname },
|
|
2918
|
+
...uin === void 0 ? {} : { uin },
|
|
2919
|
+
...enterpriseId === void 0 ? {} : { enterpriseId }
|
|
2920
|
+
};
|
|
2921
|
+
}
|
|
2922
|
+
/** Parse a JSON body; undefined for non-JSON (the caller decides severity). */
|
|
2923
|
+
async function readJson(response) {
|
|
2924
|
+
try {
|
|
2925
|
+
const text = await response.text();
|
|
2926
|
+
const parsed = JSON.parse(text);
|
|
2927
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) return parsed;
|
|
2928
|
+
} catch {}
|
|
2929
|
+
}
|
|
2930
|
+
/** The envelope's `data` object; a non-object data is an empty one. */
|
|
2931
|
+
function envelopeData(document) {
|
|
2932
|
+
const data = document["data"];
|
|
2933
|
+
return typeof data === "object" && data !== null && !Array.isArray(data) ? data : {};
|
|
2934
|
+
}
|
|
2935
|
+
/** First non-empty string among `keys`, else ''. */
|
|
2936
|
+
function firstString(source, keys) {
|
|
2937
|
+
for (const key of keys) {
|
|
2938
|
+
const value = source[key];
|
|
2939
|
+
if (typeof value === "string" && value.trim() !== "") return value.trim();
|
|
2940
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
2941
|
+
}
|
|
2942
|
+
return "";
|
|
2943
|
+
}
|
|
2944
|
+
/** First non-empty string among `keys`, else undefined. */
|
|
2945
|
+
function optionalString(source, keys) {
|
|
2946
|
+
const value = firstString(source, keys);
|
|
2947
|
+
return value === "" ? void 0 : value;
|
|
2948
|
+
}
|
|
2949
|
+
/**
|
|
2950
|
+
* An expiry field that may arrive as an absolute timestamp (s or ms) or as a
|
|
2951
|
+
* relative TTL in seconds — the same tolerance `auth.ts` `expiryToMs` and
|
|
2952
|
+
* the reference implementation's `norm_ts` apply.
|
|
2953
|
+
*/
|
|
2954
|
+
function expiryMs(source, absoluteKeys, ttlKeys) {
|
|
2955
|
+
for (const key of absoluteKeys) {
|
|
2956
|
+
const value = source[key];
|
|
2957
|
+
if (typeof value === "number" && value > 0) return value > 0xe8d4a51000 ? value : value * 1e3;
|
|
2958
|
+
if (typeof value === "string" && value.trim() !== "" && Number.isFinite(Number(value))) {
|
|
2959
|
+
const parsed = Number(value);
|
|
2960
|
+
return parsed > 0xe8d4a51000 ? parsed : parsed * 1e3;
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
for (const key of ttlKeys) {
|
|
2964
|
+
const value = source[key];
|
|
2965
|
+
if (typeof value === "number" && value > 0) return Date.now() + value * 1e3;
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
/** Human message from a thrown error, for the card's inline display. */
|
|
2969
|
+
function messageOf(error) {
|
|
2970
|
+
return error instanceof Error ? error.message : String(error);
|
|
2971
|
+
}
|
|
2972
|
+
//#endregion
|
|
2973
|
+
//#region src/transfer.ts
|
|
2974
|
+
/**
|
|
2975
|
+
* Account transfer: the export / import file format shared with sibling
|
|
2976
|
+
* credential managers for the same upstream.
|
|
2977
|
+
*
|
|
2978
|
+
* 参考:通行实现 —— 文件是一个 JSON 数组,每项为一条凭据记录,顶层字段
|
|
2979
|
+
* snake_case(`access_token` / `refresh_token` / `uid` / `nickname` /
|
|
2980
|
+
* `email` / `domain` / `expiresAt`(ms) / `refreshExpiresAt` / `auth_raw`),
|
|
2981
|
+
* 导入按 `uid` 去重(同 uid 覆盖),缺 `access_token` 的记录跳过;预览只
|
|
2982
|
+
* 回脱敏字段(含 `hasToken` 布尔量),不回明文。解析函数不依赖文件系统,
|
|
2983
|
+
* 便于无 UI 环境单测;落库由 Host 侧的 store 完成。
|
|
2984
|
+
* 互通:`expiresAt` 一族按契约是**毫秒**(由 auth_raw 的原始认证文档携带);
|
|
2985
|
+
* 本插件的 vault 文档 `expiresAt` 是秒 —— 换算只发生在导出/导入边界。
|
|
2986
|
+
* 导出时 `auth_raw` 直接携带本插件 vault 文档的 `{auth, account}` 双键块,
|
|
2987
|
+
* 与桌面认证文件同构,供写入桌面认证文件的工具原样消费。
|
|
2988
|
+
*
|
|
2989
|
+
* @module dsh-connect-workbuddy/transfer
|
|
2990
|
+
*/
|
|
2991
|
+
/**
|
|
2992
|
+
* Parse transfer-file text: a JSON array, every element an object.
|
|
2993
|
+
*
|
|
2994
|
+
* The error strings carry the position (1-based item number) because a file
|
|
2995
|
+
* from another tool can be big, and "第 N 项" is what lets the user find the
|
|
2996
|
+
* broken record in their editor.
|
|
2997
|
+
*/
|
|
2998
|
+
function parseTransferFile(text) {
|
|
2999
|
+
if (text.trim() === "") throw new Error("transfer file is empty");
|
|
3000
|
+
let parsed;
|
|
3001
|
+
try {
|
|
3002
|
+
parsed = JSON.parse(text);
|
|
3003
|
+
} catch (error) {
|
|
3004
|
+
throw new Error("transfer file is not valid JSON: " + (error instanceof Error ? error.message : String(error)));
|
|
3005
|
+
}
|
|
3006
|
+
if (!Array.isArray(parsed)) throw new Error("transfer file must be a JSON array of account records");
|
|
3007
|
+
for (const [index, item] of parsed.entries()) if (typeof item !== "object" || item === null || Array.isArray(item)) throw new Error("transfer file item " + (index + 1) + " is not an account object");
|
|
3008
|
+
return parsed;
|
|
3009
|
+
}
|
|
3010
|
+
/** One string field of a record, tolerating the sibling format's odd shapes. */
|
|
3011
|
+
function displayString(value) {
|
|
3012
|
+
if (typeof value === "string") return value;
|
|
3013
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
3014
|
+
return "";
|
|
3015
|
+
}
|
|
3016
|
+
/** Whether a record carries a usable token, envelope-shaped or plain. */
|
|
3017
|
+
function hasToken(record) {
|
|
3018
|
+
const token = record["access_token"];
|
|
3019
|
+
return typeof token === "string" && token !== "";
|
|
3020
|
+
}
|
|
3021
|
+
/**
|
|
3022
|
+
* The transfer file's desensitized preview: display fields only.
|
|
3023
|
+
*
|
|
3024
|
+
* Fields come from EXTERNAL files, so a display field may hold an object or an
|
|
3025
|
+
* array (a sibling tool can store the desktop app's encrypted-wrapper nickname
|
|
3026
|
+
* verbatim); every preview field is normalized to a string or emptied rather
|
|
3027
|
+
* than letting a non-renderable value reach the card.
|
|
3028
|
+
*/
|
|
3029
|
+
function previewTransferFile(text) {
|
|
3030
|
+
const records = parseTransferFile(text);
|
|
3031
|
+
return {
|
|
3032
|
+
accounts: records.map((record, index) => ({
|
|
3033
|
+
index,
|
|
3034
|
+
uid: displayString(record["uid"]),
|
|
3035
|
+
nickname: displayString(record["nickname"]),
|
|
3036
|
+
email: displayString(record["email"]),
|
|
3037
|
+
hasToken: hasToken(record)
|
|
3038
|
+
})),
|
|
3039
|
+
total: records.length
|
|
3040
|
+
};
|
|
3041
|
+
}
|
|
3042
|
+
/**
|
|
3043
|
+
* Project one vault credential into the shared transfer record shape.
|
|
3044
|
+
*
|
|
3045
|
+
* `expiresAt` is emitted in MILLISECONDS — the transfer contract's unit and
|
|
3046
|
+
* the sibling tools' display field — while the vault document stores seconds.
|
|
3047
|
+
* `auth_raw` carries the vault's nested `{auth, account}` document so a tool
|
|
3048
|
+
* that writes desktop auth files can consume it verbatim.
|
|
3049
|
+
*/
|
|
3050
|
+
function transferRecordOf(credential, vaultDocument) {
|
|
3051
|
+
return {
|
|
3052
|
+
access_token: credential.accessToken,
|
|
3053
|
+
...credential.refreshToken !== "" ? { refresh_token: credential.refreshToken } : {},
|
|
3054
|
+
...credential.uid !== "" ? { uid: credential.uid } : {},
|
|
3055
|
+
...credential.nickname !== void 0 ? { nickname: credential.nickname } : {},
|
|
3056
|
+
...credential.domain !== "" ? { domain: credential.domain } : {},
|
|
3057
|
+
...credential.expiresAtMs > 0 ? { expiresAt: credential.expiresAtMs } : {},
|
|
3058
|
+
...credential.refreshExpiresAtMs === void 0 ? {} : { refreshExpiresAt: credential.refreshExpiresAtMs },
|
|
3059
|
+
auth_raw: vaultDocument
|
|
3060
|
+
};
|
|
3061
|
+
}
|
|
3062
|
+
/**
|
|
3063
|
+
* Recover `uin` from a record's `auth_raw` document.
|
|
3064
|
+
*
|
|
3065
|
+
* `uin` is the billing identity the account id derives from
|
|
3066
|
+
* (`uin ?? uid ?? nickname`), but it is NOT one of the transfer format's
|
|
3067
|
+
* contract fields — sibling exports carry it only inside `auth_raw`
|
|
3068
|
+
* (the original auth document, whose `account` object holds it). Restoring it
|
|
3069
|
+
* is what makes an exported-and-re-imported account converge on the SAME
|
|
3070
|
+
* vault id instead of duplicating under a uid-derived id. Both document
|
|
3071
|
+
* shapes are honored: the nested `{auth, account}` block this plugin
|
|
3072
|
+
* exports, and a record whose `auth_raw.account` came straight from a
|
|
3073
|
+
* desktop file.
|
|
3074
|
+
*/
|
|
3075
|
+
function uinOfRaw(raw) {
|
|
3076
|
+
if (typeof raw !== "object" || raw === null) return void 0;
|
|
3077
|
+
const root = raw;
|
|
3078
|
+
const uin = displayString((typeof root["account"] === "object" && root["account"] !== null ? root["account"] : root)["uin"]);
|
|
3079
|
+
return uin === "" ? void 0 : uin;
|
|
3080
|
+
}
|
|
3081
|
+
/**
|
|
3082
|
+
* Project one transfer record into a vault credential, or undefined when the
|
|
3083
|
+
* record carries no token (the importer's skip rule).
|
|
3084
|
+
*
|
|
3085
|
+
* `uid` empty is tolerated at THIS layer because a token-only record still
|
|
3086
|
+
* describes a usable credential; the Host's region gate and the store's
|
|
3087
|
+
* account id (`uin ?? uid ?? nickname`) decide whether it is representable.
|
|
3088
|
+
* An unopenable `auth_raw` is ignored — the flat fields above are the
|
|
3089
|
+
* contract, `auth_raw` is extra fidelity for the write-back path.
|
|
3090
|
+
*/
|
|
3091
|
+
function credentialOfTransferRecord(record) {
|
|
3092
|
+
const accessToken = record["access_token"];
|
|
3093
|
+
if (typeof accessToken !== "string" || accessToken === "") return void 0;
|
|
3094
|
+
const refreshToken = typeof record["refresh_token"] === "string" ? record["refresh_token"] : "";
|
|
3095
|
+
const expiresAtMs = typeof record["expiresAt"] === "number" && Number.isFinite(record["expiresAt"]) && record["expiresAt"] > 0 ? record["expiresAt"] : 0;
|
|
3096
|
+
const refreshExpiresAtMs = typeof record["refreshExpiresAt"] === "number" && Number.isFinite(record["refreshExpiresAt"]) && record["refreshExpiresAt"] > 0 ? record["refreshExpiresAt"] : void 0;
|
|
3097
|
+
const uin = uinOfRaw(record["auth_raw"]);
|
|
3098
|
+
return {
|
|
3099
|
+
accessToken,
|
|
3100
|
+
refreshToken,
|
|
3101
|
+
expiresAtMs,
|
|
3102
|
+
...refreshExpiresAtMs === void 0 ? {} : { refreshExpiresAtMs },
|
|
3103
|
+
domain: typeof record["domain"] === "string" ? record["domain"] : "",
|
|
3104
|
+
uid: displayString(record["uid"]),
|
|
3105
|
+
...record["nickname"] !== void 0 ? { nickname: displayString(record["nickname"]) } : {},
|
|
3106
|
+
...uin === void 0 ? {} : { uin }
|
|
3107
|
+
};
|
|
3108
|
+
}
|
|
3109
|
+
/**
|
|
3110
|
+
* Pure merge: fold selected records into a CHANGES map keyed by account id.
|
|
3111
|
+
*
|
|
3112
|
+
* Mirrors the sibling merger's contract: a record with the same account id
|
|
3113
|
+
* OVERWRITES the stored entry (the imported tokens replace what is stored —
|
|
3114
|
+
* re-importing a re-login must refresh it; the write layer stores the record
|
|
3115
|
+
* atomically, so "overwrite" needs no knowledge of the incumbent), and a
|
|
3116
|
+
* record without a token is skipped. The region gate lives HERE rather than
|
|
3117
|
+
* in the route so it is testable without a Host: a record whose login domain
|
|
3118
|
+
* maps to the other region's tab is skipped, because the two regions'
|
|
3119
|
+
* providers are parallel stacks and a cross-region import would cross-bill.
|
|
3120
|
+
*
|
|
3121
|
+
* The caller writes one vault file per map entry, so a selection that is
|
|
3122
|
+
* already stored rewrites those files with identical content — idempotent,
|
|
3123
|
+
* and the steady state of importing the same file twice.
|
|
3124
|
+
*/
|
|
3125
|
+
function mergeTransferRecords(records, indexes, region, knownRegionOfDomain) {
|
|
3126
|
+
const changes = /* @__PURE__ */ new Map();
|
|
3127
|
+
let imported = 0;
|
|
3128
|
+
let skipped = 0;
|
|
3129
|
+
for (const index of indexes) {
|
|
3130
|
+
const record = records[index];
|
|
3131
|
+
if (record === void 0) {
|
|
3132
|
+
skipped += 1;
|
|
3133
|
+
continue;
|
|
3134
|
+
}
|
|
3135
|
+
const credential = credentialOfTransferRecord(record);
|
|
3136
|
+
if (credential === void 0) {
|
|
3137
|
+
skipped += 1;
|
|
3138
|
+
continue;
|
|
3139
|
+
}
|
|
3140
|
+
if (knownRegionOfDomain(credential.domain) !== region) {
|
|
3141
|
+
skipped += 1;
|
|
3142
|
+
continue;
|
|
3143
|
+
}
|
|
3144
|
+
const id = workbuddyAccountId(credential);
|
|
3145
|
+
changes.set(id, credential);
|
|
3146
|
+
imported += 1;
|
|
3147
|
+
}
|
|
3148
|
+
return {
|
|
3149
|
+
changes,
|
|
3150
|
+
imported,
|
|
3151
|
+
skipped
|
|
3152
|
+
};
|
|
3153
|
+
}
|
|
3154
|
+
//#endregion
|
|
2692
3155
|
//#region src/account-pool-store.ts
|
|
2693
3156
|
/**
|
|
2694
3157
|
* The account pool's RUNTIME store: measured facts, on disk, outside settings.
|
|
@@ -2832,6 +3295,18 @@ async function writePoolProbes(region, updates) {
|
|
|
2832
3295
|
*/
|
|
2833
3296
|
/** Plugin-owned usage endpoint consumed by its browser half. */
|
|
2834
3297
|
const WORKBUDDY_USAGE_PATH = "/plugins/dsh-connect-workbuddy/usage";
|
|
3298
|
+
/**
|
|
3299
|
+
* Per-account credit balances for one region, read ONLY while the composer
|
|
3300
|
+
* panel is open.
|
|
3301
|
+
*
|
|
3302
|
+
* Separate from {@link WORKBUDDY_USAGE_PATH} because its cost is different in
|
|
3303
|
+
* kind: the usage document is one upstream read for the selected account, while
|
|
3304
|
+
* this is one read PER account in the region. Folding it into the usage route
|
|
3305
|
+
* would make every 5-minute refresh pay for a table nobody is looking at, so it
|
|
3306
|
+
* is fetched on demand instead — the same principle the pool applies to its
|
|
3307
|
+
* per-account figures.
|
|
3308
|
+
*/
|
|
3309
|
+
const WORKBUDDY_ACCOUNT_CREDITS_PATH = "/plugins/dsh-connect-workbuddy/account-credits";
|
|
2835
3310
|
/** Plugin-owned live model refresh endpoint. */
|
|
2836
3311
|
const WORKBUDDY_MODELS_REFRESH_PATH = "/plugins/dsh-connect-workbuddy/models/refresh";
|
|
2837
3312
|
/** Plugin-owned local account rescan endpoint. */
|
|
@@ -2860,14 +3335,59 @@ const WORKBUDDY_PROBE_PATH = "/plugins/dsh-connect-workbuddy/probe";
|
|
|
2860
3335
|
* splitting them would duplicate the guards and invite them to drift apart.
|
|
2861
3336
|
*/
|
|
2862
3337
|
const WORKBUDDY_POOL_PATH = "/plugins/dsh-connect-workbuddy/pool";
|
|
3338
|
+
/**
|
|
3339
|
+
* Plugin-owned OAuth sign-in endpoint: one session step per call.
|
|
3340
|
+
*
|
|
3341
|
+
* `?action=start` asks the upstream for a login state and returns the URL to
|
|
3342
|
+
* open; `?action=poll` (+ `loginId` in the body) reports whether the user
|
|
3343
|
+
* finished, and on success persists the account into the vault. POST +
|
|
3344
|
+
* loopback-only like the card's other mutations: the poll carries the login
|
|
3345
|
+
* binding, and the Host-side session state must not be settable by another
|
|
3346
|
+
* origin.
|
|
3347
|
+
*/
|
|
3348
|
+
const WORKBUDDY_OAUTH_PATH = "/plugins/dsh-connect-workbuddy/oauth";
|
|
2863
3349
|
/** Query parameter selecting which pool batch action a request means. */
|
|
2864
3350
|
const WORKBUDDY_POOL_ACTION_PARAM = "action";
|
|
3351
|
+
/** Read the OAuth action off a request URL; unknown/absent means undefined. */
|
|
3352
|
+
function oauthActionOf(url) {
|
|
3353
|
+
const at = url.indexOf("?");
|
|
3354
|
+
if (at === -1) return void 0;
|
|
3355
|
+
const value = new URLSearchParams(url.slice(at + 1)).get(WORKBUDDY_POOL_ACTION_PARAM);
|
|
3356
|
+
return value === "start" || value === "poll" ? value : void 0;
|
|
3357
|
+
}
|
|
2865
3358
|
/** Read the pool action off a request URL; unknown/absent means undefined. */
|
|
2866
3359
|
function poolActionOf(url) {
|
|
2867
3360
|
const at = url.indexOf("?");
|
|
2868
3361
|
if (at === -1) return void 0;
|
|
2869
3362
|
const value = new URLSearchParams(url.slice(at + 1)).get(WORKBUDDY_POOL_ACTION_PARAM);
|
|
2870
|
-
return value === "checkin" || value === "test" ? value : void 0;
|
|
3363
|
+
return value === "checkin" || value === "test" || value === "remove" ? value : void 0;
|
|
3364
|
+
}
|
|
3365
|
+
/** Query parameter naming ONE pool account a per-row request addresses. */
|
|
3366
|
+
const WORKBUDDY_POOL_ACCOUNT_PARAM = "accountId";
|
|
3367
|
+
/**
|
|
3368
|
+
* Plugin-owned account transfer endpoint: the batch export / import of
|
|
3369
|
+
* credentials between machines and between tools of the SAME format family.
|
|
3370
|
+
*
|
|
3371
|
+
* `?action=export` POSTs account ids, answers a JSON ARRAY of credential
|
|
3372
|
+
* records; `?action=preview` POSTs file text, answers a token-free listing;
|
|
3373
|
+
* `?action=import` POSTs file text plus selected indexes, merges into the
|
|
3374
|
+
* vault. POST + loopback-only like every other mutation: the file content is
|
|
3375
|
+
* arbitrary user data and export carries token material.
|
|
3376
|
+
*/
|
|
3377
|
+
const WORKBUDDY_TRANSFER_PATH = "/plugins/dsh-connect-workbuddy/transfer";
|
|
3378
|
+
/** Read the transfer action off a request URL; unknown/absent means undefined. */
|
|
3379
|
+
function transferActionOf(url) {
|
|
3380
|
+
const at = url.indexOf("?");
|
|
3381
|
+
if (at === -1) return void 0;
|
|
3382
|
+
const value = new URLSearchParams(url.slice(at + 1)).get(WORKBUDDY_POOL_ACTION_PARAM);
|
|
3383
|
+
return value === "export" || value === "preview" || value === "import" ? value : void 0;
|
|
3384
|
+
}
|
|
3385
|
+
/** Read the per-row account id off a request URL, when the action takes one. */
|
|
3386
|
+
function poolAccountIdOf(url) {
|
|
3387
|
+
const at = url.indexOf("?");
|
|
3388
|
+
if (at === -1) return void 0;
|
|
3389
|
+
const value = new URLSearchParams(url.slice(at + 1)).get(WORKBUDDY_POOL_ACCOUNT_PARAM);
|
|
3390
|
+
return value !== null && value !== "" ? value : void 0;
|
|
2871
3391
|
}
|
|
2872
3392
|
/** Query parameter naming the region a card request addresses. */
|
|
2873
3393
|
const WORKBUDDY_REGION_PARAM = "region";
|
|
@@ -3027,6 +3547,23 @@ function json(res, status, body) {
|
|
|
3027
3547
|
});
|
|
3028
3548
|
res.end(payload);
|
|
3029
3549
|
}
|
|
3550
|
+
/**
|
|
3551
|
+
* A JSON response that must never be cached.
|
|
3552
|
+
*
|
|
3553
|
+
* Used for every answer that carries live credentials — the transfer export
|
|
3554
|
+
* hands the browser real access and refresh tokens. Without `no-store` such a
|
|
3555
|
+
* response is eligible for the browser's HTTP cache, where a token can outlive
|
|
3556
|
+
* the session on disk.
|
|
3557
|
+
*/
|
|
3558
|
+
function jsonPrivate(res, status, body) {
|
|
3559
|
+
const payload = JSON.stringify(body);
|
|
3560
|
+
res.writeHead(status, {
|
|
3561
|
+
"Content-Type": "application/json",
|
|
3562
|
+
"Content-Length": Buffer.byteLength(payload),
|
|
3563
|
+
"Cache-Control": "no-store"
|
|
3564
|
+
});
|
|
3565
|
+
res.end(payload);
|
|
3566
|
+
}
|
|
3030
3567
|
/** Request-body cap for the probe route: model ids only, never content. */
|
|
3031
3568
|
const PROBE_BODY_LIMIT = 65536;
|
|
3032
3569
|
/**
|
|
@@ -3158,6 +3695,39 @@ function volatileFlagOf(field) {
|
|
|
3158
3695
|
return Config.dict?.[field]?.meta?.volatile === true;
|
|
3159
3696
|
}
|
|
3160
3697
|
/**
|
|
3698
|
+
* Per-account credit balances for one region, for the composer panel's table.
|
|
3699
|
+
*
|
|
3700
|
+
* ONE upstream read per account, which is exactly why this is its own route:
|
|
3701
|
+
* the panel calls it only while it is open, so the 5-minute readout never
|
|
3702
|
+
* multiplies its cost by the account count.
|
|
3703
|
+
*
|
|
3704
|
+
* Every account is listed even when its balance cannot be read — a credential
|
|
3705
|
+
* that no longer resolves, or an upstream that refused — because the panel's
|
|
3706
|
+
* job is to show which accounts exist and let the user switch. Such a row
|
|
3707
|
+
* carries NO `credits` field rather than `0`, so the panel renders "—" instead
|
|
3708
|
+
* of telling the user they are out of credits.
|
|
3709
|
+
*
|
|
3710
|
+
* Failures are contained per account: one dead credential must not blank the
|
|
3711
|
+
* table for the accounts that are fine.
|
|
3712
|
+
*/
|
|
3713
|
+
async function workBuddyAccountCredits(deps, region) {
|
|
3714
|
+
const store = deps.store(region);
|
|
3715
|
+
const accounts = await store.accounts();
|
|
3716
|
+
const exclusions = await deps.accountExclusions?.(region).catch(() => void 0) ?? {};
|
|
3717
|
+
return Promise.all(accounts.map(async (account) => {
|
|
3718
|
+
const credential = await store.credentialFor(account.id).catch(() => void 0);
|
|
3719
|
+
const credits = credential === void 0 ? void 0 : await deps.client.fetchCredits(credential).then((answer) => answer.total).catch(() => void 0);
|
|
3720
|
+
const excludedBy = exclusions[account.id];
|
|
3721
|
+
return {
|
|
3722
|
+
id: account.id,
|
|
3723
|
+
accountName: account.accountName,
|
|
3724
|
+
selected: account.selected,
|
|
3725
|
+
...credits === void 0 ? {} : { credits },
|
|
3726
|
+
...excludedBy === void 0 ? {} : { excludedBy }
|
|
3727
|
+
};
|
|
3728
|
+
}));
|
|
3729
|
+
}
|
|
3730
|
+
/**
|
|
3161
3731
|
* Assemble one region's card document. `region` is the tab the card is on;
|
|
3162
3732
|
* the region-scoped store already answers with only that region's accounts,
|
|
3163
3733
|
* so the document's model slots and account list are that region's by
|
|
@@ -3169,13 +3739,15 @@ async function workBuddyWebStatus(deps, region) {
|
|
|
3169
3739
|
const accounts = await store.accounts();
|
|
3170
3740
|
deps.regionUsable?.(region, accounts.length > 0);
|
|
3171
3741
|
const enabled = deps.regionEnabled(region);
|
|
3742
|
+
const showCreditsInMainUi = deps.showCreditsInMainUi?.(region) ?? true;
|
|
3172
3743
|
const authStatus = await store.status();
|
|
3173
3744
|
const selectionExplicit = store.hasExplicitSelection();
|
|
3174
3745
|
if (authStatus.state !== "signed-out" && accounts.length === 0) return {
|
|
3175
3746
|
status: "signed-out",
|
|
3176
3747
|
accounts: [],
|
|
3177
3748
|
selectionExplicit,
|
|
3178
|
-
enabled
|
|
3749
|
+
enabled,
|
|
3750
|
+
showCreditsInMainUi
|
|
3179
3751
|
};
|
|
3180
3752
|
let credential;
|
|
3181
3753
|
try {
|
|
@@ -3188,6 +3760,7 @@ async function workBuddyWebStatus(deps, region) {
|
|
|
3188
3760
|
message: safeMessage(error),
|
|
3189
3761
|
selectionExplicit,
|
|
3190
3762
|
enabled,
|
|
3763
|
+
showCreditsInMainUi,
|
|
3191
3764
|
...await store.selectionLost() ? { selectionLost: true } : {},
|
|
3192
3765
|
...webAccounts.length === 0 ? await searchedPaths(store) : {}
|
|
3193
3766
|
};
|
|
@@ -3202,6 +3775,7 @@ async function workBuddyWebStatus(deps, region) {
|
|
|
3202
3775
|
tokenExpiresAtMs: credential.expiresAtMs,
|
|
3203
3776
|
selectionExplicit,
|
|
3204
3777
|
enabled,
|
|
3778
|
+
showCreditsInMainUi,
|
|
3205
3779
|
accounts: accounts.map(toWebAccount),
|
|
3206
3780
|
models: deps.displayModels(region).map((model) => toWebModel(model, deps.contextBudgets(region))),
|
|
3207
3781
|
enabledModelIds: [...deps.enabledModelIds(region)],
|
|
@@ -3354,6 +3928,21 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
|
|
|
3354
3928
|
}
|
|
3355
3929
|
}
|
|
3356
3930
|
});
|
|
3931
|
+
const disposeAccountCredits = ctx.webServer.register({
|
|
3932
|
+
kind: "exact",
|
|
3933
|
+
path: WORKBUDDY_ACCOUNT_CREDITS_PATH,
|
|
3934
|
+
handler: async (req, res) => {
|
|
3935
|
+
if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
|
|
3936
|
+
if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
|
|
3937
|
+
const region = requestRegion(req, res);
|
|
3938
|
+
if (region === void 0) return;
|
|
3939
|
+
try {
|
|
3940
|
+
json(res, 200, { accounts: await workBuddyAccountCredits(deps, region) });
|
|
3941
|
+
} catch (error) {
|
|
3942
|
+
json(res, 500, { error: safeMessage(error) });
|
|
3943
|
+
}
|
|
3944
|
+
}
|
|
3945
|
+
});
|
|
3357
3946
|
const disposeAccounts = ctx.webServer.register({
|
|
3358
3947
|
kind: "exact",
|
|
3359
3948
|
path: WORKBUDDY_ACCOUNTS_REFRESH_PATH,
|
|
@@ -3444,12 +4033,60 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
|
|
|
3444
4033
|
const region = requestRegion(req, res);
|
|
3445
4034
|
if (region === void 0) return;
|
|
3446
4035
|
const action = poolActionOf(req.url ?? "/");
|
|
3447
|
-
if (action === void 0) return json(res, 400, { error: "action must be checkin or
|
|
4036
|
+
if (action === void 0) return json(res, 400, { error: "action must be checkin, test, or remove" });
|
|
3448
4037
|
const pool = deps.pool;
|
|
3449
4038
|
if (pool === void 0) return json(res, 503, {
|
|
3450
4039
|
reason: "pool-unavailable",
|
|
3451
4040
|
error: "account pool unavailable"
|
|
3452
4041
|
});
|
|
4042
|
+
const rowAccountId = poolAccountIdOf(req.url ?? "/");
|
|
4043
|
+
if (action === "remove") {
|
|
4044
|
+
if (pool.removeOne === void 0) return json(res, 503, {
|
|
4045
|
+
reason: "pool-unavailable",
|
|
4046
|
+
error: "per-account removal unavailable"
|
|
4047
|
+
});
|
|
4048
|
+
if (rowAccountId === void 0) return json(res, 400, { error: "accountId must name the account to remove" });
|
|
4049
|
+
if (!isVaultAccountId(rowAccountId)) return json(res, 400, { error: "accountId is not a well-formed account id" });
|
|
4050
|
+
try {
|
|
4051
|
+
json(res, 200, {
|
|
4052
|
+
action: "remove",
|
|
4053
|
+
accountId: rowAccountId,
|
|
4054
|
+
removed: await pool.removeOne(region, rowAccountId)
|
|
4055
|
+
});
|
|
4056
|
+
} catch (error) {
|
|
4057
|
+
json(res, 500, {
|
|
4058
|
+
reason: "pool-failed",
|
|
4059
|
+
error: safeMessage(error)
|
|
4060
|
+
});
|
|
4061
|
+
}
|
|
4062
|
+
return;
|
|
4063
|
+
}
|
|
4064
|
+
if (action === "test" && rowAccountId !== void 0) {
|
|
4065
|
+
if (pool.testOne === void 0) return json(res, 503, {
|
|
4066
|
+
reason: "pool-unavailable",
|
|
4067
|
+
error: "per-account test unavailable"
|
|
4068
|
+
});
|
|
4069
|
+
try {
|
|
4070
|
+
const row = await pool.testOne(region, rowAccountId);
|
|
4071
|
+
if (row === void 0) return json(res, 409, {
|
|
4072
|
+
action: "test",
|
|
4073
|
+
modelId: void 0,
|
|
4074
|
+
rows: [],
|
|
4075
|
+
reason: "no-free-model",
|
|
4076
|
+
error: "no zero-multiplier model in this region; refresh the catalog or set a target model"
|
|
4077
|
+
});
|
|
4078
|
+
json(res, 200, {
|
|
4079
|
+
action: "test",
|
|
4080
|
+
rows: [row]
|
|
4081
|
+
});
|
|
4082
|
+
} catch (error) {
|
|
4083
|
+
json(res, 500, {
|
|
4084
|
+
reason: "pool-failed",
|
|
4085
|
+
error: safeMessage(error)
|
|
4086
|
+
});
|
|
4087
|
+
}
|
|
4088
|
+
return;
|
|
4089
|
+
}
|
|
3453
4090
|
try {
|
|
3454
4091
|
const savedMembers = pool.preferences(region).memberAccountIds;
|
|
3455
4092
|
if ((await pool.effectiveMemberAccountIds?.(region).catch(() => void 0) ?? savedMembers).length === 0) {
|
|
@@ -3581,13 +4218,79 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
|
|
|
3581
4218
|
}
|
|
3582
4219
|
}
|
|
3583
4220
|
});
|
|
4221
|
+
const disposeOAuth = ctx.webServer.register({
|
|
4222
|
+
kind: "exact",
|
|
4223
|
+
path: WORKBUDDY_OAUTH_PATH,
|
|
4224
|
+
handler: async (req, res) => {
|
|
4225
|
+
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
4226
|
+
if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
|
|
4227
|
+
const region = requestRegion(req, res);
|
|
4228
|
+
if (region === void 0) return;
|
|
4229
|
+
const action = oauthActionOf(req.url ?? "/");
|
|
4230
|
+
if (action === void 0) return json(res, 400, { error: "action must be start or poll" });
|
|
4231
|
+
const oauth = deps.oauth;
|
|
4232
|
+
if (oauth === void 0) return json(res, 503, {
|
|
4233
|
+
reason: "oauth-unavailable",
|
|
4234
|
+
error: "OAuth sign-in is not available in this build"
|
|
4235
|
+
});
|
|
4236
|
+
try {
|
|
4237
|
+
if (action === "start") {
|
|
4238
|
+
json(res, 200, await oauth.start(region));
|
|
4239
|
+
return;
|
|
4240
|
+
}
|
|
4241
|
+
const body = await readJsonBody(req);
|
|
4242
|
+
if (typeof body.loginId !== "string" || body.loginId === "") return json(res, 400, { error: "loginId must be a non-empty string" });
|
|
4243
|
+
json(res, 200, await oauth.poll(region, body.loginId));
|
|
4244
|
+
} catch (error) {
|
|
4245
|
+
json(res, 500, { error: safeMessage(error) });
|
|
4246
|
+
}
|
|
4247
|
+
}
|
|
4248
|
+
});
|
|
4249
|
+
const disposeTransfer = ctx.webServer.register({
|
|
4250
|
+
kind: "exact",
|
|
4251
|
+
path: WORKBUDDY_TRANSFER_PATH,
|
|
4252
|
+
handler: async (req, res) => {
|
|
4253
|
+
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
4254
|
+
if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
|
|
4255
|
+
const region = requestRegion(req, res);
|
|
4256
|
+
if (region === void 0) return;
|
|
4257
|
+
const action = transferActionOf(req.url ?? "/");
|
|
4258
|
+
if (action === void 0) return json(res, 400, { error: "action must be export, preview, or import" });
|
|
4259
|
+
const transfer = deps.transfer;
|
|
4260
|
+
if (transfer === void 0) return json(res, 503, {
|
|
4261
|
+
reason: "transfer-unavailable",
|
|
4262
|
+
error: "account transfer is not available in this build"
|
|
4263
|
+
});
|
|
4264
|
+
try {
|
|
4265
|
+
if (action === "export") {
|
|
4266
|
+
const body = await readJsonBody(req);
|
|
4267
|
+
const accountIds = Array.isArray(body.accountIds) ? body.accountIds.filter((id) => typeof id === "string" && id !== "") : [];
|
|
4268
|
+
if (accountIds.length === 0) return json(res, 400, { error: "accountIds must be a non-empty array of strings" });
|
|
4269
|
+
const answer = await transfer.exportAccounts(region, accountIds);
|
|
4270
|
+
if (answer.accounts.length === 0) return json(res, 409, { error: "none of the named accounts was found in this region" });
|
|
4271
|
+
return jsonPrivate(res, 200, answer);
|
|
4272
|
+
}
|
|
4273
|
+
const body = await readJsonBody(req);
|
|
4274
|
+
if (typeof body.text !== "string" || body.text.trim() === "") return json(res, 400, { error: "text must be the transfer file's JSON content" });
|
|
4275
|
+
if (action === "preview") return json(res, 200, await transfer.previewImport(region, body.text));
|
|
4276
|
+
const indexes = Array.isArray(body.indexes) ? body.indexes.filter((value) => typeof value === "number" && Number.isInteger(value) && value >= 0) : [];
|
|
4277
|
+
if (indexes.length === 0) return json(res, 400, { error: "indexes must name at least one file entry to import" });
|
|
4278
|
+
return json(res, 200, await transfer.importAccounts(region, body.text, indexes));
|
|
4279
|
+
} catch (error) {
|
|
4280
|
+
json(res, 500, { error: safeMessage(error) });
|
|
4281
|
+
}
|
|
4282
|
+
}
|
|
4283
|
+
});
|
|
3584
4284
|
return () => {
|
|
3585
4285
|
disposeRefresh();
|
|
3586
4286
|
disposeCheckin();
|
|
3587
4287
|
disposeAccounts();
|
|
3588
4288
|
disposeUsage();
|
|
4289
|
+
disposeAccountCredits();
|
|
3589
4290
|
disposeProbe();
|
|
3590
4291
|
disposePool();
|
|
4292
|
+
disposeOAuth();
|
|
4293
|
+
disposeTransfer();
|
|
3591
4294
|
disposeDiagWrite();
|
|
3592
4295
|
};
|
|
3593
4296
|
}, "dsh-connect-workbuddy: Web status route");
|
|
@@ -3785,6 +4488,22 @@ const poolConfig = z.object({
|
|
|
3785
4488
|
});
|
|
3786
4489
|
const regionStateConfig = z.object({
|
|
3787
4490
|
enabled: z.boolean().default(true).description("Whether this region's provider is offered to DSH (opt-out; false withdraws it entirely)"),
|
|
4491
|
+
/**
|
|
4492
|
+
* Whether this region's credit line shows at the DSH sidebar foot.
|
|
4493
|
+
*
|
|
4494
|
+
* Per-REGION rather than one global switch, because the two sides are
|
|
4495
|
+
* independent providers the user may hold only one account on: a CN-only user
|
|
4496
|
+
* has nothing to read from the international line and vice versa, and a
|
|
4497
|
+
* single shared switch would force them to either show a permanently empty
|
|
4498
|
+
* row or lose the one that works.
|
|
4499
|
+
*
|
|
4500
|
+
* ON by default (opt-out). The plugin card is not reachable in every host —
|
|
4501
|
+
* a market plugin can own the Plugins tab and never render
|
|
4502
|
+
* `plugins.bundle.config` — so a feature that is off until discovered would be
|
|
4503
|
+
* off forever for those users. The line carries its own `×` in the sidebar,
|
|
4504
|
+
* which is reachable exactly where the feature renders.
|
|
4505
|
+
*/
|
|
4506
|
+
showCreditsInMainUi: z.boolean().default(true).description("Show this region's remaining credits at the DSH sidebar foot (opt-out; the line carries its own hide button)"),
|
|
3788
4507
|
lastCatalog: z.array(modelConfig).default([]),
|
|
3789
4508
|
enabledModelIds: z.array(z.string()).default([]),
|
|
3790
4509
|
imageModelIds: z.array(z.string()).default([]),
|
|
@@ -4140,6 +4859,25 @@ function apply(ctx, config) {
|
|
|
4140
4859
|
store: (region) => stacks[region].store,
|
|
4141
4860
|
client,
|
|
4142
4861
|
accountUsable: accountUsabilityProbe,
|
|
4862
|
+
/**
|
|
4863
|
+
* Why each of the region's accounts cannot serve right now, for the composer
|
|
4864
|
+
* panel's table.
|
|
4865
|
+
*
|
|
4866
|
+
* Runs the SAME `exclusionOf` the pool ranks by, against every account the
|
|
4867
|
+
* region knows — not only pool members. Deriving it any other way (a second
|
|
4868
|
+
* expiry comparison, or `probe.outcome` read raw) would let the panel call an
|
|
4869
|
+
* account rate-limited while the pool has already moved past its cooldown.
|
|
4870
|
+
*/
|
|
4871
|
+
accountExclusions: async (region) => {
|
|
4872
|
+
const probes = await readPoolProbes(region);
|
|
4873
|
+
const nowMs = Date.now();
|
|
4874
|
+
const out = {};
|
|
4875
|
+
for (const [accountId, probe] of Object.entries(probes)) {
|
|
4876
|
+
const excludedBy = exclusionOf(probe, nowMs);
|
|
4877
|
+
if (excludedBy !== void 0) out[accountId] = excludedBy;
|
|
4878
|
+
}
|
|
4879
|
+
return out;
|
|
4880
|
+
},
|
|
4143
4881
|
displayModels: (region) => displayModels(current(), region),
|
|
4144
4882
|
enabledModelIds: (region) => regionStateOf(current(), region).enabledModelIds ?? [],
|
|
4145
4883
|
imageModelIds: (region) => regionStateOf(current(), region).imageModelIds ?? [],
|
|
@@ -4148,6 +4886,13 @@ function apply(ctx, config) {
|
|
|
4148
4886
|
discoverModels,
|
|
4149
4887
|
regionEnabled: (region) => regionEnabled(current(), region),
|
|
4150
4888
|
/**
|
|
4889
|
+
* The region's committed sidebar-credit flag, read from the same config the
|
|
4890
|
+
* card writes. Exposed on the Host's answer because the browser settings
|
|
4891
|
+
* mirror goes stale for this namespace on the affected 0.1.7 deployments,
|
|
4892
|
+
* which made the switch look unable to turn the line off ("关闭不了").
|
|
4893
|
+
*/
|
|
4894
|
+
showCreditsInMainUi: (region) => regionStateOf(current(), region).showCreditsInMainUi !== false,
|
|
4895
|
+
/**
|
|
4151
4896
|
* One minimal request per named model, through the region's own credential.
|
|
4152
4897
|
*
|
|
4153
4898
|
* Resolved here rather than in the route so the browser half never touches a
|
|
@@ -4284,7 +5029,138 @@ function apply(ctx, config) {
|
|
|
4284
5029
|
await writePoolProbes(region, probeUpdatesOf(rows));
|
|
4285
5030
|
return rows;
|
|
4286
5031
|
},
|
|
4287
|
-
catalog: (region) => displayModels(current(), region)
|
|
5032
|
+
catalog: (region) => displayModels(current(), region),
|
|
5033
|
+
/**
|
|
5034
|
+
* Test ONE account against the region's target model.
|
|
5035
|
+
*
|
|
5036
|
+
* The per-row button's Host half. Reuses the batch's probe dependency
|
|
5037
|
+
* (same client, same probe size, same quota-refresh input) so a row
|
|
5038
|
+
* result and a batch result are the SAME measurement — only the
|
|
5039
|
+
* persistence is scoped to one id instead of every member.
|
|
5040
|
+
*
|
|
5041
|
+
* Returns undefined when the region cannot name a target model (the
|
|
5042
|
+
* same refusal the batch route reports as 409) — the card says so
|
|
5043
|
+
* rather than pretending a test ran.
|
|
5044
|
+
*/
|
|
5045
|
+
testOne: async (region, accountId) => {
|
|
5046
|
+
const target = regionTargetModel(region);
|
|
5047
|
+
if (target === void 0) return void 0;
|
|
5048
|
+
const store = stacks[region].store;
|
|
5049
|
+
const credential = await store.credentialFor(accountId).catch(() => void 0);
|
|
5050
|
+
if (credential === void 0) return {
|
|
5051
|
+
accountId,
|
|
5052
|
+
accountName: "",
|
|
5053
|
+
result: {
|
|
5054
|
+
modelId: target,
|
|
5055
|
+
outcome: "credential-rejected",
|
|
5056
|
+
status: 0,
|
|
5057
|
+
message: "no stored credential for this account"
|
|
5058
|
+
}
|
|
5059
|
+
};
|
|
5060
|
+
const accountName = (await store.accounts().catch(() => [])).find((account) => account.id === accountId)?.accountName ?? "";
|
|
5061
|
+
const result = await poolRunnerDeps(region).probe(credential, target);
|
|
5062
|
+
await writePoolProbes(region, probeUpdatesOf([{
|
|
5063
|
+
accountId,
|
|
5064
|
+
accountName,
|
|
5065
|
+
result
|
|
5066
|
+
}]));
|
|
5067
|
+
return {
|
|
5068
|
+
accountId,
|
|
5069
|
+
accountName,
|
|
5070
|
+
result
|
|
5071
|
+
};
|
|
5072
|
+
},
|
|
5073
|
+
/**
|
|
5074
|
+
* Forget ONE account's plugin-stored credential (the vault entry; the
|
|
5075
|
+
* desktop app's files are never touched). The card rescans afterwards,
|
|
5076
|
+
* so the row disappears the moment the write lands.
|
|
5077
|
+
*/
|
|
5078
|
+
removeOne: async (region, accountId) => stacks[region].store.removeAccount(accountId)
|
|
5079
|
+
},
|
|
5080
|
+
/**
|
|
5081
|
+
* OAuth QR sign-in for this card. The session lives in `src/oauth.ts`
|
|
5082
|
+
* (in-process, keyed by loginId); the Host's only extra duty is
|
|
5083
|
+
* persisting the finished credential into the region's vault — the same
|
|
5084
|
+
* store the request path reads, so the account is selectable the moment
|
|
5085
|
+
* the poll answers.
|
|
5086
|
+
*/
|
|
5087
|
+
oauth: {
|
|
5088
|
+
async start(region) {
|
|
5089
|
+
return oauthStart(region);
|
|
5090
|
+
},
|
|
5091
|
+
async poll(region, loginId) {
|
|
5092
|
+
const answer = await oauthPoll(loginId, region);
|
|
5093
|
+
if (!answer.done) return answer;
|
|
5094
|
+
if ("error" in answer) return answer;
|
|
5095
|
+
const store = stacks[region].store;
|
|
5096
|
+
const accountId = await store.addOAuthAccount({
|
|
5097
|
+
accessToken: answer.account.accessToken,
|
|
5098
|
+
refreshToken: answer.account.refreshToken,
|
|
5099
|
+
expiresAtMs: answer.account.expiresAtMs,
|
|
5100
|
+
...answer.account.refreshExpiresAtMs === void 0 ? {} : { refreshExpiresAtMs: answer.account.refreshExpiresAtMs },
|
|
5101
|
+
domain: answer.account.domain,
|
|
5102
|
+
uid: answer.account.uid,
|
|
5103
|
+
...answer.account.nickname === void 0 ? {} : { nickname: answer.account.nickname },
|
|
5104
|
+
...answer.account.uin === void 0 ? {} : { uin: answer.account.uin },
|
|
5105
|
+
...answer.account.enterpriseId === void 0 ? {} : { enterpriseId: answer.account.enterpriseId }
|
|
5106
|
+
}).catch((error) => {
|
|
5107
|
+
throw new Error("the account was signed in but could not be saved: " + (error instanceof Error ? error.message : String(error)));
|
|
5108
|
+
});
|
|
5109
|
+
return {
|
|
5110
|
+
done: true,
|
|
5111
|
+
accountId,
|
|
5112
|
+
accountName: (await store.accounts().catch(() => [])).find((account) => account.id === accountId)?.accountName ?? answer.account.nickname ?? ""
|
|
5113
|
+
};
|
|
5114
|
+
}
|
|
5115
|
+
},
|
|
5116
|
+
/**
|
|
5117
|
+
* Batch credential export / import, in the transfer file format the
|
|
5118
|
+
* sibling credential managers share. The Host owns every byte of the
|
|
5119
|
+
* vault; the route only ferries parsed answers.
|
|
5120
|
+
*/
|
|
5121
|
+
transfer: {
|
|
5122
|
+
async exportAccounts(region, accountIds) {
|
|
5123
|
+
const store = stacks[region].store;
|
|
5124
|
+
const accounts = await store.accounts().catch(() => []);
|
|
5125
|
+
const wanted = new Set(accountIds);
|
|
5126
|
+
const records = [];
|
|
5127
|
+
for (const account of accounts) {
|
|
5128
|
+
if (!wanted.has(account.id)) continue;
|
|
5129
|
+
const credential = await store.credentialFor(account.id).catch(() => void 0);
|
|
5130
|
+
if (credential === void 0) continue;
|
|
5131
|
+
records.push(transferRecordOf(credential, {
|
|
5132
|
+
auth: {
|
|
5133
|
+
accessToken: credential.accessToken,
|
|
5134
|
+
refreshToken: credential.refreshToken,
|
|
5135
|
+
expiresAt: credential.expiresAtMs > 0 ? Math.floor(credential.expiresAtMs / 1e3) : 0,
|
|
5136
|
+
...credential.refreshExpiresAtMs === void 0 ? {} : { refreshExpiresAt: Math.floor(credential.refreshExpiresAtMs / 1e3) },
|
|
5137
|
+
...credential.lastRefreshAtMs === void 0 ? {} : { lastRefreshTime: Math.floor(credential.lastRefreshAtMs / 1e3) },
|
|
5138
|
+
domain: credential.domain
|
|
5139
|
+
},
|
|
5140
|
+
account: {
|
|
5141
|
+
uid: credential.uid,
|
|
5142
|
+
...credential.enterpriseId === void 0 ? {} : { enterpriseId: credential.enterpriseId },
|
|
5143
|
+
...credential.nickname === void 0 ? {} : { nickname: credential.nickname },
|
|
5144
|
+
...credential.uin === void 0 ? {} : { uin: credential.uin }
|
|
5145
|
+
}
|
|
5146
|
+
}));
|
|
5147
|
+
}
|
|
5148
|
+
return { accounts: records };
|
|
5149
|
+
},
|
|
5150
|
+
async previewImport(region, text) {
|
|
5151
|
+
return previewTransferFile(text);
|
|
5152
|
+
},
|
|
5153
|
+
async importAccounts(region, text, indexes) {
|
|
5154
|
+
const { changes, imported, skipped } = mergeTransferRecords(parseTransferFile(text), indexes, region, (domain) => knownRegionOf(domain));
|
|
5155
|
+
const store = stacks[region].store;
|
|
5156
|
+
for (const [id, credential] of changes) await store.addOAuthAccount(credential).catch((error) => {
|
|
5157
|
+
throw new Error("imported account " + id + " could not be stored: " + (error instanceof Error ? error.message : String(error)));
|
|
5158
|
+
});
|
|
5159
|
+
return {
|
|
5160
|
+
imported,
|
|
5161
|
+
skipped
|
|
5162
|
+
};
|
|
5163
|
+
}
|
|
4288
5164
|
}
|
|
4289
5165
|
}));
|
|
4290
5166
|
/**
|
|
@@ -4512,6 +5388,15 @@ function apply(ctx, config) {
|
|
|
4512
5388
|
creditsSnapshotAt[region] = Date.now();
|
|
4513
5389
|
return rows;
|
|
4514
5390
|
}
|
|
5391
|
+
/**
|
|
5392
|
+
* The model a per-row test would use right now: the SAME resolution the
|
|
5393
|
+
* batch route performs (`resolveTargetModel` over the region's displayed
|
|
5394
|
+
* catalog and the saved preference), so a row test and a batch test never
|
|
5395
|
+
* measure different models. Undefined mirrors the route's 409 states.
|
|
5396
|
+
*/
|
|
5397
|
+
const regionTargetModel = (region) => {
|
|
5398
|
+
return resolveTargetModel(displayModels(current(), region), poolPreferencesOf(current(), region).targetModelId).modelId;
|
|
5399
|
+
};
|
|
4515
5400
|
/** One region's pool preferences, with the schema's defaults applied. */
|
|
4516
5401
|
const poolPreferencesOf = (config, region) => {
|
|
4517
5402
|
const pool = regionStateOf(config, region).pool;
|
|
@@ -4693,4 +5578,4 @@ function apply(ctx, config) {
|
|
|
4693
5578
|
});
|
|
4694
5579
|
}
|
|
4695
5580
|
//#endregion
|
|
4696
|
-
export { CREDENTIAL_REJECTED_CODE, Config, DEFAULT_PROBE_INPUT_TOKENS, ENCRYPTED_CREDENTIAL_CODE, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, NATIVE_MODALITY_BY_MODEL_ID, PROBE_INPUT_TOKEN_CHOICES, PROBE_MAX_TOKENS, PROBE_SYSTEM_PROMPT, REFUSES_OFF_MODEL_IDS, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_APP_EXECUTABLE_ENV, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROBE_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, WorkBuddyCatalog, WorkBuddyCredentialRejectedError, WorkBuddyCredentialStore, WorkBuddyEncryptedCredentialError, WorkBuddyUpstreamClient, apply, asVolatile, authFileName, classifyUpstreamError, clearAtRestKeyCache, clearHostHeartbeat, cooldownOf, createAccountUsabilityProbe, createWorkBuddyAdapter, createWorkBuddyShim, creditOfStream, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, defaultOffModelIds, deriveAtRestKey, deriveAtRestKeyId, deriveCatalog, fallbackModelsFor, fetchAtRestKeyPayload, findWorkbuddyAppExecutable, hasEncryptedCredentialFields, imageDefaultFor, inject, isCredentialRejectedError, isEncryptedCredentialError, isEncryptedFieldWrapper, isHeartbeatProcessAlive, isWorkbuddyBundle, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, macosBundleExecutable, macosNestedAppBundles, name, nativeModalityOf, nextRegionEnabled, nextRegionSlots, offDefaultFor, openEncryptedField, outcomeOfFailure, parseCreditMultiplier, parseReasoning, parseRetryAfter, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, probeModel, probeRequestBody, probeSucceeded, processStartTimeMs, readAtRestKey, readHostHeartbeat, regionCleared, regionEnabled, regionEnabledOf, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, resolveCredentialRecovery, selectAccountFor, settingsNamespaceOf, toPersistedWorkBuddyModel, unwrapVolatileDeep, withEffectiveOff, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyAppExecutableCandidates, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
|
|
5581
|
+
export { CREDENTIAL_REJECTED_CODE, Config, DEFAULT_PROBE_INPUT_TOKENS, ENCRYPTED_CREDENTIAL_CODE, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, NATIVE_MODALITY_BY_MODEL_ID, PROBE_INPUT_TOKEN_CHOICES, PROBE_MAX_TOKENS, PROBE_SYSTEM_PROMPT, REFUSES_OFF_MODEL_IDS, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_APP_EXECUTABLE_ENV, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROBE_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, WorkBuddyCatalog, WorkBuddyCredentialRejectedError, WorkBuddyCredentialStore, WorkBuddyEncryptedCredentialError, WorkBuddyUpstreamClient, apply, asVolatile, authFileName, classifyUpstreamError, clearAtRestKeyCache, clearHostHeartbeat, cooldownOf, createAccountUsabilityProbe, createWorkBuddyAdapter, createWorkBuddyShim, creditOfStream, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, defaultOffModelIds, deriveAtRestKey, deriveAtRestKeyId, deriveCatalog, fallbackModelsFor, fetchAtRestKeyPayload, findWorkbuddyAppExecutable, hasEncryptedCredentialFields, imageDefaultFor, inject, isCredentialRejectedError, isEncryptedCredentialError, isEncryptedFieldWrapper, isHeartbeatProcessAlive, isWorkbuddyBundle, knownRegionOf, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, macosBundleExecutable, macosNestedAppBundles, name, nativeModalityOf, nextRegionEnabled, nextRegionSlots, offDefaultFor, openEncryptedField, outcomeOfFailure, parseCreditMultiplier, parseReasoning, parseRetryAfter, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, probeModel, probeRequestBody, probeSucceeded, processStartTimeMs, readAtRestKey, readHostHeartbeat, regionCleared, regionEnabled, regionEnabledOf, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, resolveCredentialRecovery, selectAccountFor, settingsNamespaceOf, toPersistedWorkBuddyModel, unwrapVolatileDeep, withEffectiveOff, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyAppExecutableCandidates, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
|