dsh-web-search-enhanced 0.1.3 → 0.1.4
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 -7
- package/docs/configuration.zh-CN.md +2 -2
- package/docs/design-v2.converged.zh-CN.md +2 -2
- package/docs/migration.zh-CN.md +6 -6
- package/docs/upstream-0.1.7-rc.2-assessment.zh-CN.md +35 -0
- package/docs/v2-implementation.zh-CN.md +4 -4
- package/lib/client.js +217 -55
- package/lib/client.js.map +1 -1
- package/lib/types/client/SearchConnectionSelector.d.ts +2 -1
- package/lib/types/client/bindings.d.ts +5 -5
- package/lib/types/client/selection-client.d.ts +27 -0
- package/package.json +54 -54
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
记录 `dsh-web-search-enhanced` 的用户可见变化。历史条目描述对应版本,不代表当前行为。
|
|
4
4
|
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [0.1.4] - 2026-09-24
|
|
8
|
+
|
|
9
|
+
### DSH 0.1.7-rc.2 兼容性
|
|
10
|
+
|
|
11
|
+
- 最低宿主版本提升至 `0.1.7-rc.2`;DSH peer 依赖下限、精确开发依赖、锁文件及预发布安装白名单同步对齐。
|
|
12
|
+
- 核对动态工具更新、DeepSeek Provider 拆包、模型选择校验、Schedule 与审批变更;插件不调用 selectModel,不依赖被移除的 Schedule API,无需修改搜索运行时代码。
|
|
13
|
+
- 跟随模型仍仅支持可解析的 API Key 路由,不因宿主支持官方账号而自动支持账号 / OAuth 搜索;固定连接及结构化搜索不受模型选择校验影响。
|
|
14
|
+
- 设置格式 version 2、会话 Storage Domain version 1 保持不变;完整边界见 [rc.2 影响评估](docs/upstream-0.1.7-rc.2-assessment.zh-CN.md)。
|
|
15
|
+
|
|
16
|
+
### 搜索选择器请求优化
|
|
17
|
+
|
|
18
|
+
- 移除每 2 秒常驻轮询;首次加载、模型实际变化、展开菜单及页面恢复可见/焦点时按需读取,关闭菜单和普通重渲染不请求。
|
|
19
|
+
- 增加按会话隔离的 30 秒客户端缓存与并发请求合并,最多保留 64 个空闲会话项;TTL 到期本身不会触发请求。
|
|
20
|
+
- 写入成功直接复用返回值;手动重试和写入冲突刷新绕过缓存,模型/配置失效发生在请求期间时合并补读,防止旧响应覆盖新状态。
|
|
21
|
+
- 插件配置变化及本插件保存凭据后使缓存失效;连接断开/重建与插件卸载清理缓存并隔离旧请求。
|
|
22
|
+
- 本阶段不增加服务端推送:其他窗口的修改在后续缓存过期的按需刷新时同步,不再保证两秒内自动同步;版本冲突保护保留。
|
|
23
|
+
|
|
5
24
|
## [0.1.3] - 2026-09-24
|
|
6
25
|
|
|
7
26
|
### 界面细节对齐
|
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
[](https://github.com/Yurzi/dsh-web-search-enhanced/releases)
|
|
12
12
|
[](LICENSE)
|
|
13
|
-
[](package.json)
|
|
14
14
|
[](package.json)
|
|
15
15
|
|
|
16
16
|
[🚀 快速上手](#-快速上手) · [✨ 核心特性](#-核心特性) · [🔍 搜索引擎推荐](#-选哪种搜索服务) · [⚙️ 进阶配置](#️-进阶配置与个人-key) · [❓ 常见问题](#-常见问题-faq) · [📖 详细文档](#-想了解更多)
|
|
@@ -60,27 +60,27 @@
|
|
|
60
60
|
|
|
61
61
|
### 环境要求
|
|
62
62
|
|
|
63
|
-
- **DeepSeek Harness**:≥ `0.1.7-rc.
|
|
63
|
+
- **DeepSeek Harness**:≥ `0.1.7-rc.2`
|
|
64
64
|
- **Node.js**:`^22.19.0 || >=24.0.0`
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
本版将 DSH 基线提升到 0.1.7-rc.2,保留动态配置、插件设置页和会话 projection 架构,详见[rc.2 影响评估](docs/upstream-0.1.7-rc.2-assessment.zh-CN.md)。升级前请备份 profile 和旧设置。
|
|
67
67
|
|
|
68
68
|
### 步骤 1:安装插件
|
|
69
69
|
|
|
70
70
|
**方式 A:通过 GitHub Release 离线包安装(推荐)**
|
|
71
71
|
|
|
72
|
-
当前版本为 **0.1.
|
|
72
|
+
当前版本为 **0.1.4**,最低支持 **DSH 0.1.7-rc.2**。发布后可从 [Releases](https://github.com/Yurzi/dsh-web-search-enhanced/releases) 下载对应版本安装包及 SHA256 校验文件;也可执行 `pnpm install --frozen-lockfile && pnpm run check && pnpm pack` 自行构建。使用绝对路径安装:
|
|
73
73
|
|
|
74
74
|
```sh
|
|
75
|
-
dsh plugin --profile web add /path/to/dsh-web-search-enhanced-0.1.
|
|
75
|
+
dsh plugin --profile web add /path/to/dsh-web-search-enhanced-0.1.4.tgz
|
|
76
76
|
```
|
|
77
77
|
|
|
78
78
|
**方式 B:通过 npm 在线安装**
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
该版本发布到 npm 后,也可直接安装:
|
|
81
81
|
|
|
82
82
|
```sh
|
|
83
|
-
dsh plugin --profile web add dsh-web-search-enhanced@0.1.
|
|
83
|
+
dsh plugin --profile web add dsh-web-search-enhanced@0.1.4
|
|
84
84
|
```
|
|
85
85
|
|
|
86
86
|
### 步骤 2:生效与刷新
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# 0.1.
|
|
1
|
+
# 0.1.4 配置参考
|
|
2
2
|
|
|
3
|
-
本指南面向安装版插件。包版本为 **0.1.
|
|
3
|
+
本指南面向安装版插件。包版本为 **0.1.4**,配置格式的 `version` 为 **2**;两者不是同一版本号。快速开始见 [README](../README.md),旧版用户先读[升级指南](migration.zh-CN.md)。
|
|
4
4
|
|
|
5
5
|
## 配置入口与作用范围
|
|
6
6
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# 0.1.
|
|
1
|
+
# 0.1.4 当前架构与实现边界
|
|
2
2
|
|
|
3
3
|
本文描述已存在的源码契约,不是未来设计提案或历史验收记录。为避免外部链接失效,保留原文件名。用户配置见[配置参考](configuration.zh-CN.md),验证方法见[开发验证指南](v2-implementation.zh-CN.md)。
|
|
4
4
|
|
|
@@ -46,7 +46,7 @@ tools/execute 的 exec.agent → 私有 AsyncLocalStorage → enhanced-search
|
|
|
46
46
|
|
|
47
47
|
## 3. 配置和秘密分离
|
|
48
48
|
|
|
49
|
-
当前设置格式为 version 2,包版本为 0.1.
|
|
49
|
+
当前设置格式为 version 2,包版本为 0.1.4。内置目录只做默认值;用户保存差异,不把目录整表复制到用户层。四种结构化内置连接默认 keyless,访问方式为显式选择,不根据已保存 Key 猜测。自定义连接只允许支持的 adapter 或固定模型协议;新 endpoint 需要信任确认。
|
|
50
50
|
|
|
51
51
|
内置结构化连接不能重定向地址或改变 adapter。自定义结构化连接只能使用个人 Key;固定模型总是需要显式信任。校验拒绝任意 headers/body、缓存参数、秘密字段等不受支持的 options。禁用连接也参与配置校验。
|
|
52
52
|
|
package/docs/migration.zh-CN.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# 升级到 0.1.
|
|
1
|
+
# 升级到 0.1.4 / DSH 0.1.7-rc.2
|
|
2
2
|
|
|
3
|
-
0.1.
|
|
3
|
+
0.1.4 将最低宿主版本提高为 **0.1.7-rc.2**,沿用 0.1.3 已完成的 Loader 动态配置、profile-backed SettingsForms、ConfigForms 客户端、惰性 Typert codec 和新版 PTC 适配。从 0.1.3 升级不需要新的配置或存储迁移;详见 [rc.2 影响评估](upstream-0.1.7-rc.2-assessment.zh-CN.md)。旧版已采用的**会话级搜索连接**保持不变:包版本 0.1.4、设置格式 version 2、会话 Storage Domain version 1 是三个不同概念。完整参数见[配置参考](configuration.zh-CN.md)。
|
|
4
4
|
|
|
5
5
|
## 1. 升级前准备
|
|
6
6
|
|
|
7
|
-
- 确认 DSH 满足 `>=0.1.7-rc.
|
|
7
|
+
- 确认 DSH 满足 `>=0.1.7-rc.2`,Node 满足 `^22.19.0 || >=24.0.0`。实现基线是 0.1.7-rc.2 契约,不代表对所有未来宿主版本均完成验证。
|
|
8
8
|
- 备份当前插件版本、Web profile / 插件配置和 DSH 持久存储。凭据使用宿主安全备份方式,不复制到公开 Issue 或仓库。
|
|
9
9
|
- 记录旧 `modelMode`、协议、模型、endpoint、凭据引用,以及希望保留的会话选择。
|
|
10
10
|
- 推荐升级前先将明文 `apiKey` 移到 DSH Credentials 并从配置来源删除。兼容导入仅允许旧格式短暂携带 Key:先验证转换结果,再写 Credentials,最后 CAS 替换 profile 配置。V2 配置仍拒绝明文 Key;已有不同值的同名凭据不会被自动覆盖。
|
|
@@ -12,16 +12,16 @@
|
|
|
12
12
|
|
|
13
13
|
## 2. 安装发行包
|
|
14
14
|
|
|
15
|
-
从当前源码运行 `pnpm install --frozen-lockfile && pnpm run check && pnpm pack`,或使用 [Releases](https://github.com/Yurzi/dsh-web-search-enhanced/releases) 中实际发布的 0.1.
|
|
15
|
+
从当前源码运行 `pnpm install --frozen-lockfile && pnpm run check && pnpm pack`,或使用 [Releases](https://github.com/Yurzi/dsh-web-search-enhanced/releases) 中实际发布的 0.1.4 构建包。使用绝对路径交给 DSH 插件管理器:
|
|
16
16
|
|
|
17
17
|
```sh
|
|
18
|
-
dsh plugin --profile web add /absolute/path/to/dsh-web-search-enhanced-0.1.
|
|
18
|
+
dsh plugin --profile web add /absolute/path/to/dsh-web-search-enhanced-0.1.4.tgz
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
若该版本已在 npm 发布,也可安装固定版本:
|
|
22
22
|
|
|
23
23
|
```sh
|
|
24
|
-
dsh plugin --profile web add dsh-web-search-enhanced@0.1.
|
|
24
|
+
dsh plugin --profile web add dsh-web-search-enhanced@0.1.4
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
发布附件中的构建 tgz 与 GitHub 自动生成的 Source code archive 不同:后者是源码,不能假定含 `lib/` 构建产物。npm 可用性须以 registry 实际发布结果为准,GitHub 标签 / Release 存在不代表 npm 已完成。
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# DSH 0.1.7-rc.2 对插件 0.1.4 的影响评估
|
|
2
|
+
|
|
3
|
+
## 范围与结论
|
|
4
|
+
|
|
5
|
+
根据上游 rc.1 → rc.2 版本分析,逐项核对本插件实际调用路径,并将开发依赖切换到发布的 rc.2 包验证。上游范围为 [dsh-v0.1.7-rc.1 … dsh-v0.1.7-rc.2](https://github.com/deepseek-ai/deepseek-harness/compare/dsh-v0.1.7-rc.1...dsh-v0.1.7-rc.2)。本文不是对上游全部提交的重新审计。
|
|
6
|
+
|
|
7
|
+
结论:升级风险低,未发现需要修改本插件运行时代码的直接破坏性变更;跟随模型存在原有认证与私有 adapter 兼容边界,不能据此承诺所有 Provider 均可搜索。
|
|
8
|
+
|
|
9
|
+
- 插件:`0.1.3` → `0.1.4`(patch)。
|
|
10
|
+
- 最低 DSH:`>=0.1.7-rc.2`;DSH peer 范围:`^0.1.7-rc.2`;直接 DSH 开发依赖:精确 `0.1.7-rc.2`。
|
|
11
|
+
- 设置格式仍为 version 2,Storage Domain 仍为 version 1,不重写宿主会话历史。
|
|
12
|
+
- 原 Unreleased 中的选择器按需刷新、缓存与并发合并优化一并归入 0.1.4。
|
|
13
|
+
|
|
14
|
+
## 逐项影响
|
|
15
|
+
|
|
16
|
+
| 上游变化 | 插件实际依赖与结论 |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| 动态工具增删、ToolHistory 与历史投影 | [bridge](../src/dsh/bridge.ts) 只注册 `enhanced-search` Web provider,使用 `agent/request` 和 `tools/execute` 捕获请求上下文;不构造工具增删消息、不重写历史。继续由宿主原生 `web_search` 和 PTC 链执行,无需自行接入投影协议。 |
|
|
19
|
+
| DeepSeek Provider 拆分为 API Key / Account 插件 | 依赖、测试和 [安装补丁](../cordis.patch.yml) 均不把旧 `dsh-llm-deepseek` 作为 Cordis 插件加载。补丁中的 `web-search-deepseek` 是原生搜索插件条目,不是被拆分的 LLM 插件,不能误改成新 Provider 包名。用户自行维护的宿主 profile 仍需按上游规则迁移。 |
|
|
20
|
+
| `selectModel` 严格校验 `listModels` | 插件不调用 `selectModel`,也不通过切换聊天模型执行辅助搜索。[跟随模型](../src/dsh/session-model.ts) 读取实际请求头与公开配置目录;固定模型直接使用显式 binding。因此不是所有固定搜索模型都要注册到宿主。用户切换聊天模型时则须确保模型在宿主目录中,否则会先被宿主拒绝。 |
|
|
21
|
+
| Schedule 存储导出删除、默认禁用 schedule/time-context | 插件不依赖 Schedule 或时间上下文服务。自身选择存储通过 Storage Domain,不涉及计划任务存储;实时性参数是缓存年龄偏好,也不依赖注入时钟。 |
|
|
22
|
+
| `PreToolDecision.displayReason`、Auto Review 调整 | 插件不构造该准入决策,不接管审批;`tools/execute` 钩子继续调用 next,保留宿主授权与审批链。新增可选展示字段无需适配。 |
|
|
23
|
+
| 快捷键中心、代码语言与系统提示词调整 | 插件不导入旧快捷键内部 API、不使用代码语言内部模块、不解析系统提示词。设置页、会话 projection 与插件配置槽的类型和构建由 rc.2 依赖验证;不能以编译通过替代真实 GUI 验收。 |
|
|
24
|
+
|
|
25
|
+
## 跟随模型边界
|
|
26
|
+
|
|
27
|
+
[session-model.ts](../src/dsh/session-model.ts) 仍通过 `listConfigurableProviders()` 的 settings namespace/path 获取非秘密路由信息,要求受支持的 HTTP 协议、endpoint 和显式 `apiKeyEnv`。官方账号、OAuth、订阅凭据不会被重建或借用。新官方 API Key Provider 也不能仅因包名包含 API Key 就视为自动兼容,仍须满足这些路由条件。
|
|
28
|
+
|
|
29
|
+
adapter identity 的 `registration()` 及可选 pi-ai catalog 是原有兼容 shim,并非稳定公共契约;缺失或发生替换时保持 fail-closed,不隐式改用其他连接或凭据。遇到跟随不可用可显式选择内置结构化连接或受支持的固定搜索连接。
|
|
30
|
+
|
|
31
|
+
## 升级与验收
|
|
32
|
+
|
|
33
|
+
先升级 DSH,再安装 0.1.4,重新加载服务端插件并刷新 Web 页面。从 0.1.3 升级应保留连接选择、凭据和实时性偏好,无需清空存储。选择器不再常驻两秒轮询;跨窗口修改在后续缓存过期的按需刷新时同步。
|
|
34
|
+
|
|
35
|
+
本次验证通过:冻结锁文件安装、TypeScript 类型检查、服务端 / 客户端构建、22 个测试文件共 339 项测试,以及 0.1.4 实际打包契约检查。执行命令为 `PNPM_HOME="$PWD/.pnpm-home" pnpm install --frozen-lockfile` 与 `PNPM_HOME="$PWD/.pnpm-home" pnpm run check`;显式设置工作区 PNPM_HOME 是为解决本机默认 pnpm 状态数据库打开失败,不是插件运行要求。自动测试使用模拟网络,不证明外部搜索服务实时可用;本次版本更新不等于 npm/GitHub 发布,也不等于运行中的 DSH 已安装新版本。
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
# 0.1.
|
|
1
|
+
# 0.1.4 开发与验证指南
|
|
2
2
|
|
|
3
3
|
本文件保留原路径以避免断链,内容改为可重复的开发、验证与交付方法。架构见[当前架构](design-v2.converged.zh-CN.md),用户使用见[配置参考](configuration.zh-CN.md),版本升级见[升级指南](migration.zh-CN.md)。测试存在、测试通过、供应商在线可用、运行实例完成部署是四种不同结论,不能互相替代。
|
|
4
4
|
|
|
5
5
|
## 环境与开发入口
|
|
6
6
|
|
|
7
7
|
- Node.js:`^22.19.0 || >=24.0.0`;pnpm:按 packageManager 使用 `11.7.0`。
|
|
8
|
-
- DSH:包要求 `>=0.1.7-rc.
|
|
9
|
-
- 当前开发主线为 `main`,旧版代码线为 `legacy/v0.0.x`。包版本 `0.1.
|
|
8
|
+
- DSH:包要求 `>=0.1.7-rc.2`,源码以 0.1.7-rc.2 服务契约为基线。依赖声明与锁文件是构建依据,不修改宿主安装的依赖来让测试通过。
|
|
9
|
+
- 当前开发主线为 `main`,旧版代码线为 `legacy/v0.0.x`。包版本 `0.1.4` 与 settings schema 的 `version: 2` 不同。
|
|
10
10
|
|
|
11
11
|
在仓库根执行:
|
|
12
12
|
|
|
@@ -101,7 +101,7 @@ preview-ui 写 `.dsh-smoke-home/settings-preview.html`,不会启动第二个 D
|
|
|
101
101
|
|
|
102
102
|
1. 对最终待发提交运行 check 和所需浏览器 / 网络验证,记录版本与退出状态。
|
|
103
103
|
2. 使用 `pnpm pack` 生成构建包;检查包版本、入口、声明、patch、README 与 docs 完整性。GitHub 自动 Source code archive 不是含 lib 的发行 tgz。
|
|
104
|
-
3. 用户可用 `dsh plugin --profile web add /absolute/path/to/dsh-web-search-enhanced-0.1.
|
|
104
|
+
3. 用户可用 `dsh plugin --profile web add /absolute/path/to/dsh-web-search-enhanced-0.1.4.tgz` 安装 Release 构建附件。
|
|
105
105
|
4. Git 操作、远端分支调整、标签、Release 和 npm 发布分别需要授权与结果核验;不要把本地构建当作已发布。主线为 main,旧代码保留 legacy/v0.0.x,不需要把历史文档反复改成当前成功记录。
|
|
106
106
|
5. 标签触发发布流程后,单独确认 workflow 和 npm registry 的版本 / 包内容,再宣称 npm 可安装。
|
|
107
107
|
|
package/lib/client.js
CHANGED
|
@@ -6288,71 +6288,85 @@ window.__ModuleLoader__.load({
|
|
|
6288
6288
|
}
|
|
6289
6289
|
function SessionSelector({ sessionId, getSelection, setSelection, useProjection, t }) {
|
|
6290
6290
|
const modelSelection = useProjection("modelSelection");
|
|
6291
|
+
const modelKey = JSON.stringify(modelSelection ? [
|
|
6292
|
+
modelSelection.lastUsed?.provider,
|
|
6293
|
+
modelSelection.lastUsed?.model,
|
|
6294
|
+
modelSelection.lastUsed?.reasoningEffort,
|
|
6295
|
+
modelSelection.next?.provider,
|
|
6296
|
+
modelSelection.next?.model,
|
|
6297
|
+
modelSelection.next?.reasoningEffort
|
|
6298
|
+
] : null);
|
|
6299
|
+
const latest = (0, react.useRef)({
|
|
6300
|
+
getSelection,
|
|
6301
|
+
setSelection,
|
|
6302
|
+
t,
|
|
6303
|
+
modelKey
|
|
6304
|
+
});
|
|
6305
|
+
latest.current = {
|
|
6306
|
+
getSelection,
|
|
6307
|
+
setSelection,
|
|
6308
|
+
t,
|
|
6309
|
+
modelKey
|
|
6310
|
+
};
|
|
6291
6311
|
const [value, setValue] = (0, react.useState)();
|
|
6292
6312
|
const [error, setError] = (0, react.useState)("");
|
|
6293
6313
|
const [notice, setNotice] = (0, react.useState)("");
|
|
6294
6314
|
const [pending, setPending] = (0, react.useState)(false);
|
|
6295
6315
|
const controller = (0, react.useRef)();
|
|
6296
6316
|
(0, react.useEffect)(() => {
|
|
6297
|
-
const
|
|
6317
|
+
const reads = createResponseGuard();
|
|
6318
|
+
const writes = createResponseGuard();
|
|
6298
6319
|
let writing = false;
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
setError("");
|
|
6302
|
-
setNotice("");
|
|
6303
|
-
setPending(false);
|
|
6304
|
-
const refresh = async (clearError = false) => {
|
|
6305
|
-
if (writing || reading) return;
|
|
6306
|
-
reading = true;
|
|
6307
|
-
const token = guard.begin();
|
|
6320
|
+
const refresh = async (force = false, clearError = true) => {
|
|
6321
|
+
const token = reads.begin();
|
|
6308
6322
|
try {
|
|
6309
|
-
const result = await getSelection(
|
|
6310
|
-
|
|
6323
|
+
const result = await latest.current.getSelection({
|
|
6324
|
+
force,
|
|
6325
|
+
modelKey: latest.current.modelKey
|
|
6326
|
+
});
|
|
6327
|
+
if (!reads.accepts(token)) return;
|
|
6311
6328
|
if (result.ok) {
|
|
6312
6329
|
setValue(result.value);
|
|
6313
6330
|
if (clearError) setError("");
|
|
6314
6331
|
} else setError(result.error.message);
|
|
6315
6332
|
} catch {
|
|
6316
|
-
if (
|
|
6317
|
-
} finally {
|
|
6318
|
-
reading = false;
|
|
6333
|
+
if (reads.accepts(token)) setError(latest.current.t("selectionReadFailed"));
|
|
6319
6334
|
}
|
|
6320
6335
|
};
|
|
6321
6336
|
const select = async (connectionId, expectedRevision, freshness) => {
|
|
6322
6337
|
if (writing) return;
|
|
6323
6338
|
writing = true;
|
|
6324
|
-
|
|
6339
|
+
reads.begin();
|
|
6340
|
+
const token = writes.begin();
|
|
6341
|
+
const writeModelKey = latest.current.modelKey;
|
|
6325
6342
|
setPending(true);
|
|
6326
6343
|
setError("");
|
|
6327
6344
|
setNotice("");
|
|
6328
6345
|
let refreshAfterFailure = false;
|
|
6329
6346
|
try {
|
|
6330
|
-
const result = await setSelection({
|
|
6347
|
+
const result = await latest.current.setSelection({
|
|
6331
6348
|
...connectionId !== void 0 ? { connectionId } : {},
|
|
6332
6349
|
...freshness !== void 0 ? { freshness } : {},
|
|
6333
6350
|
expectedRevision
|
|
6334
6351
|
});
|
|
6335
|
-
if (!
|
|
6352
|
+
if (!writes.accepts(token)) return;
|
|
6336
6353
|
if (result.ok) {
|
|
6337
|
-
setValue(result.value);
|
|
6338
|
-
setNotice(t("selectionSaved"));
|
|
6354
|
+
if (writeModelKey === latest.current.modelKey) setValue(result.value);
|
|
6355
|
+
setNotice(latest.current.t("selectionSaved"));
|
|
6339
6356
|
} else {
|
|
6340
|
-
setError(result.error.message + t("selectionRefreshed"));
|
|
6357
|
+
setError(result.error.message + latest.current.t("selectionRefreshed"));
|
|
6341
6358
|
refreshAfterFailure = true;
|
|
6342
6359
|
}
|
|
6343
6360
|
} catch {
|
|
6344
|
-
if (
|
|
6345
|
-
setError(t("selectionWriteFailed"));
|
|
6361
|
+
if (writes.accepts(token)) {
|
|
6362
|
+
setError(latest.current.t("selectionWriteFailed"));
|
|
6346
6363
|
refreshAfterFailure = true;
|
|
6347
6364
|
}
|
|
6348
6365
|
} finally {
|
|
6349
6366
|
writing = false;
|
|
6350
|
-
if (
|
|
6367
|
+
if (writes.accepts(token)) {
|
|
6351
6368
|
setPending(false);
|
|
6352
|
-
if (refreshAfterFailure)
|
|
6353
|
-
reading = false;
|
|
6354
|
-
refresh();
|
|
6355
|
-
}
|
|
6369
|
+
if (refreshAfterFailure) refresh(true, false);
|
|
6356
6370
|
}
|
|
6357
6371
|
}
|
|
6358
6372
|
};
|
|
@@ -6360,29 +6374,22 @@ window.__ModuleLoader__.load({
|
|
|
6360
6374
|
refresh,
|
|
6361
6375
|
select
|
|
6362
6376
|
};
|
|
6363
|
-
refresh();
|
|
6364
|
-
const timer = window.setInterval(() => {
|
|
6365
|
-
refresh();
|
|
6366
|
-
}, 2e3);
|
|
6367
6377
|
const focus = () => {
|
|
6368
|
-
refresh();
|
|
6378
|
+
if (document.visibilityState === "visible") refresh();
|
|
6369
6379
|
};
|
|
6370
6380
|
window.addEventListener("focus", focus);
|
|
6381
|
+
document.addEventListener("visibilitychange", focus);
|
|
6371
6382
|
return () => {
|
|
6372
|
-
|
|
6383
|
+
reads.cancel();
|
|
6384
|
+
writes.cancel();
|
|
6373
6385
|
controller.current = void 0;
|
|
6374
|
-
window.clearInterval(timer);
|
|
6375
6386
|
window.removeEventListener("focus", focus);
|
|
6387
|
+
document.removeEventListener("visibilitychange", focus);
|
|
6376
6388
|
};
|
|
6377
|
-
}, [
|
|
6378
|
-
sessionId,
|
|
6379
|
-
getSelection,
|
|
6380
|
-
setSelection,
|
|
6381
|
-
t
|
|
6382
|
-
]);
|
|
6389
|
+
}, [sessionId]);
|
|
6383
6390
|
(0, react.useEffect)(() => {
|
|
6384
|
-
controller.current?.refresh(
|
|
6385
|
-
}, [
|
|
6391
|
+
controller.current?.refresh();
|
|
6392
|
+
}, [sessionId, modelKey]);
|
|
6386
6393
|
const connections = (0, react.useMemo)(() => discoverableConnections(value?.connections ?? []), [value?.connections]);
|
|
6387
6394
|
(0, react.useEffect)(() => {
|
|
6388
6395
|
if (!notice) return;
|
|
@@ -6402,13 +6409,16 @@ window.__ModuleLoader__.load({
|
|
|
6402
6409
|
onFreshness: (freshness) => {
|
|
6403
6410
|
if (value) controller.current?.select(void 0, value.selection.revision, freshness);
|
|
6404
6411
|
},
|
|
6412
|
+
onOpen: () => {
|
|
6413
|
+
controller.current?.refresh();
|
|
6414
|
+
},
|
|
6405
6415
|
onRefresh: () => {
|
|
6406
6416
|
controller.current?.refresh(true);
|
|
6407
6417
|
}
|
|
6408
6418
|
});
|
|
6409
6419
|
}
|
|
6410
6420
|
/** Native popovers live in the top layer, outside the composer's clipping boundary. */
|
|
6411
|
-
function SearchSelectorControl({ t, value, connections, pending, error, notice, onSelect, onFreshness, onRefresh }) {
|
|
6421
|
+
function SearchSelectorControl({ t, value, connections, pending, error, notice, onSelect, onFreshness, onOpen, onRefresh }) {
|
|
6412
6422
|
const id = (0, react.useId)();
|
|
6413
6423
|
const panel = (0, react.useRef)(null);
|
|
6414
6424
|
const trigger = (0, react.useRef)(null);
|
|
@@ -6431,13 +6441,13 @@ window.__ModuleLoader__.load({
|
|
|
6431
6441
|
};
|
|
6432
6442
|
}, []);
|
|
6433
6443
|
const toggle = () => {
|
|
6434
|
-
onRefresh();
|
|
6435
6444
|
const node = panel.current, button = trigger.current;
|
|
6436
6445
|
if (!node || !button) return;
|
|
6437
6446
|
if (node.matches(":popover-open")) {
|
|
6438
6447
|
node.hidePopover();
|
|
6439
6448
|
return;
|
|
6440
6449
|
}
|
|
6450
|
+
(onOpen ?? onRefresh)();
|
|
6441
6451
|
const rect = button.getBoundingClientRect();
|
|
6442
6452
|
const width = Math.min(280, window.innerWidth - 32);
|
|
6443
6453
|
node.style.width = width + "px";
|
|
@@ -8214,25 +8224,165 @@ window.__ModuleLoader__.load({
|
|
|
8214
8224
|
//#endregion
|
|
8215
8225
|
//#region lib/types/client/bindings.js
|
|
8216
8226
|
/** Services stay in the apply world; the renderer binds this bare source to useSettings. */
|
|
8217
|
-
function settingsInjection(form, credentials) {
|
|
8227
|
+
function settingsInjection(form, credentials, credentialsChanged) {
|
|
8218
8228
|
return {
|
|
8219
8229
|
hooks: { settings: form },
|
|
8220
8230
|
mutateSettings: (operations, revision) => form.mutate(operations, revision),
|
|
8221
8231
|
describeCredentials: (refs) => credentials.describe(refs),
|
|
8222
|
-
setCredential: (ref, value) =>
|
|
8232
|
+
setCredential: async (ref, value) => {
|
|
8233
|
+
const result = await credentials.set(ref, value);
|
|
8234
|
+
if (result.ok) credentialsChanged?.();
|
|
8235
|
+
return result;
|
|
8236
|
+
}
|
|
8223
8237
|
};
|
|
8224
8238
|
}
|
|
8225
8239
|
/** Bind each callback to its slot's Session, rather than exposing a remote service. */
|
|
8226
|
-
function selectionInjection(
|
|
8240
|
+
function selectionInjection(client, sessionId) {
|
|
8227
8241
|
return {
|
|
8228
|
-
getSelection: () =>
|
|
8229
|
-
setSelection: (request) =>
|
|
8230
|
-
...request,
|
|
8231
|
-
sessionId
|
|
8232
|
-
})
|
|
8242
|
+
getSelection: (options) => client.get(sessionId, options),
|
|
8243
|
+
setSelection: (request) => client.set(sessionId, request)
|
|
8233
8244
|
};
|
|
8234
8245
|
}
|
|
8235
8246
|
//#endregion
|
|
8247
|
+
//#region lib/types/client/selection-client.js
|
|
8248
|
+
/** One instance per mounted Remote namespace, never a process-global/session-only cache. */
|
|
8249
|
+
var SelectionClient = class {
|
|
8250
|
+
remote;
|
|
8251
|
+
ttl;
|
|
8252
|
+
capacity;
|
|
8253
|
+
entries = /* @__PURE__ */ new Map();
|
|
8254
|
+
disposed = false;
|
|
8255
|
+
epoch = 0;
|
|
8256
|
+
constructor(remote, ttl = 3e4, capacity = 64) {
|
|
8257
|
+
this.remote = remote;
|
|
8258
|
+
this.ttl = ttl;
|
|
8259
|
+
this.capacity = capacity;
|
|
8260
|
+
}
|
|
8261
|
+
entry(sessionId) {
|
|
8262
|
+
this.assertActive();
|
|
8263
|
+
let entry = this.entries.get(sessionId);
|
|
8264
|
+
if (!entry) entry = {
|
|
8265
|
+
expires: 0,
|
|
8266
|
+
generation: 0
|
|
8267
|
+
};
|
|
8268
|
+
this.entries.delete(sessionId);
|
|
8269
|
+
this.entries.set(sessionId, entry);
|
|
8270
|
+
return entry;
|
|
8271
|
+
}
|
|
8272
|
+
prune() {
|
|
8273
|
+
for (const [id, entry] of this.entries) {
|
|
8274
|
+
if (this.entries.size <= this.capacity) break;
|
|
8275
|
+
if (!entry.reading && !entry.writing) this.entries.delete(id);
|
|
8276
|
+
}
|
|
8277
|
+
}
|
|
8278
|
+
assertActive(epoch = this.epoch) {
|
|
8279
|
+
if (this.disposed) throw new Error("Search selection client disposed");
|
|
8280
|
+
if (epoch !== this.epoch) throw new Error("Search connection changed");
|
|
8281
|
+
}
|
|
8282
|
+
invalidate() {
|
|
8283
|
+
for (const entry of this.entries.values()) {
|
|
8284
|
+
entry.expires = 0;
|
|
8285
|
+
++entry.generation;
|
|
8286
|
+
}
|
|
8287
|
+
}
|
|
8288
|
+
/** Transport loss/replacement must not reuse or replay work from the old Host. */
|
|
8289
|
+
reset() {
|
|
8290
|
+
++this.epoch;
|
|
8291
|
+
this.entries.clear();
|
|
8292
|
+
}
|
|
8293
|
+
dispose() {
|
|
8294
|
+
this.disposed = true;
|
|
8295
|
+
this.reset();
|
|
8296
|
+
}
|
|
8297
|
+
get(sessionId, options = {}) {
|
|
8298
|
+
const entry = this.entry(sessionId);
|
|
8299
|
+
const epoch = this.epoch;
|
|
8300
|
+
if (options.modelKey !== void 0 && options.modelKey !== entry.modelKey) {
|
|
8301
|
+
entry.modelKey = options.modelKey;
|
|
8302
|
+
entry.expires = 0;
|
|
8303
|
+
++entry.generation;
|
|
8304
|
+
}
|
|
8305
|
+
if (options.force) entry.expires = 0;
|
|
8306
|
+
if (entry.reading) return entry.reading;
|
|
8307
|
+
if (!entry.writing && entry.value && Date.now() < entry.expires) {
|
|
8308
|
+
this.prune();
|
|
8309
|
+
return Promise.resolve({
|
|
8310
|
+
ok: true,
|
|
8311
|
+
value: entry.value
|
|
8312
|
+
});
|
|
8313
|
+
}
|
|
8314
|
+
const read = async () => {
|
|
8315
|
+
for (;;) {
|
|
8316
|
+
this.assertActive(epoch);
|
|
8317
|
+
if (entry.writing) {
|
|
8318
|
+
await entry.writing.catch(() => {});
|
|
8319
|
+
continue;
|
|
8320
|
+
}
|
|
8321
|
+
if (entry.value && Date.now() < entry.expires) {
|
|
8322
|
+
entry.reading = void 0;
|
|
8323
|
+
return {
|
|
8324
|
+
ok: true,
|
|
8325
|
+
value: entry.value
|
|
8326
|
+
};
|
|
8327
|
+
}
|
|
8328
|
+
const generation = entry.generation;
|
|
8329
|
+
let result;
|
|
8330
|
+
try {
|
|
8331
|
+
result = await this.remote.get({ sessionId });
|
|
8332
|
+
} catch (error) {
|
|
8333
|
+
this.assertActive(epoch);
|
|
8334
|
+
if (generation !== entry.generation) continue;
|
|
8335
|
+
entry.reading = void 0;
|
|
8336
|
+
throw error;
|
|
8337
|
+
}
|
|
8338
|
+
this.assertActive(epoch);
|
|
8339
|
+
if (generation !== entry.generation) continue;
|
|
8340
|
+
if (result.ok) {
|
|
8341
|
+
entry.value = result.value;
|
|
8342
|
+
entry.expires = Date.now() + this.ttl;
|
|
8343
|
+
}
|
|
8344
|
+
entry.reading = void 0;
|
|
8345
|
+
return result;
|
|
8346
|
+
}
|
|
8347
|
+
};
|
|
8348
|
+
const pending = read().finally(() => {
|
|
8349
|
+
if (entry.reading === pending) entry.reading = void 0;
|
|
8350
|
+
this.prune();
|
|
8351
|
+
});
|
|
8352
|
+
entry.reading = pending;
|
|
8353
|
+
this.prune();
|
|
8354
|
+
return pending;
|
|
8355
|
+
}
|
|
8356
|
+
set(sessionId, request) {
|
|
8357
|
+
const entry = this.entry(sessionId);
|
|
8358
|
+
const epoch = this.epoch;
|
|
8359
|
+
const generation = ++entry.generation;
|
|
8360
|
+
entry.expires = 0;
|
|
8361
|
+
const previous = entry.writing;
|
|
8362
|
+
const write = async () => {
|
|
8363
|
+
if (previous) await previous.catch(() => {});
|
|
8364
|
+
this.assertActive(epoch);
|
|
8365
|
+
const result = await this.remote.set({
|
|
8366
|
+
...request,
|
|
8367
|
+
sessionId
|
|
8368
|
+
});
|
|
8369
|
+
this.assertActive(epoch);
|
|
8370
|
+
if (generation === entry.generation && result.ok) {
|
|
8371
|
+
entry.value = result.value;
|
|
8372
|
+
entry.expires = Date.now() + this.ttl;
|
|
8373
|
+
}
|
|
8374
|
+
return result;
|
|
8375
|
+
};
|
|
8376
|
+
const pending = write().finally(() => {
|
|
8377
|
+
if (entry.writing === pending) entry.writing = void 0;
|
|
8378
|
+
this.prune();
|
|
8379
|
+
});
|
|
8380
|
+
entry.writing = pending;
|
|
8381
|
+
this.prune();
|
|
8382
|
+
return pending;
|
|
8383
|
+
}
|
|
8384
|
+
};
|
|
8385
|
+
//#endregion
|
|
8236
8386
|
//#region node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/core.js
|
|
8237
8387
|
var _a$1;
|
|
8238
8388
|
function $constructor(name, initializer, params) {
|
|
@@ -12459,6 +12609,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
12459
12609
|
const NS = "web-search-enhanced";
|
|
12460
12610
|
const LOCALE = "settings.webSearchEnhanced";
|
|
12461
12611
|
const inject = [
|
|
12612
|
+
"connection",
|
|
12462
12613
|
"slots",
|
|
12463
12614
|
"locale",
|
|
12464
12615
|
"configForms",
|
|
@@ -12470,7 +12621,9 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
12470
12621
|
en,
|
|
12471
12622
|
zh
|
|
12472
12623
|
}), "search dictionaries");
|
|
12473
|
-
const
|
|
12624
|
+
const form = ctx.configForms.get(NS);
|
|
12625
|
+
let selectionClient;
|
|
12626
|
+
const settings = settingsInjection(form, ctx.remote.credentials, () => selectionClient?.invalidate());
|
|
12474
12627
|
ctx.effect(() => ctx.configForms.whileServed([NS], () => ctx.slots.inject("plugins.bundle.config", () => ctx.slots.register({
|
|
12475
12628
|
name: "plugins.bundle.config",
|
|
12476
12629
|
key: "dsh-web-search-enhanced",
|
|
@@ -12480,12 +12633,21 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
12480
12633
|
const dispose = await ctx.remote.$mount(searchContribution);
|
|
12481
12634
|
ctx.effect(() => dispose, "search selection remote");
|
|
12482
12635
|
ctx.inject(["remote.searchConnections"], (searchCtx) => {
|
|
12636
|
+
const client = new SelectionClient(searchCtx.remote.searchConnections);
|
|
12637
|
+
selectionClient = client;
|
|
12638
|
+
searchCtx.effect(() => () => {
|
|
12639
|
+
client.dispose();
|
|
12640
|
+
if (selectionClient === client) selectionClient = void 0;
|
|
12641
|
+
}, "search selection cache");
|
|
12642
|
+
searchCtx.effect(() => form.subscribe(() => client.invalidate()), "search settings invalidation");
|
|
12643
|
+
const connection = searchCtx.get("connection");
|
|
12644
|
+
searchCtx.effect(() => connection.generation.subscribe(() => client.reset()), "search connection invalidation");
|
|
12483
12645
|
searchCtx.slots.inject("conversation.input.right", () => searchCtx.slots.register({
|
|
12484
12646
|
name: "conversation.input.right",
|
|
12485
12647
|
id: "search-connection",
|
|
12486
12648
|
order: 20,
|
|
12487
12649
|
locale: LOCALE,
|
|
12488
|
-
inject: (sessionId) => selectionInjection(
|
|
12650
|
+
inject: (sessionId) => selectionInjection(client, sessionId)
|
|
12489
12651
|
}, SearchConnectionSelector));
|
|
12490
12652
|
});
|
|
12491
12653
|
}
|