dsh-connect-agnes-token-plan 0.13.1 → 0.13.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/CHANGELOG.md +19 -0
- package/README.md +7 -20
- package/client.js +41 -4
- package/docs/ADR.md +21 -0
- package/docs/API.md +3 -3
- package/docs/DSH-PLUGIN.md +1 -1
- package/docs/PROVIDER-HOT-RELOAD.md +17 -3
- package/docs/README.md +1 -0
- package/docs/SETUP.md +11 -0
- package/docs/TOOL-HOT-RELOAD.md +67 -0
- package/docs/TROUBLESHOOTING.md +1 -1
- package/lib/agnescode-stats-store.js +66 -19
- package/lib/index.js +337 -65
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -23,6 +23,25 @@
|
|
|
23
23
|
|
|
24
24
|
## [Unreleased]
|
|
25
25
|
|
|
26
|
+
## [0.13.3] — 2026-10-26
|
|
27
|
+
|
|
28
|
+
本版改一件事:**出图 / 视频工具开关即时生效**——工具本体就地注册 / 撤下,不再等下一次 Host 启动。
|
|
29
|
+
|
|
30
|
+
### 变更:出图 / 视频工具随开关即时注册 / 撤下
|
|
31
|
+
|
|
32
|
+
- **改(接入 API tab)**:出图与视频工具的开关、勾选与模型偏好,此前对工具本体的生效要等**下一次 Host 启动**(或重装插件)。现在翻一次开关就地把 `agnes_draw_image` / `agnes_video_generate` 注册或撤下,改模型偏好即时换模型;无需重启 `dsh web`、无需重装插件。
|
|
33
|
+
- **说明**:宿主不提供卸载句柄时与旧版行为一致(工具对本进程粘性,保持首次注册值);面板上开关状态的读写始终即时。机制与边界见 `docs/TOOL-HOT-RELOAD.md`。
|
|
34
|
+
|
|
35
|
+
## [0.13.2] — 2026-10-23
|
|
36
|
+
|
|
37
|
+
本版两件事:**网关新上的模型默认就能发图**,以及**余额胶囊上会多一段「· N 新模型」**。两处都只影响 AgnesCode(桌面端)这条线。
|
|
38
|
+
|
|
39
|
+
### 变更:新模型默认能发图,胶囊会告诉你网关多了哪个模型
|
|
40
|
+
|
|
41
|
+
- **改(识图)**:网关新上的模型不再默认被当成纯文本。旧版只认「官方文档表里写过的」那几个,表里没有的模型就不给图片入口——看不到任何提示,只能自己发现。现在没证据的模型**默认放开图片输入**,面板把它标成「可看图·未实测」;确认看不见图的模型才被收回去(记在一份否决清单里,带实测日期)。
|
|
42
|
+
- **新(胶囊)**:小浣熊/AgnesCode 的余额胶囊上会多一段「· N 新模型」。它比的是插件自己记的「这台机器见过哪些模型」,所以只在网关真的新上了模型时出现。新模型名字在 tooltip 和展开卡片里,最多列三个。标记保持 7 天,不弹窗、不需要点确认;装上新版后的第一次运行会把当时的目录当作「早就见过」,不会把整张表报成新的。
|
|
43
|
+
- **说明**:两处都只影响 AgnesCode 这条线,商汤 Token Plan 侧的面板与胶囊不变。
|
|
44
|
+
|
|
26
45
|
## [0.13.1] — 2026-10-22
|
|
27
46
|
|
|
28
47
|
本版只改一件事:**插件卡的标题**。中文叫「Agnes 模型接入」,英文叫「Agnes Model Access」,与「商汤模型接入」「魔搭模型接入」同构。
|
package/README.md
CHANGED
|
@@ -2,20 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
<p align="center"><b>把 Agnes(爱思)接进 DSH 的 Plugins 页插件卡,并在会话输入框常驻一枚用量胶囊。</b></p>
|
|
4
4
|
|
|
5
|
-
<p align="center">
|
|
6
|
-
<a href="#安装">安装</a> ·
|
|
7
|
-
<a href="#它是怎么工作的">工作原理</a> ·
|
|
8
|
-
<a href="CHANGELOG.md">更新日志</a> ·
|
|
9
|
-
<a href="https://github.com/eghrhegpe/dsh-connect-agnes-token-plan/issues">问题反馈</a>
|
|
10
|
-
</p>
|
|
5
|
+
<p align="center"><a href="#安装">安装</a> · <a href="#它是怎么工作的">工作原理</a> · <a href="CHANGELOG.md">更新日志</a> · <a href="https://github.com/eghrhegpe/dsh-connect-agnes-token-plan/issues">问题反馈</a></p>
|
|
11
6
|
|
|
12
7
|
<p align="center">
|
|
13
|
-
<a href="https://www.npmjs.com/package/dsh-connect-agnes-token-plan"><img src="https://img.shields.io/npm/v/dsh-connect-agnes-token-plan?style=flat-square&label=npm&color=cb3837" alt="npm version"></a>
|
|
14
|
-
<a href="https://
|
|
15
|
-
<a href="https://github.com/eghrhegpe/dsh-connect-agnes-token-plan/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/eghrhegpe/dsh-connect-agnes-token-plan/ci.yml?branch=main&style=flat-square&label=tests" alt="test status"></a>
|
|
16
|
-
<a href="LICENSE"><img src="https://img.shields.io/github/license/eghrhegpe/dsh-connect-agnes-token-plan?style=flat-square" alt="MIT license"></a>
|
|
17
|
-
<a href="https://github.com/eghrhegpe/dsh-connect-agnes-token-plan/stargazers"><img src="https://img.shields.io/github/stars/eghrhegpe/dsh-connect-agnes-token-plan?style=flat-square" alt="GitHub stars"></a>
|
|
18
|
-
<a href="https://dshfind.com/plugins/eghrhegpe/dsh-connect-agnes-token-plan"><img src="https://dshfind.com/api/badge/eghrhegpe/dsh-connect-agnes-token-plan" alt="dshfind plugin"></a>
|
|
8
|
+
<a href="https://www.npmjs.com/package/dsh-connect-agnes-token-plan"><img src="https://img.shields.io/npm/v/dsh-connect-agnes-token-plan?style=flat-square&label=npm&color=cb3837" alt="npm version"></a> <a href="https://www.npmjs.com/package/dsh-connect-agnes-token-plan"><img src="https://img.shields.io/npm/d18m/dsh-connect-agnes-token-plan?style=flat-square&label=downloads&color=cb3837" alt="npm downloads"></a> <a href="https://github.com/eghrhegpe/dsh-connect-agnes-token-plan/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/eghrhegpe/dsh-connect-agnes-token-plan/ci.yml?branch=main&style=flat-square&label=tests" alt="test status"></a>
|
|
9
|
+
<a href="LICENSE"><img src="https://img.shields.io/github/license/eghrhegpe/dsh-connect-agnes-token-plan?style=flat-square" alt="MIT license"></a> <a href="https://github.com/eghrhegpe/dsh-connect-agnes-token-plan/stargazers"><img src="https://img.shields.io/github/stars/eghrhegpe/dsh-connect-agnes-token-plan?style=flat-square" alt="GitHub stars"></a> <a href="https://dshfind.com/plugins/eghrhegpe/dsh-connect-agnes-token-plan"><img src="https://dshfind.com/api/badge/eghrhegpe/dsh-connect-agnes-token-plan" alt="dshfind plugin"></a>
|
|
19
10
|
</p>
|
|
20
11
|
|
|
21
12
|
把 Agnes 接进 DSH:**Plugins 页**的插件卡(三个 tab),外加**会话输入框右侧的一枚用量胶囊**。**DSH** 即 **DeepSeek Harness**;**Agnes**(爱思)是全模态大模型平台,本插件对接其**控制台 API** 与**推理 API**。
|
|
@@ -55,7 +46,7 @@
|
|
|
55
46
|
- **语言模型**:显示当前 Key 实际能调哪些模型,其中哪些能看图(按平台 `input_modalities` 判定,不靠名字猜)。
|
|
56
47
|
- **出图工具**:Host 给 agent 注册 `agnes_draw_image` ,出图模型按 catalog 的 `output_modalities` 结构化判定。
|
|
57
48
|
- **视频工具**:Host 给 agent 注册 `agnes_video_generate`,按选中模型分派 V2.0 / 2.5 两套互斥参数。
|
|
58
|
-
-
|
|
49
|
+
- 开关、勾选与工具挂载都热生效,无需重启:翻一次开关,出图 / 视频工具就地注册或撤下(0.13.x 起;宿主不给卸载句柄时工具对本进程粘性,见 [docs/TOOL-HOT-RELOAD.md](docs/TOOL-HOT-RELOAD.md))。细节见 [docs/SETUP.md](docs/SETUP.md) §3、[docs/PROVIDER-HOT-RELOAD.md](docs/PROVIDER-HOT-RELOAD.md) 与 [docs/AGNES-API.md](docs/AGNES-API.md) §7.5。
|
|
59
50
|
|
|
60
51
|

|
|
61
52
|
|
|
@@ -84,7 +75,8 @@
|
|
|
84
75
|
|
|
85
76
|
- 读数与面板**同源**:控制台百分比优先用平台自己报的 `usage_pct`、缺失才回落 `已用 / 上限`,**不做钳制**(超额窗口就显示 >100%,那是事实);桌面端画的是**存量**,所以**不造百分比**——网关只公布积分,没有「上限」可以当分母;
|
|
86
77
|
- 两条上游**互不借用**:`agnescode` 的会话只读它自己的路由、绝不画控制台窗口,反之亦然;切到**别的 provider**(其它插件)的模型时胶囊直接消失,也不再发任何请求;
|
|
87
|
-
- 读不到时**保留上一次数字并标「上次数据」**,不会清零或伪装成 0%;余额里的 `0` 是「烧完了」的读数(红色告警),`—`
|
|
78
|
+
- 读不到时**保留上一次数字并标「上次数据」**,不会清零或伪装成 0%;余额里的 `0` 是「烧完了」的读数(红色告警),`—` 才是「这次没读到」——这个区别是刻意的;
|
|
79
|
+
- 网关新上了模型时,余额胶囊会多一段**「· N 新模型」**(名字在 tooltip 与展开卡里,最多三个,保留 7 天)。这里的「新」指**这台机器第一次见到它**,不是上游的上架时间——网关目录没有这个字段。装上新版后的第一次运行只把当时的目录记下来当基线,不会把整张表报成新的。
|
|
88
80
|
|
|
89
81
|

