dsh-plugin-weapp-gateway 0.8.23 → 0.8.34

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.
Files changed (71) hide show
  1. package/LICENSE +21 -21
  2. package/PROTOCOL.md +1545 -1486
  3. package/README.md +503 -464
  4. package/THIRD-PARTY-NOTICES/dsh-cost-meter.txt +31 -31
  5. package/bin/setup-ip.mjs +474 -474
  6. package/cordis.patch.yml +50 -50
  7. package/docs/agent-team-app-integration.md +945 -0
  8. package/docs/blog-mobile-gateway.md +542 -542
  9. package/docs/dsh-0.1.5-rc.2-compatibility-audit.md +255 -255
  10. package/docs/dsh-0.1.6-alpha.1-compatibility-audit.md +122 -122
  11. package/docs/dsh-0.1.7-rc.1-compatibility-audit.md +52 -52
  12. package/docs/dsh-0.2.0-rc.2-plugin-management-mobile-integration.md +109 -109
  13. package/docs/dsh-rc2-mobile-integration.md +155 -155
  14. package/docs/file-upload-app-integration.md +250 -0
  15. package/docs/multi-gateway-app-integration.md +141 -125
  16. package/docs/multi-gateway-phase1-acceptance.md +179 -179
  17. package/docs/multi-gateway-todo.md +137 -137
  18. package/docs/remote-gateway-refactor-plan.md +52 -52
  19. package/docs/runtime-architecture.architecture.json +264 -264
  20. package/docs/runtime-architecture.html +15001 -15001
  21. package/docs/runtime-architecture.visual-check.html +32 -32
  22. package/docs/runtime-architecture.visual-check.json +548 -548
  23. package/docs/session-agent-preset-app-integration.md +93 -93
  24. package/docs/turn-outline-app-integration.md +196 -0
  25. package/docs/typert-remote-gateway-feature-checklist.md +294 -294
  26. package/docs/voice-input-implementation.md +219 -219
  27. package/docs/workspace-changes-mobile-integration.md +32 -32
  28. package/helper/dsh_mobile_gateway_helper.py +227 -227
  29. package/lib/account-bridge.mjs +57 -57
  30. package/lib/client.js +1409 -1361
  31. package/lib/cloudflare-tunnel.mjs +314 -314
  32. package/lib/cloudflared-binary.mjs +135 -135
  33. package/lib/context-bridge.mjs +3338 -1715
  34. package/lib/cost-meter-bridge.mjs +794 -794
  35. package/lib/cost-meter-dashboard-bridge.mjs +1655 -1449
  36. package/lib/devices.js +273 -245
  37. package/lib/dsh-host-adapter.mjs +508 -426
  38. package/lib/file-upload.mjs +205 -0
  39. package/lib/gateway-probe.mjs +6 -6
  40. package/lib/gateway-state.mjs +57 -57
  41. package/lib/gateway-tools.mjs +36 -36
  42. package/lib/history-cache.mjs +66 -0
  43. package/lib/index.mjs +4390 -3803
  44. package/lib/plugin-management.mjs +251 -233
  45. package/lib/public-endpoint-check.mjs +61 -61
  46. package/lib/relay-connector.mjs +213 -181
  47. package/lib/relay-state.mjs +83 -76
  48. package/lib/session-follower.mjs +140 -136
  49. package/lib/session-resume-bridge.mjs +86 -0
  50. package/lib/session-target.mjs +29 -0
  51. package/lib/speech-input.mjs +192 -192
  52. package/lib/vendor/cost-meter/aliyun-balance.mjs +123 -123
  53. package/lib/vendor/cost-meter/coding-plans.mjs +1668 -1668
  54. package/lib/vendor/cost-meter/credentials-shim.mjs +8 -8
  55. package/lib/vendor/cost-meter/custom-balance.mjs +281 -281
  56. package/lib/vendor/cost-meter/factory-quota.mjs +69 -69
  57. package/lib/vendor/cost-meter/gateway-quota-adapters.mjs +1006 -1006
  58. package/lib/vendor/cost-meter/gateway-quotas.mjs +664 -664
  59. package/lib/vendor/cost-meter/home-paths-shim.mjs +13 -13
  60. package/lib/vendor/cost-meter/net.mjs +183 -183
  61. package/lib/vendor/cost-meter/opendesign.mjs +204 -204
  62. package/lib/vendor/cost-meter/tokenrhythm-balance.mjs +95 -95
  63. package/lib/vendor/cost-meter/workbuddy-accounts.mjs +44 -44
  64. package/lib/vendor/cost-meter/workbuddy-transport.mjs +38 -38
  65. package/lib/vendor/cost-meter/workbuddy.mjs +356 -356
  66. package/lib/web-restart.mjs +47 -47
  67. package/lib/wire-json.mjs +7 -7
  68. package/lib/workspace-changes-bridge.mjs +32 -32
  69. package/package.json +54 -54
  70. package/relay/agent-connector.js +588 -569
  71. package/relay/vendor/tweetnacl.js +2391 -2391
