dsh-opendesign-corouter 0.1.4 → 0.1.5

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
@@ -2194,7 +2194,7 @@ import { randomUUID } from "node:crypto";
2194
2194
 
2195
2195
  // src/device-auth.ts
2196
2196
  var DEVICE_AUTH_ENDPOINT = "https://amr-api.open-design.ai/api/v1/cli/device-authorizations";
2197
- var DEVICE_AUTH_PLUGIN_VERSION = "0.1.4";
2197
+ var DEVICE_AUTH_PLUGIN_VERSION = "0.1.5";
2198
2198
  var DEVICE_REQUEST_TIMEOUT_MS = 2e4;
2199
2199
  var DEVICE_RESPONSE_MAX_BYTES = 65536;
2200
2200
  var DEVICE_POLL_MIN_MS = 1e3;
@@ -2406,6 +2406,11 @@ var ConsoleLogin = class {
2406
2406
  } : {}
2407
2407
  };
2408
2408
  }
2409
+ /** Host-only: the live approval link, so the Host can open it in a bounded window. */
2410
+ activationLink() {
2411
+ const status = this.status();
2412
+ return status.state === "waiting" ? status.activationUrl : void 0;
2413
+ }
2409
2414
  /** Synchronous/non-blocking and idempotent while capture/network/commit is pending. */
