dsh-qoder-connect 0.1.4 → 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-CSeJqJpR.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,22 +1049,28 @@ 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 : {
1056
1057
  ...probed,
1057
1058
  jobTokenRefreshedAt: refreshedAt
1058
1059
  };
1060
+ const checkInRecord = deps.checkIn?.();
1061
+ const withCheckIn = checkInRecord === void 0 ? withRefreshNotice : {
1062
+ ...withRefreshNotice,
1063
+ checkIn: checkInRecord
1064
+ };
1059
1065
  try {
1060
1066
  const credits = await deps.client.fetchCredits();
1061
1067
  return {
1062
- ...withRefreshNotice,
1068
+ ...withCheckIn,
1063
1069
  credits
1064
1070
  };
1065
1071
  } catch (error) {
1066
1072
  return {
1067
- ...withRefreshNotice,
1073
+ ...withCheckIn,
1068
1074
  creditsError: safeMessage(error)
1069
1075
  };
1070
1076
  }
@@ -1169,11 +1175,27 @@ function parseAction(text) {
1169
1175
  const wrapped = parsed;
1170
1176
  const action = wrapped["action"];
1171
1177
  if (action === "clear") return { action: "clear" };
1178
+ if (action === "clear-checkin-logs") return { action: "clear-checkin-logs" };
1179
+ if (action === "checkin") return { action: "checkin" };
1172
1180
  if (action === "refresh") return { action: "refresh" };
1173
1181
  if (action === "set-maximum-context-window") return typeof wrapped["enabled"] === "boolean" ? {
1174
1182
  action: "set-maximum-context-window",
1175
1183
  enabled: wrapped["enabled"]
1176
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
+ }
1177
1199
  if (action === "probe") {
1178
1200
  const model = wrapped["model"];
1179
1201
  if (typeof model !== "string" || model.trim() === "") return void 0;
@@ -1212,6 +1234,19 @@ function qoderProbeHandler(deps, key) {
1212
1234
  return;
1213
1235
  }
1214
1236
  try {
1237
+ if (action.action === "clear-checkin-logs") {
1238
+ deps.clearCheckInLogs?.();
1239
+ json(res, 200, { state: "cleared" });
1240
+ return;
1241
+ }
1242
+ if (action.action === "checkin") {
1243
+ if (deps.checkIn === void 0) {
1244
+ json(res, 404, { error: "checkin-not-supported" });
1245
+ return;
1246
+ }
1247
+ json(res, 200, await deps.checkIn());
1248
+ return;
1249
+ }
1215
1250
  if (action.action === "clear") {
1216
1251
  deps.clear();
1217
1252
  json(res, 200, { state: "cleared" });
@@ -1233,6 +1268,17 @@ function qoderProbeHandler(deps, key) {
1233
1268
  json(res, 200, await deps.setMaximumContextWindow(action.enabled === true));
1234
1269
  return;
1235
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
+ }
1236
1282
  json(res, 200, await deps.probe(action.model));
1237
1283
  } catch (error) {
1238
1284
  json(res, 500, { error: error instanceof Error ? error.message : String(error) });
@@ -1254,6 +1300,244 @@ function registerQoderProbeRoute(ctx, deps, key) {
1254
1300
  }, "dsh-qoder-connect: probe control route");
1255
1301
  }
1256
1302
  //#endregion
1303
+ //#region src/checkin-scheduler.ts
1304
+ /** Scheduling and catch-up orchestration for daily 10:00 (UTC+8) check-in. */
1305
+ var JsonFileCheckInStore = class {
1306
+ filePath;
1307
+ constructor(filePath) {
1308
+ this.filePath = filePath ?? join(qoderPluginDataDir(), "checkin-status.json");
1309
+ }
1310
+ readAll() {
1311
+ try {
1312
+ if (!existsSync(this.filePath)) return {};
1313
+ const raw = readFileSync(this.filePath, "utf-8");
1314
+ return JSON.parse(raw);
1315
+ } catch {
1316
+ return {};
1317
+ }
1318
+ }
1319
+ read(variantId) {
1320
+ return this.readAll()[variantId];
1321
+ }
1322
+ clearLogs(variantId) {
1323
+ try {
1324
+ const all = this.readAll();
1325
+ if (all[variantId]) {
1326
+ all[variantId] = {
1327
+ ...all[variantId],
1328
+ logs: []
1329
+ };
1330
+ mkdirSync(dirname(this.filePath), { recursive: true });
1331
+ writeFileSync(this.filePath, JSON.stringify(all, null, 2), "utf-8");
1332
+ }
1333
+ } catch {}
1334
+ }
1335
+ write(variantId, record) {
1336
+ try {
1337
+ const all = this.readAll();
1338
+ const existingLogs = all[variantId]?.logs ?? [];
1339
+ const newLog = {
1340
+ id: `${record.lastDate}-${record.lastAt}`,
1341
+ date: record.lastDate,
1342
+ timestamp: record.lastAt,
1343
+ status: record.status,
1344
+ ...record.amount === void 0 ? {} : { amount: record.amount },
1345
+ ...record.message === void 0 ? {} : { message: record.message }
1346
+ };
1347
+ const updatedLogs = [newLog, ...existingLogs.filter((l) => l.id !== newLog.id)].slice(0, 30);
1348
+ all[variantId] = {
1349
+ ...record,
1350
+ logs: updatedLogs
1351
+ };
1352
+ mkdirSync(dirname(this.filePath), { recursive: true });
1353
+ writeFileSync(this.filePath, JSON.stringify(all, null, 2), "utf-8");
1354
+ } catch {}
1355
+ }
1356
+ };
1357
+ /**
1358
+ * Returns the current date in YYYY-MM-DD standardized on UTC+8 (Beijing Time).
1359
+ */
1360
+ function getUtc8DateString(nowMs = Date.now()) {
1361
+ const d = new Date(nowMs);
1362
+ const utc8 = new Date(d.getTime() + (d.getTimezoneOffset() + 480) * 6e4);
1363
+ return `${utc8.getFullYear()}-${String(utc8.getMonth() + 1).padStart(2, "0")}-${String(utc8.getDate()).padStart(2, "0")}`;
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
+ }
1372
+ /**
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.
1377
+ */
1378
+ function msUntilNextCheckIn(minuteOfDay, nowMs = Date.now()) {
1379
+ const minute = normalizeCheckInMinute(minuteOfDay);
1380
+ const d = new Date(nowMs);
1381
+ const utc8Time = new Date(d.getTime() + (d.getTimezoneOffset() + 480) * 6e4);
1382
+ const targetUtc8 = new Date(utc8Time.getTime());
1383
+ targetUtc8.setHours(Math.floor(minute / 60), minute % 60, 5, 0);
1384
+ let diff = targetUtc8.getTime() - utc8Time.getTime();
1385
+ if (diff <= 0) {
1386
+ targetUtc8.setDate(targetUtc8.getDate() + 1);
1387
+ diff = targetUtc8.getTime() - utc8Time.getTime();
1388
+ }
1389
+ return diff;
1390
+ }
1391
+ /**
1392
+ * Whether today's configured check-in moment (UTC+8) has already passed.
1393
+ */
1394
+ function isPastCheckInTime(minuteOfDay, nowMs = Date.now()) {
1395
+ const minute = normalizeCheckInMinute(minuteOfDay);
1396
+ const d = new Date(nowMs);
1397
+ const utc8 = new Date(d.getTime() + (d.getTimezoneOffset() + 480) * 6e4);
1398
+ return utc8.getHours() * 60 + utc8.getMinutes() >= minute;
1399
+ }
1400
+ var CheckInScheduler = class {
1401
+ targets;
1402
+ isEnabled;
1403
+ store;
1404
+ onResult;
1405
+ now;
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();
1421
+ disposed = false;
1422
+ constructor(options) {
1423
+ this.targets = options.targets;
1424
+ this.isEnabled = options.isEnabled;
1425
+ this.store = options.store ?? new JsonFileCheckInStore();
1426
+ this.onResult = options.onResult;
1427
+ this.now = options.now ?? Date.now;
1428
+ }
1429
+ start() {
1430
+ if (this.disposed) return;
1431
+ this.sweepAll(true);
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);
1446
+ }
1447
+ dispose() {
1448
+ this.disposed = true;
1449
+ for (const timer of this.timers.values()) clearTimeout(timer);
1450
+ this.timers.clear();
1451
+ }
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() {
1468
+ if (this.disposed) return;
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
+ }
1485
+ }
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) {
1491
+ if (this.disposed) return;
1492
+ const nowMs = this.now();
1493
+ const today = getUtc8DateString(nowMs);
1494
+ for (const target of this.targets) {
1495
+ if (only !== void 0 && target.variantId !== only) continue;
1496
+ if (!this.isEnabled(target.variantId)) continue;
1497
+ if (this.inFlight.has(target.variantId)) continue;
1498
+ this.inFlight.add(target.variantId);
1499
+ try {
1500
+ await this.sweepOne(target, isCatchUp, nowMs, today);
1501
+ } finally {
1502
+ this.inFlight.delete(target.variantId);
1503
+ }
1504
+ }
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
+ }
1539
+ };
1540
+ //#endregion
1257
1541
  //#region src/job-token-hint.ts
