@dsh-plus/web-search-services 0.1.20 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dsh-plus/web-search-services",
3
- "version": "0.1.20",
4
- "description": "dsh-plus service plugin: web_search 免费后端聚合——经官方 ctx.web provider 扩展点桥接 search-services skill(Tavily/Exa/OpenAI Chat 免费额度,优先级+自动回退+key 配置),官方 web_search 工具与输出格式零修改",
3
+ "version": "0.2.0",
4
+ "description": "dsh-plus service plugin: web_search 多后端聚合——经官方 ctx.web provider 扩展点接入 Tavily/Exa/OpenAI 兼容/Context7/DeepSeek Search(顺序尝试 + 失败自动回退 + 官方 credentials 密钥),官方 web_search 工具与输出格式零修改",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/index.d.ts",
@@ -18,8 +18,7 @@
18
18
  },
19
19
  "files": [
20
20
  "lib",
21
- "src",
22
- "scripts"
21
+ "src"
23
22
  ],
24
23
  "dsh": {
25
24
  "client": {
@@ -32,21 +31,22 @@
32
31
  }
33
32
  },
34
33
  "dependencies": {
35
- "@dsh-plus/shared": "0.1.42"
34
+ "@dsh-plus/shared": "0.1.43",
35
+ "@dsh-plus/web-providers-core": "0.1.0"
36
36
  },
37
37
  "peerDependencies": {
38
- "@deepseek-ai/cordis": "^4.0.5-alpha.2",
39
- "@deepseek-ai/dsh-home-paths": "^0.2.1-alpha.4",
40
- "@deepseek-ai/dsh-settings": "^0.2.1-alpha.4",
41
- "@deepseek-ai/dsh-web": "^0.2.1-alpha.4",
42
- "@deepseek-ai/schemastery": "^3.18.5-alpha.2"
38
+ "@deepseek-ai/cordis": "^4.0.5",
39
+ "@deepseek-ai/dsh-home-paths": "^0.2.1-rc.2",
40
+ "@deepseek-ai/dsh-settings": "^0.2.1-rc.2",
41
+ "@deepseek-ai/dsh-web": "^0.2.1-rc.2",
42
+ "@deepseek-ai/schemastery": "^3.18.5"
43
43
  },
44
44
  "devDependencies": {
45
- "@deepseek-ai/cordis": "4.0.5-alpha.2",
46
- "@deepseek-ai/dsh-home-paths": "0.2.1-alpha.4",
47
- "@deepseek-ai/dsh-settings": "0.2.1-alpha.4",
48
- "@deepseek-ai/dsh-web": "0.2.1-alpha.4",
49
- "@deepseek-ai/schemastery": "3.18.5-alpha.2",
45
+ "@deepseek-ai/cordis": "4.0.5",
46
+ "@deepseek-ai/dsh-home-paths": "0.2.1-rc.2",
47
+ "@deepseek-ai/dsh-settings": "0.2.1-rc.2",
48
+ "@deepseek-ai/dsh-web": "0.2.1-rc.2",
49
+ "@deepseek-ai/schemastery": "3.18.5",
50
50
  "@types/react": "~18.3.31",
51
51
  "react": "^18.2.0"
52
52
  },
@@ -1,56 +1,131 @@
1
1
  /**
2
2
  * 「搜索后端聚合」配置卡片:经 injectPluginConfigCard 注册(插件页
3
3
  * plugins.row.config / plugins.bundle.config,view 分发 summary/page)。
4
- * 字段:scriptPath / envFile / python / priority(每行一个后端)/ timeoutMs。
5
- * 密钥不走卡片:keys 段的 role(secret) 字段只允许 profile 行级注入,用户层
6
- * 写入会被脱敏遮蔽——卡片改为把密钥来源(env 文件)与覆盖方式写在页面上。
7
- * 外壳与控件走 @dsh-plus/shared/client 套件(CardChrome/TextField/
8
- * useNamespaceDraft);纯校验逻辑在同目录 draft.ts(node --test 可直测)。
4
+ *
5
+ * 结构:总开关 / 暴露条目 / 代理 → 后端链(条目块)→ 密钥(官方 credentials)。
6
+ * 字段与默认值来自 `@dsh-plus/web-providers-core/spec`,校验逻辑来自其 `card` 工具箱,
7
+ * 本文件只负责布局与文案。
9
8
  * @module web-search-services/client/card
10
9
  */
