dsh-codex-subscription 1.14.2 → 1.14.3

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
@@ -22,16 +22,6 @@ No OpenAI API key or Codex CLI. Models, search, quota, and image generation stay
22
22
  <img src="https://raw.githubusercontent.com/WSL043/dsh-codex-subscription/main/docs/assets/readme-hero-en.webp" width="900" alt="Your Codex subscription inside DSH: models, web search, quota and safe reset, image generation, and Fast mode">
23
23
  </p>
24
24
 
25
- ## What's included in 1.14.2
26
-
27
- - GPT-6 Astra now supports model-catalog context settings: Standard preserves the catalog default, Extended uses 872K, and Custom is bounded to 128K–872K.
28
-
29
- ### From 1.14.1
30
-
31
- - The composer shows both 5-hour and weekly quota windows when returned by the service, so neither window is hidden.
32
- - Image edits validate attachment references, normalize bare SHA-256 digests, and resolve the actual attachment metadata from the current session. Paths produce an actionable error asking the model to obtain the correct reference with `read_image` and retry.
33
- - Location edits attach the clean source, a numbered annotation image, and coordinate instructions so the model can match each requested change to its location.
34
-
35
25
  ## Three-step start
36
26
 
37
27
  1. **Install the plugin.** Run the standard DSH bundle command:
@@ -50,14 +40,14 @@ DSH-Portable exposes the same standard plugin command, so the command above also
50
40
  | Capability | What you get |
51
41
  | --- | --- |
52
42
  | **Subscription models** | Sign in to ChatGPT and use Codex without an OpenAI API key or Codex CLI |
53
- | **Recoverable and diagnosable** | Sign-in state reconciles automatically; Settings can create a support report without credentials or account identifiers |
43
+ | **Recoverable and diagnosable** | Sign-in state reconciles automatically; failed reads can be retried in place, while timeouts and stale account responses cannot overwrite current state; Settings can create a support report without credentials or account identifiers |
54
44
  | **Visible quota** | Keep backend-provided standard Codex, Spark, and other limits separate |
55
45
  | **Composer quota** | Choose a compact percentage, progress bar, or no inline quota display |
56
46
  | **Safe quota reset** | See each reset credit separately and deliberately try one with a cooldown and acknowledgement |
57
47
  | **Subscription search** | Explicitly route search globally through DSH default search or the signed-in Codex subscription |
58
48
  | **Codex image generation and editing (Beta)** | Generate without references, or explicitly edit one selected image; preview, zoom, annotate regions, download the original, and get the original host path for new or edited images |
59
49
  | **Fast mode** | Switch between Standard and Fast directly in the composer |
60
- | **Model-aware context** | Keep catalog defaults, use each model's supported extended window, or enter a full numeric token limit for each model |
50
+ | **Model-aware context** | Keep catalog defaults, use each model's supported extended window, or enter a full numeric token limit for each model; Settings refreshes the model directory on open, account changes, and connection resets without overwriting an unsaved draft |
61
51
  | **Headless runs** | Use the same signed-in Codex provider for one-shot DSH tasks that print their answer and exit |
62
52
 
63
53
  These capabilities reuse the same local ChatGPT sign-in. Subscription routing failures stay visible and never silently switch to another paid route.
@@ -181,7 +171,7 @@ A new image request does not silently include earlier images. GPT Image 2 can ta
181
171
  <img src="https://raw.githubusercontent.com/WSL043/dsh-codex-subscription/main/docs/assets/image-preview-annotations-en.png" width="800" alt="Generated image, region note, and continue editing inside the DSH Image Viewer">
182
172
  </p>
183
173
 
184
- The screenshot above illustrates image viewing and on-image notes. Version 1.14.0 includes the same basic experience; available buttons can vary with the image and installed viewer version.
174
+ The screenshot above illustrates image viewing and on-image notes; available buttons can vary with the image and installed viewer version.
185
175
 
186
176
  ### Composer speed
187
177
 
package/README.md CHANGED
@@ -23,16 +23,6 @@
23
23
  <img src="https://raw.githubusercontent.com/WSL043/dsh-codex-subscription/main/docs/assets/readme-hero.webp" width="900" alt="Codex 订阅直接用在 DSH:订阅模型、联网搜索、额度与安全重置、图片生成和高速模式">
24
24
  </p>
25
25
 
26
- ## 1.14.2 重点变化
27
-
28
- - GPT-6 Astra 支持模型目录上下文设置:标准模式保留目录默认值,扩展模式使用 872K,自定义模式限制为 128K–872K。
29
-
30
- ### 1.14.1 更新回顾
31
-
32
- - 输入框同时显示服务端返回的 5 小时与每周额度,避免其中一个额度窗口被隐藏。
33
- - 图片编辑会检查附件引用,自动补全裸 SHA-256,并从当前会话取回真实附件元数据;误填路径时提示模型通过 `read_image` 获取正确引用并重试。
34
- - 部位修改会同时附上干净原图、编号标注图和坐标说明,让模型能够对应每条修改要求。
35
-
36
26
  ## 三步开始
37
27
 
38
28
  1. **安装插件**:直接运行 DSH 标准 bundle 命令:
@@ -51,14 +41,14 @@ DSH-Portable 也提供相同的标准插件命令,因此同样使用上面的
51
41
  | 能力 | 用户得到什么 |
52
42
  | --- | --- |
53
43
  | **订阅模型直连** | 登录 ChatGPT 后直接使用 Codex,不需要 OpenAI API Key 或 Codex CLI |
54
- | **可恢复、可诊断** | 登录状态会自动对账;设置页可生成不含凭据和账号标识的支持报告 |
44
+ | **可恢复、可诊断** | 登录状态会自动对账;读取失败时可在原处重试,超时和旧账号响应不会覆盖当前状态;设置页可生成不含凭据和账号标识的支持报告 |
55
45
  | **额度可见** | 普通 Codex、Spark 等服务端实际返回的额度分开显示 |
56
46
  | **输入框额度** | 可选择紧凑百分比、进度条或完全关闭输入框额度显示 |
57
47
  | **安全额度重置** | 每张重置卡单独显示,并通过冷静期和知情确认主动尝试重置 |
58
48
  | **订阅搜索** | 可将全部模型的搜索明确路由到 DSH 默认搜索或已登录的 Codex 订阅 |
59
49
  | **Codex 图片生成与编辑(Beta)** | 可无参考图全新生成,也可明确选择会话图片继续编辑;支持预览、缩放、区域备注、下载原图,并为新生成或编辑的图片提供 DSH 主机上的原图路径 |
