dsh-connect-minimaxcode 0.4.2 → 0.5.0

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,11 +1,12 @@
1
1
  import { readFileSync } from "node:fs";
2
- import { readFile, rename, writeFile } from "node:fs/promises";
2
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
- import { join } from "node:path";
4
+ import { dirname, join } from "node:path";
5
5
  import { createProvider } from "@earendil-works/pi-ai";
6
6
  import { anthropicMessagesApi } from "@earendil-works/pi-ai/api/anthropic-messages.lazy";
7
7
  import { LlmAdapter, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
8
8
  import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
9
+ import { createHash, randomBytes } from "node:crypto";
9
10
  //#region src/auth.ts
10
11
  /**
11
12
  * Reading the MiniMax Code desktop app's own sign-in.
@@ -625,6 +626,8 @@ function createMinimaxCodeAdapter(options) {
625
626
  //#region src/status-route.ts
626
627
  /** Route this plugin mounts, matching its settings card. */
627
628
  const MINIMAX_STATUS_PATH = "/plugins/dsh-connect-minimaxcode/status";
629
+ /** Route the card posts to when it wants something done rather than read. */
630
+ const MINIMAX_ACTION_PATH = "/plugins/dsh-connect-minimaxcode/action";
628
631
  const HINTS = {
629
632
  "app-missing": "未找到 MiniMax Code 的登录信息。请先安装并登录 MiniMax Code 桌面 App,然后重启 DSH。",
630
633
  "not-signed-in": "MiniMax Code 尚未登录。请在 MiniMax Code 桌面 App 中登录后重启 DSH。",
@@ -648,7 +651,9 @@ function statusDocument(deps) {
648
651
  models: deps.models,
649
652
  refreshing: deps.refreshing,
650
653
  ...deps.credits === void 0 ? {} : { credits: deps.credits },
651
- ...deps.usage === void 0 ? {} : { usage: deps.usage }
654
+ ...deps.usage === void 0 ? {} : { usage: deps.usage },
655
+ ...deps.signin === void 0 ? {} : { signin: deps.signin },
656
+ ...deps.login === void 0 ? {} : { login: deps.login }
652
657
  };
653
658
  if (auth.state === "expired") return {
654
659
  ...base,
@@ -719,7 +724,7 @@ const MEMBERSHIP_PATH = "/matrix/api/v1/commerce/get_membership_info";
719
724
  /** Credit ledger endpoint: per-grant amounts and expiry. */
720
725
  const CREDIT_DETAILS_PATH = "/minimax-cloud/api/v1/credit/details";
721
726
  /** OAuth token endpoint. */
722
- const TOKEN_PATH = "/oauth2/token";
727
+ const TOKEN_PATH$1 = "/oauth2/token";
723
728
  /** Absolute path of the app's credential file for a region. */
724
729
  function oauthAuthPath(region, env = process.env) {
725
730
  return join(dataDir(env), "auth", "prod", region, "mcode-public", "auth.json");
@@ -823,7 +828,7 @@ async function refreshOAuthCredential(region, credential, env = process.env, now
823
828
  scope: credential.scopes.join(" "),
824
829
  audience: credential.audience
825
830
  });
826
- const response = await fetchImpl(`${MINIMAX_AUTH_HOSTS[region]}${TOKEN_PATH}`, {
831
+ const response = await fetchImpl(`${MINIMAX_AUTH_HOSTS[region]}${TOKEN_PATH$1}`, {
827
832
  method: "POST",
828
833
  headers: {
829
834
  accept: "application/json",
@@ -920,17 +925,17 @@ function parseCreditSummary(document) {
920
925
  const summary = record.op_credit_summary;
921
926
  if (typeof summary === "object" && summary !== null) {
922
927
  const entry = summary;
923
- const total = toNumber(entry.total_remaining_amount);
928
+ const total = toNumber$1(entry.total_remaining_amount);
924
929
  if (total !== void 0) {
925
- const purchased = toNumber(entry.purchased_remaining_amount) ?? 0;
930
+ const purchased = toNumber$1(entry.purchased_remaining_amount) ?? 0;
926
931
  return {
927
932
  total,
928
933
  purchased,
929
- free: toNumber(entry.free_remaining_amount) ?? total - purchased
934
+ free: toNumber$1(entry.free_remaining_amount) ?? total - purchased
930
935
  };
931
936
  }
932
937
  }
933
- const flat = toNumber(record.opcredit_balance) ?? toNumber(record.total_remains_credit);
938
+ const flat = toNumber$1(record.opcredit_balance) ?? toNumber$1(record.total_remains_credit);
934
939
  return flat === void 0 ? void 0 : {
935
940
  total: flat,
936
941
  purchased: 0,
@@ -938,7 +943,7 @@ function parseCreditSummary(document) {
938
943
  };
939
944
  }
940
945
  /** Read a numeric field the upstream may send as a string. */
941
- function toNumber(value) {
946
+ function toNumber$1(value) {
942
947
  if (typeof value === "number" && Number.isFinite(value)) return value;
943
948
  if (typeof value === "string" && value.trim() !== "") {
944
949
  const parsed = Number(value);
@@ -976,17 +981,17 @@ function parseCreditLots(document) {
976
981
  for (const entry of details) {
977
982
  if (typeof entry !== "object" || entry === null) continue;
978
983
  const record = entry;
979
- const expireAtMs = toNumber(record.expire_at_ms);
984
+ const expireAtMs = toNumber$1(record.expire_at_ms);
980
985
  if (expireAtMs === void 0) continue;
981
- const granted = toNumber(record.granted_amount) ?? 0;
982
- const remaining = toNumber(record.remaining_amount) ?? 0;
986
+ const granted = toNumber$1(record.granted_amount) ?? 0;
987
+ const remaining = toNumber$1(record.remaining_amount) ?? 0;
983
988
  lots.push({
984
989
  granted,
985
990
  remaining,
986
- consumed: toNumber(record.consumed_amount) ?? Math.max(0, granted - remaining),
987
- grantedAtMs: toNumber(record.granted_at_ms) ?? 0,
991
+ consumed: toNumber$1(record.consumed_amount) ?? Math.max(0, granted - remaining),
992
+ grantedAtMs: toNumber$1(record.granted_at_ms) ?? 0,
988
993
  expireAtMs,
989
- creditType: toNumber(record.credit_type)
994
+ creditType: toNumber$1(record.credit_type)
990
995
  });
991
996
  }
992
997
  return lots.sort((a, b) => a.expireAtMs - b.expireAtMs);
@@ -1013,7 +1018,7 @@ async function fetchCreditLots(credential, region, fetchImpl = fetch) {
1013
1018
  const batch = parseCreditLots(document);
1014
1019
  const before = lots.length;
1015
1020
  lots.push(...batch);
1016
- const count = toNumber(document.total_count);
1021
+ const count = toNumber$1(document.total_count);
1017
1022
  if (count !== void 0) total = count;
1018
1023
  if (batch.length < pageSize) break;
1019
1024
  if (lots.length === before) break;
@@ -1132,6 +1137,581 @@ var MinimaxCredits = class {
1132
1137
  }
1133
1138
  };
1134
1139
  //#endregion
1140
+ //#region src/credentials.ts
1141
+ /**
1142
+ * The plugin's own credential and state.
1143
+ *
1144
+ * The desktop app signs in with the OAuth 2.0 device authorization grant
1145
+ * (RFC 8628): the app asks the authorization server for a device code, opens
1146
+ * the account page in the browser, and polls the token endpoint until the
1147
+ * person signed in. The plugin can run the same grant itself, which means the
1148
+ * plugin works on a machine where the desktop app was never installed, and
1149
+ * the only human step is the person signing in to their own account in their
1150
+ * own browser.
1151
+ *
1152
+ * What this module stores is deliberately *not* written into the desktop
1153
+ * app's files. The app keeps two files whose `generation` counters must stay
1154
+ * in step, and it treats a mismatch as "signed in somewhere else" and demands
1155
+ * a new sign-in; the plugin has no business writing there. So the plugin keeps
1156
+ * its own copy under the Harness home, next to the files the sibling desktop
1157
+ * connectors keep (`~/.dsh/.minimaxcode-auth.json`).
1158
+ *
1159
+ * @module dsh-connect-minimaxcode/credentials
1160
+ */
1161
+ /** The file the plugin's own credential lives in. */
1162
+ const PLUGIN_CREDENTIAL_FILENAME = ".minimaxcode-auth.json";
1163
+ /** The file the plugin's own settings and session live in. */
1164
+ const PLUGIN_STATE_FILENAME = ".minimaxcode-state.json";
1165
+ /**
1166
+ * The Harness home.
1167
+ *
1168
+ * The host resolves it the same way: the `DSH_HOME` environment variable when
1169
+ * it is set, otherwise `~/.dsh`. The plugin resolves it on its own rather
1170
+ * than importing the host's helper, so it also works in a plain command line
1171
+ * where the host's packages are not installed.
1172
+ */
1173
+ function dshHome(env = process.env) {
1174
+ const override = env.DSH_HOME;
1175
+ return typeof override === "string" && override.length > 0 ? override : join(homedir(), ".dsh");
1176
+ }
1177
+ /** Where the plugin's credential lives. */
1178
+ function pluginCredentialPath(env = process.env) {
1179
+ return join(dshHome(env), PLUGIN_CREDENTIAL_FILENAME);
1180
+ }
1181
+ /** Where the plugin's state lives. */
1182
+ function pluginStatePath(env = process.env) {
1183
+ return join(dshHome(env), PLUGIN_STATE_FILENAME);
1184
+ }
1185
+ /** Write a document atomically, readable by its owner only. */
1186
+ async function writePrivate(path, payload) {
1187
+ await mkdir(dirname(path), { recursive: true });
1188
+ const temporary = `${path}.tmp`;
1189
+ await writeFile(temporary, `${JSON.stringify(payload, null, 2)}\n`, { mode: 384 });
1190
+ await rename(temporary, path);
1191
+ }
1192
+ /** Read the plugin's own credential. */
1193
+ async function readPluginCredential(env = process.env) {
1194
+ let text;
1195
+ try {
1196
+ text = await readFile(pluginCredentialPath(env), "utf8");
1197
+ } catch {
1198
+ return;
1199
+ }
1200
+ let document;
1201
+ try {
1202
+ document = JSON.parse(text);
1203
+ } catch {
1204
+ return;
1205
+ }
1206
+ if (typeof document !== "object" || document === null) return void 0;
1207
+ const record = document;
1208
+ const accessToken = record.accessToken;
1209
+ if (typeof accessToken !== "string" || accessToken.length === 0) return void 0;
1210
+ const expiresAtMs = typeof record.expiresAtMs === "number" ? record.expiresAtMs : void 0;
1211
+ return {
1212
+ schemaVersion: typeof record.schemaVersion === "number" ? record.schemaVersion : 1,
1213
+ clientId: typeof record.clientId === "string" ? record.clientId : MINIMAX_OAUTH_CLIENT_ID,
1214
+ scopes: Array.isArray(record.scopes) ? record.scopes.filter((s) => typeof s === "string") : [...MINIMAX_OAUTH_SCOPES],
1215
+ audience: typeof record.audience === "string" ? record.audience : MINIMAX_OAUTH_AUDIENCE,
1216
+ accessToken,
1217
+ refreshToken: typeof record.refreshToken === "string" ? record.refreshToken : void 0,
1218
+ expiresAtMs,
1219
+ obtainedAtMs: typeof record.obtainedAtMs === "number" ? record.obtainedAtMs : void 0
1220
+ };
1221
+ }
1222
+ /** Write the plugin's own credential. */
1223
+ async function writePluginCredential(credential, env = process.env) {
1224
+ await writePrivate(pluginCredentialPath(env), credential);
1225
+ }
1226
+ /** Read the plugin's own state. */
1227
+ async function readPluginState(env = process.env) {
1228
+ let text;
1229
+ try {
1230
+ text = await readFile(pluginStatePath(env), "utf8");
1231
+ } catch {
1232
+ return {
1233
+ schemaVersion: 1,
1234
+ autoCheckin: false
1235
+ };
1236
+ }
1237
+ let document;
1238
+ try {
1239
+ document = JSON.parse(text);
1240
+ } catch {
1241
+ return {
1242
+ schemaVersion: 1,
1243
+ autoCheckin: false
1244
+ };
1245
+ }
1246
+ if (typeof document !== "object" || document === null) return {
1247
+ schemaVersion: 1,
1248
+ autoCheckin: false
1249
+ };
1250
+ const record = document;
1251
+ const session = record.deviceLogin;
1252
+ const deviceLogin = typeof session === "object" && session !== null ? (() => {
1253
+ const value = session;
1254
+ if (typeof value.deviceCode !== "string" || typeof value.codeVerifier !== "string") return void 0;
1255
+ return {
1256
+ deviceCode: value.deviceCode,
1257
+ codeVerifier: value.codeVerifier,
1258
+ userCode: typeof value.userCode === "string" ? value.userCode : "",
1259
+ verificationUri: typeof value.verificationUri === "string" ? value.verificationUri : "",
1260
+ verificationUriComplete: typeof value.verificationUriComplete === "string" ? value.verificationUriComplete : "",
1261
+ expiresAtMs: typeof value.expiresAtMs === "number" ? value.expiresAtMs : 0
1262
+ };
1263
+ })() : void 0;
1264
+ return {
1265
+ schemaVersion: typeof record.schemaVersion === "number" ? record.schemaVersion : 1,
1266
+ autoCheckin: record.autoCheckin === true,
1267
+ lastClaimDay: typeof record.lastClaimDay === "string" ? record.lastClaimDay : void 0,
1268
+ lastClaimAtMs: typeof record.lastClaimAtMs === "number" ? record.lastClaimAtMs : void 0,
1269
+ lastClaimPoints: typeof record.lastClaimPoints === "number" ? record.lastClaimPoints : void 0,
1270
+ deviceLogin
1271
+ };
1272
+ }
1273
+ /** Write the plugin's own state. */
1274
+ async function writePluginState(state, env = process.env) {
1275
+ await writePrivate(pluginStatePath(env), state);
1276
+ }
1277
+ /** Whether the credential is close enough to expiry to be worth refreshing. */
1278
+ function credentialNeedsRefresh(credential, now = Date.now()) {
1279
+ if (credential.expiresAtMs === void 0) return false;
1280
+ return credential.expiresAtMs - now <= 12e4;
1281
+ }
1282
+ /** The credential in the shape the account endpoints expect. */
1283
+ function toOAuthCredential(credential) {
1284
+ return {
1285
+ token: credential.accessToken,
1286
+ refreshToken: credential.refreshToken,
1287
+ expiresAtMs: credential.expiresAtMs,
1288
+ clientId: credential.clientId,
1289
+ scopes: credential.scopes,
1290
+ audience: credential.audience
1291
+ };
1292
+ }
1293
+ //#endregion
1294
+ //#region src/login.ts
1295
+ /**
1296
+ * Signing in without the desktop app.
1297
+ *
1298
+ * The desktop app's only sign-in is the OAuth 2.0 device authorization grant
1299
+ * (RFC 8628). The plugin runs the same grant:
1300
+ *
1301
+ * 1. `POST /oauth2/device/code` asks for a device code. The answer carries a
1302
+ * short `user_code` and the page where the person authorizes it.
1303
+ * 2. The person opens that page, signs in to their own MiniMax account, and
1304
+ * approves the request.
1305
+ * 3. `POST /oauth2/token` is polled with the device code until the grant
1306
+ * arrives.
1307
+ *
1308
+ * The plugin never sees a password and never sees a verification code: the
1309
+ * one human step happens in the person's own browser, on the account site.
1310
+ * PKCE is used, so a device code that leaks cannot be redeemed without the
1311
+ * verifier that never left this process.
1312
+ *
1313
+ * The token this produces is the same `mmoat_` token the desktop app holds.
1314
+ * The account endpoints need it, and the model gateway accepts it too — so
1315
+ * one sign-in is enough for models, credits and the daily grant.
1316
+ *
1317
+ * @module dsh-connect-minimaxcode/login
1318
+ */
1319
+ /** The endpoint that issues device codes. */
1320
+ const DEVICE_CODE_PATH = "/oauth2/device/code";
1321
+ /** The endpoint that exchanges a device code for a token. */
1322
+ const TOKEN_PATH = "/oauth2/token";
1323
+ /** The device authorization grant type. */
1324
+ const DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
1325
+ /**
1326
+ * Make a PKCE verifier and its S256 challenge.
1327
+ *
1328
+ * The verifier is 43 characters of base64url entropy, which is what RFC 7636
1329
+ * asks for, and it never leaves this process.
1330
+ */
1331
+ function makePkce() {
1332
+ const verifier = randomBytes(32).toString("base64url");
1333
+ return {
1334
+ verifier,
1335
+ challenge: createHash("sha256").update(verifier).digest("base64url")
1336
+ };
1337
+ }
1338
+ /**
1339
+ * Ask for a device code.
1340
+ *
1341
+ * @returns The session the person authorizes, or `undefined` when the server
1342
+ * would not issue one.
1343
+ */
1344
+ async function startDeviceLogin(region, fetchImpl = fetch, now = Date.now()) {
1345
+ const { verifier, challenge } = makePkce();
1346
+ const response = await fetchImpl(`${MINIMAX_AUTH_HOSTS[region]}${DEVICE_CODE_PATH}`, {
1347
+ method: "POST",
1348
+ headers: {
1349
+ accept: "application/json",
1350
+ "content-type": "application/x-www-form-urlencoded"
1351
+ },
1352
+ body: new URLSearchParams({
1353
+ client_id: MINIMAX_OAUTH_CLIENT_ID,
1354
+ scope: MINIMAX_OAUTH_SCOPES.join(" "),
1355
+ audience: MINIMAX_OAUTH_AUDIENCE,
1356
+ code_challenge: challenge,
1357
+ code_challenge_method: "S256"
1358
+ })
1359
+ });
1360
+ if (!response.ok) throw new Error(`申请设备码失败: HTTP ${response.status}`);
1361
+ const document = await response.json();
1362
+ const deviceCode = document.device_code;
1363
+ const userCode = document.user_code;
1364
+ const verificationUri = document.verification_uri;
1365
+ if (typeof deviceCode !== "string" || deviceCode.length === 0) throw new Error("设备码响应里没有 device_code");
1366
+ const expiresInSec = typeof document.expires_in === "number" ? document.expires_in : 300;
1367
+ const intervalSec = typeof document.interval === "number" ? document.interval : 3;
1368
+ return {
1369
+ deviceCode,
1370
+ codeVerifier: verifier,
1371
+ userCode: typeof userCode === "string" ? userCode : "",
1372
+ verificationUri: typeof verificationUri === "string" ? verificationUri : "",
1373
+ verificationUriComplete: typeof document.verification_uri_complete === "string" ? document.verification_uri_complete : "",
1374
+ expiresAtMs: now + expiresInSec * 1e3,
1375
+ intervalSec
1376
+ };
1377
+ }
1378
+ /** Read the token document the authorization server answers with. */
1379
+ function readToken(document) {
1380
+ if (typeof document !== "object" || document === null) return void 0;
1381
+ const record = document;
1382
+ const accessToken = record.access_token;
1383
+ if (typeof accessToken !== "string" || accessToken.length === 0) return void 0;
1384
+ return {
1385
+ token: accessToken,
1386
+ refreshToken: typeof record.refresh_token === "string" ? record.refresh_token : void 0,
1387
+ expiresInSec: typeof record.expires_in === "number" ? record.expires_in : void 0,
1388
+ scope: typeof record.scope === "string" ? record.scope : void 0
1389
+ };
1390
+ }
1391
+ /**
1392
+ * Poll for the grant.
1393
+ *
1394
+ * The authorization server answers `400` while the person has not authorized
1395
+ * yet, with `authorization_pending` in the body; that is a normal answer, not
1396
+ * an error.
1397
+ */
1398
+ async function pollDeviceLogin(session, region, fetchImpl = fetch, now = Date.now()) {
1399
+ if (session.expiresAtMs !== 0 && session.expiresAtMs <= now) return {
1400
+ state: "expired",
1401
+ message: "设备码已过期,请重新发起登录"
1402
+ };
1403
+ const response = await fetchImpl(`${MINIMAX_AUTH_HOSTS[region]}${TOKEN_PATH}`, {
1404
+ method: "POST",
1405
+ headers: {
1406
+ accept: "application/json",
1407
+ "content-type": "application/x-www-form-urlencoded"
1408
+ },
1409
+ body: new URLSearchParams({
1410
+ client_id: MINIMAX_OAUTH_CLIENT_ID,
1411
+ grant_type: DEVICE_CODE_GRANT,
1412
+ device_code: session.deviceCode,
1413
+ code_verifier: session.codeVerifier
1414
+ })
1415
+ });
1416
+ const text = await response.text();
1417
+ let document;
1418
+ try {
1419
+ document = JSON.parse(text);
1420
+ } catch {
1421
+ document = void 0;
1422
+ }
1423
+ if (response.ok) {
1424
+ const token = readToken(document);
1425
+ if (token === void 0) return {
1426
+ state: "expired",
1427
+ message: "登录响应里没有访问令牌"
1428
+ };
1429
+ return {
1430
+ state: "signed-in",
1431
+ credential: {
1432
+ schemaVersion: 1,
1433
+ clientId: MINIMAX_OAUTH_CLIENT_ID,
1434
+ scopes: [...MINIMAX_OAUTH_SCOPES],
1435
+ audience: MINIMAX_OAUTH_AUDIENCE,
1436
+ accessToken: token.token,
1437
+ refreshToken: token.refreshToken,
1438
+ expiresAtMs: token.expiresInSec === void 0 ? void 0 : now + token.expiresInSec * 1e3,
1439
+ obtainedAtMs: now
1440
+ }
1441
+ };
1442
+ }
1443
+ const error = typeof document === "object" && document !== null ? document.error : void 0;
1444
+ const description = typeof document === "object" && document !== null ? document.error_description : void 0;
1445
+ const message = typeof description === "string" ? description : void 0;
1446
+ if (error === "authorization_pending") return { state: "pending" };
1447
+ if (error === "slow_down") return { state: "slow-down" };
1448
+ if (error === "expired_token") return {
1449
+ state: "expired",
1450
+ message
1451
+ };
1452
+ if (error === "access_denied") return {
1453
+ state: "denied",
1454
+ message
1455
+ };
1456
+ return {
1457
+ state: "expired",
1458
+ message: message ?? `登录失败: HTTP ${response.status}`
1459
+ };
1460
+ }
1461
+ /**
1462
+ * Exchange the stored refresh token for a fresh access token.
1463
+ *
1464
+ * The authorization server rotates the refresh token, so the new pair
1465
+ * replaces the old one wherever it is kept.
1466
+ */
1467
+ async function refreshPluginCredential(credential, region, fetchImpl = fetch, now = Date.now()) {
1468
+ if (credential.refreshToken === void 0) throw new Error("没有可用于续期的 refresh token,请重新登录");
1469
+ const response = await fetchImpl(`${MINIMAX_AUTH_HOSTS[region]}${TOKEN_PATH}`, {
1470
+ method: "POST",
1471
+ headers: {
1472
+ accept: "application/json",
1473
+ "content-type": "application/x-www-form-urlencoded"
1474
+ },
1475
+ body: new URLSearchParams({
1476
+ grant_type: "refresh_token",
1477
+ refresh_token: credential.refreshToken,
1478
+ client_id: credential.clientId,
1479
+ scope: credential.scopes.join(" "),
1480
+ audience: credential.audience
1481
+ })
1482
+ });
1483
+ if (!response.ok) throw new Error(`续期失败: HTTP ${response.status}`);
1484
+ const token = readToken(await response.json());
1485
+ if (token === void 0) throw new Error("续期响应里没有访问令牌");
1486
+ return {
1487
+ ...credential,
1488
+ accessToken: token.token,
1489
+ refreshToken: token.refreshToken ?? credential.refreshToken,
1490
+ expiresAtMs: token.expiresInSec === void 0 ? void 0 : now + token.expiresInSec * 1e3,
1491
+ obtainedAtMs: now
1492
+ };
1493
+ }
1494
+ //#endregion
1495
+ //#region src/signin.ts
1496
+ /**
1497
+ * The daily check-in that grants the free credits.
1498
+ *
1499
+ * MiniMax Code hands out a grant of points every day, and the desktop app
1500
+ * shows it as a "每日签到" card: a seven-day cycle worth 4000 points, each
1501
+ * grant valid for thirty days. The app does **not** claim on its own — the
1502
+ * card opens, and a button does the work — so an account that is only used
1503
+ * through the model gateway leaves those points unclaimed.
1504
+ *
1505
+ * The endpoints are the same ones the card calls:
1506
+ *
1507
+ * - `GET /minimax-cloud/api/v1/signin/status` — the cycle, with today's day
1508
+ * marked
1509
+ * - `POST /minimax-cloud/api/v1/signin/claim` — claim today
1510
+ *
1511
+ * Both need the account's `mmoat_` bearer token, and both need a **time zone
1512
+ ***: the server answers `invalid timezone_id` when the request carries an
1513
+ * offset or nothing at all, and it is that time zone that decides which day
1514
+ * "today" is. The client sends an IANA name (`Asia/Shanghai`), so this does
1515
+ * too.
1516
+ *
1517
+ * The claim is the one request here that changes something upstream. It is
1518
+ * idempotent per day — claiming again answers "already claimed" and grants
1519
+ * nothing — but the caller still guards it, because "cannot double-grant" is
1520
+ * a property of the upstream that is verified rather than assumed.
1521
+ *
1522
+ * @module dsh-connect-minimaxcode/signin
1523
+ */
1524
+ /** The claim endpoint. */
1525
+ const SIGNIN_CLAIM_PATH = "/minimax-cloud/api/v1/signin/claim";
1526
+ /** The cycle endpoint, which reports the panel. */
1527
+ const SIGNIN_PANEL_PATH = "/minimax-cloud/api/v1/signin/status";
1528
+ /** Per-day status, as the desktop app enumerates it. */
1529
+ const SIGNIN_DAY_STATUS = {
1530
+ /** The day has not come yet. */
1531
+ upcoming: 1,
1532
+ /** The day is today and can be claimed. */
1533
+ claimable: 2,
1534
+ /** The day is claimed. */
1535
+ claimed: 3,
1536
+ /** The day does not grant anything. */
1537
+ disabled: 4
1538
+ };
1539
+ /** How many days a cycle runs. */
1540
+ const SIGNIN_CYCLE_DAYS = 7;
1541
+ /** The outcome of a claim. */
1542
+ const SIGNIN_CLAIM_RESULT = {
1543
+ /** The grant was credited. */
1544
+ credited: 1,
1545
+ /** Today was already claimed; nothing was granted. */
1546
+ alreadyClaimed: 2
1547
+ };
1548
+ /**
1549
+ * The time zone the server should treat the request as coming from.
1550
+ *
1551
+ * The server wants an IANA name; an offset or a missing value is refused.
1552
+ * The system's own name is the right answer, since "today" should be the
1553
+ * user's today.
1554
+ */
1555
+ function timezoneId() {
1556
+ try {
1557
+ const resolved = Intl.DateTimeFormat().resolvedOptions().timeZone;
1558
+ return typeof resolved === "string" && resolved.length > 0 ? resolved : "UTC";
1559
+ } catch {
1560
+ return "UTC";
1561
+ }
1562
+ }
1563
+ /** Read a number the upstream may send as a string. */
1564
+ function toNumber(value) {
1565
+ if (typeof value === "number" && Number.isFinite(value)) return value;
1566
+ if (typeof value === "string" && value.trim() !== "") {
1567
+ const parsed = Number(value);
1568
+ return Number.isFinite(parsed) ? parsed : void 0;
1569
+ }
1570
+ }
1571
+ /**
1572
+ * Unwrap the upstream's envelope.
1573
+ *
1574
+ * The account APIs answer `200` with a `base_resp` that carries the real
1575
+ * status, so the HTTP code alone says nothing.
1576
+ */
1577
+ function unwrap(document) {
1578
+ if (typeof document !== "object" || document === null) return {
1579
+ ok: false,
1580
+ message: "签到接口返回了无法解析的响应"
1581
+ };
1582
+ const record = document;
1583
+ const base = record.base_resp;
1584
+ if (typeof base === "object" && base !== null) {
1585
+ const status = toNumber(base.status_code);
1586
+ if (status !== void 0 && status !== 0) {
1587
+ const message = base.status_msg;
1588
+ return {
1589
+ ok: false,
1590
+ message: typeof message === "string" ? message : `签到接口返回状态码 ${status}`
1591
+ };
1592
+ }
1593
+ }
1594
+ return {
1595
+ ok: true,
1596
+ data: record.data
1597
+ };
1598
+ }
1599
+ /**
1600
+ * Read the cycle out of a panel response.
1601
+ *
1602
+ * The server marks exactly one day as today and at most one day as claimable;
1603
+ * a panel that says otherwise is not something to act on, so it is refused
1604
+ * rather than half-believed.
1605
+ */
1606
+ function parseSigninPanel(document) {
1607
+ const unwrapped = unwrap(document);
1608
+ if (!unwrapped.ok) return void 0;
1609
+ const data = unwrapped.data;
1610
+ if (typeof data !== "object" || data === null) return void 0;
1611
+ const record = data;
1612
+ const scene = toNumber(record.scene);
1613
+ const rawDays = record.days;
1614
+ if (scene === void 0 || !Array.isArray(rawDays)) return void 0;
1615
+ const days = [];
1616
+ let claimable = 0;
1617
+ let today = 0;
1618
+ for (const entry of rawDays) {
1619
+ if (typeof entry !== "object" || entry === null) continue;
1620
+ const day = entry;
1621
+ const dayNo = toNumber(day.day_no);
1622
+ const points = toNumber(day.points);
1623
+ const status = toNumber(day.status);
1624
+ if (dayNo === void 0 || points === void 0 || status === void 0) continue;
1625
+ if (status < 1 || status > 4) continue;
1626
+ const isToday = day.is_today === true;
1627
+ if (status === SIGNIN_DAY_STATUS.claimable) claimable += 1;
1628
+ if (isToday) today += 1;
1629
+ days.push({
1630
+ dayNo,
1631
+ points,
1632
+ bonusPoints: toNumber(day.bonus_points) ?? 0,
1633
+ status,
1634
+ isToday
1635
+ });
1636
+ }
1637
+ if (days.length === 0) return void 0;
1638
+ if (claimable > 1 || today > 1) return void 0;
1639
+ return {
1640
+ scene,
1641
+ days
1642
+ };
1643
+ }
1644
+ /** Read the result of a claim. */
1645
+ function parseSigninClaim(document) {
1646
+ const unwrapped = unwrap(document);
1647
+ if (!unwrapped.ok) return void 0;
1648
+ const data = unwrapped.data;
1649
+ if (typeof data !== "object" || data === null) return void 0;
1650
+ const record = data;
1651
+ const claimId = record.claim_id;
1652
+ const result = toNumber(record.claim_result);
1653
+ const dayNo = toNumber(record.day_no);
1654
+ const points = toNumber(record.points);
1655
+ const expireAtMs = toNumber(record.expire_at_ms);
1656
+ if (typeof claimId !== "string" || claimId.length === 0) return void 0;
1657
+ if (result !== SIGNIN_CLAIM_RESULT.credited && result !== SIGNIN_CLAIM_RESULT.alreadyClaimed) return void 0;
1658
+ if (dayNo === void 0 || dayNo < 1 || dayNo > 7) return void 0;
1659
+ if (points === void 0 || points < 0) return void 0;
1660
+ if (expireAtMs === void 0) return void 0;
1661
+ return {
1662
+ claimId,
1663
+ result,
1664
+ dayNo,
1665
+ points,
1666
+ bonusPoints: toNumber(record.bonus_points) ?? 0,
1667
+ expireAtMs,
1668
+ panel: parseSigninPanel({ data: record.panel })
1669
+ };
1670
+ }
1671
+ /**
1672
+ * Read the cycle.
1673
+ *
1674
+ * @returns The panel, or `undefined` when the account cannot read it.
1675
+ */
1676
+ async function fetchSigninPanel(credential, region, fetchImpl = fetch, timezone = timezoneId()) {
1677
+ const response = await fetchImpl(`${MINIMAX_ACCOUNT_HOSTS[region]}${SIGNIN_PANEL_PATH}?timezone_id=${encodeURIComponent(timezone)}`, { headers: { authorization: `Bearer ${credential.token}` } });
1678
+ if (!response.ok) throw new Error(`签到状态请求失败: HTTP ${response.status}`);
1679
+ return parseSigninPanel(await response.json());
1680
+ }
1681
+ /**
1682
+ * Claim today's grant.
1683
+ *
1684
+ * This is the only request in the plugin that changes something upstream. It
1685
+ * grants the day's points, which expire thirty days later.
1686
+ *
1687
+ * @returns What the server answered, or `undefined` when it refused.
1688
+ */
1689
+ async function claimSignin(credential, region, fetchImpl = fetch, timezone = timezoneId()) {
1690
+ const response = await fetchImpl(`${MINIMAX_ACCOUNT_HOSTS[region]}${SIGNIN_CLAIM_PATH}?timezone_id=${encodeURIComponent(timezone)}`, {
1691
+ method: "POST",
1692
+ headers: {
1693
+ authorization: `Bearer ${credential.token}`,
1694
+ "content-type": "application/json"
1695
+ },
1696
+ body: JSON.stringify({})
1697
+ });
1698
+ if (!response.ok) throw new Error(`签到请求失败: HTTP ${response.status}`);
1699
+ return parseSigninClaim(await response.json());
1700
+ }
1701
+ /** The day's key in the account's own time zone, `YYYY-MM-DD`. */
1702
+ function localDay(now = Date.now(), timezone = timezoneId()) {
1703
+ try {
1704
+ return new Intl.DateTimeFormat("en-CA", {
1705
+ timeZone: timezone,
1706
+ year: "numeric",
1707
+ month: "2-digit",
1708
+ day: "2-digit"
1709
+ }).format(new Date(now));
1710
+ } catch {
1711
+ return new Date(now).toISOString().slice(0, 10);
1712
+ }
1713
+ }
1714
+ //#endregion
1135
1715
  //#region src/usage.ts
1136
1716
  /** An empty usage record. */
1137
1717
  const EMPTY_USAGE = {
@@ -1268,6 +1848,13 @@ const CATALOG_TTL_MS = 3e5;
1268
1848
  */
1269
1849
  const CREDIT_TTL_MS = 3e4;
1270
1850
  /**
1851
+ * How often the check-in cycle may be re-read.
1852
+ *
1853
+ * The cycle changes once a day, so reading it as often as the balance is
1854
+ * read would buy nothing but load on the account API.
1855
+ */
1856
+ const SIGNIN_TTL_MS = 3e5;
1857
+ /**
1271
1858
  * Compose the plugin.
1272
1859
  *
1273
1860
  * The credential is re-read on every request rather than cached at start: the
@@ -1281,20 +1868,106 @@ function apply(ctx) {
1281
1868
  const credits = new MinimaxCredits();
1282
1869
  const meter = new UsageMeter();
1283
1870
  let region = "cn";
1284
- let credential;
1285
1871
  let lastCatalogAtMs = 0;
1286
1872
  let lastCreditAtMs = 0;
1873
+ let lastSigninAtMs = 0;
1287
1874
  let refreshing = false;
1288
1875
  let creditState = "unavailable";
1289
- const readNow = async () => {
1876
+ /** The token the model descriptors carry, refreshed with the credential. */
1877
+ let modelToken;
1878
+ /** The plugin's own settings, read from its own file. */
1879
+ let pluginState = {
1880
+ schemaVersion: 1,
1881
+ autoCheckin: false
1882
+ };
1883
+ /** The cycle as the account last reported it. */
1884
+ let signinPanel;
1885
+ /** What the last claim answered. */
1886
+ let signinClaim;
1887
+ /** Why the cycle or the claim could not be read. */
1888
+ let signinError;
1889
+ /** Where the sign-in came from. */
1890
+ let loginSource;
1891
+ /** Whether a device login is waiting to be authorized. */
1892
+ let loginPending = false;
1893
+ /**
1894
+ * The credential the model gateway is called with.
1895
+ *
1896
+ * The plugin's own sign-in comes first: it is the credential the person
1897
+ * chose, and on a machine where the desktop app was never installed it is
1898
+ * the only one there is. The desktop app's token is the fallback, and the
1899
+ * two are interchangeable as far as the gateway is concerned — it accepts
1900
+ * either bearer.
1901
+ */
1902
+ const resolveGatewayCredential = async () => {
1903
+ const own = await readPluginCredential();
1904
+ if (own !== void 0) {
1905
+ if (credentialNeedsRefresh(own)) try {
1906
+ const next = await refreshPluginCredential(own, region);
1907
+ await writePluginCredential(next);
1908
+ loginSource = "plugin";
1909
+ modelToken = next.accessToken;
1910
+ return {
1911
+ token: next.accessToken,
1912
+ claims: { exp: Math.floor((next.expiresAtMs ?? Date.now()) / 1e3) }
1913
+ };
1914
+ } catch {}
1915
+ else {
1916
+ loginSource = "plugin";
1917
+ modelToken = own.accessToken;
1918
+ return {
1919
+ token: own.accessToken,
1920
+ claims: { exp: Math.floor((own.expiresAtMs ?? Date.now()) / 1e3) }
1921
+ };
1922
+ }
1923
+ }
1290
1924
  const state = await readCredential();
1291
1925
  if (state.state !== "signed-in") {
1292
- credential = void 0;
1926
+ loginSource = void 0;
1927
+ modelToken = void 0;
1928
+ return;
1929
+ }
1930
+ loginSource = "desktop-app";
1931
+ modelToken = state.credential.token;
1932
+ return state.credential;
1933
+ };
1934
+ /**
1935
+ * The credential the account endpoints are called with.
1936
+ *
1937
+ * The account API needs the `mmoat_` token, not the gateway's JWT, so this
1938
+ * is a second credential: the plugin's own sign-in when there is one, and
1939
+ * otherwise the token the desktop app stored.
1940
+ */
1941
+ const resolveAccountCredential = async (now = Date.now()) => {
1942
+ const own = await readPluginCredential();
1943
+ if (own !== void 0) {
1944
+ if (credentialNeedsRefresh(own, now)) try {
1945
+ const next = await refreshPluginCredential(own, region, fetch, now);
1946
+ await writePluginCredential(next);
1947
+ return {
1948
+ state: "signed-in",
1949
+ credential: toOAuthCredential(next)
1950
+ };
1951
+ } catch {
1952
+ return {
1953
+ state: "expired",
1954
+ credential: toOAuthCredential(own)
1955
+ };
1956
+ }
1957
+ return {
1958
+ state: "signed-in",
1959
+ credential: toOAuthCredential(own)
1960
+ };
1961
+ }
1962
+ return ensureOAuthCredential(region, process.env, now);
1963
+ };
1964
+ const readNow = async () => {
1965
+ const state = await resolveGatewayCredential();
1966
+ if (state === void 0) {
1293
1967
  catalog.invalidate();
1294
1968
  return;
1295
1969
  }
1296
- credential = state.credential;
1297
- return credential;
1970
+ return state;
1298
1971
  };
1299
1972
  /** Refresh the catalog at most once per TTL, never blocking a request. */
1300
1973
  const refreshCatalog = async (current) => {
@@ -1319,7 +1992,7 @@ function apply(ctx) {
1319
1992
  const now = Date.now();
1320
1993
  if (now - lastCreditAtMs < CREDIT_TTL_MS) return;
1321
1994
  lastCreditAtMs = now;
1322
- const state = await ensureOAuthCredential(region, process.env, now);
1995
+ const state = await resolveAccountCredential(now);
1323
1996
  if (state.state === "signed-out") {
1324
1997
  creditState = "signed-out";
1325
1998
  return;
@@ -1331,6 +2004,65 @@ function apply(ctx) {
1331
2004
  await credits.refresh(state.credential, region, fetch, now);
1332
2005
  creditState = credits.snapshot === void 0 ? "unavailable" : "ok";
1333
2006
  };
2007
+ /**
2008
+ * Read the check-in cycle, and claim the day's grant when asked to.
2009
+ *
2010
+ * The claim is the one request here that changes something upstream. It is
2011
+ * made at most once a day, only when the person turned it on, and only when
2012
+ * the server itself says the day can be claimed — so a day that is already
2013
+ * claimed never produces a request, and a claim that already happened
2014
+ * upstream is answered with "already claimed" and grants nothing.
2015
+ */
2016
+ const refreshSignin = async () => {
2017
+ const now = Date.now();
2018
+ if (now - lastSigninAtMs < SIGNIN_TTL_MS && signinPanel !== void 0) return;
2019
+ lastSigninAtMs = now;
2020
+ const state = await resolveAccountCredential();
2021
+ if (state.state !== "signed-in") {
2022
+ signinPanel = void 0;
2023
+ return;
2024
+ }
2025
+ let panel;
2026
+ try {
2027
+ panel = await fetchSigninPanel(state.credential, region, fetch);
2028
+ signinPanel = panel;
2029
+ signinError = void 0;
2030
+ } catch (error) {
2031
+ signinError = error instanceof Error ? error.message : String(error);
2032
+ return;
2033
+ }
2034
+ if (!pluginState.autoCheckin || panel === void 0) return;
2035
+ const today = panel.days.find((day) => day.isToday);
2036
+ if (today === void 0 || today.status !== SIGNIN_DAY_STATUS.claimable) return;
2037
+ if (pluginState.lastClaimDay === localDay()) return;
2038
+ await performClaim(state.credential);
2039
+ };
2040
+ /** Claim today's grant and remember that it happened. */
2041
+ const performClaim = async (credential) => {
2042
+ try {
2043
+ const claim = await claimSignin(credential, region, fetch);
2044
+ if (claim === void 0) {
2045
+ signinError = "签到接口没有返回可识别的结果";
2046
+ return;
2047
+ }
2048
+ signinClaim = claim;
2049
+ signinError = void 0;
2050
+ if (claim.panel !== void 0) signinPanel = claim.panel;
2051
+ pluginState = {
2052
+ ...pluginState,
2053
+ lastClaimDay: localDay(),
2054
+ lastClaimAtMs: Date.now(),
2055
+ lastClaimPoints: claim.points
2056
+ };
2057
+ await writePluginState(pluginState);
2058
+ lastCreditAtMs = 0;
2059
+ refreshCredits();
2060
+ return claim;
2061
+ } catch (error) {
2062
+ signinError = error instanceof Error ? error.message : String(error);
2063
+ return;
2064
+ }
2065
+ };
1334
2066
  /** The credit balance as the card and the browser half read it. */
1335
2067
  const buildCreditStatus = () => {
1336
2068
  const snapshot = credits.snapshot;
@@ -1373,11 +2105,60 @@ function apply(ctx) {
1373
2105
  requests: meter.records.length
1374
2106
  };
1375
2107
  };
2108
+ /** The check-in cycle, as the card reads it. */
2109
+ const buildSigninStatus = () => {
2110
+ if (signinPanel === void 0 && signinError === void 0) return void 0;
2111
+ const panel = signinPanel;
2112
+ const today = panel?.days.find((day) => day.isToday);
2113
+ return {
2114
+ state: panel === void 0 ? "unavailable" : "ok",
2115
+ ...today === void 0 ? {} : { today: {
2116
+ dayNo: today.dayNo,
2117
+ points: today.points,
2118
+ status: today.status,
2119
+ claimable: today.status === SIGNIN_DAY_STATUS.claimable,
2120
+ claimed: today.status === SIGNIN_DAY_STATUS.claimed
2121
+ } },
2122
+ ...panel === void 0 ? {} : {
2123
+ days: panel.days.map((day) => ({
2124
+ dayNo: day.dayNo,
2125
+ points: day.points,
2126
+ status: day.status,
2127
+ isToday: day.isToday
2128
+ })),
2129
+ cyclePoints: panel.days.reduce((sum, day) => sum + day.points, 0)
2130
+ },
2131
+ autoCheckin: pluginState.autoCheckin,
2132
+ ...pluginState.lastClaimDay === void 0 ? {} : { lastClaimDay: pluginState.lastClaimDay },
2133
+ ...pluginState.lastClaimAtMs === void 0 ? {} : { lastClaimAtMs: pluginState.lastClaimAtMs },
2134
+ ...pluginState.lastClaimPoints === void 0 ? {} : { lastClaimPoints: pluginState.lastClaimPoints },
2135
+ ...signinClaim === void 0 ? {} : {
2136
+ lastClaimResult: signinClaim.result,
2137
+ lastClaimPoints: signinClaim.points
2138
+ },
2139
+ ...signinError === void 0 ? {} : { error: signinError }
2140
+ };
2141
+ };
2142
+ /** How the plugin is signed in, as the card reads it. */
2143
+ const buildLoginStatus = () => {
2144
+ const session = pluginState.deviceLogin;
2145
+ const pending = loginPending && session !== void 0;
2146
+ return {
2147
+ state: pending ? "pending" : loginSource === void 0 ? "signed-out" : "signed-in",
2148
+ ...loginSource === void 0 ? {} : { source: loginSource },
2149
+ ...pending && session !== void 0 ? {
2150
+ userCode: session.userCode,
2151
+ verificationUri: session.verificationUri,
2152
+ verificationUriComplete: session.verificationUriComplete,
2153
+ expiresAtMs: session.expiresAtMs
2154
+ } : {}
2155
+ };
2156
+ };
1376
2157
  const { adapter, invalidate } = createMinimaxCodeAdapter({
1377
2158
  catalog,
1378
2159
  region: () => region,
1379
2160
  resolveApiKey: async () => (await readNow())?.token,
1380
- bearerToken: () => bearerToken(),
2161
+ bearerToken: () => modelToken,
1381
2162
  onUsage: (model, usage) => {
1382
2163
  meter.record(model, usage);
1383
2164
  refreshCredits();
@@ -1396,6 +2177,7 @@ function apply(ctx) {
1396
2177
  reason: state.reason
1397
2178
  };
1398
2179
  await refreshCredits();
2180
+ await refreshSignin();
1399
2181
  return statusDocument({
1400
2182
  auth,
1401
2183
  region,
@@ -1408,7 +2190,9 @@ function apply(ctx) {
1408
2190
  })),
1409
2191
  refreshing,
1410
2192
  credits: buildCreditStatus(),
1411
- usage: buildUsageStatus()
2193
+ usage: buildUsageStatus(),
2194
+ signin: buildSigninStatus(),
2195
+ login: buildLoginStatus()
1412
2196
  });
1413
2197
  };
1414
2198
  (async () => {
@@ -1419,6 +2203,121 @@ function apply(ctx) {
1419
2203
  invalidate();
1420
2204
  }
1421
2205
  })();
2206
+ /**
2207
+ * The actions the card can ask for.
2208
+ *
2209
+ * Every one of them is a request the person made by clicking something, so
2210
+ * they are all safe to repeat: a claim that was already made upstream
2211
+ * answers "already claimed", and a sign-in that is already waiting answers
2212
+ * with the same device code.
2213
+ */
2214
+ const runAction = async (action, body) => {
2215
+ switch (action) {
2216
+ case "claim": {
2217
+ const state = await resolveAccountCredential();
2218
+ if (state.state !== "signed-in") return {
2219
+ ok: false,
2220
+ state: state.state,
2221
+ error: "尚未登录"
2222
+ };
2223
+ const claim = await performClaim(state.credential);
2224
+ return claim === void 0 ? {
2225
+ ok: false,
2226
+ error: signinError ?? "签到失败"
2227
+ } : {
2228
+ ok: true,
2229
+ claim,
2230
+ panel: signinPanel
2231
+ };
2232
+ }
2233
+ case "login-start": {
2234
+ const session = await startDeviceLogin(region);
2235
+ if (session === void 0) return {
2236
+ ok: false,
2237
+ error: "没有拿到设备码"
2238
+ };
2239
+ pluginState = {
2240
+ ...pluginState,
2241
+ deviceLogin: session
2242
+ };
2243
+ await writePluginState(pluginState);
2244
+ loginPending = true;
2245
+ return {
2246
+ ok: true,
2247
+ userCode: session.userCode,
2248
+ verificationUri: session.verificationUri,
2249
+ verificationUriComplete: session.verificationUriComplete,
2250
+ expiresAtMs: session.expiresAtMs
2251
+ };
2252
+ }
2253
+ case "login-poll": {
2254
+ const session = pluginState.deviceLogin;
2255
+ if (session === void 0) return {
2256
+ ok: false,
2257
+ state: "signed-out",
2258
+ error: "没有待完成的登录"
2259
+ };
2260
+ const result = await pollDeviceLogin(session, region);
2261
+ if (result.state === "signed-in") {
2262
+ await writePluginCredential(result.credential);
2263
+ pluginState = {
2264
+ ...pluginState,
2265
+ deviceLogin: void 0
2266
+ };
2267
+ await writePluginState(pluginState);
2268
+ loginPending = false;
2269
+ loginSource = "plugin";
2270
+ modelToken = result.credential.accessToken;
2271
+ catalog.invalidate();
2272
+ lastCatalogAtMs = 0;
2273
+ lastCreditAtMs = 0;
2274
+ readNow();
2275
+ return {
2276
+ ok: true,
2277
+ state: "signed-in"
2278
+ };
2279
+ }
2280
+ if (result.state === "expired" || result.state === "denied") {
2281
+ pluginState = {
2282
+ ...pluginState,
2283
+ deviceLogin: void 0
2284
+ };
2285
+ await writePluginState(pluginState);
2286
+ loginPending = false;
2287
+ return {
2288
+ ok: false,
2289
+ state: result.state,
2290
+ error: result.message
2291
+ };
2292
+ }
2293
+ return {
2294
+ ok: true,
2295
+ state: result.state
2296
+ };
2297
+ }
2298
+ case "settings": {
2299
+ const autoCheckin = body.autoCheckin;
2300
+ if (typeof autoCheckin !== "boolean") return {
2301
+ ok: false,
2302
+ error: "autoCheckin 必须是布尔值"
2303
+ };
2304
+ pluginState = {
2305
+ ...pluginState,
2306
+ autoCheckin
2307
+ };
2308
+ await writePluginState(pluginState);
2309
+ if (autoCheckin) refreshSignin();
2310
+ return {
2311
+ ok: true,
2312
+ autoCheckin
2313
+ };
2314
+ }
2315
+ default: return {
2316
+ ok: false,
2317
+ error: `未知操作: ${action}`
2318
+ };
2319
+ }
2320
+ };
1422
2321
  ctx.inject(["webServer"], (webCtx) => {
1423
2322
  webCtx.effect(() => webCtx.webServer.register({
1424
2323
  kind: "exact",
@@ -1438,7 +2337,50 @@ function apply(ctx) {
1438
2337
  }
1439
2338
  }
1440
2339
  }));
2340
+ webCtx.effect(() => webCtx.webServer.register({
2341
+ kind: "exact",
2342
+ path: MINIMAX_ACTION_PATH,
2343
+ handler: async (req, res) => {
2344
+ if (!isLoopback(req.headers.host)) {
2345
+ res.writeHead(403, { "Content-Type": "application/json" });
2346
+ res.end(JSON.stringify({ error: "loopback only" }));
2347
+ return;
2348
+ }
2349
+ if (req.method !== "POST") {
2350
+ res.writeHead(405, { "Content-Type": "application/json" });
2351
+ res.end(JSON.stringify({ error: "POST only" }));
2352
+ return;
2353
+ }
2354
+ let action = "";
2355
+ let body = {};
2356
+ try {
2357
+ action = new URL(req.url ?? "/", "http://127.0.0.1").searchParams.get("action") ?? "";
2358
+ const text = await readRequestBody(req);
2359
+ if (text.length > 0) {
2360
+ const parsed = JSON.parse(text);
2361
+ if (typeof parsed === "object" && parsed !== null) body = parsed;
2362
+ }
2363
+ } catch {}
2364
+ try {
2365
+ const result = await runAction(action, body);
2366
+ res.writeHead(200, { "Content-Type": "application/json" });
2367
+ res.end(JSON.stringify(result));
2368
+ } catch (error) {
2369
+ res.writeHead(500, { "Content-Type": "application/json" });
2370
+ res.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
2371
+ }
2372
+ }
2373
+ }));
1441
2374
  });
1442
2375
  }
2376
+ /** Read a request body, up to a size that no card needs to exceed. */
2377
+ async function readRequestBody(req) {
2378
+ let text = "";
2379
+ for await (const chunk of req) {
2380
+ text += typeof chunk === "string" ? chunk : chunk.toString("utf8");
2381
+ if (text.length > 65536) break;
2382
+ }
2383
+ return text;
2384
+ }
1443
2385
  //#endregion
1444
- export { EMPTY_USAGE, FALLBACK_MODELS, MEMBERSHIP_PATH, MINIMAXCODE_PROVIDER_ID, MINIMAX_AUTH_FILENAME, MINIMAX_DATA_DIR, MINIMAX_GATEWAYS, MINIMAX_OAUTH_CLIENT_ID, MINIMAX_REGIONS, MINIMAX_STATUS_PATH, MINIMAX_STREAM_IDLE_TIMEOUT_MS, MODEL_PRICES, MinimaxCatalog, MinimaxCredits, UsageMeter, apply, authPath, bearerToken, catalogUrl, chatBaseUrl, createMinimaxCodeAdapter, dataDir, daysRemaining, decodeClaims, ensureOAuthCredential, estimateCny, expiresInMs, expiryLevel, fetchCreditSummary, inject, name, parseAuthDocument, parseCatalog, parseCreditSummary, parseOAuthDocument, priceFor, readCredential, readOAuthCredential, refreshOAuthCredential, statusDocument, writeOAuthCredential };
2386
+ export { DEVICE_CODE_GRANT, DEVICE_CODE_PATH, EMPTY_USAGE, FALLBACK_MODELS, MEMBERSHIP_PATH, MINIMAXCODE_PROVIDER_ID, MINIMAX_AUTH_FILENAME, MINIMAX_DATA_DIR, MINIMAX_GATEWAYS, MINIMAX_OAUTH_CLIENT_ID, MINIMAX_REGIONS, MINIMAX_STATUS_PATH, MINIMAX_STREAM_IDLE_TIMEOUT_MS, MODEL_PRICES, MinimaxCatalog, MinimaxCredits, PLUGIN_CREDENTIAL_FILENAME, PLUGIN_STATE_FILENAME, SIGNIN_CLAIM_PATH, SIGNIN_CLAIM_RESULT, SIGNIN_CYCLE_DAYS, SIGNIN_DAY_STATUS, SIGNIN_PANEL_PATH, UsageMeter, apply, authPath, bearerToken, catalogUrl, chatBaseUrl, claimSignin, createMinimaxCodeAdapter, dataDir, daysRemaining, decodeClaims, dshHome, ensureOAuthCredential, estimateCny, expiresInMs, expiryLevel, fetchCreditSummary, fetchSigninPanel, inject, localDay, makePkce, name, parseAuthDocument, parseCatalog, parseCreditSummary, parseOAuthDocument, parseSigninClaim, parseSigninPanel, pluginCredentialPath, pluginStatePath, pollDeviceLogin, priceFor, readCredential, readOAuthCredential, readPluginCredential, readPluginState, refreshOAuthCredential, refreshPluginCredential, startDeviceLogin, statusDocument, timezoneId, writeOAuthCredential, writePluginCredential, writePluginState };