11
-
12
10
  import {
13
11
  CardChrome,
14
12
  CardLoading,
13
+ type CardStatusState,
14
+ CheckRow,
15
+ type CredentialsRemoteFace,
16
+ CredentialsSection,
15
17
  type NamespaceSettingsApi,
16
18
  type PluginConfigViewProps,
17
19
  type Scope,
18
20
  TextField,
19
21
  useNamespaceDraft,
20
22
  } from '@dsh-plus/shared/client'
21
- import type { ReactElement } from 'react'
23
+ import type { IssueCode } from '@dsh-plus/web-providers-core/card'
24
+ import { EntryEditor, type EntryEditorLabels } from '@dsh-plus/web-providers-core/card-ui'
25
+ import { type ReactElement, useMemo } from 'react'
22
26
 
23
27
  import {
24
28
  type ConfigValue,
25
29
  type Draft,
26
30
  draftFromValue,
27
- priorityTextOk,
28
- timeoutTextOk,
31
+ draftValid,
32
+ refsOf,
33
+ SEARCH_KIT,
29
34
  toPatch,
30
35
  } from './draft.ts'
31
- import type { Translate } from './i18n.ts'
36
+ import type { DictKey, Translate } from './i18n.ts'
32
37
 
33
38
  export interface CardProps extends PluginConfigViewProps {
34
39
  t: Translate
35
40
  scope: Scope
36
41
  api: NamespaceSettingsApi
42
+ credentials: CredentialsRemoteFace
37
43
  }
38
44
 
