dsh-connect-agnes-token-plan 0.11.1 → 0.11.2

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/CHANGELOG.md CHANGED
@@ -23,6 +23,14 @@
23
23
 
24
24
  ## [Unreleased]
25
25
 
26
+ ## [0.11.2] — 2026-10-17
27
+
28
+ ### 修复:AgnesCode 写操作不再被在途的读盖回、胶囊在后台页面上不再轮询
29
+
30
+ - **修(AgnesCode 面板)**:切换「启用 AgnesCode 提供方」开关、点「检测本机登录态」、保存模型清单之后,你看到的一直是操作后的状态。此前若操作发出前还有一次例行刷新在途,它的结果(操作前的)会在最长一个 60 秒周期里把操作后的状态盖回去——面板上会闪回旧值。
31
+ - **修(用量胶囊)**:页面处于后台(不可见)时挂载的胶囊,不再在没人看的时候持续轮询后端——每次轮询都占着一条连接;页面回到前台的瞬间立即读一次并恢复节奏。
32
+ - 影响面:`src/client/{agnescode-tab,composer-usage-pill}.ts`、`client.js`、`test/dom-render.test.mjs`。
33
+
26
34
  ## [0.11.1] — 2026-10-16
27
35
 
28
36
  ### 修复:用量胶囊首读失败不再卡死到会话结束、保存不再被旧轮询盖回
package/client.js CHANGED
@@ -3188,6 +3188,10 @@ var dsh_connect_agnes_token_plan_client = (function() {
3188
3188
  setError("unable to reach the Host");
3189
3189
  }
3190
3190
  }, []);