1258
1542
  /** The name recorded on the hint row (and therefore its displayed title). */
1259
1543
  const JOB_TOKEN_HINT_NAME = "qoder";
@@ -1421,8 +1705,21 @@ const MAXIMUM_CONTEXT_WINDOW_CN_FIELD = z.boolean().default(true).description("U
1421
1705
  * overrides" rather than a default object the host must keep in sync.
1422
1706
  */
1423
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");
1424
1710
  /** Sidebar quota toggle (one per variant; both live on the shared quota card). */
1425
1711
  const QUOTA_TOGGLE_FIELD = z.boolean().default(false).description("Show this variant’s remaining-credit card in the sidebar footer (off by default)");
1712
+ /** Automatic check-in toggle. */
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)");
1426
1723
  /**
1427
1724
  * Quota poll interval: default 5 minutes, floor 1 minute. The status route
1428
1725
  * performs a live upstream billing call per request with no cache, so an
@@ -1441,6 +1738,10 @@ const Config = z.object({
1441
1738
  modelContextWindowsCN: MODEL_CONTEXT_WINDOWS_FIELD,
1442
1739
  sidebarQuotaCN: QUOTA_TOGGLE_FIELD,
1443
1740
  sidebarQuotaGlobal: QUOTA_TOGGLE_FIELD,
1741
+ autoCheckInCN: AUTO_CHECK_IN_FIELD,
1742
+ autoCheckInGlobal: AUTO_CHECK_IN_FIELD,
1743
+ checkInMinuteCN: CHECK_IN_MINUTE_FIELD,
1744
+ checkInMinuteGlobal: CHECK_IN_MINUTE_FIELD,
1444
1745
  quotaPollMs: QUOTA_POLL_FIELD
1445
1746
  });
1446
1747
  /**
@@ -1455,12 +1756,14 @@ const Config = z.object({
1455
1756
  const CHINA_SECTION = z.object({
1456
1757
  probeConsent: PROBE_CONSENT_FIELD,
1457
1758
  useMaximumContextWindowCN: MAXIMUM_CONTEXT_WINDOW_CN_FIELD,
1458
- modelContextWindowsCN: MODEL_CONTEXT_WINDOWS_FIELD
1759
+ modelContextWindowsCN: MODEL_CONTEXT_WINDOWS_FIELD,
1760
+ disabledModelsCN: DISABLED_MODELS_FIELD
1459
1761
  });
1460
1762
  /** The global card's settings section and its context-window preferences. */