60
50
  | **高速模式** | 直接在输入框切换标准或高速,无需离开当前会话 |
61
- | **模型感知上下文** | 可保留目录默认值、按模型启用扩展窗口,或为每个模型填写完整数字 Token 上限 |
51
+ | **模型感知上下文** | 可保留目录默认值、按模型启用扩展窗口,或为每个模型填写完整数字 Token 上限;设置页打开、账号切换和连接重置后会刷新模型目录,失败时可重试且不会覆盖未保存的草稿 |
62
52
  | **Headless 任务** | 使用同一份已登录的 Codex Provider 运行一次性 DSH 任务,输出答案后自动退出 |
63
53
 
64
54
  这些能力共用同一份本机 ChatGPT 登录。订阅路由失败时会明确报错,不会静默切换到其他付费路由。
@@ -179,7 +169,7 @@ ChatGPT 返回可用重置卡时,设置页会把每张卡分别显示为紧凑
179
169
  <img src="https://raw.githubusercontent.com/WSL043/dsh-codex-subscription/main/docs/assets/image-preview-annotations.png" width="800" alt="DSH 图片查看器中的生成图、区域备注和继续编辑">
180
170
  </p>
181
171
 
182
- 上图展示图片查看与图上备注的基本交互。1.14.0 已内置相同的基础体验;具体按钮会随图片和所安装的查看器版本变化。
172
+ 上图展示图片查看与图上备注的基本交互;具体按钮会随图片和所安装的查看器版本变化。
183
173
 
184
174
  ### 输入框速度
185
175
 
