dsh-qoder-connect 0.1.8 → 0.2.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.
@@ -362,6 +362,15 @@ function qoderOwnAuthPath(variant) {
362
362
  /** Plugin-owned status endpoint consumed by its browser half. */
363
363
  const QODER_STATUS_PATH = "/plugins/dsh-qoder-connect/status";
364
364
  /**
365
+ * Plugin-owned settings endpoint consumed by its browser half.
366
+ *
367
+ * GET answers the whole entry configuration as three layers (value/base/user)
368
+ * plus the write key; POST applies one patch. This is the plugin's own settings
369
+ * surface, replacing writes through the host's settings service — see
370
+ * {@link ./settings-store.ts} for why.
371
+ */
372
+ const QODER_SETTINGS_FACE_PATH = "/plugins/dsh-qoder-connect/settings";
373
+ /**
365
374
  * Plugin-owned probe control endpoint.
366
375
  *
367
376
  * Separate from the status route because it accepts writes: the status route's
@@ -844,6 +853,125 @@ async function probeModel(options) {
844
853
  };
845
854
  }
846
855
  //#endregion
856
+ //#region src/qoder/errors.ts
857
+ /**
858
+ * DSH-compatible LLM error representation.
859
+ *
860
+ * @module dsh-provider-qoder/qoder/errors
861
+ */
862
+ var QoderLlmError = class extends LlmError {
863
+ constructor(message, code = "UNKNOWN_ERROR", options) {
864
+ super(message, code, options);
865
+ }
866
+ };
867
+ /** The queue/limit markers Qoder puts in a rejection body (observed 2026-09-26). */
868
+ const QUEUE_BODY_MARKERS = [
869
+ /"code"\s*:\s*"?10605"?/u,
870
+ /"isQueued"\s*:\s*true/u,
871
+ /"queueCount"/u,
872
+ /"serviceAvailable"\s*:\s*false/u
873
+ ];
874
+ /**
875
+ * Whether an upstream rejection body is Qoder's **queue/limit** answer rather
876
+ * than an authorization failure.
877
+ *
878
+ * Qoder answers a saturated per-model queue (and a region whose service is not
879
+ * available) with an HTTP 401/403 — or a 200 SSE envelope carrying
880
+ * `statusCodeValue: 403` — whose body says `code: 10605`, `isQueued: true`,
881
+ * `queueCount`, `retryAfterSeconds` and sometimes `serviceAvailable: false`.
882
+ * Observed verbatim on 2026-09-26:
883
+ *
884
+ * {"code":"10605","message":"{\"isQueued\":true,\"modelKey\":\"qfmodel\",
885
+ * \"queueCount\":8887,\"queueType\":\"p3\",\"retryAfterSeconds\":30,
886
+ * \"serviceAvailable\":true,\"waitTime\":274}"}
887
+ *
888
+ * Reading that status as AUTH made the self-heal exchange fresh job tokens
889
+ * against a queue no token can jump, and — because the host retries
890
+ * `RATE_LIMIT` but deliberately not `AUTH` — it also suppressed the automatic
891
+ * retry that would have sailed through the 30-second window. The body is the
892
+ * only place the distinction is visible, so it gets the last say over status.
893
+ */
894
+ function qoderQueueSignal(body) {
895
+ if (body === void 0 || body === "") return void 0;
896
+ const text = body.replace(/\\/gu, "");
897
+ if (!QUEUE_BODY_MARKERS.some((marker) => marker.test(text))) return void 0;
898
+ const match = /"retryAfterSeconds"\s*:\s*(\d+)/u.exec(text);
899
+ return match === null ? {} : { retryAfterMs: Number(match[1]) * 1e3 };
900
+ }
901
+ /**
902
+ * Replace status digits that read as authorization failure with a neutral
903
+ * word, for messages the upstream used to announce a queue.
904
+ *
905
+ * `dsh-llm-pi-ai`'s error classifier is a text matcher whose first test is
906
+ * `/\b(?:401|403)\b/` — ahead of its 429/rate-limit test — so any message
907
+ * carrying those digits classifies AUTH no matter the structured code
908
+ * travelling beside it. A queue answer quoting the upstream's 403 (in the
909
+ * message prefix, the envelope status, or the body's `"code":"403"`) must not
910
+ * reach that matcher as-is, or the user sees "API 密钥无效" for a throttle
911
+ * (observed verbatim on 2026-09-26 23:30 and again at 23:40).
912
+ */
913
+ function deauthorizeDigits(message) {
914
+ return message.replace(/\b(?:401|403)\b/gu, "throttled");
915
+ }
916
+ function qoderHttpError(message, response, body) {
917
+ const { status } = response;
918
+ const headerRetryAfterMs = retryAfterMs(response.headers?.get("retry-after") ?? null);
919
+ const requestId = qoderRequestId(response.headers);
920
+ const queue = status === 401 || status === 403 || status === 429 ? qoderQueueSignal(body ?? message) : void 0;
921
+ if (queue !== void 0) {
922
+ const retryAfterMs = queue.retryAfterMs ?? headerRetryAfterMs;
923
+ return new QoderLlmError(deauthorizeDigits(message), "RATE_LIMIT", {
924
+ status,
925
+ ...retryAfterMs === void 0 ? {} : { providerRetryAfterMs: retryAfterMs },
926
+ ...requestId === void 0 ? {} : { requestId }
927
+ });
928
+ }
929
+ return new QoderLlmError(message, status === 401 || status === 403 ? "AUTH" : status === 408 ? "TIMEOUT" : status === 429 ? "RATE_LIMIT" : status >= 500 && status <= 599 ? "SERVER" : status >= 400 && status <= 499 ? "INVALID_REQUEST" : "PROVIDER_ERROR", {
930
+ status,
931
+ ...headerRetryAfterMs === void 0 ? {} : { providerRetryAfterMs: headerRetryAfterMs },
932
+ ...requestId === void 0 ? {} : { requestId }
933
+ });
934
+ }
935
+ function qoderRequestId(headers) {
936
+ const normalized = (headers?.get("x-request-id") ?? headers?.get("request-id") ?? headers?.get("x-amzn-requestid"))?.trim();
937
+ return normalized ? ProviderRequestId(normalized) : void 0;
938
+ }
939
+ /**
940
+ * Whether this failure is an upstream authorization rejection worth one
941
+ * re-auth retry.
942
+ *
943
+ * The job token the transport signs requests with is cached in memory; an
944
+ * upstream that invalidates it mid-lifetime (gateway rotation or a fault
945
+ * window) answers HTTP 401/403 before any payload is produced. That state is
946
+ * distinguishable from a genuinely revoked PAT only by trying a fresh
947
+ * exchange, so callers clear their credential cache and retry once before
948
+ * reporting `AUTH` to the user.
949
+ *
950
+ * A `RATE_LIMIT` code is never a re-auth candidate, **even when its status is
951
+ * 401/403**: that is the upstream's queue answer (code 10605 / isQueued /
952
+ * retryAfterSeconds in the body), which keeps the status but says the
953
+ * credential was not the problem. Healing it exchanged job tokens against a
954
+ * queue no token can jump (observed 2026-09-26) and, by consuming the retry,
955
+ * suppressed the rate-limit retry that would have ridden out the window.
956
+ */
957
+ function isQoderAuthRejection(error) {
958
+ if (!(error instanceof LlmError)) return false;
959
+ if (error.code === "RATE_LIMIT") return false;
960
+ return error.code === "AUTH" || error.failure.status === 401 || error.failure.status === 403;
961
+ }
962
+ function retryAfterMs(value, nowMs = Date.now()) {
963
+ const normalized = value?.trim();
964
+ if (!normalized) return void 0;
965
+ if (/^\d+$/u.test(normalized)) {
966
+ const delayMs = Number(normalized) * 1e3;
967
+ return Number.isFinite(delayMs) && delayMs > 0 ? delayMs : void 0;
968
+ }
969
+ const retryAt = Date.parse(normalized);
970
+ if (Number.isNaN(retryAt)) return void 0;
971
+ const delayMs = retryAt - nowMs;
972
+ return delayMs > 0 ? delayMs : void 0;
973
+ }
974
+ //#endregion
847
975
  //#region src/qoder/transport/endpoints.ts
848
976
  const qoderRegionEndpoints = {
849
977
  global: {
@@ -906,59 +1034,6 @@ function getQoderClaimCampaignUrl(region = "global", campaignId) {
906
1034
  return `${openApiUrl}/sash/api/v1/me/campaigns/${encodeURIComponent(campaignId)}/claim`;
907
1035
  }
908
1036
  //#endregion
909
- //#region src/qoder/errors.ts
910
- /**
911
- * DSH-compatible LLM error representation.
912
- *
913
- * @module dsh-provider-qoder/qoder/errors
914
- */
915
- var QoderLlmError = class extends LlmError {
916
- constructor(message, code = "UNKNOWN_ERROR", options) {
917
- super(message, code, options);
918
- }
919
- };
920
- function qoderHttpError(message, response) {
921
- const { status } = response;
922
- const code = status === 401 || status === 403 ? "AUTH" : status === 408 ? "TIMEOUT" : status === 429 ? "RATE_LIMIT" : status >= 500 && status <= 599 ? "SERVER" : status >= 400 && status <= 499 ? "INVALID_REQUEST" : "PROVIDER_ERROR";
923
- const providerRetryAfterMs = retryAfterMs(response.headers?.get("retry-after") ?? null);
924
- const requestId = qoderRequestId(response.headers);
925
- return new QoderLlmError(message, code, {
926
- status,
927
- ...providerRetryAfterMs === void 0 ? {} : { providerRetryAfterMs },
928
- ...requestId === void 0 ? {} : { requestId }
929
- });
930
- }
931
- function qoderRequestId(headers) {
932
- const normalized = (headers?.get("x-request-id") ?? headers?.get("request-id") ?? headers?.get("x-amzn-requestid"))?.trim();
933
- return normalized ? ProviderRequestId(normalized) : void 0;
934
- }
935
- /**
936
- * Whether this failure is an upstream authorization rejection worth one
937
- * re-auth retry.
938
- *
939
- * The job token the transport signs requests with is cached in memory; an
940
- * upstream that invalidates it mid-lifetime (gateway rotation or a fault
941
- * window) answers HTTP 401/403 before any payload is produced. That state is
942
- * distinguishable from a genuinely revoked PAT only by trying a fresh
943
- * exchange, so callers clear their credential cache and retry once before
944
- * reporting `AUTH` to the user.
945
- */
946
- function isQoderAuthRejection(error) {
947
- return error instanceof LlmError && (error.code === "AUTH" || error.failure.status === 401 || error.failure.status === 403);
948
- }
949
- function retryAfterMs(value, nowMs = Date.now()) {
950
- const normalized = value?.trim();
951
- if (!normalized) return void 0;
952
- if (/^\d+$/u.test(normalized)) {
953
- const delayMs = Number(normalized) * 1e3;
954
- return Number.isFinite(delayMs) && delayMs > 0 ? delayMs : void 0;
955
- }
956
- const retryAt = Date.parse(normalized);
957
- if (Number.isNaN(retryAt)) return void 0;
958
- const delayMs = retryAt - nowMs;
959
- return delayMs > 0 ? delayMs : void 0;
960
- }
961
- //#endregion
962
1037
  //#region src/qoder/transport/machine-id.ts
963
1038
  function defaultMachineIdPaths() {
964
1039
  return [join(homedir(), ".qoder", ".auth", "machine_id"), join(homedir(), ".dsh", "qoder", "machine_id")];
@@ -1285,7 +1360,7 @@ async function openApiJsonRequest(fetchImpl, options) {
1285
1360
  const text = await readLimitedText(response, response.ok ? defaultMaxJsonBytes : defaultMaxErrorBytes, `Qoder ${options.operation} response`);
1286
1361
  if (!response.ok) {
1287
1362
  options.logger?.error?.(`[Qoder ${options.operation}] Request failed`, redactLogPayload(text));
1288
- throw qoderHttpError(`Failed to execute Qoder ${options.operation} with status ${response.status}.`, response);
1363
+ throw qoderHttpError(`Failed to execute Qoder ${options.operation} with status ${response.status}.`, response, text);
1289
1364
  }
1290
1365
  let data;
1291
1366
  try {
@@ -1323,6 +1398,14 @@ async function waitForFlight(promise, signal) {
1323
1398
  var QoderAuthService = class {
1324
1399
  cache = /* @__PURE__ */ new Map();
1325
1400
  inFlight = /* @__PURE__ */ new Map();
1401
+ /**
1402
+ * Self-heal exchanges in flight, keyed like the cache. Deliberately separate
1403
+ * from {@link inFlight}: that flight may be aborted by a departing
1404
+ * concurrent waiter (a quota poll, a catalog sweep) whose abort has nothing
1405
+ * to do with the heal, so healers must not share its controller. Concurrent
1406
+ * heals instead share this flight — see {@link exchangeFresh}.
1407
+ */
1408
+ healFlights = /* @__PURE__ */ new Map();
1326
1409
  fetchImpl;
1327
1410
  timeoutMs;
1328
1411
  resolveMachineId;
@@ -1345,10 +1428,31 @@ var QoderAuthService = class {
1345
1428
  * (the quota poll, the catalog sweep), and a retry that joins it would then
1346
1429
  * be cancelled by a path that has nothing to do with the chat. The result
1347
1430
  * replaces the cache entry.
1431
+ *
1432
+ * Two heals for the same credential that overlap share one exchange instead
1433
+ * of racing: each racer's `clear` deleted what the previous one had just
1434
+ * cached, and its own `cache.set` overwrote the other's — so the cache could
1435
+ * end up naming a token the losing request was no longer using, and with an
1436
+ * upstream that retires the previous job token on a new exchange the racers
1437
+ * also invalidated each other's credentials (one chat recovering while a
1438
+ * concurrent one failed, exactly the 2026-09-26 22:36 observation). The
1439
+ * shared exchange runs on its own timeout signal, so one healer's abort —
1440
+ * its chat request went away — cannot cancel an exchange other healers, and
1441
+ * the future `getCredentials` callers about to read the refreshed cache, are
1442
+ * waiting on.
1348
1443
  */
1349
1444
  async exchangeFresh(pat, signal) {
1445
+ const cacheKey = `${this.region}:${opaqueCredentialKey(pat)}`;
1446
+ const existing = this.healFlights.get(cacheKey);
1447
+ if (existing !== void 0 && !existing.settled) return waitForFlight(existing.promise, signal);
1350
1448
  this.clear(pat);
1351
- return await this.exchangeAndResolve(pat, signal ?? AbortSignal.timeout(this.timeoutMs));
1449
+ const created = { settled: false };
1450
+ created.promise = this.exchangeAndResolve(pat, AbortSignal.timeout(this.timeoutMs)).finally(() => {
1451
+ created.settled = true;
1452
+ if (this.healFlights.get(cacheKey) === created) this.healFlights.delete(cacheKey);
1453
+ });
1454
+ this.healFlights.set(cacheKey, created);
1455
+ return waitForFlight(created.promise, signal);
1352
1456
  }
1353
1457
  async getCredentials(pat, signal) {
1354
1458
  if (!pat || typeof pat !== "string") throw new QoderLlmError("Qoder Personal Access Token is missing or invalid. Configure Qoder in the Qoder settings page.", "MISSING_CREDENTIAL");
@@ -2784,7 +2888,7 @@ function parseEnvelope(rawData) {
2784
2888
  if (envelope.statusCodeValue !== void 0 && (!Number.isInteger(envelope.statusCodeValue) || envelope.statusCodeValue < 100 || envelope.statusCodeValue > 599)) throw malformed("Qoder SSE envelope has an invalid status code.");
2785
2889
  if (envelope.statusCodeValue !== void 0 && envelope.statusCodeValue !== 200) {
2786
2890
  const status = envelope.statusCodeValue;
2787
- throw qoderHttpError(envelope.body ? `Qoder service returned upstream error status ${status}: ${envelope.body}` : `Qoder service returned upstream error status ${status}.`, { status });
2891
+ throw qoderHttpError(envelope.body ? `Qoder service returned upstream error status ${status}: ${envelope.body}` : `Qoder service returned upstream error status ${status}.`, { status }, envelope.body);
2788
2892
  }
2789
2893
  if (envelope.body !== void 0 && typeof envelope.body !== "string") throw malformed("Qoder SSE envelope has an invalid model body.");
2790
2894
  return envelope;
@@ -3119,7 +3223,15 @@ async function* streamQoderChat(options, model, credentials, messages, dependenc
3119
3223
  ...reqId === void 0 ? {} : { requestId: reqId }
3120
3224
  });
3121
3225
  resetIdleTimer();
3122
- if (!response.ok) throw qoderHttpError(`Qoder upstream service returned HTTP ${response.status}.`, response);
3226
+ if (!response.ok) {
3227
+ let errorBody;
3228
+ try {
3229
+ errorBody = await readLimitedText(response, defaultMaxErrorBytes, "Qoder chat error response");
3230
+ } catch {
3231
+ errorBody = void 0;
3232
+ }
3233
+ throw qoderHttpError(`Qoder upstream service returned HTTP ${response.status}.`, response, errorBody);
3234
+ }
3123
3235
  if (!response.body) throw new QoderLlmError("Qoder response contains no readable body stream.", "EMPTY_RESPONSE");
3124
3236
  let firstChunkDurationMs;
3125
3237
  let tokenUsage;
@@ -3407,9 +3519,11 @@ var DefaultQoderTransport = class {
3407
3519
  this.logger?.warn?.("[Qoder Stream] Chat rejected as unauthorized; exchanging a fresh job token and retrying once", { status: error.failure.status });
3408
3520
  }
3409
3521
  let lastRejection;
3522
+ let noticeAt;
3410
3523
  for (let round = 0; round < 2; round++) {
3411
3524
  const refreshed = await this.auth.exchangeFresh(pat, options.signal);
3412
3525
  this.pendingRefreshAt = Date.now();
3526
+ noticeAt = this.pendingRefreshAt;
3413
3527
  try {
3414
3528
  yield* this.streamChat(options, model, refreshed, messages, () => {
3415
3529
  this.onChatAccepted();
@@ -3433,7 +3547,7 @@ var DefaultQoderTransport = class {
3433
3547
  }
3434
3548
  }
3435
3549
  }
3436
- this.discardPendingRefreshNotice();
3550
+ if (this.pendingRefreshAt === noticeAt) this.discardPendingRefreshNotice();
3437
3551
  if (!this.refreshFailureAnnounced) {
3438
3552
  this.refreshFailureAnnounced = true;
3439
3553
  this.logger?.warn?.("[Qoder Stream] Job token refresh did not recover the chat; the upstream still rejects it", { status: lastRejection instanceof LlmError ? lastRejection.failure.status : void 0 });
@@ -3509,6 +3623,7 @@ function normalizeCredits(credits) {
3509
3623
  */
3510
3624
  function classifyUpstreamError(status, body) {
3511
3625
  const lower = body.toLowerCase();
3626
+ if (qoderQueueSignal(body) !== void 0) return "soft_rate";
3512
3627
  if (status === 402) return "quota_exceeded";
3513
3628
  if (status === 429) return "soft_rate";
3514
3629
  if (status === 401 || status === 403) return "auth";
@@ -3542,11 +3657,18 @@ function classifyUpstreamError(status, body) {
3542
3657
  * The transport's taxonomy (`src/qoder/errors.ts`) codes an HTTP 402 as
3543
3658
  * `INVALID_REQUEST`, so the status gets the last say for the quota and auth
3544
3659
  * families before the code-based defaults apply.
3660
+ *
3661
+ * `RATE_LIMIT` is tested before the auth family because the upstream's queue
3662
+ * answer keeps its 401/403 status: a saturated qfmodel queue reads
3663
+ * `code: RATE_LIMIT, status: 403` (body carries 10605/isQueued/
3664
+ * retryAfterSeconds), and letting the status arm claim it reported a dead
3665
+ * credential — which the host never retries — instead of a throttle it would.
3545
3666
  */
3546
3667
  function kindFromQoderFailure(failure) {
3547
3668
  if (failure.code === "MISSING_CREDENTIAL") return "missing_credential";
3669
+ if (failure.code === "RATE_LIMIT") return "soft_rate";
3548
3670
  if (failure.code === "AUTH" || failure.status === 401 || failure.status === 403) return "auth";
3549
- if (failure.code === "RATE_LIMIT" || failure.status === 429) return "soft_rate";
3671
+ if (failure.status === 429) return "soft_rate";
3550
3672
  if (failure.code === "QUOTA" || failure.status === 402) return "quota_exceeded";
3551
3673
  if (failure.code.startsWith("INVALID_") || failure.code.startsWith("UNSUPPORTED_") || failure.code === "ATTACHMENT") return "client";
3552
3674
  return "server";
@@ -4266,7 +4388,7 @@ const DEFAULT_MAX_TOKENS = 32768;
4266
4388
  const DEFAULT_CONTEXT_WINDOW = 18e4;
4267
4389
  //#endregion
4268
4390
  //#region src/version.ts
4269
- const QODER_CONNECT_VERSION = "0.1.8";
4391
+ const QODER_CONNECT_VERSION = "0.2.0";
4270
4392
  //#endregion
4271
4393
  //#region src/host-heartbeat.ts
4272
4394
  /**
@@ -4463,4 +4585,4 @@ function variantFor(id) {
4463
4585
  return QODER_VARIANTS.find((variant) => variant.id === id);
4464
4586
  }
4465
4587
  //#endregion
4466
- export { QODER_GLOBAL_AUTH_PATH as A, qoderCredentialIdentity as B, probeModel as C, FALLBACK_QODER_MODELS as D, qoderCatalogPath as E, QODER_AUTH_FILENAME as F, qoderPluginDataDir as G, QODER_DATA_DIR_ENV as H, QODER_PAT_ENV_CN as I, qoderStateDir as K, QODER_PAT_ENV_GLOBAL as L, QODER_GLOBAL_STATUS_PATH as M, QODER_PROBE_PATH as N, QoderCatalog as O, QODER_STATUS_PATH as P, QoderCredentialStore as R, PROBE_EFFORT_CANDIDATES as S, QoderCatalogStore as T, QODER_DATA_DIR_NAME as U, qoderOwnAuthPath as V, qoderMachineIdPath as W, modelInfoOf as _, QODER_HOST_HEARTBEAT_FILENAME as a, createQoderTransport as b, processStartTimeMs as c, writeHostHeartbeat as d, QODER_CONNECT_VERSION as f, kindFromQoderFailure as g, classifyUpstreamError as h, variantFor as i, QODER_GLOBAL_PROBE_PATH as j, QODER_AUTH_PATH as k, qoderHostHeartbeatPath as l, QoderUpstreamClient as m, GLOBAL_VARIANT as n, clearHostHeartbeat as o, KIND_STATUS as p, QODER_VARIANTS as r, isHeartbeatProcessAlive as s, CHINA_VARIANT as t, readHostHeartbeat as u, normalizeCredits as v, randomSentinel as w, getMachineId as x, validateApiKey as y, patTail as z };
4588
+ export { QODER_GLOBAL_AUTH_PATH as A, patTail as B, probeModel as C, FALLBACK_QODER_MODELS as D, qoderCatalogPath as E, QODER_STATUS_PATH as F, qoderMachineIdPath as G, qoderOwnAuthPath as H, QODER_AUTH_FILENAME as I, qoderPluginDataDir as K, QODER_PAT_ENV_CN as L, QODER_GLOBAL_STATUS_PATH as M, QODER_PROBE_PATH as N, QoderCatalog as O, QODER_SETTINGS_FACE_PATH as P, QODER_PAT_ENV_GLOBAL as R, PROBE_EFFORT_CANDIDATES as S, QoderCatalogStore as T, QODER_DATA_DIR_ENV as U, qoderCredentialIdentity as V, QODER_DATA_DIR_NAME as W, modelInfoOf as _, QODER_HOST_HEARTBEAT_FILENAME as a, createQoderTransport as b, processStartTimeMs as c, writeHostHeartbeat as d, QODER_CONNECT_VERSION as f, kindFromQoderFailure as g, classifyUpstreamError as h, variantFor as i, QODER_GLOBAL_PROBE_PATH as j, QODER_AUTH_PATH as k, qoderHostHeartbeatPath as l, QoderUpstreamClient as m, GLOBAL_VARIANT as n, clearHostHeartbeat as o, KIND_STATUS as p, qoderStateDir as q, QODER_VARIANTS as r, isHeartbeatProcessAlive as s, CHINA_VARIANT as t, readHostHeartbeat as u, normalizeCredits as v, randomSentinel as w, getMachineId as x, validateApiKey as y, QoderCredentialStore as z };
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Qoder Connector",
4
+ "description": "Connect Qoder CLI's model service: per-region model toggles, context windows, quota lookup, and daily check-in."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Qoder 连接器",
4
+ "description": "接入 Qoder CLI 的模型服务:双区域模型开关、上下文窗口、额度查询与每日签到。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dsh-qoder-connect",
3
3
  "displayName": "DSH Qoder Connect",
4
4
  "description": "将 Qoder(国内版 / 国际版)的模型接入 DeepSeek Harness — bring Qoder (CN & international) models into DeepSeek Harness.",
5
- "version": "0.1.8",
5
+ "version": "0.2.0",
6
6
  "author": "corrinehu",
7
7
  "keywords": [
8
8
  "dsh-plugin",
@@ -13,6 +13,7 @@
13
13
  ],
14
14
  "type": "module",
15
15
  "engines": {
16
+ "dsh": ">=0.1.5-rc.2",
16
17
  "node": "^22.19.0 || >=24.0.0"
17
18
  },
18
19
  "main": "lib/index.js",
@@ -35,7 +36,8 @@
35
36
  },
36
37
  "./client": "./lib/client.js",
37
38
  "./cordis.patch.yml": "./cordis.patch.yml",
38
- "./package.json": "./package.json"
39
+ "./package.json": "./package.json",
40
+ "./locale/*.json": "./locale/*.json"
39
41
  },
40
42
  "files": [
41
43
  "lib",
@@ -43,7 +45,8 @@
43
45
  "cordis.patch.yml",
44
46
  "README.md",
45
47
  "README.en.md",
46
- "LICENSE"
48
+ "LICENSE",
49
+ "locale/*.json"
47
50
  ],
48
51
  "license": "MIT",
49
52
  "repository": {
@@ -112,5 +115,9 @@
112
115
  "tsdown": "^0.22.2",
113
116
  "typescript": "^5.9.3",
114
117
  "vitest": "^4.1.8"
115
- }
118
+ },
119
+ "dependencies": {
120
+ "js-yaml": "^4.1.0"
121
+ },
122
+ "icon": "./assets/icon.svg"
116
123
  }