dsh-search-plugin 0.0.0-stage → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +102 -2
- package/cordis.patch.yml +13 -0
- package/lib/client.js +372 -0
- package/lib/connection.js +86 -0
- package/lib/index.js +21 -0
- package/lib/provider.js +123 -0
- package/lib/types/connection.d.ts +15 -0
- package/lib/types/index.d.ts +36 -0
- package/lib/types/provider.d.ts +20 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +86 -4
package/README.md
CHANGED
|
@@ -1,3 +1,103 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 联网搜索(Web search)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
将互联网作为智能体的信息来源,为分析与判断补充外部依据。
|
|
4
|
+
|
|
5
|
+
TypeScript ESM 搜索组合包,目标运行版本 **DSH 0.2.1-rc.4**。注册 `codex` 搜索 Provider,复用官方 `web_search`;网页抓取复用本机 `http` Provider 与 `web_fetch`,不重复注册工具。
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
web_search → ctx.web → codex → POST /v1/alpha/search
|
|
9
|
+
web_fetch → ctx.web → http → 本机直接请求公开网页
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## 安装与构建
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
pnpm install
|
|
16
|
+
pnpm check
|
|
17
|
+
# 在父目录执行,将 <name> 替换为目标 profile:
|
|
18
|
+
dsh plugin --profile <name> add ./dsh-search-plugin
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
本地 checkout 安装需要保留插件目录及构建产物。`pnpm build` 构建 Host 和浏览器 lazy-CJS 模块;修改后需重新构建。首次安装或更新 bundle 后需要重启现有 DSH profile,再刷新网页;加载新版后,UI 的即时配置与凭据更新对下一次搜索生效。插件不会自动更改正在运行的 profile。
|
|
22
|
+
|
|
23
|
+
bundle 要求标准 DSH base 已有 `web`、`web-fetch-http` 与工具消费者,并选择 `searchProvider: codex`、`fetchProvider: http`。不覆盖已有 HTTP fetch 安全限制、超时或 User-Agent;其他后应用的 patch 可覆盖路由。
|
|
24
|
+
|
|
25
|
+
## 默认配置:无需填写端点或模型
|
|
26
|
+
|
|
27
|
+
默认 `authMode: openai`、`openaiProvider: openai`,复用所选 OpenAI Provider 的实时 `baseURL` 与显式 `apiKeyEnv` 引用。例如:
|
|
28
|
+
|
|
29
|
+
```yaml
|
|
30
|
+
baseURL: https://your-api.example/v1
|
|
31
|
+
apiKeyEnv: OPENAI_API_KEY
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
搜索自动请求 `https://your-api.example/v1/alpha/search`,通过官方 Credentials 服务解析同一个密钥引用。**不要求填写搜索模型,不调用 Responses,不做协议探测或失败回退。** 上游必须实现 Alpha 搜索接口,普通 OpenAI 兼容聊天服务不一定支持。
|
|
35
|
+
|
|
36
|
+
端点与凭据一起继承。复用模式始终使用派生地址,忽略独立模式的 `endpoint`(包括旧版残留值);不同地址请切换独立凭据模式,明确授权该地址使用的凭据引用。每次搜索捕获当前配置并解析一次凭据,不复制存储,不跨请求缓存。
|
|
37
|
+
|
|
38
|
+
仅支持复用显式 `apiKeyEnv` 引用,不读取 Provider 私有对象、不提取 OAuth token、不绕过 stored-record 优先级、不实现 OAuth 刷新。自定义认证 headers 不会复制;这些连接请改用独立凭据。插件发送标准 Bearer API Key,不支持其他认证协议。
|
|
39
|
+
|
|
40
|
+
## 配置 UI
|
|
41
|
+
|
|
42
|
+
进入 **插件 → 联网搜索** 的组合包详情,或点击 **dsh-search-plugin 组件行 → 配置**。两处共用同一份实时配置:
|
|
43
|
+
|
|
44
|
+
- 复用模式只显示 OpenAI Provider 路由;无需填写端点、模型或密钥。
|
|
45
|
+
- 独立模式显示完整搜索端点 URL、凭据引用与密钥管理。
|
|
46
|
+
- 两种模式均可设置超时、响应字节上限、答案字符上限。
|
|
47
|
+
|
|
48
|
+
配置页使用官方 ConfigForms 服务和 revision 冲突检测,提供保存、重置与放弃编辑。重置只重置公开配置,不删除凭据或修改旧版密钥字段。密钥单独通过官方 Credentials API 管理:不返回明文、不进入普通配置、不写入浏览器存储;输入留空不会清除已有密钥。删除只删除该引用的托管值,环境变量或其他来源仍可能提供同名凭据。
|
|
49
|
+
|
|
50
|
+
组合包与组件提供中英文名称和描述;包名与 Provider id 保持不变。自建最小 profile 需要挂载官方 Settings、ConfigEditor、Credentials 及对应 Web 客户端服务;仅 Host schema 不会自动生成页面。
|
|
51
|
+
|
|
52
|
+
## 配置字段
|
|
53
|
+
|
|
54
|
+
| 字段 | 默认值 | 含义 |
|
|
55
|
+
| --- | --- | --- |
|
|
56
|
+
| `authMode` | `openai` | `openai` 复用模式或 `credential` 独立模式 |
|
|
57
|
+
| `openaiProvider` | `openai` | 复用模式的官方 LLM Provider id |
|
|
58
|
+
| `credentialRef` | `DSH_CODEX_API_KEY` | 独立模式的官方凭据引用 / 环境变量名 |
|
|
59
|
+
| `endpoint` | 空 | 复用模式自动派生 Alpha 搜索 URL;独立模式必须填写完整 POST URL |
|
|
60
|
+
| `apiKey` | 无 | 兼容旧版的 Host-only 密钥,仅独立模式使用,优先于引用;推荐改用凭据管理 |
|
|
61
|
+
| `timeoutMs` | `60000` | 单次搜索请求超时,毫秒 |
|
|
62
|
+
| `maxResponseBytes` | `2000000` | 搜索响应主体的字节上限 |
|
|
63
|
+
| `maxAnswerChars` | `16000` | 返回答案的字符上限 |
|
|
64
|
+
|
|
65
|
+
以上字段为即时配置。上限仅控制搜索,不控制本机 HTTP fetch。旧版 `model` 不再使用。复用模式忽略旧 `endpoint`;独立模式升级后需把原 `/responses` 端点改为实际的 Alpha 搜索 URL,插件不会自动改写独立端点。
|
|
66
|
+
|
|
67
|
+
bundle 可从启动进程环境读取 `DSH_CODEX_AUTH_MODE`(默认 `openai`)、`DSH_CODEX_ENDPOINT` 和旧版 `DSH_CODEX_API_KEY`。用环境变量直接提供搜索 API Key 时需同时设置 `DSH_CODEX_AUTH_MODE=credential`。不再需要 `DSH_CODEX_MODEL`。
|
|
68
|
+
|
|
69
|
+
独立模式的 profile patch 示例:
|
|
70
|
+
|
|
71
|
+
```yaml
|
|
72
|
+
- id: dsh-search-plugin
|
|
73
|
+
config:
|
|
74
|
+
authMode: credential
|
|
75
|
+
endpoint: https://your-search.example/v1/alpha/search
|
|
76
|
+
credentialRef: SEARCH_API_KEY
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**patch 替换整份 `config`,不是深度合并。** 修改时保留需要的字段,省略的字段使用 schema 默认值。不要把真实密钥提交进仓库或放入普通配置。插件自身不额外解析 `.env`。
|
|
80
|
+
|
|
81
|
+
## 协议与安全边界
|
|
82
|
+
|
|
83
|
+
只支持 **POST `/v1/alpha/search`**,不支持 Responses、Chat Completions 或直接连接 ChatGPT 私有后端;独立模式的 `endpoint` 是完整 URL,不自动补路径。协议按 [Codex 官方 SearchRequest/SearchResponse](https://github.com/openai/codex/blob/main/codex-rs/codex-api/src/search.rs) 实现,而不是替换 Responses URL:
|
|
84
|
+
|
|
85
|
+
- 每次请求生成 `id`,发送 `commands.search_query: [{ q: 查询文本 }]`、用户 `input`、`settings: { allowed_callers: ['direct'], external_web_access: true }` 和 `max_output_tokens: 2048`,不发送 Responses `tools`、`tool_choice`、`include`、`stream` 或 `store`。
|
|
86
|
+
- Alpha 协议自身要求 `model`。适配器内置路由模型 `gpt-5.6-luna`([已公开 Alpha 适配器的默认值](https://unpkg.com/@caiqy/dsh-plugin-web-search@0.1.0/lib/providers.js)),不继承聊天模型,也不要求用户填写。目标服务必须支持此路由模型。
|
|
87
|
+
- 响应读取文本 `output` 和可选 `results`(缺失、`null`、空数组均支持)。`results` 是可扩展的不透明条目,只提取有合法 HTTP(S) `url` 的来源及可选 `title`,不要求存在这些字段,不输出 `encrypted_output`。清理 Alpha 私有引用标记,来源去重后交给 DSH 管理数量限制。
|
|
88
|
+
|
|
89
|
+
搜索端点会收到密钥与查询,请仅配置可信服务,推荐 HTTPS。配置缺失、鉴权错误、上游协议错误、超时等明确失败,不静默切换 Provider,不返回伪造的空结果。不自动重试,不跟随携带认证的重定向。来源只取自结构化响应,不从自然语言猜测或编造链接。DSH 的 `truncated` 表示来源被裁剪,不表示答案字符裁剪。
|
|
90
|
+
|
|
91
|
+
网页与答案均属于不可信外部数据,不能作为系统指令或工具授权。排错时不要打印未脱敏请求头、真实密钥或完整远端错误正文。
|
|
92
|
+
|
|
93
|
+
## 本机 HTTP fetch
|
|
94
|
+
|
|
95
|
+
本机直接请求公开网页,不额外调用付费抓取 API,不携带搜索 API Key。仍有网络成本、搜索端点费用及正文进入模型后的 token 费用。
|
|
96
|
+
|
|
97
|
+
复用官方 HTTP Provider 的 SSRF 防护、DNS 固定、同源重定向和正文限制。默认 30 秒超时、5 MB 响应、100,000 字符正文、最多 5 次同源跳转,已有 profile 的定制值不被本插件覆盖。不支持 JS 渲染、登录、验证码、PDF/二进制正文或跨源自动重定向;失败不会自动改用浏览器或付费抓取。
|
|
98
|
+
|
|
99
|
+
若代理 Fake-IP DNS 返回 `198.18.0.0/15` 等非公开地址,官方 fetch 可能以 `WEB_BLOCKED_URL` 拒绝。应修正运行环境的 DNS/路由或按官方能力显式授权,不能关闭 SSRF 防护或伪造公开地址绕过。
|
|
100
|
+
|
|
101
|
+
## 验证
|
|
102
|
+
|
|
103
|
+
运行 `pnpm check` 完成类型检查、构建与自动测试。真实搜索与抓取可能产生费用,需要另外验证目标服务、凭据和网络环境;本地测试不等于正在运行的 GUI 部署验收。
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Requires the standard DSH base bundle (web service, HTTP fetch and tool consumer).
|
|
2
|
+
- id: web
|
|
3
|
+
config:
|
|
4
|
+
searchProvider: codex
|
|
5
|
+
fetchProvider: http
|
|
6
|
+
|
|
7
|
+
- insert:
|
|
8
|
+
- id: dsh-search-plugin
|
|
9
|
+
name: dsh-search-plugin
|
|
10
|
+
config:
|
|
11
|
+
authMode: !!js process.env.DSH_CODEX_AUTH_MODE || 'openai'
|
|
12
|
+
endpoint: !!js process.env.DSH_CODEX_ENDPOINT
|
|
13
|
+
apiKey: !!js process.env.DSH_CODEX_API_KEY
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({id:"dsh-search-plugin",factory(require){
|
|
2
|
+
const module={exports:{}};const exports=module.exports;
|
|
3
|
+
"use strict";
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
21
|
+
|
|
22
|
+
// src/client/index.ts
|
|
23
|
+
var index_exports = {};
|
|
24
|
+
__export(index_exports, {
|
|
25
|
+
apply: () => apply,
|
|
26
|
+
inject: () => inject
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(index_exports);
|
|
29
|
+
|
|
30
|
+
// src/client/SearchConfig.tsx
|
|
31
|
+
var import_react = require("react");
|
|
32
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
33
|
+
var fields = ["authMode", "openaiProvider", "credentialRef", "endpoint", "timeoutMs", "maxResponseBytes", "maxAnswerChars"];
|
|
34
|
+
var defaults = { authMode: "openai", openaiProvider: "openai", credentialRef: "DSH_CODEX_API_KEY", endpoint: "", timeoutMs: "60000", maxResponseBytes: "2000000", maxAnswerChars: "16000" };
|
|
35
|
+
var numeric = /* @__PURE__ */ new Set(["timeoutMs", "maxResponseBytes", "maxAnswerChars"]);
|
|
36
|
+
function valuesOf(value) {
|
|
37
|
+
return Object.fromEntries(fields.map((field) => [field, String(value[field] ?? defaults[field])]));
|
|
38
|
+
}
|
|
39
|
+
function SearchConfig(props) {
|
|
40
|
+
const { t } = props;
|
|
41
|
+
const state = props.useConfig((snapshot) => snapshot);
|
|
42
|
+
const credential = props.useCredential((snapshot) => snapshot);
|
|
43
|
+
const [draft, setDraft] = (0, import_react.useState)(null);
|
|
44
|
+
const [busy, setBusy] = (0, import_react.useState)(false);
|
|
45
|
+
const [message, setMessage] = (0, import_react.useState)(null);
|
|
46
|
+
const [key, setKey] = (0, import_react.useState)(null);
|
|
47
|
+
const [keyBusy, setKeyBusy] = (0, import_react.useState)(false);
|
|
48
|
+
const [keyMessage, setKeyMessage] = (0, import_react.useState)(null);
|
|
49
|
+
if (props.view === "summary") return t("description");
|
|
50
|
+
if (state.status !== "ready" || !state.value) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "status", children: t(state.status === "loading" ? "loading" : "unavailable") });
|
|
51
|
+
const accepted = valuesOf(state.value);
|
|
52
|
+
const values = draft?.values ?? accepted;
|
|
53
|
+
const keyText = key?.ref === values.credentialRef ? key.text : "";
|
|
54
|
+
const conflict = draft !== null && draft.revision !== state.revision;
|
|
55
|
+
const disabled = !state.writable || busy;
|
|
56
|
+
const independent = values.authMode === "credential";
|
|
57
|
+
const visibleFields = fields.filter((field) => numeric.has(field) || (independent ? field === "credentialRef" || field === "endpoint" : field === "openaiProvider"));
|
|
58
|
+
const validRef = /^[A-Za-z_][A-Za-z0-9_]*$/.test(values.credentialRef);
|
|
59
|
+
const credentialCurrent = credential.ref === values.credentialRef;
|
|
60
|
+
const info = credentialCurrent ? credential.info : void 0;
|
|
61
|
+
const credentialWritable = info?.writable === true && !keyBusy;
|
|
62
|
+
const edit = (field, value) => {
|
|
63
|
+
setDraft({ values: { ...values, [field]: value }, revision: draft ? draft.revision : state.revision });
|
|
64
|
+
setMessage(null);
|
|
65
|
+
if (field === "authMode") {
|
|
66
|
+
setKey(null);
|
|
67
|
+
setKeyMessage(null);
|
|
68
|
+
}
|
|
69
|
+
if (field === "credentialRef") {
|
|
70
|
+
setKey(null);
|
|
71
|
+
setKeyMessage(null);
|
|
72
|
+
props.describeCredential(value);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const save = async (reset) => {
|
|
76
|
+
if (conflict) {
|
|
77
|
+
setMessage("conflict");
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (!reset) {
|
|
81
|
+
let valid = fields.filter((field) => numeric.has(field)).every((field) => Number.isSafeInteger(Number(values[field])) && Number(values[field]) > 0);
|
|
82
|
+
if (independent) {
|
|
83
|
+
try {
|
|
84
|
+
const endpoint = new URL(values.endpoint);
|
|
85
|
+
valid &&= validRef && ["http:", "https:"].includes(endpoint.protocol) && !endpoint.username && !endpoint.password && !endpoint.hash;
|
|
86
|
+
} catch {
|
|
87
|
+
valid = false;
|
|
88
|
+
}
|
|
89
|
+
} else valid &&= Boolean(values.openaiProvider.trim());
|
|
90
|
+
if (!valid) {
|
|
91
|
+
setMessage("invalid");
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const savedValues = independent ? values : { ...values, endpoint: "" };
|
|
96
|
+
const savedFields = ["authMode", ...visibleFields, ...!independent ? ["endpoint"] : []];
|
|
97
|
+
const ops = reset ? fields.map((field) => ({ op: "unset", path: [field] })) : savedFields.filter((field) => savedValues[field] !== accepted[field]).map((field) => ({ op: "set", path: [field], value: numeric.has(field) ? Number(savedValues[field]) : savedValues[field] }));
|
|
98
|
+
setBusy(true);
|
|
99
|
+
setMessage(null);
|
|
100
|
+
try {
|
|
101
|
+
if (await props.mutateConfig(ops, draft ? draft.revision : state.revision)) {
|
|
102
|
+
setDraft(null);
|
|
103
|
+
setMessage("saved");
|
|
104
|
+
} else setMessage("failed");
|
|
105
|
+
} catch {
|
|
106
|
+
setMessage("failed");
|
|
107
|
+
} finally {
|
|
108
|
+
setBusy(false);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
const writeKey = async (remove) => {
|
|
112
|
+
const ref = values.credentialRef;
|
|
113
|
+
setKeyBusy(true);
|
|
114
|
+
setKeyMessage(null);
|
|
115
|
+
try {
|
|
116
|
+
if (remove) await props.removeCredential(ref);
|
|
117
|
+
else await props.storeCredential(ref, keyText);
|
|
118
|
+
setKey(null);
|
|
119
|
+
setKeyMessage("keySaved");
|
|
120
|
+
} catch {
|
|
121
|
+
setKeyMessage("keyFailed");
|
|
122
|
+
} finally {
|
|
123
|
+
setKeyBusy(false);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-search-config", children: [
|
|
127
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dsh-search-hint", children: t("inherit") }),
|
|
128
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dsh-search-hint", children: t("trust") }),
|
|
129
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { onSubmit: (event) => {
|
|
130
|
+
event.preventDefault();
|
|
131
|
+
void save(false);
|
|
132
|
+
}, children: [
|
|
133
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("fieldset", { disabled, children: [
|
|
134
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
|
|
135
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("authMode") }),
|
|
136
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", { value: values.authMode, onChange: (event) => edit("authMode", event.target.value), children: [
|
|
137
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "openai", children: t("openai") }),
|
|
138
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "credential", children: t("credential") })
|
|
139
|
+
] })
|
|
140
|
+
] }),
|
|
141
|
+
visibleFields.map((field) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
|
|
142
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t(field) }),
|
|
143
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
144
|
+
"input",
|
|
145
|
+
{
|
|
146
|
+
type: numeric.has(field) ? "number" : "text",
|
|
147
|
+
min: numeric.has(field) ? 1 : void 0,
|
|
148
|
+
step: numeric.has(field) ? 1 : void 0,
|
|
149
|
+
value: values[field],
|
|
150
|
+
onChange: (event) => edit(field, event.target.value),
|
|
151
|
+
disabled: field === "credentialRef" && keyBusy
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
] }, field))
|
|
155
|
+
] }),
|
|
156
|
+
!state.writable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "status", children: t("readOnly") }),
|
|
157
|
+
conflict && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "alert", children: t("conflict") }),
|
|
158
|
+
message && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "status", children: t(message) }),
|
|
159
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-search-actions", children: [
|
|
160
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "submit", disabled: disabled || conflict || !draft, children: t(busy ? "saving" : "save") }),
|
|
161
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: disabled || conflict, onClick: () => {
|
|
162
|
+
void save(true);
|
|
163
|
+
}, children: t("reset") }),
|
|
164
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: busy || !draft, onClick: () => {
|
|
165
|
+
setDraft(null);
|
|
166
|
+
setMessage(null);
|
|
167
|
+
setKey(null);
|
|
168
|
+
props.describeCredential(accepted.credentialRef);
|
|
169
|
+
}, children: t("discard") })
|
|
170
|
+
] })
|
|
171
|
+
] }),
|
|
172
|
+
values.authMode === "credential" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: "dsh-search-secret", children: [
|
|
173
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
|
|
174
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("key") }),
|
|
175
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
176
|
+
"input",
|
|
177
|
+
{
|
|
178
|
+
type: "password",
|
|
179
|
+
autoComplete: "new-password",
|
|
180
|
+
value: keyText,
|
|
181
|
+
disabled: !credentialWritable,
|
|
182
|
+
onChange: (event) => {
|
|
183
|
+
setKey({ ref: values.credentialRef, text: event.target.value });
|
|
184
|
+
setKeyMessage(null);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
)
|
|
188
|
+
] }),
|
|
189
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dsh-search-hint", children: t("keyHint") }),
|
|
190
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "status", children: t(!validRef ? "invalidRef" : credentialCurrent && credential.error ? "keyFailed" : !info ? "keyLoading" : info.configured ? "keySet" : "keyUnset") }),
|
|
191
|
+
info && !info.writable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: t("keyReadOnly") }),
|
|
192
|
+
keyMessage && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "status", children: t(keyMessage) }),
|
|
193
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-search-actions", children: [
|
|
194
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: !credentialWritable || !keyText.trim(), onClick: () => {
|
|
195
|
+
void writeKey(false);
|
|
196
|
+
}, children: t("keySave") }),
|
|
197
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: !credentialWritable || !info?.configured, onClick: () => {
|
|
198
|
+
void writeKey(true);
|
|
199
|
+
}, children: t("keyRemove") }),
|
|
200
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: !validRef || keyBusy, onClick: () => props.describeCredential(values.credentialRef), children: t("keyRefresh") })
|
|
201
|
+
] })
|
|
202
|
+
] })
|
|
203
|
+
] });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/client/locales.ts
|
|
207
|
+
var en = {
|
|
208
|
+
title: "Web search",
|
|
209
|
+
description: "Give agents access to information from the web to inform their analysis and decisions.",
|
|
210
|
+
authMode: "Authentication",
|
|
211
|
+
openai: "Reuse OpenAI provider (default)",
|
|
212
|
+
credential: "Independent credential",
|
|
213
|
+
openaiProvider: "OpenAI provider route",
|
|
214
|
+
credentialRef: "Credential reference",
|
|
215
|
+
endpoint: "Alpha search endpoint (full URL)",
|
|
216
|
+
timeoutMs: "Timeout (ms)",
|
|
217
|
+
maxResponseBytes: "Maximum response bytes",
|
|
218
|
+
maxAnswerChars: "Maximum answer characters",
|
|
219
|
+
inherit: "By default, reuse the OpenAI baseURL and API-key reference: https://example.com/v1 becomes https://example.com/v1/alpha/search. No endpoint or model is required. Only Alpha search is supported; there is no Responses fallback. Reuse requires an explicit apiKeyEnv reference; OAuth and native stored credentials are not supported. For another address, choose independent authentication and enter its full endpoint URL.",
|
|
220
|
+
trust: "The endpoint receives your credential and search queries. Only use a trusted service.",
|
|
221
|
+
save: "Save configuration",
|
|
222
|
+
reset: "Reset overrides",
|
|
223
|
+
discard: "Discard edits",
|
|
224
|
+
saving: "Saving\u2026",
|
|
225
|
+
saved: "Configuration saved.",
|
|
226
|
+
failed: "The configuration was not accepted. Review your edits or reload the page.",
|
|
227
|
+
loading: "Loading configuration\u2026",
|
|
228
|
+
unavailable: "Configuration is unavailable.",
|
|
229
|
+
readOnly: "Configuration is read-only.",
|
|
230
|
+
conflict: "Configuration changed elsewhere. Discard edits to reload the current values before saving.",
|
|
231
|
+
key: "New API key",
|
|
232
|
+
keyHint: "Stored only in the credentials domain, not configuration. Blank keeps the existing key. Values are never returned.",
|
|
233
|
+
keySet: "A credential is configured.",
|
|
234
|
+
keyUnset: "No credential is configured.",
|
|
235
|
+
keyLoading: "Checking credential\u2026",
|
|
236
|
+
keyReadOnly: "This credential source is read-only.",
|
|
237
|
+
keySave: "Store key",
|
|
238
|
+
keyRemove: "Remove stored key",
|
|
239
|
+
keyRefresh: "Refresh status",
|
|
240
|
+
keySaved: "Credential updated. Environment or other source layers may still supply a value after removal.",
|
|
241
|
+
keyFailed: "Credential operation failed. Check the credential provider and retry.",
|
|
242
|
+
invalidRef: "Use an environment-style reference name (letters, digits, underscore; not starting with a digit).",
|
|
243
|
+
invalid: "Use positive integer limits. Reuse requires a provider route; independent authentication requires a valid credential reference and a full HTTP(S) endpoint without credentials or a fragment."
|
|
244
|
+
};
|
|
245
|
+
var zh = {
|
|
246
|
+
title: "\u8054\u7F51\u641C\u7D22",
|
|
247
|
+
description: "\u5C06\u4E92\u8054\u7F51\u4F5C\u4E3A\u667A\u80FD\u4F53\u7684\u4FE1\u606F\u6765\u6E90\uFF0C\u4E3A\u5206\u6790\u4E0E\u5224\u65AD\u8865\u5145\u5916\u90E8\u4F9D\u636E\u3002",
|
|
248
|
+
authMode: "\u8BA4\u8BC1\u65B9\u5F0F",
|
|
249
|
+
openai: "\u590D\u7528 OpenAI Provider\uFF08\u9ED8\u8BA4\uFF09",
|
|
250
|
+
credential: "\u72EC\u7ACB\u51ED\u636E",
|
|
251
|
+
openaiProvider: "OpenAI Provider \u8DEF\u7531",
|
|
252
|
+
credentialRef: "\u51ED\u636E\u5F15\u7528\u540D",
|
|
253
|
+
endpoint: "Alpha \u641C\u7D22\u7AEF\u70B9\uFF08\u5B8C\u6574 URL\uFF09",
|
|
254
|
+
timeoutMs: "\u8D85\u65F6\uFF08\u6BEB\u79D2\uFF09",
|
|
255
|
+
maxResponseBytes: "\u54CD\u5E94\u5B57\u8282\u4E0A\u9650",
|
|
256
|
+
maxAnswerChars: "\u7B54\u6848\u5B57\u7B26\u4E0A\u9650",
|
|
257
|
+
inherit: "\u9ED8\u8BA4\u590D\u7528 OpenAI \u7684 baseURL \u4E0E API Key \u5F15\u7528\uFF0C\u4F8B\u5982 https://example.com/v1 \u81EA\u52A8\u6D3E\u751F https://example.com/v1/alpha/search\uFF0C\u65E0\u9700\u586B\u5199\u7AEF\u70B9\u6216\u6A21\u578B\u3002\u4EC5\u652F\u6301 Alpha \u641C\u7D22\uFF0C\u4E0D\u56DE\u9000 Responses\u3002\u590D\u7528\u8981\u6C42\u663E\u5F0F apiKeyEnv \u5F15\u7528\uFF0C\u4E0D\u652F\u6301 OAuth \u6216\u539F\u751F\u5B58\u50A8\u51ED\u636E\uFF1B\u5982\u9700\u5176\u4ED6\u5730\u5740\uFF0C\u8BF7\u5207\u6362\u72EC\u7ACB\u51ED\u636E\u5E76\u586B\u5199\u5B8C\u6574\u7AEF\u70B9 URL\u3002",
|
|
258
|
+
trust: "\u7AEF\u70B9\u4F1A\u6536\u5230\u51ED\u636E\u4E0E\u641C\u7D22\u67E5\u8BE2\uFF0C\u8BF7\u4EC5\u4F7F\u7528\u53EF\u4FE1\u670D\u52A1\u3002",
|
|
259
|
+
save: "\u4FDD\u5B58\u914D\u7F6E",
|
|
260
|
+
reset: "\u91CD\u7F6E\u8986\u76D6",
|
|
261
|
+
discard: "\u653E\u5F03\u7F16\u8F91",
|
|
262
|
+
saving: "\u4FDD\u5B58\u4E2D\u2026",
|
|
263
|
+
saved: "\u914D\u7F6E\u5DF2\u4FDD\u5B58\u3002",
|
|
264
|
+
failed: "\u914D\u7F6E\u672A\u88AB\u63A5\u53D7\uFF0C\u8BF7\u68C0\u67E5\u7F16\u8F91\u5185\u5BB9\u6216\u91CD\u65B0\u52A0\u8F7D\u9875\u9762\u3002",
|
|
265
|
+
loading: "\u6B63\u5728\u52A0\u8F7D\u914D\u7F6E\u2026",
|
|
266
|
+
unavailable: "\u5F53\u524D\u65E0\u6CD5\u914D\u7F6E\u6B64\u63D2\u4EF6\u3002",
|
|
267
|
+
readOnly: "\u914D\u7F6E\u4E3A\u53EA\u8BFB\u3002",
|
|
268
|
+
conflict: "\u914D\u7F6E\u5DF2\u5728\u5176\u4ED6\u4F4D\u7F6E\u66F4\u6539\uFF0C\u8BF7\u653E\u5F03\u7F16\u8F91\u4EE5\u52A0\u8F7D\u6700\u65B0\u503C\uFF0C\u518D\u91CD\u65B0\u4FDD\u5B58\u3002",
|
|
269
|
+
key: "\u65B0 API \u5BC6\u94A5",
|
|
270
|
+
keyHint: "\u4EC5\u5199\u5165\u72EC\u7ACB\u51ED\u636E\u57DF\uFF0C\u4E0D\u8FDB\u5165\u914D\u7F6E\u3002\u7559\u7A7A\u4FDD\u7559\u73B0\u6709\u5BC6\u94A5\uFF1B\u5DF2\u5B58\u5BC6\u94A5\u6C38\u4E0D\u56DE\u663E\u3002",
|
|
271
|
+
keySet: "\u5DF2\u914D\u7F6E\u51ED\u636E\u3002",
|
|
272
|
+
keyUnset: "\u5C1A\u672A\u914D\u7F6E\u51ED\u636E\u3002",
|
|
273
|
+
keyLoading: "\u6B63\u5728\u68C0\u67E5\u51ED\u636E\u2026",
|
|
274
|
+
keyReadOnly: "\u6B64\u51ED\u636E\u6765\u6E90\u4E0D\u53EF\u5199\u3002",
|
|
275
|
+
keySave: "\u5B58\u50A8\u5BC6\u94A5",
|
|
276
|
+
keyRemove: "\u79FB\u9664\u5DF2\u5B58\u5BC6\u94A5",
|
|
277
|
+
keyRefresh: "\u5237\u65B0\u72B6\u6001",
|
|
278
|
+
keySaved: "\u51ED\u636E\u5DF2\u66F4\u65B0\uFF1B\u79FB\u9664\u540E\u73AF\u5883\u53D8\u91CF\u6216\u5176\u4ED6\u6765\u6E90\u4ECD\u53EF\u80FD\u63D0\u4F9B\u51ED\u636E\u3002",
|
|
279
|
+
keyFailed: "\u51ED\u636E\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u51ED\u636E\u63D0\u4F9B\u65B9\u540E\u91CD\u8BD5\u3002",
|
|
280
|
+
invalidRef: "\u8BF7\u8F93\u5165\u73AF\u5883\u53D8\u91CF\u683C\u5F0F\u7684\u5F15\u7528\u540D\uFF08\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u4E0B\u5212\u7EBF\uFF0C\u4E0D\u80FD\u4EE5\u6570\u5B57\u5F00\u5934\uFF09\u3002",
|
|
281
|
+
invalid: "\u8BF7\u586B\u5199\u6B63\u6574\u6570\u4E0A\u9650\u3002\u590D\u7528\u6A21\u5F0F\u9700\u8981 Provider \u8DEF\u7531\uFF1B\u72EC\u7ACB\u8BA4\u8BC1\u9700\u8981\u6709\u6548\u51ED\u636E\u5F15\u7528\u540D\u4E0E\u5B8C\u6574 HTTP(S) \u7AEF\u70B9\uFF0CURL \u4E0D\u5F97\u5305\u542B\u7528\u6237\u540D\u3001\u5BC6\u7801\u6216\u7247\u6BB5\u3002"
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// src/client/style.css
|
|
285
|
+
var style_default = ".dsh-search-config { display: flex; flex-direction: column; gap: 16px; color: var(--dsw-alias-label-primary); }\n.dsh-search-config fieldset { border: 0; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr)); gap: 16px; }\n.dsh-search-config label { display: flex; flex-direction: column; gap: 8px; font-size: 14px; }\n.dsh-search-config input, .dsh-search-config select { box-sizing: border-box; width: 100%; min-height: 38px; padding: 8px 10px; border-radius: 8px; border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-1); color: inherit; font: inherit; }\n.dsh-search-config button { min-height: 36px; padding: 8px 12px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; background: var(--dsw-alias-button-elevated-fill); color: inherit; cursor: pointer; font: inherit; }\n.dsh-search-config :disabled { opacity: .55; cursor: not-allowed; }\n.dsh-search-config :focus-visible { outline: 2px solid var(--dsw-alias-link); outline-offset: 2px; }\n.dsh-search-hint { margin: 0; color: var(--dsw-alias-label-tertiary); font-size: 13px; line-height: 1.6; }\n.dsh-search-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }\n.dsh-search-secret { border-top: 1px solid var(--dsw-alias-border-l2); padding-top: 20px; }\n.dsh-search-secret .dsh-search-hint { margin-top: 8px; }\n";
|
|
286
|
+
|
|
287
|
+
// src/client/index.ts
|
|
288
|
+
var inject = ["slots", "locale", "remote", "remote.credentials", "configForms"];
|
|
289
|
+
function apply(ctx) {
|
|
290
|
+
ctx.effect(() => ctx.locale.register("searchPlugin", { en, zh }));
|
|
291
|
+
ctx.effect(() => {
|
|
292
|
+
const style = document.createElement("style");
|
|
293
|
+
style.dataset.plugin = "dsh-search-plugin";
|
|
294
|
+
style.textContent = style_default;
|
|
295
|
+
document.head.append(style);
|
|
296
|
+
return () => style.remove();
|
|
297
|
+
});
|
|
298
|
+
let state = { ref: "", error: false };
|
|
299
|
+
let sequence = 0;
|
|
300
|
+
let disposed = false;
|
|
301
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
302
|
+
const publish = (next) => {
|
|
303
|
+
state = next;
|
|
304
|
+
for (const listener of listeners) listener();
|
|
305
|
+
};
|
|
306
|
+
const describeCredential = (ref) => {
|
|
307
|
+
if (disposed) return;
|
|
308
|
+
const request = ++sequence;
|
|
309
|
+
publish({ ref, error: false });
|
|
310
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(ref)) return;
|
|
311
|
+
void ctx.remote.credentials.describe([ref]).then((result) => {
|
|
312
|
+
if (!disposed && request === sequence) {
|
|
313
|
+
publish(result.ok ? { ref, info: result.value[ref], error: false } : { ref, error: true });
|
|
314
|
+
}
|
|
315
|
+
}, () => {
|
|
316
|
+
if (!disposed && request === sequence) publish({ ref, error: true });
|
|
317
|
+
});
|
|
318
|
+
};
|
|
319
|
+
const form = ctx.configForms.get("dsh-search-plugin");
|
|
320
|
+
const refresh = () => {
|
|
321
|
+
const value = form.getSnapshot().value;
|
|
322
|
+
if (value) describeCredential(typeof value.credentialRef === "string" ? value.credentialRef : "DSH_CODEX_API_KEY");
|
|
323
|
+
};
|
|
324
|
+
ctx.effect(() => form.subscribe(refresh));
|
|
325
|
+
refresh();
|
|
326
|
+
ctx.effect(() => ctx.remote.$on("credentials/reference-updated", (ref) => {
|
|
327
|
+
if (ref === state.ref) describeCredential(ref);
|
|
328
|
+
}));
|
|
329
|
+
ctx.effect(() => () => {
|
|
330
|
+
disposed = true;
|
|
331
|
+
sequence++;
|
|
332
|
+
listeners.clear();
|
|
333
|
+
});
|
|
334
|
+
const face = {
|
|
335
|
+
hooks: {
|
|
336
|
+
credential: { getSnapshot: () => state, subscribe: (listener) => {
|
|
337
|
+
listeners.add(listener);
|
|
338
|
+
return () => {
|
|
339
|
+
listeners.delete(listener);
|
|
340
|
+
};
|
|
341
|
+
} },
|
|
342
|
+
config: { getSnapshot: () => form.getSnapshot(), subscribe: (listener) => form.subscribe(listener) }
|
|
343
|
+
},
|
|
344
|
+
mutateConfig: (ops, expectedRevision) => form.mutate(ops, expectedRevision),
|
|
345
|
+
describeCredential,
|
|
346
|
+
storeCredential: async (ref, value) => {
|
|
347
|
+
const result = await ctx.remote.credentials.set(ref, value);
|
|
348
|
+
if (!result.ok) throw result.error;
|
|
349
|
+
describeCredential(ref);
|
|
350
|
+
},
|
|
351
|
+
removeCredential: async (ref) => {
|
|
352
|
+
const result = await ctx.remote.credentials.unset(ref);
|
|
353
|
+
if (!result.ok) throw result.error;
|
|
354
|
+
describeCredential(ref);
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
ctx.effect(() => ctx.slots.inject("plugins.bundle.config", () => ctx.slots.register({
|
|
358
|
+
name: "plugins.bundle.config",
|
|
359
|
+
key: "dsh-search-plugin",
|
|
360
|
+
locale: "searchPlugin",
|
|
361
|
+
inject: () => face
|
|
362
|
+
}, SearchConfig)));
|
|
363
|
+
ctx.effect(() => ctx.slots.inject("plugins.row.config", () => ctx.slots.register({
|
|
364
|
+
name: "plugins.row.config",
|
|
365
|
+
key: "dsh-search-plugin#dsh-search-plugin",
|
|
366
|
+
locale: "searchPlugin",
|
|
367
|
+
inject: () => face
|
|
368
|
+
}, SearchConfig)));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return module.exports;
|
|
372
|
+
}});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { credentialRef } from '@deepseek-ai/dsh-credentials';
|
|
2
|
+
import { WebError } from '@deepseek-ai/dsh-web';
|
|
3
|
+
import { CodexSearchProvider } from './provider.js';
|
|
4
|
+
function fail(message) {
|
|
5
|
+
throw new WebError(`Web search: ${message}`, 'CODEX_CONFIGURATION_ERROR');
|
|
6
|
+
}
|
|
7
|
+
function record(value) {
|
|
8
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
9
|
+
fail('OpenAI provider configuration is unavailable');
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
/** Resolve configuration and credentials for each search, without retaining secrets between operations. */
|
|
13
|
+
export class ConfiguredSearchProvider {
|
|
14
|
+
ctx;
|
|
15
|
+
config;
|
|
16
|
+
id = 'codex';
|
|
17
|
+
constructor(ctx, config) {
|
|
18
|
+
this.ctx = ctx;
|
|
19
|
+
this.config = config;
|
|
20
|
+
}
|
|
21
|
+
available() { return true; }
|
|
22
|
+
/** Capture endpoint, limits and the selected credential as one request configuration. */
|
|
23
|
+
async resolve() {
|
|
24
|
+
const config = this.config;
|
|
25
|
+
let endpoint = config.endpoint.get();
|
|
26
|
+
let ref = config.credentialRef.get();
|
|
27
|
+
const literal = config.apiKey.get();
|
|
28
|
+
const limits = {
|
|
29
|
+
timeoutMs: config.timeoutMs.get(), maxResponseBytes: config.maxResponseBytes.get(),
|
|
30
|
+
maxAnswerChars: config.maxAnswerChars.get(),
|
|
31
|
+
};
|
|
32
|
+
if (config.authMode.get() === 'openai') {
|
|
33
|
+
const provider = config.openaiProvider.get();
|
|
34
|
+
const llm = this.ctx.get('llm');
|
|
35
|
+
const settings = this.ctx.get('settings');
|
|
36
|
+
if (!llm || !settings)
|
|
37
|
+
fail('OpenAI authentication reuse requires the LLM and Settings services');
|
|
38
|
+
const route = llm.listConfigurableProviders().find(entry => entry.provider === provider);
|
|
39
|
+
if (!route)
|
|
40
|
+
fail('selected OpenAI provider is not configured');
|
|
41
|
+
if (route.error)
|
|
42
|
+
fail('repair the selected OpenAI provider in model settings');
|
|
43
|
+
const section = settings.describe({ redactSecrets: true }).find(entry => entry.ns === route.settingsNs);
|
|
44
|
+
if (!section)
|
|
45
|
+
fail('selected OpenAI provider settings are unavailable');
|
|
46
|
+
let value = section.value;
|
|
47
|
+
for (const key of route.settingsPath)
|
|
48
|
+
value = record(value)[key];
|
|
49
|
+
const profile = record(value);
|
|
50
|
+
if (profile.headers !== undefined && Object.keys(record(profile.headers)).length) {
|
|
51
|
+
fail('custom OpenAI headers cannot be reused; configure a dedicated credential');
|
|
52
|
+
}
|
|
53
|
+
let base = typeof profile.baseURL === 'string' && profile.baseURL ? profile.baseURL : undefined;
|
|
54
|
+
if (!base)
|
|
55
|
+
for (const name of route.baseURLEnv ?? []) {
|
|
56
|
+
if (process.env[name]) {
|
|
57
|
+
base = process.env[name];
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
base ??= route.defaultBaseURL;
|
|
62
|
+
if (!base)
|
|
63
|
+
fail('selected OpenAI provider has no endpoint');
|
|
64
|
+
const expected = new URL(`${base.replace(/\/+$/, '')}/alpha/search`);
|
|
65
|
+
// endpoint belongs to independent authentication; never forward reused keys there.
|
|
66
|
+
endpoint = expected.href;
|
|
67
|
+
ref = typeof profile.apiKeyEnv === 'string' ? profile.apiKeyEnv : '';
|
|
68
|
+
if (!ref)
|
|
69
|
+
fail('this provider authentication cannot be reused; configure a dedicated credential');
|
|
70
|
+
}
|
|
71
|
+
if (!endpoint)
|
|
72
|
+
fail('独立凭据模式需要填写 Alpha Search 完整端点;默认复用 OpenAI 模式无需填写端点');
|
|
73
|
+
const credentials = this.ctx.get('credentials');
|
|
74
|
+
const key = config.authMode.get() === 'credential' && literal
|
|
75
|
+
? literal : ref && credentials ? (await credentials.resolve(credentialRef(ref)))?.value : undefined;
|
|
76
|
+
if (!key)
|
|
77
|
+
fail('API key is unavailable; configure the selected credential (OAuth reuse is not supported)');
|
|
78
|
+
return { endpoint, apiKey: key, ...limits };
|
|
79
|
+
}
|
|
80
|
+
async search(request, signal) {
|
|
81
|
+
signal?.throwIfAborted();
|
|
82
|
+
const resolved = await this.resolve();
|
|
83
|
+
signal?.throwIfAborted();
|
|
84
|
+
return new CodexSearchProvider(resolved).search(request, signal);
|
|
85
|
+
}
|
|
86
|
+
}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import Schema from '@deepseek-ai/schemastery';
|
|
2
|
+
import { ConfiguredSearchProvider } from './connection.js';
|
|
3
|
+
export const name = 'dsh-search-plugin';
|
|
4
|
+
export const inject = ['web'];
|
|
5
|
+
export const Config = Schema.object({
|
|
6
|
+
authMode: Schema.union(['openai', 'credential']).default('openai').volatile(),
|
|
7
|
+
openaiProvider: Schema.string().min(1).default('openai').volatile(),
|
|
8
|
+
credentialRef: Schema.string().pattern(/^[A-Za-z_][A-Za-z0-9_]*$/).default('DSH_CODEX_API_KEY').volatile(),
|
|
9
|
+
endpoint: Schema.string().pattern(/^$|^https?:\/\/[^\s]+$/).default('').volatile(),
|
|
10
|
+
apiKey: Schema.string().role('secret').volatile(),
|
|
11
|
+
timeoutMs: Schema.number().step(1).min(1).default(60000).volatile(),
|
|
12
|
+
maxResponseBytes: Schema.number().step(1).min(1).default(2000000).volatile(),
|
|
13
|
+
maxAnswerChars: Schema.number().step(1).min(1).default(16000).volatile(),
|
|
14
|
+
});
|
|
15
|
+
/** Register the provider; Cordis owns its registration and optional settings policy. */
|
|
16
|
+
export function apply(ctx, config) {
|
|
17
|
+
ctx.web.registerSearchProvider(new ConfiguredSearchProvider(ctx, config));
|
|
18
|
+
ctx.inject(['settings'], child => {
|
|
19
|
+
child.effect(() => child.settings.configure({ auto: false }, ctx.fiber));
|
|
20
|
+
});
|
|
21
|
+
}
|
package/lib/provider.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { WebError } from '@deepseek-ai/dsh-web';
|
|
3
|
+
// Alpha Search's routing model, not the user's chat/Responses model.
|
|
4
|
+
export const ALPHA_SEARCH_MODEL = 'gpt-5.6-luna';
|
|
5
|
+
function protocolError(message) {
|
|
6
|
+
throw new WebError(`Codex search: ${message}`, 'CODEX_PROTOCOL_ERROR');
|
|
7
|
+
}
|
|
8
|
+
/** Decode Alpha's output/results, never Responses output items or guessed links. */
|
|
9
|
+
export function parseSearchResponse(value, maxAnswerChars) {
|
|
10
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
11
|
+
protocolError('expected an object');
|
|
12
|
+
const response = value;
|
|
13
|
+
if (typeof response.output !== 'string')
|
|
14
|
+
protocolError('output text is missing');
|
|
15
|
+
if (response.results != null && !Array.isArray(response.results))
|
|
16
|
+
protocolError('expected results to be an array');
|
|
17
|
+
const sources = new Map();
|
|
18
|
+
// Alpha results are opaque, extensible DTOs; only web URLs are citeable sources.
|
|
19
|
+
for (const item of response.results ?? []) {
|
|
20
|
+
if (typeof item !== 'object' || item === null || typeof item.url !== 'string')
|
|
21
|
+
continue;
|
|
22
|
+
let url;
|
|
23
|
+
try {
|
|
24
|
+
url = new URL(item.url);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password)
|
|
30
|
+
continue;
|
|
31
|
+
const previous = sources.get(item.url);
|
|
32
|
+
sources.set(item.url, {
|
|
33
|
+
url: item.url,
|
|
34
|
+
...(previous?.title !== undefined ? { title: previous.title } : typeof item.title === 'string' ? { title: item.title } : {}),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
// Alpha ref_ids have no meaning in DSH; structured sources are returned separately.
|
|
38
|
+
const answer = response.output.replace(/\uE200cite\uE202[^\uE201]*\uE201(?:\s*\[wordlim:\s*\d+\])?/gu, '').trim();
|
|
39
|
+
let content = answer.slice(0, maxAnswerChars);
|
|
40
|
+
if (content.length < answer.length && /[\uD800-\uDBFF]$/.test(content))
|
|
41
|
+
content = content.slice(0, -1);
|
|
42
|
+
return { ...(content ? { content } : {}), sources: [...sources.values()], truncated: false };
|
|
43
|
+
}
|
|
44
|
+
/** Alpha Search only. No retry, redirect following, or protocol fallback. */
|
|
45
|
+
export class CodexSearchProvider {
|
|
46
|
+
id = 'codex';
|
|
47
|
+
config;
|
|
48
|
+
constructor(config) {
|
|
49
|
+
const endpoint = new URL(config.endpoint);
|
|
50
|
+
if (!['http:', 'https:'].includes(endpoint.protocol) || endpoint.username || endpoint.password || endpoint.hash) {
|
|
51
|
+
throw new Error('Codex endpoint must be an HTTP(S) URL without credentials or a fragment');
|
|
52
|
+
}
|
|
53
|
+
if (!config.apiKey.trim())
|
|
54
|
+
throw new Error('Codex apiKey must be non-empty');
|
|
55
|
+
this.config = { ...config };
|
|
56
|
+
}
|
|
57
|
+
available() { return true; }
|
|
58
|
+
async search(request, signal) {
|
|
59
|
+
const timeout = AbortSignal.timeout(this.config.timeoutMs);
|
|
60
|
+
const combined = signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
61
|
+
try {
|
|
62
|
+
const response = await fetch(this.config.endpoint, {
|
|
63
|
+
method: 'POST', redirect: 'manual', signal: combined,
|
|
64
|
+
headers: { Authorization: `Bearer ${this.config.apiKey}`, 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
65
|
+
body: JSON.stringify({
|
|
66
|
+
id: randomUUID(), model: ALPHA_SEARCH_MODEL,
|
|
67
|
+
input: [{ type: 'message', role: 'user', content: [{ type: 'input_text', text: request.query }] }],
|
|
68
|
+
commands: { search_query: [{ q: request.query }] },
|
|
69
|
+
settings: { allowed_callers: ['direct'], external_web_access: true },
|
|
70
|
+
max_output_tokens: 2048,
|
|
71
|
+
}),
|
|
72
|
+
});
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
await response.body?.cancel();
|
|
75
|
+
throw new WebError(`Codex search HTTP ${response.status}`, 'CODEX_HTTP_ERROR');
|
|
76
|
+
}
|
|
77
|
+
if (!response.body)
|
|
78
|
+
protocolError('empty response body');
|
|
79
|
+
const reader = response.body.getReader();
|
|
80
|
+
const chunks = [];
|
|
81
|
+
let bytes = 0;
|
|
82
|
+
try {
|
|
83
|
+
while (true) {
|
|
84
|
+
const { done, value } = await reader.read();
|
|
85
|
+
if (done)
|
|
86
|
+
break;
|
|
87
|
+
bytes += value.byteLength;
|
|
88
|
+
if (bytes > this.config.maxResponseBytes) {
|
|
89
|
+
await reader.cancel();
|
|
90
|
+
throw new WebError('Codex search response exceeds maxResponseBytes', 'CODEX_RESPONSE_TOO_LARGE');
|
|
91
|
+
}
|
|
92
|
+
chunks.push(value);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
reader.releaseLock();
|
|
97
|
+
}
|
|
98
|
+
const buffer = new Uint8Array(bytes);
|
|
99
|
+
let offset = 0;
|
|
100
|
+
for (const chunk of chunks) {
|
|
101
|
+
buffer.set(chunk, offset);
|
|
102
|
+
offset += chunk.length;
|
|
103
|
+
}
|
|
104
|
+
let value;
|
|
105
|
+
try {
|
|
106
|
+
value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(buffer));
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
protocolError('expected a UTF-8 JSON response; SSE is not supported');
|
|
110
|
+
}
|
|
111
|
+
return parseSearchResponse(value, this.config.maxAnswerChars);
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
if (error instanceof WebError)
|
|
115
|
+
throw error;
|
|
116
|
+
if (signal?.aborted)
|
|
117
|
+
throw new WebError('Codex search cancelled', 'WEB_ABORTED');
|
|
118
|
+
if (timeout.aborted)
|
|
119
|
+
throw new WebError('Codex search timed out', 'CODEX_TIMEOUT');
|
|
120
|
+
throw new WebError('Codex search transport failed', 'CODEX_TRANSPORT_ERROR');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import { type WebSearchProvider, type WebSearchRequest, type WebSearchResult } from '@deepseek-ai/dsh-web';
|
|
3
|
+
import type { Config } from './index.js';
|
|
4
|
+
import { type AlphaSearchConfig } from './provider.js';
|
|
5
|
+
/** Resolve configuration and credentials for each search, without retaining secrets between operations. */
|
|
6
|
+
export declare class ConfiguredSearchProvider implements WebSearchProvider {
|
|
7
|
+
private readonly ctx;
|
|
8
|
+
private readonly config;
|
|
9
|
+
readonly id = "codex";
|
|
10
|
+
constructor(ctx: Context, config: Config);
|
|
11
|
+
available(): boolean;
|
|
12
|
+
/** Capture endpoint, limits and the selected credential as one request configuration. */
|
|
13
|
+
resolve(): Promise<AlphaSearchConfig>;
|
|
14
|
+
search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Context, Volatile } from '@deepseek-ai/cordis';
|
|
2
|
+
import Schema from '@deepseek-ai/schemastery';
|
|
3
|
+
export declare const name = "dsh-search-plugin";
|
|
4
|
+
export declare const inject: string[];
|
|
5
|
+
/** Live search settings. Credentials are resolved by reference for each operation. */
|
|
6
|
+
export interface Config {
|
|
7
|
+
authMode: Volatile<'openai' | 'credential'>;
|
|
8
|
+
openaiProvider: Volatile<string>;
|
|
9
|
+
credentialRef: Volatile<string>;
|
|
10
|
+
endpoint: Volatile<string>;
|
|
11
|
+
apiKey: Volatile<string | undefined>;
|
|
12
|
+
timeoutMs: Volatile<number>;
|
|
13
|
+
maxResponseBytes: Volatile<number>;
|
|
14
|
+
maxAnswerChars: Volatile<number>;
|
|
15
|
+
}
|
|
16
|
+
export declare const Config: Schema<Schemastery.ObjectS<NoInfer<{
|
|
17
|
+
authMode: Schema<"openai" | "credential", "openai" | "credential", "volatile-defined">;
|
|
18
|
+
openaiProvider: Schema<string, string, "volatile-defined">;
|
|
19
|
+
credentialRef: Schema<string, string, "volatile-defined">;
|
|
20
|
+
endpoint: Schema<string, string, "volatile-defined">;
|
|
21
|
+
apiKey: Schema<string, string, "volatile">;
|
|
22
|
+
timeoutMs: Schema<number, number, "volatile-defined">;
|
|
23
|
+
maxResponseBytes: Schema<number, number, "volatile-defined">;
|
|
24
|
+
maxAnswerChars: Schema<number, number, "volatile-defined">;
|
|
25
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
26
|
+
authMode: Schema<"openai" | "credential", "openai" | "credential", "volatile-defined">;
|
|
27
|
+
openaiProvider: Schema<string, string, "volatile-defined">;
|
|
28
|
+
credentialRef: Schema<string, string, "volatile-defined">;
|
|
29
|
+
endpoint: Schema<string, string, "volatile-defined">;
|
|
30
|
+
apiKey: Schema<string, string, "volatile">;
|
|
31
|
+
timeoutMs: Schema<number, number, "volatile-defined">;
|
|
32
|
+
maxResponseBytes: Schema<number, number, "volatile-defined">;
|
|
33
|
+
maxAnswerChars: Schema<number, number, "volatile-defined">;
|
|
34
|
+
}>>, "plain">;
|
|
35
|
+
/** Register the provider; Cordis owns its registration and optional settings policy. */
|
|
36
|
+
export declare function apply(ctx: Context, config: Config): void;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type WebSearchProvider, type WebSearchRequest, type WebSearchResult } from '@deepseek-ai/dsh-web';
|
|
2
|
+
/** Resolved values for one Alpha Search request; never persisted or exposed to the browser. */
|
|
3
|
+
export interface AlphaSearchConfig {
|
|
4
|
+
endpoint: string;
|
|
5
|
+
apiKey: string;
|
|
6
|
+
timeoutMs: number;
|
|
7
|
+
maxResponseBytes: number;
|
|
8
|
+
maxAnswerChars: number;
|
|
9
|
+
}
|
|
10
|
+
export declare const ALPHA_SEARCH_MODEL = "gpt-5.6-luna";
|
|
11
|
+
/** Decode Alpha's output/results, never Responses output items or guessed links. */
|
|
12
|
+
export declare function parseSearchResponse(value: unknown, maxAnswerChars: number): WebSearchResult;
|
|
13
|
+
/** Alpha Search only. No retry, redirect following, or protocol fallback. */
|
|
14
|
+
export declare class CodexSearchProvider implements WebSearchProvider {
|
|
15
|
+
readonly id = "codex";
|
|
16
|
+
private readonly config;
|
|
17
|
+
constructor(config: AlphaSearchConfig);
|
|
18
|
+
available(): boolean;
|
|
19
|
+
search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult>;
|
|
20
|
+
}
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,88 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-search-plugin",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
"version": "0.1.5",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "git+https://github.com/urzz/dsh-search-plugin.git"
|
|
7
|
+
},
|
|
8
|
+
"description": "Give agents access to information from the web to inform their analysis and decisions.",
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "lib/index.js",
|
|
11
|
+
"types": "lib/types/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./lib/types/index.d.ts",
|
|
15
|
+
"default": "./lib/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./client": "./lib/client.js",
|
|
18
|
+
"./package.json": "./package.json",
|
|
19
|
+
"./locale/*.json": "./locale/*.json"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"lib",
|
|
23
|
+
"locale/*.json",
|
|
24
|
+
"cordis.patch.yml",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"engines": {
|
|
28
|
+
"dsh": ">=0.2.1-rc.4 <0.3.0"
|
|
29
|
+
},
|
|
30
|
+
"dsh": {
|
|
31
|
+
"bundle": {
|
|
32
|
+
"patch": "./cordis.patch.yml"
|
|
33
|
+
},
|
|
34
|
+
"client": {
|
|
35
|
+
"platform": "web",
|
|
36
|
+
"inject": [
|
|
37
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
38
|
+
"@deepseek-ai/dsh-client-ui-plugin-manager",
|
|
39
|
+
"@deepseek-ai/dsh-client-locale",
|
|
40
|
+
"@deepseek-ai/dsh-client-ui-renderer",
|
|
41
|
+
"@deepseek-ai/dsh-api-remotes"
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsc -p tsconfig.json && node scripts/build-client.mjs",
|
|
47
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.client.json",
|
|
48
|
+
"test": "pnpm build && node --test tests/*.test.mjs",
|
|
49
|
+
"check": "pnpm typecheck && pnpm test",
|
|
50
|
+
"prepack": "pnpm build"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@deepseek-ai/schemastery": "~3.18.4"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"@deepseek-ai/cordis": "^4.0.0",
|
|
57
|
+
"@deepseek-ai/dsh-credentials": "0.2.1-rc.4",
|
|
58
|
+
"@deepseek-ai/dsh-web": "0.2.1-rc.4"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@deepseek-ai/cordis": "^4.0.0",
|
|
62
|
+
"@deepseek-ai/cordis-plugin-loader": "1.0.6",
|
|
63
|
+
"@deepseek-ai/dsh-api-gateway": "0.2.1-rc.4",
|
|
64
|
+
"@deepseek-ai/dsh-api-remotes": "0.2.1-rc.4",
|
|
65
|
+
"@deepseek-ai/dsh-api-settings-controller": "0.2.1-rc.4",
|
|
66
|
+
"@deepseek-ai/dsh-app-boot": "0.2.1-rc.4",
|
|
67
|
+
"@deepseek-ai/dsh-client-locale": "0.2.1-rc.4",
|
|
68
|
+
"@deepseek-ai/dsh-client-store": "0.2.1-rc.4",
|
|
69
|
+
"@deepseek-ai/dsh-client-ui-plugin-manager": "0.2.1-rc.4",
|
|
70
|
+
"@deepseek-ai/dsh-client-ui-renderer": "0.2.1-rc.4",
|
|
71
|
+
"@deepseek-ai/dsh-client-ui-settings": "0.2.1-rc.4",
|
|
72
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.2.1-rc.4",
|
|
73
|
+
"@deepseek-ai/dsh-credentials": "0.2.1-rc.4",
|
|
74
|
+
"@deepseek-ai/dsh-credentials-local": "0.2.1-rc.4",
|
|
75
|
+
"@deepseek-ai/dsh-llm": "0.2.1-rc.4",
|
|
76
|
+
"@deepseek-ai/dsh-settings": "0.2.1-rc.4",
|
|
77
|
+
"@deepseek-ai/dsh-tool-web": "0.2.1-rc.4",
|
|
78
|
+
"@deepseek-ai/dsh-tools": "0.2.1-rc.4",
|
|
79
|
+
"@deepseek-ai/dsh-web": "0.2.1-rc.4",
|
|
80
|
+
"@deepseek-ai/dsh-web-fetch-http": "0.2.1-rc.4",
|
|
81
|
+
"@types/node": "^22.0.0",
|
|
82
|
+
"@types/react": "^19.0.0",
|
|
83
|
+
"esbuild": "^0.25.0",
|
|
84
|
+
"react": "^19.0.0",
|
|
85
|
+
"react-dom": "^19.3.1",
|
|
86
|
+
"typescript": "^5.9.3"
|
|
87
|
+
}
|
|
88
|
+
}
|