package/lib/client.js CHANGED
@@ -1097,7 +1097,7 @@ window.__ModuleLoader__.load({
1097
1097
  }
1098
1098
  //#endregion
1099
1099
  //#region src/preference-controller.js
1100
- const CHANNEL$1 = "/codex-subscription";
1100
+ const CHANNEL$2 = "/codex-subscription";
1101
1101
  const unwrap$1 = (response) => {
1102
1102
  if (!response?.ok) throw new Error(response?.error?.message ?? "Codex RPC failed");
1103
1103
  return response.value;
@@ -1112,6 +1112,11 @@ window.__ModuleLoader__.load({
1112
1112
  let generation = 0;
1113
1113
  let contextModels = [];
1114
1114
  let verbosityModels = [];
1115
+ let modelError = false;
1116
+ let modelRefreshGeneration = 0;
1117
+ let modelRefreshStarted = false;
1118
+ let disposed = false;
1119
+ const sameModels = (left, right) => left.length === right.length && left.every((model, index) => JSON.stringify(model) === JSON.stringify(right[index]));
1115
1120
  const nativeSnapshot = () => scope.getSnapshot();
1116
1121
  const read = () => {
1117
1122
  const native = nativeSnapshot();
@@ -1131,6 +1136,7 @@ window.__ModuleLoader__.load({
1131
1136
  customContextWindows: Object.fromEntries(Object.entries(CUSTOM_CONTEXT_MODEL_FIELDS).map(([modelKey, field]) => [modelKey, normalizeCustomContextWindow(value?.[field] ?? CUSTOM_CONTEXT_MODEL_DEFAULTS[modelKey], CUSTOM_CONTEXT_MODEL_CAPS[modelKey])])),
1132
1137
  contextModels,
1133
1138
  verbosityModels,
1139
+ modelError,
1134
1140
  writable: !updating && current.status === "ready" && current.writable === true,
1135
1141
  saving: updating,
1136
1142
  error
@@ -1148,8 +1154,10 @@ window.__ModuleLoader__.load({
1148
1154
  publish();
1149
1155
  });
1150
1156
  const acceptFallback = (value) => {
1151
- contextModels = Array.isArray(value?.contextModels) ? value.contextModels : [];
1152
- verbosityModels = Array.isArray(value?.verbosityModels) ? value.verbosityModels : [];
1157
+ if (!modelRefreshStarted) {
1158
+ contextModels = Array.isArray(value?.contextModels) ? value.contextModels : [];
1159
+ verbosityModels = Array.isArray(value?.verbosityModels) ? value.verbosityModels : [];
1160
+ }
1153
1161
  fallbackStatus = "ready";
1154
1162
  fallback = {
1155
1163
  status: "ready",
@@ -1174,21 +1182,50 @@ window.__ModuleLoader__.load({
1174
1182
  error = false;
1175
1183
  publish();
1176
1184
  try {
1177
- const value = unwrap$1(await rpc.call(CHANNEL$1, "preferences/status", {}));
1178
- if (current !== generation) return;
1185
+ const value = unwrap$1(await rpc.call(CHANNEL$2, "preferences/status", {}));
1186
+ if (current !== generation || disposed) return;
1179
1187
  if (nativeSnapshot().status === "ready") {
1180
- contextModels = Array.isArray(value?.contextModels) ? value.contextModels : [];
1181
- verbosityModels = Array.isArray(value?.verbosityModels) ? value.verbosityModels : [];
1188
+ if (!modelRefreshStarted) {
1189
+ contextModels = Array.isArray(value?.contextModels) ? value.contextModels : [];
1190
+ verbosityModels = Array.isArray(value?.verbosityModels) ? value.verbosityModels : [];
1191
+ }
1182
1192
  } else acceptFallback(value);
1183
1193
  publish();
1184
1194
  } catch {
1185
- if (current !== generation || nativeSnapshot().status === "ready") return;
1195
+ if (current !== generation || disposed || nativeSnapshot().status === "ready") return;
1186
1196
  fallbackStatus = "unavailable";
1187
1197
  publish();
1188
1198
  }
1189
1199
  };
1200
+ const refreshModels = async () => {
1201
+ const current = ++modelRefreshGeneration;
1202
+ modelRefreshStarted = true;
1203
+ const hadError = modelError;
1204
+ modelError = false;
1205
+ if (hadError) publish();
1206
+ try {
1207
+ const value = unwrap$1(await rpc.call(CHANNEL$2, "preferences/models", {}));
1208
+ if (disposed || current !== modelRefreshGeneration) return false;
1209
+ const nextContextModels = Array.isArray(value?.contextModels) ? value.contextModels : [];
1210
+ const nextVerbosityModels = Array.isArray(value?.verbosityModels) ? value.verbosityModels : [];
1211
+ const changed = !sameModels(contextModels, nextContextModels) || !sameModels(verbosityModels, nextVerbosityModels);
1212
+ if (changed) {
1213
+ contextModels = nextContextModels;
1214
+ verbosityModels = nextVerbosityModels;
1215
+ publish();
1216
+ }
1217
+ return changed;
1218
+ } catch {
1219
+ if (disposed || current !== modelRefreshGeneration) return false;
1220
+ if (!modelError) {
1221
+ modelError = true;
1222
+ publish();
1223
+ }
1224
+ return false;
1225
+ }
1226
+ };
1190
1227
  const set = async (patch) => {
1191
- if (snapshot.status !== "ready" || snapshot.writable !== true) return;
1228
+ if (disposed || snapshot.status !== "ready" || snapshot.writable !== true) return;
1192
1229
  const current = ++generation;
1193
1230
  const entries = Object.entries(patch);
1194
1231
  updating = true;
@@ -1207,7 +1244,7 @@ window.__ModuleLoader__.load({
1207
1244
  error = entries.some(([field, value]) => accepted?.[field] !== value);
1208
1245
  pendingPatch = void 0;
1209
1246
  } else {
1210
- const value = unwrap$1(await rpc.call(CHANNEL$1, "preferences/update", patch));
1247
+ const value = unwrap$1(await rpc.call(CHANNEL$2, "preferences/update", patch));
1211
1248
  if (current !== generation) return;
1212
1249
  acceptFallback(value);
1213
1250
  pendingPatch = void 0;
@@ -1234,10 +1271,219 @@ window.__ModuleLoader__.load({
1234
1271
  load,
1235
1272
  set,
1236
1273
  retry: () => failedPatch === void 0 ? load() : set(failedPatch),
1237
- dispose: disposeScope
1274
+ refreshModels,
1275
+ dispose: () => {
1276
+ disposed = true;
1277
+ generation += 1;
1278
+ modelRefreshGeneration += 1;
1279
+ disposeScope();
1280
+ }
1238
1281
  };
1239
1282
  }
1240
1283
  //#endregion
1284
+ //#region src/account-status-controller.js
1285
+ const CHANNEL$1 = "/codex-subscription";
1286
+ const DEFAULT_TIMEOUT_MS = 1e4;
1287
+ const STATUS_ERROR_CODES = /* @__PURE__ */ new Set([
1288
+ "credential-unavailable",
1289
+ "credential-malformed",
1290
+ "transport",
1291
+ "timeout",
1292
+ "unknown"
1293
+ ]);
1294
+ const STATUS_ERROR_MESSAGES = /* @__PURE__ */ new Map([
1295
+ ["Codex account credentials are unavailable", "credential-unavailable"],
1296
+ ["Codex account credentials are malformed", "credential-malformed"],
1297
+ ["Codex account status service is unavailable", "transport"],
1298
+ ["Could not read Codex account status", "unknown"]
1299
+ ]);
1300
+ const TIMEOUT_CODES = /* @__PURE__ */ new Set([
1301
+ "TIMEOUT",
1302
+ "ETIMEDOUT",
1303
+ "ERR_TIMEOUT",
1304
+ "UND_ERR_CONNECT_TIMEOUT"
1305
+ ]);
1306
+ const TRANSPORT_CODES = /* @__PURE__ */ new Set([
1307
+ "ECONNRESET",
1308
+ "ECONNREFUSED",
1309
+ "ENOTFOUND",
1310
+ "EAI_AGAIN",
1311
+ "NETWORK",
1312
+ "NETWORK_ERROR",
1313
+ "TRANSPORT",
1314
+ "CONNECTION_CLOSED",
1315
+ "DISCONNECTED"
1316
+ ]);
1317
+ const asCode = (value) => typeof value === "string" ? value.trim().toLowerCase() : void 0;
1318
+ function rpcError(response) {
1319
+ const code = asCode(response?.error?.code);
1320
+ const message = typeof response?.error?.message === "string" ? response.error.message : "";
1321
+ const error = /* @__PURE__ */ new Error("Codex account status request failed");
1322
+ error.code = code === "internal" && STATUS_ERROR_MESSAGES.has(message) ? STATUS_ERROR_MESSAGES.get(message) : "unknown";
1323
+ return error;
1324
+ }
1325
+ function timeoutError() {
1326
+ const error = /* @__PURE__ */ new Error("Codex account status request timed out");
1327
+ error.code = "timeout";
1328
+ error.name = "TimeoutError";
1329
+ return error;
1330
+ }
1331
+ function classifyAccountStatusError(error) {
1332
+ const code = typeof error?.code === "string" ? error.code.trim().toUpperCase() : "";
1333
+ if (code === "TIMEOUT" || TIMEOUT_CODES.has(code) || error?.name === "TimeoutError") return "timeout";
1334
+ if (STATUS_ERROR_CODES.has(asCode(error?.code))) return asCode(error.code);
1335
+ if (TRANSPORT_CODES.has(code) || error?.name === "NetworkError") return "transport";
1336
+ return "unknown";
1337
+ }
1338
+ function publicAccountStatusError(error) {
1339
+ return Object.freeze({ code: classifyAccountStatusError(error) });
1340
+ }
1341
+ /** Own the account-status request lifecycle independently from account actions. */
1342
+ function createAccountStatusController(rpc, options = {}) {
1343
+ const request = options.request ?? (() => rpc.call(CHANNEL$1, "status", {}));
1344
+ const scheduleTimeout = options.setTimeout ?? setTimeout;
1345
+ const cancelTimeout = options.clearTimeout ?? clearTimeout;
1346
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1347
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new Error("Account status timeout must be positive");
1348
+ let snapshot = Object.freeze({
1349
+ status: "loading",
1350
+ account: void 0,
1351
+ error: void 0,
1352
+ retrying: false
1353
+ });
1354
+ let generation = 0;
1355
+ let active;
1356
+ let disposed = false;
1357
+ const listeners = /* @__PURE__ */ new Set();
1358
+ const publish = (next) => {
1359
+ snapshot = Object.freeze(next);
1360
+ for (const listener of [...listeners]) listener();
1361
+ };
1362
+ const load = () => {
1363
+ if (disposed) return Promise.resolve(void 0);
1364
+ if (active !== void 0) return active.promise;
1365
+ const id = ++generation;
1366
+ const retrying = snapshot.error !== void 0;
1367
+ publish({
1368
+ status: retrying ? "error" : "loading",
1369
+ account: retrying ? snapshot.account : void 0,
1370
+ error: retrying ? snapshot.error : void 0,
1371
+ retrying: true
1372
+ });
1373
+ const controller = new AbortController();
1374
+ let timer;
1375
+ let onAbort;
1376
+ const cancelled = new Promise((resolve, reject) => {
1377
+ onAbort = () => reject(controller.signal.reason);
1378
+ controller.signal.addEventListener("abort", onAbort, { once: true });
1379
+ if (controller.signal.aborted) onAbort();
1380
+ });
1381
+ const timeout = new Promise((resolve, reject) => {
1382
+ timer = scheduleTimeout(() => {
1383
+ const error = timeoutError();
1384
+ controller.abort(error);
1385
+ reject(error);
1386
+ }, timeoutMs);
1387
+ });
1388
+ const work = Promise.resolve().then(() => request(controller.signal)).then((response) => {
1389
+ if (!response?.ok) throw rpcError(response);
1390
+ return response.value;
1391
+ });
1392
+ const promise = Promise.race([
1393
+ work,
1394
+ timeout,
1395
+ cancelled
1396
+ ]).then((account) => {
1397
+ if (disposed || id !== generation || controller.signal.aborted) return void 0;
1398
+ if (account === null || typeof account !== "object" || Array.isArray(account) || typeof account.authenticated !== "boolean") throw new Error("Invalid account status");
1399
+ publish({
1400
+ status: "ready",
1401
+ account,
1402
+ error: void 0,
1403
+ retrying: false
1404
+ });
1405
+ return account;
1406
+ }).catch((error) => {
1407
+ if (disposed || id !== generation || controller.signal.aborted && error?.code !== "timeout") return void 0;
1408
+ publish({
1409
+ status: "error",
1410
+ account: void 0,
1411
+ error: publicAccountStatusError(error),
1412
+ retrying: false
1413
+ });
1414
+ }).finally(() => {
1415
+ cancelTimeout(timer);
1416
+ controller.signal.removeEventListener("abort", onAbort);
1417
+ if (active?.id === id) active = void 0;
1418
+ });
1419
+ active = {
1420
+ id,
1421
+ controller,
1422
+ promise
1423
+ };
1424
+ return promise;
1425
+ };
1426
+ const acceptAccount = (account) => {
1427
+ if (disposed) return false;
1428
+ generation += 1;
1429
+ active?.controller.abort(/* @__PURE__ */ new Error("Account status superseded by an account action"));
1430
+ active = void 0;
1431
+ publish({
1432
+ status: "ready",
1433
+ account,
1434
+ error: void 0,
1435
+ retrying: false
1436
+ });
1437
+ return true;
1438
+ };
1439
+ const reload = () => {
1440
+ if (disposed) return Promise.resolve(void 0);
1441
+ if (active !== void 0) {
1442
+ generation += 1;
1443
+ active.controller.abort(/* @__PURE__ */ new Error("Account status reload superseded the previous request"));
1444
+ active = void 0;
1445
+ }
1446
+ return load();
1447
+ };
1448
+ const dispose = () => {
1449
+ if (disposed) return;
1450
+ disposed = true;
1451
+ generation += 1;
1452
+ active?.controller.abort(/* @__PURE__ */ new Error("Account status controller disposed"));
1453
+ active = void 0;
1454
+ listeners.clear();
1455
+ };
1456
+ return Object.freeze({
1457
+ getSnapshot: () => snapshot,
1458
+ subscribe(listener) {
1459
+ listeners.add(listener);
1460
+ return () => listeners.delete(listener);
1461
+ },
1462
+ load,
1463
+ retry: load,
1464
+ reload,
1465
+ acceptAccount,
1466
+ dispose
1467
+ });
1468
+ }
1469
+ //#endregion
1470
+ //#region src/context-draft-state.js
1471
+ const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value ?? {}, key);
1472
+ /**
1473
+ * Reconcile saved context values with the inputs currently shown in Settings.
1474
+ * A draft survives a catalog refresh while its saved value is unchanged. New
1475
+ * rows and rows whose saved value changed start from the new saved value.
1476
+ */
1477
+ function reconcileContextDrafts({ modelRows, drafts, previousSavedValues, savedValues }) {
1478
+ const next = {};
1479
+ for (const model of modelRows) {
1480
+ const key = model.key;
1481
+ const saved = String(savedValues?.[key] ?? "");
1482
+ next[key] = hasOwn(previousSavedValues, key) && previousSavedValues[key] === saved && hasOwn(drafts, key) ? drafts[key] : saved;
1483
+ }
1484
+ return next;
1485
+ }
1486
+ //#endregion
1241
1487
  //#region src/client.jsx
1242
1488
  const inject = [
1243
1489
  "slots",
@@ -1281,6 +1527,12 @@ window.__ModuleLoader__.load({
1281
1527
  failed: "登录失败,请重试。",
1282
1528
  loadFailed: "无法读取账户状态。",
1283
1529
  accountRetry: "重试",
1530
+ accountRetrying: "正在重试账户状态…",
1531
+ accountCredentialUnavailable: "登录凭据暂时不可用。请重试;不会删除已保存的登录信息。",
1532
+ accountCredentialMalformed: "登录凭据格式异常,无法读取账户状态。重试不会删除已保存的登录信息。",
1533
+ accountStatusTimeout: "读取账户状态超时,请重试。",
1534
+ accountStatusTransport: "无法连接账户服务,请检查连接后重试。",
1535
+ accountStatusUnknown: "无法读取账户状态,请重试。",
1284
1536
  diagnostics: "支持诊断",
1285
1537
  diagnosticsLoad: "生成诊断",
1286
1538
  diagnosticsLoading: "生成中…",
@@ -1409,6 +1661,7 @@ window.__ModuleLoader__.load({
1409
1661
  modelsEmpty: "没有可用模型。",
1410
1662
  effortsEmpty: "当前模型未提供推理等级。",
1411
1663
  modelRetry: "重试",
1664
+ modelDirectoryFailed: "模型目录加载失败,请重试。",
1412
1665
  modelFailed: "模型目录加载失败:{value}",
1413
1666
  groupFailed: "{name}:{value}",
1414
1667
  imageGenerate: "生成图片",
@@ -1476,6 +1729,12 @@ window.__ModuleLoader__.load({
1476
1729
  failed: "Sign-in failed. Try again.",
1477
1730
  loadFailed: "Could not read account status.",
1478
1731
  accountRetry: "Retry",
1732
+ accountRetrying: "Retrying account status…",
1733
+ accountCredentialUnavailable: "The saved sign-in credentials are temporarily unavailable. Retry; saved sign-in information will not be deleted.",
1734
+ accountCredentialMalformed: "The saved sign-in credentials are malformed, so account status cannot be read. Retrying will not delete saved sign-in information.",
1735
+ accountStatusTimeout: "Reading account status timed out. Retry.",
1736
+ accountStatusTransport: "The account service is unavailable. Check the connection and retry.",
1737
+ accountStatusUnknown: "Could not read account status. Retry.",
1479
1738
  diagnostics: "Support diagnostics",
1480
1739
  diagnosticsLoad: "Create report",
1481
1740
  diagnosticsLoading: "Creating…",
@@ -1604,6 +1863,7 @@ window.__ModuleLoader__.load({
1604
1863
  modelsEmpty: "No models available.",
1605
1864
  effortsEmpty: "This model provides no reasoning effort levels.",
1606
1865
  modelRetry: "Retry",
1866
+ modelDirectoryFailed: "Could not load the model directory. Try again.",
1607
1867
  modelFailed: "Could not load models: {value}",
1608
1868
  groupFailed: "{name}: {value}",
1609
1869
  imageGenerate: "Generate image",
@@ -1694,6 +1954,16 @@ window.__ModuleLoader__.load({
1694
1954
  if (!response?.ok) throw new Error(response?.error?.message ?? "Codex RPC failed");
1695
1955
  return response.value;
1696
1956
  };
1957
+ const accountStatusErrorText = (error, t) => {
1958
+ const key = {
1959
+ "credential-unavailable": "accountCredentialUnavailable",
1960
+ "credential-malformed": "accountCredentialMalformed",
1961
+ timeout: "accountStatusTimeout",
1962
+ transport: "accountStatusTransport",
1963
+ unknown: "accountStatusUnknown"
1964
+ }[error?.code];
1965
+ return t(key ?? "accountStatusUnknown");
1966
+ };
1697
1967
  const fill = (text, values) => Object.entries(values).reduce((next, [key, value]) => next.replace(`{${key}}`, String(value)), text);
1698
1968
  const maskEmail = (value) => {
1699
1969
  if (typeof value !== "string" || !value.includes("@")) return "••••";
@@ -1927,6 +2197,7 @@ window.__ModuleLoader__.load({
1927
2197
  });
1928
2198
  }
1929
2199
  const usePreferenceSnapshot = (preference) => (0, react.useSyncExternalStore)(preference.subscribe, preference.getSnapshot);
2200
+ const useAccountStatusSnapshot = (accountStatus) => (0, react.useSyncExternalStore)(accountStatus.subscribe, accountStatus.getSnapshot);
1930
2201
  const notifyQuickQuota = () => window.dispatchEvent(new Event(QUICK_QUOTA_REFRESH_EVENT));
1931
2202
  const formatRunway = (seconds, t) => {
1932
2203
  if (!Number.isFinite(seconds) || seconds <= 0) return void 0;
@@ -2089,7 +2360,18 @@ window.__ModuleLoader__.load({
2089
2360
  const modelRows = snapshot.contextModels.filter((model) => model.fixed !== true);
2090
2361
  const fixedRows = snapshot.contextModels.filter((model) => model.fixed === true);
2091
2362
  const [drafts, setDrafts] = (0, react.useState)({});
2092
- (0, react.useEffect)(() => setDrafts(Object.fromEntries(modelRows.map((model) => [model.key, String(snapshot.customContextWindows[model.key])]))), [snapshot.customContextWindows, snapshot.contextModels]);
2363
+ const previousSavedValues = (0, react.useRef)();
2364
+ (0, react.useEffect)(() => {
2365
+ const savedValues = Object.fromEntries(modelRows.map((model) => [model.key, String(snapshot.customContextWindows[model.key])]));
2366
+ const previous = previousSavedValues.current;
2367
+ setDrafts((current) => reconcileContextDrafts({
2368
+ modelRows,
2369
+ drafts: current,
2370
+ previousSavedValues: previous,
2371
+ savedValues
2372
+ }));
2373
+ previousSavedValues.current = savedValues;
2374
+ }, [modelRows.map((model) => `${model.key}\u0000${snapshot.customContextWindows[model.key]}`).join("")]);
2093
2375
  const hint = snapshot.contextMode === "extended" ? t("contextExtendedHint") : snapshot.contextMode === "custom" ? t("contextCustomHint") : t("contextStandardHint");
2094
2376
  const commit = (modelKey) => {
2095
2377
  const parsed = parseContextWindow(drafts[modelKey]);
@@ -2124,81 +2406,99 @@ window.__ModuleLoader__.load({
2124
2406
  const selectedMode = contextModeItems.find((item) => item.id === snapshot.contextMode)?.label ?? t("contextStandard");
2125
2407
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2126
2408
  className: "codexSubscriptionContext",
2127
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2128
- className: "codexSubscriptionContextHead",
2129
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2130
- className: "codexSubscriptionContextCopy",
2131
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2132
- className: "codexSubscriptionPreferenceLabel",
2133
- children: t("contextTitle")
2134
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2135
- className: "codexSubscriptionContextHint",
2136
- children: hint
2409
+ children: [
2410
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2411
+ className: "codexSubscriptionContextHead",
2412
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2413
+ className: "codexSubscriptionContextCopy",
2414
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2415
+ className: "codexSubscriptionPreferenceLabel",
2416
+ children: t("contextTitle")
2417
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2418
+ className: "codexSubscriptionContextHint",
2419
+ children: hint
2420
+ })]
2421
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
2422
+ open: menuOpen,
2423
+ items: contextModeItems,
2424
+ selectedId: snapshot.contextMode,
2425
+ onSelect: (value) => {
2426
+ setMenuOpen(false);
2427
+ preference.set({ [CONTEXT_MODE_FIELD]: value });
2428
+ },
2429
+ onClose: () => setMenuOpen(false),
2430
+ align: "end",
2431
+ side: "bottom",
2432
+ portal: true,
2433
+ compact: true,
2434
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2435
+ className: "codexSubscriptionContextTrigger",
2436
+ type: "button",
2437
+ "aria-label": t("contextTitle"),
2438
+ "aria-haspopup": "menu",
2439
+ "aria-expanded": menuOpen,
2440
+ disabled: !writable,
2441
+ onClick: () => setMenuOpen((value) => !value),
2442
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: selectedMode }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, {})]
2443
+ })
2137
2444
  })]
2138
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
2139
- open: menuOpen,
2140
- items: contextModeItems,
2141
- selectedId: snapshot.contextMode,
2142
- onSelect: (value) => {
2143
- setMenuOpen(false);
2144
- preference.set({ [CONTEXT_MODE_FIELD]: value });
2145
- },
2146
- onClose: () => setMenuOpen(false),
2147
- align: "end",
2148
- side: "bottom",
2149
- portal: true,
2150
- compact: true,
2151
- anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2152
- className: "codexSubscriptionContextTrigger",
2445
+ }),
2446
+ snapshot.contextMode === "custom" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2447
+ className: "codexSubscriptionContextModels",
2448
+ children: [modelRows.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2449
+ className: "codexSubscriptionContextModel",
2450
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2451
+ className: "codexSubscriptionContextModelCopy",
2452
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: model.label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: fill(t("contextMaximum"), { value: String(model.maximum) }) })]
2453
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
2454
+ "aria-label": `${model.label} ${t("contextTokens")}`,
2455
+ className: "codexSubscriptionContextInput",
2456
+ type: "number",
2457
+ inputMode: "numeric",
2458
+ min: MIN_CUSTOM_CONTEXT_WINDOW,
2459
+ max: model.maximum,
2460
+ step: 1,
2461
+ value: drafts[model.key] ?? "",
2462
+ disabled: !writable,
2463
+ onChange: (event) => {
2464
+ const nextValue = event.currentTarget.value;
2465
+ setDrafts((current) => ({
2466
+ ...current,
2467
+ [model.key]: nextValue
2468
+ }));
2469
+ },
2470
+ onBlur: () => commit(model.key),
2471
+ onKeyDown: (event) => {
2472
+ if (event.key === "Enter") event.currentTarget.blur();
2473
+ }
2474
+ })]
2475
+ }, model.key)), fixedRows.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2476
+ className: "codexSubscriptionContextModel",
2477
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2478
+ className: "codexSubscriptionContextModelCopy",
2479
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: model.label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: fill(t("contextFixed"), { value: formatContextWindow(model.maximum) }) })]
2480
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2481
+ className: "codexSubscriptionContextHint",
2482
+ children: formatContextWindow(model.maximum)
2483
+ })]
2484
+ }, model.key))]
2485
+ }) : null,
2486
+ snapshot.modelError ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2487
+ className: "codexSubscriptionRecover",
2488
+ role: "alert",
2489
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2490
+ className: "codexSubscriptionError",
2491
+ children: t("modelDirectoryFailed")
2492
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2153
2493
  type: "button",
