hiwork-core 0.0.2 → 0.0.6

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/lib/index.js CHANGED
@@ -3137,9 +3137,21 @@ function isUnloading(ctx) {
3137
3137
  }
3138
3138
 
3139
3139
  // src/version.ts
3140
- var PKG_VERSION = false ? "dev" : "0.0.2";
3140
+ var PKG_VERSION = false ? "dev" : "0.0.6";
3141
3141
 
3142
3142
  // src/ai-router-client.ts
3143
+ var AUTH_AUTHORIZE_PATH = "/api/auth/authorize";
3144
+ var AUTH_TOKEN_PATH = "/api/auth/token";
3145
+ var AUTH_SESSIONS_PATH = "/api/auth/sessions";
3146
+ var ME_ANALYTICS_SUMMARY_PATH = "/api/me/analytics/summary";
3147
+ var ME_ANALYTICS_TRENDS_PATH = "/api/me/analytics/trends";
3148
+ var ME_ANALYTICS_MODEL_TRENDS_PATH = "/api/me/analytics/model-trends";
3149
+ var ME_KEY_RECHARGES_PATH = "/api/me/key/recharges";
3150
+ var ME_COST_ESTIMATE_PATH = "/api/me/cost-estimate";
3151
+ var ME_DAILY_QUOTE_PATH = "/api/me/daily-quote";
3152
+ var ME_DAILY_QUOTE_RANDOM_PATH = "/api/me/daily-quote/random";
3153
+ var DEFAULT_CLIENT_ID = "hiwork-desktop";
3154
+ var DEFAULT_REDIRECT_URI = "hiwork://auth/callback";
3143
3155
  var GatewayError = class extends Error {
3144
3156
  constructor(code, message, status) {
3145
3157
  super(message);
@@ -3148,13 +3160,32 @@ var GatewayError = class extends Error {
3148
3160
  this.name = "GatewayError";
3149
3161
  }
3150
3162
  };
3163
+ function buildAuthorizeURL(opts) {
3164
+ const params = [
3165
+ ["client_id", opts.clientId ?? DEFAULT_CLIENT_ID],
3166
+ ["redirect_uri", opts.redirectUri ?? DEFAULT_REDIRECT_URI],
3167
+ ["code_challenge", opts.codeChallenge],
3168
+ ["code_challenge_method", "S256"],
3169
+ ["state", opts.state],
3170
+ ["device_id", opts.deviceId],
3171
+ ["device_name", opts.deviceName],
3172
+ ["platform", opts.platform],
3173
+ ["app_version", opts.appVersion]
3174
+ ];
3175
+ const query = params.map(([key, value]) => `${key}=${encodeURIComponent(value)}`).join("&");
3176
+ return `${opts.baseURL}${AUTH_AUTHORIZE_PATH}?${query}`;
3177
+ }
3151
3178
  var AiRouterClient = class {
3152
3179
  constructor(baseURL, timeoutMs = 1e4) {
3153
3180
  this.baseURL = baseURL;
3154
3181
  this.timeoutMs = timeoutMs;
3155
3182
  }
3156
- async login(username, password) {
3157
- const res = await this.request("/api/admin/auth/login", { method: "POST", body: { username, password } });
3183
+ async login(username, password, device) {
3184
+ const res = await this.request("/api/admin/auth/login", {
3185
+ method: "POST",
3186
+ body: { username, password },
3187
+ headers: deviceHeaders(device)
3188
+ });
3158
3189
  const sid = extractSessionId(res.headers);
3159
3190
  if (res.status === 429) throw new GatewayError("RATE_LIMITED", "\u767B\u5F55\u5C1D\u8BD5\u8FC7\u4E8E\u9891\u7E41\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5", 429);
3160
3191
  if (res.status === 401) throw new GatewayError("AUTH_FAILED", "\u7528\u6237\u540D\u6216\u5BC6\u7801\u9519\u8BEF", 401);
@@ -3163,6 +3194,45 @@ var AiRouterClient = class {
3163
3194
  const user = await res.json();
3164
3195
  return { sessionCookie: `session_id=${sid}`, user };
3165
3196
  }
3197
+ /**
3198
+ * POST /api/auth/token:用一次性 code + PKCE verifier 换取会话。
3199
+ * 返回 `session_id=<sid>`(与 login 同形状,编排层可直接复用),user 原样返回。
3200
+ * 任何日志/异常信息都不得包含 code / code_verifier / session_id 明文。
3201
+ */
3202
+ async exchangeCode(input) {
3203
+ const res = await this.requestOrUnreachable(AUTH_TOKEN_PATH, {
3204
+ method: "POST",
3205
+ body: { code: input.code, code_verifier: input.codeVerifier, device_id: input.deviceId }
3206
+ });
3207
+ if (res.status === 400) {
3208
+ const payload = await readJson(res);
3209
+ if (isInvalidGrant(payload)) throw new GatewayError("AUTH_FAILED", "\u6388\u6743\u5931\u8D25\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55", 400);
3210
+ throw new GatewayError("GATEWAY_UNREACHABLE", `\u7F51\u5173\u8FD4\u56DE\u9519\u8BEF\uFF08HTTP ${res.status}\uFF09`, res.status);
3211
+ }
3212
+ if (!res.ok) throw new GatewayError("GATEWAY_UNREACHABLE", `\u7F51\u5173\u8FD4\u56DE\u9519\u8BEF\uFF08HTTP ${res.status}\uFF09`, res.status);
3213
+ const data = await readJson(res);
3214
+ if (data === void 0 || data.session_id === void 0 || data.session_id === "") {
3215
+ throw new GatewayError("GATEWAY_UNREACHABLE", "\u7F51\u5173\u5151\u6362\u6210\u529F\u4F46\u672A\u8FD4\u56DE\u4F1A\u8BDD\u6807\u8BC6", res.status);
3216
+ }
3217
+ return { sessionCookie: `session_id=${data.session_id}`, user: data.user };
3218
+ }
3219
+ /** GET /api/auth/sessions:当前账号的登录设备列表。 */
3220
+ async listSessions(sessionCookie) {
3221
+ const res = await this.requestOrUnreachable(AUTH_SESSIONS_PATH, { method: "GET", sessionCookie });
3222
+ if (res.status === 401) throw new GatewayError("SESSION_EXPIRED", "\u7F51\u5173\u4F1A\u8BDD\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55", 401);
3223
+ if (!res.ok) throw new GatewayError("GATEWAY_UNREACHABLE", `\u7F51\u5173\u8FD4\u56DE\u9519\u8BEF\uFF08HTTP ${res.status}\uFF09`, res.status);
3224
+ return await res.json();
3225
+ }
3226
+ /** DELETE /api/auth/sessions/:id:远程吊销指定会话(204 成功;404 表示已不存在)。 */
3227
+ async revokeSession(sessionCookie, sessionId) {
3228
+ const res = await this.requestOrUnreachable(`${AUTH_SESSIONS_PATH}/${encodeURIComponent(sessionId)}`, {
3229
+ method: "DELETE",
3230
+ sessionCookie
3231
+ });
3232
+ if (res.status === 401) throw new GatewayError("SESSION_EXPIRED", "\u7F51\u5173\u4F1A\u8BDD\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55", 401);
3233
+ if (res.status === 404) throw new GatewayError("NOT_FOUND", "\u4F1A\u8BDD\u4E0D\u5B58\u5728\u6216\u5DF2\u88AB\u540A\u9500", 404);
3234
+ if (!res.ok) throw new GatewayError("GATEWAY_UNREACHABLE", `\u7F51\u5173\u8FD4\u56DE\u9519\u8BEF\uFF08HTTP ${res.status}\uFF09`, res.status);
3235
+ }
3166
3236
  async getMyKey(sessionCookie, keyId) {
3167
3237
  const qs = keyId === void 0 ? "" : `?key_id=${encodeURIComponent(keyId)}`;
3168
3238
  const res = await this.request(`/api/me/key${qs}`, { method: "GET", sessionCookie });
@@ -3192,10 +3262,85 @@ var AiRouterClient = class {
3192
3262
  } catch {
3193
3263
  }
3194
3264
  }
3265
+ /** GET /api/me/analytics/summary:按模型聚合的用量汇总。 */
3266
+ async getMySummary(sessionCookie, keyId, range) {
3267
+ const query = meQuery([["key_id", keyId], ["range", range]]);
3268
+ return await this.getMeJson(`${ME_ANALYTICS_SUMMARY_PATH}${query}`, sessionCookie);
3269
+ }
3270
+ /** GET /api/me/analytics/trends:token/费用走势。 */
3271
+ async getMyTrends(sessionCookie, keyId, range) {
3272
+ const query = meQuery([["key_id", keyId], ["range", range]]);
3273
+ return await this.getMeJson(`${ME_ANALYTICS_TRENDS_PATH}${query}`, sessionCookie);
3274
+ }
3275
+ /** GET /api/me/analytics/model-trends:按模型拆分的走势。 */
3276
+ async getMyModelTrends(sessionCookie, keyId, range) {
3277
+ const query = meQuery([["key_id", keyId], ["range", range]]);
3278
+ return await this.getMeJson(`${ME_ANALYTICS_MODEL_TRENDS_PATH}${query}`, sessionCookie);
3279
+ }
3280
+ /** GET /api/me/key/recharges:当前 Key 的充值台账(可按模型过滤)。 */
3281
+ async getMyKeyRecharges(sessionCookie, keyId, modelName) {
3282
+ const query = meQuery([["key_id", keyId], ["model_name", modelName]]);
3283
+ return await this.getMeJson(`${ME_KEY_RECHARGES_PATH}${query}`, sessionCookie);
3284
+ }
3285
+ /** GET /api/me/daily-quote:当日条目(服务端东八区取模轮换,全网同一天同一条)。 */
3286
+ async getMyDailyQuote(sessionCookie) {
3287
+ return await this.getMeJson(ME_DAILY_QUOTE_PATH, sessionCookie);
3288
+ }
3289
+ /** GET /api/me/daily-quote/random:换一句(exclude 为当前条目 id;非法值由网关按缺省处理)。 */
3290
+ async getMyDailyQuoteRandom(sessionCookie, excludeId) {
3291
+ const query = meQuery([["exclude", excludeId === void 0 ? void 0 : String(excludeId)]]);
3292
+ return await this.getMeJson(`${ME_DAILY_QUOTE_RANDOM_PATH}${query}`, sessionCookie);
3293
+ }
3294
+ /**
3295
+ * POST /api/me/cost-estimate:网关权威计价。请求体里的四个 token 桶原样上报,
3296
+ * `input_tokens` 不含 cache;任何折算与单价计算都在网关侧完成。
3297
+ */
3298
+ async estimateCost(sessionCookie, items) {
3299
+ const res = await this.requestOrUnreachable(ME_COST_ESTIMATE_PATH, {
3300
+ method: "POST",
3301
+ sessionCookie,
3302
+ body: { items }
3303
+ });
3304
+ if (res.status === 401) throw new GatewayError("SESSION_EXPIRED", "\u7F51\u5173\u4F1A\u8BDD\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55", 401);
3305
+ if (res.status === 403) {
3306
+ throw new GatewayError("NO_KEY_ASSIGNED", "\u5F53\u524D\u8D26\u53F7\u5C1A\u672A\u88AB\u5206\u914D\u865A\u62DF KEY\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u5206\u914D KEY \u540E\u518D\u767B\u5F55/\u5237\u65B0", 403);
3307
+ }
3308
+ if (!res.ok) throw new GatewayError("GATEWAY_UNREACHABLE", `\u7F51\u5173\u8FD4\u56DE\u9519\u8BEF\uFF08HTTP ${res.status}\uFF09`, res.status);
3309
+ return await parseMeJson(res);
3310
+ }
3311
+ /**
3312
+ * /api/me 自助端点的 GET 公共路径:网络异常 → GATEWAY_UNREACHABLE,
3313
+ * 401/403/400 映射为稳定 code,响应体非 JSON 时不把原文带进异常。
3314
+ */
3315
+ async getMeJson(path, sessionCookie) {
3316
+ const res = await this.requestOrUnreachable(path, { method: "GET", sessionCookie });
3317
+ if (res.status === 401) throw new GatewayError("SESSION_EXPIRED", "\u7F51\u5173\u4F1A\u8BDD\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55", 401);
3318
+ if (res.status === 403) {
3319
+ throw new GatewayError("NO_KEY_ASSIGNED", "\u5F53\u524D\u8D26\u53F7\u5C1A\u672A\u88AB\u5206\u914D\u865A\u62DF KEY\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u5206\u914D KEY \u540E\u518D\u767B\u5F55/\u5237\u65B0", 403);
3320
+ }
3321
+ if (res.status === 400) {
3322
+ throw new GatewayError("KEY_INVALID", "\u6240\u9009 KEY \u65E0\u6548\u6216\u5DF2\u88AB\u5220\u9664\uFF0C\u8BF7\u91CD\u65B0\u9009\u62E9 KEY \u540E\u5237\u65B0", 400);
3323
+ }
3324
+ if (!res.ok) throw new GatewayError("GATEWAY_UNREACHABLE", `\u7F51\u5173\u8FD4\u56DE\u9519\u8BEF\uFF08HTTP ${res.status}\uFF09`, res.status);
3325
+ return await parseMeJson(res);
3326
+ }
3327
+ /**
3328
+ * 浏览器授权相关端点专用:网络层异常(连接失败 / 超时 / DNS)统一映射为
3329
+ * GATEWAY_UNREACHABLE,避免裸 fetch 错误漏到编排层。
3330
+ * login 等既有方法不经过此处,行为保持不变。
3331
+ */
3332
+ async requestOrUnreachable(path, opts) {
3333
+ try {
3334
+ return await this.request(path, opts);
3335
+ } catch {
3336
+ throw new GatewayError("GATEWAY_UNREACHABLE", "\u7F51\u5173\u4E0D\u53EF\u8FBE\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u540E\u91CD\u8BD5");
3337
+ }
3338
+ }
3195
3339
  async request(path, opts) {
3196
3340
  const headers = { "user-agent": `hiwork-core/${PKG_VERSION}` };
3197
3341
  if (opts.body !== void 0) headers["content-type"] = "application/json";
3198
3342
  if (opts.sessionCookie !== void 0) headers["cookie"] = opts.sessionCookie;
3343
+ Object.assign(headers, opts.headers ?? {});
3199
3344
  const init = {
3200
3345
  method: opts.method,
3201
3346
  headers,
@@ -3205,6 +3350,33 @@ var AiRouterClient = class {
3205
3350
  return fetch(`${this.baseURL}${path}`, init);
3206
3351
  }
3207
3352
  };
3353
+ function headerSafeValue(value) {
3354
+ if (typeof value !== "string") return void 0;
3355
+ let out = "";
3356
+ for (const char of value) {
3357
+ const code = char.codePointAt(0) ?? 0;
3358
+ if (code >= 32 && code <= 126) out += char;
3359
+ }
3360
+ const trimmed = out.trim();
3361
+ return trimmed === "" ? void 0 : trimmed;
3362
+ }
3363
+ function safeDeviceId(value) {
3364
+ if (typeof value !== "string") return void 0;
3365
+ const trimmed = value.trim();
3366
+ return /^[\x21-\x7e]+$/.test(trimmed) ? trimmed : void 0;
3367
+ }
3368
+ function deviceHeaders(device) {
3369
+ const deviceId = device?.deviceId === void 0 ? void 0 : safeDeviceId(device.deviceId);
3370
+ if (deviceId === void 0) return {};
3371
+ const headers = { "X-HiWork-Device-Id": deviceId };
3372
+ const deviceName = device?.deviceName === void 0 ? void 0 : headerSafeValue(device.deviceName);
3373
+ if (deviceName !== void 0) headers["X-HiWork-Device-Name"] = deviceName;
3374
+ const platform = device?.platform === void 0 ? void 0 : headerSafeValue(device.platform);
3375
+ if (platform !== void 0) headers["X-HiWork-Platform"] = platform;
3376
+ const appVersion = device?.appVersion === void 0 ? void 0 : headerSafeValue(device.appVersion);
3377
+ if (appVersion !== void 0) headers["X-HiWork-App-Version"] = appVersion;
3378
+ return headers;
3379
+ }
3208
3380
  function extractSessionId(headers) {
3209
3381
  const raws = typeof headers.getSetCookie === "function" ? headers.getSetCookie() : [headers.get("set-cookie")].filter((raw) => raw !== null);
3210
3382
  for (const raw of raws) {
@@ -3213,22 +3385,35 @@ function extractSessionId(headers) {
3213
3385
  }
3214
3386
  return void 0;
3215
3387
  }
3216
-
3217
- // src/default-workspace.ts
3218
- import { mkdir } from "node:fs/promises";
3219
- import { homedir } from "node:os";
3220
- import { join } from "node:path";
3221
- var DEFAULT_WORKSPACE_DIR = join(homedir(), "HiWork", "Default");
3222
- var DEFAULT_WORKSPACE_TITLE = "\u9ED8\u8BA4\u5DE5\u4F5C\u533A";
3223
- async function ensureDefaultWorkspace(registry, warn, dir = DEFAULT_WORKSPACE_DIR) {
3388
+ async function readJson(res) {
3224
3389
  try {
3225
- if (registry.list().length > 0) return;
3226
- await mkdir(dir, { recursive: true });
3227
- await registry.create(dir, DEFAULT_WORKSPACE_TITLE);
3228
- } catch (error) {
3229
- warn(`hiwork-core: ensure default workspace failed: ${error instanceof Error ? error.message : String(error)}`);
3390
+ return await res.json();
3391
+ } catch {
3392
+ return void 0;
3393
+ }
3394
+ }
3395
+ function meQuery(params) {
3396
+ const pairs = [];
3397
+ for (const [key, value] of params) {
3398
+ if (value === void 0) continue;
3399
+ pairs.push(`${key}=${encodeURIComponent(value)}`);
3400
+ }
3401
+ return pairs.length === 0 ? "" : `?${pairs.join("&")}`;
3402
+ }
3403
+ async function parseMeJson(res) {
3404
+ const data = await readJson(res);
3405
+ if (data === void 0 || data === null) {
3406
+ throw new GatewayError("GATEWAY_UNREACHABLE", "\u7F51\u5173\u8FD4\u56DE\u7684\u54CD\u5E94\u65E0\u6CD5\u89E3\u6790\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5", res.status);
3230
3407
  }
3408
+ return data;
3231
3409
  }
3410
+ function isInvalidGrant(payload) {
3411
+ return typeof payload === "object" && payload !== null && payload.error === "invalid_grant";
3412
+ }
3413
+
3414
+ // src/browser-login.ts
3415
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
3416
+ import { hostname } from "node:os";
3232
3417
 
3233
3418
  // src/provision.ts
3234
3419
  var API_KEY_REF = "AI_ROUTER_API_KEY";
@@ -3236,22 +3421,34 @@ var SESSION_REF = "AI_ROUTER_SESSION";
3236
3421
  var PENDING_PASSWORD_REF = "AI_ROUTER_PENDING_PASSWORD";
3237
3422
  var REMEMBERED_PASSWORD_REF = "AI_ROUTER_REMEMBERED_PASSWORD";
3238
3423
  var PROVIDER_ROUTES = {
3239
- "ai-router": "openai-responses",
3240
- "ai-router-anthropic": "anthropic-messages"
3424
+ "ai-router-chat": "openai-completions",
3425
+ "ai-router-anthropic": "anthropic-messages",
3426
+ "ai-router": "openai-responses"
3241
3427
  };
3242
3428
  var DSH_SUPPORTED_MODALITIES = ["text", "image"];
3243
- var PROVIDER_ROUTE_NAMES = ["ai-router", "ai-router-anthropic"];
3429
+ var PROVIDER_ROUTE_NAMES = [
3430
+ "ai-router-chat",
3431
+ "ai-router-anthropic",
3432
+ "ai-router"
3433
+ ];
3244
3434
  var LEGACY_PROVIDER_ROUTE_NAMES = ["ai-router-responses"];
3245
3435
  var ROUTE_DISPLAY_NAMES = {
3246
- "ai-router": "AI Router",
3247
- "ai-router-anthropic": "AI Router \xB7 Anthropic"
3436
+ "ai-router-chat": "\u7EBF\u8DEFC",
3437
+ "ai-router-anthropic": "\u7EBF\u8DEFM",
3438
+ "ai-router": "\u7EBF\u8DEFR"
3248
3439
  };
3440
+ var GATEWAY_PROTOCOLS = ["openai_chat_completions", "anthropic_messages", "openai_responses"];
3249
3441
  var GATEWAY_PROTOCOL_TO_ROUTE = {
3250
- openai_chat_completions: "ai-router",
3442
+ openai_responses: "ai-router",
3251
3443
  anthropic_messages: "ai-router-anthropic",
3252
- openai_responses: "ai-router"
3444
+ openai_chat_completions: "ai-router-chat"
3253
3445
  };
3254
- var ROUTE_PRIORITY = ["ai-router", "ai-router-anthropic"];
3446
+ function routeToProtocol(route) {
3447
+ for (const protocol of GATEWAY_PROTOCOLS) {
3448
+ if (GATEWAY_PROTOCOL_TO_ROUTE[protocol] === route) return protocol;
3449
+ }
3450
+ return void 0;
3451
+ }
3255
3452
  function computeModelIds(budgets) {
3256
3453
  return [...new Set(
3257
3454
  budgets.filter((b) => b.model_status !== "inactive").map((b) => b.model_name).filter((id) => typeof id === "string" && id.length > 0)
@@ -3296,17 +3493,24 @@ function computeRouteModels(budgets, mapping) {
3296
3493
  const routes = {};
3297
3494
  const unmatched = [];
3298
3495
  for (const model of computeModelDescriptors(budgets)) {
3299
- const route = ROUTE_PRIORITY.find((r) => mapping.some((item) => matchesRoute(item, r, model.id)));
3300
- if (route === void 0) {
3301
- unmatched.push(model);
3302
- continue;
3496
+ let matched = false;
3497
+ for (const route of PROVIDER_ROUTE_NAMES) {
3498
+ if (!mapping.some((item) => matchesRoute(item, route, model.id))) continue;
3499
+ (routes[route] ??= []).push(model);
3500
+ matched = true;
3303
3501
  }
3304
- ;
3305
- (routes[route] ??= []).push(model);
3502
+ if (!matched) unmatched.push(model);
3306
3503
  }
3307
- if (unmatched.length > 0) routes["ai-router"] = [...routes["ai-router"] ?? [], ...unmatched];
3504
+ if (unmatched.length > 0) routes["ai-router-chat"] = [...routes["ai-router-chat"] ?? [], ...unmatched];
3308
3505
  return routes;
3309
3506
  }
3507
+ function pickFallbackDefaultModel(routeModels) {
3508
+ for (const route of PROVIDER_ROUTE_NAMES) {
3509
+ const first = routeModels[route]?.[0];
3510
+ if (first !== void 0) return { provider: route, model: first.id };
3511
+ }
3512
+ return void 0;
3513
+ }
3310
3514
  function protocolBaseURL(baseURL, protocol) {
3311
3515
  const bare = baseURL.replace(/\/+$/, "");
3312
3516
  return protocol === "anthropic-messages" ? bare : `${bare}/v1`;
@@ -3333,7 +3537,7 @@ function buildPiAiProviderProfiles(opts) {
3333
3537
  return profiles;
3334
3538
  }
3335
3539
 
3336
- // src/orchestration.ts
3540
+ // src/provision-session.ts
3337
3541
  function computeBudgets(budgets) {
3338
3542
  return budgets.filter((b) => typeof b.model_name === "string" && b.model_name.length > 0).filter((b) => b.model_status !== "inactive").map((b) => ({
3339
3543
  model_name: b.model_name,
@@ -3356,6 +3560,385 @@ function toLoginError(error, fallback) {
3356
3560
  if (error instanceof GatewayError) return { code: error.code, message: error.message };
3357
3561
  return { code: "GATEWAY_UNREACHABLE", message: fallback };
3358
3562
  }
3563
+ async function provisionSession(opts) {
3564
+ const keys = await opts.gateway.getMyKeys(opts.sessionCookie);
3565
+ const key = await opts.gateway.getMyKey(opts.sessionCookie, opts.value.keyId);
3566
+ const modelIds = computeModelIds(key.budgets);
3567
+ await opts.credentials.set(API_KEY_REF, key.key.key);
3568
+ await opts.onApiKeyWritten?.();
3569
+ const routeModels = modelIds.length > 0 ? computeRouteModels(key.budgets, key.model_mapping ?? []) : void 0;
3570
+ await opts.piAi.applyAiRouterProfiles(
3571
+ routeModels === void 0 ? null : buildPiAiProviderProfiles({ baseURL: opts.baseURL, routeModels })
3572
+ );
3573
+ if (routeModels !== void 0) await opts.defaultModel?.ensure(routeModels);
3574
+ const displayName = opts.user.display_name?.trim() || void 0;
3575
+ const set2 = {
3576
+ ...opts.username === void 0 ? {} : { username: opts.username },
3577
+ ...displayName === void 0 ? {} : { displayName },
3578
+ lastLogin: {
3579
+ at: (/* @__PURE__ */ new Date()).toISOString(),
3580
+ modelCount: modelIds.length,
3581
+ gatewayName: key.gateway_name ?? ""
3582
+ },
3583
+ budgets: computeBudgets(key.budgets),
3584
+ keys: computeKeys(keys)
3585
+ };
3586
+ const unset = [...opts.requestUnset ?? []];
3587
+ if (displayName === void 0) unset.push("displayName");
3588
+ if (key.key.enabled === false) {
3589
+ set2.lastError = { code: "KEY_DISABLED", message: "Key \u5F53\u524D\u88AB\u7981\u7528\uFF0C\u8C03\u7528\u4F1A\u5931\u8D25" };
3590
+ } else {
3591
+ unset.push("lastError");
3592
+ }
3593
+ await opts.state.patch({ set: set2, unset });
3594
+ await opts.ensureWorkspace?.();
3595
+ }
3596
+
3597
+ // src/browser-login.ts
3598
+ var BROWSER_LOGIN_TTL_MS = 10 * 60 * 1e3;
3599
+ var DEVICE_ID_REF = "hiwork_device_id";
3600
+ var NO_BASE_URL_ERROR = {
3601
+ code: "GATEWAY_UNREACHABLE",
3602
+ message: "\u7F51\u5173\u5730\u5740\u672A\u914D\u7F6E\uFF0C\u65E0\u6CD5\u53D1\u8D77\u6D4F\u89C8\u5668\u767B\u5F55"
3603
+ };
3604
+ var pushedDeviceInfo;
3605
+ var pendingAuth;
3606
+ function setDeviceInfo(info) {
3607
+ const deviceId = typeof info.deviceId === "string" ? info.deviceId.trim() : "";
3608
+ if (deviceId === "") return;
3609
+ const deviceName = typeof info.deviceName === "string" ? info.deviceName.trim() : "";
3610
+ const appVersion = typeof info.appVersion === "string" ? info.appVersion.trim() : "";
3611
+ pushedDeviceInfo = {
3612
+ deviceId,
3613
+ ...deviceName === "" ? {} : { deviceName },
3614
+ ...appVersion === "" ? {} : { appVersion }
3615
+ };
3616
+ }
3617
+ function hostName() {
3618
+ try {
3619
+ return hostname().trim();
3620
+ } catch {
3621
+ return "";
3622
+ }
3623
+ }
3624
+ async function resolveDeviceInfo(credentials) {
3625
+ const platform = process.platform;
3626
+ const appVersion = PKG_VERSION;
3627
+ const pushed = pushedDeviceInfo;
3628
+ if (pushed !== void 0) {
3629
+ const stored2 = await credentials.resolve(DEVICE_ID_REF).catch(() => void 0);
3630
+ if (stored2?.value.trim() !== pushed.deviceId) {
3631
+ await credentials.set(DEVICE_ID_REF, pushed.deviceId).catch(() => {
3632
+ });
3633
+ }
3634
+ return { deviceId: pushed.deviceId, ...pushed.deviceName === void 0 ? {} : { deviceName: pushed.deviceName }, platform, appVersion };
3635
+ }
3636
+ const stored = await credentials.resolve(DEVICE_ID_REF).catch(() => void 0);
3637
+ const storedId = stored?.value.trim();
3638
+ if (storedId !== void 0 && storedId !== "") {
3639
+ return { deviceId: storedId, deviceName: hostName(), platform, appVersion };
3640
+ }
3641
+ const generated = randomUUID();
3642
+ await credentials.set(DEVICE_ID_REF, generated).catch(() => {
3643
+ });
3644
+ return { deviceId: generated, deviceName: hostName(), platform, appVersion };
3645
+ }
3646
+ function randomToken(bytes) {
3647
+ return randomBytes(bytes).toString("base64url");
3648
+ }
3649
+ function pkceChallenge(codeVerifier) {
3650
+ return createHash("sha256").update(codeVerifier).digest("base64url");
3651
+ }
3652
+ async function startBrowserLogin(opts) {
3653
+ const baseURL = opts.value.baseURL;
3654
+ if (baseURL === void 0 || baseURL === "") {
3655
+ await opts.state.patch({
3656
+ set: { lastError: NO_BASE_URL_ERROR },
3657
+ unset: ["browserLoginRequest", "browserLogin"]
3658
+ }).catch(() => {
3659
+ });
3660
+ return void 0;
3661
+ }
3662
+ const now = opts.now ?? Date.now;
3663
+ const startedAt = now();
3664
+ const codeVerifier = randomToken(32);
3665
+ const state = randomToken(16);
3666
+ const device = await resolveDeviceInfo(opts.credentials);
3667
+ pendingAuth = { state, codeVerifier, deviceId: device.deviceId, startedAt };
3668
+ const authorizeUrl = buildAuthorizeURL({
3669
+ baseURL,
3670
+ codeChallenge: pkceChallenge(codeVerifier),
3671
+ state,
3672
+ deviceId: device.deviceId,
3673
+ deviceName: device.deviceName ?? "",
3674
+ platform: device.platform ?? process.platform,
3675
+ appVersion: device.appVersion ?? PKG_VERSION
3676
+ });
3677
+ try {
3678
+ await opts.state.patch({
3679
+ set: { browserLogin: { authorizeUrl, startedAt: new Date(startedAt).toISOString() } },
3680
+ unset: ["browserLoginRequest", "lastError"]
3681
+ });
3682
+ } catch (error) {
3683
+ pendingAuth = void 0;
3684
+ throw error;
3685
+ }
3686
+ return { authorizeUrl, state };
3687
+ }
3688
+ async function failBrowserLogin(state, loginError) {
3689
+ await state.patch({
3690
+ set: { lastError: loginError },
3691
+ unset: ["browserLoginRequest", "browserLogin"]
3692
+ }).catch(() => {
3693
+ });
3694
+ return loginError;
3695
+ }
3696
+ async function reconcileStaleBrowserLogin(state, value) {
3697
+ if (value.browserLogin === void 0) return;
3698
+ await state.patch({
3699
+ set: { lastError: { code: "AUTH_FAILED", message: "\u4E0A\u6B21\u6D4F\u89C8\u5668\u6388\u6743\u672A\u5B8C\u6210\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55" } },
3700
+ unset: ["browserLoginRequest", "browserLogin"]
3701
+ }).catch(() => {
3702
+ });
3703
+ }
3704
+ async function completeBrowserLogin(input, opts) {
3705
+ const now = opts.now ?? Date.now;
3706
+ const log = opts.log ?? (() => {
3707
+ });
3708
+ const pending = pendingAuth;
3709
+ if (pending === void 0) {
3710
+ log("hiwork-core: \u6536\u5230\u6D4F\u89C8\u5668\u6388\u6743\u56DE\u8C03\u4F46\u6CA1\u6709\u5F85\u5B8C\u6210\u7684\u6388\u6743\u8BF7\u6C42\uFF0C\u5DF2\u5FFD\u7565");
3711
+ return null;
3712
+ }
3713
+ if (typeof input.state !== "string" || input.state !== pending.state) {
3714
+ log("hiwork-core: \u6D4F\u89C8\u5668\u6388\u6743\u56DE\u8C03 state \u6821\u9A8C\u5931\u8D25\uFF08\u7591\u4F3C\u52AB\u6301\uFF09\uFF0C\u5DF2\u4E22\u5F03\u672C\u6B21\u56DE\u8C03");
3715
+ return null;
3716
+ }
3717
+ try {
3718
+ if (now() - pending.startedAt > BROWSER_LOGIN_TTL_MS) {
3719
+ log("hiwork-core: \u6D4F\u89C8\u5668\u6388\u6743\u56DE\u8C03\u5DF2\u8D85\u65F6\uFF08\u8D85\u8FC7 10 \u5206\u949F\uFF09\uFF0C\u5DF2\u62D2\u7EDD");
3720
+ return await failBrowserLogin(opts.state, { code: "AUTH_FAILED", message: "\u6388\u6743\u5DF2\u8D85\u65F6\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55" });
3721
+ }
3722
+ if (typeof input.error === "string" && input.error !== "") {
3723
+ log(`hiwork-core: \u6D4F\u89C8\u5668\u6388\u6743\u88AB\u62D2\u7EDD\uFF08${input.error.slice(0, 64)}\uFF09`);
3724
+ return await failBrowserLogin(opts.state, { code: "AUTH_FAILED", message: "\u6388\u6743\u88AB\u53D6\u6D88\u6216\u5931\u8D25\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55" });
3725
+ }
3726
+ if (typeof input.code !== "string" || input.code === "") {
3727
+ log("hiwork-core: \u6D4F\u89C8\u5668\u6388\u6743\u56DE\u8C03\u7F3A\u5C11 code\uFF0C\u5DF2\u4E22\u5F03");
3728
+ return await failBrowserLogin(opts.state, { code: "AUTH_FAILED", message: "\u6388\u6743\u56DE\u8C03\u4E0D\u5B8C\u6574\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55" });
3729
+ }
3730
+ const baseURL = opts.value.baseURL;
3731
+ if (baseURL === void 0 || baseURL === "") return await failBrowserLogin(opts.state, NO_BASE_URL_ERROR);
3732
+ const { sessionCookie, user } = await opts.gateway.exchangeCode({
3733
+ code: input.code,
3734
+ codeVerifier: pending.codeVerifier,
3735
+ deviceId: pending.deviceId
3736
+ });
3737
+ await opts.credentials.set(SESSION_REF, sessionCookie);
3738
+ const username = typeof user.username === "string" && user.username.trim() !== "" ? user.username.trim() : void 0;
3739
+ await provisionSession({
3740
+ gateway: opts.gateway,
3741
+ credentials: opts.credentials,
3742
+ piAi: opts.piAi,
3743
+ state: opts.state,
3744
+ baseURL,
3745
+ value: opts.value,
3746
+ sessionCookie,
3747
+ user,
3748
+ username,
3749
+ requestUnset: ["browserLoginRequest", "browserLogin"],
3750
+ ...opts.defaultModel === void 0 ? {} : { defaultModel: opts.defaultModel },
3751
+ ...opts.ensureWorkspace === void 0 ? {} : { ensureWorkspace: opts.ensureWorkspace }
3752
+ });
3753
+ return null;
3754
+ } catch (error) {
3755
+ const loginError = toLoginError(error, "\u65E0\u6CD5\u8FDE\u63A5\u7F51\u5173\uFF0C\u8BF7\u68C0\u67E5 baseURL \u6216\u7F51\u7EDC");
3756
+ return await failBrowserLogin(opts.state, loginError);
3757
+ } finally {
3758
+ pendingAuth = void 0;
3759
+ }
3760
+ }
3761
+
3762
+ // src/client-policy.ts
3763
+ var DEFAULT_LOGIN_MODE = "both";
3764
+ var CLIENT_POLICY_PATH = "/api/client/policy";
3765
+ var CLIENT_POLICY_TIMEOUT_MS = 5e3;
3766
+ function buildClientPolicyURL(opts) {
3767
+ const baseURL = typeof opts.baseURL === "string" ? opts.baseURL.trim().replace(/\/+$/, "") : "";
3768
+ if (baseURL === "") return void 0;
3769
+ const params = [
3770
+ ["platform", opts.platform],
3771
+ ["arch", opts.arch],
3772
+ ["version", opts.version],
3773
+ ["device_id", opts.deviceId]
3774
+ ];
3775
+ const query = params.filter(([, value]) => typeof value === "string" && value !== "").map(([key, value]) => `${key}=${encodeURIComponent(value)}`).join("&");
3776
+ return query === "" ? `${baseURL}${CLIENT_POLICY_PATH}` : `${baseURL}${CLIENT_POLICY_PATH}?${query}`;
3777
+ }
3778
+ function parseLoginMode(payload) {
3779
+ if (typeof payload !== "object" || payload === null) return void 0;
3780
+ const value = payload.loginMode;
3781
+ return value === "browser" || value === "form" || value === "both" ? value : void 0;
3782
+ }
3783
+ async function fetchClientPolicy(opts) {
3784
+ const url = buildClientPolicyURL(opts);
3785
+ if (url === void 0) return void 0;
3786
+ try {
3787
+ const res = await fetch(url, {
3788
+ method: "GET",
3789
+ headers: { "user-agent": `hiwork-core/${PKG_VERSION}`, accept: "application/json" },
3790
+ signal: AbortSignal.timeout(opts.timeoutMs ?? CLIENT_POLICY_TIMEOUT_MS)
3791
+ });
3792
+ if (res.status !== 200) return void 0;
3793
+ let payload;
3794
+ try {
3795
+ payload = await res.json();
3796
+ } catch {
3797
+ return void 0;
3798
+ }
3799
+ const loginMode = parseLoginMode(payload);
3800
+ return loginMode === void 0 ? void 0 : { loginMode };
3801
+ } catch {
3802
+ return void 0;
3803
+ }
3804
+ }
3805
+
3806
+ // src/default-model.ts
3807
+ var DEFAULT_MODEL_NS = "agent-default-model";
3808
+ async function ensureDefaultModel(routeModels, ports) {
3809
+ if (!ports.available()) return void 0;
3810
+ const current = ports.read();
3811
+ if (current !== void 0 && await routable(current, routeModels, ports)) return void 0;
3812
+ const next = pickFallbackDefaultModel(routeModels);
3813
+ if (next === void 0) return void 0;
3814
+ await ports.write(next);
3815
+ return next;
3816
+ }
3817
+ async function routable(selection, routeModels, ports) {
3818
+ const models = routeModels[selection.provider];
3819
+ if (models !== void 0) return models.some((model) => model.id === selection.model);
3820
+ return ports.isRoutableElsewhere(selection);
3821
+ }
3822
+
3823
+ // src/default-workspace.ts
3824
+ import { mkdir } from "node:fs/promises";
3825
+ import { homedir } from "node:os";
3826
+ import { join } from "node:path";
3827
+ var DEFAULT_WORKSPACE_DIR = join(homedir(), "HiWork", "Default");
3828
+ var DEFAULT_WORKSPACE_TITLE = "\u9ED8\u8BA4\u5DE5\u4F5C\u533A";
3829
+ async function ensureDefaultWorkspace(registry, warn, dir = DEFAULT_WORKSPACE_DIR) {
3830
+ try {
3831
+ if (registry.list().length > 0) return;
3832
+ await mkdir(dir, { recursive: true });
3833
+ await registry.create(dir, DEFAULT_WORKSPACE_TITLE);
3834
+ } catch (error) {
3835
+ warn(`hiwork-core: ensure default workspace failed: ${error instanceof Error ? error.message : String(error)}`);
3836
+ }
3837
+ }
3838
+
3839
+ // src/daily-rpc.ts
3840
+ var DAILY_RPC_CHANNEL = "/hiwork-daily";
3841
+ var DAILY_RPC_ENDPOINTS = ["today", "random"];
3842
+ var DailyRequestError = class extends Error {
3843
+ constructor(code, message) {
3844
+ super(message);
3845
+ this.code = code;
3846
+ this.name = "DailyRequestError";
3847
+ }
3848
+ };
3849
+ var DAILY_MESSAGE_FALLBACK = {
3850
+ SESSION_EXPIRED: "\u767B\u5F55\u72B6\u6001\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55",
3851
+ GATEWAY_UNREACHABLE: "\u7F51\u5173\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
3852
+ RATE_LIMITED: "\u8BF7\u6C42\u8FC7\u4E8E\u9891\u7E41\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5",
3853
+ NOT_FOUND: "\u7F51\u5173\u672A\u627E\u5230\u5BF9\u5E94\u7684\u6570\u636E",
3854
+ AUTH_FAILED: "\u767B\u5F55\u72B6\u6001\u5F02\u5E38\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55"
3855
+ };
3856
+ var DAILY_GENERIC_FAILURE_MESSAGE = "\u6BCF\u65E5\u4E00\u53E5\u6682\u65F6\u65E0\u6CD5\u83B7\u53D6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5";
3857
+ var DAILY_SESSION_EXPIRED_MESSAGE = "\u767B\u5F55\u72B6\u6001\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55";
3858
+ function displayDailyGatewayMessage(code, message) {
3859
+ const text = typeof message === "string" ? message.trim() : "";
3860
+ if (text === "") return DAILY_MESSAGE_FALLBACK[code] ?? DAILY_GENERIC_FAILURE_MESSAGE;
3861
+ const leaksImplementationDetail = /https?:\/\//i.test(text) || /\bHTTP\s*\d{3}\b/i.test(text) || /\bbaseURL\b/i.test(text);
3862
+ if (leaksImplementationDetail) return DAILY_MESSAGE_FALLBACK[code] ?? DAILY_GENERIC_FAILURE_MESSAGE;
3863
+ return text;
3864
+ }
3865
+ function parseDailyPayload(endpoint, raw) {
3866
+ if (raw === void 0 || raw === null) return {};
3867
+ if (typeof raw !== "object" || Array.isArray(raw)) {
3868
+ throw new DailyRequestError("bad-request", "\u6BCF\u65E5\u4E00\u53E5\u8BF7\u6C42\u53C2\u6570\u4E0D\u5408\u6CD5\u3002");
3869
+ }
3870
+ const record = raw;
3871
+ const keys = Object.keys(record);
3872
+ if (endpoint === "today") {
3873
+ if (keys.length > 0) throw new DailyRequestError("bad-request", `\u6BCF\u65E5\u4E00\u53E5\u8BF7\u6C42\u5305\u542B\u672A\u77E5\u5B57\u6BB5 '${keys[0]}'\u3002`);
3874
+ return {};
3875
+ }
3876
+ for (const key of keys) {
3877
+ if (key !== "excludeId") throw new DailyRequestError("bad-request", `\u6BCF\u65E5\u4E00\u53E5\u8BF7\u6C42\u5305\u542B\u672A\u77E5\u5B57\u6BB5 '${key}'\u3002`);
3878
+ }
3879
+ const payload = {};
3880
+ const rawExcludeId = record["excludeId"];
3881
+ if (rawExcludeId !== void 0) {
3882
+ if (typeof rawExcludeId !== "number" || !Number.isInteger(rawExcludeId) || rawExcludeId <= 0) {
3883
+ throw new DailyRequestError("bad-request", "\u6BCF\u65E5\u4E00\u53E5\u8BF7\u6C42\u7684 excludeId \u5FC5\u987B\u662F\u6B63\u6574\u6570\u3002");
3884
+ }
3885
+ payload.excludeId = rawExcludeId;
3886
+ }
3887
+ return payload;
3888
+ }
3889
+ function isDailyRpcEndpoint(endpoint) {
3890
+ return DAILY_RPC_ENDPOINTS.includes(endpoint);
3891
+ }
3892
+ function isAborted(signal) {
3893
+ return signal?.aborted === true;
3894
+ }
3895
+ async function handleDailyEndpoint(endpoint, rawPayload, deps, signal) {
3896
+ try {
3897
+ if (isAborted(signal)) return failure("cancelled", "\u6BCF\u65E5\u4E00\u53E5\u8BF7\u6C42\u5DF2\u53D6\u6D88\u3002");
3898
+ if (!isDailyRpcEndpoint(endpoint)) {
3899
+ throw new DailyRequestError("bad-request", `\u672A\u77E5\u7684\u6BCF\u65E5\u4E00\u53E5\u7AEF\u70B9 '${endpoint}'`);
3900
+ }
3901
+ const payload = parseDailyPayload(endpoint, rawPayload);
3902
+ const sessionCookie = await deps.resolveSession();
3903
+ if (sessionCookie === void 0 || sessionCookie === "") {
3904
+ return failure("SESSION_EXPIRED", DAILY_SESSION_EXPIRED_MESSAGE);
3905
+ }
3906
+ const gateway = deps.gateway();
3907
+ if (gateway === void 0) {
3908
+ return failure("GATEWAY_UNREACHABLE", "\u7F51\u5173\u5730\u5740\u672A\u914D\u7F6E\uFF0C\u8BF7\u5148\u5728\u300C\u7F51\u5173\u6A21\u578B\u300D\u91CC\u5B8C\u6210\u767B\u5F55");
3909
+ }
3910
+ if (isAborted(signal)) return failure("cancelled", "\u6BCF\u65E5\u4E00\u53E5\u8BF7\u6C42\u5DF2\u53D6\u6D88\u3002");
3911
+ const value = await callEndpoint(gateway, sessionCookie, endpoint, payload);
3912
+ return { ok: true, value };
3913
+ } catch (error) {
3914
+ if (error instanceof DailyRequestError) return failure(error.code, error.message);
3915
+ if (error instanceof GatewayError) {
3916
+ return failure(error.code, displayDailyGatewayMessage(error.code, error.message));
3917
+ }
3918
+ deps.log?.(`hiwork-core: \u6BCF\u65E5\u4E00\u53E5 RPC '${endpoint}' \u5931\u8D25\uFF08${error instanceof Error ? error.name : typeof error}\uFF09`);
3919
+ return failure("internal", DAILY_GENERIC_FAILURE_MESSAGE);
3920
+ }
3921
+ }
3922
+ async function callEndpoint(gateway, sessionCookie, endpoint, payload) {
3923
+ switch (endpoint) {
3924
+ case "today":
3925
+ return await gateway.getMyDailyQuote(sessionCookie);
3926
+ case "random":
3927
+ return await gateway.getMyDailyQuoteRandom(sessionCookie, payload.excludeId);
3928
+ }
3929
+ }
3930
+ function failure(code, message) {
3931
+ return { ok: false, error: { code, message, details: {} } };
3932
+ }
3933
+ function registerDailyRpc(ctx, deps) {
3934
+ const handler = (endpoint, payload, signal) => handleDailyEndpoint(endpoint, payload, deps, signal);
3935
+ const remove = ctx.connection.rpc.handle(DAILY_RPC_CHANNEL, handler, { authority: "loopback" });
3936
+ return async () => {
3937
+ await remove();
3938
+ };
3939
+ }
3940
+
3941
+ // src/orchestration.ts
3359
3942
  async function runLogin(opts) {
3360
3943
  const pending = opts.value.pendingLogin;
3361
3944
  const baseURL = opts.value.baseURL;
@@ -3369,40 +3952,23 @@ async function runLogin(opts) {
3369
3952
  try {
3370
3953
  const password = await opts.credentials.resolve(pending.passwordRef);
3371
3954
  if (password === void 0) throw new GatewayError("AUTH_FAILED", "\u767B\u5F55\u51ED\u636E\u7F3A\u5931\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55");
3372
- const { sessionCookie, user } = await opts.gateway.login(pending.username, password.value);
3955
+ const device = await resolveDeviceInfo(opts.credentials);
3956
+ const { sessionCookie, user } = await opts.gateway.login(pending.username, password.value, device);
3373
3957
  await opts.credentials.set(SESSION_REF, sessionCookie);
3374
- const keys = await opts.gateway.getMyKeys(sessionCookie);
3375
- const key = await opts.gateway.getMyKey(sessionCookie, opts.value.keyId);
3376
- const modelIds = computeModelIds(key.budgets);
3377
- await opts.credentials.set(API_KEY_REF, key.key.key);
3378
- if (opts.value.rememberPassword === true) await opts.credentials.set(REMEMBERED_PASSWORD_REF, password.value);
3379
- else await opts.credentials.unset(REMEMBERED_PASSWORD_REF).catch(() => {
3380
- });
3381
- if (pending.passwordRef !== REMEMBERED_PASSWORD_REF) await opts.credentials.unset(pending.passwordRef);
3382
- await opts.piAi.applyAiRouterProfiles(
3383
- modelIds.length > 0 ? buildPiAiProviderProfiles({ baseURL, routeModels: computeRouteModels(key.budgets, key.model_mapping ?? []) }) : null
3384
- );
3385
- const displayName = user.display_name?.trim() || void 0;
3386
- const set2 = {
3958
+ await provisionSession({
3959
+ ...opts,
3960
+ baseURL,
3961
+ sessionCookie,
3962
+ user,
3387
3963
  username: pending.username,
3388
- ...displayName === void 0 ? {} : { displayName },
3389
- lastLogin: {
3390
- at: (/* @__PURE__ */ new Date()).toISOString(),
3391
- modelCount: modelIds.length,
3392
- gatewayName: key.gateway_name ?? ""
3393
- },
3394
- budgets: computeBudgets(key.budgets),
3395
- keys: computeKeys(keys)
3396
- };
3397
- const unset = ["pendingLogin"];
3398
- if (displayName === void 0) unset.push("displayName");
3399
- if (key.key.enabled === false) {
3400
- set2.lastError = { code: "KEY_DISABLED", message: "Key \u5F53\u524D\u88AB\u7981\u7528\uFF0C\u8C03\u7528\u4F1A\u5931\u8D25" };
3401
- } else {
3402
- unset.push("lastError");
3403
- }
3404
- await opts.state.patch({ set: set2, unset });
3405
- await opts.ensureWorkspace?.();
3964
+ requestUnset: ["pendingLogin"],
3965
+ onApiKeyWritten: async () => {
3966
+ if (opts.value.rememberPassword === true) await opts.credentials.set(REMEMBERED_PASSWORD_REF, password.value);
3967
+ else await opts.credentials.unset(REMEMBERED_PASSWORD_REF).catch(() => {
3968
+ });
3969
+ if (pending.passwordRef !== REMEMBERED_PASSWORD_REF) await opts.credentials.unset(pending.passwordRef);
3970
+ }
3971
+ });
3406
3972
  return null;
3407
3973
  } catch (error) {
3408
3974
  const loginError = toLoginError(error, "\u65E0\u6CD5\u8FDE\u63A5\u7F51\u5173\uFF0C\u8BF7\u68C0\u67E5 baseURL \u6216\u7F51\u7EDC");
@@ -3430,9 +3996,11 @@ async function runRefresh(opts) {
3430
3996
  const key = await opts.gateway.getMyKey(session.value, opts.value.keyId);
3431
3997
  const modelIds = computeModelIds(key.budgets);
3432
3998
  await opts.credentials.set(API_KEY_REF, key.key.key);
3999
+ const routeModels = modelIds.length > 0 ? computeRouteModels(key.budgets, key.model_mapping ?? []) : void 0;
3433
4000
  await opts.piAi.applyAiRouterProfiles(
3434
- modelIds.length > 0 ? buildPiAiProviderProfiles({ baseURL, routeModels: computeRouteModels(key.budgets, key.model_mapping ?? []) }) : null
4001
+ routeModels === void 0 ? null : buildPiAiProviderProfiles({ baseURL, routeModels })
3435
4002
  );
4003
+ if (routeModels !== void 0) await opts.defaultModel?.ensure(routeModels);
3436
4004
  const set2 = {
3437
4005
  lastLogin: {
3438
4006
  at: (/* @__PURE__ */ new Date()).toISOString(),
@@ -3482,12 +4050,31 @@ async function runLogout(opts) {
3482
4050
  });
3483
4051
  await opts.piAi.applyAiRouterProfiles(null);
3484
4052
  await opts.state.patch({
3485
- unset: ["pendingLogin", "refreshRequest", "logoutRequest", "username", "displayName", "lastLogin", "lastError", "keyId", "budgets", "keys"]
4053
+ unset: [
4054
+ "pendingLogin",
4055
+ "refreshRequest",
4056
+ "logoutRequest",
4057
+ "browserLoginRequest",
4058
+ "browserLogin",
4059
+ "username",
4060
+ "displayName",
4061
+ "lastLogin",
4062
+ "lastError",
4063
+ "keyId",
4064
+ "budgets",
4065
+ "keys"
4066
+ ]
3486
4067
  });
3487
4068
  }
4069
+ async function handleBrowserLoginRequest(opts) {
4070
+ const started = await startBrowserLogin(opts);
4071
+ if (started !== void 0) return null;
4072
+ return NO_BASE_URL_ERROR;
4073
+ }
3488
4074
  async function handleAiRouterSectionChange(opts) {
3489
4075
  const v = opts.value;
3490
4076
  if (v.pendingLogin !== void 0) return runLogin(opts);
4077
+ if (v.browserLoginRequest !== void 0) return handleBrowserLoginRequest(opts);
3491
4078
  if (v.refreshRequest !== void 0) return runRefresh(opts);
3492
4079
  if (v.logoutRequest !== void 0) {
3493
4080
  await runLogout(opts);
@@ -3496,9 +4083,269 @@ async function handleAiRouterSectionChange(opts) {
3496
4083
  return null;
3497
4084
  }
3498
4085
 
4086
+ // src/usage-rpc.ts
4087
+ var USAGE_RPC_CHANNEL = "/hiwork-usage";
4088
+ var USAGE_RPC_ENDPOINTS = ["summary", "trends", "model-trends", "recharges", "keys", "key", "estimate"];
4089
+ var USAGE_RPC_RANGES = ["today", "yesterday", "24h", "7d", "30d", "mtd"];
4090
+ var USAGE_ESTIMATE_MAX_ITEMS = 50;
4091
+ var UsageRequestError = class extends Error {
4092
+ constructor(code, message) {
4093
+ super(message);
4094
+ this.code = code;
4095
+ this.name = "UsageRequestError";
4096
+ }
4097
+ };
4098
+ var GATEWAY_MESSAGE_FALLBACK = {
4099
+ SESSION_EXPIRED: "\u767B\u5F55\u72B6\u6001\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55",
4100
+ GATEWAY_UNREACHABLE: "\u7F51\u5173\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
4101
+ NO_KEY_ASSIGNED: "\u5F53\u524D\u8D26\u53F7\u5C1A\u672A\u88AB\u5206\u914D\u865A\u62DF KEY\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458",
4102
+ KEY_INVALID: "\u6240\u9009 KEY \u65E0\u6548\u6216\u5DF2\u88AB\u5220\u9664\uFF0C\u8BF7\u91CD\u65B0\u9009\u62E9 KEY",
4103
+ RATE_LIMITED: "\u8BF7\u6C42\u8FC7\u4E8E\u9891\u7E41\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5",
4104
+ NOT_FOUND: "\u7F51\u5173\u672A\u627E\u5230\u5BF9\u5E94\u7684\u6570\u636E",
4105
+ AUTH_FAILED: "\u767B\u5F55\u72B6\u6001\u5F02\u5E38\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55"
4106
+ };
4107
+ var USAGE_GENERIC_FAILURE_MESSAGE = "\u7528\u91CF\u6570\u636E\u6682\u65F6\u65E0\u6CD5\u83B7\u53D6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5";
4108
+ var USAGE_SESSION_EXPIRED_MESSAGE = "\u767B\u5F55\u72B6\u6001\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55";
4109
+ function displayGatewayMessage(code, message) {
4110
+ const text = typeof message === "string" ? message.trim() : "";
4111
+ if (text === "") return GATEWAY_MESSAGE_FALLBACK[code] ?? USAGE_GENERIC_FAILURE_MESSAGE;
4112
+ const leaksImplementationDetail = /https?:\/\//i.test(text) || /\bHTTP\s*\d{3}\b/i.test(text) || /\bbaseURL\b/i.test(text);
4113
+ if (leaksImplementationDetail) return GATEWAY_MESSAGE_FALLBACK[code] ?? USAGE_GENERIC_FAILURE_MESSAGE;
4114
+ return text;
4115
+ }
4116
+ function parseUsagePayload(raw) {
4117
+ if (raw === void 0 || raw === null) return {};
4118
+ if (typeof raw !== "object" || Array.isArray(raw)) {
4119
+ throw new UsageRequestError("bad-request", "\u7528\u91CF\u8BF7\u6C42\u53C2\u6570\u4E0D\u5408\u6CD5\u3002");
4120
+ }
4121
+ const record = raw;
4122
+ for (const key of Object.keys(record)) {
4123
+ if (key !== "keyId" && key !== "range") {
4124
+ throw new UsageRequestError("bad-request", `\u7528\u91CF\u8BF7\u6C42\u5305\u542B\u672A\u77E5\u5B57\u6BB5 '${key}'\u3002`);
4125
+ }
4126
+ }
4127
+ const payload = {};
4128
+ const rawKeyId = record["keyId"];
4129
+ if (rawKeyId !== void 0) {
4130
+ if (typeof rawKeyId !== "string") throw new UsageRequestError("bad-request", "\u7528\u91CF\u8BF7\u6C42\u7684 keyId \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u3002");
4131
+ const keyId = rawKeyId.trim();
4132
+ if (keyId !== "") payload.keyId = keyId;
4133
+ }
4134
+ const rawRange = record["range"];
4135
+ if (rawRange !== void 0) {
4136
+ if (typeof rawRange !== "string" || !USAGE_RPC_RANGES.includes(rawRange)) {
4137
+ throw new UsageRequestError("bad-request", `\u7528\u91CF\u8BF7\u6C42\u7684 range \u4E0D\u5408\u6CD5\uFF08\u53EF\u7528\u503C\uFF1A${USAGE_RPC_RANGES.join(" / ")}\uFF09\u3002`);
4138
+ }
4139
+ payload.range = rawRange;
4140
+ }
4141
+ return payload;
4142
+ }
4143
+ function isUsageRpcEndpoint(endpoint) {
4144
+ return USAGE_RPC_ENDPOINTS.includes(endpoint);
4145
+ }
4146
+ var ESTIMATE_ITEM_KEYS = [
4147
+ "route",
4148
+ "model",
4149
+ "input_tokens",
4150
+ "output_tokens",
4151
+ "cache_read_tokens",
4152
+ "cache_write_tokens",
4153
+ "at"
4154
+ ];
4155
+ function parseEstimatePayload(raw) {
4156
+ if (raw === void 0 || raw === null || typeof raw !== "object" || Array.isArray(raw)) {
4157
+ throw new UsageRequestError("bad-request", "\u8BA1\u4EF7\u8BF7\u6C42\u53C2\u6570\u4E0D\u5408\u6CD5\u3002");
4158
+ }
4159
+ const record = raw;
4160
+ for (const key of Object.keys(record)) {
4161
+ if (key !== "items") throw new UsageRequestError("bad-request", `\u8BA1\u4EF7\u8BF7\u6C42\u5305\u542B\u672A\u77E5\u5B57\u6BB5 '${key}'\u3002`);
4162
+ }
4163
+ const rawItems = record["items"];
4164
+ if (!Array.isArray(rawItems)) throw new UsageRequestError("bad-request", "\u8BA1\u4EF7\u8BF7\u6C42\u7684 items \u5FC5\u987B\u662F\u6570\u7EC4\u3002");
4165
+ if (rawItems.length === 0) throw new UsageRequestError("bad-request", "\u8BA1\u4EF7\u8BF7\u6C42\u7684 items \u4E0D\u80FD\u4E3A\u7A7A\u3002");
4166
+ return rawItems.map((item, index) => parseEstimateItem(item, index));
4167
+ }
4168
+ function parseEstimateItem(raw, index) {
4169
+ const position = index + 1;
4170
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
4171
+ throw new UsageRequestError("bad-request", `\u8BA1\u4EF7\u8BF7\u6C42\u7B2C ${position} \u9879\u5FC5\u987B\u662F\u5BF9\u8C61\u3002`);
4172
+ }
4173
+ const record = raw;
4174
+ for (const key of Object.keys(record)) {
4175
+ if (!ESTIMATE_ITEM_KEYS.includes(key)) {
4176
+ throw new UsageRequestError("bad-request", `\u8BA1\u4EF7\u8BF7\u6C42\u7B2C ${position} \u9879\u5305\u542B\u672A\u77E5\u5B57\u6BB5 '${key}'\u3002`);
4177
+ }
4178
+ }
4179
+ const item = {
4180
+ route: requiredText(record["route"], position, "route"),
4181
+ model: requiredText(record["model"], position, "model"),
4182
+ input_tokens: tokenCount(record["input_tokens"], position, "input_tokens"),
4183
+ output_tokens: tokenCount(record["output_tokens"], position, "output_tokens")
4184
+ };
4185
+ const cacheRead = optionalTokenCount(record["cache_read_tokens"], position, "cache_read_tokens");
4186
+ if (cacheRead !== void 0) item.cache_read_tokens = cacheRead;
4187
+ const cacheWrite = optionalTokenCount(record["cache_write_tokens"], position, "cache_write_tokens");
4188
+ if (cacheWrite !== void 0) item.cache_write_tokens = cacheWrite;
4189
+ const at = record["at"];
4190
+ if (at !== void 0) {
4191
+ if (typeof at !== "string" || !isRfc3339(at)) {
4192
+ throw new UsageRequestError("bad-request", `\u8BA1\u4EF7\u8BF7\u6C42\u7B2C ${position} \u9879\u7684 at \u5FC5\u987B\u662F RFC3339 \u65F6\u95F4\u3002`);
4193
+ }
4194
+ item.at = at;
4195
+ }
4196
+ return item;
4197
+ }
4198
+ function isRfc3339(value) {
4199
+ const match = RFC3339_PATTERN.exec(value);
4200
+ if (match === null) return false;
4201
+ const field = (index) => Number(match[index] ?? Number.NaN);
4202
+ const year = field(1);
4203
+ const month = field(2);
4204
+ const day = field(3);
4205
+ const hour = field(4);
4206
+ const minute = field(5);
4207
+ const second = field(6);
4208
+ if (![year, month, day, hour, minute, second].every((part) => Number.isInteger(part))) return false;
4209
+ if (month < 1 || month > 12 || day < 1 || day > daysInMonth(year, month)) return false;
4210
+ if (hour > 23 || minute > 59 || second > 59) return false;
4211
+ if (match[7] !== void 0) {
4212
+ const offsetHour = field(8);
4213
+ const offsetMinute = field(9);
4214
+ if (!Number.isInteger(offsetHour) || !Number.isInteger(offsetMinute)) return false;
4215
+ if (offsetHour > 23 || offsetMinute > 59) return false;
4216
+ }
4217
+ return true;
4218
+ }
4219
+ var RFC3339_PATTERN = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:Z|([+-])(\d{2}):(\d{2}))$/;
4220
+ function daysInMonth(year, month) {
4221
+ if (month === 2) return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28;
4222
+ return [31, 0, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1] ?? 0;
4223
+ }
4224
+ function requiredText(value, position, field) {
4225
+ if (typeof value !== "string" || value.trim() === "") {
4226
+ throw new UsageRequestError("bad-request", `\u8BA1\u4EF7\u8BF7\u6C42\u7B2C ${position} \u9879\u7684 ${field} \u4E0D\u80FD\u4E3A\u7A7A\u3002`);
4227
+ }
4228
+ return value.trim();
4229
+ }
4230
+ function tokenCount(value, position, field) {
4231
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
4232
+ throw new UsageRequestError("bad-request", `\u8BA1\u4EF7\u8BF7\u6C42\u7B2C ${position} \u9879\u7684 ${field} \u5FC5\u987B\u662F\u975E\u8D1F\u6574\u6570\u3002`);
4233
+ }
4234
+ return value;
4235
+ }
4236
+ function optionalTokenCount(value, position, field) {
4237
+ return value === void 0 ? void 0 : tokenCount(value, position, field);
4238
+ }
4239
+ function unpriced(reason) {
4240
+ return { cost: 0, priced: false, channelResolved: false, reason };
4241
+ }
4242
+ function summarizeEstimate(results, expected) {
4243
+ if (results.length !== expected) return unpriced("incomplete");
4244
+ let cost = 0;
4245
+ let channelResolved = true;
4246
+ for (const result of results) {
4247
+ if (result?.priced !== true) return unpriced(result?.reason ?? "unpriced");
4248
+ const value = result.cost;
4249
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return unpriced("invalid_cost");
4250
+ cost += value;
4251
+ if (result.channel_resolved !== true) channelResolved = false;
4252
+ }
4253
+ return { cost, priced: true, channelResolved };
4254
+ }
4255
+ async function handleEstimateEndpoint(items, deps, signal) {
4256
+ const sessionCookie = await deps.resolveSession();
4257
+ if (sessionCookie === void 0 || sessionCookie === "") return { ok: true, value: unpriced("no_session") };
4258
+ if (isAborted2(signal)) return failure2("cancelled", "\u7528\u91CF\u8BF7\u6C42\u5DF2\u53D6\u6D88\u3002");
4259
+ if (items.length > USAGE_ESTIMATE_MAX_ITEMS) return { ok: true, value: unpriced("too_many_items") };
4260
+ const gatewayItems = [];
4261
+ for (const item of items) {
4262
+ const protocol = routeToProtocol(item.route);
4263
+ if (protocol === void 0) continue;
4264
+ gatewayItems.push({
4265
+ route: protocol,
4266
+ model: item.model,
4267
+ input_tokens: item.input_tokens,
4268
+ output_tokens: item.output_tokens,
4269
+ ...item.cache_read_tokens === void 0 ? {} : { cache_read_tokens: item.cache_read_tokens },
4270
+ ...item.cache_write_tokens === void 0 ? {} : { cache_write_tokens: item.cache_write_tokens },
4271
+ ...item.at === void 0 ? {} : { at: item.at }
4272
+ });
4273
+ }
4274
+ if (gatewayItems.length === 0) return { ok: true, value: unpriced("no_route") };
4275
+ const gateway = deps.gateway();
4276
+ if (gateway === void 0) {
4277
+ return failure2("GATEWAY_UNREACHABLE", "\u7F51\u5173\u5730\u5740\u672A\u914D\u7F6E\uFF0C\u8BF7\u5148\u5728\u300C\u7F51\u5173\u6A21\u578B\u300D\u91CC\u5B8C\u6210\u767B\u5F55");
4278
+ }
4279
+ if (isAborted2(signal)) return failure2("cancelled", "\u7528\u91CF\u8BF7\u6C42\u5DF2\u53D6\u6D88\u3002");
4280
+ const response = await gateway.estimateCost(sessionCookie, gatewayItems);
4281
+ const results = response?.items;
4282
+ if (!Array.isArray(results)) {
4283
+ deps.log?.("hiwork-core: \u8BA1\u4EF7 RPC \u6536\u5230\u65E0\u6548\u54CD\u5E94\uFF08items \u4E0D\u662F\u6570\u7EC4\uFF09");
4284
+ return failure2("internal", USAGE_GENERIC_FAILURE_MESSAGE);
4285
+ }
4286
+ return { ok: true, value: summarizeEstimate(results, gatewayItems.length) };
4287
+ }
4288
+ function isAborted2(signal) {
4289
+ return signal?.aborted === true;
4290
+ }
4291
+ async function handleUsageEndpoint(endpoint, rawPayload, deps, signal) {
4292
+ try {
4293
+ if (isAborted2(signal)) return failure2("cancelled", "\u7528\u91CF\u8BF7\u6C42\u5DF2\u53D6\u6D88\u3002");
4294
+ if (!isUsageRpcEndpoint(endpoint)) {
4295
+ throw new UsageRequestError("bad-request", `\u672A\u77E5\u7684\u7528\u91CF\u7AEF\u70B9 '${endpoint}'`);
4296
+ }
4297
+ if (endpoint === "estimate") return await handleEstimateEndpoint(parseEstimatePayload(rawPayload), deps, signal);
4298
+ const payload = parseUsagePayload(rawPayload);
4299
+ const sessionCookie = await deps.resolveSession();
4300
+ if (sessionCookie === void 0 || sessionCookie === "") {
4301
+ return failure2("SESSION_EXPIRED", USAGE_SESSION_EXPIRED_MESSAGE);
4302
+ }
4303
+ const gateway = deps.gateway();
4304
+ if (gateway === void 0) {
4305
+ return failure2("GATEWAY_UNREACHABLE", "\u7F51\u5173\u5730\u5740\u672A\u914D\u7F6E\uFF0C\u8BF7\u5148\u5728\u300C\u7F51\u5173\u6A21\u578B\u300D\u91CC\u5B8C\u6210\u767B\u5F55");
4306
+ }
4307
+ if (isAborted2(signal)) return failure2("cancelled", "\u7528\u91CF\u8BF7\u6C42\u5DF2\u53D6\u6D88\u3002");
4308
+ const value = await callEndpoint2(gateway, sessionCookie, endpoint, payload);
4309
+ return { ok: true, value };
4310
+ } catch (error) {
4311
+ if (error instanceof UsageRequestError) return failure2(error.code, error.message);
4312
+ if (error instanceof GatewayError) {
4313
+ return failure2(error.code, displayGatewayMessage(error.code, error.message));
4314
+ }
4315
+ deps.log?.(`hiwork-core: \u7528\u91CF RPC '${endpoint}' \u5931\u8D25\uFF08${error instanceof Error ? error.name : typeof error}\uFF09`);
4316
+ return failure2("internal", USAGE_GENERIC_FAILURE_MESSAGE);
4317
+ }
4318
+ }
4319
+ async function callEndpoint2(gateway, sessionCookie, endpoint, payload) {
4320
+ switch (endpoint) {
4321
+ case "summary":
4322
+ return await gateway.getMySummary(sessionCookie, payload.keyId, payload.range);
4323
+ case "trends":
4324
+ return await gateway.getMyTrends(sessionCookie, payload.keyId, payload.range);
4325
+ case "model-trends":
4326
+ return await gateway.getMyModelTrends(sessionCookie, payload.keyId, payload.range);
4327
+ case "recharges":
4328
+ return await gateway.getMyKeyRecharges(sessionCookie, payload.keyId);
4329
+ case "keys":
4330
+ return await gateway.getMyKeys(sessionCookie);
4331
+ case "key":
4332
+ return await gateway.getMyKey(sessionCookie, payload.keyId);
4333
+ }
4334
+ }
4335
+ function failure2(code, message) {
4336
+ return { ok: false, error: { code, message, details: {} } };
4337
+ }
4338
+ function registerUsageRpc(ctx, deps) {
4339
+ const handler = (endpoint, payload, signal) => handleUsageEndpoint(endpoint, payload, deps, signal);
4340
+ const remove = ctx.connection.rpc.handle(USAGE_RPC_CHANNEL, handler, { authority: "loopback" });
4341
+ return async () => {
4342
+ await remove();
4343
+ };
4344
+ }
4345
+
3499
4346
  // src/index.ts
3500
4347
  var name = "hiwork-core";
3501
- var inject = ["settings", "credentials", "workspaceRegistry"];
4348
+ var inject = ["settings", "credentials", "workspaceRegistry", "connection"];
3502
4349
  var NS = "ai-router";
3503
4350
  var PI_AI_NS = "llm-pi-ai";
3504
4351
  var lastErrorSchema = Schema.object({ code: Schema.string().required(), message: Schema.string().required() }).default(void 0);
@@ -3513,6 +4360,10 @@ var Config = Schema.object({
3513
4360
  rememberPassword: Schema.boolean().default(false),
3514
4361
  refreshRequest: Schema.object({ at: Schema.string().required() }).default(void 0),
3515
4362
  logoutRequest: Schema.object({ at: Schema.string().required() }).default(void 0),
4363
+ browserLoginRequest: Schema.object({ at: Schema.string().required() }).default(void 0),
4364
+ browserLogin: Schema.object({ authorizeUrl: Schema.string().required(), startedAt: Schema.string().required() }).default(void 0),
4365
+ // 三值枚举:非法值写入直接被 schema 拒绝(客户端还会再归一一次,双保险)。
4366
+ loginMode: Schema.union([Schema.const("browser"), Schema.const("form"), Schema.const("both")]),
3516
4367
  budgets: Schema.array(Schema.object({
3517
4368
  model_name: Schema.string().required(),
3518
4369
  total_budget: Schema.number(),
@@ -3550,10 +4401,169 @@ async function writeNamespace(settings, ns, ops) {
3550
4401
  }
3551
4402
  throw new Error(`settings namespace "${ns}" write conflicted repeatedly`);
3552
4403
  }
4404
+ function readDefaultSelection(settings) {
4405
+ const value = settings.get(DEFAULT_MODEL_NS);
4406
+ if (value === null || typeof value !== "object") return void 0;
4407
+ const { provider, model } = value;
4408
+ if (typeof provider !== "string" || typeof model !== "string") return void 0;
4409
+ return { provider, model };
4410
+ }
4411
+ function createDefaultModelPort(opts) {
4412
+ const { settings, llm } = opts;
4413
+ return {
4414
+ ensure: async (routeModels) => {
4415
+ const next = await ensureDefaultModel(routeModels, {
4416
+ available: () => settings.describe().some((descriptor) => descriptor.ns === DEFAULT_MODEL_NS),
4417
+ read: () => readDefaultSelection(settings),
4418
+ isRoutableElsewhere: async ({ provider, model }) => {
4419
+ if (llm === void 0) return false;
4420
+ if (!llm.listProviders().some((entry) => entry.id === provider)) return false;
4421
+ try {
4422
+ return (await llm.listModels(provider)).some((entry) => entry.id === model);
4423
+ } catch {
4424
+ return false;
4425
+ }
4426
+ },
4427
+ write: async (selection) => {
4428
+ await writeNamespace(settings, DEFAULT_MODEL_NS, [
4429
+ { op: "set", path: ["provider"], value: selection.provider },
4430
+ { op: "set", path: ["model"], value: selection.model },
4431
+ // 换模型后旧的 effort 可能不被新模型支持,一并清掉。
4432
+ { op: "unset", path: ["reasoningEffort"] }
4433
+ ]);
4434
+ }
4435
+ });
4436
+ if (next !== void 0) opts.log?.(`hiwork-core: \u9ED8\u8BA4\u6A21\u578B\u6821\u6B63\u4E3A ${next.provider}/${next.model}`);
4437
+ }
4438
+ };
4439
+ }
4440
+ var IPC_AUTH_CALLBACK = "hiwork:auth-callback";
4441
+ var IPC_DEVICE_INFO = "hiwork:device-info";
4442
+ var IPC_FIELD_MAX_LENGTH = 4096;
4443
+ var IPC_DEVICE_ID_MAX_LENGTH = 128;
4444
+ var IPC_DEVICE_NAME_MAX_LENGTH = 256;
4445
+ var IPC_APP_VERSION_MAX_LENGTH = 32;
4446
+ function readOptionalString(value, maxLength) {
4447
+ if (value === void 0) return { ok: true, value: void 0 };
4448
+ if (typeof value !== "string" || value.length > maxLength) return { ok: false };
4449
+ return { ok: true, value };
4450
+ }
4451
+ function parseAuthCallback(message) {
4452
+ const code = readOptionalString(message.code, IPC_FIELD_MAX_LENGTH);
4453
+ const state = readOptionalString(message.state, IPC_FIELD_MAX_LENGTH);
4454
+ const error = readOptionalString(message.error, IPC_FIELD_MAX_LENGTH);
4455
+ if (!code.ok || !state.ok || !error.ok) return void 0;
4456
+ return {
4457
+ ...code.value === void 0 ? {} : { code: code.value },
4458
+ ...state.value === void 0 ? {} : { state: state.value },
4459
+ ...error.value === void 0 ? {} : { error: error.value }
4460
+ };
4461
+ }
4462
+ function parseDeviceInfo(message) {
4463
+ const deviceId = readOptionalString(message.deviceId, IPC_DEVICE_ID_MAX_LENGTH);
4464
+ const deviceName = readOptionalString(message.deviceName, IPC_DEVICE_NAME_MAX_LENGTH);
4465
+ const appVersion = readOptionalString(message.appVersion, IPC_APP_VERSION_MAX_LENGTH);
4466
+ if (!deviceId.ok || !deviceName.ok || !appVersion.ok) return void 0;
4467
+ const id = deviceId.value?.trim();
4468
+ if (id === void 0 || id === "") return void 0;
4469
+ const name2 = deviceName.value?.trim();
4470
+ const version = appVersion.value?.trim();
4471
+ return {
4472
+ deviceId: id,
4473
+ ...name2 === void 0 || name2 === "" ? {} : { deviceName: name2 },
4474
+ ...version === void 0 || version === "" ? {} : { appVersion: version }
4475
+ };
4476
+ }
4477
+ function errorName(error) {
4478
+ return error instanceof Error ? error.name : typeof error;
4479
+ }
4480
+ function registerIpcMessageHandler(ports) {
4481
+ const log = ports.log ?? (() => {
4482
+ });
4483
+ let authInFlight = false;
4484
+ let disposed = false;
4485
+ const handler = (message) => {
4486
+ if (disposed) return;
4487
+ if (typeof message !== "object" || message === null || Array.isArray(message)) return;
4488
+ const envelope = message;
4489
+ const type = envelope.type;
4490
+ if (typeof type !== "string") return;
4491
+ if (type === IPC_AUTH_CALLBACK) {
4492
+ const input = parseAuthCallback(envelope);
4493
+ if (input === void 0) {
4494
+ log("hiwork-core: \u6536\u5230\u5B57\u6BB5\u975E\u6CD5\u7684\u6D4F\u89C8\u5668\u6388\u6743 IPC \u6D88\u606F\uFF0C\u5DF2\u5FFD\u7565");
4495
+ return;
4496
+ }
4497
+ if (authInFlight) {
4498
+ log("hiwork-core: \u4E0A\u4E00\u6B21\u6D4F\u89C8\u5668\u6388\u6743\u56DE\u8C03\u4ECD\u5728\u5904\u7406\u4E2D\uFF0C\u5DF2\u5FFD\u7565\u672C\u6B21\u91CD\u590D\u56DE\u8C03");
4499
+ return;
4500
+ }
4501
+ authInFlight = true;
4502
+ try {
4503
+ void Promise.resolve(ports.complete(input)).catch((error) => {
4504
+ log(`hiwork-core: \u6D4F\u89C8\u5668\u6388\u6743\u56DE\u8C03\u5904\u7406\u5F02\u5E38\uFF08${errorName(error)}\uFF09`);
4505
+ }).finally(() => {
4506
+ authInFlight = false;
4507
+ });
4508
+ } catch (error) {
4509
+ log(`hiwork-core: \u6D4F\u89C8\u5668\u6388\u6743\u56DE\u8C03\u5206\u53D1\u5931\u8D25\uFF08${errorName(error)}\uFF09`);
4510
+ authInFlight = false;
4511
+ }
4512
+ return;
4513
+ }
4514
+ if (type === IPC_DEVICE_INFO) {
4515
+ const info = parseDeviceInfo(envelope);
4516
+ if (info === void 0) {
4517
+ log("hiwork-core: \u6536\u5230\u5B57\u6BB5\u975E\u6CD5\u7684\u8BBE\u5907\u4FE1\u606F IPC \u6D88\u606F\uFF0C\u5DF2\u5FFD\u7565");
4518
+ return;
4519
+ }
4520
+ ports.setDevice(info);
4521
+ try {
4522
+ void Promise.resolve(ports.persistDeviceId(info.deviceId)).catch(() => {
4523
+ });
4524
+ } catch {
4525
+ }
4526
+ return;
4527
+ }
4528
+ };
4529
+ ports.source.on("message", handler);
4530
+ return () => {
4531
+ if (disposed) return;
4532
+ disposed = true;
4533
+ ports.source.off("message", handler);
4534
+ };
4535
+ }
4536
+ async function projectLoginMode(opts) {
4537
+ let loginMode = DEFAULT_LOGIN_MODE;
4538
+ try {
4539
+ const baseURL = opts.baseURL;
4540
+ if (baseURL !== void 0 && baseURL !== "") {
4541
+ const device = await resolveDeviceInfo(opts.credentials);
4542
+ const policy = await (opts.fetchPolicy ?? fetchClientPolicy)({
4543
+ baseURL,
4544
+ version: opts.version ?? PKG_VERSION,
4545
+ platform: opts.platform ?? process.platform,
4546
+ arch: opts.arch ?? process.arch,
4547
+ deviceId: device.deviceId,
4548
+ ...opts.timeoutMs === void 0 ? {} : { timeoutMs: opts.timeoutMs }
4549
+ });
4550
+ if (policy !== void 0) loginMode = policy.loginMode;
4551
+ }
4552
+ } catch (error) {
4553
+ opts.log?.(`hiwork-core: \u5BA2\u6237\u7AEF\u7B56\u7565\u62C9\u53D6\u5931\u8D25\uFF0C\u767B\u5F55\u65B9\u5F0F\u56DE\u9000 both\uFF08${errorName(error)}\uFF09`);
4554
+ }
4555
+ try {
4556
+ await opts.state.patch({ set: { loginMode } });
4557
+ } catch (error) {
4558
+ opts.log?.(`hiwork-core: \u5BA2\u6237\u7AEF\u7B56\u7565\u5199\u5165\u5931\u8D25\uFF08${errorName(error)}\uFF09`);
4559
+ }
4560
+ return loginMode;
4561
+ }
3553
4562
  function apply(ctx, config) {
3554
4563
  let current = () => ({ ...config });
3555
4564
  const settings = ctx.get("settings");
3556
4565
  const credentials = ctx.get("credentials");
4566
+ const llm = ctx.get("llm");
3557
4567
  if (settings === void 0) throw new Error("settings service unavailable");
3558
4568
  const gatewayFor = (baseURL) => new AiRouterClient(baseURL.replace(/\/+$/, ""));
3559
4569
  const ports = {
@@ -3587,6 +4597,11 @@ function apply(ctx, config) {
3587
4597
  await writeNamespace(settings, PI_AI_NS, ops);
3588
4598
  }
3589
4599
  },
4600
+ defaultModel: createDefaultModelPort({
4601
+ settings,
4602
+ llm,
4603
+ log: (message) => ctx.logger.info(message)
4604
+ }),
3590
4605
  state: {
3591
4606
  patch: async (patch) => {
3592
4607
  if (settings === void 0) throw new Error("settings service unavailable");
@@ -3602,26 +4617,50 @@ function apply(ctx, config) {
3602
4617
  }
3603
4618
  };
3604
4619
  let inFlight;
4620
+ let policyRequested = false;
4621
+ const requestClientPolicy = () => {
4622
+ if (policyRequested) return;
4623
+ const baseURL = current().baseURL;
4624
+ if (baseURL === void 0 || baseURL === "") return;
4625
+ policyRequested = true;
4626
+ void projectLoginMode({
4627
+ baseURL,
4628
+ credentials: ports.credentials,
4629
+ state: ports.state,
4630
+ log: (message) => {
4631
+ ctx.logger.warn(message);
4632
+ }
4633
+ }).catch(() => {
4634
+ });
4635
+ };
3605
4636
  settings.installSection(ctx, NS, Config, config, {
3606
4637
  setSource: (source) => {
3607
4638
  current = source;
3608
4639
  },
3609
4640
  onChange: () => {
4641
+ requestClientPolicy();
3610
4642
  const value = current();
3611
- const requested = value.pendingLogin ?? value.refreshRequest ?? value.logoutRequest;
4643
+ const requested = value.pendingLogin ?? value.browserLoginRequest ?? value.refreshRequest ?? value.logoutRequest;
3612
4644
  if (requested === void 0) return;
3613
4645
  if (inFlight !== void 0) return;
3614
4646
  inFlight = (async () => {
3615
4647
  try {
3616
4648
  const gateway = gatewayFor(value.baseURL ?? "");
3617
- await handleAiRouterSectionChange({ gateway, ...ports, value });
4649
+ await handleAiRouterSectionChange({
4650
+ gateway,
4651
+ ...ports,
4652
+ value,
4653
+ log: (message) => {
4654
+ ctx.logger.warn(message);
4655
+ }
4656
+ });
3618
4657
  } catch (error) {
3619
4658
  ctx.logger.error("hiwork-core: ai-router section change failed", error);
3620
4659
  await ports.credentials.unset(PENDING_PASSWORD_REF).catch(() => {
3621
4660
  });
3622
4661
  await ports.state.patch({
3623
4662
  set: { lastError: { code: "GATEWAY_UNREACHABLE", message: "\u7F51\u5173\u63D2\u4EF6\u5904\u7406\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u914D\u7F6E\u4E0E\u7F51\u7EDC" } },
3624
- unset: ["pendingLogin", "refreshRequest", "logoutRequest"]
4663
+ unset: ["pendingLogin", "browserLoginRequest", "browserLogin", "refreshRequest", "logoutRequest"]
3625
4664
  }).catch(() => {
3626
4665
  });
3627
4666
  }
@@ -3630,6 +4669,51 @@ function apply(ctx, config) {
3630
4669
  });
3631
4670
  }
3632
4671
  });
4672
+ requestClientPolicy();
4673
+ void reconcileStaleBrowserLogin(ports.state, current());
4674
+ const connection = ctx.get("connection");
4675
+ if (connection === void 0) throw new Error("connection service unavailable");
4676
+ ctx.effect(() => registerUsageRpc({ connection, logger: ctx.logger }, {
4677
+ resolveSession: async () => (await ports.credentials.resolve(SESSION_REF))?.value,
4678
+ gateway: () => {
4679
+ const baseURL = current().baseURL;
4680
+ return baseURL === void 0 || baseURL === "" ? void 0 : gatewayFor(baseURL);
4681
+ },
4682
+ log: (message) => {
4683
+ ctx.logger.warn(message);
4684
+ }
4685
+ }), "hiwork-core: usage rpc");
4686
+ ctx.effect(() => registerDailyRpc({ connection, logger: ctx.logger }, {
4687
+ resolveSession: async () => (await ports.credentials.resolve(SESSION_REF))?.value,
4688
+ gateway: () => {
4689
+ const baseURL = current().baseURL;
4690
+ return baseURL === void 0 || baseURL === "" ? void 0 : gatewayFor(baseURL);
4691
+ },
4692
+ log: (message) => {
4693
+ ctx.logger.warn(message);
4694
+ }
4695
+ }), "hiwork-core: daily rpc");
4696
+ ctx.effect(() => registerIpcMessageHandler({
4697
+ source: process,
4698
+ complete: (input) => {
4699
+ const value = current();
4700
+ return completeBrowserLogin(input, {
4701
+ ...ports,
4702
+ value,
4703
+ gateway: gatewayFor(value.baseURL ?? ""),
4704
+ log: (message) => {
4705
+ ctx.logger.warn(message);
4706
+ }
4707
+ });
4708
+ },
4709
+ setDevice: (info) => {
4710
+ setDeviceInfo(info);
4711
+ },
4712
+ persistDeviceId: (deviceId) => ports.credentials.set(DEVICE_ID_REF, deviceId),
4713
+ log: (message) => {
4714
+ ctx.logger.warn(message);
4715
+ }
4716
+ }), "hiwork-core: ipc message listener");
3633
4717
  if (config.lastLogin === void 0 && config.rememberPassword === true && config.username !== void 0 && config.baseURL !== void 0 && credentials !== void 0) {
3634
4718
  void credentials.resolve(REMEMBERED_PASSWORD_REF).then((hit) => {
3635
4719
  if (hit === void 0 || settings === void 0) return;
@@ -3641,6 +4725,9 @@ function apply(ctx, config) {
3641
4725
  export {
3642
4726
  Config,
3643
4727
  apply,
4728
+ createDefaultModelPort,
3644
4729
  inject,
3645
- name
4730
+ name,
4731
+ projectLoginMode,
4732
+ registerIpcMessageHandler
3646
4733
  };