dsh-qoder-connect 0.1.2 → 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 +8 -0
- package/README.md +156 -148
- package/assets/7.png +0 -0
- package/lib/bin.js +1 -1
- package/lib/client.js +1449 -1217
- package/lib/index.d.ts +40 -1
- package/lib/index.js +280 -7
- package/lib/{variants-C0iZD8AD.js → variants-BEiQve5M.js} +226 -6
- package/package.json +2 -2
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-
|
|
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
|
-
...
|
|
1067
|
+
...withCheckIn,
|
|
1063
1068
|
credits
|
|
1064
1069
|
};
|
|
1065
1070
|
} catch (error) {
|
|
1066
1071
|
return {
|
|
1067
|
-
...
|
|
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";
|
|
@@ -1290,16 +1466,42 @@ function emitJobTokenHint(at, text) {
|
|
|
1290
1466
|
const agent = lastRunningAgent;
|
|
1291
1467
|
if (agent?.session === void 0) return;
|
|
1292
1468
|
lastRefreshAt.set(String(agent.id), at);
|
|
1469
|
+
appendHintRow(agent, "success", text);
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1472
|
+
* Print the failed-heal row into the conversation whose request triggered it.
|
|
1473
|
+
*
|
|
1474
|
+
* The mirror of {@link emitJobTokenHint}: the self-heal ran, exchanged a fresh
|
|
1475
|
+
* job token, retried, and the upstream still refused. Without this row that
|
|
1476
|
+
* outcome was silent — the user saw only the resulting authorization failure
|
|
1477
|
+
* and could not tell that recovery had already been attempted and lost.
|
|
1478
|
+
*
|
|
1479
|
+
* Rate limited by the transport (once per unresolved outage), not here: this
|
|
1480
|
+
* module stays a pure printer so the dedupe policy lives with the state that
|
|
1481
|
+
* knows when an outage ends.
|
|
1482
|
+
*
|
|
1483
|
+
* @param at - When the failed heal was recorded.
|
|
1484
|
+
* @param text - The row's summary line.
|
|
1485
|
+
*/
|
|
1486
|
+
function emitJobTokenRefreshFailedHint(at, text) {
|
|
1487
|
+
const agent = lastRunningAgent;
|
|
1488
|
+
if (agent?.session === void 0) return;
|
|
1489
|
+
appendHintRow(agent, "error", text);
|
|
1490
|
+
}
|
|
1491
|
+
/** Append one log-only command pair carrying `text` as its outcome. */
|
|
1492
|
+
function appendHintRow(agent, kind, text) {
|
|
1493
|
+
const session = agent.session;
|
|
1494
|
+
if (session === void 0) return;
|
|
1293
1495
|
const commandId = `cmd-qoder-hint-${Date.now().toString(36)}-${++hintSeq}`;
|
|
1294
1496
|
try {
|
|
1295
|
-
|
|
1497
|
+
session.append("command/run", {
|
|
1296
1498
|
commandId,
|
|
1297
1499
|
name: JOB_TOKEN_HINT_NAME,
|
|
1298
1500
|
source: { kind: "user" }
|
|
1299
1501
|
});
|
|
1300
|
-
|
|
1502
|
+
session.append("command/done", {
|
|
1301
1503
|
commandId,
|
|
1302
|
-
kind
|
|
1504
|
+
kind,
|
|
1303
1505
|
text
|
|
1304
1506
|
});
|
|
1305
1507
|
} catch {}
|
|
@@ -1397,6 +1599,8 @@ const MAXIMUM_CONTEXT_WINDOW_CN_FIELD = z.boolean().default(true).description("U
|
|
|
1397
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)");
|
|
1398
1600
|
/** Sidebar quota toggle (one per variant; both live on the shared quota card). */
|
|
1399
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) 自动签到领取算力额度(默认关闭)");
|
|
1400
1604
|
/**
|
|
1401
1605
|
* Quota poll interval: default 5 minutes, floor 1 minute. The status route
|
|
1402
1606
|
* performs a live upstream billing call per request with no cache, so an
|
|
@@ -1415,6 +1619,8 @@ const Config = z.object({
|
|
|
1415
1619
|
modelContextWindowsCN: MODEL_CONTEXT_WINDOWS_FIELD,
|
|
1416
1620
|
sidebarQuotaCN: QUOTA_TOGGLE_FIELD,
|
|
1417
1621
|
sidebarQuotaGlobal: QUOTA_TOGGLE_FIELD,
|
|
1622
|
+
autoCheckInCN: AUTO_CHECK_IN_FIELD,
|
|
1623
|
+
autoCheckInGlobal: AUTO_CHECK_IN_FIELD,
|
|
1418
1624
|
quotaPollMs: QUOTA_POLL_FIELD
|
|
1419
1625
|
});
|
|
1420
1626
|
/**
|
|
@@ -1446,6 +1652,8 @@ const GLOBAL_SECTION = z.object({
|
|
|
1446
1652
|
const QUOTA_SECTION = z.object({
|
|
1447
1653
|
sidebarQuotaCN: QUOTA_TOGGLE_FIELD,
|
|
1448
1654
|
sidebarQuotaGlobal: QUOTA_TOGGLE_FIELD,
|
|
1655
|
+
autoCheckInCN: AUTO_CHECK_IN_FIELD,
|
|
1656
|
+
autoCheckInGlobal: AUTO_CHECK_IN_FIELD,
|
|
1449
1657
|
quotaPollMs: QUOTA_POLL_FIELD
|
|
1450
1658
|
});
|
|
1451
1659
|
/** Stable identity key used by credentials, probe records, and catalog entries. */
|
|
@@ -1475,6 +1683,17 @@ function fallbackFor(_variant) {
|
|
|
1475
1683
|
function jobTokenHintText(at) {
|
|
1476
1684
|
return `jobToken 已自动刷新(${new Date(at).toLocaleTimeString("zh-CN", { hour12: false })})— 旧令牌被上游拒绝,已自动重换并恢复`;
|
|
1477
1685
|
}
|
|
1686
|
+
/**
|
|
1687
|
+
* The failed-heal row's summary line.
|
|
1688
|
+
*
|
|
1689
|
+
* Deliberately does not name a cause: the upstream rejection that survived a
|
|
1690
|
+
* fresh token is not necessarily an authorization problem at all, and the real
|
|
1691
|
+
* reason travels in the failure message itself (see the SSE envelope body).
|
|
1692
|
+
* Claiming "quota" or "revoked" here would be a guess presented as a finding.
|
|
1693
|
+
*/
|
|
1694
|
+
function jobTokenRefreshFailedHintText(at) {
|
|
1695
|
+
return `jobToken 已重换但仍被上游拒绝(${new Date(at).toLocaleTimeString("zh-CN", { hour12: false })})— 自愈未恢复,请查看上方错误详情`;
|
|
1696
|
+
}
|
|
1478
1697
|
/** Build one variant's stores, transport, and probe state. */
|
|
1479
1698
|
function createVariantRuntime(ctx, config, variant, current, identityOf) {
|
|
1480
1699
|
const store = new QoderCredentialStore({
|
|
@@ -1490,6 +1709,10 @@ function createVariantRuntime(ctx, config, variant, current, identityOf) {
|
|
|
1490
1709
|
jobTokenRefreshedAt = info.at;
|
|
1491
1710
|
ctx.logger.warn(`dsh-qoder-connect: ${variant.displayName} job token was auto-refreshed after an upstream rejection`);
|
|
1492
1711
|
emitJobTokenHint(info.at, jobTokenHintText(info.at));
|
|
1712
|
+
},
|
|
1713
|
+
onJobTokenRefreshFailed: (info) => {
|
|
1714
|
+
ctx.logger.warn(`dsh-qoder-connect: ${variant.displayName} job token refresh did not recover the chat (upstream status ${info.status ?? "unknown"})`);
|
|
1715
|
+
emitJobTokenRefreshFailedHint(info.at, jobTokenRefreshFailedHintText(info.at));
|
|
1493
1716
|
}
|
|
1494
1717
|
});
|
|
1495
1718
|
const client = new QoderUpstreamClient({
|
|
@@ -1699,6 +1922,24 @@ function apply(ctx, config) {
|
|
|
1699
1922
|
*/
|
|
1700
1923
|
const lastIdentities = /* @__PURE__ */ new Map();
|
|
1701
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();
|
|
1702
1943
|
installJobTokenHint(ctx);
|
|
1703
1944
|
const probeKey = createProbeKey();
|
|
1704
1945
|
/**
|
|
@@ -1782,7 +2023,8 @@ function apply(ctx, config) {
|
|
|
1782
2023
|
probeKey,
|
|
1783
2024
|
authKey,
|
|
1784
2025
|
...runtime.variant.id === CHINA_VARIANT.id ? { useMaximumContextWindow: () => current().useMaximumContextWindowCN === true } : { useMaximumContextWindow: () => current().useMaximumContextWindow === true },
|
|
1785
|
-
jobTokenRefreshedAt: runtime.jobTokenRefreshedAt
|
|
2026
|
+
jobTokenRefreshedAt: runtime.jobTokenRefreshedAt,
|
|
2027
|
+
checkIn: () => checkInStore.read(runtime.variant.id)
|
|
1786
2028
|
});
|
|
1787
2029
|
registerQoderAuthRoute(webCtx, {
|
|
1788
2030
|
path: runtime.variant.authPath,
|
|
@@ -1845,6 +2087,36 @@ function apply(ctx, config) {
|
|
|
1845
2087
|
reason: runtime.catalogError
|
|
1846
2088
|
};
|
|
1847
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
|
+
},
|
|
1848
2120
|
...runtime.variant.id === CHINA_VARIANT.id ? { setMaximumContextWindow: async (enabled) => {
|
|
1849
2121
|
if (setMaximumContextWindowCN === void 0) return {
|
|
1850
2122
|
state: "failed",
|
|
@@ -1925,6 +2197,7 @@ function apply(ctx, config) {
|
|
|
1925
2197
|
});
|
|
1926
2198
|
ctx.effect(() => () => {
|
|
1927
2199
|
stopped = true;
|
|
2200
|
+
checkInScheduler.dispose();
|
|
1928
2201
|
for (const timer of timers) clearInterval(timer);
|
|
1929
2202
|
timers.length = 0;
|
|
1930
2203
|
detach(ctx, clearHostHeartbeat(), "host heartbeat cleanup");
|