2154
- "aria-label": t("contextTitle"),
2155
- "aria-haspopup": "menu",
2156
- "aria-expanded": menuOpen,
2157
- disabled: !writable,
2158
- onClick: () => setMenuOpen((value) => !value),
2159
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: selectedMode }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, {})]
2160
- })
2161
- })]
2162
- }), snapshot.contextMode === "custom" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2163
- className: "codexSubscriptionContextModels",
2164
- children: [modelRows.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2165
- className: "codexSubscriptionContextModel",
2166
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2167
- className: "codexSubscriptionContextModelCopy",
2168
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: model.label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: fill(t("contextMaximum"), { value: String(model.maximum) }) })]
2169
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
2170
- "aria-label": `${model.label} ${t("contextTokens")}`,
2171
- className: "codexSubscriptionContextInput",
2172
- type: "number",
2173
- inputMode: "numeric",
2174
- min: MIN_CUSTOM_CONTEXT_WINDOW,
2175
- max: model.maximum,
2176
- step: 1,
2177
- value: drafts[model.key] ?? "",
2178
- disabled: !writable,
2179
- onChange: (event) => {
2180
- const nextValue = event.currentTarget.value;
2181
- setDrafts((current) => ({
2182
- ...current,
2183
- [model.key]: nextValue
2184
- }));
2494
+ variant: "outline",
2495
+ onClick: () => {
2496
+ preference.refreshModels();
2185
2497
  },
2186
- onBlur: () => commit(model.key),
2187
- onKeyDown: (event) => {
2188
- if (event.key === "Enter") event.currentTarget.blur();
2189
- }
2190
- })]
2191
- }, model.key)), fixedRows.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2192
- className: "codexSubscriptionContextModel",
2193
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2194
- className: "codexSubscriptionContextModelCopy",
2195
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: model.label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: fill(t("contextFixed"), { value: formatContextWindow(model.maximum) }) })]
2196
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2197
- className: "codexSubscriptionContextHint",
2198
- children: formatContextWindow(model.maximum)
2498
+ children: t("modelRetry")
2199
2499
  })]
