dsh-qoder-connect 0.1.5 → 0.1.8

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
@@ -1,4 +1,4 @@
1
- import { A as QODER_GLOBAL_AUTH_PATH, B as qoderCredentialIdentity, C as probeModel, D as FALLBACK_QODER_MODELS, E as qoderCatalogPath, F as QODER_AUTH_FILENAME, G as qoderPluginDataDir, H as QODER_DATA_DIR_ENV, I as QODER_PAT_ENV_CN, K as qoderStateDir, L as QODER_PAT_ENV_GLOBAL, M as QODER_GLOBAL_STATUS_PATH, N as QODER_PROBE_PATH, O as QoderCatalog, P as QODER_STATUS_PATH, R as QoderCredentialStore, S as PROBE_EFFORT_CANDIDATES, T as QoderCatalogStore, U as QODER_DATA_DIR_NAME, V as qoderOwnAuthPath, W as qoderMachineIdPath, _ as modelInfoOf, a as QODER_HOST_HEARTBEAT_FILENAME, b as createQoderTransport, c as processStartTimeMs, d as writeHostHeartbeat, f as QODER_CONNECT_VERSION, g as kindFromQoderFailure, h as classifyUpstreamError, i as variantFor, j as QODER_GLOBAL_PROBE_PATH, k as QODER_AUTH_PATH, l as qoderHostHeartbeatPath, m as QoderUpstreamClient, n as GLOBAL_VARIANT, o as clearHostHeartbeat, p as KIND_STATUS, r as QODER_VARIANTS, s as isHeartbeatProcessAlive, t as CHINA_VARIANT, u as readHostHeartbeat, v as normalizeCredits, w as randomSentinel, x as getMachineId, y as validateApiKey, z as patTail } from "./variants-BEiQve5M.js";
1
+ import { A as QODER_GLOBAL_AUTH_PATH, B as qoderCredentialIdentity, C as probeModel, D as FALLBACK_QODER_MODELS, E as qoderCatalogPath, F as QODER_AUTH_FILENAME, G as qoderPluginDataDir, H as QODER_DATA_DIR_ENV, I as QODER_PAT_ENV_CN, K as qoderStateDir, L as QODER_PAT_ENV_GLOBAL, M as QODER_GLOBAL_STATUS_PATH, N as QODER_PROBE_PATH, O as QoderCatalog, P as QODER_STATUS_PATH, R as QoderCredentialStore, S as PROBE_EFFORT_CANDIDATES, T as QoderCatalogStore, U as QODER_DATA_DIR_NAME, V as qoderOwnAuthPath, W as qoderMachineIdPath, _ as modelInfoOf, a as QODER_HOST_HEARTBEAT_FILENAME, b as createQoderTransport, c as processStartTimeMs, d as writeHostHeartbeat, f as QODER_CONNECT_VERSION, g as kindFromQoderFailure, h as classifyUpstreamError, i as variantFor, j as QODER_GLOBAL_PROBE_PATH, k as QODER_AUTH_PATH, l as qoderHostHeartbeatPath, m as QoderUpstreamClient, n as GLOBAL_VARIANT, o as clearHostHeartbeat, p as KIND_STATUS, r as QODER_VARIANTS, s as isHeartbeatProcessAlive, t as CHINA_VARIANT, u as readHostHeartbeat, v as normalizeCredits, w as randomSentinel, x as getMachineId, y as validateApiKey, z as patTail } from "./variants-DE76gF0Y.js";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
4
4
  import { dirname, join, resolve } from "node:path";
@@ -1049,7 +1049,8 @@ async function qoderWebStatus(deps) {
1049
1049
  ...statusWithModels,
1050
1050
  probe: deps.probe(),
1051
1051
  ...deps.probeKey === void 0 ? {} : { probeKey: deps.probeKey },
1052
- ...deps.useMaximumContextWindow === void 0 ? {} : { useMaximumContextWindow: deps.useMaximumContextWindow() }
1052
+ ...deps.useMaximumContextWindow === void 0 ? {} : { useMaximumContextWindow: deps.useMaximumContextWindow() },
1053
+ ...deps.disabledModels === void 0 ? {} : { disabledModels: deps.disabledModels() }
1053
1054
  };
1054
1055
  const refreshedAt = deps.jobTokenRefreshedAt?.();