1461
1763
  const GLOBAL_SECTION = z.object({
1462
1764
  useMaximumContextWindow: MAXIMUM_CONTEXT_WINDOW_FIELD,
1463
- modelContextWindows: MODEL_CONTEXT_WINDOWS_FIELD
1765
+ modelContextWindows: MODEL_CONTEXT_WINDOWS_FIELD,
1766
+ disabledModels: DISABLED_MODELS_FIELD
1464
1767
  });
1465
1768
  /**
1466
1769
  * The shared quota card's section: both sidebar toggles and the poll interval.
@@ -1472,8 +1775,51 @@ const GLOBAL_SECTION = z.object({
1472
1775
  const QUOTA_SECTION = z.object({
1473
1776
  sidebarQuotaCN: QUOTA_TOGGLE_FIELD,
1474
1777
  sidebarQuotaGlobal: QUOTA_TOGGLE_FIELD,
1778
+ autoCheckInCN: AUTO_CHECK_IN_FIELD,
1779
+ autoCheckInGlobal: AUTO_CHECK_IN_FIELD,
1780
+ checkInMinuteCN: CHECK_IN_MINUTE_FIELD,
1781
+ checkInMinuteGlobal: CHECK_IN_MINUTE_FIELD,
1475
1782
  quotaPollMs: QUOTA_POLL_FIELD
1476
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
+ }
1477
1823
  /** Stable identity key used by credentials, probe records, and catalog entries. */