2200
- }, model.key))]
2201
- }) : null]
2500
+ }) : null
2501
+ ]
2202
2502
  });
2203
2503
  }
2204
2504
  function PreferencesCard({ preference, t }) {
@@ -2935,18 +3235,23 @@ window.__ModuleLoader__.load({
2935
3235
  ]
2936
3236
  });
2937
3237
  }
2938
- function AccountFailureCard({ retry, t }) {
3238
+ function AccountFailureCard({ accountStatus, snapshot, t }) {
3239
+ const retrying = snapshot.retrying === true;
2939
3240
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2940
3241
  className: "codexSubscriptionCard codexSubscriptionRecover",
2941
3242
  role: "alert",
2942
3243
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2943
3244
  className: "codexSubscriptionError",
2944
- children: t("loadFailed")
3245
+ children: retrying ? t("accountRetrying") : accountStatusErrorText(snapshot.error, t)
2945
3246
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2946
3247
  type: "button",
2947
3248
  variant: "outline",
2948
- onClick: retry,
2949
- children: t("accountRetry")
3249
+ disabled: retrying,
3250
+ "aria-busy": retrying,
3251
+ onClick: () => {
3252
+ accountStatus.retry();
3253
+ },
3254
+ children: retrying ? t("accountRetrying") : t("accountRetry")
2950
3255
  })]