2410
2415
  start(request) {
2411
2416
  if (this.disposed) throw new LoginError("opendesign/login-disposed");
@@ -2421,7 +2426,8 @@ var ConsoleLogin = class {
2421
2426
  status: { jobId: null, state: "launching", startedAt: new Date(started).toISOString(), message: "\u6B63\u5728\u7533\u8BF7\u5B98\u65B9\u8BBE\u5907\u6388\u6743\u7801" },
2422
2427
  captured: void 0,
2423
2428
  committing: false,
2424
- cancelRequested: false
2429
+ cancelRequested: false,
2430
+ settled: false
2425
2431
  };
2426
2432
  job.status.jobId = job.id;
2427
2433
  this.job = job;
@@ -2446,7 +2452,11 @@ var ConsoleLogin = class {
2446
2452
  }
2447
2453
  dispose() {
2448
2454
  this.disposed = true;
2449
- if (this.job) this.requestCancellation(this.job, "\u670D\u52A1\u5DF2\u5378\u8F7D\uFF0C\u767B\u5F55\u5DF2\u53D6\u6D88");
2455
+ const job = this.job;
2456
+ if (job) {
2457
+ this.requestCancellation(job, "\u670D\u52A1\u5DF2\u5378\u8F7D\uFF0C\u767B\u5F55\u5DF2\u53D6\u6D88");
2458
+ this.settle(job);
2459
+ }
2450
2460
  }
2451
2461
  alive(job) {
2452
2462
  return this.job === job && !this.disposed && !job.controller.signal.aborted && ACTIVE.has(job.status.state);
@@ -2495,6 +2505,16 @@ var ConsoleLogin = class {
2495
2505
  this.clearCode(job);
2496
2506
  this.stopTimer(job);
2497
2507
  job.controller.abort();
2508
+ this.settle(job);
2509
+ }
2510
+ /** Report one terminal transition per job, exactly once. */
2511
+ settle(job) {
2512
+ if (job.settled) return;
2513
+ job.settled = true;
2514
+ try {
2515
+ this.options.onSettled?.();
2516
+ } catch {
2517
+ }
2498
2518
  }
2499
2519
  async run(job) {
2500
2520
  const signal = job.controller.signal;
@@ -2514,7 +2534,7 @@ var ConsoleLogin = class {
2514
2534
  }
2515
2535
  this.armExpiry(job);
2516
2536
  job.status.state = "waiting";
2517
- job.status.message = "\u8BF7\u5728\u5E38\u7528\u6D4F\u89C8\u5668\u6253\u5F00\u5B98\u65B9\u6388\u6743\u9875\uFF0C\u4F7F\u7528\u5DF2\u4FDD\u5B58\u7684\u5BC6\u7801\u767B\u5F55\u5E76\u786E\u8BA4\u8BBE\u5907\u7801";
2537
+ job.status.message = "\u8BF7\u5728\u6388\u6743\u7A97\u53E3\u6216\u5E38\u7528\u6D4F\u89C8\u5668\u6253\u5F00\u5B98\u65B9\u6388\u6743\u9875\uFF0C\u767B\u5F55\u5E76\u786E\u8BA4\u8BBE\u5907\u7801";
2518
2538
  job.status.activationUrl = device.activationUrl;
2519
2539
  job.status.userCode = device.userCode;
2520
2540
  job.status.expiresAt = new Date(job.deadline).toISOString();
@@ -2572,6 +2592,258 @@ function failureMessage(error) {
2572
2592
  return "\u8BBE\u5907\u6388\u6743\u8BF7\u6C42\u5931\u8D25\uFF0C\u8BF7\u91CD\u65B0\u5F00\u59CB";
2573
2593
  }
2574
2594
 
2595
+ // src/browser-open.ts
2596
+ var ACTIVATION_NAVIGATE_TIMEOUT_MS = 15e3;
2597
+ var ACTIVATION_OPEN_BUDGET_MS = 2e4;
2598
+ var ACTIVATION_CLOSE_TIMEOUT_MS = 5e3;
2599
+ function chromeLaunchOptions() {
2600
+ return {
2601
+ channel: "chrome",
2602
+ headless: false,
2603
+ chromiumSandbox: true,
2604
+ ignoreDefaultArgs: ["--enable-automation"],
2605
+ args: ["--disable-blink-features=AutomationControlled"]
2606
+ };
2607
+ }
2608
+ var ActivationLauncher = class {
2609
+ browser;
2610
+ page;
2611
+ pending;
2612
+ disposal;
2613
+ /** In-flight window closure, shared so disposal can join it. */
2614
+ closing;
2615
+ /** Closures of handles that were never adopted; disposal joins them too. */
2616
+ tracked = [];
2617
+ /** Chrome starts in flight, so disposal waits for the handle they will produce. */
2618
+ starting = [];
2619
+ disposed = false;
2620
+ /** Bumped by `close()`: an attempt from an older session must not resurrect a window. */
2621
+ session = 0;
2622
+ options;
2623
+ constructor(options = {}) {
2624
+ this.options = options;
2625
+ }
2626
+ /**
2627
+ * Show `url` in the temporary window, reusing one already open. Concurrent calls share
2628
+ * a single attempt: the caller's button stays honest about what actually happened.
2629
+ */
2630
+ async open(url) {
2631
+ if (validateActivationUrl(url) === null) return { code: "invalid-url", opened: false };
2632
+ if (this.disposed) return { code: "failed", opened: false };
2633
+ if (this.pending) return this.pending;
2634
+ const attempt = this.attempt(url);
2635
+ this.pending = attempt;
2636
+ try {
2637
+ return await attempt;
2638
+ } finally {
2639
+ if (this.pending === attempt) this.pending = void 0;
2640
+ }
2641
+ }
2642
+ /**
2643
+ * Close the temporary window and stay usable: a cancelled or expired job takes its
2644
+ * window away, while the next login may open a new one.
2645
+ */
2646
+ async close() {
2647
+ this.session++;
2648
+ await this.closeCurrent();
2649
+ }
2650
+ /**
2651
+ * Retire the launcher for good. Cleanup is best-effort and bounded: it joins in-flight
2652
+ * starts and closures, but a browser that ignores `close()` may outlive the 5 s wait.
2653
+ */
2654
+ async dispose() {
2655
+ this.disposed = true;
2656
+ this.session++;
2657
+ if (this.disposal) return this.disposal;
2658
+ this.disposal = (async () => {
2659
+ await this.bounded(Date.now() + ACTIVATION_CLOSE_TIMEOUT_MS, async () => {
2660
+ await Promise.allSettled(this.starting);
2661
+ });
2662
+ await this.closing;
2663
+ await this.closeCurrent();
2664
+ await this.bounded(Date.now() + ACTIVATION_CLOSE_TIMEOUT_MS, async () => {
2665
+ await Promise.all(this.tracked);
2666
+ });
2667
+ })();
2668
+ return this.disposal;
2669
+ }
2670
+ /**
2671
+ * Close the window this launcher owns right now. Ownership is taken before the async
2672
+ * close, so a job that adopts a new window afterwards is not affected by this closure.
2673
+ * The closure promise is shared so a later `dispose()` joins it.
2674
+ */
2675
+ async closeCurrent() {
2676
+ if (this.closing === void 0) {
2677
+ const browser = this.browser;
2678
+ this.browser = void 0;
2679
+ this.page = void 0;
2680
+ if (browser === void 0) return;
2681
+ this.closing = closeBounded(browser);
2682
+ }
2683
+ const closing = this.closing;
2684
+ try {
2685
+ await closing;
2686
+ } finally {
2687
+ if (this.closing === closing) this.closing = void 0;
2688
+ }
2689
+ }
2690
+ async attempt(url) {
2691
+ const session = this.session;
2692
+ const budget = this.options.timeoutMs ?? ACTIVATION_OPEN_BUDGET_MS;
2693
+ const deadline = Date.now() + budget;
2694
+ try {
2695
+ if (this.options.available?.() === false) return { code: "unavailable", opened: false };
2696
+ const browser = await this.ensureBrowser(session, deadline);
2697
+ if (browser === void 0) return { code: this.stale(session) ? "failed" : "unavailable", opened: false };
2698
+ if (this.stale(session)) {
2699
+ await this.bounded(deadline, () => this.closeCurrent());
2700
+ return { code: "failed", opened: false };
2701
+ }
2702
+ const page = await this.ensurePage(browser, session, deadline);
2703
+ if (page === void 0) {
2704
+ await this.bounded(deadline, () => this.closeCurrent());
2705
+ return { code: "failed", opened: false };
2706
+ }
2707
+ const remaining = Math.max(1, Math.min(ACTIVATION_NAVIGATE_TIMEOUT_MS, deadline - Date.now()));
2708
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout: remaining });
2709
+ if (this.stale(session)) {
2710
+ await this.bounded(deadline, () => this.closeCurrent());
2711
+ return { code: "failed", opened: false };
2712
+ }
2713
+ return { code: "opened", opened: true };
2714
+ } catch {
2715
+ await this.bounded(deadline, () => this.closeCurrent());
2716
+ return { code: "failed", opened: false };
2717
+ }
2718
+ }
2719
+ /** The job that started this attempt ended, or the launcher retired. */
2720
+ stale(session) {
2721
+ return this.disposed || session !== this.session;
2722
+ }
2723
+ async ensureBrowser(session, deadline) {
2724
+ const open = this.browser;
2725
+ if (open !== void 0) {
2726
+ try {
2727
+ if (open.isConnected()) return open;
2728
+ } catch {
2729
+ }
2730
+ await this.bounded(deadline, () => this.closeCurrent());
2731
+ }
2732
+ for (let round = 0; round < 2; round++) {
2733
+ await this.bounded(deadline, async () => {
2734
+ await Promise.allSettled([this.closing, ...this.tracked, ...this.starting]);
2735
+ });
2736
+ if (this.stale(session) || Date.now() >= deadline) return void 0;
2737
+ if (this.closing === void 0 && this.tracked.length === 0 && this.starting.length === 0) break;
2738
+ }
2739
+ if (this.stale(session) || Date.now() >= deadline) return void 0;
2740
+ const launch = this.options.launch ?? defaultLaunch;
2741
+ const starting = launch();
2742
+ this.starting.push(starting);
2743
+ const browser = await this.bounded(deadline, () => starting);
2744
+ void starting.then((handle) => {
2745
+ if (handle !== browser) void this.trackClose(handle);
2746
+ }).catch(() => void 0).finally(() => {
2747
+ this.starting = this.starting.filter((item) => item !== starting);
2748
+ });
2749
+ if (browser === void 0) return void 0;
2750
+ if (this.stale(session)) {
2751
+ void this.trackClose(browser);
2752
+ return void 0;
2753
+ }
2754
+ this.adopt(browser);
2755
+ return browser;
2756
+ }
2757
+ async ensurePage(browser, session, deadline) {
2758
+ if (this.page !== void 0) return this.page;
2759
+ const page = await this.bounded(deadline, () => browser.newPage(), (late) => {
2760
+ void this.trackPageClose(late);
2761
+ });
2762
+ if (page === void 0) return void 0;
2763
+ if (this.stale(session) || this.browser !== browser) {
2764
+ await this.bounded(deadline, () => this.closeCurrent());
2765
+ return void 0;
2766
+ }
2767
+ this.page = page;
2768
+ return page;
2769
+ }
2770
+ /** A handle that arrived too late is closed, and still counts as work disposal joins. */
2771
+ trackClose(browser) {
2772
+ const closing = closeBounded(browser);
2773
+ this.tracked.push(closing);
2774
+ void closing.finally(() => {
2775
+ this.tracked = this.tracked.filter((item) => item !== closing);
2776
+ });
2777
+ return closing;
2778
+ }
2779
+ trackPageClose(page) {
2780
+ try {
2781
+ return Promise.resolve(page.close?.()).catch(() => void 0);
2782
+ } catch {
2783
+ return Promise.resolve();
2784
+ }
2785
+ }
2786
+ adopt(browser) {
2787
+ this.browser = browser;
2788
+ browser.on?.("disconnected", () => {
2789
+ if (this.browser === browser) {
2790
+ this.browser = void 0;
2791
+ this.page = void 0;
2792
+ }
2793
+ });
2794
+ }
2795
+ /**
2796
+ * Bound one launch step by the attempt's shared deadline. A step that settles only
2797
+ * after that deadline still hands its handle back: dropping it would leak a Chrome
2798
+ * process nobody can close.
2799
+ */
2800
+ async bounded(deadline, start, discardOnLate) {
2801
+ let timer;
2802
+ let expired = false;
2803
+ const work = (async () => {
2804
+ try {
2805
+ return await start();
2806
+ } catch {
2807
+ return void 0;
2808
+ }
2809
+ })().then((value) => {
2810
+ if (expired && value !== void 0) discardOnLate?.(value);
2811
+ return value;
2812
+ });
2813
+ const timeout = new Promise((resolve) => {
2814
+ timer = setTimeout(() => {
2815
+ expired = true;
2816
+ resolve(void 0);
2817
+ }, Math.max(1, deadline - Date.now()));
2818
+ });
2819
+ try {
2820
+ return await Promise.race([work, timeout]);
2821
+ } finally {
2822
+ if (timer !== void 0) clearTimeout(timer);
2823
+ }
2824
+ }
2825
+ };
2826
+ function closeBounded(browser) {
2827
+ let timer;
2828
+ return Promise.race([
2829
+ (async () => {
2830
+ try {
2831
+ await browser.close();
2832
+ } catch {
2833
+ }
2834
+ })(),
2835
+ new Promise((resolve) => {
2836
+ timer = setTimeout(resolve, ACTIVATION_CLOSE_TIMEOUT_MS);
2837
+ })
2838
+ ]).finally(() => {
2839
+ if (timer !== void 0) clearTimeout(timer);
2840
+ });
2841
+ }
2842
+ async function defaultLaunch() {
2843
+ const playwright = await import("playwright-core");
2844
+ return playwright.chromium.launch(chromeLaunchOptions());
2845
+ }
2846
+
2575
2847
  // src/quota.ts
2576
2848
  import { attributionHeaders as attributionHeaders3 } from "@deepseek-ai/dsh-llm";
2577
2849
  var API_BASE = "https://amr-api.open-design.ai/api/v1";
@@ -2859,6 +3131,7 @@ var MAX_API_KEY = 16384;
2859
3131
  var HTTP_ASCII = /^[\x21-\x7e]+$/u;
2860
3132
  var ENV_REF = /^[A-Za-z_][A-Za-z0-9_]*$/u;
2861
3133
  var MODEL_ID2 = /^[A-Za-z0-9._:/-]{1,128}$/u;
3134
+ var ACTIVATION_OPEN_CODES = ["opened", "no-link", "invalid-url", "unavailable", "failed"];
2862
3135
  var LOGIN_PHASES = ["idle", "launching", "waiting", "verifying", "success", "cancelled", "error"];
2863
3136
  var QUOTA_STATES = ["ok", "partial", "need-login", "error"];
2864
3137
  var SESSION_SOURCES = ["desktop", "login", "environment"];
@@ -3034,6 +3307,10 @@ function parseSaveApiKeyRequest(value) {
3034
3307
  if (key.length > MAX_API_KEY) throw new Error(`API Key \u4E0D\u80FD\u8D85\u8FC7 ${MAX_API_KEY} \u5B57\u7B26`);
3035
3308
  return { value: key, expectedRevision: text(value.expectedRevision, "\u914D\u7F6E\u7248\u672C", MAX_REVISION) };
3036
3309
  }
3310
+ function parseActivationOpenState(value) {
3311
+ if (!record(value)) throw new Error("\u6388\u6743\u9875\u6253\u5F00\u7ED3\u679C\u5FC5\u987B\u662F\u5BF9\u8C61");
3312
+ return { code: member(value.code, ACTIVATION_OPEN_CODES, "\u6388\u6743\u9875\u6253\u5F00\u7ED3\u679C"), opened: flag(value.opened, "\u6388\u6743\u9875\u662F\u5426\u5DF2\u6253\u5F00") };
3313
+ }
3037
3314
 
3038
3315
  // src/index.ts
3039
3316
  var name = "llm-opendesign-corouter";
@@ -3048,14 +3325,15 @@ function grantOf(value) {
3048
3325
  if (typeof value.payload.cookie !== "string") return;
3049
3326
  return { cookie: value.payload.cookie, ...typeof value.payload.expiresAt === "number" ? { expiresAt: value.payload.expiresAt } : {} };
3050
3327
  }
3051
- var _saveApiKey_dec, _cancelLogin_dec, _loginStatus_dec, _startLogin_dec, _refreshQuotaNow_dec, _refreshQuota_dec, _refreshModels_dec, _status_dec, _a, _init;
3052
- var OpenDesignCoRouterService = class extends (_a = TypertRemoteService, _status_dec = [Remote("status")], _refreshModels_dec = [Remote("refreshModels")], _refreshQuota_dec = [Remote("refreshQuota")], _refreshQuotaNow_dec = [Remote("refreshQuotaNow")], _startLogin_dec = [Remote("startLogin")], _loginStatus_dec = [Remote("loginStatus")], _cancelLogin_dec = [Remote("cancelLogin")], _saveApiKey_dec = [Remote("saveApiKey")], _a) {
3328
+ var _saveApiKey_dec, _openActivationPage_dec, _cancelLogin_dec, _loginStatus_dec, _startLogin_dec, _refreshQuotaNow_dec, _refreshQuota_dec, _refreshModels_dec, _status_dec, _a, _init;
3329
+ var OpenDesignCoRouterService = class extends (_a = TypertRemoteService, _status_dec = [Remote("status")], _refreshModels_dec = [Remote("refreshModels")], _refreshQuota_dec = [Remote("refreshQuota")], _refreshQuotaNow_dec = [Remote("refreshQuotaNow")], _startLogin_dec = [Remote("startLogin")], _loginStatus_dec = [Remote("loginStatus")], _cancelLogin_dec = [Remote("cancelLogin")], _openActivationPage_dec = [Remote("openActivationPage")], _saveApiKey_dec = [Remote("saveApiKey")], _a) {
3053
3330
  constructor(ctx, config, dependencies = { resolveSession, readQuota, readDesktopQuota }) {
3054
3331
  super(ctx, "openDesignCoRouter");
3055
3332
  this.dependencies = dependencies;
3056
3333
  __runInitializers(_init, 5, this);
3057
3334
  __publicField(this, "_config");
3058
3335
  __publicField(this, "_login");
3336
+ __publicField(this, "_activation");
3059
3337
  __publicField(this, "_lifetime", new AbortController());
3060
3338
  __publicField(this, "_revision", randomUUID2());
3061
3339
  __publicField(this, "_fingerprint", "");
@@ -3098,10 +3376,19 @@ var OpenDesignCoRouterService = class extends (_a = TypertRemoteService, _status
3098
3376
  this._modelsFingerprint = "";
3099
3377
  this._invalidateModels();
3100
3378
  }
3101
- }, dependencies.loginOptions);
3379
+ }, {
3380
+ ...dependencies.loginOptions,
3381
+ // The Host owns the temporary window, so it closes it on every terminal transition —
3382
+ // including expiry, which happens on a timer even when no UI is polling.
3383
+ onSettled: () => {
3384
+ void this._activation.close();
3385
+ }
3386
+ });
3387
+ this._activation = new ActivationLauncher(dependencies.activationOptions);
3102
3388
  ctx.effect(() => () => {
3103
3389
  this._lifetime.abort();
3104
3390
  this._login.dispose();
3391
+ return this._activation.dispose();
3105
3392
  });