@@ -1,233 +1,251 @@
1
- const OFFICIAL_CONFIGURATION_ITEMS = Object.freeze([
2
- { id: 'shell', namespaces: ['bash-sandbox', 'pwsh-sandbox'] },
3
- { id: 'agent-loop', namespaces: ['agent-loop'] },
4
- { id: 'subagent', namespaces: ['subagent', 'subagent-model-selection-settings'] },
5
- { id: 'web-search', namespaces: ['web-search-deepseek'] },
6
- ])
7
-
8
- export const PLUGIN_REQUEST_TYPES = new Set([
9
- 'plugin-catalog',
10
- 'plugin-market-indexes',
11
- 'plugin-market-set-indexes',
12
- 'plugin-market-search',
13
- 'plugin-registries',
14
- 'plugin-inspect',
15
- 'plugin-install',
16
- 'plugin-install-status',
17
- 'plugin-install-cancel',
18
- 'plugin-bundle-set-enabled',
19
- 'plugin-entry-set-enabled',
20
- 'plugin-remove',
21
- 'plugin-credential-describe',
22
- 'plugin-credential-set',
23
- 'plugin-translation-limits',
24
- 'plugin-settings',
25
- 'plugin-settings-mutate',
26
- 'plugin-version-exemptions',
27
- 'plugin-version-exemption-set',
28
- ])
29
-
30
- const FLAT_RESPONSE_TYPES = new Set([
31
- 'plugin-catalog', 'plugin-registries', 'plugin-settings',
32
- 'plugin-settings-mutate', 'plugin-version-exemptions',
33
- ])
34
-
35
- function record(value) {
36
- return value !== null && typeof value === 'object' && !Array.isArray(value)
37
- }
38
-
39
- function nonempty(value, maxLength = 2048) {
40
- return typeof value === 'string' && value.trim().length > 0 && value.length <= maxLength
41
- }
42
-
43
- function badRequest(type, requestId, message) {
44
- return { kind: 'error', code: 'bad-request', message, requestType: type,
45
- ...(requestId === undefined ? {} : { requestId }) }
46
- }
47
-
48
- function registryOption(msg, type, requestId) {
49
- if (msg.registry === undefined) return { value: undefined }
50
- if (msg.registry === null || (nonempty(msg.registry) && /^https?:\/\//i.test(msg.registry))) {
51
- return { value: msg.registry }
52
- }
53
- return { error: badRequest(type, requestId, 'registry must be null or an HTTP(S) URL') }
54
- }
55
-
56
- function requireName(msg, field, type, requestId) {
57
- if (!nonempty(msg[field], 512)) return { error: badRequest(type, requestId, `${field} must be a non-empty string`) }
58
- return { value: msg[field].trim() }
59
- }
60
-
61
- async function pluginCatalog(api) {
62
- const inventory = await api.plugins.inventory()
63
- if (!record(inventory) || !Array.isArray(inventory.entries)) throw new Error('pluginInventory/list returned an invalid inventory')
64
- if (inventory.managementAvailable !== true) {
65
- return { managementAvailable: false, bundles: [], plugins: [], officialItems: [], settingsWritable: false }
66
- }
67
- const [bundles, plugins, settings] = await Promise.all([
68
- api.plugins.listBundles(),
69
- api.plugins.listPlugins(),
70
- api.settings.describe(),
71
- ])
72
- if (!Array.isArray(bundles) || !Array.isArray(plugins) || !Array.isArray(settings?.namespaces)) {
73
- throw new Error('plugin manager returned an invalid catalog')
74
- }
75
- const available = new Set(settings.namespaces.map(item => item.ns))
76
- const officialItems = OFFICIAL_CONFIGURATION_ITEMS
77
- .filter(item => item.namespaces.some(ns => available.has(ns)))
78
- .map(item => ({ id: item.id, namespaces: item.namespaces.filter(ns => available.has(ns)) }))
79
- return {
80
- managementAvailable: true,
81
- bundles: bundles.map(bundle => ({
82
- ...bundle,
83
- experimental: bundle.name.startsWith('@deepseek-ai/dsh-experimental-'),
84
- })),
85
- plugins,
86
- officialItems,
87
- settingsWritable: settings.writable === true,
88
- }
89
- }
90
-
91
- /**
92
- * The mobile wire owns its request names and correlation ids. The Host adapter
93
- * owns Typert endpoint names and argument descriptors.
94
- */
95
- export async function handlePluginQuery(api, msg, { onInstallStart, onInstallEnd } = {}) {
96
- const type = msg?.type
97
- if (!PLUGIN_REQUEST_TYPES.has(type)) return null
98
- const requestId = nonempty(msg.requestId, 128) ? msg.requestId.trim() : undefined
99
- if (!requestId) return badRequest(type, undefined, `${type} requires a requestId`)
100
-
101
- try {
102
- let value
103
- let kind = type
104
- if (type === 'plugin-market-indexes') {
105
- value = await api.pluginMarket.listIndexes()
106
- } else if (type === 'plugin-market-set-indexes') {
107
- if (!Array.isArray(msg.urls) || msg.urls.length > 100 || msg.urls.some(url => !nonempty(url, 2048))) return badRequest(type, requestId, 'urls must be an array of index URLs')
108
- // Host validates and normalizes new URLs and preserves existing invalid sources for removal.
109
- await api.pluginMarket.setIndexes(msg.urls)
110
- value = { saved: true }
111
- } else if (type === 'plugin-market-search') {
112
- if (!nonempty(msg.url, 2048) || !record(msg.query)) return badRequest(type, requestId, 'url and query are required')
113
- const query = msg.query
114
- if (!Number.isSafeInteger(query.offset) || query.offset < 0 || !Number.isInteger(query.limit) || query.limit < 1 || query.limit > 100 || (query.text !== undefined && (typeof query.text !== 'string' || query.text.length > 2048))) return badRequest(type, requestId, 'invalid market query')
115
- if (!record(query.sort) || query.sort.kind !== 'index') return badRequest(type, requestId, 'unsupported sort')
116
- if (query.fetchedAt !== undefined && (typeof query.fetchedAt !== 'number' || !Number.isFinite(query.fetchedAt))) return badRequest(type, requestId, 'invalid fetchedAt')
117
- if (query.category !== undefined && !nonempty(query.category, 2048)) return badRequest(type, requestId, 'invalid category')
118
- value = await api.pluginMarket.search(msg.url, { offset: query.offset, limit: query.limit, sort: { kind: 'index' }, ...(query.category === undefined ? {} : { category: query.category }), ...(query.text ? { text: query.text } : {}), ...(query.fetchedAt === undefined ? {} : { fetchedAt: query.fetchedAt }) })
119
- } else if (type === 'plugin-catalog') {
120
- value = await pluginCatalog(api)
121
- } else if (type === 'plugin-registries') {
122
- value = await api.plugins.registries()
123
- } else if (type === 'plugin-inspect' || type === 'plugin-install') {
124
- const spec = requireName(msg, 'spec', type, requestId)
125
- if (spec.error) return spec.error
126
- const registry = registryOption(msg, type, requestId)
127
- if (registry.error) return registry.error
128
- if (type === 'plugin-inspect') {
129
- value = await api.plugins.inspect(spec.value, registry.value === undefined ? undefined : { registry: registry.value })
130
- kind = 'plugin-inspection'
131
- } else {
132
- if (msg.enabled !== undefined && typeof msg.enabled !== 'boolean') {
133
- return badRequest(type, requestId, 'enabled must be a boolean')
134
- }
135
- if (msg.approvedBuilds !== undefined && (!Array.isArray(msg.approvedBuilds)
136
- || msg.approvedBuilds.length > 100 || msg.approvedBuilds.some(name => !nonempty(name, 512)))) {
137
- return badRequest(type, requestId, 'approvedBuilds must be an array of package names')
138
- }
139
- const options = {
140
- requestId,
141
- ...(msg.enabled === undefined ? {} : { enabled: msg.enabled }),
142
- ...(registry.value === undefined ? {} : { registry: registry.value }),
143
- ...(msg.approvedBuilds === undefined ? {} : { approvedBuilds: msg.approvedBuilds }),
144
- }
145
- onInstallStart?.(requestId)
146
- try {
147
- value = await api.plugins.installBundle(spec.value, options)
148
- } finally {
149
- onInstallEnd?.(requestId)
150
- }
151
- kind = 'plugin-install-result'
152
- }
153
- } else if (type === 'plugin-install-status' || type === 'plugin-install-cancel') {
154
- const installId = requireName(msg, 'installId', type, requestId)
155
- if (installId.error) return installId.error
156
- value = type === 'plugin-install-status'
157
- ? await api.plugins.waitForInstall(installId.value)
158
- : await api.plugins.cancelInstall(installId.value)
159
- } else if (type === 'plugin-bundle-set-enabled' || type === 'plugin-entry-set-enabled') {
160
- const field = type === 'plugin-bundle-set-enabled' ? 'name' : 'entryId'
161
- const target = requireName(msg, field, type, requestId)
162
- if (target.error) return target.error
163
- if (typeof msg.enabled !== 'boolean') return badRequest(type, requestId, 'enabled must be a boolean')
164
- value = type === 'plugin-bundle-set-enabled'
165
- ? await api.plugins.setBundleEnabled(target.value, msg.enabled)
166
- : await api.plugins.setPluginEnabled(target.value, msg.enabled)
167
- } else if (type === 'plugin-remove') {
168
- const name = requireName(msg, 'name', type, requestId)
169
- if (name.error) return name.error
170
- value = await api.plugins.removeBundle(name.value)
171
- } else if (type === 'plugin-translation-limits') {
172
- value = await api.pluginConfiguration.translationLimits()
173
- } else if (type === 'plugin-credential-describe' || type === 'plugin-credential-set') {
174
- if (typeof msg.ref !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]{0,255}$/.test(msg.ref)) return badRequest(type, requestId, 'invalid credential reference')
175
- const settings = await api.settings.describe()
176
- const scopes = (settings.namespaces || []).filter(item => ['web-search-deepseek', 'web-deepseek-api'].includes(item.ns))
177
- if (!scopes.some(item => (item.value?.apiKeyEnv || 'DEEPSEEK_API_KEY') === msg.ref)) return badRequest(type, requestId, 'reference is not used by web search')
178
- if (type === 'plugin-credential-set') {
179
- if (settings.writable !== true) return badRequest(type, requestId, 'settings are read-only')
180
- if (typeof msg.value !== 'string' || !msg.value.trim() || msg.value.length > 8192) return badRequest(type, requestId, 'invalid credential value')
181
- try { await api.pluginConfiguration.credentialSet(msg.ref, msg.value) }
182
- catch { return { kind: 'error', code: 'plugin-credentials/write-failed', message: 'Credential write failed', requestId, requestType: type } }
183
- value = { saved: true }
184
- } else {
185
- const descriptions = await api.pluginConfiguration.credentialDescribe(msg.ref)
186
- const view = descriptions[msg.ref]
187
- value = { configured: view?.configured === true, writable: settings.writable === true && view?.writable === true }
188
- }
189
- } else if (type === 'plugin-settings') {
190
- const described = await api.settings.describe()
191
- if (!record(described) || !Array.isArray(described.namespaces)) throw new Error('settings/describe returned an invalid response')
192
- if (msg.ns !== undefined) {
193
- const ns = requireName(msg, 'ns', type, requestId)
194
- if (ns.error) return ns.error
195
- const namespace = described.namespaces.find(item => item.ns === ns.value)
196
- if (!namespace) return { kind: 'error', code: 'plugin-settings/not-found',
197
- message: `settings namespace is unavailable: ${ns.value}`, requestType: type, requestId }
198
- value = { writable: described.writable === true, namespace }
199
- } else {
200
- value = described
201
- }
202
- } else if (type === 'plugin-settings-mutate') {
203
- const ns = requireName(msg, 'ns', type, requestId)
204
- if (ns.error) return ns.error
205
- if (!Number.isSafeInteger(msg.expectedRevision) || msg.expectedRevision < 0) {
206
- return badRequest(type, requestId, 'expectedRevision must be a non-negative integer')
207
- }
208
- if (!Array.isArray(msg.ops) || msg.ops.length < 1 || msg.ops.length > 100
209
- || msg.ops.some(op => !record(op) || !['set', 'unset'].includes(op.op)
210
- || !Array.isArray(op.path) || op.path.some(part => !nonempty(part, 256))
211
- || (op.op === 'set' && !Object.hasOwn(op, 'value')))) {
212
- return badRequest(type, requestId, 'ops must contain valid set or unset path edits')
213
- }
214
- value = await api.settings.mutate({ ns: ns.value, ops: msg.ops, expectedRevision: msg.expectedRevision })
215
- } else if (type === 'plugin-version-exemptions') {
216
- value = await api.plugins.listVersionExemptions()
217
- } else if (type === 'plugin-version-exemption-set') {
218
- const packageVersion = requireName(msg, 'packageVersion', type, requestId)
219
- const runtimeVersion = requireName(msg, 'runtimeVersion', type, requestId)
220
- if (packageVersion.error || runtimeVersion.error) return packageVersion.error || runtimeVersion.error
221
- if (typeof msg.enabled !== 'boolean' || (msg.acceptRisk !== undefined && typeof msg.acceptRisk !== 'boolean')) {
222
- return badRequest(type, requestId, 'enabled and acceptRisk must be booleans')
223
- }
224
- value = await api.plugins.setVersionExemption(packageVersion.value, runtimeVersion.value,
225
- msg.enabled, msg.acceptRisk)
226
- }
227
- return { kind, requestId, ...(FLAT_RESPONSE_TYPES.has(type) && record(value)
228
- ? value : { result: value }) }
229
- } catch (error) {
230
- return { kind: 'error', code: error?.code || 'internal',
231
- message: error?.message || String(error), requestType: type, requestId }
232
- }
233
- }
1
+ // 官方配置项图标:与 DSH 客户端注册表一致(图标资源取自 DSH 客户端,MIT)。
2
+ // 同步给移动端,避免客户端各自内置一份导致版本漂移。
3
+ const OFFICIAL_ITEM_ICONS = Object.freeze({
4
+ 'subagent': '<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="14.9893" y="15.1877" width="12.2365" height="12.2365" rx="2" fill="url(#dsh_plugin_art_R0a)" fill-opacity="0.8"></rect><rect x="8.87109" y="9.0697" width="12.2365" height="12.2365" rx="2" fill="url(#dsh_plugin_art_R0b)" fill-opacity="0.8"></rect><defs><linearGradient id="dsh_plugin_art_R0a" x1="21.1075" y1="15.1877" x2="21.1075" y2="27.4243" gradientUnits="userSpaceOnUse"><stop stop-color="#45E7A4"></stop><stop offset="1" stop-color="#05909D"></stop></linearGradient><linearGradient id="dsh_plugin_art_R0b" x1="14.9894" y1="9.0697" x2="14.9894" y2="21.3063" gradientUnits="userSpaceOnUse"><stop stop-color="#69B9FF"></stop><stop offset="1" stop-color="#324DE2"></stop></linearGradient></defs></svg>',
5
+ 'web-search': '<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" fill="none" viewBox="0 0 36 36"><path stroke="#658EFF" stroke-width="3" d="m26.536 26.987-4.155-4.155"/><circle cx="15.766" cy="16.216" r="6.206" stroke="url(#a)" stroke-width="4"/><defs><linearGradient id="a" x1="7.561" x2="23.972" y1="8.01" y2="24.422" gradientUnits="userSpaceOnUse"><stop stop-color="#41E1AC"/><stop offset=".65" stop-color="#658EFF"/><stop offset="1" stop-color="#41E1AC"/></linearGradient></defs></svg>',
6
+ 'shell': '<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" fill="none" viewBox="0 0 36 36"><path stroke="#679EFE" stroke-width="3.5" d="m10 11 6.606 6.606a.2.2 0 0 1 0 .283L10 24.495m10.121 0h6.748"/></svg>',
7
+ 'agent-loop': '<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" fill="none" viewBox="0 0 36 36"><path fill="#A797FC" d="M18.049 28.49c-5.963 0-10.797-4.834-10.797-10.797 5.963 0 10.797 4.834 10.797 10.797"/><path fill="url(#a)" d="M18.049 6.897c-5.963 0-10.797 4.833-10.797 10.796 5.963 0 10.797-4.834 10.797-10.796"/><path fill="#4561EE" d="M18.049 28.49c5.962 0 10.796-4.834 10.796-10.797-5.963 0-10.796 4.834-10.796 10.797"/><path fill="#658EFF" d="M18.049 6.897c5.962 0 10.796 4.833 10.796 10.796-5.963 0-10.796-4.834-10.796-10.796"/><defs><linearGradient id="a" x1="16.791" x2="8.982" y1="16.166" y2="8.743" gradientUnits="userSpaceOnUse"><stop stop-color="#A23AE7"/><stop offset="1" stop-color="#E2E2E2"/></linearGradient></defs></svg>',
8
+ })
9
+
10
+ // 插件自身未提供图标时,DSH 客户端使用的默认图标(同样随目录下发)。
11
+ const DEFAULT_PLUGIN_ICON = '<svg xmlns="http://www.w3.org/2000/svg" width="70" height="66" viewBox="0 0 70 66" fill="none">\n <defs>\n <linearGradient id="dshPlugArt" x1="62" y1="4" x2="12" y2="62" gradientUnits="userSpaceOnUse">\n <stop stop-color="#8FE0EA"/><stop offset=".45" stop-color="#6FA8FF"/><stop offset="1" stop-color="#5C8CFF"/>\n </linearGradient>\n </defs>\n <path d="M51.00 2.00 Q61.50 2.00 64.50 5.00 Q67.50 8.00 68.00 12.25 Q68.50 16.50 67.50 19.50 Q66.50 22.50 64.50 24.00 Q62.50 25.50 57.25 26.00 Q52.00 26.50 49.75 28.25 Q47.50 30.00 48.25 33.00 Q49.00 36.00 55.75 37.00 Q62.50 38.00 64.50 39.50 Q66.50 41.00 67.50 44.00 Q68.50 47.00 68.00 52.75 Q67.50 58.50 64.50 61.00 Q61.50 63.50 54.25 63.50 Q47.00 63.50 44.50 60.50 Q42.00 57.50 42.00 52.75 Q42.00 48.00 40.75 46.25 Q39.50 44.50 34.25 45.00 Q29.00 45.50 27.75 47.25 Q26.50 49.00 26.50 52.25 Q26.50 55.50 25.50 58.00 Q24.50 60.50 22.00 62.00 Q19.50 63.50 12.75 63.50 Q6.00 63.50 6.50 62.75 Q7.00 62.00 6.00 61.75 Q5.00 61.50 3.50 58.50 Q2.00 55.50 2.00 50.75 Q2.00 46.00 3.00 44.50 Q4.00 43.00 7.50 41.00 Q11.00 39.00 14.75 39.00 Q18.50 39.00 20.75 37.25 Q23.00 35.50 23.50 28.75 Q24.00 22.00 26.00 21.00 Q28.00 20.00 35.25 19.50 Q42.50 19.00 44.25 12.00 Q46.00 5.00 48.50 3.75 L51.00 2.50 Z" fill="url(#dshPlugArt)"/>\n <circle cx="11" cy="10.5" r="9" fill="#5CD0F1"/>\n</svg>'
12
+
13
+ const OFFICIAL_CONFIGURATION_ITEMS = Object.freeze([
14
+ { id: 'shell', namespaces: ['bash-sandbox', 'pwsh-sandbox'] },
15
+ { id: 'agent-loop', namespaces: ['agent-loop'] },
16
+ { id: 'subagent', namespaces: ['subagent', 'subagent-model-selection-settings'] },
17
+ { id: 'web-search', namespaces: ['web-search-deepseek'] },
18
+ ])
19
+
20
+ export const PLUGIN_REQUEST_TYPES = new Set([
21
+ 'plugin-catalog',
22
+ 'plugin-market-indexes',
23
+ 'plugin-market-set-indexes',
24
+ 'plugin-market-search',
25
+ 'plugin-registries',
26
+ 'plugin-inspect',
27
+ 'plugin-install',
28
+ 'plugin-install-status',
29
+ 'plugin-install-cancel',
30
+ 'plugin-bundle-set-enabled',
31
+ 'plugin-entry-set-enabled',
32
+ 'plugin-remove',
33
+ 'plugin-credential-describe',
34
+ 'plugin-credential-set',
35
+ 'plugin-translation-limits',
36
+ 'plugin-settings',
37
+ 'plugin-settings-mutate',
38
+ 'plugin-version-exemptions',
39
+ 'plugin-version-exemption-set',
40
+ ])
41
+
42
+ const FLAT_RESPONSE_TYPES = new Set([
43
+ 'plugin-catalog', 'plugin-registries', 'plugin-settings',
44
+ 'plugin-settings-mutate', 'plugin-version-exemptions',
45
+ ])
46
+
47
+ function record(value) {
48
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
49
+ }
50
+
51
+ function nonempty(value, maxLength = 2048) {
52
+ return typeof value === 'string' && value.trim().length > 0 && value.length <= maxLength
53
+ }
54
+
55
+ function badRequest(type, requestId, message) {
56
+ return { kind: 'error', code: 'bad-request', message, requestType: type,
57
+ ...(requestId === undefined ? {} : { requestId }) }
58
+ }
59
+
60
+ function registryOption(msg, type, requestId) {
61
+ if (msg.registry === undefined) return { value: undefined }
62
+ if (msg.registry === null || (nonempty(msg.registry) && /^https?:\/\//i.test(msg.registry))) {
63
+ return { value: msg.registry }
64
+ }
65
+ return { error: badRequest(type, requestId, 'registry must be null or an HTTP(S) URL') }
66
+ }
67
+
68
+ function requireName(msg, field, type, requestId) {
69
+ if (!nonempty(msg[field], 512)) return { error: badRequest(type, requestId, `${field} must be a non-empty string`) }
70
+ return { value: msg[field].trim() }
71
+ }
72
+
73
+ async function pluginCatalog(api) {
74
+ const inventory = await api.plugins.inventory()
75
+ if (!record(inventory) || !Array.isArray(inventory.entries)) throw new Error('pluginInventory/list returned an invalid inventory')
76
+ if (inventory.managementAvailable !== true) {
77
+ return { managementAvailable: false, bundles: [], plugins: [], officialItems: [], settingsWritable: false }
78
+ }
79
+ const [bundles, plugins, settings] = await Promise.all([
80
+ api.plugins.listBundles(),
81
+ api.plugins.listPlugins(),
82
+ api.settings.describe(),
83
+ ])
84
+ if (!Array.isArray(bundles) || !Array.isArray(plugins) || !Array.isArray(settings?.namespaces)) {
85
+ throw new Error('plugin manager returned an invalid catalog')
86
+ }
87
+ const available = new Set(settings.namespaces.map(item => item.ns))
88
+ const officialItems = OFFICIAL_CONFIGURATION_ITEMS
89
+ .filter(item => item.namespaces.some(ns => available.has(ns)))
90
+ .map(item => ({
91
+ id: item.id,
92
+ namespaces: item.namespaces.filter(ns => available.has(ns)),
93
+ icon: OFFICIAL_ITEM_ICONS[item.id] || null,
94
+ }))
95
+ return {
96
+ managementAvailable: true,
97
+ bundles: bundles.map(bundle => ({
98
+ ...bundle,
99
+ experimental: bundle.name.startsWith('@deepseek-ai/dsh-experimental-'),
100
+ })),
101
+ plugins,
102
+ officialItems,
103
+ // 插件自身无图标时客户端用它(与 DSH 客户端默认图标一致)
104
+ defaultPluginIcon: DEFAULT_PLUGIN_ICON,
105
+ settingsWritable: settings.writable === true,
106
+ }
107
+ }
108
+
109
+ /**
110
+ * The mobile wire owns its request names and correlation ids. The Host adapter
111
+ * owns Typert endpoint names and argument descriptors.
112
+ */
113
+ export async function handlePluginQuery(api, msg, { onInstallStart, onInstallEnd } = {}) {
114
+ const type = msg?.type
115
+ if (!PLUGIN_REQUEST_TYPES.has(type)) return null
116
+ const requestId = nonempty(msg.requestId, 128) ? msg.requestId.trim() : undefined
117
+ if (!requestId) return badRequest(type, undefined, `${type} requires a requestId`)
118
+
119
+ try {
120
+ let value
121
+ let kind = type
122
+ if (type === 'plugin-market-indexes') {
123
+ value = await api.pluginMarket.listIndexes()
124
+ } else if (type === 'plugin-market-set-indexes') {
125
+ if (!Array.isArray(msg.urls) || msg.urls.length > 100 || msg.urls.some(url => !nonempty(url, 2048))) return badRequest(type, requestId, 'urls must be an array of index URLs')
126
+ // Host validates and normalizes new URLs and preserves existing invalid sources for removal.
127
+ await api.pluginMarket.setIndexes(msg.urls)
128
+ value = { saved: true }
129
+ } else if (type === 'plugin-market-search') {
130
+ if (!nonempty(msg.url, 2048) || !record(msg.query)) return badRequest(type, requestId, 'url and query are required')
131
+ const query = msg.query
132
+ if (!Number.isSafeInteger(query.offset) || query.offset < 0 || !Number.isInteger(query.limit) || query.limit < 1 || query.limit > 100 || (query.text !== undefined && (typeof query.text !== 'string' || query.text.length > 2048))) return badRequest(type, requestId, 'invalid market query')
133
+ if (!record(query.sort) || query.sort.kind !== 'index') return badRequest(type, requestId, 'unsupported sort')
134
+ if (query.fetchedAt !== undefined && (typeof query.fetchedAt !== 'number' || !Number.isFinite(query.fetchedAt))) return badRequest(type, requestId, 'invalid fetchedAt')
135
+ if (query.category !== undefined && !nonempty(query.category, 2048)) return badRequest(type, requestId, 'invalid category')
136
+ value = await api.pluginMarket.search(msg.url, { offset: query.offset, limit: query.limit, sort: { kind: 'index' }, ...(query.category === undefined ? {} : { category: query.category }), ...(query.text ? { text: query.text } : {}), ...(query.fetchedAt === undefined ? {} : { fetchedAt: query.fetchedAt }) })
137
+ } else if (type === 'plugin-catalog') {
138
+ value = await pluginCatalog(api)
139
+ } else if (type === 'plugin-registries') {
140
+ value = await api.plugins.registries()
141
+ } else if (type === 'plugin-inspect' || type === 'plugin-install') {
142
+ const spec = requireName(msg, 'spec', type, requestId)
143
+ if (spec.error) return spec.error
144
+ const registry = registryOption(msg, type, requestId)
145
+ if (registry.error) return registry.error
146
+ if (type === 'plugin-inspect') {
147
+ value = await api.plugins.inspect(spec.value, registry.value === undefined ? undefined : { registry: registry.value })
148
+ kind = 'plugin-inspection'
149
+ } else {
150
+ if (msg.enabled !== undefined && typeof msg.enabled !== 'boolean') {
151
+ return badRequest(type, requestId, 'enabled must be a boolean')
152
+ }
153
+ if (msg.approvedBuilds !== undefined && (!Array.isArray(msg.approvedBuilds)
154
+ || msg.approvedBuilds.length > 100 || msg.approvedBuilds.some(name => !nonempty(name, 512)))) {
155
+ return badRequest(type, requestId, 'approvedBuilds must be an array of package names')
156
+ }
157
+ const options = {
158
+ requestId,
159
+ ...(msg.enabled === undefined ? {} : { enabled: msg.enabled }),
160
+ ...(registry.value === undefined ? {} : { registry: registry.value }),
161
+ ...(msg.approvedBuilds === undefined ? {} : { approvedBuilds: msg.approvedBuilds }),
162
+ }
163
+ onInstallStart?.(requestId)
164
+ try {
165
+ value = await api.plugins.installBundle(spec.value, options)
166
+ } finally {
167
+ onInstallEnd?.(requestId)
168
+ }
169
+ kind = 'plugin-install-result'
170
+ }
171
+ } else if (type === 'plugin-install-status' || type === 'plugin-install-cancel') {
172
+ const installId = requireName(msg, 'installId', type, requestId)
173
+ if (installId.error) return installId.error
174
+ value = type === 'plugin-install-status'
175
+ ? await api.plugins.waitForInstall(installId.value)
176
+ : await api.plugins.cancelInstall(installId.value)
177
+ } else if (type === 'plugin-bundle-set-enabled' || type === 'plugin-entry-set-enabled') {
178
+ const field = type === 'plugin-bundle-set-enabled' ? 'name' : 'entryId'
179
+ const target = requireName(msg, field, type, requestId)
180
+ if (target.error) return target.error
181
+ if (typeof msg.enabled !== 'boolean') return badRequest(type, requestId, 'enabled must be a boolean')
182
+ value = type === 'plugin-bundle-set-enabled'
183
+ ? await api.plugins.setBundleEnabled(target.value, msg.enabled)
184
+ : await api.plugins.setPluginEnabled(target.value, msg.enabled)
185
+ } else if (type === 'plugin-remove') {
186
+ const name = requireName(msg, 'name', type, requestId)
187
+ if (name.error) return name.error
188
+ value = await api.plugins.removeBundle(name.value)
189
+ } else if (type === 'plugin-translation-limits') {
190
+ value = await api.pluginConfiguration.translationLimits()
191
+ } else if (type === 'plugin-credential-describe' || type === 'plugin-credential-set') {
192
+ if (typeof msg.ref !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]{0,255}$/.test(msg.ref)) return badRequest(type, requestId, 'invalid credential reference')
193
+ const settings = await api.settings.describe()
194
+ const scopes = (settings.namespaces || []).filter(item => ['web-search-deepseek', 'web-deepseek-api'].includes(item.ns))
195
+ if (!scopes.some(item => (item.value?.apiKeyEnv || 'DEEPSEEK_API_KEY') === msg.ref)) return badRequest(type, requestId, 'reference is not used by web search')
196
+ if (type === 'plugin-credential-set') {
197
+ if (settings.writable !== true) return badRequest(type, requestId, 'settings are read-only')
198
+ if (typeof msg.value !== 'string' || !msg.value.trim() || msg.value.length > 8192) return badRequest(type, requestId, 'invalid credential value')
199
+ try { await api.pluginConfiguration.credentialSet(msg.ref, msg.value) }
200
+ catch { return { kind: 'error', code: 'plugin-credentials/write-failed', message: 'Credential write failed', requestId, requestType: type } }
201
+ value = { saved: true }
202
+ } else {
203
+ const descriptions = await api.pluginConfiguration.credentialDescribe(msg.ref)
204
+ const view = descriptions[msg.ref]
205
+ value = { configured: view?.configured === true, writable: settings.writable === true && view?.writable === true }
206
+ }
207
+ } else if (type === 'plugin-settings') {
208
+ const described = await api.settings.describe()
209
+ if (!record(described) || !Array.isArray(described.namespaces)) throw new Error('settings/describe returned an invalid response')
210
+ if (msg.ns !== undefined) {
211
+ const ns = requireName(msg, 'ns', type, requestId)
212
+ if (ns.error) return ns.error
213
+ const namespace = described.namespaces.find(item => item.ns === ns.value)
214
+ if (!namespace) return { kind: 'error', code: 'plugin-settings/not-found',
215
+ message: `settings namespace is unavailable: ${ns.value}`, requestType: type, requestId }
216
+ value = { writable: described.writable === true, namespace }
217
+ } else {
218
+ value = described
219
+ }
220
+ } else if (type === 'plugin-settings-mutate') {
221
+ const ns = requireName(msg, 'ns', type, requestId)
222
+ if (ns.error) return ns.error
223
+ if (!Number.isSafeInteger(msg.expectedRevision) || msg.expectedRevision < 0) {
224
+ return badRequest(type, requestId, 'expectedRevision must be a non-negative integer')
225
+ }
226
+ if (!Array.isArray(msg.ops) || msg.ops.length < 1 || msg.ops.length > 100
227
+ || msg.ops.some(op => !record(op) || !['set', 'unset'].includes(op.op)
228
+ || !Array.isArray(op.path) || op.path.some(part => !nonempty(part, 256))
229
+ || (op.op === 'set' && !Object.hasOwn(op, 'value')))) {
230
+ return badRequest(type, requestId, 'ops must contain valid set or unset path edits')
231
+ }
232
+ value = await api.settings.mutate({ ns: ns.value, ops: msg.ops, expectedRevision: msg.expectedRevision })
233
+ } else if (type === 'plugin-version-exemptions') {
234
+ value = await api.plugins.listVersionExemptions()
235
+ } else if (type === 'plugin-version-exemption-set') {
236
+ const packageVersion = requireName(msg, 'packageVersion', type, requestId)
237
+ const runtimeVersion = requireName(msg, 'runtimeVersion', type, requestId)
238
+ if (packageVersion.error || runtimeVersion.error) return packageVersion.error || runtimeVersion.error
239
+ if (typeof msg.enabled !== 'boolean' || (msg.acceptRisk !== undefined && typeof msg.acceptRisk !== 'boolean')) {
240
+ return badRequest(type, requestId, 'enabled and acceptRisk must be booleans')
241
+ }
242
+ value = await api.plugins.setVersionExemption(packageVersion.value, runtimeVersion.value,
243
+ msg.enabled, msg.acceptRisk)
244
+ }
245
+ return { kind, requestId, ...(FLAT_RESPONSE_TYPES.has(type) && record(value)
246
+ ? value : { result: value }) }
247
+ } catch (error) {
248
+ return { kind: 'error', code: error?.code || 'internal',
249
+ message: error?.message || String(error), requestType: type, requestId }
250
+ }
251
+ }