1478
1824
  const credentialIdentity = qoderCredentialIdentity;
1479
1825
  /** The settings namespace a variant's card and provider directory entry use. */
@@ -1519,10 +1865,23 @@ function createVariantRuntime(ctx, config, variant, current, identityOf) {
1519
1865
  logger: ctx.logger
1520
1866
  });
1521
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
+ };
1522
1880
  const transport = createQoderTransport({
1523
1881
  region: variant.region,
1524
1882
  resolvePat: () => store.patPromise(),
1525
1883
  resolveMachineId: () => getMachineId([qoderMachineIdPath()]),
1884
+ attachments,
1526
1885
  onJobTokenRefreshed: (info) => {
1527
1886
  jobTokenRefreshedAt = info.at;
1528
1887
  ctx.logger.warn(`dsh-qoder-connect: ${variant.displayName} job token was auto-refreshed after an upstream rejection`);
@@ -1538,18 +1897,17 @@ function createVariantRuntime(ctx, config, variant, current, identityOf) {
1538
1897
  providerId: variant.id,
1539
1898
  getPat: () => store.patPromise(),
1540
1899
  transport,
1541
- attachments: { saveImage: async (request) => {
1542
- const service = ctx.get("attachments");
1543
- if (service === void 0) throw new Error("dsh-qoder-connect: no attachment service is available to store images");
1544
- return await service.saveImage(request);
1545
- } }
1900
+ attachments
1546
1901
  });
1547
1902
  const fallback = fallbackFor(variant);
1548
1903
  const catalog = new QoderCatalog(fallback);
1549
- if (variant.id !== CHINA_VARIANT.id) catalog.setUseMaximumContextWindow(config.useMaximumContextWindow === true);
1550
- 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 {
1551
1908
  catalog.setUseMaximumContextWindow(config.useMaximumContextWindowCN === true);
1552
1909
  if (config.modelContextWindowsCN !== void 0) catalog.setModelContextWindows(config.modelContextWindowsCN);
1910
+ if (config.disabledModelsCN !== void 0) catalog.setDisabledModels(config.disabledModelsCN);
1553
1911
  }
1554
1912
  catalog.setVisible(false);
1555
1913
  const probeStore = new QoderProbeStore({
@@ -1582,7 +1940,8 @@ function createVariantRuntime(ctx, config, variant, current, identityOf) {
1582
1940
  inflightFetch: void 0,
1583
1941
  invalidate: () => {},
1584
1942
  registered: false,
1585
- jobTokenRefreshedAt: () => jobTokenRefreshedAt
1943
+ jobTokenRefreshedAt: () => jobTokenRefreshedAt,
1944
+ checkIn: (signal) => transport.checkIn(signal)
1586
1945
  };
1587
1946
  }
1588
1947
  /** The catalog provenance the card displays. */
@@ -1740,6 +2099,41 @@ function apply(ctx, config) {
1740
2099
  */
1741
2100
  const lastIdentities = /* @__PURE__ */ new Map();
1742
2101
  const runtimes = QODER_VARIANTS.map((variant) => createVariantRuntime(ctx, config, variant, () => current(), (id) => lastIdentities.get(id)));
2102
+ const checkInStore = new JsonFileCheckInStore();
2103
+ const checkInScheduler = new CheckInScheduler({
2104
+ targets: runtimes.map((runtime) => ({
2105
+ variantId: runtime.variant.id,
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
+ },
2111
+ onClaimed: () => {
2112
+ runtime.client.fetchCredits().catch(() => void 0);
2113
+ }
2114
+ })),
2115
+ isEnabled: (variantId) => {
2116
+ const cfg = current();
2117
+ if (variantId === CHINA_VARIANT.id) return cfg.autoCheckInCN === true;
2118
+ return cfg.autoCheckInGlobal === true;
2119
+ },
2120
+ store: checkInStore
2121
+ });
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
+ };
1743
2137
  installJobTokenHint(ctx);
1744
2138
  const probeKey = createProbeKey();
1745
2139
  /**
@@ -1754,6 +2148,8 @@ function apply(ctx, config) {
1754
2148
  const authKey = createAuthKey();
1755
2149
  let setMaximumContextWindow;
1756
2150
  let setMaximumContextWindowCN;
2151
+ let setModelsEnabled;
2152
+ let setModelsEnabledCN;
1757
2153
  /**
1758
2154
  * Point a variant at an account identity, invalidating whatever the previous
1759
2155
  * one left behind.
@@ -1817,13 +2213,28 @@ function apply(ctx, config) {
1817
2213
  path: runtime.variant.statusPath,
1818
2214
  store: runtime.store,
1819
2215
  client: runtime.client,
1820
- models: () => runtime.catalog.current(),
2216
+ models: () => runtime.catalog.all(),
1821
2217
  catalog: () => catalogSection(runtime),
1822
2218
  probe: () => probeSection(runtime, current().probeConsent === true),
1823
2219
  probeKey,
1824
2220
  authKey,
1825
- ...runtime.variant.id === CHINA_VARIANT.id ? { useMaximumContextWindow: () => current().useMaximumContextWindowCN === true } : { useMaximumContextWindow: () => current().useMaximumContextWindow === true },
1826
- jobTokenRefreshedAt: runtime.jobTokenRefreshedAt
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
+ },
2228
+ jobTokenRefreshedAt: runtime.jobTokenRefreshedAt,
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
+ }
1827
2238
  });
1828
2239
  registerQoderAuthRoute(webCtx, {
1829
2240
  path: runtime.variant.authPath,
@@ -1886,19 +2297,58 @@ function apply(ctx, config) {
1886
2297
  reason: runtime.catalogError
1887
2298
  };
1888
2299
  },
1889
- ...runtime.variant.id === CHINA_VARIANT.id ? { setMaximumContextWindow: async (enabled) => {
1890
- if (setMaximumContextWindowCN === void 0) return {
1891
- state: "failed",
1892
- reason: "settings are unavailable"
1893
- };
1894
- return setMaximumContextWindowCN(enabled);
1895
- } } : { setMaximumContextWindow: async (enabled) => {
1896
- if (setMaximumContextWindow === void 0) return {
1897
- state: "failed",
1898
- reason: "settings are unavailable"
2300
+ clearCheckInLogs: () => {
2301
+ checkInStore.clearLogs(runtime.variant.id);
2302
+ },
2303
+ checkIn: async () => {
2304
+ const result = await runtime.checkIn();
2305
+ if (result.status !== "error") {
2306
+ checkInStore.write(runtime.variant.id, {
2307
+ lastDate: result.date,
2308
+ lastAt: result.timestamp,
2309
+ status: result.status,
2310
+ ...result.amount === void 0 ? {} : { amount: result.amount },
2311
+ ...result.message === void 0 ? {} : { message: result.message }
2312
+ });
2313
+ if (result.status === "claimed") runtime.client.fetchCredits().catch(() => void 0);
2314
+ }
2315
+ return {
2316
+ state: result.status,
2317
+ ...result.amount === void 0 ? {} : { amount: result.amount },
2318
+ ...result.message === void 0 ? {} : { reason: result.message }
1899
2319
  };
1900
- return setMaximumContextWindow(enabled);
1901
- } }
2320
+ },
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
+ }
1902
2352
  }, probeKey);
1903
2353
  }
1904
2354
  });
@@ -1911,16 +2361,11 @@ function apply(ctx, config) {
1911
2361
  };
1912
2362
  /** Merge all sections into the whole config the rest of the plugin reads. */
1913
2363
  const merged = () => ({
1914
- ...sources.cn().probeConsent === void 0 ? {} : { probeConsent: sources.cn().probeConsent },
1915
- ...sources.cn().useMaximumContextWindowCN === void 0 ? {} : { useMaximumContextWindowCN: sources.cn().useMaximumContextWindowCN },
1916
- ...sources.cn().modelContextWindowsCN === void 0 ? {} : { modelContextWindowsCN: sources.cn().modelContextWindowsCN },
1917
- ...sources.global().useMaximumContextWindow === void 0 ? {} : { useMaximumContextWindow: sources.global().useMaximumContextWindow },
1918
- ...sources.global().modelContextWindows === void 0 ? {} : { modelContextWindows: sources.global().modelContextWindows },
1919
- ...sources.quota().sidebarQuotaCN === void 0 ? {} : { sidebarQuotaCN: sources.quota().sidebarQuotaCN },
1920
- ...sources.quota().sidebarQuotaGlobal === void 0 ? {} : { sidebarQuotaGlobal: sources.quota().sidebarQuotaGlobal },
1921
- ...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)
1922
2367
  });
1923
- const applyMaximumContextWindow = (next) => {
2368
+ const applyCatalogSettings = (next) => {
1924
2369
  let changed = false;
1925
2370
  for (const runtime of runtimes) {
1926
2371
  const isChina = runtime.variant.id === CHINA_VARIANT.id;
@@ -1928,11 +2373,13 @@ function apply(ctx, config) {
1928
2373
  if (runtime.catalog.setUseMaximumContextWindow(preference === true)) changed = true;
1929
2374
  const overrides = isChina ? next.modelContextWindowsCN : next.modelContextWindows;
1930
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;
1931
2378
  }
1932
2379
  if (changed) for (const runtime of runtimes) runtime.invalidate();
1933
2380
  };
1934
2381
  const repointStores = () => {
1935
- applyMaximumContextWindow(merged());
2382
+ applyCatalogSettings(merged());
1936
2383
  };
1937
2384
  settingsCtx.settings.installSection(ctx, QODER_SETTINGS_NS, CHINA_SECTION, config, {
1938
2385
  setSource(source) {
@@ -1952,8 +2399,13 @@ function apply(ctx, config) {
1952
2399
  setSource(source) {
1953
2400
  sources.quota = source;
1954
2401
  current = merged;
2402
+ checkInScheduler.rearm();
2403
+ runStartupCatchUpOnce();
1955
2404
  },
1956
- onChange: () => {}
2405
+ onChange: () => {
2406
+ checkInScheduler.rearm();
2407
+ checkInScheduler.sweepAll(true);
2408
+ }
1957
2409
  });
1958
2410
  setMaximumContextWindow = async (enabled) => {
1959
2411
  await settingsCtx.settings.update(QODER_GLOBAL_SETTINGS_NS, { useMaximumContextWindow: enabled });
@@ -1963,9 +2415,24 @@ function apply(ctx, config) {
1963
2415
  await settingsCtx.settings.update(QODER_SETTINGS_NS, { useMaximumContextWindowCN: enabled });
1964
2416
  return { state: "updated" };
1965
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
+ };
1966
2432
  });
1967
2433
  ctx.effect(() => () => {
1968
2434
  stopped = true;
2435
+ checkInScheduler.dispose();
1969
2436
  for (const timer of timers) clearInterval(timer);
1970
2437
  timers.length = 0;
1971
2438
  detach(ctx, clearHostHeartbeat(), "host heartbeat cleanup");
@@ -2107,4 +2574,4 @@ function apply(ctx, config) {
2107
2574
  }), "variant startup");
2108
2575
  }
2109
2576
  //#endregion
2110
- 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 };