39
45
  export function WebSearchServicesCard(props: CardProps): ReactElement | string | null {
40
- const { t, scope, api } = props
46
+ const { t, scope, api, credentials } = props
41
47
  const { draft, dirty, saving, status, disabled, edit, reset, save } = useNamespaceDraft<
42
48
  ConfigValue,
43
49
  Draft
44
50
  >({ scope, api, from: draftFromValue, patch: toPatch, saveFailedLabel: t('saveFailed') })
45
- // 插件页 page 视图为表单落地页,默认展开;旧槽位无 view,保持折叠。
46
51
 
47
52
  // 插件页 summary 视图只出一行简介(hooks 已全部落定,可安全提前返回)。
48
53
  if (props.view === 'summary') return t('summaryLine')
49
-
50
54
  if (draft === null) return <CardLoading prefix="wsv" text={t('loading')} />
51
55
 
52
- const priorityInvalid = !priorityTextOk(draft.priorityText)
53
- const timeoutInvalid = !timeoutTextOk(draft.timeoutMsText)
56
+ return (
57
+ <CardBody
58
+ t={t}
59
+ draft={draft}
60
+ status={status}
61
+ disabled={disabled}
62
+ dirty={dirty}
63
+ saving={saving}
64
+ credentials={credentials}
65
+ onEdit={edit}
66
+ onReset={reset}
67
+ onSave={save}
68
+ />
69
+ )
70
+ }
71
+
72
+ /** 校验代号 → 文案键。 */
73
+ const ISSUE_KEYS: Record<IssueCode, DictKey> = {
74
+ 'id-empty': 'issueIdEmpty',
75
+ required: 'issueRequired',
76
+ 'id-duplicate': 'issueIdDuplicate',
77
+ 'type-unknown': 'issueTypeUnknown',
78
+ int: 'issueInt',
79
+ 'below-min': 'issueBelowMin',
80
+ 'above-max': 'issueAboveMax',
81
+ option: 'issueOption',
82
+ json: 'issueJson',
83
+ 'json-array': 'issueJsonArray',
84
+ 'json-object': 'issueJsonObject',
85
+ 'json-strings': 'issueJsonStrings',
86
+ 'library-shape': 'issueLibraryShape',
87
+ }
88
+
89
+ /** 条目编辑器的已翻译文案(字段标签用 spec 的技术名,不做翻译)。 */
90
+ function editorLabels(t: Translate): EntryEditorLabels {
91
+ return {
92
+ title: t('entriesTitle'),
93
+ description: t('entriesHint'),
94
+ addEntry: t('addEntry'),
95
+ empty: t('entriesEmpty'),
96
+ unknownType: t('unknownType'),
97
+ entryId: t('entryId'),
98
+ entryIdHint: t('entryIdHint'),
99
+ entryEnabled: t('entryEnabled'),
100
+ moveUp: t('moveUp'),
101
+ moveDown: t('moveDown'),
102
+ remove: t('remove'),
103
+ unsetOption: t('unsetOption'),
104
+ csvHint: t('csvHint'),
105
+ librariesHint: t('librariesHint'),
106
+ headersHint: t('headersHint'),
107
+ issue: (code) => t(ISSUE_KEYS[code]),
108
+ }
109
+ }
110
+
111
+ interface CardBodyProps {
112
+ t: Translate
113
+ draft: Draft
114
+ status: CardStatusState
115
+ disabled: boolean
116
+ dirty: boolean
117
+ saving: boolean
118
+ credentials: CredentialsRemoteFace
119
+ onEdit(patch: Partial<Draft>): void
120
+ onReset(): void
121
+ onSave(): void
122
+ }
123
+
124
+ function CardBody(props: CardBodyProps): ReactElement {
125
+ const { t, draft, status, disabled, dirty, saving, credentials } = props
126
+ const refs = useMemo(() => refsOf(draft), [draft])
127
+ const valid = draftValid(draft)
128
+
54
129
  return (
55
130
  <CardChrome
56
131
  prefix="wsv"
@@ -61,83 +136,66 @@ export function WebSearchServicesCard(props: CardProps): ReactElement | string |
61
136
  readOnlyNotice={disabled ? t('readOnly') : undefined}
62
137
  status={status}
63
138
  actions={[
64
- {
65
- key: 'discard',
66
- label: t('discard'),
67
- disabled: !dirty || saving,
68
- onClick: reset,
69
- },
139
+ { key: 'discard', label: t('discard'), disabled: !dirty || saving, onClick: props.onReset },
70
140
  {
71
141
  key: 'save',
72
142
  label: t(saving ? 'saving' : 'save'),
73
143
  variant: 'primary',
74
- disabled: !dirty || priorityInvalid || timeoutInvalid || saving || disabled,
75
- onClick: save,
144
+ disabled: !dirty || !valid || saving || disabled,
145
+ onClick: props.onSave,
76
146
  },
77
147
  ]}
78
148
  >
79
- <TextField
149
+ <CheckRow
80
150
  prefix="wsv"
81
- id="wsv-script"
82
- label={t('scriptPath')}
83
- hint={t('scriptPathHint')}
84
- value={draft.scriptPath}
151
+ id="wsv-enabled"
152
+ label={t('enabled')}
153
+ checked={draft.enabled}
85
154
  disabled={disabled}
86
- onEdit={(v) => edit({ scriptPath: v })}
155
+ onEdit={(checked) => props.onEdit({ enabled: checked })}
87
156
  />
88
- <TextField
157
+ <CheckRow
89
158
  prefix="wsv"
90
- id="wsv-envfile"
91
- label={t('envFile')}
92
- hint={t('envFileHint')}
93
- value={draft.envFile}
159
+ id="wsv-expose"
160
+ label={t('exposeEntries')}
161
+ checked={draft.exposeEntries}
94
162
  disabled={disabled}
95
- onEdit={(v) => edit({ envFile: v })}
163
+ onEdit={(checked) => props.onEdit({ exposeEntries: checked })}
96
164
  />
165
+ <p className="wsv-hint">{t('exposeEntriesHint')}</p>
97
166
  <TextField
98
167
  prefix="wsv"
99
- id="wsv-python"
100
- label={t('python')}
101
- hint={t('pythonHint')}
102
- value={draft.python}
168
+ id="wsv-proxy"
169
+ label={t('proxy')}
170
+ hint={t('proxyHint')}
171
+ value={draft.proxy}
103
172
  disabled={disabled}
104
- onEdit={(v) => edit({ python: v })}
173
+ onEdit={(value) => props.onEdit({ proxy: value })}
105
174
  />
106
- <div className="wsv-field">
107
- <div className="wsv-head">
108
- <label className="wsv-label" htmlFor="wsv-priority">
109
- {t('priority')}
110
- </label>
111
- </div>
112
- <textarea
113
- id="wsv-priority"
114
- className="wsv-priority"
115
- rows={3}
116
- value={draft.priorityText}
117
- disabled={disabled}
118
- onChange={(event) => edit({ priorityText: event.target.value })}
119
- />
120
- <p className={priorityInvalid ? 'wsv-invalid' : 'wsv-hint'}>
121
- {priorityInvalid ? t('invalidPriority') : t('priorityHint')}
122
- </p>
123
- </div>
124
- <TextField
175
+ <p className="wsv-hint">{t('enabledHint')}</p>
176
+ <EntryEditor
125
177
  prefix="wsv"
126
- id="wsv-timeout"
127
- label={t('timeoutMs')}
128
- hint={t('timeoutMsHint')}
129
- value={draft.timeoutMsText}
130
- numeric
178
+ kit={SEARCH_KIT}
179
+ labels={editorLabels(t)}
180
+ draft={draft}
131
181
  disabled={disabled}
132
- invalid={timeoutInvalid}
133
- invalidLabel={t('invalidTimeout')}
134
- onEdit={(v) => edit({ timeoutMsText: v })}
182
+ onChange={(next) => props.onEdit(next)}
183
+ />
184
+ <CredentialsSection
185
+ prefix="wsv"
186
+ title={t('secretsTitle')}
187
+ description={t('secretsBody')}
188
+ rows={refs.map((ref) => ({ ref, label: ref }))}
189
+ face={credentials}
190
+ labels={{
191
+ save: t('save'),
192
+ clear: t('secretClear'),
193
+ configured: t('secretConfigured'),
194
+ missing: t('secretMissing'),
195
+ shadowed: t('secretShadowed'),
196
+ failed: t('secretFailed'),
197
+ }}
135
198
  />
136
- <div className="wsv-keys">
137
- <p className="wsv-keysTitle">{t('keysGroup')}</p>
138
- <p className="wsv-hint">{t('keysHint')}</p>
139
- <p className="wsv-hint">{t('keysUserLayerHint')}</p>
140
- </div>
141
199
  <p className="wsv-hint">{t('reloadHint')}</p>
142
200
  </CardChrome>
143
201
  )
@@ -1,19 +1,20 @@
1
1
  /**
2
2
  * 浏览器半入口:注册 locale 字典 + 注册「搜索后端聚合」卡片
3
- * (injectPluginConfigCard 两槽位:插件页 plugins.row.config /
4
- * plugins.bundle.config)。
3
+ * (injectPluginConfigCard 两槽位:插件页 plugins.row.config / plugins.bundle.config)。
5
4
  * 构建产物为 window.__ModuleLoader__.load({id, factory}) 形式的 CJS factory
6
5
  * (包装见 tsdown.config.ts);样式沿用官方 data-plugin-css 约定,HMR 据此卸载。
7
- * 配置读写经 ctx.remote.settings 直连(scope/api 来自 @dsh-plus/shared/client);
8
- * 密钥不经卡片(role(secret) 只允许行级注入),见 card.tsx 的说明块。
6
+ * 配置读写经 ctx.remote.settings、密钥读写经 ctx.remote.credentials(均来自
7
+ * @dsh-plus/shared/client 的窄面)。
9
8
  * @module web-search-services/client
10
9
  */
11
10
  import type { Context } from '@deepseek-ai/cordis'
12
11
  import {
13
12
  createNamespaceApi,
14
13
  createSettingsScope,
14
+ credentialsFace,
15
15
  injectPluginConfigCard,
16
16
  type PluginClientContext,
17
+ type RemoteWithCredentials,
17
18
  } from '@dsh-plus/shared/client'
18
19
 
19
20
  import { SETTINGS_NS } from '../ns.ts'
@@ -23,8 +24,18 @@ import { injectStyle } from './styles.ts'
23
24
 
24
25
  export const name = 'dsh-plus-web-search-services'
25
26
 
26
- /** 浏览器半需要的 cordis 服务 key(loader 据此注入;package.json 的 dsh.client.inject 管包加载顺序)。 */
27
- export const inject = ['slots', 'locale', 'remote', 'remote.settings'] as const
27
+ /**
28
+ * 浏览器半需要的 cordis 服务 key(loader 据此注入;package.json 的 dsh.client.inject
29
+ * 管包加载顺序)。`remote.credentials` 为硬依赖:卡片是密钥的唯一配置面,
30
+ * 命名空间缺席时整张卡片都不该挂(官方设置页同样如此声明)。
31
+ */
32
+ export const inject = [
33
+ 'slots',
34
+ 'locale',
35
+ 'remote',
36
+ 'remote.settings',
37
+ 'remote.credentials',
38
+ ] as const
28
39
 
29
40
  /** 宿主窄面收编在 @dsh-plus/shared/client;TKey 传入本包 DictKey 使键名受检。 */
30
41
  type ClientContext = PluginClientContext<DictKey>
@@ -41,11 +52,12 @@ export function apply(ctx: Context): void {
41
52
  c.effect(() => c.locale.register(NS, { zh, en }), 'web-search-services: locale')
42
53
  const scope = createSettingsScope(c, SETTINGS_NS, 'web-search-services: settings scope')
43
54
  const api = createNamespaceApi(c.get('remote').settings, SETTINGS_NS)
55
+ const credentials = credentialsFace(c.get('remote') as RemoteWithCredentials)
44
56
  injectPluginConfigCard(c.slots, {
45
57
  ns: SETTINGS_NS,
46
58
  rowId: 'dsh-plus-web-search-services',
47
59
  pkg: '@dsh-plus/web-search-services',
48
60
  component: WebSearchServicesCard,
49
- inject: () => ({ t: c.locale.bind(NS), scope, api }),
61
+ inject: () => ({ t: c.locale.bind(NS), scope, api, credentials }),
50
62
  })
51
63
  }
@@ -1,92 +1,32 @@
1
1
  /**
2
- * 配置卡片草稿的纯逻辑(无 React/JSX,供 node --test 直接导入):
3
- * 解析值 → 草稿、草稿 → 提交形状,以及优先级列表与超时的校验。
2
+ * 搜索插件的卡片逻辑绑定:字段清单/默认值/校验全部来自
3
+ * `@dsh-plus/web-providers-core`(与后端 schema 同源),本文件只把搜索那份 spec 绑上。
4
4
  * @module web-search-services/client/draft
5
5
  */
6
- import type { SearchBackend } from '../config.ts'
7
-
8
- /** 可选后端(与 config.ts 的 PRIORITY_ENTRY union 同源顺序)。 */
9
- export const SEARCH_BACKENDS: readonly SearchBackend[] = ['tavily', 'exa', 'openai-chat']
10
-
11
- /** 默认后端顺序(与 config.ts 的 DEFAULT_PRIORITY 一致)。 */
12
- export const DEFAULT_PRIORITY: readonly SearchBackend[] = ['tavily', 'exa', 'openai-chat']
13
-
14
- /** 单次搜索子进程超时下限(与 config.ts 的 .min(1000) 一致)。 */
15
- export const TIMEOUT_MIN_MS = 1000
16
-
17
- /** settings 命名空间的解析值(keys 内 secret 字段已被 describe 层遮蔽,卡片不消费)。 */
18
- export interface ConfigValue {
19
- scriptPath: string
20
- envFile: string
21
- python: string
22
- priority: SearchBackend[]
23
- timeoutMs: number
24
- }
25
-
26
- /** 编辑草稿:字符串字段原样,数组与数值以文本承载(保存时校验并折算)。 */
27
- export interface Draft {
28
- scriptPath: string
29
- envFile: string
30
- python: string
31
- priorityText: string
32
- timeoutMsText: string
33
- }
34
-
35
- export function draftFromValue(value: ConfigValue): Draft {
36
- return {
37
- scriptPath: value.scriptPath,
38
- envFile: value.envFile,
39
- python: value.python,
40
- priorityText: value.priority.join('\n'),
41
- timeoutMsText: String(value.timeoutMs),
42
- }
43
- }
44
-
45
- /** 优先级文本 → 后端序列;顺带返回无法识别的行(供 UI 提示)。 */
46
- export function parsePriority(text: string): {
47
- backends: SearchBackend[]
48
- unknown: string[]
49
- duplicated: string[]
50
- } {
51
- const backends: SearchBackend[] = []
52
- const unknown: string[] = []
53
- const duplicated: string[] = []
54
- for (const raw of text.split('\n')) {
55
- const line = raw.trim()
56
- if (line === '') continue
57
- const match = SEARCH_BACKENDS.find((backend) => backend === line)
58
- if (match === undefined) {
59
- unknown.push(line)
60
- continue
61
- }
62
- if (backends.includes(match)) {
63
- duplicated.push(line)
64
- continue
65
- }
66
- backends.push(match)
67
- }
68
- return { backends, unknown, duplicated }
69
- }
70
-
71
- /** 优先级是否可用:至少一个后端、无未知项、无重复项。 */
72
- export function priorityTextOk(text: string): boolean {
73
- const { backends, unknown, duplicated } = parsePriority(text)
74
- return backends.length > 0 && unknown.length === 0 && duplicated.length === 0
75
- }
76
-
77
- /** 超时是否可用:≥1000 的整数(对齐 schema 的 .natural().min(1000))。 */
78
- export function timeoutTextOk(text: string): boolean {
79
- if (!/^\d+$/.test(text.trim())) return false
80
- return Number(text) >= TIMEOUT_MIN_MS
81
- }
82
-
83
- /** 草稿 → 提交形状(调用方保证已通过校验)。 */
84
- export function toPatch(draft: Draft): Record<string, unknown> {
85
- return {
86
- scriptPath: draft.scriptPath,
87
- envFile: draft.envFile,
88
- python: draft.python,
89
- priority: parsePriority(draft.priorityText).backends,
90
- timeoutMs: Number(draft.timeoutMsText),
91
- }
92
- }
6
+ import {
7
+ type ConfigValue,
8
+ createCardKit,
9
+ type Draft,
10
+ type EntryDraft,
11
+ type FieldValue,
12
+ type IssueCode,
13
+ } from '@dsh-plus/web-providers-core/card'
14
+ import { SEARCH_ENTRY_SPECS } from '@dsh-plus/web-providers-core/spec'
15
+
16
+ export type { ConfigValue, Draft, EntryDraft, FieldValue, IssueCode }
17
+
18
+ export const SEARCH_KIT = createCardKit(SEARCH_ENTRY_SPECS)
19
+
20
+ const kit = SEARCH_KIT
21
+
22
+ export const entryTypes = kit.entryTypes
23
+ export const specOf = kit.specOf
24
+ export const draftFromValue = kit.draftFromValue
25
+ export const addEntry = kit.addEntry
26
+ export const removeEntry = kit.removeEntry
27
+ export const moveEntry = kit.moveEntry
28
+ export const entryIssues = kit.entryIssues
29
+ export const issuesOf = kit.issuesOf
30
+ export const draftValid = kit.draftValid
31
+ export const toPatch = kit.toPatch
32
+ export const refsOf = kit.refsOf