3106
3393
  }
3107
3394
  dependencies;
@@ -3353,6 +3640,12 @@ var OpenDesignCoRouterService = class extends (_a = TypertRemoteService, _status
3353
3640
  cancelLogin(input) {
3354
3641
  return parseLoginState(this._login.cancel(parseCancelLoginRequest(input)));
3355
3642
  }
3643
+ async openActivationPage() {
3644
+ const url = this._login.activationLink();
3645
+ if (url === void 0) return { code: "no-link", opened: false };
3646
+ const result = await this._activation.open(url);
3647
+ return parseActivationOpenState({ code: result.code, opened: result.opened });
3648
+ }
3356
3649
  async saveApiKey(input) {
3357
3650
  const request = parseSaveApiKeyRequest(input), state = await this._state();
3358
3651
  this._expect(request.expectedRevision);
@@ -3372,6 +3665,7 @@ __decorateElement(_init, 1, "refreshQuotaNow", _refreshQuotaNow_dec, OpenDesignC
3372
3665
  __decorateElement(_init, 1, "startLogin", _startLogin_dec, OpenDesignCoRouterService);
3373
3666
  __decorateElement(_init, 1, "loginStatus", _loginStatus_dec, OpenDesignCoRouterService);
3374
3667
  __decorateElement(_init, 1, "cancelLogin", _cancelLogin_dec, OpenDesignCoRouterService);
3668
+ __decorateElement(_init, 1, "openActivationPage", _openActivationPage_dec, OpenDesignCoRouterService);
3375
3669
  __decorateElement(_init, 1, "saveApiKey", _saveApiKey_dec, OpenDesignCoRouterService);
3376
3670
  __decoratorMetadata(_init, OpenDesignCoRouterService);
3377
3671
  function apply(ctx, config) {