1055
1056
  const withRefreshNotice = refreshedAt === void 0 ? probed : {
@@ -1181,6 +1182,20 @@ function parseAction(text) {
1181
1182
  action: "set-maximum-context-window",
1182
1183
  enabled: wrapped["enabled"]
1183
1184
  } : void 0;
1185
+ if (action === "set-models-enabled") {
1186
+ if (typeof wrapped["enabled"] !== "boolean") return void 0;
1187
+ const rawModels = wrapped["models"];
1188
+ const rawModel = wrapped["model"];
1189
+ let models = [];
1190
+ if (Array.isArray(rawModels)) models = rawModels.filter((m) => typeof m === "string" && m.trim() !== "").map((m) => m.trim());
1191
+ else if (typeof rawModel === "string" && rawModel.trim() !== "") models = [rawModel.trim()];
1192
+ if (models.length === 0) return void 0;
1193
+ return {
1194
+ action: "set-models-enabled",
1195
+ models,
1196
+ enabled: wrapped["enabled"]
1197
+ };
1198
+ }
1184
1199
  if (action === "probe") {
1185
1200
  const model = wrapped["model"];
1186
1201
  if (typeof model !== "string" || model.trim() === "") return void 0;
@@ -1253,6 +1268,17 @@ function qoderProbeHandler(deps, key) {
1253
1268
  json(res, 200, await deps.setMaximumContextWindow(action.enabled === true));
1254
1269
  return;
1255
1270
  }
1271
+ if (action.action === "set-models-enabled") {
1272
+ if (deps.setModelsEnabled === void 0) {
1273
+ json(res, 404, { error: "models-enabled-setting-not-supported" });
1274
+ return;
1275
+ }
1276
+ json(res, 200, await deps.setModelsEnabled({
1277
+ models: action.models ?? (action.model ? [action.model] : []),
1278
+ enabled: action.enabled === true
1279
+ }));
1280
+ return;
1281
+ }
1256
1282
  json(res, 200, await deps.probe(action.model));
1257
1283
  } catch (error) {
1258
1284
  json(res, 500, { error: error instanceof Error ? error.message : String(error) });
@@ -1336,14 +1362,25 @@ function getUtc8DateString(nowMs = Date.now()) {
1336
1362
  const utc8 = new Date(d.getTime() + (d.getTimezoneOffset() + 480) * 6e4);
1337
1363
  return `${utc8.getFullYear()}-${String(utc8.getMonth() + 1).padStart(2, "0")}-${String(utc8.getDate()).padStart(2, "0")}`;
1338
1364
  }
1365
+ /** Clamp any stored/typed value onto a real minute of the day. */
1366
+ function normalizeCheckInMinute(value) {
1367
+ if (typeof value !== "number" || !Number.isFinite(value)) return 600;
1368
+ const whole = Math.trunc(value);
1369
+ if (whole < 0 || whole > 1439) return 600;
1370
+ return whole;
1371
+ }
1339
1372
  /**
1340
- * Calculates milliseconds until the next 10:00:05 AM in UTC+8.
1373
+ * Calculates milliseconds until the next occurrence of `minuteOfDay` (UTC+8).
1374
+ *
1375
+ * Five seconds past the configured minute are used so the request lands after
1376
+ * the upstream has flipped the day over rather than on the boundary itself.
1341
1377
  */
1342
- function msUntilNext10amUtc8(nowMs = Date.now()) {
1378
+ function msUntilNextCheckIn(minuteOfDay, nowMs = Date.now()) {
1379
+ const minute = normalizeCheckInMinute(minuteOfDay);
1343
1380
  const d = new Date(nowMs);
1344
1381
  const utc8Time = new Date(d.getTime() + (d.getTimezoneOffset() + 480) * 6e4);
1345
1382
  const targetUtc8 = new Date(utc8Time.getTime());
1346
- targetUtc8.setHours(10, 0, 5, 0);
1383
+ targetUtc8.setHours(Math.floor(minute / 60), minute % 60, 5, 0);
1347
1384
  let diff = targetUtc8.getTime() - utc8Time.getTime();
1348
1385
  if (diff <= 0) {
1349
1386
  targetUtc8.setDate(targetUtc8.getDate() + 1);
@@ -1352,13 +1389,13 @@ function msUntilNext10amUtc8(nowMs = Date.now()) {
1352
1389
  return diff;
1353
1390
  }
1354
1391
  /**
1355
- * Checks whether catch-up is needed today:
1356
- * Current time is past today's 10:00:00 AM (UTC+8) and today has not yet settled a check-in.
1392
+ * Whether today's configured check-in moment (UTC+8) has already passed.
1357
1393
  */
1358
- function shouldCatchUp(today, lastDate, nowMs = Date.now()) {
1359
- if (lastDate === today) return false;
1394
+ function isPastCheckInTime(minuteOfDay, nowMs = Date.now()) {
1395
+ const minute = normalizeCheckInMinute(minuteOfDay);
1360
1396
  const d = new Date(nowMs);
1361
- return new Date(d.getTime() + (d.getTimezoneOffset() + 480) * 6e4).getHours() >= 10;
1397
+ const utc8 = new Date(d.getTime() + (d.getTimezoneOffset() + 480) * 6e4);
1398
+ return utc8.getHours() * 60 + utc8.getMinutes() >= minute;
1362
1399
  }
1363
1400
  var CheckInScheduler = class {
1364
1401
  targets;
@@ -1366,7 +1403,21 @@ var CheckInScheduler = class {
1366
1403
  store;
1367
1404
  onResult;
1368
1405
  now;
1369
- timer;
1406
+ /**
1407
+ * One timer per enabled variant, keyed by variant id.
1408
+ *
1409
+ * Per-variant rather than one shared timer because the two products may be
1410
+ * configured to different moments; a single timer would have to wake for the
1411
+ * earliest and then decide who was due, which is the same bookkeeping with a
1412
+ * worse failure mode.
1413
+ */
1414
+ timers = /* @__PURE__ */ new Map();
1415
+ /**
1416
+ * Variants with a sweep in progress, so concurrent callers cannot double-claim.
1417
+ */
1418
+ inFlight = /* @__PURE__ */ new Set();
1419
+ /** When each variant's timer is next due, epoch ms, for the card to show. */
1420
+ nextRuns = /* @__PURE__ */ new Map();
1370
1421
  disposed = false;
1371
1422
  constructor(options) {
1372
1423
  this.targets = options.targets;
@@ -1378,56 +1429,113 @@ var CheckInScheduler = class {
1378
1429
  start() {
1379
1430
  if (this.disposed) return;
1380
1431
  this.sweepAll(true);
1381
- this.armNextTimer();
1432
+ this.rearm();
1433
+ }
1434
+ /**
1435
+ * Re-run the startup catch-up once the toggles are actually readable.
1436
+ *
1437
+ * `start()` runs while the plugin is still assembling, before the settings
1438
+ * section that owns these toggles has handed over its stored values, so that
1439
+ * first sweep sees the raw plugin config and skips the day. The host calls
1440
+ * this again from the section's source callback; the sweep is idempotent, so
1441
+ * a day already handled costs nothing.
1442
+ */
1443
+ catchUp() {
1444
+ if (this.disposed) return;
1445
+ this.sweepAll(true);
1382
1446
  }
1383
1447
  dispose() {
1384
1448
  this.disposed = true;
1385
- if (this.timer) {
1386
- clearTimeout(this.timer);
1387
- this.timer = void 0;
1388
- }
1449
+ for (const timer of this.timers.values()) clearTimeout(timer);
1450
+ this.timers.clear();
1389
1451
  }
1390
- armNextTimer() {
1452
+ /**
1453
+ * Re-place every variant's timer.
1454
+ *
1455
+ * Called after each fire and whenever configuration lands or changes, so a
1456
+ * user editing the time on the card does not have to restart DSH for it to
1457
+ * take effect.
1458
+ *
1459
+ * A timer is placed for every target, including variants whose toggle is
1460
+ * currently off. `start()` runs while the plugin is still assembling, when
1461
+ * the stored toggles are not readable yet, so gating placement on
1462
+ * `isEnabled` left a variant with NO timer at all — and nothing re-armed it
1463
+ * afterwards, which is precisely how a configured 12:13 check-in never
1464
+ * fired. Whether the work is due is decided inside the sweep, where the
1465
+ * configuration is current; the timer only decides when to look.
1466
+ */
1467
+ rearm() {
1391
1468
  if (this.disposed) return;
1392
- const delay = msUntilNext10amUtc8(this.now());
1393
- this.timer = setTimeout(() => {
1394
- this.sweepAll(false);
1395
- this.armNextTimer();
1396
- }, delay);
1397
- this.timer.unref?.();
1469
+ for (const timer of this.timers.values()) clearTimeout(timer);
1470
+ this.timers.clear();
1471
+ this.nextRuns.clear();
1472
+ const nowMs = this.now();
1473
+ for (const target of this.targets) {
1474
+ const delay = msUntilNextCheckIn(target.minuteOfDay(), nowMs);
1475
+ this.nextRuns.set(target.variantId, nowMs + delay);
1476
+ const timer = setTimeout(() => {
1477
+ this.timers.delete(target.variantId);
1478
+ this.sweepAll(false, target.variantId).finally(() => {
1479
+ this.rearm();
1480
+ });
1481
+ }, delay);
1482
+ timer.unref?.();
1483
+ this.timers.set(target.variantId, timer);
1484
+ }
1398
1485
  }
1399
- async sweepAll(isCatchUp) {
1486
+ /** When this variant's timer is next due, epoch ms; absent before first arm. */
1487
+ nextRunAt(variantId) {
1488
+ return this.nextRuns.get(variantId);
1489
+ }
1490
+ async sweepAll(isCatchUp, only) {
1400
1491
  if (this.disposed) return;
1401
1492
  const nowMs = this.now();
1402
1493
  const today = getUtc8DateString(nowMs);
1403
1494
  for (const target of this.targets) {
1495
+ if (only !== void 0 && target.variantId !== only) continue;
1404
1496
  if (!this.isEnabled(target.variantId)) continue;
1405
- const record = this.store.read(target.variantId);
1406
- if (isCatchUp && !shouldCatchUp(today, record?.lastDate, nowMs)) continue;
1407
- if (!isCatchUp && record?.lastDate === today && (record.status === "claimed" || record.status === "already-claimed")) continue;
1408
- let pat;
1497
+ if (this.inFlight.has(target.variantId)) continue;
1498
+ this.inFlight.add(target.variantId);
1409
1499
  try {
1410
- pat = await target.getPat();
1411
- } catch {
1412
- continue;
1500
+ await this.sweepOne(target, isCatchUp, nowMs, today);
1501
+ } finally {
1502
+ this.inFlight.delete(target.variantId);
1413
1503
  }
1414
- if (!pat) continue;
1415
- try {
1416
- const result = await target.service.checkIn(pat);
1417
- if (result.status !== "error") {
1418
- this.store.write(target.variantId, {
1419
- lastDate: result.date,
1420
- lastAt: result.timestamp,
1421
- status: result.status,
1422
- amount: result.amount,
1423
- message: result.message
1424
- });
1425
- if (result.status === "claimed") target.onClaimed?.();
1426
- }
1427
- this.onResult?.(result);
1428
- } catch {}
1429
1504
  }
1430
1505
  }
1506
+ async sweepOne(target, isCatchUp, nowMs, today) {
1507
+ const record = this.store.read(target.variantId);
1508
+ if (record?.lastDate === today && (record.status === "claimed" || record.status === "already-claimed")) {
1509
+ if (!isCatchUp) this.store.write(target.variantId, {
1510
+ lastDate: today,
1511
+ lastAt: nowMs,
1512
+ status: "already-claimed",
1513
+ ...record.amount === void 0 ? {} : { amount: record.amount },
1514
+ message: "Scheduled check-in ran; today was already claimed"
1515
+ });
1516
+ return;
1517
+ }
1518
+ if (isCatchUp && !isPastCheckInTime(target.minuteOfDay(), nowMs)) return;
1519
+ let result;
1520
+ try {
1521
+ result = await target.checkIn();
1522
+ } catch {
1523
+ return;
1524
+ }
1525
+ try {
1526
+ if (result.status !== "error") {
1527
+ this.store.write(target.variantId, {
1528
+ lastDate: result.date,
1529
+ lastAt: result.timestamp,
1530
+ status: result.status,
1531
+ amount: result.amount,
1532
+ message: result.message
1533
+ });
1534
+ if (result.status === "claimed") target.onClaimed?.();
1535
+ }
1536
+ this.onResult?.(result);
1537
+ } catch {}
1538
+ }
1431
1539
  };
1432
1540
  //#endregion
1433
1541
  //#region src/job-token-hint.ts
@@ -1597,10 +1705,21 @@ const MAXIMUM_CONTEXT_WINDOW_CN_FIELD = z.boolean().default(true).description("U
1597
1705
  * overrides" rather than a default object the host must keep in sync.
1598
1706
  */
1599
1707
  const MODEL_CONTEXT_WINDOWS_FIELD = z.dict(z.number().min(1), z.string()).description("Per-model context-window overrides for Qoder Global (model id → tokens)");
1708
+ /** Disabled models list (blacklist) */
1709
+ const DISABLED_MODELS_FIELD = z.array(z.string()).default([]).description("Models hidden from the provider catalog and picker");
1600
1710
  /** Sidebar quota toggle (one per variant; both live on the shared quota card). */
1601
1711
  const QUOTA_TOGGLE_FIELD = z.boolean().default(false).description("Show this variant’s remaining-credit card in the sidebar footer (off by default)");
1602
1712
  /** Automatic check-in toggle. */
1603
- const AUTO_CHECK_IN_FIELD = z.boolean().default(false).description("每天 10:00 (UTC+8) 自动签到领取算力额度(默认关闭)");
1713
+ const AUTO_CHECK_IN_FIELD = z.boolean().default(false).description("每天自动签到领取算力额度(默认关闭)");
1714
+ /**
1715
+ * When a variant checks in, as minutes past midnight in UTC+8.
1716
+ *
1717
+ * Stored as a plain minute count rather than a "HH:mm" string so the schema
1718
+ * itself rejects an impossible time: a browser time input yields 0–1439, and
1719
+ * anything outside that range fails Host validation instead of silently
1720
+ * scheduling a request at a moment that never arrives.
1721
+ */
1722
+ const CHECK_IN_MINUTE_FIELD = z.number().default(600).min(0).max(1439).description("每日自动签到的时刻(自 UTC+8 午夜起的分钟数,600 = 10:00)");
1604
1723
  /**
1605
1724
  * Quota poll interval: default 5 minutes, floor 1 minute. The status route
1606
1725
  * performs a live upstream billing call per request with no cache, so an
@@ -1621,6 +1740,8 @@ const Config = z.object({
1621
1740
  sidebarQuotaGlobal: QUOTA_TOGGLE_FIELD,
1622
1741
  autoCheckInCN: AUTO_CHECK_IN_FIELD,
1623
1742
  autoCheckInGlobal: AUTO_CHECK_IN_FIELD,
1743
+ checkInMinuteCN: CHECK_IN_MINUTE_FIELD,
1744
+ checkInMinuteGlobal: CHECK_IN_MINUTE_FIELD,
1624
1745
  quotaPollMs: QUOTA_POLL_FIELD
1625
1746
  });
1626
1747
  /**
@@ -1635,12 +1756,14 @@ const Config = z.object({
1635
1756
  const CHINA_SECTION = z.object({
1636
1757
  probeConsent: PROBE_CONSENT_FIELD,
1637
1758
  useMaximumContextWindowCN: MAXIMUM_CONTEXT_WINDOW_CN_FIELD,
1638
- modelContextWindowsCN: MODEL_CONTEXT_WINDOWS_FIELD
1759
+ modelContextWindowsCN: MODEL_CONTEXT_WINDOWS_FIELD,
1760
+ disabledModelsCN: DISABLED_MODELS_FIELD
1639
1761
  });
1640
1762
  /** The global card's settings section and its context-window preferences. */
1641
1763
  const GLOBAL_SECTION = z.object({
1642
1764
  useMaximumContextWindow: MAXIMUM_CONTEXT_WINDOW_FIELD,
1643
- modelContextWindows: MODEL_CONTEXT_WINDOWS_FIELD
1765
+ modelContextWindows: MODEL_CONTEXT_WINDOWS_FIELD,
1766
+ disabledModels: DISABLED_MODELS_FIELD
1644
1767
  });
1645
1768
  /**
1646
1769
  * The shared quota card's section: both sidebar toggles and the poll interval.
@@ -1654,8 +1777,49 @@ const QUOTA_SECTION = z.object({
1654
1777
  sidebarQuotaGlobal: QUOTA_TOGGLE_FIELD,
1655
1778
  autoCheckInCN: AUTO_CHECK_IN_FIELD,
1656
1779
  autoCheckInGlobal: AUTO_CHECK_IN_FIELD,
1780
+ checkInMinuteCN: CHECK_IN_MINUTE_FIELD,
1781
+ checkInMinuteGlobal: CHECK_IN_MINUTE_FIELD,
1657
1782
  quotaPollMs: QUOTA_POLL_FIELD
1658
1783
  });
1784
+ /**
1785
+ * Every field each settings section owns, and therefore every field the live
1786
+ * configuration has to carry through.
1787
+ *
1788
+ * One list per section, shared by the merge and by the test that pins it to
1789
+ * the schema. Writing the merge out by hand is what broke auto check-in: the
1790
+ * `qoder-quota` section grew four fields (`autoCheckInCN`, `autoCheckInGlobal`,
1791
+ * `checkInMinuteCN`, `checkInMinuteGlobal`) while the merge kept copying only
1792
+ * the three that predated them, so `current().autoCheckInCN` read `undefined`
1793
+ * forever and the scheduler saw the toggle as permanently off. The card saved
1794
+ * it, the file held it, and nothing ever acted on it.
1795
+ */
1796
+ const CN_SECTION_KEYS = [
1797
+ "probeConsent",
1798
+ "useMaximumContextWindowCN",
1799
+ "modelContextWindowsCN",
1800
+ "disabledModelsCN"
1801
+ ];
1802
+ const GLOBAL_SECTION_KEYS = [
1803
+ "useMaximumContextWindow",
1804
+ "modelContextWindows",
1805
+ "disabledModels"
1806
+ ];
1807
+ const QUOTA_SECTION_KEYS = [
1808
+ "sidebarQuotaCN",
1809
+ "sidebarQuotaGlobal",
1810
+ "autoCheckInCN",
1811
+ "autoCheckInGlobal",
1812
+ "checkInMinuteCN",
1813
+ "checkInMinuteGlobal",
1814
+ "quotaPollMs"
1815
+ ];
1816
+ /** Copy the declared fields off one section's source, skipping absent ones. */
1817
+ function pickFields(source, keys) {
1818
+ const value = source();
1819
+ const picked = {};
1820
+ for (const key of keys) if (value[key] !== void 0) picked[key] = value[key];
1821
+ return picked;
1822
+ }
1659
1823
  /** Stable identity key used by credentials, probe records, and catalog entries. */
1660
1824
  const credentialIdentity = qoderCredentialIdentity;
1661
1825
  /** The settings namespace a variant's card and provider directory entry use. */
@@ -1701,10 +1865,23 @@ function createVariantRuntime(ctx, config, variant, current, identityOf) {
1701
1865
  logger: ctx.logger
1702
1866
  });
1703
1867
  let jobTokenRefreshedAt;
1868
+ const resolveAttachmentService = () => {
1869
+ const service = ctx.get("attachments");
1870
+ if (service === void 0) throw new Error("dsh-qoder-connect: no attachment service is available");
1871
+ return service;
1872
+ };
1873
+ const attachments = {
1874
+ get imageLimits() {
1875
+ return resolveAttachmentService().imageLimits;
1876
+ },
1877
+ readImageRequest: async (attachment, policy, signal) => await resolveAttachmentService().readImageRequest(attachment, policy, signal),
1878
+ saveImage: async (request) => await resolveAttachmentService().saveImage(request)
1879
+ };
1704
1880
  const transport = createQoderTransport({
1705
1881
  region: variant.region,
1706
1882
  resolvePat: () => store.patPromise(),
1707
1883
  resolveMachineId: () => getMachineId([qoderMachineIdPath()]),
1884
+ attachments,
1708
1885
  onJobTokenRefreshed: (info) => {
1709
1886
  jobTokenRefreshedAt = info.at;
1710
1887
  ctx.logger.warn(`dsh-qoder-connect: ${variant.displayName} job token was auto-refreshed after an upstream rejection`);
@@ -1720,18 +1897,17 @@ function createVariantRuntime(ctx, config, variant, current, identityOf) {
1720
1897
  providerId: variant.id,
1721
1898
  getPat: () => store.patPromise(),
1722
1899
  transport,
1723
- attachments: { saveImage: async (request) => {
1724
- const service = ctx.get("attachments");
1725
- if (service === void 0) throw new Error("dsh-qoder-connect: no attachment service is available to store images");
1726
- return await service.saveImage(request);
1727
- } }
1900
+ attachments
1728
1901
  });
1729
1902
  const fallback = fallbackFor(variant);
1730
1903
  const catalog = new QoderCatalog(fallback);
1731
- if (variant.id !== CHINA_VARIANT.id) catalog.setUseMaximumContextWindow(config.useMaximumContextWindow === true);
1732
- else {
1904
+ if (variant.id !== CHINA_VARIANT.id) {
1905
+ catalog.setUseMaximumContextWindow(config.useMaximumContextWindow === true);
1906
+ if (config.disabledModels !== void 0) catalog.setDisabledModels(config.disabledModels);
1907
+ } else {
1733
1908
  catalog.setUseMaximumContextWindow(config.useMaximumContextWindowCN === true);
1734
1909
  if (config.modelContextWindowsCN !== void 0) catalog.setModelContextWindows(config.modelContextWindowsCN);
1910
+ if (config.disabledModelsCN !== void 0) catalog.setDisabledModels(config.disabledModelsCN);
1735
1911
  }
1736
1912
  catalog.setVisible(false);
1737
1913
  const probeStore = new QoderProbeStore({
@@ -1764,7 +1940,8 @@ function createVariantRuntime(ctx, config, variant, current, identityOf) {
1764
1940
  inflightFetch: void 0,
1765
1941
  invalidate: () => {},
1766
1942
  registered: false,
1767
- jobTokenRefreshedAt: () => jobTokenRefreshedAt
1943
+ jobTokenRefreshedAt: () => jobTokenRefreshedAt,
1944
+ checkIn: (signal) => transport.checkIn(signal)
1768
1945
  };
1769
1946
  }
1770
1947
  /** The catalog provenance the card displays. */
@@ -1926,8 +2103,11 @@ function apply(ctx, config) {
1926
2103
  const checkInScheduler = new CheckInScheduler({
1927
2104
  targets: runtimes.map((runtime) => ({
1928
2105
  variantId: runtime.variant.id,
1929
- service: runtime.transport.checkInService,
1930
- getPat: async () => runtime.store.patPromise(),
2106
+ checkIn: (signal) => runtime.checkIn(signal),
2107
+ minuteOfDay: () => {
2108
+ const cfg = current();
2109
+ return normalizeCheckInMinute((runtime.variant.id === CHINA_VARIANT.id ? cfg.checkInMinuteCN : cfg.checkInMinuteGlobal) ?? 600);
2110
+ },
1931
2111
  onClaimed: () => {
1932
2112
  runtime.client.fetchCredits().catch(() => void 0);
1933
2113
  }
@@ -1940,6 +2120,20 @@ function apply(ctx, config) {
1940
2120
  store: checkInStore
1941
2121
  });
1942
2122
  checkInScheduler.start();
2123
+ /**
2124
+ * The startup catch-up, run once more from the settings source callback.
2125
+ *
2126
+ * `start()` above runs while the plugin is still assembling: the section
2127
+ * that stores these toggles has not handed over its values yet, so that
2128
+ * sweep reads the raw plugin config, sees no toggle, and skips the day. The
2129
+ * retry is what makes "boot after 10:00" actually claim.
2130
+ */
2131
+ let startupCatchUpDone = false;
2132
+ const runStartupCatchUpOnce = () => {
2133
+ if (startupCatchUpDone) return;
2134
+ startupCatchUpDone = true;
2135
+ checkInScheduler.catchUp();
2136
+ };
1943
2137
  installJobTokenHint(ctx);
1944
2138
  const probeKey = createProbeKey();
1945
2139
  /**
@@ -1954,6 +2148,8 @@ function apply(ctx, config) {
1954
2148
  const authKey = createAuthKey();
1955
2149
  let setMaximumContextWindow;
1956
2150
  let setMaximumContextWindowCN;
2151
+ let setModelsEnabled;
2152
+ let setModelsEnabledCN;
1957
2153
  /**
1958
2154
  * Point a variant at an account identity, invalidating whatever the previous
1959
2155
  * one left behind.
@@ -2017,14 +2213,28 @@ function apply(ctx, config) {
2017
2213
  path: runtime.variant.statusPath,
2018
2214
  store: runtime.store,
2019
2215
  client: runtime.client,
2020
- models: () => runtime.catalog.current(),
2216
+ models: () => runtime.catalog.all(),
2021
2217
  catalog: () => catalogSection(runtime),
2022
2218
  probe: () => probeSection(runtime, current().probeConsent === true),
2023
2219
  probeKey,
2024
2220
  authKey,
2025
- ...runtime.variant.id === CHINA_VARIANT.id ? { useMaximumContextWindow: () => current().useMaximumContextWindowCN === true } : { useMaximumContextWindow: () => current().useMaximumContextWindow === true },
2221
+ ...runtime.variant.id === CHINA_VARIANT.id ? {
2222
+ useMaximumContextWindow: () => current().useMaximumContextWindowCN === true,
2223
+ disabledModels: () => current().disabledModelsCN ?? []
2224
+ } : {
2225
+ useMaximumContextWindow: () => current().useMaximumContextWindow === true,
2226
+ disabledModels: () => current().disabledModels ?? []
2227
+ },
2026
2228
  jobTokenRefreshedAt: runtime.jobTokenRefreshedAt,
2027
- checkIn: () => checkInStore.read(runtime.variant.id)
2229
+ checkIn: () => {
2230
+ const record = checkInStore.read(runtime.variant.id);
2231
+ if (record === void 0) return void 0;
2232
+ const nextRunAt = checkInScheduler.nextRunAt(runtime.variant.id);
2233
+ return {
2234
+ ...record,
2235
+ ...nextRunAt === void 0 ? {} : { nextRunAt }
2236
+ };
2237
+ }
2028
2238
  });
2029
2239
  registerQoderAuthRoute(webCtx, {
2030
2240
  path: runtime.variant.authPath,
@@ -2091,16 +2301,7 @@ function apply(ctx, config) {
2091
2301
  checkInStore.clearLogs(runtime.variant.id);
2092
2302
  },
2093
2303
  checkIn: async () => {
2094
- let pat;
2095
- try {
2096
- pat = await runtime.store.patPromise();
2097
- } catch {
2098
- return {
2099
- state: "failed",
2100
- reason: "No PAT available"
2101
- };
2102
- }
2103
- const result = await runtime.transport.checkInService.checkIn(pat);
2304
+ const result = await runtime.checkIn();
2104
2305
  if (result.status !== "error") {
2105
2306
  checkInStore.write(runtime.variant.id, {
2106
2307
  lastDate: result.date,
@@ -2117,19 +2318,37 @@ function apply(ctx, config) {
2117
2318
  ...result.message === void 0 ? {} : { reason: result.message }
2118
2319
  };
2119
2320
  },
2120
- ...runtime.variant.id === CHINA_VARIANT.id ? { setMaximumContextWindow: async (enabled) => {
2121
- if (setMaximumContextWindowCN === void 0) return {
2122
- state: "failed",
2123
- reason: "settings are unavailable"
2124
- };
2125
- return setMaximumContextWindowCN(enabled);
2126
- } } : { setMaximumContextWindow: async (enabled) => {
2127
- if (setMaximumContextWindow === void 0) return {
2128
- state: "failed",
2129
- reason: "settings are unavailable"
2130
- };
2131
- return setMaximumContextWindow(enabled);
2132
- } }
2321
+ ...runtime.variant.id === CHINA_VARIANT.id ? {
2322
+ setMaximumContextWindow: async (enabled) => {
2323
+ if (setMaximumContextWindowCN === void 0) return {
2324
+ state: "failed",
2325
+ reason: "settings are unavailable"
2326
+ };
2327
+ return setMaximumContextWindowCN(enabled);
2328
+ },
2329
+ setModelsEnabled: async (opts) => {
2330
+ if (setModelsEnabledCN === void 0) return {
2331
+ state: "failed",
2332
+ reason: "settings are unavailable"
2333
+ };
2334
+ return setModelsEnabledCN(opts);
2335
+ }
2336
+ } : {
2337
+ setMaximumContextWindow: async (enabled) => {
2338
+ if (setMaximumContextWindow === void 0) return {
2339
+ state: "failed",
2340
+ reason: "settings are unavailable"
2341
+ };
2342
+ return setMaximumContextWindow(enabled);
2343
+ },
2344
+ setModelsEnabled: async (opts) => {
2345
+ if (setModelsEnabled === void 0) return {
2346
+ state: "failed",
2347
+ reason: "settings are unavailable"
2348
+ };
2349
+ return setModelsEnabled(opts);
2350
+ }
2351
+ }
2133
2352
  }, probeKey);
2134
2353
  }
2135
2354
  });
@@ -2142,16 +2361,11 @@ function apply(ctx, config) {
2142
2361
  };
2143
2362
  /** Merge all sections into the whole config the rest of the plugin reads. */
2144
2363
  const merged = () => ({
2145
- ...sources.cn().probeConsent === void 0 ? {} : { probeConsent: sources.cn().probeConsent },
2146
- ...sources.cn().useMaximumContextWindowCN === void 0 ? {} : { useMaximumContextWindowCN: sources.cn().useMaximumContextWindowCN },
2147
- ...sources.cn().modelContextWindowsCN === void 0 ? {} : { modelContextWindowsCN: sources.cn().modelContextWindowsCN },
2148
- ...sources.global().useMaximumContextWindow === void 0 ? {} : { useMaximumContextWindow: sources.global().useMaximumContextWindow },
2149
- ...sources.global().modelContextWindows === void 0 ? {} : { modelContextWindows: sources.global().modelContextWindows },
2150
- ...sources.quota().sidebarQuotaCN === void 0 ? {} : { sidebarQuotaCN: sources.quota().sidebarQuotaCN },
2151
- ...sources.quota().sidebarQuotaGlobal === void 0 ? {} : { sidebarQuotaGlobal: sources.quota().sidebarQuotaGlobal },
2152
- ...sources.quota().quotaPollMs === void 0 ? {} : { quotaPollMs: sources.quota().quotaPollMs }
2364
+ ...pickFields(sources.cn, CN_SECTION_KEYS),
2365
+ ...pickFields(sources.global, GLOBAL_SECTION_KEYS),
2366
+ ...pickFields(sources.quota, QUOTA_SECTION_KEYS)
2153
2367
  });
2154
- const applyMaximumContextWindow = (next) => {
2368
+ const applyCatalogSettings = (next) => {
2155
2369
  let changed = false;
2156
2370
  for (const runtime of runtimes) {
2157
2371
  const isChina = runtime.variant.id === CHINA_VARIANT.id;
@@ -2159,11 +2373,13 @@ function apply(ctx, config) {
2159
2373
  if (runtime.catalog.setUseMaximumContextWindow(preference === true)) changed = true;
2160
2374
  const overrides = isChina ? next.modelContextWindowsCN : next.modelContextWindows;
2161
2375
  if (overrides !== void 0 && runtime.catalog.setModelContextWindows(overrides)) changed = true;
2376
+ const disabled = isChina ? next.disabledModelsCN : next.disabledModels;
2377
+ if (runtime.catalog.setDisabledModels(disabled ?? [])) changed = true;
2162
2378
  }
2163
2379
  if (changed) for (const runtime of runtimes) runtime.invalidate();
2164
2380
  };
2165
2381
  const repointStores = () => {
2166
- applyMaximumContextWindow(merged());
2382
+ applyCatalogSettings(merged());
2167
2383
  };
2168
2384
  settingsCtx.settings.installSection(ctx, QODER_SETTINGS_NS, CHINA_SECTION, config, {
2169
2385
  setSource(source) {
@@ -2183,8 +2399,13 @@ function apply(ctx, config) {
2183
2399
  setSource(source) {
2184
2400
  sources.quota = source;
2185
2401
  current = merged;
2402
+ checkInScheduler.rearm();
2403
+ runStartupCatchUpOnce();
2186
2404
  },
2187
- onChange: () => {}
2405
+ onChange: () => {
2406
+ checkInScheduler.rearm();
2407
+ checkInScheduler.sweepAll(true);
2408
+ }
2188
2409
  });
2189
2410
  setMaximumContextWindow = async (enabled) => {
2190
2411
  await settingsCtx.settings.update(QODER_GLOBAL_SETTINGS_NS, { useMaximumContextWindow: enabled });
@@ -2194,6 +2415,20 @@ function apply(ctx, config) {
2194
2415
  await settingsCtx.settings.update(QODER_SETTINGS_NS, { useMaximumContextWindowCN: enabled });
2195
2416
  return { state: "updated" };
2196
2417
  };
2418
+ setModelsEnabled = async ({ models, enabled }) => {
2419
+ const currentDisabled = new Set(sources.global().disabledModels ?? []);
2420
+ for (const m of models) if (enabled) currentDisabled.delete(m);
2421
+ else currentDisabled.add(m);
2422
+ await settingsCtx.settings.update(QODER_GLOBAL_SETTINGS_NS, { disabledModels: Array.from(currentDisabled) });
2423
+ return { state: "updated" };
2424
+ };
2425
+ setModelsEnabledCN = async ({ models, enabled }) => {
2426
+ const currentDisabled = new Set(sources.cn().disabledModelsCN ?? []);
2427
+ for (const m of models) if (enabled) currentDisabled.delete(m);
2428
+ else currentDisabled.add(m);
2429
+ await settingsCtx.settings.update(QODER_SETTINGS_NS, { disabledModelsCN: Array.from(currentDisabled) });
2430
+ return { state: "updated" };
2431
+ };
2197
2432
  });
2198
2433
  ctx.effect(() => () => {
2199
2434
  stopped = true;
@@ -2339,4 +2574,4 @@ function apply(ctx, config) {
2339
2574
  }), "variant startup");
2340
2575
  }
2341
2576
  //#endregion
2342
- export { CHINA_VARIANT, Config, FALLBACK_QODER_MODELS, GLOBAL_VARIANT, KIND_STATUS, PROBE_EFFORT_CANDIDATES, QODER_AUTH_FILENAME, QODER_AUTH_PATH, QODER_DATA_DIR_ENV, QODER_DATA_DIR_NAME, QODER_GLOBAL_AUTH_PATH, QODER_GLOBAL_PROBE_PATH, QODER_GLOBAL_SETTINGS_NS, QODER_GLOBAL_STATUS_PATH, QODER_HOST_HEARTBEAT_FILENAME, QODER_PAT_ENV_CN, QODER_PAT_ENV_GLOBAL, QODER_PROBE_FILENAME, QODER_PROBE_PATH, QODER_PROVIDER, QODER_QUOTA_SETTINGS_NS, QODER_SETTINGS_NS, QODER_STATUS_PATH, QODER_STREAM_IDLE_TIMEOUT_MS, QODER_VARIANTS, QUOTA_POLL_DEFAULT_MS, QUOTA_POLL_MIN_MS, QoderCatalog, QoderCatalogStore, QoderCredentialStore, QoderProbeService, QoderProbeStore, QoderUpstreamClient, apply, classifyUpstreamError, clearHostHeartbeat, createAuthKey, createQoderAdapter, createQoderShim, fingerprintModel, inject, isHeartbeatProcessAlive, kindFromQoderFailure, modelInfoOf, name, normalizeCredits, patTail, probeModel, processStartTimeMs, qoderAuthHandler, qoderCatalogPath, qoderCredentialIdentity, qoderHostHeartbeatPath, qoderOwnAuthPath, qoderPluginDataDir, qoderProbePath, randomSentinel, readHostHeartbeat, registerQoderAuthRoute, variantFor };
2577
+ export { CHINA_VARIANT, CN_SECTION_KEYS, Config, FALLBACK_QODER_MODELS, GLOBAL_SECTION_KEYS, GLOBAL_VARIANT, KIND_STATUS, PROBE_EFFORT_CANDIDATES, QODER_AUTH_FILENAME, QODER_AUTH_PATH, QODER_DATA_DIR_ENV, QODER_DATA_DIR_NAME, QODER_GLOBAL_AUTH_PATH, QODER_GLOBAL_PROBE_PATH, QODER_GLOBAL_SETTINGS_NS, QODER_GLOBAL_STATUS_PATH, QODER_HOST_HEARTBEAT_FILENAME, QODER_PAT_ENV_CN, QODER_PAT_ENV_GLOBAL, QODER_PROBE_FILENAME, QODER_PROBE_PATH, QODER_PROVIDER, QODER_QUOTA_SETTINGS_NS, QODER_SETTINGS_NS, QODER_STATUS_PATH, QODER_STREAM_IDLE_TIMEOUT_MS, QODER_VARIANTS, QUOTA_POLL_DEFAULT_MS, QUOTA_POLL_MIN_MS, QUOTA_SECTION_KEYS, QoderCatalog, QoderCatalogStore, QoderCredentialStore, QoderProbeService, QoderProbeStore, QoderUpstreamClient, apply, classifyUpstreamError, clearHostHeartbeat, createAuthKey, createQoderAdapter, createQoderShim, fingerprintModel, inject, isHeartbeatProcessAlive, kindFromQoderFailure, modelInfoOf, name, normalizeCredits, patTail, probeModel, processStartTimeMs, qoderAuthHandler, qoderCatalogPath, qoderCredentialIdentity, qoderHostHeartbeatPath, qoderOwnAuthPath, qoderPluginDataDir, qoderProbePath, randomSentinel, readHostHeartbeat, registerQoderAuthRoute, variantFor };