dsh-qoder-connect 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/README.en.md CHANGED
@@ -17,6 +17,7 @@ Configure only what you use: a variant with no saved PAT shows no model group an
17
17
  - **Two independent variants** — `qoder` (china) and `qoder-global` (global) keep separate credential files, routes and saved catalogs: a token minted on qoder.com does not work against the China deployment and vice versa.
18
18
  - **Three-tier model catalog (live → saved → fallback)** — the card states where the list on screen came from: *“Model list updated …”* (fetched now), *“Showing the saved model list from …”* (this account's last successful fetch, restored after a restart or a failed fetch), or *“Showing the built-in model list (not yet updated from Qoder)”* (the roster compiled into the plugin). The reason of the most recent failed fetch is shown too, and a *Refresh model list* button sits right there.
19
19
  - **One-toggle context window** — each variant card's *Context window* tab lists every model's capacity (the default and the largest declared window) and carries its own *“Use the largest declared context window”* preference (on by default): on, requests declare the maximum (e.g. Qwen3.8-Max at 1M); off, the default (200K). The two variants keep independent toggle state.
20
+ - **Daily auto check-in & logs** — automatically checks in daily at 10:00 (UTC+8) to claim 100 Credits, with independent toggles and startup catch-up protection. The card features a dedicated *Check-in log* panel with audit trails, *Check in now*, *Refresh*, and *Clear logs* quick actions.
20
21
  - **Sidebar quota card + credit details** — the *“Qoder sidebar display”* card toggles a per-variant sidebar quota widget (off by default; each toggle needs its variant's PAT saved) with one shared refresh interval (default 5 min, minimum 1 min). Clicking the sidebar widget opens the *Qoder quota* panel: one row per credit package — *“Remaining / Total + bar | Expires”* — plus the cycle share and reset time. Click the same widget again to close; click the other to switch variants.
21
22
  - **Rate display `x<priceFactor>`** — each model name is suffixed with the upstream-reported price multiplier (e.g. `Some Model · x0.79`, free is `x0`), spelled `x<n>` from the catalog's `price_factor`. Display only — it never changes the request; models whose rate the upstream did not report simply show no suffix.
22
23
 
@@ -58,6 +59,12 @@ Each variant card's *Context window* tab lists capacities per model under the *
58
59
 
59
60
  ![International context-window settings](assets/6.png)
60
61
 
62
+ ### Daily Auto Check-In & Check-In Log
63
+
64
+ Enable daily automatic check-in in the settings card, and inspect check-in history logs, trigger manual check-in, or clear logs in the *Check-in log* tab:
65
+
66
+ ![Daily Auto Check-In & Check-In Log](assets/7.png)
67
+
61
68
  ## Install
62
69
 
63
70
  Prerequisites:
@@ -109,6 +116,7 @@ Everything the plugin owns lives in one data directory: `<profile>/.dsh-qoder-co
109
116
  <profile>/.dsh-qoder-connect/
110
117
  ├── .qoder-auth.json # China PAT ({version:2, pat, region, savedAt})
111
118
  ├── .qoder-global-auth.json # Global PAT
119
+ ├── checkin-status.json # check-in status and history logs
112
120
  └── state/
113
121
  ├── .qoder-catalog.json # China per-account saved catalog
114
122
  ├── .qoder-global-catalog.json
package/README.md CHANGED
@@ -17,6 +17,7 @@
17
17
  - **双变体独立配置**:`qoder`(china)与 `qoder-global`(global)各有自己的凭证文件、路由与已保存目录——qoder.com 生成的令牌对中国区无效,反之亦然,两套永不互串。
18
18
  - **模型目录三级降级(live → saved → fallback)**:卡片明示当前列表来自哪里——「模型列表更新于 …」(实时拉取)、「当前显示已保存的模型列表,更新于 …」(本账号上次成功目录,重启或断网后顶上来)、「当前显示内置模型列表(尚未从 Qoder 更新)」(编译进插件的兜底名单);最近一次拉取失败时附带原因,并可在卡片上手动「刷新模型列表」。
19
19
  - **上下文窗口一键切换**:两张变体卡片的「上下文窗口」页列出每个模型的容量(默认值与上游声明的最大值),并各自提供「使用上游声明的最大上下文窗口」开关(默认勾选):勾选按最大窗口(如 Qwen3.8-Max 的 1M)发请求,不勾选按默认窗口(200K),两版开关状态独立保存、互不影响。
20
+ - **每日自动签到与日志明细**:支持每天 10:00 (UTC+8) 自动签到领取 100 Credits 算力额度,配备双端开关与开机防漏补签机制;卡片常驻「签到日志」面板,提供执行流水记录与「立即签到」、「刷新」、「清空日志」快捷操作。
20
21
  - **侧栏额度展示 + 额度明细**:「Qoder 侧栏展示」卡片可分别为两版开启侧栏底部的额度小卡(默认关,开启前需为该变体保存 PAT),共享一个刷新间隔(默认 5 分钟,最小 1 分钟)。点侧栏小卡在中间面板打开「Qoder 额度」明细:按资源包逐行给出「剩余 / 总量 + 进度条|到期时间」,合计占比与本轮重置时间一并展示;再点同一张卡关闭,点另一张切换到对应版本。
21
22
  - **倍率显示 `x<priceFactor>`**:模型名后直接拼上游报价的价格倍率(如 `某模型 · x0.79`,免费为 `x0`),来自目录接口的 `price_factor` 字段,仅作展示、不影响请求;上游未报倍率的模型不显示后缀,不会凭空补一个数。
22
23
 
@@ -58,6 +59,12 @@
58
59
 
59
60
  ![国际版上下文窗口设置](assets/6.png)
60
61
 
62
+ ### 每日自动签到与签到日志
63
+
64
+ 支持在设置卡片中开启每日自动签到,并在卡片底部的「签到日志」面板中查看签到流水、立即签到或清空日志:
65
+
66
+ ![每日自动签到与签到日志](assets/7.png)
67
+
61
68
  ## 安装
62
69
 
63
70
  前置条件:
@@ -109,6 +116,7 @@ DSH → 设置 → 插件 → 对应卡片,展开后把 PAT 粘贴进密码输
109
116
  <profile>/.dsh-qoder-connect/
110
117
  ├── .qoder-auth.json # 中国版 PAT({version:2, pat, region, savedAt})
111
118
  ├── .qoder-global-auth.json # 国际版 PAT
119
+ ├── checkin-status.json # 签到状态与历史日志记录
112
120
  └── state/
113
121
  ├── .qoder-catalog.json # 中国版按账号保存的模型目录
114
122
  ├── .qoder-global-catalog.json
package/assets/7.png ADDED
Binary file
package/lib/bin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { B as qoderCredentialIdentity, D as FALLBACK_QODER_MODELS, E as qoderCatalogPath, R as QoderCredentialStore, T as QoderCatalogStore, b as createQoderTransport, f as QODER_CONNECT_VERSION, i as variantFor, l as qoderHostHeartbeatPath, m as QoderUpstreamClient, r as QODER_VARIANTS, s as isHeartbeatProcessAlive, t as CHINA_VARIANT, u as readHostHeartbeat, y as validateApiKey } from "./variants-CSeJqJpR.js";
2
+ import { B as qoderCredentialIdentity, D as FALLBACK_QODER_MODELS, E as qoderCatalogPath, R as QoderCredentialStore, T as QoderCatalogStore, b as createQoderTransport, f as QODER_CONNECT_VERSION, i as variantFor, l as qoderHostHeartbeatPath, m as QoderUpstreamClient, r as QODER_VARIANTS, s as isHeartbeatProcessAlive, t as CHINA_VARIANT, u as readHostHeartbeat, y as validateApiKey } from "./variants-BEiQve5M.js";
3
3
  import { readFile } from "node:fs/promises";
4
4
  import { realpathSync } from "node:fs";
5
5
  import { fileURLToPath } from "node:url";
package/lib/client.js CHANGED
@@ -238,6 +238,8 @@ window.__ModuleLoader__.load({
238
238
  values: {
239
239
  sidebarQuotaCN: false,
240
240
  sidebarQuotaGlobal: false,
241
+ autoCheckInCN: false,
242
+ autoCheckInGlobal: false,
241
243
  quotaPollMs: POLL_DEFAULT_MS
242
244
  }
243
245
  };
@@ -249,6 +251,8 @@ window.__ModuleLoader__.load({
249
251
  values: {
250
252
  sidebarQuotaCN: value.sidebarQuotaCN === true,
251
253
  sidebarQuotaGlobal: value.sidebarQuotaGlobal === true,
254
+ autoCheckInCN: value.autoCheckInCN === true,
255
+ autoCheckInGlobal: value.autoCheckInGlobal === true,
252
256
  quotaPollMs: typeof value.quotaPollMs === "number" ? value.quotaPollMs : POLL_DEFAULT_MS
253
257
  }
254
258
  };
@@ -272,13 +276,15 @@ window.__ModuleLoader__.load({
272
276
  values: {
273
277
  sidebarQuotaCN: false,
274
278
  sidebarQuotaGlobal: false,
279
+ autoCheckInCN: false,
280
+ autoCheckInGlobal: false,
275
281
  quotaPollMs: POLL_DEFAULT_MS
276
282
  }
277
283
  };
278
284
  function stableProject(scope) {
279
285
  if (scope === void 0) return UNAVAILABLE;
280
286
  const next = project(scope);
281
- if (cachedProjection === void 0 || cachedScope !== scope || cachedProjection.status !== next.status || cachedProjection.writable !== next.writable || cachedProjection.values.sidebarQuotaCN !== next.values.sidebarQuotaCN || cachedProjection.values.sidebarQuotaGlobal !== next.values.sidebarQuotaGlobal || cachedProjection.values.quotaPollMs !== next.values.quotaPollMs) {
287
+ if (cachedProjection === void 0 || cachedScope !== scope || cachedProjection.status !== next.status || cachedProjection.writable !== next.writable || cachedProjection.values.sidebarQuotaCN !== next.values.sidebarQuotaCN || cachedProjection.values.sidebarQuotaGlobal !== next.values.sidebarQuotaGlobal || cachedProjection.values.autoCheckInCN !== next.values.autoCheckInCN || cachedProjection.values.autoCheckInGlobal !== next.values.autoCheckInGlobal || cachedProjection.values.quotaPollMs !== next.values.quotaPollMs) {
282
288
  cachedScope = scope;
283
289
  cachedProjection = next;
284
290
  }
@@ -369,6 +375,8 @@ window.__ModuleLoader__.load({
369
375
  const write = (field, value) => {
370
376
  if (field === "sidebarQuotaCN" && value === true && !signed.cn) return;
371
377
  if (field === "sidebarQuotaGlobal" && value === true && !signed.global) return;
378
+ if (field === "autoCheckInCN" && value === true && !signed.cn) return;
379
+ if (field === "autoCheckInGlobal" && value === true && !signed.global) return;
372
380
  scope?.set(field, value);
373
381
  };
374
382
  const minutes = Math.max(POLL_MIN_MS / 6e4, Math.round(projection.values.quotaPollMs / 6e4));
@@ -395,6 +403,22 @@ window.__ModuleLoader__.load({
395
403
  disabledHint: t("quotaSignInRequired"),
396
404
  onToggle: (next) => write("sidebarQuotaGlobal", next)
397
405
  }),
406
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
407
+ label: t("autoCheckInCN"),
408
+ hint: t("autoCheckInHintCN"),
409
+ checked: projection.values.autoCheckInCN,
410
+ disabled: !signed.cn,
411
+ disabledHint: t("quotaSignInRequired"),
412
+ onToggle: (next) => write("autoCheckInCN", next)
413
+ }),
414
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
415
+ label: t("autoCheckInGlobal"),
416
+ hint: t("autoCheckInHintGlobal"),
417
+ checked: projection.values.autoCheckInGlobal,
418
+ disabled: !signed.global,
419
+ disabledHint: t("quotaSignInRequired"),
420
+ onToggle: (next) => write("autoCheckInGlobal", next)
421
+ }),
398
422
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
399
423
  style: {
400
424
  ...rowStyle$1,
@@ -1040,6 +1064,136 @@ window.__ModuleLoader__.load({
1040
1064
  if (tokens >= 1e3 && tokens % 1e3 === 0) return `${tokens / 1e3}K`;
1041
1065
  return String(tokens);
1042
1066
  }
1067
+ function CheckInLogTable({ logs = [], t, onCheckIn, onRefresh, onClear, busy, checkingIn, clearing, disabled, notice }) {
1068
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1069
+ style: quotaListStyle,
1070
+ children: [
1071
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1072
+ style: {
1073
+ display: "flex",
1074
+ alignItems: "center",
1075
+ justifyContent: "space-between",
1076
+ flexWrap: "wrap",
1077
+ gap: 8
1078
+ },
1079
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1080
+ style: quotaTitleStyle,
1081
+ children: t("tabCheckIn")
1082
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1083
+ style: {
1084
+ display: "flex",
1085
+ alignItems: "center",
1086
+ gap: 6
1087
+ },
1088
+ children: [
1089
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1090
+ type: "button",
1091
+ style: buttonStyle$1,
1092
+ disabled: disabled || busy || checkingIn,
1093
+ onClick: onCheckIn,
1094
+ children: checkingIn ? t("checkInChecking") : t("checkInNow")
1095
+ }),
1096
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1097
+ type: "button",
1098
+ style: buttonStyle$1,
1099
+ disabled: busy || checkingIn,
1100
+ onClick: onRefresh,
1101
+ children: busy ? t("checkInRefreshing") : t("checkInRefresh")
1102
+ }),
1103
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1104
+ type: "button",
1105
+ style: buttonStyle$1,
1106
+ disabled: busy || clearing || !logs || logs.length === 0,
1107
+ onClick: onClear,
1108
+ children: clearing ? t("checkInClearing") : t("checkInClear")
1109
+ })
1110
+ ]
1111
+ })]
1112
+ }),
1113
+ notice === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1114
+ style: bodyStyle,
1115
+ children: notice
1116
+ }),
1117
+ !logs || logs.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1118
+ style: descriptionStyle,
1119
+ children: t("checkInLogEmpty")
1120
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1121
+ style: {
1122
+ display: "flex",
1123
+ flexDirection: "column",
1124
+ gap: 6
1125
+ },
1126
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1127
+ style: {
1128
+ display: "flex",
1129
+ borderBottom: "1px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.15))",
1130
+ paddingBottom: 6,
1131
+ fontSize: 12,
1132
+ color: "var(--dsw-alias-label-tertiary)"
1133
+ },
1134
+ children: [
1135
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1136
+ style: { flex: 2 },
1137
+ children: t("checkInLogTime")
1138
+ }),
1139
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1140
+ style: { flex: 3 },
1141
+ children: t("checkInLogResult")
1142
+ }),
1143
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1144
+ style: {
1145
+ flex: 1,
1146
+ textAlign: "right"
1147
+ },
1148
+ children: t("checkInLogAmount")
1149
+ })
1150
+ ]
1151
+ }), logs.map((log) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1152
+ style: {
1153
+ display: "flex",
1154
+ alignItems: "center",
1155
+ padding: "6px 0",
1156
+ fontSize: 13,
1157
+ borderBottom: "1px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.08))"
1158
+ },
1159
+ children: [
1160
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1161
+ style: {
1162
+ flex: 2,
1163
+ color: "var(--dsw-alias-label-secondary)"
1164
+ },
1165
+ children: formatTime(log.timestamp)
1166
+ }),
1167
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1168
+ style: {
1169
+ flex: 3,
1170
+ display: "flex",
1171
+ alignItems: "center",
1172
+ gap: 6
1173
+ },
1174
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: {
1175
+ width: 6,
1176
+ height: 6,
1177
+ borderRadius: "50%",
1178
+ flexShrink: 0,
1179
+ background: log.status === "claimed" ? "var(--dsw-alias-status-success, #52c41a)" : log.status === "already-claimed" ? "var(--dsw-alias-status-info, #1890ff)" : log.status === "no-campaign" ? "var(--dsw-alias-label-tertiary, #999)" : "var(--dsw-alias-status-error, #f5222d)"
1180
+ } }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: log.status === "claimed" ? t("autoCheckInStatusClaimed", { amount: log.amount ?? 100 }) : log.status === "already-claimed" ? t("autoCheckInStatusAlready") : log.status === "no-campaign" ? t("autoCheckInStatusNoCampaign") : t("autoCheckInStatusError", { message: log.message ?? "" }) })]
1181
+ }),
1182
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1183
+ style: {
1184
+ flex: 1,
1185
+ textAlign: "right",
1186
+ fontWeight: 600,
1187
+ color: log.amount ? "var(--dsw-alias-brand-primary)" : "inherit"
1188
+ },
1189
+ children: log.amount ? `+${log.amount}` : "-"
1190
+ })
1191
+ ]
1192
+ }, log.id))]
1193
+ })
1194
+ ]
1195
+ });
1196
+ }
1043
1197
  /** Render Qoder PAT state, quota, catalog, and context capacities as one expandable card. */
1044
1198
  function QoderPluginCard(props) {
1045
1199
  const { t, scope, signedIn, variant, unified } = props;
@@ -1062,6 +1216,9 @@ window.__ModuleLoader__.load({
1062
1216
  const [patNotice, setPatNotice] = (0, react.useState)();
1063
1217
  const patInput = (0, react.useRef)(null);
1064
1218
  const [tab, setTab] = (0, react.useState)("status");
1219
+ const [checkingIn, setCheckingIn] = (0, react.useState)(false);
1220
+ const [clearingLogs, setClearingLogs] = (0, react.useState)(false);
1221
+ const [checkInNotice, setCheckInNotice] = (0, react.useState)();
1065
1222
  const mounted = (0, react.useRef)(true);
1066
1223
  const readSeq = (0, react.useRef)(0);
1067
1224
  const manualControllers = (0, react.useRef)(/* @__PURE__ */ new Set());
@@ -1202,6 +1359,83 @@ window.__ModuleLoader__.load({
1202
1359
  t,
1203
1360
  trackController
1204
1361
  ]);
1362
+ const manualCheckIn = (0, react.useCallback)(async () => {
1363
+ const key = status?.status === "signed-in" ? status.probeKey : void 0;
1364
+ if (key === void 0) return;
1365
+ setCheckingIn(true);
1366
+ setCheckInNotice(void 0);
1367
+ const controller = trackController();
1368
+ try {
1369
+ const response = await fetch(currentVariant.probePath, {
1370
+ method: "POST",
1371
+ headers: {
1372
+ "Content-Type": "application/json",
1373
+ "X-Qoder-Probe-Key": key
1374
+ },
1375
+ credentials: "same-origin",
1376
+ signal: controller.signal,
1377
+ body: JSON.stringify({ action: "checkin" })
1378
+ });
1379
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
1380
+ const result = await response.json();
1381
+ if (result.state === "claimed") setCheckInNotice(t("autoCheckInStatusClaimed", { amount: result.amount ?? 100 }));
1382
+ else if (result.state === "already-claimed") setCheckInNotice(t("autoCheckInStatusAlready"));
1383
+ else if (result.state === "no-campaign") setCheckInNotice(t("autoCheckInStatusNoCampaign"));
1384
+ else if (result.reason) setCheckInNotice(t("autoCheckInStatusError", { message: result.reason }));
1385
+ } catch (error) {
1386
+ if (mounted.current && controller.signal.aborted !== true) setCheckInNotice(error instanceof Error ? error.message : t("requestFailed"));
1387
+ } finally {
1388
+ manualControllers.current.delete(controller);
1389
+ if (mounted.current) setCheckingIn(false);
1390
+ }
1391
+ try {
1392
+ await refresh(controller.signal);
1393
+ } finally {
1394
+ manualControllers.current.delete(controller);
1395
+ }
1396
+ }, [
1397
+ currentVariant.probePath,
1398
+ refresh,
1399
+ status,
1400
+ t,
1401
+ trackController
1402
+ ]);
1403
+ const clearCheckInLogs = (0, react.useCallback)(async () => {
1404
+ const key = status?.status === "signed-in" ? status.probeKey : void 0;
1405
+ if (key === void 0) return;
1406
+ setClearingLogs(true);
1407
+ setCheckInNotice(void 0);
1408
+ const controller = trackController();
1409
+ try {
1410
+ const response = await fetch(currentVariant.probePath, {
1411
+ method: "POST",
1412
+ headers: {
1413
+ "Content-Type": "application/json",
1414
+ "X-Qoder-Probe-Key": key
1415
+ },
1416
+ credentials: "same-origin",
1417
+ signal: controller.signal,
1418
+ body: JSON.stringify({ action: "clear-checkin-logs" })
1419
+ });
1420
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
1421
+ } catch (error) {
1422
+ if (mounted.current && controller.signal.aborted !== true) setCheckInNotice(error instanceof Error ? error.message : t("requestFailed"));
1423
+ } finally {
1424
+ manualControllers.current.delete(controller);
1425
+ if (mounted.current) setClearingLogs(false);
1426
+ }
1427
+ try {
1428
+ await refresh(controller.signal);
1429
+ } finally {
1430
+ manualControllers.current.delete(controller);
1431
+ }
1432
+ }, [
1433
+ currentVariant.probePath,
1434
+ refresh,
1435
+ status,
1436
+ t,
1437
+ trackController
1438
+ ]);
1205
1439
  /**
1206
1440
  * Run one probe-route control action and refresh the card's state afterwards.
1207
1441
  *
@@ -1622,7 +1856,8 @@ window.__ModuleLoader__.load({
1622
1856
  children: [
1623
1857
  "status",
1624
1858
  "context",
1625
- "details"
1859
+ "details",
1860
+ "checkin"
1626
1861
  ].map((id) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1627
1862
  type: "button",
1628
1863
  role: "tab",
@@ -1634,7 +1869,7 @@ window.__ModuleLoader__.load({
1634
1869
  ...tabStyle,
1635
1870
  ...tab === id ? tabActiveStyle : {}
1636
1871
  },
1637
- children: t(id === "status" ? "tabStatus" : id === "context" ? "tabContext" : "tabDetails")
1872
+ children: t(id === "status" ? "tabStatus" : id === "context" ? "tabContext" : id === "details" ? "tabDetails" : "tabCheckIn")
1638
1873
  }, id))
1639
1874
  }),
1640
1875
  tab === "status" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -1672,7 +1907,7 @@ window.__ModuleLoader__.load({
1672
1907
  });
1673
1908
  }
1674
1909
  })
1675
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1910
+ }) : tab === "details" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1676
1911
  style: tabPanelStyle,
1677
1912
  children: status.credits === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1678
1913
  style: quotaListStyle,
@@ -1688,6 +1923,26 @@ window.__ModuleLoader__.load({
1688
1923
  t
1689
1924
  }, `${account.packageName}-${String(index)}`))]
1690
1925
  })
1926
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1927
+ style: tabPanelStyle,
1928
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckInLogTable, {
1929
+ logs: status.checkIn?.logs,
1930
+ t,
1931
+ busy,
1932
+ checkingIn,
1933
+ clearing: clearingLogs,
1934
+ disabled: status.status !== "signed-in",
1935
+ ...checkInNotice === void 0 ? {} : { notice: checkInNotice },
1936
+ onCheckIn: () => {
1937
+ manualCheckIn();
1938
+ },
1939
+ onRefresh: () => {
1940
+ manualRefresh();
1941
+ },
1942
+ onClear: () => {
1943
+ clearCheckInLogs();
1944
+ }
1945
+ })
1691
1946
  })
1692
1947
  ] }) : null,
1693
1948
  status?.status === "signed-out" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
@@ -2958,6 +3213,17 @@ window.__ModuleLoader__.load({
2958
3213
  tabStatus: "Status",
2959
3214
  tabContext: "Context window",
2960
3215
  tabDetails: "Credit details",
3216
+ tabCheckIn: "Check-in log",
3217
+ checkInLogTime: "Check-in time",
3218
+ checkInLogResult: "Result",
3219
+ checkInLogAmount: "Credits",
3220
+ checkInLogEmpty: "No check-in logs recorded yet.",
3221
+ checkInNow: "Check in now",
3222
+ checkInChecking: "Checking in…",
3223
+ checkInRefresh: "Refresh",
3224
+ checkInRefreshing: "Refreshing…",
3225
+ checkInClear: "Clear logs",
3226
+ checkInClearing: "Clearing…",
2961
3227
  creditsDetailHeading: "By package",
2962
3228
  creditsUsed: "Used this cycle: {percent}%",
2963
3229
  creditsTotalUnlimited: "Total: Unlimited",
@@ -3020,6 +3286,14 @@ window.__ModuleLoader__.load({
3020
3286
  quotaToggleCN: "Show China credit card",
3021
3287
  quotaToggleGlobal: "Show global credit card",
3022
3288
  quotaToggleHint: "Show this account’s remaining credit in the sidebar footer.",
3289
+ autoCheckInCN: "Qoder (China) daily auto check-in (100 Credits)",
3290
+ autoCheckInGlobal: "Qoder Global (International) daily auto check-in",
3291
+ autoCheckInHintCN: "Automatically check in daily at 10:00 (UTC+8) to claim 100 Credits for China account.",
3292
+ autoCheckInHintGlobal: "Automatically check in daily at 10:00 (UTC+8) to claim available perks for Global account.",
3293
+ autoCheckInStatusClaimed: "Checked in today (+{amount} Credits)",
3294
+ autoCheckInStatusAlready: "Already checked in today",
3295
+ autoCheckInStatusNoCampaign: "No active benefit campaign today",
3296
+ autoCheckInStatusError: "Auto check-in error: {message}",
3023
3297
  quotaSignInRequired: "Save a PAT for this variant first to enable this card.",
3024
3298
  quotaPollLabel: "Refresh interval",
3025
3299
  quotaPollHint: "Applies to both quota cards. Longer is kinder to the billing endpoint.",
@@ -3092,6 +3366,17 @@ window.__ModuleLoader__.load({
3092
3366
  tabStatus: "状态",
3093
3367
  tabContext: "上下文窗口",
3094
3368
  tabDetails: "额度明细",
3369
+ tabCheckIn: "签到日志",
3370
+ checkInLogTime: "签到时间",
3371
+ checkInLogResult: "签到结果",
3372
+ checkInLogAmount: "获得额度",
3373
+ checkInLogEmpty: "暂无签到日志记录。",
3374
+ checkInNow: "立即签到",
3375
+ checkInChecking: "正在签到…",
3376
+ checkInRefresh: "刷新",
3377
+ checkInRefreshing: "正在刷新…",
3378
+ checkInClear: "清空日志",
3379
+ checkInClearing: "正在清空…",
3095
3380
  creditsDetailHeading: "按套餐",
3096
3381
  creditsUsed: "本轮已用 {percent}%",
3097
3382
  creditsTotalUnlimited: "合计:不限额",
@@ -3154,6 +3439,14 @@ window.__ModuleLoader__.load({
3154
3439
  quotaToggleCN: "展示国内版额度",
3155
3440
  quotaToggleGlobal: "展示国际版额度",
3156
3441
  quotaToggleHint: "在侧栏底部展示该账号的剩余额度。",
3442
+ autoCheckInCN: "Qoder(国内版)每日自动签到",
3443
+ autoCheckInGlobal: "Qoder Global(国际版)每日自动签到",
3444
+ autoCheckInHintCN: "每天 10:00 (UTC+8) 自动签到领取 100 Credits 算力额度。",
3445
+ autoCheckInHintGlobal: "每天 10:00 (UTC+8) 自动检测并领取国际版可用福利额度。",
3446
+ autoCheckInStatusClaimed: "今日已自动签到(+{amount} Credits)",
3447
+ autoCheckInStatusAlready: "今日已完成签到",
3448
+ autoCheckInStatusNoCampaign: "今日无可用签到福利活动",
3449
+ autoCheckInStatusError: "自动签到出错:{message}",
3157
3450
  quotaSignInRequired: "请先为该变体保存 PAT,再开启此开关。",
3158
3451
  quotaPollLabel: "刷新间隔",
3159
3452
  quotaPollHint: "对两张额度卡片同时生效。间隔越长对计费接口越友好。",
package/lib/index.d.ts CHANGED
@@ -141,7 +141,7 @@ interface QoderProbeAction {
141
141
  * All are writes, which is why they share this route's in-process key
142
142
  * and loopback guards rather than the read-only status GET.
143
143
  */
144
- action: 'probe' | 'clear' | 'refresh' | 'set-maximum-context-window';
144
+ action: 'probe' | 'clear' | 'refresh' | 'set-maximum-context-window' | 'clear-checkin-logs' | 'checkin';
145
145
  /** Target model id; required for `probe`. */
146
146
  model?: string;
147
147
  /** Requested value for `set-maximum-context-window`. */
@@ -259,6 +259,25 @@ type QoderWebStatus = {
259
259
  * loopback guard); it is never persisted and rotates per process.
260
260
  */
261
261
  probeKey?: string;
262
+ /**
263
+ * Daily check-in status record for this variant.
264
+ */
265
+ checkIn?: {
266
+ lastDate: string;
267
+ lastAt: number;
268
+ status: 'claimed' | 'already-claimed' | 'no-campaign' | 'error';
269
+ amount?: number | undefined;
270
+ message?: string | undefined;
271
+ logs?: readonly {
272
+ id: string;
273
+ date: string;
274
+ timestamp: number;
275
+ status: 'claimed' | 'already-claimed' | 'no-campaign' | 'error';
276
+ amount?: number | undefined;
277
+ campaignKey?: string | undefined;
278
+ message?: string | undefined;
279
+ }[] | undefined;
280
+ };
262
281
  /**
263
282
  * In-process key authorizing PAT writes, including clearing. Travels with
264
283
  * the document for the same reason `probeKey` does.
@@ -692,6 +711,17 @@ interface QoderAccountInfo {
692
711
  updatedAt: string;
693
712
  }
694
713
  //#endregion
714
+ //#region src/qoder/transport/checkin.d.ts
715
+ interface QoderCheckInResult {
716
+ variantId: string;
717
+ date: string;
718
+ timestamp: number;
719
+ status: 'claimed' | 'already-claimed' | 'no-campaign' | 'error';
720
+ amount?: number | undefined;
721
+ campaignKey?: string | undefined;
722
+ message?: string | undefined;
723
+ }
724
+ //#endregion
695
725
  //#region src/qoder/transport/index.d.ts
696
726
  interface QoderTransport {
697
727
  stream(options: GenerateOptions, model?: QoderCatalogModel): AsyncIterable<StreamChunk>;
@@ -700,6 +730,7 @@ interface QoderTransport {
700
730
  force?: boolean | undefined;
701
731
  signal?: AbortSignal | undefined;
702
732
  }): Promise<QoderAccountInfo>;
733
+ checkIn(signal?: AbortSignal): Promise<QoderCheckInResult>;
703
734
  }
704
735
  //#endregion
705
736
  //#region src/upstream.d.ts
@@ -840,6 +871,10 @@ declare class QoderUpstreamClient {
840
871
  * account's "unlimited" state.
841
872
  */
842
873
  fetchCredits(signal?: AbortSignal): Promise<QoderCredits>;
874
+ /**
875
+ * Run daily benefit check-in for this client's variant.
876
+ */
877
+ checkIn(signal?: AbortSignal): Promise<QoderCheckInResult>;
843
878
  /**
844
879
  * Stream one translated chat-completions request.
845
880
  *
@@ -1345,6 +1380,10 @@ interface Config {
1345
1380
  sidebarQuotaCN?: boolean;
1346
1381
  /** Show the global variant's sidebar quota card. */
1347
1382
  sidebarQuotaGlobal?: boolean;
1383
+ /** Automatically check in daily at 10:00 (UTC+8) to claim credits for China variant. */
1384
+ autoCheckInCN?: boolean;
1385
+ /** Automatically check in daily at 10:00 (UTC+8) to claim credits for Global variant. */
1386
+ autoCheckInGlobal?: boolean;
1348
1387
  /**
1349
1388
  * Sidebar quota refresh interval in milliseconds. One shared value (both
1350
1389
  * cards poll on it) because the two widgets hit the same rate-limited
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-BEiQve5M.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";
@@ -1056,15 +1056,20 @@ async function qoderWebStatus(deps) {
1056
1056
  ...probed,
1057
1057
  jobTokenRefreshedAt: refreshedAt
1058
1058
  };
1059
+ const checkInRecord = deps.checkIn?.();
1060
+ const withCheckIn = checkInRecord === void 0 ? withRefreshNotice : {
1061
+ ...withRefreshNotice,
1062
+ checkIn: checkInRecord
1063
+ };
1059
1064
  try {
1060
1065
  const credits = await deps.client.fetchCredits();
1061
1066
  return {
1062
- ...withRefreshNotice,
1067
+ ...withCheckIn,
1063
1068
  credits
1064
1069
  };
1065
1070
  } catch (error) {
1066
1071
  return {
1067
- ...withRefreshNotice,
1072
+ ...withCheckIn,
1068
1073
  creditsError: safeMessage(error)
1069
1074
  };
1070
1075
  }
@@ -1169,6 +1174,8 @@ function parseAction(text) {
1169
1174
  const wrapped = parsed;
1170
1175
  const action = wrapped["action"];
1171
1176
  if (action === "clear") return { action: "clear" };
1177
+ if (action === "clear-checkin-logs") return { action: "clear-checkin-logs" };
1178
+ if (action === "checkin") return { action: "checkin" };
1172
1179
  if (action === "refresh") return { action: "refresh" };
1173
1180
  if (action === "set-maximum-context-window") return typeof wrapped["enabled"] === "boolean" ? {
1174
1181
  action: "set-maximum-context-window",
@@ -1212,6 +1219,19 @@ function qoderProbeHandler(deps, key) {
1212
1219
  return;
1213
1220
  }
1214
1221
  try {
1222
+ if (action.action === "clear-checkin-logs") {
1223
+ deps.clearCheckInLogs?.();
1224
+ json(res, 200, { state: "cleared" });
1225
+ return;
1226
+ }
1227
+ if (action.action === "checkin") {
1228
+ if (deps.checkIn === void 0) {
1229
+ json(res, 404, { error: "checkin-not-supported" });
1230
+ return;
1231
+ }
1232
+ json(res, 200, await deps.checkIn());
1233
+ return;
1234
+ }
1215
1235
  if (action.action === "clear") {
1216
1236
  deps.clear();
1217
1237
  json(res, 200, { state: "cleared" });
@@ -1254,6 +1274,162 @@ function registerQoderProbeRoute(ctx, deps, key) {
1254
1274
  }, "dsh-qoder-connect: probe control route");
1255
1275
  }
1256
1276
  //#endregion
1277
+ //#region src/checkin-scheduler.ts
1278
+ /** Scheduling and catch-up orchestration for daily 10:00 (UTC+8) check-in. */
1279
+ var JsonFileCheckInStore = class {
1280
+ filePath;
1281
+ constructor(filePath) {
1282
+ this.filePath = filePath ?? join(qoderPluginDataDir(), "checkin-status.json");
1283
+ }
1284
+ readAll() {
1285
+ try {
1286
+ if (!existsSync(this.filePath)) return {};
1287
+ const raw = readFileSync(this.filePath, "utf-8");
1288
+ return JSON.parse(raw);
1289
+ } catch {
1290
+ return {};
1291
+ }
1292
+ }
1293
+ read(variantId) {
1294
+ return this.readAll()[variantId];
1295
+ }
1296
+ clearLogs(variantId) {
1297
+ try {
1298
+ const all = this.readAll();
1299
+ if (all[variantId]) {
1300
+ all[variantId] = {
1301
+ ...all[variantId],
1302
+ logs: []
1303
+ };
1304
+ mkdirSync(dirname(this.filePath), { recursive: true });
1305
+ writeFileSync(this.filePath, JSON.stringify(all, null, 2), "utf-8");
1306
+ }
1307
+ } catch {}
1308
+ }
1309
+ write(variantId, record) {
1310
+ try {
1311
+ const all = this.readAll();
1312
+ const existingLogs = all[variantId]?.logs ?? [];
1313
+ const newLog = {
1314
+ id: `${record.lastDate}-${record.lastAt}`,
1315
+ date: record.lastDate,
1316
+ timestamp: record.lastAt,
1317
+ status: record.status,
1318
+ ...record.amount === void 0 ? {} : { amount: record.amount },
1319
+ ...record.message === void 0 ? {} : { message: record.message }
1320
+ };
1321
+ const updatedLogs = [newLog, ...existingLogs.filter((l) => l.id !== newLog.id)].slice(0, 30);
1322
+ all[variantId] = {
1323
+ ...record,
1324
+ logs: updatedLogs
1325
+ };
1326
+ mkdirSync(dirname(this.filePath), { recursive: true });
1327
+ writeFileSync(this.filePath, JSON.stringify(all, null, 2), "utf-8");
1328
+ } catch {}
1329
+ }
1330
+ };
1331
+ /**
1332
+ * Returns the current date in YYYY-MM-DD standardized on UTC+8 (Beijing Time).
1333
+ */
1334
+ function getUtc8DateString(nowMs = Date.now()) {
1335
+ const d = new Date(nowMs);
1336
+ const utc8 = new Date(d.getTime() + (d.getTimezoneOffset() + 480) * 6e4);
1337
+ return `${utc8.getFullYear()}-${String(utc8.getMonth() + 1).padStart(2, "0")}-${String(utc8.getDate()).padStart(2, "0")}`;
1338
+ }
1339
+ /**
1340
+ * Calculates milliseconds until the next 10:00:05 AM in UTC+8.
1341
+ */
1342
+ function msUntilNext10amUtc8(nowMs = Date.now()) {
1343
+ const d = new Date(nowMs);
1344
+ const utc8Time = new Date(d.getTime() + (d.getTimezoneOffset() + 480) * 6e4);
1345
+ const targetUtc8 = new Date(utc8Time.getTime());
1346
+ targetUtc8.setHours(10, 0, 5, 0);
1347
+ let diff = targetUtc8.getTime() - utc8Time.getTime();
1348
+ if (diff <= 0) {
1349
+ targetUtc8.setDate(targetUtc8.getDate() + 1);
1350
+ diff = targetUtc8.getTime() - utc8Time.getTime();
1351
+ }
1352
+ return diff;
1353
+ }
1354
+ /**
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.
1357
+ */
1358
+ function shouldCatchUp(today, lastDate, nowMs = Date.now()) {
1359
+ if (lastDate === today) return false;
1360
+ const d = new Date(nowMs);
1361
+ return new Date(d.getTime() + (d.getTimezoneOffset() + 480) * 6e4).getHours() >= 10;
1362
+ }
1363
+ var CheckInScheduler = class {
1364
+ targets;
1365
+ isEnabled;
1366
+ store;
1367
+ onResult;
1368
+ now;
1369
+ timer;
1370
+ disposed = false;
1371
+ constructor(options) {
1372
+ this.targets = options.targets;
1373
+ this.isEnabled = options.isEnabled;
1374
+ this.store = options.store ?? new JsonFileCheckInStore();
1375
+ this.onResult = options.onResult;
1376
+ this.now = options.now ?? Date.now;
1377
+ }
1378
+ start() {
1379
+ if (this.disposed) return;
1380
+ this.sweepAll(true);
1381
+ this.armNextTimer();
1382
+ }
1383
+ dispose() {
1384
+ this.disposed = true;
1385
+ if (this.timer) {
1386
+ clearTimeout(this.timer);
1387
+ this.timer = void 0;
1388
+ }
1389
+ }
1390
+ armNextTimer() {
1391
+ 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?.();
1398
+ }
1399
+ async sweepAll(isCatchUp) {
1400
+ if (this.disposed) return;
1401
+ const nowMs = this.now();
1402
+ const today = getUtc8DateString(nowMs);
1403
+ for (const target of this.targets) {
1404
+ 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;
1409
+ try {
1410
+ pat = await target.getPat();
1411
+ } catch {
1412
+ continue;
1413
+ }
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
+ }
1430
+ }
1431
+ };
1432
+ //#endregion
1257
1433
  //#region src/job-token-hint.ts
1258
1434
  /** The name recorded on the hint row (and therefore its displayed title). */
1259
1435
  const JOB_TOKEN_HINT_NAME = "qoder";
@@ -1423,6 +1599,8 @@ const MAXIMUM_CONTEXT_WINDOW_CN_FIELD = z.boolean().default(true).description("U
1423
1599
  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)");
1424
1600
  /** Sidebar quota toggle (one per variant; both live on the shared quota card). */
1425
1601
  const QUOTA_TOGGLE_FIELD = z.boolean().default(false).description("Show this variant’s remaining-credit card in the sidebar footer (off by default)");
1602
+ /** Automatic check-in toggle. */
1603
+ const AUTO_CHECK_IN_FIELD = z.boolean().default(false).description("每天 10:00 (UTC+8) 自动签到领取算力额度(默认关闭)");
1426
1604
  /**
1427
1605
  * Quota poll interval: default 5 minutes, floor 1 minute. The status route
1428
1606
  * performs a live upstream billing call per request with no cache, so an
@@ -1441,6 +1619,8 @@ const Config = z.object({
1441
1619
  modelContextWindowsCN: MODEL_CONTEXT_WINDOWS_FIELD,
1442
1620
  sidebarQuotaCN: QUOTA_TOGGLE_FIELD,
1443
1621
  sidebarQuotaGlobal: QUOTA_TOGGLE_FIELD,
1622
+ autoCheckInCN: AUTO_CHECK_IN_FIELD,
1623
+ autoCheckInGlobal: AUTO_CHECK_IN_FIELD,
1444
1624
  quotaPollMs: QUOTA_POLL_FIELD
1445
1625
  });
1446
1626
  /**
@@ -1472,6 +1652,8 @@ const GLOBAL_SECTION = z.object({
1472
1652
  const QUOTA_SECTION = z.object({
1473
1653
  sidebarQuotaCN: QUOTA_TOGGLE_FIELD,
1474
1654
  sidebarQuotaGlobal: QUOTA_TOGGLE_FIELD,
1655
+ autoCheckInCN: AUTO_CHECK_IN_FIELD,
1656
+ autoCheckInGlobal: AUTO_CHECK_IN_FIELD,
1475
1657
  quotaPollMs: QUOTA_POLL_FIELD
1476
1658
  });
1477
1659
  /** Stable identity key used by credentials, probe records, and catalog entries. */
@@ -1740,6 +1922,24 @@ function apply(ctx, config) {
1740
1922
  */
1741
1923
  const lastIdentities = /* @__PURE__ */ new Map();
1742
1924
  const runtimes = QODER_VARIANTS.map((variant) => createVariantRuntime(ctx, config, variant, () => current(), (id) => lastIdentities.get(id)));
1925
+ const checkInStore = new JsonFileCheckInStore();
1926
+ const checkInScheduler = new CheckInScheduler({
1927
+ targets: runtimes.map((runtime) => ({
1928
+ variantId: runtime.variant.id,
1929
+ service: runtime.transport.checkInService,
1930
+ getPat: async () => runtime.store.patPromise(),
1931
+ onClaimed: () => {
1932
+ runtime.client.fetchCredits().catch(() => void 0);
1933
+ }
1934
+ })),
1935
+ isEnabled: (variantId) => {
1936
+ const cfg = current();
1937
+ if (variantId === CHINA_VARIANT.id) return cfg.autoCheckInCN === true;
1938
+ return cfg.autoCheckInGlobal === true;
1939
+ },
1940
+ store: checkInStore
1941
+ });
1942
+ checkInScheduler.start();
1743
1943
  installJobTokenHint(ctx);
1744
1944
  const probeKey = createProbeKey();
1745
1945
  /**
@@ -1823,7 +2023,8 @@ function apply(ctx, config) {
1823
2023
  probeKey,
1824
2024
  authKey,
1825
2025
  ...runtime.variant.id === CHINA_VARIANT.id ? { useMaximumContextWindow: () => current().useMaximumContextWindowCN === true } : { useMaximumContextWindow: () => current().useMaximumContextWindow === true },
1826
- jobTokenRefreshedAt: runtime.jobTokenRefreshedAt
2026
+ jobTokenRefreshedAt: runtime.jobTokenRefreshedAt,
2027
+ checkIn: () => checkInStore.read(runtime.variant.id)
1827
2028
  });
1828
2029
  registerQoderAuthRoute(webCtx, {
1829
2030
  path: runtime.variant.authPath,
@@ -1886,6 +2087,36 @@ function apply(ctx, config) {
1886
2087
  reason: runtime.catalogError
1887
2088
  };
1888
2089
  },
2090
+ clearCheckInLogs: () => {
2091
+ checkInStore.clearLogs(runtime.variant.id);
2092
+ },
2093
+ 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);
2104
+ if (result.status !== "error") {
2105
+ checkInStore.write(runtime.variant.id, {
2106
+ lastDate: result.date,
2107
+ lastAt: result.timestamp,
2108
+ status: result.status,
2109
+ ...result.amount === void 0 ? {} : { amount: result.amount },
2110
+ ...result.message === void 0 ? {} : { message: result.message }
2111
+ });
2112
+ if (result.status === "claimed") runtime.client.fetchCredits().catch(() => void 0);
2113
+ }
2114
+ return {
2115
+ state: result.status,
2116
+ ...result.amount === void 0 ? {} : { amount: result.amount },
2117
+ ...result.message === void 0 ? {} : { reason: result.message }
2118
+ };
2119
+ },
1889
2120
  ...runtime.variant.id === CHINA_VARIANT.id ? { setMaximumContextWindow: async (enabled) => {
1890
2121
  if (setMaximumContextWindowCN === void 0) return {
1891
2122
  state: "failed",
@@ -1966,6 +2197,7 @@ function apply(ctx, config) {
1966
2197
  });
1967
2198
  ctx.effect(() => () => {
1968
2199
  stopped = true;
2200
+ checkInScheduler.dispose();
1969
2201
  for (const timer of timers) clearInterval(timer);
1970
2202
  timers.length = 0;
1971
2203
  detach(ctx, clearHostHeartbeat(), "host heartbeat cleanup");
@@ -876,6 +876,14 @@ function getQoderUserStatusUrl(region = "global") {
876
876
  const { openApiUrl } = resolveQoderEndpoints(region);
877
877
  return `${openApiUrl}/api/v3/user/status`;
878
878
  }
879
+ function getQoderCampaignsUrl(region = "global") {
880
+ const { openApiUrl } = resolveQoderEndpoints(region);
881
+ return `${openApiUrl}/sash/api/v1/me/campaigns`;
882
+ }
883
+ function getQoderClaimCampaignUrl(region = "global", campaignId) {
884
+ const { openApiUrl } = resolveQoderEndpoints(region);
885
+ return `${openApiUrl}/sash/api/v1/me/campaigns/${encodeURIComponent(campaignId)}/claim`;
886
+ }
879
887
  //#endregion
880
888
  //#region src/qoder/errors.ts
881
889
  /**
@@ -2408,6 +2416,131 @@ var QoderUsageReader = class {
2408
2416
  }
2409
2417
  };
2410
2418
  //#endregion
2419
+ //#region src/qoder/transport/checkin.ts
2420
+ const defaultCheckInTimeoutMs = 15e3;
2421
+ var QoderCheckInService = class {
2422
+ authService;
2423
+ fetchImpl;
2424
+ region;
2425
+ variantId;
2426
+ logger;
2427
+ timeoutMs;
2428
+ constructor(options) {
2429
+ this.authService = options.authService;
2430
+ this.fetchImpl = options.fetch ?? globalThis.fetch;
2431
+ this.region = options.region ?? "china";
2432
+ this.variantId = options.variantId ?? "qoder";
2433
+ this.logger = options.logger;
2434
+ this.timeoutMs = options.timeoutMs ?? defaultCheckInTimeoutMs;
2435
+ }
2436
+ async fetchCampaigns(token, signal) {
2437
+ const url = getQoderCampaignsUrl(this.region);
2438
+ const data = await openApiJsonRequest(this.fetchImpl, {
2439
+ url,
2440
+ token,
2441
+ signal,
2442
+ timeoutMs: this.timeoutMs,
2443
+ logger: this.logger,
2444
+ operation: "Campaigns",
2445
+ logCategory: "campaigns.list"
2446
+ });
2447
+ return Array.isArray(data?.campaigns) ? data.campaigns : [];
2448
+ }
2449
+ async claimCampaign(token, campaignId, signal) {
2450
+ const url = getQoderClaimCampaignUrl(this.region, campaignId);
2451
+ const { openApiUrl } = resolveQoderEndpoints(this.region);
2452
+ return openApiJsonRequest(this.fetchImpl, {
2453
+ url,
2454
+ method: "POST",
2455
+ token,
2456
+ headers: { origin: openApiUrl },
2457
+ signal,
2458
+ timeoutMs: this.timeoutMs,
2459
+ logger: this.logger,
2460
+ operation: "ClaimCampaign",
2461
+ logCategory: "campaigns.claim"
2462
+ });
2463
+ }
2464
+ getTodayDateString() {
2465
+ const now = /* @__PURE__ */ new Date();
2466
+ const utc8Time = new Date(now.getTime() + (now.getTimezoneOffset() + 480) * 6e4);
2467
+ return `${utc8Time.getFullYear()}-${String(utc8Time.getMonth() + 1).padStart(2, "0")}-${String(utc8Time.getDate()).padStart(2, "0")}`;
2468
+ }
2469
+ async checkIn(pat, signal) {
2470
+ const today = this.getTodayDateString();
2471
+ const nowMs = Date.now();
2472
+ if (!pat || typeof pat !== "string" || pat.trim() === "") return {
2473
+ variantId: this.variantId,
2474
+ date: today,
2475
+ timestamp: nowMs,
2476
+ status: "error",
2477
+ message: "No PAT available"
2478
+ };
2479
+ const executeWithToken = async (authToken) => {
2480
+ const benefitCampaign = (await this.fetchCampaigns(authToken, signal)).find((c) => c.actionType === "CLAIM_BENEFIT");
2481
+ if (!benefitCampaign) return {
2482
+ variantId: this.variantId,
2483
+ date: today,
2484
+ timestamp: nowMs,
2485
+ status: "no-campaign",
2486
+ message: "No claimable benefit campaign found for this region"
2487
+ };
2488
+ if (benefitCampaign.claimStatus === "CLAIMED") return {
2489
+ variantId: this.variantId,
2490
+ date: today,
2491
+ timestamp: nowMs,
2492
+ status: "already-claimed",
2493
+ campaignKey: benefitCampaign.campaignKey,
2494
+ amount: benefitCampaign.benefit?.amount ?? 100,
2495
+ message: "Already claimed today"
2496
+ };
2497
+ const claimResult = await this.claimCampaign(authToken, benefitCampaign.campaignId, signal);
2498
+ const isClaimed = claimResult.status === "CLAIMED";
2499
+ const replayed = Boolean(claimResult.replayed);
2500
+ const amount = claimResult.benefit?.amount ?? benefitCampaign.benefit?.amount ?? 100;
2501
+ if (isClaimed) return {
2502
+ variantId: this.variantId,
2503
+ date: today,
2504
+ timestamp: nowMs,
2505
+ status: replayed ? "already-claimed" : "claimed",
2506
+ amount,
2507
+ campaignKey: benefitCampaign.campaignKey,
2508
+ message: replayed ? "Already claimed today" : `Successfully claimed ${amount} credits`
2509
+ };
2510
+ return {
2511
+ variantId: this.variantId,
2512
+ date: today,
2513
+ timestamp: nowMs,
2514
+ status: "error",
2515
+ campaignKey: benefitCampaign.campaignKey,
2516
+ message: `Claim returned status ${claimResult.status ?? "unknown"}`
2517
+ };
2518
+ };
2519
+ try {
2520
+ const creds = await this.authService.getCredentials(pat, signal);
2521
+ try {
2522
+ return await executeWithToken(creds.authToken);
2523
+ } catch (innerError) {
2524
+ if (isQoderAuthRejection(innerError)) {
2525
+ this.logger?.warn?.(`[Qoder CheckIn] Auth token rejected, retrying with fresh exchange`);
2526
+ return await executeWithToken((await this.authService.exchangeFresh(pat, signal)).authToken);
2527
+ }
2528
+ throw innerError;
2529
+ }
2530
+ } catch (error) {
2531
+ const message = error instanceof Error ? error.message : String(error);
2532
+ this.logger?.error?.(`[Qoder CheckIn] Check-in failed: ${message}`);
2533
+ return {
2534
+ variantId: this.variantId,
2535
+ date: today,
2536
+ timestamp: nowMs,
2537
+ status: "error",
2538
+ message
2539
+ };
2540
+ }
2541
+ }
2542
+ };
2543
+ //#endregion
2411
2544
  //#region src/qoder/transport/wire/encoding.ts
2412
2545
  /**
2413
2546
  * Qoder WAF Body encoding algorithm.
@@ -3058,6 +3191,7 @@ var DefaultQoderTransport = class {
3058
3191
  metadataTimeoutMs;
3059
3192
  auth;
3060
3193
  usage;
3194
+ checkInService;
3061
3195
  modelFlights = new SingleFlight();
3062
3196
  attachments;
3063
3197
  imageUploader;
@@ -3119,6 +3253,13 @@ var DefaultQoderTransport = class {
3119
3253
  region: this.region,
3120
3254
  timeoutMs: this.metadataTimeoutMs
3121
3255
  });
3256
+ this.checkInService = new QoderCheckInService({
3257
+ authService: this.auth,
3258
+ fetch: this.fetchImpl,
3259
+ logger: this.logger,
3260
+ region: this.region,
3261
+ timeoutMs: this.metadataTimeoutMs
3262
+ });
3122
3263
  }
3123
3264
  stream(options, model) {
3124
3265
  return this.generate(options, model);
@@ -3184,6 +3325,10 @@ var DefaultQoderTransport = class {
3184
3325
  signal: options?.signal
3185
3326
  });
3186
3327
  }
3328
+ async checkIn(signal) {
3329
+ const pat = await this.requirePat(signal);
3330
+ return this.checkInService.checkIn(pat, signal);
3331
+ }
3187
3332
  async requirePat(signal) {
3188
3333
  if (signal?.aborted) throw aborted("Qoder request was aborted.");
3189
3334
  const pat = (await this.resolvePat()).trim();
@@ -3848,6 +3993,12 @@ var QoderUpstreamClient = class {
3848
3993
  };
3849
3994
  }
3850
3995
  /**
3996
+ * Run daily benefit check-in for this client's variant.
3997
+ */
3998
+ async checkIn(signal) {
3999
+ return this.transport.checkIn(signal);
4000
+ }
4001
+ /**
3851
4002
  * Stream one translated chat-completions request.
3852
4003
  *
3853
4004
  * The first transport chunk is pulled before the response is constructed:
@@ -4082,7 +4233,7 @@ const DEFAULT_MAX_TOKENS = 32768;
4082
4233
  const DEFAULT_CONTEXT_WINDOW = 18e4;
4083
4234
  //#endregion
4084
4235
  //#region src/version.ts
4085
- const QODER_CONNECT_VERSION = "0.1.4";
4236
+ const QODER_CONNECT_VERSION = "0.1.5";
4086
4237
  //#endregion
4087
4238
  //#region src/host-heartbeat.ts
4088
4239
  /**
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dsh-qoder-connect",
3
3
  "displayName": "DSH Qoder Connect",
4
4
  "description": "将 Qoder(国内版 / 国际版)的模型接入 DeepSeek Harness — bring Qoder (CN & international) models into DeepSeek Harness.",
5
- "version": "0.1.4",
5
+ "version": "0.1.5",
6
6
  "author": "corrinehu",
7
7
  "keywords": [
8
8
  "dsh-plugin",