2951
3256
  });
2952
3257
  }
@@ -3406,27 +3711,19 @@ window.__ModuleLoader__.load({
3406
3711
  ]
3407
3712
  });
3408
3713
  }
3409
- function CodexSection({ preference, rpc, t }) {
3410
- const [account, setAccount] = (0, react.useState)();
3411
- const [accountError, setAccountError] = (0, react.useState)();
3714
+ function CodexSection({ preference, rpc, accountStatus, t }) {
3715
+ const accountSnapshot = useAccountStatusSnapshot(accountStatus);
3716
+ const account = accountSnapshot.account;
3412
3717
  const [resetKey, setResetKey] = (0, react.useState)(0);
3413
- const accountRequest = (0, react.useRef)(0);
3414
- const loadAccount = () => {
3415
- const id = ++accountRequest.current;
3416
- setAccount(void 0);
3417
- setAccountError(void 0);
3418
- rpc.call(CHANNEL, "status", {}).then(unwrap).then((next) => {
3419
- if (accountRequest.current === id) setAccount(next);
3420
- }).catch(() => {
3421
- if (accountRequest.current === id) setAccountError(true);
3422
- });
3718
+ const setAccount = accountStatus.acceptAccount;
3719
+ const accountChanged = () => {
3720
+ setResetKey((value) => value + 1);
3721
+ preference.refreshModels();
3423
3722
  };
3424
3723
  (0, react.useEffect)(() => {
3425
- loadAccount();
3426
- return () => {
3427
- accountRequest.current += 1;
3428
- };
3429
- }, []);
3724
+ accountStatus.load();
3725
+ preference.refreshModels();
3726
+ }, [accountStatus, preference]);
3430
3727
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
3431
3728
  className: "codexSubscription",
3432
3729
  children: [
@@ -3434,15 +3731,16 @@ window.__ModuleLoader__.load({
3434
3731
  className: "codexSubscriptionHead",
3435
3732
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", { children: t("title") })
3436
3733
  }),
3437
- accountError === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountCard, {
3734
+ accountSnapshot.status === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountFailureCard, {
3735
+ accountStatus,
3736
+ snapshot: accountSnapshot,
3737
+ t
3738
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountCard, {
3438
3739
  rpc,
3439
3740
  t,
3440
3741
  account,
3441
3742
  setAccount,
3442
- onSignedOut: () => setResetKey((value) => value + 1)
3443
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountFailureCard, {
3444
- retry: loadAccount,
3445
- t
3743
+ onSignedOut: accountChanged
3446
3744
  }),
3447
3745
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PreferencesCard, {
3448
3746
  preference,
@@ -3476,16 +3774,21 @@ window.__ModuleLoader__.load({
3476
3774
  }, "codex-subscription: style");
3477
3775
  const connection = ctx.get("connection");
3478
3776
  const preference = createPreferenceController(ctx.settingsScope.bind({ namespace: SETTINGS_NAMESPACE }), connection.rpc);
3777
+ const accountStatus = createAccountStatusController(connection.rpc);
3479
3778
  ctx.effect(() => {
3480
3779
  preference.load();
3780
+ accountStatus.load();
3481
3781
  const disposeReset = ctx.on("connection/reset", () => {
3482
3782
  preference.load();
3783
+ preference.refreshModels();
3784
+ accountStatus.reload();
3483
3785
  });
3484
3786
  return () => {
3485
3787
  disposeReset?.();
3486
3788
  preference.dispose();
3789
+ accountStatus.dispose();
3487
3790
  };
3488
- }, "codex-subscription: preferences");
3791
+ }, "codex-subscription: preferences and account status");
3489
3792
  const t = ctx.locale.bind(NS);
3490
3793
  ctx.slots.inject("shell.overlay", () => ctx.slots.register({
3491
3794
  name: "shell.overlay",
@@ -3505,6 +3808,7 @@ window.__ModuleLoader__.load({
3505
3808
  inject: () => ({
3506
3809
  preference,
3507
3810
  rpc: connection.rpc,
3811
+ accountStatus,
3508
3812
  t
3509
3813
  })
3510
3814
  }, CodexSection));
package/lib/index.js CHANGED
@@ -565,6 +565,35 @@ const badRequest = (message) => ({
565
565
  details: { issues: [] }
566
566
  }
567
567
  });
568
+ const accountStatusError = (message) => ({
569
+ ok: false,
570
+ error: {
571
+ code: "internal",
572
+ message,
573
+ details: { issues: [] }
574
+ }
575
+ });
576
+ const classifyAccountStatusError = (error) => {
577
+ const message = error instanceof Error ? error.message : "";
578
+ if (/malformed (?:OAuth|grant|account vault)|received a malformed OAuth|contains malformed OAuth/iu.test(message)) return ["credential-malformed", "Codex account credentials are malformed"];
579
+ if (/credential|account vault|readRecord|credential store|credentials service/iu.test(message)) return ["credential-unavailable", "Codex account credentials are unavailable"];
580
+ const code = typeof error?.code === "string" ? error.code.toUpperCase() : "";
581
+ if (error?.name === "TimeoutError" || [
582
+ "TIMEOUT",
583
+ "ETIMEDOUT",
584
+ "UND_ERR_CONNECT_TIMEOUT"
585
+ ].includes(code)) return ["transport", "Codex account status service is unavailable"];
586
+ if ([
587
+ "ECONNRESET",
588
+ "ECONNREFUSED",
589
+ "ENOTFOUND",
590
+ "EAI_AGAIN",
591
+ "NETWORK",
592
+ "NETWORK_ERROR",
593
+ "TRANSPORT"
594
+ ].includes(code) || error?.name === "NetworkError") return ["transport", "Codex account status service is unavailable"];
595
+ return ["unknown", "Could not read Codex account status"];
596
+ };
568
597
  const deferred = () => {
569
598
  let resolve;
570
599
  let reject;
@@ -804,7 +833,13 @@ function createCodexRpcHandler(coordinator, options = {}) {
804
833
  try {
805
834
  signal.throwIfAborted();
806
835
  const input = asObject(payload);
807
- if (endpoint === "status") return ok(await coordinator.accountStatus({ signal }));
836
+ if (endpoint === "status") try {
837
+ return ok(await coordinator.accountStatus({ signal }));
838
+ } catch (error) {
839
+ if (signal.aborted) throw error;
840
+ const [, message] = classifyAccountStatusError(error);
841
+ return accountStatusError(message);
842
+ }
808
843
  if (endpoint === "login/start") {
809
844
  const started = await coordinator.start({
810
845
  method: input.method,
@@ -1310,7 +1345,7 @@ function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0, reso
1310
1345
  }
1311
1346
  //#endregion
1312
1347
  //#region src/version.js
1313
- const PACKAGE_VERSION = "1.14.2";
1348
+ const PACKAGE_VERSION = "1.14.3";
1314
1349
  const USER_AGENT = `dsh-codex-subscription/${PACKAGE_VERSION}`;
1315
1350
  //#endregion
1316
1351
  //#region src/model-catalog.js
@@ -1324,6 +1359,7 @@ const LEVELS = [
1324
1359
  "xhigh",
1325
1360
  "max"
1326
1361
  ];
1362
+ const DEFAULT_REFRESH_TIMEOUT_MS = 1e4;
1327
1363
  const record$4 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
1328
1364
  const nonEmpty$2 = (value) => typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
1329
1365
  const positiveInteger$1 = (value) => Number.isSafeInteger(value) && value > 0 ? value : void 0;
@@ -1384,16 +1420,32 @@ function mergeModel(baseModels, remote) {
1384
1420
  }
1385
1421
  function createOfficialModelCatalog(options = {}) {
1386
1422
  const fetchCatalog = options.fetch ?? fetch;
1423
+ const scheduleTimeout = options.setTimeout ?? setTimeout;
1424
+ const cancelTimeout = options.clearTimeout ?? clearTimeout;
1425
+ const timeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : DEFAULT_REFRESH_TIMEOUT_MS;
1387
1426
  let models;
1388
1427
  let metadata = /* @__PURE__ */ new Map();
1389
1428
  let etag;
1390
1429
  let revision = 0;
1391
1430
  let refreshing;
1392
- const refresh = async ({ signal } = {}) => {
1393
- if (refreshing !== void 0) return refreshing;
1394
- refreshing = (async () => {
1395
- const auth = await options.getAuth({ signal });
1396
- const credential = await options.readCredential({ signal });
1431
+ let generation = 0;
1432
+ const refresh = ({ signal } = {}) => {
1433
+ if (signal?.aborted) return Promise.reject(signal.reason ?? /* @__PURE__ */ new Error("Codex model catalog refresh aborted"));
1434
+ if (refreshing?.generation === generation) return refreshing.promise;
1435
+ const currentGeneration = generation;
1436
+ const controller = new AbortController();
1437
+ const abort = () => {
1438
+ if (!controller.signal.aborted) controller.abort(signal?.reason ?? /* @__PURE__ */ new Error("Codex model catalog refresh aborted"));
1439
+ };
1440
+ signal?.addEventListener("abort", abort, { once: true });
1441
+ const requestSignal = controller.signal;
1442
+ let timer;
1443
+ const timeoutError = /* @__PURE__ */ new Error("Codex model catalog refresh timed out");
1444
+ const work = (async () => {
1445
+ const auth = await options.getAuth({ signal: requestSignal });
1446
+ if (currentGeneration !== generation || requestSignal.aborted) return false;
1447
+ const credential = await options.readCredential({ signal: requestSignal });
1448
+ if (currentGeneration !== generation || requestSignal.aborted) return false;
1397
1449
  const access = auth?.auth?.apiKey;
1398
1450
  const accountId = credential?.type === "oauth" ? credential.accountId : void 0;
1399
1451
  if (typeof access !== "string" || access.length === 0 || typeof accountId !== "string" || accountId.length === 0) return false;
@@ -1409,24 +1461,44 @@ function createOfficialModelCatalog(options = {}) {
1409
1461
  method: "GET",
1410
1462
  redirect: "error",
1411
1463
  headers,
1412
- signal
1464
+ signal: requestSignal
1413
1465
  });
1466
+ if (currentGeneration !== generation || requestSignal.aborted) return false;
1414
1467
  if (response.status === 304) return false;
1415
1468
  if (!response.ok) throw new Error(`Codex model catalog failed (HTTP ${response.status})`);
1416
1469
  const remote = parseOfficialModelCatalog(await response.json());
1470
+ if (currentGeneration !== generation || requestSignal.aborted) return false;
1417
1471
  if (remote.length === 0) throw new Error("Codex returned an empty model catalog");
1418
1472
  const baseModels = options.baseModels();
1419
1473
  const next = remote.map((model) => mergeModel(baseModels, model)).filter(Boolean);
1420
1474
  if (next.length === 0) throw new Error("Codex model catalog has no compatible models");
1475
+ if (currentGeneration !== generation || requestSignal.aborted) return false;
1421
1476
  models = next;
1422
1477
  metadata = new Map(remote.map((model) => [model.id, model]));
1423
1478
  etag = nonEmpty$2(response.headers.get("etag")) ?? etag;
1424
1479
  revision += 1;
1425
1480
  return true;
1426
- })().finally(() => {
1427
- refreshing = void 0;
1481
+ })();
1482
+ let rejectAborted;
1483
+ const abortPromise = new Promise((_, reject) => {
1484
+ rejectAborted = () => reject(requestSignal.reason ?? /* @__PURE__ */ new Error("Codex model catalog refresh aborted"));
1485
+ if (requestSignal.aborted) rejectAborted();
1486
+ else requestSignal.addEventListener("abort", rejectAborted, { once: true });
1428
1487
  });
1429
- return refreshing;
1488
+ timer = scheduleTimeout(() => controller.abort(timeoutError), timeoutMs);
1489
+ timer.unref?.();
1490
+ const promise = Promise.race([work, abortPromise]).finally(() => {
1491
+ cancelTimeout(timer);
1492
+ signal?.removeEventListener("abort", abort);
1493
+ requestSignal.removeEventListener("abort", rejectAborted);
1494
+ if (refreshing?.promise === promise) refreshing = void 0;
1495
+ });
1496
+ refreshing = {
1497
+ generation: currentGeneration,
1498
+ promise,
1499
+ cancel: () => controller.abort()
1500
+ };
1501
+ return promise;
1430
1502
  };
1431
1503
  return Object.freeze({
1432
1504
  refresh,
@@ -1434,6 +1506,10 @@ function createOfficialModelCatalog(options = {}) {
1434
1506
  metadata: (modelId) => metadata.get(modelId),
1435
1507
  revision: () => revision,
1436
1508
  clear() {
1509
+ generation += 1;
1510
+ const flight = refreshing;
1511
+ refreshing = void 0;
1512
+ flight?.cancel();
1437
1513
  models = void 0;
1438
1514
  metadata = /* @__PURE__ */ new Map();
1439
1515
  etag = void 0;
@@ -3124,6 +3200,22 @@ function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditSer
3124
3200
  if (signal.aborted) throw error;
3125
3201
  return publicError("internal", "Could not create support diagnostics");
3126
3202
  }
3203
+ if (endpoint === "preferences/models") try {
3204
+ signal.throwIfAborted();
3205
+ if (typeof modelCatalog?.refresh !== "function" || typeof preferences?.status !== "function") return publicError("internal", "Could not refresh Codex model catalog");
3206
+ await modelCatalog.refresh({ signal });
3207
+ const value = preferences.status();
3208
+ return {
3209
+ ok: true,
3210
+ value: {
3211
+ contextModels: Array.isArray(value?.contextModels) ? value.contextModels : [],
3212
+ verbosityModels: Array.isArray(value?.verbosityModels) ? value.verbosityModels : []
3213
+ }
3214
+ };
3215
+ } catch (error) {
3216
+ if (signal.aborted) throw error;
3217
+ return publicError("internal", "Could not refresh Codex model catalog");
3218
+ }
3127
3219
  if (endpoint === "preferences/status" || endpoint === "preferences/update") try {
3128
3220
  signal.throwIfAborted();
3129
3221
  if (endpoint === "preferences/update") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-codex-subscription",
3
- "version": "1.14.2",
3
+ "version": "1.14.3",
4
4
  "description": "Use ChatGPT and Codex subscriptions in DeepSeek Harness with OAuth, quota, safe resets, web search, images, and Fast mode",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",