|
|
90
82
|
|
|
@@ -118,12 +110,7 @@
|
|
|
118
110
|
|
|
119
111
|
## 运维诊断:这台机器现在挂没挂 provider?
|
|
120
112
|
|
|
121
|
-
provider /
|
|
122
|
-
|
|
123
|
-
```powershell
|
|
124
|
-
npm run doctor # 人读:每个 profile 的 provider / draw / video / agnescode 开关与模型清单
|
|
125
|
-
npm run doctor:json # 机器读:JSON(可进你的巡检 / 工单脚本)
|
|
126
|
-
```
|
|
113
|
+
provider / 出图 / 视频 / AgnesCode 开关的生效值存在插件私有状态文件里(`$DSH_HOME/state/<name>/`),不在任何配置或路由上;`npm run doctor` 只读状态文件、不碰凭据,Host 没起也能跑(人读 / 机器读两个口径见 [docs/SETUP.md](docs/SETUP.md) §6.1)。
|
|
127
114
|
|
|
128
115
|
## 文档
|
|
129
116
|
|
package/client.js
CHANGED
|
@@ -264,6 +264,12 @@ var dsh_connect_agnes_token_plan_client = (function() {
|
|
|
264
264
|
"agnescode.models": "模型({count})",
|
|
265
265
|
"agnescode.fallbackRoster": "未取到实时目录,以下为内置清单,倍率截至 {asOf}。",
|
|
266
266
|
"agnescode.memberOnly": "会员",
|
|
267
|
+
"agnescode.visionAssumed": "可看图·未实测",
|
|
268
|
+
"agnescode.visionAssumedTitle": "网关没声明这个模型能否看图,插件默认放开图片入口(未实测)。真发过去模型说看不到,就把它的 id 记进否决清单。",
|
|
269
|
+
"agnescd.newModels": "新模型",
|
|
270
|
+
"agnescd.newModelsRow": "本机新见",
|
|
271
|
+
"agnescd.newModelsTitle": "本机首次见到的模型:",
|
|
272
|
+
"agnescd.newModelsSep": "、",
|
|
267
273
|
"agnescode.rateTitle": "平台公布的积分消耗倍率(0 为免费)",
|
|
268
274
|
"agnescode.registeredPill": "已注册",
|
|
269
275
|
"agnescode.rosterHint": "勾选决定哪些模型推送进 DSH 模型列表,改动即时生效。",
|
|
@@ -524,6 +530,12 @@ var dsh_connect_agnes_token_plan_client = (function() {
|
|
|
524
530
|
"agnescode.models": "Models ({count})",
|
|
525
531
|
"agnescode.fallbackRoster": "Live catalogue unavailable — showing the built-in list, rates as of {asOf}.",
|
|
526
532
|
"agnescode.memberOnly": "member",
|
|
533
|
+
"agnescode.visionAssumed": "vision (unverified)",
|
|
534
|
+
"agnescode.visionAssumedTitle": "The gateway did not declare whether this model sees images, so picture input is allowed by default and this is unverified. If the model answers it cannot see the image, add its id to the vision denylist.",
|
|
535
|
+
"agnescd.newModels": "new",
|
|
536
|
+
"agnescd.newModelsRow": "New here",
|
|
537
|
+
"agnescd.newModelsTitle": "First seen on this machine: ",
|
|
538
|
+
"agnescd.newModelsSep": ", ",
|
|
527
539
|
"agnescode.rateTitle": "Credit multiplier as published by the platform (0 = free)",
|
|
528
540
|
"agnescode.registeredPill": "Registered",
|
|
529
541
|
"agnescode.rosterHint": "Ticking decides which models are pushed into DSH's model list; the change takes effect immediately.",
|
|
@@ -1562,6 +1574,10 @@ var dsh_connect_agnes_token_plan_client = (function() {
|
|
|
1562
1574
|
},
|
|
1563
1575
|
pillReading: { fontVariantNumeric: "tabular-nums" },
|
|
1564
1576
|
pillStale: { color: "var(--dsw-alias-label-secondary)" },
|
|
1577
|
+
pillNewTag: {
|
|
1578
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
1579
|
+
flexShrink: 0
|
|
1580
|
+
},
|
|
1565
1581
|
pillErrorText: { color: "var(--dsw-alias-state-error-primary)" },
|
|
1566
1582
|
pillTierOk: { background: "var(--dsw-alias-state-success-primary, #22c55e)" },
|
|
1567
1583
|
pillTierWarn: { background: "var(--dsw-alias-state-warn-primary, #f59e0b)" },
|
|
@@ -2542,8 +2558,9 @@ var dsh_connect_agnes_token_plan_client = (function() {
|
|
|
2542
2558
|
/**
|
|
2543
2559
|
* The shared body of an agent-tool card (`DrawSwitch`, `VideoSwitch`).
|
|
2544
2560
|
*
|
|
2545
|
-
* Both tools are mounted by the SAME Host ladder (`
|
|
2546
|
-
* `lifecycle.ts
|
|
2561
|
+
* Both tools are mounted by the SAME Host ladder (`createToolMount` in
|
|
2562
|
+
* `lifecycle.ts`, each a live handle the switch route re-syncs on every
|
|
2563
|
+
* successful write) and both routes are served by the SAME handler shape
|
|
2547
2564
|
* (`registerToolSwitchRoute` in `routes.ts`), so their cards differ only in
|
|
2548
2565
|
* which route they post to, which `llm.*` fields they read, and which
|
|
2549
2566
|
* dictionary prefix labels them. Writing the card once is what keeps the two
|
|
@@ -4050,7 +4067,10 @@ var dsh_connect_agnes_token_plan_client = (function() {
|
|
|
4050
4067
|
}), h("span", { style: S.spacer }), promo === null ? null : h("span", { style: S.modelPromo }, promo), rate === null ? null : h("span", {
|
|
4051
4068
|
style: S.modelRate,
|
|
4052
4069
|
title: tt("agnescode.rateTitle")
|
|
4053
|
-
}, `×${rate}`), row
|
|
4070
|
+
}, `×${rate}`), row?.vision === true ? h("span", {
|
|
4071
|
+
style: S.modelBadge,
|
|
4072
|
+
title: row?.visionSource === "assumed" ? tt("agnescode.visionAssumedTitle") : void 0
|
|
4073
|
+
}, row?.visionSource === "assumed" ? tt("agnescode.visionAssumed") : tt("llm.rosterVision")) : null, row.memberOnly === true ? h("span", { style: S.modelBadge }, tt("agnescode.memberOnly")) : null), meta === "" ? null : h("div", { style: S.modelMeta }, meta));
|
|
4054
4074
|
})));
|
|
4055
4075
|
}
|
|
4056
4076
|
/**
|
|
@@ -4665,10 +4685,17 @@ var dsh_connect_agnes_token_plan_client = (function() {
|
|
|
4665
4685
|
const permanent = partOf(state, "permanentBalance");
|
|
4666
4686
|
const split = timeLimited === null || permanent === null ? null : `${tt("agnescode.balanceTimeLabel")} ${count(timeLimited)} · ${tt("agnescode.balancePermanentLabel")} ${count(permanent)}`;
|
|
4667
4687
|
const usageText = agnescodeUsageText(state?.usage ?? null, tt);
|
|
4688
|
+
const newIds = (Array.isArray(state?.newModelIds) ? state.newModelIds : []).filter((id) => typeof id === "string" && id !== "");
|
|
4689
|
+
const shownNames = newIds.map((id) => {
|
|
4690
|
+
const row = state?.models?.find((model) => model.id === id);
|
|
4691
|
+
return row?.name ?? row?.id ?? id;
|
|
4692
|
+
}).slice(0, 3);
|
|
4693
|
+
const newModelTitle = newIds.length === 0 ? null : `${tt("agnescd.newModelsTitle")}${shownNames.join(tt("agnescd.newModelsSep"))}${newIds.length > shownNames.length ? "…" : ""}`;
|
|
4668
4694
|
const title = [
|
|
4669
4695
|
tt("composer.balanceTitle"),
|
|
4670
4696
|
split,
|
|
4671
4697
|
why,
|
|
4698
|
+
newModelTitle,
|
|
4672
4699
|
stale ? tt("composer.stale") : null
|
|
4673
4700
|
].filter(Boolean).join(" · ");
|
|
4674
4701
|
const { popoverId, open, setOpen, rootRef, cardRef, triggerRef } = usePillPopover();
|
|
@@ -4693,6 +4720,10 @@ var dsh_connect_agnes_token_plan_client = (function() {
|
|
|
4693
4720
|
key: "why",
|
|
4694
4721
|
style: S.pillPopMuted
|
|
4695
4722
|
}, why) : null,
|
|
4723
|
+
newIds.length > 0 ? h("div", {
|
|
4724
|
+
key: "new-models",
|
|
4725
|
+
style: S.pillPopMuted
|
|
4726
|
+
}, `${tt("agnescd.newModelsRow")} ${shownNames.join(tt("agnescd.newModelsSep"))}${newIds.length > shownNames.length ? "…" : ""}`) : null,
|
|
4696
4727
|
stale ? h("div", {
|
|
4697
4728
|
key: "stale",
|
|
4698
4729
|
style: S.pillPopMuted
|
|
@@ -4716,7 +4747,13 @@ var dsh_connect_agnes_token_plan_client = (function() {
|
|
|
4716
4747
|
popoverOpen: open,
|
|
4717
4748
|
popoverId,
|
|
4718
4749
|
buttonRef: triggerRef,
|
|
4719
|
-
children: h("span", {
|
|
4750
|
+
children: [h("span", {
|
|
4751
|
+
key: "reading",
|
|
4752
|
+
style: S.pillReading
|
|
4753
|
+
}, pending ? "…" : balance === null ? "—" : `${count(balance)} ${tt("composer.balanceUnit")}`), newIds.length === 0 ? null : h("span", {
|
|
4754
|
+
key: "new-models",
|
|
4755
|
+
style: S.pillNewTag
|
|
4756
|
+
}, ` · ${newIds.length} ${tt("agnescd.newModels")}`)]
|
|
4720
4757
|
}), open ? h("div", {
|
|
4721
4758
|
ref: cardRef,
|
|
4722
4759
|
id: popoverId,
|
package/docs/ADR.md
CHANGED
|
@@ -206,3 +206,24 @@
|
|
|
206
206
|
- **受影响的文档**:`src/host/agnescode-models.ts`(`agnescodeThinkingLevelMap(id)` per-model + 模块头决策 1)、`test/agnescode.test.mjs`(新增 agnes-2.5-pro 断言块)、`docs/ROADMAP.md` §6.3.1(新增 2026-10-18 探针行)、`CHANGELOG.md`([Unreleased])。ADR-009 原文按考古纪律保留不改写。
|
|
207
207
|
- **与既有条目的关系**:细化 ADR-009 的档位映射(该裁定以 3.0-flash 探针为基础,未覆盖 2.5-pro 的阶梯差异);不改变 `docs/DSH-LLM-DEVELOP.md` §4 的通用裁法(Token Plan 侧 `thinkingLevelMapFor` 未动)。
|
|
208
208
|
|
|
209
|
+
## ADR-018 agnescode 识图默认放行:没证据的行也收图,但面板必须说「未实测」
|
|
210
|
+
|
|
211
|
+
- **日期**:2026-10-22
|
|
212
|
+
- **状态**:现行(实现见 `src/host/agnescode-models.ts` 的 `AGNESCODE_VISION_DENYLIST` / `agnescodeVisionVerdict` / `agnescodeVisionSource`,`agnescodeRoster` 把 `vision` 与 `visionSource` 一起透出;面板 chip 见 `src/client/agnescode-tab.ts`;门禁见 `test/agnescode.test.mjs` 的判定阶梯段与 `test/dom-render.test.mjs` 的 chip 段)
|
|
213
|
+
- **裁定**:判定阶梯四级——**实测否决集 → 行内声明 → `PROBED_VISION` 实测表 → 默认放行(`assumed`)**。前三级给面板一个朴素 chip;`assumed` 必须画成「可看图·未实测」并带 tooltip;否决项画 `vision:false`、无 chip。换言之:**默认值是「给它图片」,唯一能否掉它的是带日期实测的否决条目**。
|
|
214
|
+
- **理由**:网关 `/models` 的每一行都只写 `model_type: "text"`,从不说自己能不能看图(`fetchAgnescodeCatalog` 因此钉死 `vision:false`——那是 wire 事实,不是判决)。旧逻辑把「文档表里没有」读成「不支持」,于是网关每上一次新模型,图片入口就静默消失一次:用户看不到任何提示,只能自己发现并来报。这条失效方向是不可见的(能力缺失不像错报那样会在会话里露头),而错报在当前网关是可观测的:模型收到图片后自述看不见,HTTP 仍是 200。
|
|
215
|
+
- **代价与护栏**:默认放行把失效方向翻成「错报」,但没有翻成「谎报」——`visionSource` 让面板把未实测的行标出来,读者因此知道这是插件的默认,不是平台的声明。否决集是唯一维护面:发现某模型自述看不见,就把它带日期加进 `AGNESCODE_VISION_DENYLIST`。
|
|
216
|
+
- **边界**:只动 agnescode 这条上游;Token Plan 侧 `identifyVisionModel`(按 `/v1/models` 的 `input_modalities` 判定)不受影响,两侧判定函数不合并。`AGNESCODE_FALLBACK_MODELS` 的 `vision` 列是手写快照,不经过本判定函数;`visionSource` 只影响面板显示,不改变 descriptor 的 `input` 数组长度约定(`vision:true` → `["text","image"]`)。
|
|
217
|
+
- **复核条件**:若网关开始在 `/models` 里声明模态(`input_modalities` 或视觉标签),第二级(`declared`)就会接管绝大多数行,`assumed` 退化为稀有位——那时本条仍然成立,只是很少被用到。若哪天实测发现「未实测默认可看图」在本网关的错报代价高到不可接受(例如图片被静默吞掉且模型照常编造),把默认改回 `false` 并把本条的 `assumed` 换成一个显式的 `unknown` chip。
|
|
218
|
+
|
|
219
|
+
## ADR-019 网关新增模型走胶囊的「· N 新模型」,不弹窗、不留确认态
|
|
220
|
+
|
|
221
|
+
- **日期**:2026-10-22
|
|
222
|
+
- **状态**:现行(实现见 `src/host/agnescode-roster-seen-store.ts` 的首次见到历史、`src/host/routes/agnescode.ts` 的 `newModelIds` 差分、`src/client/composer-usage-pill.ts` 的 AgnesCode 胶囊段;门禁见 `test/agnescode.test.mjs` 的两个新段与 `test/dom-render.test.mjs` 的胶囊三帧)
|
|
223
|
+
- **裁定**:网关目录里出现本机没见过的 model id 时,**只在胶囊上多一段「· N 新模型」**(tooltip 与 popover 给名字,最多列三个),不弹窗、不阻塞、不留「已读」按钮。判定基准是插件自己写的首次见到文件 `agnescode-roster-seen.json`:**首次写入是播种(整表记为已见、一条都不报)**,之后新增的 id 记为「现在」并保持 7 天,离开目录的 id 从表里删除(重新上架算新)。
|
|
224
|
+
- **理由**:DSH 的 Host 面没有通知/弹窗服务,插件的可见面只有插件卡与胶囊;而胶囊本来就是读者扫余额的地方,把「网关多了个模型」放在余额旁边是同一类信息(都关于「现在能用什么」)。播种是必须的:升级后的第一次运行会把当时整张目录写成历史,否则每个老用户都会被告知全部七个模型都是新的——那是噪音,不是提醒。
|
|
225
|
+
- **为什么不是弹窗 / 已读态**:弹窗需要用户处理,而「多了个模型」不需要处理;已读态需要一个点击,等于把一个纯信息变成一次任务。7 天窗口是提醒的寿命:够长到用户会看见,够短到不会永远挂着。重上架的 id 再报一次,因为那说明这一行背后的东西换过手,值得知道。
|
|
226
|
+
- **边界**:只在 `modelsSource === "live"` 时差分——回退快照表是插件自己的六行文本,拿它差分会在登录/离线那几帧凭空造出「新模型」。差分对象是**整张 live 目录**,不是用户勾选的子集:某个模型是不是网关的新闻,与这台机器勾了哪些框无关;拿子集差分还会让一个上架很久的模型在「被勾上」的那天冒充新模型。`newModelIds` 缺席(旧生产者、离线、首次播种)时胶囊什么都不画,不画空段、不留悬空分隔符。该标记只出现在 agnescode provider 被选中的胶囊上;Token Plan 侧名册没有这个字段。
|
|
227
|
+
- **复核条件**:若网关开始提供「上架时间」字段,差分就应改成读那个字段,本文件降级为离线兜底;若 DSH 之后提供通知服务,这段可以迁过去,胶囊只留计数。
|
|
228
|
+
|
|
229
|
+
|
package/docs/API.md
CHANGED
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
// quotaBlockedModelIds 是交给选择器剔除的 id 集。Agnes 下**恒为空**——
|
|
116
116
|
// 平台不按模型分配额,账号级用尽由面板明说,而不是静默摘模型。
|
|
117
117
|
"quotaBlockedModelIds": [],
|
|
118
|
-
// 出图工具开关生效值与来源(0.4.2
|
|
118
|
+
// 出图工具开关生效值与来源(0.4.2);工具实际挂载随之即时收敛(0.13.x 起,见 TOOL-HOT-RELOAD.md)
|
|
119
119
|
"drawEnabled": false, "drawSource": "config",
|
|
120
120
|
// 一次出图调用实际会寻址的模型 id:用与工具本身相同的 pickDrawModel
|
|
121
121
|
// 优先级(调用参数 > 配置的 drawModelId > 目录首个出图模型)从同一份
|
|
@@ -224,9 +224,9 @@
|
|
|
224
224
|
|
|
225
225
|
### `POST /api/dsh-connect-agnes-token-plan/draw`
|
|
226
226
|
|
|
227
|
-
`{ "enabled": true|false }` —— 把出图开关写入插件私有状态文件(`$DSH_HOME/state/<profile>/<plugin>/draw.json`,按 profile 分段、见 [PITFALLS.md](./PITFALLS.md) §23),与 `/provider`
|
|
227
|
+
`{ "enabled": true|false }` —— 把出图开关写入插件私有状态文件(`$DSH_HOME/state/<profile>/<plugin>/draw.json`,按 profile 分段、见 [PITFALLS.md](./PITFALLS.md) §23),与 `/provider` 走的是同一套「存私有状态」机制,但**不发布 provider**——写成功后立即收敛**工具挂载**:协调器 `sync()` 就地注册 / 撤下 / 换模型(0.13.x 起,机制见 [TOOL-HOT-RELOAD.md](./TOOL-HOT-RELOAD.md);宿主不给卸载句柄时工具粘性,保持首次注册值)。优先级:面板保存的值 > `cordis.patch.yml` 的 `drawEnabled`。非布尔 `enabled` 返回 400;跨域返回 403。
|
|
228
228
|
|
|
229
|
-
`{ "drawModelId": "agnes-image-2.5-flash" }`(或 `null` = 自动选择)—— 把出图模型偏好写入同一个 `draw.json
|
|
229
|
+
`{ "drawModelId": "agnes-image-2.5-flash" }`(或 `null` = 自动选择)—— 把出图模型偏好写入同一个 `draw.json`。生效时机与开关相同:写成功后协调器就地收敛(已挂载则先撤旧再注新,见 [TOOL-HOT-RELOAD.md](./TOOL-HOT-RELOAD.md));优先级:面板 > 配置,面板清除后回落 `cordis.patch.yml` 的 `drawModelId`,配置也为空则自动取目录第一个出图模型。非空字符串之外的非 null 值返回 400;跨域返回 403。`{ "forget": true }` 同时清除开关与模型偏好的面板保存值。
|
|
230
230
|
|
|
231
231
|
### `GET /api/dsh-connect-agnes-token-plan/video`
|
|
232
232
|
|
package/docs/DSH-PLUGIN.md
CHANGED
|
@@ -25,7 +25,7 @@ DSH 插件是一段在 **Host**(桌面版或 `dsh web`)进程内运行的代
|
|
|
25
25
|
{
|
|
26
26
|
"name": "dsh-connect-agnes-token-plan",
|
|
27
27
|
"icon": "./icon.svg", // Plugins 页插件卡图标:Host 读此字段按 SVG/PNG/JPEG/WebP 渲染(相对路径、落在 files 内、≤256 KiB,任一不满足会让整条元数据报错)
|
|
28
|
-
"version": "0.13.
|
|
28
|
+
"version": "0.13.3",
|
|
29
29
|
"main": "./lib/index.js", // Host 半边入口:src/host/*.ts 经 tsdown 打成的单条 bundle
|
|
30
30
|
"exports": {
|
|
31
31
|
".": "./lib/index.js", // 源码在 src/host/,lib/ 为纯构建产物(随库提交,git/市场直装零构建)
|
|
@@ -77,9 +77,9 @@ trae/workbuddy 的 volatile 路线(把 `registerProvider` 标成 Config schema
|
|
|
77
77
|
| `snapshot-aggregate.ts` | 快照 `llm.drawEnabled` / `llm.drawSource` 回显生效值与来源 |
|
|
78
78
|
| `panel-page.ts` | 独立 SectionCard 承载「出图工具」卡片,含 `DrawSwitch` 控件(`ProviderForm` 已移到 `provider-controls.ts`,见其脚注) |
|
|
79
79
|
|
|
80
|
-
与 provider
|
|
80
|
+
与 provider 开关在 0.4.2 时期存在一个**语义差异**(该差异现已解除,见 §9):provider 开关改的是「当前请求立刻重新发布注册对」,改完立即生效;而当时 draw 开关改的是「挂载时是否注册 agent 工具」,`mountAgentTool` 丢弃了宿主返回的卸载句柄,**已注册的工具不会随开关在进程内消失或出现**——要真正生效需等**下一个 Host (re)mount**(重启 `dsh web` 或重装插件)时 `startSideEffects` 重新读生效值。面板文案里「立即生效」当时仅指**开关值**本身的读写。0.13.x 起补齐了热挂载/卸载协调层,draw 与 video 开关也随之**无需重启即时生效**(机制与边界见 §9)。
|
|
81
81
|
|
|
82
|
-
**已知边界**:两个 Host 进程共享同一状态目录时,后写者胜(与 provider / throttle / catalog 文件语义一致)。`POST /draw`
|
|
82
|
+
**已知边界**:两个 Host 进程共享同一状态目录时,后写者胜(与 provider / throttle / catalog 文件语义一致)。`POST /draw` 在 0.4.x 时期只改开关值、不直接操作 tools registry——因为当时宿主未提供可捕获的卸载句柄,工具卸载要等 Host 生命周期自然结束。这一限制**已在 0.13.x 解除**:`POST /draw`(与 `/video`)现会触发即时再同步(`createToolMount.sync()`),机制与「宿主不给卸载路径则工具粘性挂载」的边界见 §9。
|
|
83
83
|
|
|
84
84
|
## 8. 0.4.3 增量:视频工具开关(videoEnabled)
|
|
85
85
|
|
|
@@ -101,9 +101,23 @@ trae/workbuddy 的 volatile 路线(把 `registerProvider` 标成 Config schema
|
|
|
101
101
|
|
|
102
102
|
---
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
## 9. 0.13.x 增量:出图 / 视频工具的「真热开关」(disposer 保留的即时挂载/卸载)
|
|
105
|
+
|
|
106
|
+
§7 里那句「工具的实际挂载/卸载要等到下次 Host 启动」在 0.4.x 时期是对的——当时 `mountAgentTool` 把宿主 `tools.register()` 返回的卸载句柄直接丢弃,所以 agent 工具只能「挂载一次」。现在改掉了:
|
|
107
|
+
|
|
108
|
+
1. **宿主本就支持卸载**。`tools.register(definition)` 返回一个 `() => void` 的 disposer(同族插件 workbuddy 的 image/video 工具已长期以「常驻注册 + 保留句柄 + 开关切换时 live 卸载/换模型」的形态跑在生产),`types.ts` 把 `HostCtx.tools.register` 的类型从 `unknown` 收紧成 `(() => void) | undefined`。不给卸载路径的宿主返回 `undefined`,工具退化为粘性「挂载一次」——这是宿主能力边界,不是缺陷。
|
|
109
|
+
2. **每工具一个「热挂载协调器」** `createToolMount`(`lifecycle.ts`,替代旧的一次性 `mountAgentTool`)。它持有闭包状态(缓存的宿主 `tools` 服务引用、`registered` / `disposer` / `regModel`)并把**所有**协调串在**同一条 promise 链**上,使「开关 POST / 模型 POST / forget / 挂载种子」的并发触发不会重复注册同一工具:链上每跑一次都**重读当前生效值**再收敛,所以排队的旧协调照样落到最新态。收敛规则:关 → 有 disposer 就卸载(没有则保持粘性,不动);开且与已注册模型一致 → 幂等不动;开但模型变了 → 有 disposer 先卸旧再注新(模型偏好也即时生效)。
|
|
110
|
+
3. **句柄落在接线袋**(`wiring.drawToolMount` / `wiring.videoToolMount`),开关路由(`routes/tool-switch.ts` 的 `registerToolSwitchRoute`,draw 与 video 共用)在**每次成功的写入后**(`enabled` / `forget` / 模型偏好)调用协调器 `sync()`。协调是 fire-and-forget、不阻塞 HTTP 响应;GET 只读路径**绝不**协调——读不应改写宿主注册表。
|
|
111
|
+
4. **teardown 顺序**(PITFALLS §18)在路由 `off()` 之前释放两个工具协调器:`stop → dispose → release → tools → off`,让在途的开关 POST 无法再往一个正在离场的宿主里同步工具。
|
|
112
|
+
|
|
113
|
+
**已知边界**:宿主不提供卸载路径时(`register` 返回 `undefined`),工具对本进程粘性——开关/模型改动能被协调器读到,但没有句柄可卸,注册保持首次值;此时行为与 0.4.x 完全一致。面板开关的状态读写永远即时生效,工具注册跟不跟上取决于宿主能力。
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
> **结构沿革注记**:本节两张改动表是 0.4.x 各增量落地时的定格记录,表中「新增」文件此后又经历过收敛与拆分。查现行结构以 [ARCHITECTURE.md](./ARCHITECTURE.md) §2 为准;与上表直接相关的四处:
|
|
105
118
|
>
|
|
106
119
|
> - **video 逻辑层已拆分**:表中的单文件 `video.ts` 现在只保留 `agnes_video_generate` 工具定义与兼容 barrel;建任务/轮询状态机在 `video-client.ts`,V2.0 与 2.5 两套请求体分别在 `video-protocol.ts` / `video-protocol-25.ts`,目录花名册在 `video-models.ts`。
|
|
107
120
|
> - **两个开关 store 不再逐字镜像**:重复体已收敛到 `switch-store.ts`(provider / draw / video / AgnesCode 共用一层),`draw-store.ts` 与 `video-store.ts` 只剩各自的文件名、形状版本、model wire key 与报错措辞。事故与收敛过程见 [PITFALLS.md](./PITFALLS.md) §34。
|
|
108
121
|
> - **draw / video 共用的路由处理器**现在落在 `routes/tool-switch.ts`,由 `routes.ts` 门面统一注册。
|
|
122
|
+
> - **§7/§8 表格里指名的共享私有函数 `mountAgentTool` 已于 0.13.x 换成 `createToolMount`**(热挂载协调器,§9):工具从「挂载一次、卸载等 Host 重启」变成「开关/模型写入后即时挂载、卸载、换模型」。表格里 `lifecycle.ts` 一行的 `registerDrawTool` / `registerVideoTool` 名字不变,现在它们返回并保留热句柄而不是 fire-and-forget 地丢弃。
|
|
109
123
|
|
package/docs/README.md
CHANGED
|
@@ -17,6 +17,7 @@ AI 协作会话从根目录 [AGENTS.md](../AGENTS.md) 进入:验证怎么跑
|
|
|
17
17
|
| [AUTH.md](./AUTH.md) | 一跳账号密码登录、无 refresh token(重登即续期)、密码明文过 TLS 与不落盘纪律、登录 trace、防锁号节流 | 改登录/重登、排查登录失败 |
|
|
18
18
|
| [API.md](./API.md) | 本地路由(`snapshot` / `account` / `api-key` / `provider` / `models` / `draw` / `video` / `agnescode` 共 8 组)、控制台端点、配置端点清单 | 对接路由、看返回结构、调端点 |
|
|
19
19
|
| [PROVIDER-HOT-RELOAD.md](./PROVIDER-HOT-RELOAD.md) | 提供方注册开关:从「配置字段 + 重启」到「面板开关 + 立即生效」的设计决策与同类插件调研 | 改 provider 注册、理解开关语义 |
|
|
20
|
+
| [TOOL-HOT-RELOAD.md](./TOOL-HOT-RELOAD.md) | 出图 / 视频 agent 工具热挂载机制(0.13.x):disposer 保留、每工具协调器、开关写入触发 `sync()`、teardown 顺序、宿主不给卸载句柄的粘性边界 | 改工具挂载 / 开关语义、排「翻了开关工具没变」 |
|
|
20
21
|
| [TESTING.md](./TESTING.md) | 离线测试体系、`panel-decision.js` 机制、已知缺口 | 跑测试、理解测试为什么这样写 |
|
|
21
22
|
| [AGNES-API.md](./AGNES-API.md) | **现行接口事实源**:控制台额度侧(端点表、响应信封、四窗口额度模型、套餐目录与 uuid 匹配)+推理侧(provider `agnes-token-plan`、key `AGNES_TOKEN_PLAN_API_KEY`、思考档位 safe-set、live-contract 护栏) | 改额度/推理接口、排「打到错网关」、对 live-contract 基线 |
|
|
22
23
|
| [PITFALLS.md](./PITFALLS.md) | 真实踩坑(现象→根因→修法);**条数以文件内 `## N.` 编号为准,别在此写死** | 改代码前避坑、理解防御性代码的来由 |
|
package/docs/SETUP.md
CHANGED
|
@@ -120,3 +120,14 @@ AgnesCode tab 读的是**另一个应用**(AgnesCode 桌面端)已经登录
|
|
|
120
120
|
信号与处置的整张表已迁到 **[TROUBLESHOOTING.md](./TROUBLESHOOTING.md)**(按面板上的**界面原文**索引:面板读不出来、登录被拒、模型目录为空、以及「看着像坏了其实是设计」的清单)。
|
|
121
121
|
|
|
122
122
|
本文件只管装、配、重启;排障从那份进。`shapeWarnings`、`quota.error` 按源降级、「可看图」为空是「读不到」等接口侧含义,那份表指到 [AGNES-API.md](./AGNES-API.md) 与 [ADR.md](./ADR.md),不在此重复。
|
|
123
|
+
|
|
124
|
+
### 6.1 查这台机器的开关生效值(doctor)
|
|
125
|
+
|
|
126
|
+
provider / 出图 / 视频 / AgnesCode 四个开关的生效值存在插件私有状态文件里(`$DSH_HOME/state/<name>/`),不在任何配置或路由上;`npm run doctor` 只读状态文件、不碰凭据,Host 没起也能跑:
|
|
127
|
+
|
|
128
|
+
```powershell
|
|
129
|
+
npm run doctor # 人读:每个 profile 的 provider / draw / video / agnescode 开关与模型清单
|
|
130
|
+
npm run doctor:json # 机器读:JSON(可进你的巡检 / 工单脚本)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
doctor 的只读范围与实现见 [PITFALLS.md](./PITFALLS.md) §22 与 [ROADMAP.md](./ROADMAP.md) 的落地坐标。
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# 出图 / 视频工具热挂载(Tool Hot-Mount)
|
|
2
|
+
|
|
3
|
+
本文记录 0.13.x 起出图 / 视频 agent 工具(`agnes_draw_image` / `agnes_video_generate`)的**热挂载机制**:面板开关、模型偏好的每次写入都**就地**收敛工具的注册 / 撤下 / 换模型,无需重启 Host。**路由与开关的现行契约以 [API.md](./API.md) §1(`/draw` / `/video` 路由段)为准,provider 注册开关的决策记录见 [PROVIDER-HOT-RELOAD.md](./PROVIDER-HOT-RELOAD.md),本篇侧重机制与边界。**
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. 为什么 0.4.x 时期不热
|
|
8
|
+
|
|
9
|
+
出图(0.4.2)与视频(0.4.3)工具开关落地时,「面板开关 + 立即生效」的语义只兑现了一半——**开关值本身**即时读写,工具的实际挂载 / 缺席要等**下一个 Host 启动**时 `startSideEffects` 重读生效值。三个成因,全部是本插件自己的接线选择,不是宿主能力边界:
|
|
10
|
+
|
|
11
|
+
1. 开关状态存插件私有状态文件(`draw-store.ts` / `video-store.ts`),由自有路由 `POST /api/<name>/draw|video` 写入——路由只做**持久化 + 回显**,从不同步宿主注册表。
|
|
12
|
+
2. 旧的一次性 `mountAgentTool` 把宿主 `tools.register()` 返回的卸载句柄直接丢弃(fire-and-forget),工具「挂载一次」后进程内无法撤下。
|
|
13
|
+
3. 一条过时的注释(「the tools registry has no unregister call」)让「宿主没有卸载能力」当成了已知事实——**这是错的**。宿主 `tools.register(definition)` 本就返回一个 `() => void` 的 disposer;同族插件 workbuddy 的 image / video 工具长期以「常驻注册 + 保留句柄 + 开关切换时 live 卸载 / 换模型」的形态跑在生产。
|
|
14
|
+
|
|
15
|
+
## 2. 机制
|
|
16
|
+
|
|
17
|
+
### 2.1 宿主能力
|
|
18
|
+
|
|
19
|
+
`HostCtx.tools.register(definition)` 返回 `(() => void) | undefined`(`types.ts` 把返回类型从 `unknown` 收紧为 `(() => void) | undefined`)。返回 `undefined` 的宿主(不提供卸载路径)下工具退化为**粘性挂载一次**——那是宿主能力边界,行为与 0.4.x 一致,不是缺陷。
|
|
20
|
+
|
|
21
|
+
### 2.2 每工具一个热挂载协调器
|
|
22
|
+
|
|
23
|
+
`createToolMount`(`src/host/lifecycle.ts`,替代旧的一次性 `mountAgentTool`;`registerDrawTool` / `registerVideoTool` 是它的两个薄包装,差异只有开关键、模型键、fetch 与工厂,降级阶梯写一份,两个工具**不可能降级不同**)。每个协调器持有自己的闭包状态:
|
|
24
|
+
|
|
25
|
+
| 状态 | 含义 |
|
|
26
|
+
|---|---|
|
|
27
|
+
| `tools` | 缓存的宿主 tools 服务(`resolveServiceWithRetry` 找到一次后复用;没找到时每次 `sync()` 重新探测,晚到的服务能被后续协调接住) |
|
|
28
|
+
| `registered` | 宿主是否真的持有该工具的一份副本 |
|
|
29
|
+
| `disposer` | 撤下句柄;宿主不给卸载路径时为 `null`(粘性) |
|
|
30
|
+
| `regModel` | 当前注册所用模型偏好(面板保存值 > 配置默认) |
|
|
31
|
+
| `chain` | **同一条 promise 链**:所有协调(挂载种子、开关 POST、模型 POST、forget、teardown 前在途调用)串在这条链上,并发触发不会重复注册同一工具;链上每跑一次都**重读当前生效值**再收敛,排队的旧协调照样落到最新态 |
|
|
32
|
+
|
|
33
|
+
收敛规则(`sync()` 每跑一次):
|
|
34
|
+
|
|
35
|
+
- **关** → 有 disposer 就撤下(清状态后 `safeCall(disposer)`);没有则保持粘性,不动。
|
|
36
|
+
- **开且与已注册模型一致** → 幂等,不动。
|
|
37
|
+
- **开但模型变了** → 有 disposer 先撤旧再注新(模型偏好也即时生效);无 disposer 保持首次注册值。
|
|
38
|
+
- 挂载路径:读 tools 服务 → 懒加载 tools peer(`defineTool`)→ 工厂构造工具定义(目录**调用时**读取:活目录缺席回落 `catalogStore.list()`,目录刷新落地不需要重注册)→ `tools.register(definition)`(catch:拒绝的注册表降级为工具缺席)→ 捕获 disposer。
|
|
39
|
+
|
|
40
|
+
**双降级纪律不变**:无 tools 服务 / peer 装载失败 / 注册表拒绝——每一档都是**静默**降级:面板与 provider 照常,工具只是缺席,没有任何一行日志把失败打成配置错误。
|
|
41
|
+
|
|
42
|
+
### 2.3 句柄与触发点
|
|
43
|
+
|
|
44
|
+
句柄落在接线袋(`wiring.drawToolMount` / `wiring.videoToolMount`,`startSideEffects` 填充)。开关路由(`src/host/routes/tool-switch.ts` 的 `registerToolSwitchRoute`,draw 与 video 共用一份处理器)在**每次成功的 POST 写入后**(`enabled` / `forget` / 模型偏好三处)调用协调器 `sync()`:
|
|
45
|
+
|
|
46
|
+
- 协调是 **fire-and-forget**,不阻塞 HTTP 响应;失败自吞(面板状态永远是对的,工具注册跟不跟上取决于宿主能力)。
|
|
47
|
+
- **GET 只读路径绝不协调**——读不应改写宿主注册表。
|
|
48
|
+
|
|
49
|
+
## 3. teardown 顺序
|
|
50
|
+
|
|
51
|
+
卸载顺序按 [PITFALLS.md](./PITFALLS.md) §18 钉死:`stop → dispose → release → tools → off`。两个工具协调器的 `dispose()` 在路由 `off()` **之前**执行(撤下活工具 + 关闭句柄),让在途的开关 POST 无法再往一个正在离场的宿主里同步工具;句柄关闭后,队列里未跑的 `sync()` 变成 no-op 而不是迟到的注册。
|
|
52
|
+
|
|
53
|
+
## 4. 边界与失败形状
|
|
54
|
+
|
|
55
|
+
- **宿主不给卸载句柄**(`register` 返回 `undefined`):工具对本进程**粘性**——开关 / 模型改动能被协调器读到,但没有句柄可撤,注册保持首次值;行为与 0.4.x 完全一致。面板开关的状态读写永远即时生效。
|
|
56
|
+
- **两个 Host 进程共享同一状态目录**:后写者胜(与 provider / throttle / catalog 状态文件语义一致)。
|
|
57
|
+
- **协调失败自静默**:tools 服务缺席、peer 装载失败、注册表拒绝——面板与 provider 零影响,工具缺席即可;没有任何一行日志为它点名。
|
|
58
|
+
|
|
59
|
+
## 5. 测试锚点
|
|
60
|
+
|
|
61
|
+
- `test/routes.test.mjs`:开关路由的无 tools 服务降级路径(假宿主 ctx 无 `tools` 服务)与键名隔离(组 S 交叉投递 / 各自回读,[API.md](./API.md) §1 `/draw` / `/video` 段引注)。
|
|
62
|
+
- **已知缺口**:disposer 路径(挂载 / 撤下 / 换模型)尚无离线套件——需要向挂载缝注入一个返回 disposer 的假 `tools` 服务;补套件前先冻结现状。
|
|
63
|
+
- 工具纯逻辑(失败分诊 / 冷却 / hints)由 `test/draw.test.mjs` 等覆盖,与挂载无关。
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
> 跨链:决策记录(为何不走 volatile、开关语义沿革)见 [PROVIDER-HOT-RELOAD.md](./PROVIDER-HOT-RELOAD.md) §7–§9;出图对接点(ARCHITECTURE §5.4 接法 B)见 [ARCHITECTURE.md](./ARCHITECTURE.md) §5.4;「为何自己造工具」的 runtime 取证见 [DSH-UNIFIED-INTERFACE-AUDIT.md](./DSH-UNIFIED-INTERFACE-AUDIT.md)。
|
package/docs/TROUBLESHOOTING.md
CHANGED
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
- **没有「登出」按钮**:能停的是 provider 开关,不是令牌。「删除已存 JWT」是隐私动作,被刻意移出日常操作;真正吊销只能在桌面 App 里登出 / 改密码。见 [SETUP.md](./SETUP.md) §3。
|
|
77
77
|
- **「可看图」清单为空**:Agnes 目录条目**不带** `input_modalities`(实测只有 `id`/`object`/`created`/`owned_by`/`supported_endpoint_types` 五个字段),`identifyVisionModel` 只能读目录 → 恒 false。这是**「读不到」而不是「没有」**——官方文档(`AGNES-API-docs/`)明说 `agnes-3.0-flash` / `agnes-2.5-pro` / `agnes-2.5-flash` 支持「文本 + 图像 URL 输入」,所以空清单不能当作「真没有可看图模型」的证据。见 [AGNES-API.md](./AGNES-API.md) §7.1。
|
|
78
78
|
- **「可看图」整行消失**:不是被裁掉,是没有 API Key → 目录没拉 → 视觉清单无从生成。见 §1.3 第一行。
|
|
79
|
-
-
|
|
79
|
+
- **翻了开关出图 / 视频工具却没变**:开关值读写永远即时;工具挂载跟不跟得上取决于宿主能力——宿主不给卸载句柄时工具对本进程**粘性**(保持首次注册值,0.13.x 起的行为边界),机制与失败形状见 [TOOL-HOT-RELOAD.md](./TOOL-HOT-RELOAD.md) §4。
|
|
80
80
|
- **拼错的配置键只记日志、不报错**:老 patch 里的历史键不该把面板打成配置错误。见 [SETUP.md](./SETUP.md) §3。
|
|
81
81
|
- **429 与额度耗尽不是一件事**:402 是额度不足,429 多为速率上限(订阅用尽也可能回 429)。见 [AGNES-API.md](./AGNES-API.md)。
|
|
82
82
|
|
|
@@ -2087,28 +2087,73 @@ function agnescodeRequestHeaders() {
|
|
|
2087
2087
|
return { ...AGNESCODE_BASE_HEADERS };
|
|
2088
2088
|
}
|
|
2089
2089
|
/**
|
|
2090
|
-
*
|
|
2091
|
-
*
|
|
2092
|
-
*
|
|
2093
|
-
*
|
|
2094
|
-
*
|
|
2095
|
-
*
|
|
2096
|
-
*
|
|
2097
|
-
*
|
|
2098
|
-
*
|
|
2099
|
-
*
|
|
2100
|
-
*
|
|
2101
|
-
*
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2090
|
+
* Models confirmed on this gateway to CANNOT read an image, in a LIVE probe
|
|
2091
|
+
* with the same picture + question the allow side was measured with (a live
|
|
2092
|
+
* re-probe runs against this set, so the day a gateway routing change makes a
|
|
2093
|
+
* denied model able to see, the baseline turns and the entry comes out).
|
|
2094
|
+
* Membership is a dated measurement, never a design call about the model.
|
|
2095
|
+
*
|
|
2096
|
+
* STARTS EMPTY: nothing about this gateway is known to be blind. An entry is
|
|
2097
|
+
* added only when a user reports a model that cannot see the image (with a
|
|
2098
|
+
* probe that proves it), and it leaves the same way. Until a report lands
|
|
2099
|
+
* here, every untagged row is OFFERED image input by default and labelled
|
|
2100
|
+
* unverified — see the companion verdict below.
|
|
2101
|
+
* @type {ReadonlySet<string>}
|
|
2102
|
+
*/
|
|
2103
|
+
const AGNESCODE_VISION_DENYLIST = Object.freeze(/* @__PURE__ */ new Set([]));
|
|
2104
|
+
/**
|
|
2105
|
+
* Whether an AgnesCode roster row accepts image input, together with how the
|
|
2106
|
+
* verdict was reached.
|
|
2107
|
+
*
|
|
2108
|
+
* The BFF's own `/models` rows declare no modality field (only `model_type:
|
|
2109
|
+
* "text"`), so `fetchAgnescodeCatalog` pins `vision: false` — a raw fact about
|
|
2110
|
+
* what the wire said, NOT a verdict. The verdict is made here. The ladder is
|
|
2111
|
+
* asymmetric on purpose: a false "it can see" (an image sent to a model that
|
|
2112
|
+
* ignores it, HTTP 200, no error anywhere upstream) is worse than a hidden
|
|
2113
|
+
* capability, so the DEFAULT is to offer the entry but SAY it is unverified,
|
|
2114
|
+
* and only a dated live probe may deny it:
|
|
2115
|
+
*
|
|
2116
|
+
* 1. `AGNESCODE_VISION_DENYLIST` wins: a row confirmed blind in a live probe is
|
|
2117
|
+
* not offered image input; `source: "denied"` tells the panel to draw no chip.
|
|
2118
|
+
* 2. A row carrying `vision: true` (a future BFF that starts declaring its own
|
|
2119
|
+
* modality) is `declared`.
|
|
2120
|
+
* 3. The `PROBED_VISION` official-docs table (llm-models.ts) — the Agnes family
|
|
2121
|
+
* the platform already proved — is `measured`.
|
|
2122
|
+
* 4. Otherwise: `assumed` (true). A model the docs table never mentioned is
|
|
2123
|
+
* still offered image input — it does not vanish from the image picker —
|
|
2124
|
+
* and the panel marks it unverified. Reading "absent from `PROBED_VISION`"
|
|
2125
|
+
* as "text-only" is what made every newly-listed gateway model silently
|
|
2126
|
+
* lose the attachment entry.
|
|
2105
2127
|
* @param {object} row - one AgnesCode roster row (must carry `id`).
|
|
2128
|
+
* @returns {object} the verdict and how it was reached.
|
|
2129
|
+
*/
|
|
2130
|
+
function agnescodeVisionVerdict(row) {
|
|
2131
|
+
const id = str(row?.id, "");
|
|
2132
|
+
if (id !== "" && AGNESCODE_VISION_DENYLIST.has(id)) return {
|
|
2133
|
+
vision: false,
|
|
2134
|
+
source: "denied"
|
|
2135
|
+
};
|
|
2136
|
+
if (row?.vision === true) return {
|
|
2137
|
+
vision: true,
|
|
2138
|
+
source: "declared"
|
|
2139
|
+
};
|
|
2140
|
+
if (id !== "" && PROBED_VISION[id] === true) return {
|
|
2141
|
+
vision: true,
|
|
2142
|
+
source: "measured"
|
|
2143
|
+
};
|
|
2144
|
+
return {
|
|
2145
|
+
vision: true,
|
|
2146
|
+
source: "assumed"
|
|
2147
|
+
};
|
|
2148
|
+
}
|
|
2149
|
+
/**
|
|
2150
|
+
* The boolean-only view of {@link agnescodeVisionVerdict}, kept for call sites
|
|
2151
|
+
* and tests that want the capability bit without the provenance.
|
|
2152
|
+
* @param {object} row - one AgnesCode roster row.
|
|
2106
2153
|
* @returns {boolean} whether the row accepts image input.
|
|
2107
2154
|
*/
|
|
2108
2155
|
function agnescodeVisionOf(row) {
|
|
2109
|
-
|
|
2110
|
-
const id = str(row?.id, "");
|
|
2111
|
-
return PROBED_VISION[id] === true;
|
|
2156
|
+
return agnescodeVisionVerdict(row).vision;
|
|
2112
2157
|
}
|
|
2113
2158
|
/**
|
|
2114
2159
|
* The AgnesCode model rows the panel shows and the adapter offers.
|
|
@@ -2128,10 +2173,12 @@ function agnescodeRoster(catalog, maxTokensOverrides) {
|
|
|
2128
2173
|
const multiplier = numZeroOk(row?.multiplier);
|
|
2129
2174
|
const maxOutputLength = agnescodeMaxTokensFor(id, row?.maxOutputLength, maxTokensOverrides);
|
|
2130
2175
|
const displayLabel = str(row?.displayLabel, "");
|
|
2176
|
+
const verdict = agnescodeVisionVerdict(row);
|
|
2131
2177
|
out.push({
|
|
2132
2178
|
id,
|
|
2133
2179
|
name: str(row?.name, id),
|
|
2134
|
-
vision:
|
|
2180
|
+
vision: verdict.vision,
|
|
2181
|
+
visionSource: verdict.source,
|
|
2135
2182
|
memberOnly: row?.memberOnly === true,
|
|
2136
2183
|
...multiplier === void 0 ? {} : { multiplier },
|
|
2137
2184
|
...displayLabel === "" ? {} : { displayLabel },
|
package/lib/index.js
CHANGED
|
@@ -106,7 +106,7 @@ function throttleDir() {
|
|
|
106
106
|
* @param {() => number} now - clock source.
|
|
107
107
|
* @returns {{code: string, parked: boolean, until: number|null, attempt: number}|null}
|
|
108
108
|
*/
|
|
109
|
-
function parse(raw, now) {
|
|
109
|
+
function parse$1(raw, now) {
|
|
110
110
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
111
111
|
const body = raw;
|
|
112
112
|
const version = num(body.version, 0);
|
|
@@ -119,7 +119,7 @@ function parse(raw, now) {
|
|
|
119
119
|
};
|
|
120
120
|
const migrated = migrateToCurrent(body);
|
|
121
121
|
if (migrated === null) return null;
|
|
122
|
-
return parse({
|
|
122
|
+
return parse$1({
|
|
123
123
|
...migrated,
|
|
124
124
|
version: THROTTLE_VERSION
|
|
125
125
|
}, now);
|
|
@@ -158,7 +158,7 @@ function createFileThrottleStore({ dir = throttleDir(), now = Date.now } = {}) {
|
|
|
158
158
|
const file = join(dir, "throttle.json");
|
|
159
159
|
return {
|
|
160
160
|
async read() {
|
|
161
|
-
return parse(await readStateJson(file), now);
|
|
161
|
+
return parse$1(await readStateJson(file), now);
|
|
162
162
|
},
|
|
163
163
|
async write(state) {
|
|
164
164
|
const temporary = temporaryOf(dir, "throttle.json");
|
|
@@ -201,7 +201,7 @@ function createMemoryThrottleStore(now = Date.now) {
|
|
|
201
201
|
let held = null;
|
|
202
202
|
return {
|
|
203
203
|
async read() {
|
|
204
|
-
return parse(held, now);
|
|
204
|
+
return parse$1(held, now);
|
|
205
205
|
},
|
|
206
206
|
async write(state) {
|
|
207
207
|
held = {
|
|
@@ -2260,6 +2260,171 @@ function createFileAgnescodeModelsStore(options = {}) {
|
|
|
2260
2260
|
};
|
|
2261
2261
|
}
|
|
2262
2262
|
|
|
2263
|
+
//#endregion
|
|
2264
|
+
//#region src/host/agnescode-roster-seen-store.ts
|
|
2265
|
+
/**
|
|
2266
|
+
* dsh-connect-agnes-token-plan — when each AgnesCode roster id was FIRST seen.
|
|
2267
|
+
*
|
|
2268
|
+
* WHY this exists (and why it is a separate file, not `agnescode-store.ts`): the
|
|
2269
|
+
* gateway's catalogue rows carry no "listed since" fact — the model list is the
|
|
2270
|
+
* only signal that "a model is new to this user", and it is knowable only by
|
|
2271
|
+
* diffing today's roster against a roster THIS plugin wrote down. In memory
|
|
2272
|
+
* would do for one process, but the composer pill has to survive a restart to
|
|
2273
|
+
* be worth rendering, so the history lives in a state file of its own (the same
|
|
2274
|
+
* discipline as `agnescode-stats-store.ts`: one concern, one file, one reader).
|
|
2275
|
+
*
|
|
2276
|
+
* Semantics, stated once because two consumers read them:
|
|
2277
|
+
* - the FIRST ever write SEEDS: it stamps the whole roster one window back and
|
|
2278
|
+
* announces nothing new — an upgrade must not tell every existing user the
|
|
2279
|
+
* entire roster is brand new;
|
|
2280
|
+
* - an id that LEAVES the roster is dropped, so a model the gateway re-lists
|
|
2281
|
+
* later counts as new again (a re-listing IS a change the reader may want);
|
|
2282
|
+
* - "new" is a WINDOW, not a tick: an id first seen less than
|
|
2283
|
+
* {@link AGNESCODE_ROSTER_NEW_WINDOW_MS} ago stays new across polls, so the
|
|
2284
|
+
* marker does not vanish between two catalogue reads;
|
|
2285
|
+
* - a roster larger than {@link AGNESCODE_ROSTER_SEEN_MAX_IDS} is a shape
|
|
2286
|
+
* drift: nothing is written or announced for its duration (evicting a live
|
|
2287
|
+
* id would re-announce it next poll and the count would oscillate);
|
|
2288
|
+
* - an unrecognised or foreign-version file reads as "no history" (PITFALLS
|
|
2289
|
+
* §23), but the FIRST write over it is REFUSED, not stomped — the ADR-006
|
|
2290
|
+
* discipline. A caller that cannot write degrades to "no marker", never to a
|
|
2291
|
+
* wrong one.
|
|
2292
|
+
*
|
|
2293
|
+
* The marker is only fed from a LIVE roster: the static fallback table is this
|
|
2294
|
+
* plugin's own text, and diffing it would announce our rows as gateway news.
|
|
2295
|
+
*
|
|
2296
|
+
* @module dsh-connect-agnes-token-plan/agnescode-roster-seen-store
|
|
2297
|
+
*/
|
|
2298
|
+
/** Shape version, bumped when the persisted form changes incompatibly. */
|
|
2299
|
+
const AGNESCODE_ROSTER_SEEN_VERSION = 1;
|
|
2300
|
+
/**
|
|
2301
|
+
* How long a first sighting keeps reading as "new". Seven days: long enough that
|
|
2302
|
+
* a user who opens the panel twice a week still catches it, short enough that a
|
|
2303
|
+
* stale marker cannot outlive the model it announces.
|
|
2304
|
+
*/
|
|
2305
|
+
const AGNESCODE_ROSTER_NEW_WINDOW_MS = 6048e5;
|
|
2306
|
+
/**
|
|
2307
|
+
* The largest roster one file will track — a SHAPE-DRIFT guard, not a queue.
|
|
2308
|
+
* A roster past the cap is read as a broken/looping producer: NOTHING is
|
|
2309
|
+
* written or announced for its duration, and the previous map is kept as-is.
|
|
2310
|
+
*/
|
|
2311
|
+
const AGNESCODE_ROSTER_SEEN_MAX_IDS = 200;
|
|
2312
|
+
/** Where the record lives: the SHARED directory, `$DSH_HOME/state/<plugin>`. */
|
|
2313
|
+
function agnescodeRosterSeenDir() {
|
|
2314
|
+
return stateDir(name);
|
|
2315
|
+
}
|
|
2316
|
+
/**
|
|
2317
|
+
* Read one persisted map, keeping only well-formed entries. A non-numeric stamp
|
|
2318
|
+
* is dropped rather than coerced (a coerced `0` would read as "seen since 1970"
|
|
2319
|
+
* and mark nothing new forever). Never throws.
|
|
2320
|
+
* @param {unknown} raw - the persisted or posted value.
|
|
2321
|
+
* @returns {Record<string, number>} the usable entries (possibly empty).
|
|
2322
|
+
*/
|
|
2323
|
+
function normalizeRosterSeen(raw) {
|
|
2324
|
+
const source = raw !== null && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
2325
|
+
const seen = {};
|
|
2326
|
+
for (const [id, stamp] of Object.entries(source)) {
|
|
2327
|
+
if (id === "" || typeof stamp !== "number" || !Number.isFinite(stamp) || stamp <= 0) continue;
|
|
2328
|
+
seen[id] = stamp;
|
|
2329
|
+
}
|
|
2330
|
+
return seen;
|
|
2331
|
+
}
|
|
2332
|
+
/**
|
|
2333
|
+
* Keep the map ONLY while the roster is a sane size: a roster past the cap is a
|
|
2334
|
+
* shape drift, so NOTHING is written or announced and the previous map survives.
|
|
2335
|
+
* @param {Record<string, number>} next - this round's map.
|
|
2336
|
+
* @returns {Record<string, number>|null} the map to persist, or `null` to skip.
|
|
2337
|
+
*/
|
|
2338
|
+
function bounded(next) {
|
|
2339
|
+
return Object.keys(next).length <= 200 ? next : null;
|
|
2340
|
+
}
|
|
2341
|
+
/** Is the parsed payload a shape THIS build recognises (a known version)? */
|
|
2342
|
+
function parse(raw) {
|
|
2343
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
2344
|
+
const body = raw;
|
|
2345
|
+
if (body.version !== 1) return null;
|
|
2346
|
+
return { seen: normalizeRosterSeen(body.seen) };
|
|
2347
|
+
}
|
|
2348
|
+
/**
|
|
2349
|
+
* The file-backed first-sighting history.
|
|
2350
|
+
* @param {object} [options] - `{ dir, now }` (a clock source for the tests).
|
|
2351
|
+
* @returns {object} the store.
|
|
2352
|
+
*/
|
|
2353
|
+
function createFileAgnescodeRosterSeenStore({ dir = agnescodeRosterSeenDir(), now = Date.now } = {}) {
|
|
2354
|
+
const file = join(dir, "agnescode-roster-seen.json");
|
|
2355
|
+
const readThrough = async () => {
|
|
2356
|
+
const raw = await readStateJson(file);
|
|
2357
|
+
return {
|
|
2358
|
+
raw: raw === null ? null : raw,
|
|
2359
|
+
parsed: parse(raw)
|
|
2360
|
+
};
|
|
2361
|
+
};
|
|
2362
|
+
/** Persist one map (atomic). Refuses to stomp a foreign-version file (ADR-006). */
|
|
2363
|
+
const persist = async (seen) => {
|
|
2364
|
+
const { raw } = await readThrough();
|
|
2365
|
+
if (raw !== null && parse(raw) === null) return;
|
|
2366
|
+
const body = JSON.stringify({
|
|
2367
|
+
version: 1,
|
|
2368
|
+
seen,
|
|
2369
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2370
|
+
});
|
|
2371
|
+
const temporary = temporaryOf(dir, "agnescode-roster-seen.json");
|
|
2372
|
+
await ensureStateDir(dir);
|
|
2373
|
+
await writeStateFile(file, body, { temporary });
|
|
2374
|
+
};
|
|
2375
|
+
return {
|
|
2376
|
+
/** The whole first-sighting map. */
|
|
2377
|
+
async seen() {
|
|
2378
|
+
const { parsed } = await readThrough();
|
|
2379
|
+
return parsed?.seen ?? {};
|
|
2380
|
+
},
|
|
2381
|
+
/** Whether this store has ever written a recognisable history. */
|
|
2382
|
+
async isSet() {
|
|
2383
|
+
const { parsed } = await readThrough();
|
|
2384
|
+
return parsed !== null;
|
|
2385
|
+
},
|
|
2386
|
+
/**
|
|
2387
|
+
* Record today's roster and answer which ids still read as new.
|
|
2388
|
+
* @param {string[]} ids - the FULL live roster as the gateway just listed it.
|
|
2389
|
+
* @param {number} [nowMs] - the clock (tests inject it).
|
|
2390
|
+
* @returns {Promise<{newIds: string[]}>} ids first seen inside the window,
|
|
2391
|
+
* oldest first; empty on the seeding write and on a roster with nothing fresh.
|
|
2392
|
+
*/
|
|
2393
|
+
async note(ids, nowMs = now()) {
|
|
2394
|
+
const list = Array.isArray(ids) ? ids.filter((id) => typeof id === "string" && id !== "") : [];
|
|
2395
|
+
const unique = [...new Set(list)];
|
|
2396
|
+
if (unique.length === 0) return { newIds: [] };
|
|
2397
|
+
const nowT = Number.isFinite(nowMs) ? nowMs : now();
|
|
2398
|
+
const { raw, parsed } = await readThrough();
|
|
2399
|
+
if (raw !== null && parsed === null) return { newIds: [] };
|
|
2400
|
+
const previous = parsed?.seen ?? {};
|
|
2401
|
+
const next = {};
|
|
2402
|
+
const seeded = parsed === null && raw === null;
|
|
2403
|
+
for (const id of unique) {
|
|
2404
|
+
const first = previous[id];
|
|
2405
|
+
next[id] = typeof first === "number" && first > 0 ? Math.min(first, nowT) : seeded ? nowT - AGNESCODE_ROSTER_NEW_WINDOW_MS : nowT;
|
|
2406
|
+
}
|
|
2407
|
+
const trimmed = bounded(next);
|
|
2408
|
+
if (trimmed === null) return { newIds: [] };
|
|
2409
|
+
if (seeded || Object.keys(previous).length !== Object.keys(trimmed).length || Object.keys(trimmed).some((id) => previous[id] !== trimmed[id])) await persist(trimmed);
|
|
2410
|
+
return { newIds: seeded ? [] : Object.entries(trimmed).filter(([, first]) => nowT - first < AGNESCODE_ROSTER_NEW_WINDOW_MS).sort((left, right) => left[1] - right[1]).map(([id]) => id) };
|
|
2411
|
+
},
|
|
2412
|
+
/** Forget the history (the next `note` reseeds). */
|
|
2413
|
+
async forget() {
|
|
2414
|
+
const { raw } = await readThrough();
|
|
2415
|
+
if (raw !== null && parse(raw) === null) return;
|
|
2416
|
+
const body = JSON.stringify({
|
|
2417
|
+
version: 1,
|
|
2418
|
+
seen: {},
|
|
2419
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2420
|
+
});
|
|
2421
|
+
const temporary = temporaryOf(dir, "agnescode-roster-seen.json");
|
|
2422
|
+
await ensureStateDir(dir);
|
|
2423
|
+
await writeStateFile(file, body, { temporary });
|
|
2424
|
+
}
|
|
2425
|
+
};
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2263
2428
|
//#endregion
|
|
2264
2429
|
//#region src/host/switch-precedence.ts
|
|
2265
2430
|
/**
|
|
@@ -5325,9 +5490,12 @@ const VIDEO_PATH = `/api/${name}/video`;
|
|
|
5325
5490
|
* @param {string} options.modelSourceKey - the response key carrying its source.
|
|
5326
5491
|
* @param {string} options.configModelId - the config default for the model id.
|
|
5327
5492
|
* @param {Set<string>} options.allowedHosts - the trust fence.
|
|
5493
|
+
* @param {ToolMount} [options.mount] - the live tool mount handle; each
|
|
5494
|
+
* successful POST write converges it (hot reload). Absent = the route only
|
|
5495
|
+
* persists state, the mount is reconciled elsewhere (mount seed / teardown).
|
|
5328
5496
|
* @returns {Function} the `off()` unregister callback.
|
|
5329
5497
|
*/
|
|
5330
|
-
function registerToolSwitchRoute(ctx, { path, label, store, enabledKey, enabledSourceKey, configEnabled, modelKey, modelSourceKey, configModelId, allowedHosts }) {
|
|
5498
|
+
function registerToolSwitchRoute(ctx, { path, label, store, enabledKey, enabledSourceKey, configEnabled, modelKey, modelSourceKey, configModelId, allowedHosts, mount }) {
|
|
5331
5499
|
return ctx.webServer.register({
|
|
5332
5500
|
kind: "exact",
|
|
5333
5501
|
path,
|
|
@@ -5351,6 +5519,9 @@ function registerToolSwitchRoute(ctx, { path, label, store, enabledKey, enabledS
|
|
|
5351
5519
|
...extra
|
|
5352
5520
|
}, { "cache-control": "no-store" });
|
|
5353
5521
|
};
|
|
5522
|
+
const commit = () => {
|
|
5523
|
+
if (mount !== null && mount !== void 0) mount.sync().catch(() => {});
|
|
5524
|
+
};
|
|
5354
5525
|
if (method === "GET") {
|
|
5355
5526
|
await answer();
|
|
5356
5527
|
return;
|
|
@@ -5378,6 +5549,7 @@ function registerToolSwitchRoute(ctx, { path, label, store, enabledKey, enabledS
|
|
|
5378
5549
|
});
|
|
5379
5550
|
return;
|
|
5380
5551
|
}
|
|
5552
|
+
commit();
|
|
5381
5553
|
await answer();
|
|
5382
5554
|
return;
|
|
5383
5555
|
}
|
|
@@ -5406,6 +5578,7 @@ function registerToolSwitchRoute(ctx, { path, label, store, enabledKey, enabledS
|
|
|
5406
5578
|
});
|
|
5407
5579
|
return;
|
|
5408
5580
|
}
|
|
5581
|
+
commit();
|
|
5409
5582
|
await answer();
|
|
5410
5583
|
return;
|
|
5411
5584
|
}
|
|
@@ -5432,12 +5605,14 @@ function registerToolSwitchRoute(ctx, { path, label, store, enabledKey, enabledS
|
|
|
5432
5605
|
});
|
|
5433
5606
|
return;
|
|
5434
5607
|
}
|
|
5608
|
+
commit();
|
|
5435
5609
|
await answer();
|
|
5436
5610
|
}
|
|
5437
5611
|
});
|
|
5438
5612
|
}
|
|
5439
5613
|
/**
|
|
5440
|
-
* Register the draw switch route. Wiring subset: `settings`, `drawStore
|
|
5614
|
+
* Register the draw switch route. Wiring subset: `settings`, `drawStore`,
|
|
5615
|
+
* `drawToolMount` (the live handle the successful writes converge).
|
|
5441
5616
|
* @param ctx - the host root context.
|
|
5442
5617
|
* @param {object} wiring - as assembled by `apply()` in `index.ts`.
|
|
5443
5618
|
* @returns {Function} the `off()` unregister callback.
|
|
@@ -5454,11 +5629,13 @@ function registerDrawRoute(ctx, wiring) {
|
|
|
5454
5629
|
modelKey: "drawModelId",
|
|
5455
5630
|
modelSourceKey: "drawModelSource",
|
|
5456
5631
|
configModelId: settings.drawModelId,
|
|
5457
|
-
allowedHosts: settings.allowedHosts
|
|
5632
|
+
allowedHosts: settings.allowedHosts,
|
|
5633
|
+
mount: wiring.drawToolMount ?? null
|
|
5458
5634
|
});
|
|
5459
5635
|
}
|
|
5460
5636
|
/**
|
|
5461
|
-
* Register the video switch route. Wiring subset: `settings`, `videoStore
|
|
5637
|
+
* Register the video switch route. Wiring subset: `settings`, `videoStore`,
|
|
5638
|
+
* `videoToolMount` (the live handle the successful writes converge).
|
|
5462
5639
|
* @param ctx - the host root context.
|
|
5463
5640
|
* @param {object} wiring - as assembled by `apply()` in `index.ts`.
|
|
5464
5641
|
* @returns {Function} the `off()` unregister callback.
|
|
@@ -5475,7 +5652,8 @@ function registerVideoRoute(ctx, wiring) {
|
|
|
5475
5652
|
modelKey: "videoModelId",
|
|
5476
5653
|
modelSourceKey: "videoModelSource",
|
|
5477
5654
|
configModelId: settings.videoModelId,
|
|
5478
|
-
allowedHosts: settings.allowedHosts
|
|
5655
|
+
allowedHosts: settings.allowedHosts,
|
|
5656
|
+
mount: wiring.videoToolMount ?? null
|
|
5479
5657
|
});
|
|
5480
5658
|
}
|
|
5481
5659
|
|
|
@@ -5994,7 +6172,7 @@ function readThroughAgnescode(key, cache, inflight, produce) {
|
|
|
5994
6172
|
* @returns {Function} the `off()` unregister callback.
|
|
5995
6173
|
*/
|
|
5996
6174
|
function registerAgnescodeRoute(ctx, wiring) {
|
|
5997
|
-
const { settings, agnescodeStore, agnescodeSwitch, agnescodePublisher, agnescodeModels, cache, inflight, agnescodeFetch } = wiring;
|
|
6175
|
+
const { settings, agnescodeStore, agnescodeSwitch, agnescodePublisher, agnescodeModels, agnescodeRosterSeen, cache, inflight, agnescodeFetch } = wiring;
|
|
5998
6176
|
let lastHarvest = null;
|
|
5999
6177
|
let agnescodeHarvestInFlight = null;
|
|
6000
6178
|
let agnescodeLoginInFlight = null;
|
|
@@ -6067,6 +6245,12 @@ function registerAgnescodeRoute(ctx, wiring) {
|
|
|
6067
6245
|
const publisherState = agnescodePublisher?.state ?? null;
|
|
6068
6246
|
const enabledModelIds = agnescodeModels !== null && agnescodeModels !== void 0 ? await agnescodeModels.listEnabledIds().catch(() => []) : [];
|
|
6069
6247
|
const { usage, ...rateLimit } = await agnescodeStats.read();
|
|
6248
|
+
let newModelIds;
|
|
6249
|
+
if (liveRoster && agnescodeRosterSeen !== null && agnescodeRosterSeen !== void 0) {
|
|
6250
|
+
const offered = roster.map((r) => r.id).filter((id) => typeof id === "string" && id !== "");
|
|
6251
|
+
const noted = await agnescodeRosterSeen.note(offered);
|
|
6252
|
+
if (noted.newIds.length > 0) newModelIds = noted.newIds;
|
|
6253
|
+
}
|
|
6070
6254
|
return {
|
|
6071
6255
|
ok: true,
|
|
6072
6256
|
enabled: effectiveEnabled,
|
|
@@ -6083,6 +6267,7 @@ function registerAgnescodeRoute(ctx, wiring) {
|
|
|
6083
6267
|
models: roster,
|
|
6084
6268
|
modelsSource: liveRoster ? "live" : "fallback",
|
|
6085
6269
|
...liveRoster ? {} : { modelsAsOf: AGNESCODE_FALLBACK_ASOF },
|
|
6270
|
+
...newModelIds !== void 0 ? { newModelIds } : {},
|
|
6086
6271
|
enabledModelIds,
|
|
6087
6272
|
providerRegistered: publisherState?.registered === true,
|
|
6088
6273
|
...publisherState?.error !== null && publisherState?.error !== void 0 ? { providerError: publisherState.error } : {},
|
|
@@ -7384,11 +7569,13 @@ function defineVideoTool({ defineTool, resolveApiKey, getEntries, settings, fetc
|
|
|
7384
7569
|
* - the mount seed (`seedPublisherFromCatalog`): offer models before the
|
|
7385
7570
|
* first poll, fire-and-forget;
|
|
7386
7571
|
* - the two generation tools (opt-in `drawEnabled` / `videoEnabled`,
|
|
7387
|
-
* doubly degraded through the shared {@link
|
|
7572
|
+
* doubly degraded through the shared {@link createToolMount} ladder —
|
|
7573
|
+
* each handle is stored on the wiring bag, so the switch route re-syncs it
|
|
7574
|
+
* live and teardown disposes it);
|
|
7388
7575
|
* - vision step two: the settings-row writer filled for the snapshot route.
|
|
7389
7576
|
*
|
|
7390
7577
|
* and {@link teardown} for the unmount order that PITFALLS §18 pins
|
|
7391
|
-
* (`dispose → release →
|
|
7578
|
+
* (`stop → dispose → release → tools → off` — it must NOT be simplified).
|
|
7392
7579
|
*
|
|
7393
7580
|
* Peer-free discipline: no Host peer is imported here. The only lazy peer
|
|
7394
7581
|
* loads (the adapter / tools modules) are injected from `apply` via `deps`.
|
|
@@ -7402,12 +7589,14 @@ const SERVICE_RETRY_DELAY_MS = 300;
|
|
|
7402
7589
|
/**
|
|
7403
7590
|
* Read an optional Host service with a bounded retry.
|
|
7404
7591
|
*
|
|
7405
|
-
* A service may register AFTER this plugin mounts
|
|
7406
|
-
*
|
|
7407
|
-
*
|
|
7408
|
-
*
|
|
7409
|
-
*
|
|
7410
|
-
*
|
|
7592
|
+
* A service may register AFTER this plugin mounts: a one-shot read misses a
|
|
7593
|
+
* service that arrives a moment late, silently — the draw tool would be
|
|
7594
|
+
* absent with the switch visibly on, and no line on any panel naming the
|
|
7595
|
+
* reason. A late service is catchable LATER too, not only here: the tool
|
|
7596
|
+
* coordinator caches the service once found, but a `sync()` that has not
|
|
7597
|
+
* found one yet re-probes on every run (so a switch flip picks up a tools
|
|
7598
|
+
* service that registered late). The AgnesCode mount seed uses the same
|
|
7599
|
+
* retry for its publish.
|
|
7411
7600
|
*
|
|
7412
7601
|
* This retries the READ ONLY. Whatever it returns is used exactly once by the
|
|
7413
7602
|
* caller: retrying a call that mutates (a tool registration) would register the
|
|
@@ -7444,7 +7633,8 @@ async function resolveServiceWithRetry(ctx, service, options = {}) {
|
|
|
7444
7633
|
return found;
|
|
7445
7634
|
}
|
|
7446
7635
|
/**
|
|
7447
|
-
*
|
|
7636
|
+
* The live mount handle for ONE opt-in agent tool, through the shared
|
|
7637
|
+
* degradation ladder.
|
|
7448
7638
|
*
|
|
7449
7639
|
* `registerDrawTool` and `registerVideoTool` differ only in which switch, which
|
|
7450
7640
|
* model preference, which fetch and which factory they name — everything else
|
|
@@ -7454,42 +7644,84 @@ async function resolveServiceWithRetry(ctx, service, options = {}) {
|
|
|
7454
7644
|
* degrade differently: a tool that vanished on a Host where the other survived
|
|
7455
7645
|
* would be a bug with no visible cause.
|
|
7456
7646
|
*
|
|
7457
|
-
*
|
|
7458
|
-
*
|
|
7459
|
-
*
|
|
7647
|
+
* The returned {@link ToolMount} is what makes the tool HOT: the panel switch
|
|
7648
|
+
* route re-runs `sync()` after it writes the state file, and `sync()` mounts
|
|
7649
|
+
* or unmounts the tool in place using the disposer the host's
|
|
7650
|
+
* `tools.register` returns. Every rung still returns silently rather than
|
|
7651
|
+
* throwing: a Host without a tools service, or a peer that fails to load,
|
|
7652
|
+
* leaves the panel and the provider untouched — the tool is simply absent.
|
|
7653
|
+
*
|
|
7654
|
+
* State is per-handle (a closure), shared by the mount-time seed, every switch
|
|
7655
|
+
* POST and teardown, so the two callers cannot drift into registering the same
|
|
7656
|
+
* tool twice: all reconciliations are serialized behind one promise chain,
|
|
7657
|
+
* each re-reading the CURRENT effective value before it acts.
|
|
7460
7658
|
* @param {object} options - wiring.
|
|
7461
|
-
* @param {
|
|
7462
|
-
* @param {
|
|
7463
|
-
* @param {
|
|
7464
|
-
* @param {
|
|
7465
|
-
* @param {
|
|
7466
|
-
* @param {
|
|
7659
|
+
* @param {HostCtx} options.ctx - the host root context.
|
|
7660
|
+
* @param {HostWiring} options.wiring - see {@link startSideEffects}.
|
|
7661
|
+
* @param {ToolSide} options.side - the test seams from `apply`'s `deps`.
|
|
7662
|
+
* @param {SwitchStore|null|undefined} [options.store] - the panel switch store.
|
|
7663
|
+
* @param {"drawEnabled"|"videoEnabled"} options.enabledKey - the config-default switch key.
|
|
7664
|
+
* @param {"drawModelId"|"videoModelId"} options.modelKey - the config-default model key.
|
|
7467
7665
|
* @param {Function} options.fetchImpl - the request fetch (stubbed in tests).
|
|
7468
7666
|
* @param {Function} options.factory - `defineDrawTool` / `defineVideoTool`.
|
|
7469
|
-
* @returns {
|
|
7667
|
+
* @returns {ToolMount} the live handle; call `sync()` to reconcile.
|
|
7470
7668
|
*/
|
|
7471
|
-
|
|
7669
|
+
function createToolMount({ ctx, wiring, side, store, enabledKey, modelKey, fetchImpl, factory }) {
|
|
7472
7670
|
const { settings, configError, providerState, catalogStore, resolveApiKey, publisher } = wiring;
|
|
7473
|
-
|
|
7474
|
-
|
|
7475
|
-
|
|
7476
|
-
|
|
7477
|
-
|
|
7478
|
-
|
|
7479
|
-
|
|
7480
|
-
|
|
7481
|
-
|
|
7482
|
-
|
|
7483
|
-
|
|
7484
|
-
|
|
7485
|
-
|
|
7486
|
-
|
|
7487
|
-
|
|
7488
|
-
|
|
7489
|
-
|
|
7490
|
-
|
|
7491
|
-
|
|
7492
|
-
|
|
7671
|
+
let tools = null;
|
|
7672
|
+
let registered = false;
|
|
7673
|
+
let disposer = null;
|
|
7674
|
+
let regModel = null;
|
|
7675
|
+
let closed = false;
|
|
7676
|
+
let chain = Promise.resolve();
|
|
7677
|
+
const safeCall = (fn) => {
|
|
7678
|
+
if (fn === null) return;
|
|
7679
|
+
try {
|
|
7680
|
+
fn();
|
|
7681
|
+
} catch {}
|
|
7682
|
+
};
|
|
7683
|
+
async function syncImpl() {
|
|
7684
|
+
if (closed) return;
|
|
7685
|
+
if (configError !== null) return;
|
|
7686
|
+
const panelEnabled = await readPanelValue(async () => await store?.enabled() ?? null);
|
|
7687
|
+
const panelModel = await readPanelValue(async () => await store?.modelId() ?? null);
|
|
7688
|
+
const { enabled } = resolveSwitchEnabled(panelEnabled, settings[enabledKey]);
|
|
7689
|
+
const { value: targetModel } = resolveSwitchValue(panelModel, settings[modelKey]);
|
|
7690
|
+
if (enabled !== true) {
|
|
7691
|
+
if (registered) {
|
|
7692
|
+
const d = disposer;
|
|
7693
|
+
registered = false;
|
|
7694
|
+
disposer = null;
|
|
7695
|
+
regModel = null;
|
|
7696
|
+
safeCall(d);
|
|
7697
|
+
}
|
|
7698
|
+
return;
|
|
7699
|
+
}
|
|
7700
|
+
if (registered && regModel === targetModel) return;
|
|
7701
|
+
if (registered && disposer === null) return;
|
|
7702
|
+
if (registered && disposer !== null) {
|
|
7703
|
+
const d = disposer;
|
|
7704
|
+
registered = false;
|
|
7705
|
+
disposer = null;
|
|
7706
|
+
regModel = null;
|
|
7707
|
+
safeCall(d);
|
|
7708
|
+
}
|
|
7709
|
+
const service = tools ?? await resolveServiceWithRetry(ctx, "tools", { isDisposed: () => publisher.isDisposed() });
|
|
7710
|
+
if (service === null || typeof service.register !== "function") return;
|
|
7711
|
+
tools = service;
|
|
7712
|
+
let defineTool;
|
|
7713
|
+
try {
|
|
7714
|
+
const mod = await Promise.resolve(side.loadToolsModule?.());
|
|
7715
|
+
defineTool = mod?.defineTool ?? mod?.default?.defineTool ?? null;
|
|
7716
|
+
} catch {
|
|
7717
|
+
return;
|
|
7718
|
+
}
|
|
7719
|
+
if (typeof defineTool !== "function") return;
|
|
7720
|
+
const effectiveSettings = targetModel !== settings[modelKey] ? {
|
|
7721
|
+
...settings,
|
|
7722
|
+
[modelKey]: targetModel
|
|
7723
|
+
} : settings;
|
|
7724
|
+
const definition = factory({
|
|
7493
7725
|
defineTool,
|
|
7494
7726
|
resolveApiKey,
|
|
7495
7727
|
getEntries: async () => {
|
|
@@ -7499,21 +7731,48 @@ async function mountAgentTool({ ctx, wiring, side, store, enabledKey, modelKey,
|
|
|
7499
7731
|
settings: effectiveSettings,
|
|
7500
7732
|
fetchImpl,
|
|
7501
7733
|
isDisposed: () => publisher.isDisposed()
|
|
7502
|
-
})
|
|
7503
|
-
|
|
7734
|
+
});
|
|
7735
|
+
let newDisposer;
|
|
7736
|
+
try {
|
|
7737
|
+
newDisposer = service.register(definition);
|
|
7738
|
+
} catch {
|
|
7739
|
+
return;
|
|
7740
|
+
}
|
|
7741
|
+
registered = true;
|
|
7742
|
+
regModel = targetModel;
|
|
7743
|
+
disposer = typeof newDisposer === "function" ? newDisposer : null;
|
|
7744
|
+
}
|
|
7745
|
+
return {
|
|
7746
|
+
sync: () => {
|
|
7747
|
+
const run = chain.then(() => syncImpl());
|
|
7748
|
+
chain = run.then(() => void 0, () => void 0);
|
|
7749
|
+
return run;
|
|
7750
|
+
},
|
|
7751
|
+
dispose: () => {
|
|
7752
|
+
closed = true;
|
|
7753
|
+
const d = disposer;
|
|
7754
|
+
registered = false;
|
|
7755
|
+
disposer = null;
|
|
7756
|
+
regModel = null;
|
|
7757
|
+
safeCall(d);
|
|
7758
|
+
},
|
|
7759
|
+
isMounted: () => registered
|
|
7760
|
+
};
|
|
7504
7761
|
}
|
|
7505
7762
|
/**
|
|
7506
|
-
*
|
|
7507
|
-
* Opt-in (`drawEnabled`, default off) and
|
|
7508
|
-
*
|
|
7509
|
-
* it
|
|
7763
|
+
* Create the live mount handle for the `agnes_draw_image` agent tool
|
|
7764
|
+
* (ARCHITECTURE.md §5.4, route B). Opt-in (`drawEnabled`, default off) and
|
|
7765
|
+
* doubly degraded. The handle is stored on the wiring bag (`wiring.drawToolMount`)
|
|
7766
|
+
* so the switch route can re-sync it live and teardown can dispose it; the
|
|
7767
|
+
* initial `sync()` seeds the mount-time behavior (register now when the
|
|
7768
|
+
* effective value is on).
|
|
7510
7769
|
* @param ctx - the host root context (reads `ctx.get("tools")` / `ctx.tools`).
|
|
7511
7770
|
* @param {object} wiring - see {@link startSideEffects}.
|
|
7512
7771
|
* @param {object} side - test seams from `apply`'s `deps`.
|
|
7513
|
-
* @returns {
|
|
7772
|
+
* @returns {ToolMount} the live handle, also stored at `wiring.drawToolMount`.
|
|
7514
7773
|
*/
|
|
7515
|
-
|
|
7516
|
-
|
|
7774
|
+
function registerDrawTool(ctx, wiring, side) {
|
|
7775
|
+
const mount = createToolMount({
|
|
7517
7776
|
ctx,
|
|
7518
7777
|
wiring,
|
|
7519
7778
|
side,
|
|
@@ -7523,21 +7782,25 @@ async function registerDrawTool(ctx, wiring, side) {
|
|
|
7523
7782
|
fetchImpl: side.drawFetch,
|
|
7524
7783
|
factory: defineDrawTool
|
|
7525
7784
|
});
|
|
7785
|
+
wiring.drawToolMount = mount;
|
|
7786
|
+
mount.sync();
|
|
7787
|
+
return mount;
|
|
7526
7788
|
}
|
|
7527
7789
|
/**
|
|
7528
|
-
*
|
|
7790
|
+
* Create the live mount handle for the `agnes_video_generate` agent tool
|
|
7791
|
+
* (`video.ts`).
|
|
7529
7792
|
*
|
|
7530
7793
|
* A SEPARATE opt-in from the draw tool (`videoEnabled`), sharing only the
|
|
7531
|
-
* mounting ladder — see {@link
|
|
7794
|
+
* mounting ladder — see {@link createToolMount}. The protocol is the reason the
|
|
7532
7795
|
* two are not one module: video is an asynchronous task, so this tool creates a
|
|
7533
7796
|
* task and then polls it for minutes rather than issuing one request.
|
|
7534
7797
|
* @param ctx - the host root context.
|
|
7535
7798
|
* @param {object} wiring - see {@link startSideEffects}.
|
|
7536
7799
|
* @param {object} side - test seams from `apply`'s `deps`.
|
|
7537
|
-
* @returns {
|
|
7800
|
+
* @returns {ToolMount} the live handle, also stored at `wiring.videoToolMount`.
|
|
7538
7801
|
*/
|
|
7539
|
-
|
|
7540
|
-
|
|
7802
|
+
function registerVideoTool(ctx, wiring, side) {
|
|
7803
|
+
const mount = createToolMount({
|
|
7541
7804
|
ctx,
|
|
7542
7805
|
wiring,
|
|
7543
7806
|
side,
|
|
@@ -7547,6 +7810,9 @@ async function registerVideoTool(ctx, wiring, side) {
|
|
|
7547
7810
|
fetchImpl: side.videoFetch,
|
|
7548
7811
|
factory: defineVideoTool
|
|
7549
7812
|
});
|
|
7813
|
+
wiring.videoToolMount = mount;
|
|
7814
|
+
mount.sync();
|
|
7815
|
+
return mount;
|
|
7550
7816
|
}
|
|
7551
7817
|
/**
|
|
7552
7818
|
* Run the mount-time side effects: the persisted-catalog seed, the draw tool
|
|
@@ -7620,7 +7886,9 @@ function startSideEffects(ctx, wiring, side) {
|
|
|
7620
7886
|
* must not register into a Host that is letting this plugin go;
|
|
7621
7887
|
* 2. `release` — stop offering the provider first, so a request cannot be
|
|
7622
7888
|
* routed to an adapter whose Host services are already half gone;
|
|
7623
|
-
* 3. the
|
|
7889
|
+
* 3. the live agent tools (draw / video) are unmounted, so an in-flight
|
|
7890
|
+
* switch handler cannot re-sync a tool into a Host that is going away;
|
|
7891
|
+
* 4. the route `off()` callbacks, each guarded (the web server may already
|
|
7624
7892
|
* be gone during shutdown).
|
|
7625
7893
|
*
|
|
7626
7894
|
* This order is a concurrency fix and must NOT be simplified.
|
|
@@ -7640,6 +7908,8 @@ function teardown(wiring, offs, stopTick) {
|
|
|
7640
7908
|
agnescodePublisher?.dispose();
|
|
7641
7909
|
agnescodePublisher?.release?.();
|
|
7642
7910
|
releaseProvider();
|
|
7911
|
+
wiring.drawToolMount?.dispose();
|
|
7912
|
+
wiring.videoToolMount?.dispose();
|
|
7643
7913
|
for (const off of offs) try {
|
|
7644
7914
|
off();
|
|
7645
7915
|
} catch {}
|
|
@@ -7760,6 +8030,7 @@ function apply(ctx, config = {}, deps = {}) {
|
|
|
7760
8030
|
const agnescodeStore = createAgnescodeStore({ credentials: () => ctx.get("credentials") ?? null });
|
|
7761
8031
|
const agnescodeSwitch = createFileAgnescodeStore({ profile });
|
|
7762
8032
|
const agnescodeModels = createFileAgnescodeModelsStore({ profile });
|
|
8033
|
+
const agnescodeRosterSeen = createFileAgnescodeRosterSeenStore();
|
|
7763
8034
|
const { publisher: agnescodePublisher, seed: agnescodeSeed } = wireAgnescodePublisher({
|
|
7764
8035
|
store: agnescodeStore,
|
|
7765
8036
|
panelSwitch: () => agnescodeSwitch.enabled().catch(() => null),
|
|
@@ -7806,6 +8077,7 @@ function apply(ctx, config = {}, deps = {}) {
|
|
|
7806
8077
|
agnescodeStore,
|
|
7807
8078
|
agnescodeSwitch,
|
|
7808
8079
|
agnescodeModels,
|
|
8080
|
+
agnescodeRosterSeen,
|
|
7809
8081
|
agnescodePublisher,
|
|
7810
8082
|
agnescodeFetch: deps.agnescodeFetch ?? ((url, options) => fetch(url, options))
|
|
7811
8083
|
};
|
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.13.
|
|
5
|
+
"version": "0.13.3",
|
|
6
6
|
"description": "One-tap connect Agnes language, image and video models into DSH (off by default), with image recognition and thinking level filled in automatically. After signing in to the Agnes console (re-login uses AGNES_PASSWORD), you can see the quota windows (5-hour requests, weekly requests, daily images, video), account-wide cumulative usage, and a usage pill beside the input box. AgnesCode desktop models can be connected too (Windows only), re-reading the desktop app login when the token expires, with rate-limit hits backing off on their own.",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"dsh-plugin",
|