3191
+ const retireInFlight = () => {
3192
+ generation.current += 1;
3193
+ inFlight.current?.abort?.();
3194
+ };
3191
3195
  useEffect(() => {
3192
3196
  alive.current = true;
3193
3197
  let timer = null;
@@ -3239,7 +3243,10 @@ var dsh_connect_agnes_token_plan_client = (function() {
3239
3243
  action: "switch",
3240
3244
  enabled
3241
3245
  });
3242
- if (alive.current) setState(body);
3246
+ if (alive.current) {
3247
+ retireInFlight();
3248
+ setState(body);
3249
+ }
3243
3250
  } catch (why) {
3244
3251
  if (alive.current) setNote(format(tt("agnescode.switchError"), { error: why instanceof Error ? why.message : String(why) }));
3245
3252
  }
@@ -3250,6 +3257,7 @@ var dsh_connect_agnes_token_plan_client = (function() {
3250
3257
  try {
3251
3258
  const body = await postJson(AGNESCODE_PATH, { action: "harvest" });
3252
3259
  if (alive.current) {
3260
+ retireInFlight();
3253
3261
  if (body !== null) setState(body);
3254
3262
  if (body?.ok === true) setNote(tt("agnescode.harvestOk"));
3255
3263
  else setNote(tt("agnescode.harvestFail"));
@@ -3265,7 +3273,10 @@ var dsh_connect_agnes_token_plan_client = (function() {
3265
3273
  action: "saveModels",
3266
3274
  enabledModelIds: ids
3267
3275
  });
3268
- if (alive.current) setState(body);
3276
+ if (alive.current) {
3277
+ retireInFlight();
3278
+ setState(body);
3279
+ }
3269
3280
  }, []);
3270
3281
  const enabled = state?.enabled === true;
3271
3282
  const view = agnescodeView(state, error);
@@ -3703,7 +3714,9 @@ var dsh_connect_agnes_token_plan_client = (function() {
3703
3714
  }
3704
3715
  }, []);
3705
3716
  const dataRef = useRef(null);
3706
- dataRef.current = data;
3717
+ useEffect(() => {
3718
+ dataRef.current = data;
3719
+ }, [data]);
3707
3720
  const echoLlm = useCallback((patch) => {
3708
3721
  generation.current += 1;
3709
3722
  inFlight.current?.abort?.();
@@ -4129,6 +4142,7 @@ var dsh_connect_agnes_token_plan_client = (function() {
4129
4142
  }
4130
4143
  }, [route]);
4131
4144
  useEffect(() => {
4145
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
4132
4146
  load();
4133
4147
  }, [load]);
4134
4148
  useEffect(() => {
@@ -4146,7 +4160,7 @@ var dsh_connect_agnes_token_plan_client = (function() {
4146
4160
  timer = null;
4147
4161
  }
4148
4162
  };
4149
- start();
4163
+ if (!(typeof document !== "undefined" && document.visibilityState === "hidden")) start();
4150
4164
  const onVisibility = () => {
4151
4165
  if (typeof document === "undefined") return;
4152
4166
  if (document.visibilityState === "hidden") stop();
@@ -1,4 +1,4 @@
1
- import { L as str, M as obj, T as name, i as PROBED_VISION, j as numZeroOk, k as num } from "./llm-models.js";
1
+ import { M as obj, R as str, T as name, i as PROBED_VISION, j as numZeroOk, k as num } from "./llm-models.js";
2
2
  import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import { homedir } from "node:os";
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { A as numOrNull, C as inject, D as resolveSettings, E as resolveAuthOverrides, F as redactSecrets, I as retryBounded, L as str, M as obj, N as pluginError, O as forget, P as redactError, R as verbatim, S as hostName, T as name, _ as parseUsageOverview, b as readSubscriptionExpiry, c as summarizeCatalog, d as isVideoGenModel, f as checkShape, g as parseSubscriptionUsage, h as parsePlans, j as numZeroOk, k as num, l as visionOf, m as matchCurrentPlan, n as LLM_DISPLAY_NAME, o as filterByEnabled, p as countOf, r as LLM_PROVIDER_ID, s as rosterWithAvailability, t as DEFAULT_REASONING_EFFORT, u as isImageGenModel, v as parseUsageSeries, w as isAdmitted, x as CONFIG_DEFAULTS, y as quotaWindows } from "./llm-models.js";
1
+ import { A as numOrNull, C as inject, D as resolveSettings, E as resolveAuthOverrides, F as redactSecrets, I as resolveToolInputs, L as retryBounded, M as obj, N as pluginError, O as forget, P as redactError, R as str, S as hostName, T as name, _ as parseUsageOverview, b as readSubscriptionExpiry, c as summarizeCatalog, d as isVideoGenModel, f as checkShape, g as parseSubscriptionUsage, h as parsePlans, j as numZeroOk, k as num, l as visionOf, m as matchCurrentPlan, n as LLM_DISPLAY_NAME, o as filterByEnabled, p as countOf, r as LLM_PROVIDER_ID, s as rosterWithAvailability, t as DEFAULT_REASONING_EFFORT, u as isImageGenModel, v as parseUsageSeries, w as isAdmitted, x as CONFIG_DEFAULTS, y as quotaWindows, z as verbatim } from "./llm-models.js";
2
2
  import { S as writeStateFile, _ as profileSegment, a as filterAgnescodeRows, b as stateDir, c as fetchAgnescodeBalance, d as trustAgnescodeBffBase, f as createFileCatalogStore, g as ensureStateDir, h as createStateReadCache, l as fetchAgnescodeCatalog, m as STATE_READ_TTL_MS, n as AGNESCODE_PROVIDER_ID, o as AGNESCODE_FALLBACK_MODELS, p as normalizeEnabledIds, s as decodeAgnescodeJwtExpMs, t as AGNESCODE_DISPLAY_NAME, u as harvestAgnescodeLocalSession, v as profileStateDir, x as temporaryOf, y as readStateJson } from "./agnescode-models.js";
3
3
  import { rm } from "node:fs/promises";
4
4
  import { join } from "node:path";
@@ -3595,6 +3595,58 @@ function sweepCache(cache) {
3595
3595
  for (const [key, entry] of cache) if (nowMs - entry.at > MAX_CACHE_AGE_MS) cache.delete(key);
3596
3596
  }
3597
3597
  /**
3598
+ * The cache + single-flight protocol every console fetch in this module uses.
3599
+ *
3600
+ * All three callers — {@link fetchConsole}, {@link fetchPlans},
3601
+ * {@link fetchModelCatalog} — need the same four moves in the same order, and
3602
+ * each used to spell them out: a TTL read off `cache`, a shared in-flight
3603
+ * promise per URL, the write-back, and the bounded sweep. The difference
3604
+ * between them is only what a REQUEST does (`produce`), never how an answer is
3605
+ * remembered. Keeping the protocol here means a fourth endpoint cannot pick up
3606
+ * a subtly different variant of it (the same "hand-copied rule drifts" failure
3607
+ * `switch-precedence.ts` was extracted to end).
3608
+ *
3609
+ * Two properties are load-bearing and are why `produce` is a callback rather
3610
+ * than a pre-made promise:
3611
+ *
3612
+ * 1. **Nothing is cached until the answer has been judged.** `produce` throws
3613
+ * for a refusal instead of returning one, so a transient 401 never gets
3614
+ * written down and left to stick for the whole TTL — which would leave the
3615
+ * panel reading "not signed in" long after the store had healed.
3616
+ * 2. **A failed flight leaves no trace.** The `finally` drops the in-flight
3617
+ * entry on the error path too, so the next poll genuinely retries rather
3618
+ * than awaiting a rejected promise it never saw.
3619
+ *
3620
+ * @param {string} url - the fully built request URL; also the cache and
3621
+ * in-flight key, which is what makes the de-duplication per-endpoint.
3622
+ * @param {number} cacheMs - how long a successful answer stays fresh.
3623
+ * @param {CacheMap} cache - the shared response cache.
3624
+ * @param {InflightMap} inflight - the shared in-flight map.
3625
+ * @param {() => Promise<unknown>} produce - performs the request and returns
3626
+ * the value to cache; throws to refuse, and a throw caches nothing.
3627
+ * @returns {Promise<unknown>} the cached, shared, or freshly produced value.
3628
+ */
3629
+ async function sharedFetch(url, cacheMs, cache, inflight, produce) {
3630
+ const cached = cache.get(url);
3631
+ if (cached !== void 0 && Date.now() - cached.at < cacheMs) return cached.body;
3632
+ const pending = inflight.get(url);
3633
+ if (pending !== void 0) return pending;
3634
+ const run = async () => {
3635
+ const body = await produce();
3636
+ cache.set(url, {
3637
+ body,
3638
+ at: Date.now()
3639
+ });
3640
+ sweepCache(cache);
3641
+ return body;
3642
+ };
3643
+ const flight = run().finally(() => {
3644
+ inflight.delete(url);
3645
+ });
3646
+ inflight.set(url, flight);
3647
+ return flight;
3648
+ }
3649
+ /**
3598
3650
  * Whether an envelope code means "the token is no longer good".
3599
3651
  *
3600
3652
  * Agnes reports an expired session as `code: 401` inside an HTTP 401, but the
@@ -3670,11 +3722,7 @@ function unwrapEnvelope(body, label) {
3670
3722
  async function fetchConsole(settings, path, params, cacheMs, cache, inflight, tokenStore) {
3671
3723
  const query = params && Object.keys(params).length > 0 ? `?${new URLSearchParams(params).toString()}` : "";
3672
3724
  const url = `${settings.consoleBase}${path}${query}`;
3673
- const cached = cache.get(url);
3674
- if (cached !== void 0 && Date.now() - cached.at < cacheMs) return cached.body;
3675
- const pending = inflight.get(url);
3676
- if (pending !== void 0) return pending;
3677
- const run = async () => {
3725
+ return sharedFetch(url, cacheMs, cache, inflight, async () => {
3678
3726
  /** One attempt: `{data}` on success, `{authRefused: true}` when renewing helps. */
3679
3727
  const attempt = async (token) => {
3680
3728
  const response = await fetch(url, {
@@ -3706,18 +3754,8 @@ async function fetchConsole(settings, path, params, cacheMs, cache, inflight, to
3706
3754
  error.code = CODE.JWT_EXPIRED;
3707
3755
  throw error;
3708
3756
  }
3709
- cache.set(url, {
3710
- body: result.data,
3711
- at: Date.now()
3712
- });
3713
- sweepCache(cache);
3714
3757
  return result.data;
3715
- };
3716
- const flight = run().finally(() => {
3717
- inflight.delete(url);
3718
3758
  });
3719
- inflight.set(url, flight);
3720
- return flight;
3721
3759
  }
3722
3760
  /**
3723
3761
  * Fetch the public plan catalogue — the only quota source that needs no login.
@@ -3736,29 +3774,14 @@ async function fetchConsole(settings, path, params, cacheMs, cache, inflight, to
3736
3774
  */
3737
3775
  async function fetchPlans(settings, cacheMs, cache, inflight) {
3738
3776
  const url = `${settings.consoleBase}${PLANS_PATH}`;
3739
- const cached = cache.get(url);
3740
- if (cached !== void 0 && Date.now() - cached.at < cacheMs) return cached.body;
3741
- const pending = inflight.get(url);
3742
- if (pending !== void 0) return pending;
3743
- const run = async () => {
3777
+ return sharedFetch(url, cacheMs, cache, inflight, async () => {
3744
3778
  const response = await fetch(url, {
3745
3779
  headers: { accept: "application/json" },
3746
3780
  signal: AbortSignal.timeout(settings.consoleTimeoutMs)
3747
3781
  });
3748
3782
  if (!response.ok) throw new Error(`${PLANS_PATH} returned HTTP ${response.status}`);
3749
- const body = unwrapEnvelope(await response.json(), PLANS_PATH);
3750
- cache.set(url, {
3751
- body,
3752
- at: Date.now()
3753
- });
3754
- sweepCache(cache);
3755
- return body;
3756
- };
3757
- const flight = run().finally(() => {
3758
- inflight.delete(url);
3783
+ return unwrapEnvelope(await response.json(), PLANS_PATH);
3759
3784
  });
3760
- inflight.set(url, flight);
3761
- return flight;
3762
3785
  }
3763
3786
  /**
3764
3787
  * Fetch the API-key model catalog: the models this key can actually call.
@@ -3777,11 +3800,7 @@ async function fetchPlans(settings, cacheMs, cache, inflight) {
3777
3800
  */
3778
3801
  async function fetchModelCatalog(settings, cacheMs, cache, inflight, apiKey) {
3779
3802
  const url = `${settings.apiBase}/models`;
3780
- const cached = cache.get(url);
3781
- if (cached !== void 0 && Date.now() - cached.at < cacheMs) return cached.body;
3782
- const pending = inflight.get(url);
3783
- if (pending !== void 0) return pending;
3784
- const run = async () => {
3803
+ return sharedFetch(url, cacheMs, cache, inflight, async () => {
3785
3804
  const response = await fetch(url, {
3786
3805
  headers: {
3787
3806
  authorization: `Bearer ${apiKey}`,
@@ -3791,25 +3810,14 @@ async function fetchModelCatalog(settings, cacheMs, cache, inflight, apiKey) {
3791
3810
  });
3792
3811
  if (!response.ok) throw new Error(`/v1/models returned HTTP ${response.status}`);
3793
3812
  const body = await response.json();
3794
- const models = Array.isArray(body?.data) ? body.data.map((entry) => {
3813
+ return Array.isArray(body?.data) ? body.data.map((entry) => {
3795
3814
  const source = obj(entry);
3796
3815
  return {
3797
3816
  id: str(source.id, ""),
3798
3817
  ...source
3799
3818
  };
3800
3819
  }).filter((entry) => entry.id !== "") : [];
3801
- cache.set(url, {
3802
- body: models,
3803
- at: Date.now()
3804
- });
3805
- sweepCache(cache);
3806
- return models;
3807
- };
3808
- const flight = run().finally(() => {
3809
- inflight.delete(url);
3810
3820
  });
3811
- inflight.set(url, flight);
3812
- return flight;
3813
3821
  }
3814
3822
 
3815
3823
  //#endregion
@@ -4135,15 +4143,10 @@ function defineDrawTool({ defineTool, resolveApiKey, getEntries, settings, fetch
4135
4143
  const prompt = str(params?.prompt, "").trim();
4136
4144
  if (prompt === "") throw new Error("prompt is required");
4137
4145
  if (cooldown.blocked()) throw new Error(`draw cooldown: 上一次出图失败,${Math.ceil(cooldown.remainingMs() / 1e3)}s 后再试`);
4138
- const apiKey = await resolveApiKey();
4139
- if (typeof apiKey !== "string" || apiKey.trim() === "") throw new Error("AGNES_TOKEN_PLAN_API_KEY 未配置:在面板「模型接入」粘贴 API Key(免费版 sk- 或 Token Plan cpk-),或设置该环境变量");
4140
- let picked;
4141
- try {
4142
- picked = await getEntries?.() ?? [];
4143
- } catch {
4144
- picked = [];
4145
- }
4146
- const entries = Array.isArray(picked) ? picked : [];
4146
+ const { apiKey, entries } = await resolveToolInputs({
4147
+ resolveApiKey,
4148
+ getEntries
4149
+ });
4147
4150
  const model = pickDrawModel(entries, params?.model, settings?.drawModelId);
4148
4151
  if (model === null || model === void 0) throw new Error("catalog 中没有出图模型(`output_modalities` 字段与 `agnes-image-*` 名称判定均为空):确认 Key 已配置、面板已至少轮询一次,且套餐含出图模型");
4149
4152
  const body = buildDrawBody({
@@ -4603,15 +4606,66 @@ async function applyCatalogEffects({ catalogStore, publisher, providerState, cat
4603
4606
  };
4604
4607
  }
4605
4608
  /**
4606
- * Fetch the four console sources plus the model catalog and aggregate them
4607
- * into the snapshot body the route writes.
4609
+ * Fetch the four console sources plus the model catalogue — the NETWORK half of
4610
+ * the poll, kept apart from the projection that shapes the answer.
4611
+ *
4612
+ * All five go out through the shared `cache` + `inflight` maps (a single-flight
4613
+ * per URL so concurrent polls share one call) and the `tokenStore` (so a 401
4614
+ * triggers one renewal before the call). Only the catalogue is authenticated by
4615
+ * the API key instead; a missing key degrades the model lists, not the quota —
4616
+ * resolved per poll so a key that arrives after the plugin mounted still lights
4617
+ * the lists on the next poll.
4618
+ *
4619
+ * Extracted from `buildSnapshotBody`, which is now "fetch, then shape": this was
4620
+ * the only part of it that touches the network, and the reasoning about WHY the
4621
+ * five requests share one batch belongs next to the batch rather than in the
4622
+ * middle of the field assembly. The outcomes are passed straight through —
4623
+ * `soft` has already turned every failure into `{value: null, error}`.
4624
+ *
4625
+ * @param {object} context
4626
+ * @param {Settings} context.settings - the resolved settings row.
4627
+ * @param {CacheMap} context.cache - the console-response cache (shared across polls).
4628
+ * @param {InflightMap} context.inflight - the single-flight map (shared across polls).
4629
+ * @param {object} context.tokenStore - the `createTokenStore` instance.
4630
+ * @param {object} context.apiKeyStore - the `createApiKeyStore` instance.
4631
+ * @returns {Promise<{overview: object, series: object, subscription: object, plans: object, catalog: unknown}>}
4632
+ * one `{value, error}` outcome per console source, plus the raw catalogue.
4633
+ */
4634
+ async function fetchQuotaSources({ settings, cache, inflight, tokenStore, apiKeyStore }) {
4635
+ const resolveApiKey = async () => (await apiKeyStore.resolve()).value;
4636
+ const { startDate, endDate } = usageWindow(settings.usageDays);
4637
+ const [overview, series, subscription, plans, catalog] = await Promise.all([
4638
+ soft(() => fetchConsole(settings, USAGE_OVERVIEW_PATH, void 0, settings.cacheSeconds * 1e3, cache, inflight, tokenStore)),
4639
+ soft(() => fetchConsole(settings, USAGE_SERIES_PATH, {
4640
+ range: "custom",
4641
+ start_date: startDate,
4642
+ end_date: endDate
4643
+ }, Math.max(settings.cacheSeconds, 300) * 1e3, cache, inflight, tokenStore)),
4644
+ soft(() => fetchConsole(settings, SUBSCRIPTION_PATH, void 0, settings.cacheSeconds * 1e3, cache, inflight, tokenStore)),
4645
+ soft(() => fetchPlans(settings, 36e5, cache, inflight)),
4646
+ (async () => {
4647
+ const apiKey = await resolveApiKey();
4648
+ return apiKey === "" ? null : fetchModelCatalog(settings, 36e5, cache, inflight, apiKey).catch(() => null);
4649
+ })()
4650
+ ]);
4651
+ return {
4652
+ overview,
4653
+ series,
4654
+ subscription,
4655
+ plans,
4656
+ catalog
4657
+ };
4658
+ }
4659
+ /**
4660
+ * Assemble the snapshot body the route writes, from the five sources the poll
4661
+ * fetched plus the stores' current state.
4608
4662
  *
4609
- * All five are fetched in parallel through the shared `cache` + `inflight`
4610
- * maps (a single-flight per URL so concurrent polls share one call) and the
4611
- * `tokenStore` (so a 401 triggers one renewal before the call). Only the
4612
- * catalog is authenticated by the API key instead; a missing key degrades the
4613
- * model lists, not the quota — resolved per poll so a key that arrives after
4614
- * the plugin mounted still lights the lists on the next poll.
4663
+ * The NETWORK half is {@link fetchQuotaSources}: this function starts from its
4664
+ * `{value, error}` outcomes, so every field below is either a degraded source
4665
+ * or a value read off a store, and the only side effects it performs are the
4666
+ * catalogue persist + provider republish isolated in `applyCatalogEffects`.
4667
+ * The five requests' parallelism, caching and 401 handling are that function's
4668
+ * concern, not this one's.
4615
4669
  *
4616
4670
  * @param {object} context
4617
4671
  * @param {object} context.settings - the resolved settings row.
@@ -4636,22 +4690,13 @@ async function applyCatalogEffects({ catalogStore, publisher, providerState, cat
4636
4690
  */
4637
4691
  async function buildSnapshotBody({ settings, cache, inflight, tokenStore, apiKeyStore, publisher, catalogStore, panelSwitch, drawSwitch, drawModelId, videoSwitch, videoModelId }) {
4638
4692
  const providerState = publisher.state;
4639
- const resolveApiKey = async () => (await apiKeyStore.resolve()).value;
4640
- const { startDate, endDate } = usageWindow(settings.usageDays);
4641
- const [overview, series, subscription, plans, catalog] = await Promise.all([
4642
- soft(() => fetchConsole(settings, USAGE_OVERVIEW_PATH, void 0, settings.cacheSeconds * 1e3, cache, inflight, tokenStore)),
4643
- soft(() => fetchConsole(settings, USAGE_SERIES_PATH, {
4644
- range: "custom",
4645
- start_date: startDate,
4646
- end_date: endDate
4647
- }, Math.max(settings.cacheSeconds, 300) * 1e3, cache, inflight, tokenStore)),
4648
- soft(() => fetchConsole(settings, SUBSCRIPTION_PATH, void 0, settings.cacheSeconds * 1e3, cache, inflight, tokenStore)),
4649
- soft(() => fetchPlans(settings, 36e5, cache, inflight)),
4650
- (async () => {
4651
- const apiKey = await resolveApiKey();
4652
- return apiKey === "" ? null : fetchModelCatalog(settings, 36e5, cache, inflight, apiKey).catch(() => null);
4653
- })()
4654
- ]);
4693
+ const { overview, series, subscription, plans, catalog } = await fetchQuotaSources({
4694
+ settings,
4695
+ cache,
4696
+ inflight,
4697
+ tokenStore,
4698
+ apiKeyStore
4699
+ });
4655
4700
  const quota = buildQuotaBlock(overview, series, subscription, plans);
4656
4701
  const usage = series.value === null ? null : (() => {
4657
4702
  const parsed = parseUsageSeries(series.value, settings.usageDays);
@@ -6934,15 +6979,11 @@ function defineVideoTool({ defineTool, resolveApiKey, getEntries, settings, fetc
6934
6979
  if (isDisposed()) throw new Error("Agnes video tool is no longer mounted");
6935
6980
  const prompt = str(params?.prompt, "").trim();
6936
6981
  if (prompt === "") throw new Error("prompt is required");
6937
- const apiKey = await resolveApiKey();
6938
- if (typeof apiKey !== "string" || apiKey.trim() === "") throw new Error("AGNES_TOKEN_PLAN_API_KEY 未配置:在面板「模型接入」粘贴 API Key(免费版 sk- 或 Token Plan cpk-),或设置该环境变量");
6939
- let picked;
6940
- try {
6941
- picked = await getEntries?.() ?? [];
6942
- } catch {
6943
- picked = [];
6944
- }
6945
- const model = pickVideoModel(Array.isArray(picked) ? picked : [], params?.model, settings?.videoModelId);
6982
+ const { apiKey, entries } = await resolveToolInputs({
6983
+ resolveApiKey,
6984
+ getEntries
6985
+ });
6986
+ const model = pickVideoModel(entries, params?.model, settings?.videoModelId);
6946
6987
  if (model === null) throw new Error("catalog 中没有视频模型(`output_modalities` 字段与 `agnes-video-*` 名称判定均为空):确认 Key 已配置、面板已至少轮询一次,且套餐含视频模型");
6947
6988
  if (!isVideo25Family(model)) {
6948
6989
  const twentyFiveOnly = VIDEO25_ONLY_FIELDS.filter((field) => params?.[field] !== void 0 && params?.[field] !== null);
package/lib/llm-models.js CHANGED
@@ -130,6 +130,45 @@ async function retryBounded({ attempts, delayMs, run }) {
130
130
  return false;
131
131
  }
132
132
  /**
133
+ * The shared preamble every agent tool runs before it can build a request:
134
+ * resolve the live API key, refuse when there is none, then read the model
135
+ * catalogue defensively.
136
+ *
137
+ * `defineDrawTool` and `defineVideoTool` each spelled this out, which meant the
138
+ * key-missing message — the one line a user actually reads, naming the panel
139
+ * section and both key shapes — existed twice and could be fixed once. The
140
+ * ORDER is load-bearing and is why this is a function rather than two call
141
+ * sites: the key is resolved and validated BEFORE the catalogue is touched, so
142
+ * an unconfigured deployment costs no catalogue read, and a `getEntries` that
143
+ * throws degrades the picker to empty rather than failing the tool (the catalog
144
+ * is a cache-shaped input; a stale or missing one must not kill a call that the
145
+ * caller may still be able to make with an explicit model id).
146
+ *
147
+ * What it deliberately does NOT do is anything modality-specific: draw's
148
+ * cooldown gate and video's family-dispatch checks stay with their own tools
149
+ * because those are real domain differences, not ceremony.
150
+ *
151
+ * @param {{resolveApiKey: () => Promise<string|undefined>, getEntries?: () => Promise<unknown[]>}} wiring
152
+ * the two `ToolWiring` seams the tools are mounted with.
153
+ * @returns {Promise<{apiKey: string, entries: unknown[]}>} a non-empty key and
154
+ * the catalogue entries (empty array when the read is absent or throws).
155
+ * @throws {Error} when no key is configured — the message names the fix.
156
+ */
157
+ async function resolveToolInputs(wiring) {
158
+ const apiKey = await wiring.resolveApiKey();
159
+ if (typeof apiKey !== "string" || apiKey.trim() === "") throw new Error("AGNES_TOKEN_PLAN_API_KEY 未配置:在面板「模型接入」粘贴 API Key(免费版 sk- 或 Token Plan cpk-),或设置该环境变量");
160
+ let picked;
161
+ try {
162
+ picked = await wiring.getEntries?.() ?? [];
163
+ } catch {
164
+ picked = [];
165
+ }
166
+ return {
167
+ apiKey,
168
+ entries: Array.isArray(picked) ? picked : []
169
+ };
170
+ }
171
+ /**
133
172
  * Run a promise to completion without awaiting it — and guarantee it cannot
134
173
  * become an unhandled rejection.
135
174
  *
@@ -1873,4 +1912,4 @@ function summarizeCatalog(entries) {
1873
1912
  }
1874
1913
 
1875
1914
  //#endregion
1876
- export { numOrNull as A, inject as C, resolveSettings as D, resolveAuthOverrides as E, redactSecrets as F, retryBounded as I, str as L, obj as M, pluginError as N, forget as O, redactError as P, verbatim as R, hostName as S, name as T, parseUsageOverview as _, buildDescriptors as a, readSubscriptionExpiry as b, summarizeCatalog as c, isVideoGenModel as d, checkShape as f, parseSubscriptionUsage as g, parsePlans as h, PROBED_VISION as i, numZeroOk as j, num as k, visionOf as l, matchCurrentPlan as m, LLM_DISPLAY_NAME as n, filterByEnabled as o, countOf as p, LLM_PROVIDER_ID as r, rosterWithAvailability as s, DEFAULT_REASONING_EFFORT as t, isImageGenModel as u, parseUsageSeries as v, isAdmitted as w, CONFIG_DEFAULTS as x, quotaWindows as y };
1915
+ export { numOrNull as A, inject as C, resolveSettings as D, resolveAuthOverrides as E, redactSecrets as F, resolveToolInputs as I, retryBounded as L, obj as M, pluginError as N, forget as O, redactError as P, str as R, hostName as S, name as T, parseUsageOverview as _, buildDescriptors as a, readSubscriptionExpiry as b, summarizeCatalog as c, isVideoGenModel as d, checkShape as f, parseSubscriptionUsage as g, parsePlans as h, PROBED_VISION as i, numZeroOk as j, num as k, visionOf as l, matchCurrentPlan as m, LLM_DISPLAY_NAME as n, filterByEnabled as o, countOf as p, LLM_PROVIDER_ID as r, rosterWithAvailability as s, DEFAULT_REASONING_EFFORT as t, isImageGenModel as u, parseUsageSeries as v, isAdmitted as w, CONFIG_DEFAULTS as x, quotaWindows as y, verbatim as z };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dsh-connect-agnes-token-plan",
3
3
  "displayName": "DSH Connect Agnes Token Plan",
4
4
  "icon": "./icon.svg",
5
- "version": "0.11.1",
5
+ "version": "0.11.2",
6
6
  "description": "Agnes connect bundle for the Harness Web UI: Token Plan quota panel (account-wide quota windows, cumulative usage, console sign-in) plus optional provider registration that brings Agnes models into DSH conversation, with host-side rate-limit self-healing.",
7
7
  "keywords": [
8
8
  "dsh-plugin",