dsh-plugin-weapp-gateway 0.8.13 → 0.8.14

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/lib/index.mjs CHANGED
@@ -2821,6 +2821,7 @@ const plugin = {
2821
2821
  api,
2822
2822
  currentVersion: PLUGIN_VERSION,
2823
2823
  requestId: crypto.randomUUID(),
2824
+ profileName: ctx.get?.('profileContext')?.name,
2824
2825
  })
2825
2826
  sendJson(res, 200, result)
2826
2827
  } finally {
@@ -1,5 +1,6 @@
1
1
  export const PLUGIN_PACKAGE_NAME = 'dsh-plugin-weapp-gateway'
2
2
  const REGISTRY_LATEST_URL = `https://registry.npmjs.org/${PLUGIN_PACKAGE_NAME}/latest`
3
+ const FAILED_INSTALL_STATES = new Set(['failed', 'cancelled', 'refused'])
3
4
 
4
5
  function errorField(value) {
5
6
  return typeof value === 'string' && value.trim() ? value.trim() : ''
@@ -22,6 +23,7 @@ export function formatPluginInstallError(error, depth = 0) {
22
23
  if (detail) return `${code && code !== detail ? `${code}: ` : ''}${detail}`.slice(0, 1000)
23
24
  }
24
25
 
26
+ if (code) return code
25
27
  try {
26
28
  const serialized = JSON.stringify(error, (key, value) => {
27
29
  if (/stack|authorization|cookie|password|secret|token|credential|api.?key/i.test(key)) return undefined
@@ -29,7 +31,28 @@ export function formatPluginInstallError(error, depth = 0) {
29
31
  })
30
32
  if (serialized && serialized !== '{}') return serialized.slice(0, 1000)
31
33
  } catch { /* use a generic message for cyclic or non-serializable errors */ }
32
- return code || 'The plugin manager did not provide an error message'
34
+ return 'The plugin manager did not provide an error message'
35
+ }
36
+
37
+ function operationErrorCode(error, depth = 0) {
38
+ if (!error || typeof error !== 'object' || depth > 4) return ''
39
+ if (typeof error.code === 'string') return error.code
40
+ for (const nested of [error.error, error.cause, error.details]) {
41
+ const code = operationErrorCode(nested, depth + 1)
42
+ if (code) return code
43
+ }
44
+ return ''
45
+ }
46
+
47
+ function failedOperation(operation) {
48
+ return FAILED_INSTALL_STATES.has(operation?.application) || Boolean(operation?.error)
49
+ }
50
+
51
+ function operationMessage(operation) {
52
+ if (operation?.error) return formatPluginInstallError(operation.error)
53
+ return typeof operation?.message === 'string' && operation.message.trim()
54
+ ? operation.message.trim()
55
+ : `Plugin manager reported ${operation?.application || 'an unknown failure'}`
33
56
  }
34
57
 
35
58
  function parseVersion(value) {
@@ -97,11 +120,14 @@ export async function checkPluginUpdate({ currentVersion, fetchImpl = globalThis
97
120
  }
98
121
  }
99
122
 
100
- export async function installLatestPlugin({ api, currentVersion, fetchImpl = globalThis.fetch, requestId }) {
123
+ export async function installLatestPlugin({ api, currentVersion, fetchImpl = globalThis.fetch, requestId, profileName }) {
101
124
  const update = await checkPluginUpdate({ currentVersion, fetchImpl })
102
125
  if (!update.updateAvailable) return { ...update, installed: false, reason: 'already-current' }
103
126
 
104
- const command = `dsh plugin --profile web add ${PLUGIN_PACKAGE_NAME}@${update.latestVersion}`
127
+ const profile = typeof profileName === 'string' && /^[A-Za-z0-9._-]{1,80}$/.test(profileName)
128
+ ? profileName
129
+ : 'web'
130
+ const command = `dsh plugin --profile ${profile} add ${PLUGIN_PACKAGE_NAME}@${update.latestVersion}`
105
131
  let inventory
106
132
  try {
107
133
  inventory = await api.plugins.inventory()
@@ -112,16 +138,83 @@ export async function installLatestPlugin({ api, currentVersion, fetchImpl = glo
112
138
  return { ...update, installed: false, reason: 'plugin-manager-unavailable', command }
113
139
  }
114
140
 
115
- const installation = await api.plugins.installBundle(
116
- `${PLUGIN_PACKAGE_NAME}@${update.latestVersion}`,
117
- { requestId },
118
- )
119
- const errorMessage = formatPluginInstallError(installation?.error)
141
+ let bundles
142
+ try {
143
+ bundles = await api.plugins.listBundles()
144
+ } catch (error) {
145
+ return { ...update, installed: false, reason: 'plugin-manager-unavailable', command, detail: error?.message }
146
+ }
147
+ if (!Array.isArray(bundles)) {
148
+ return { ...update, installed: false, reason: 'plugin-manager-unavailable', command }
149
+ }
150
+
151
+ const existing = bundles.find(bundle => bundle?.name === PLUGIN_PACKAGE_NAME && bundle.installed === true)
152
+ const installSpec = `${PLUGIN_PACKAGE_NAME}@${update.latestVersion}`
153
+ const installOptions = {
154
+ requestId,
155
+ saveExact: true,
156
+ ...(typeof existing?.enabled === 'boolean' ? { enabled: existing.enabled } : {}),
157
+ }
158
+
159
+ let removed = false
160
+ if (existing) {
161
+ if (typeof api.plugins.removeBundle !== 'function') {
162
+ return { ...update, installed: false, reason: 'plugin-manager-unavailable', command }
163
+ }
164
+
165
+ let removal
166
+ try {
167
+ removal = await api.plugins.removeBundle(PLUGIN_PACKAGE_NAME)
168
+ } catch (error) {
169
+ removal = { application: 'failed', error }
170
+ }
171
+
172
+ // Older hosts cannot hot-remove a running bundle; their package manager can still replace it for restart.
173
+ if (failedOperation(removal) && operationErrorCode(removal) !== 'stop-profile') {
174
+ return {
175
+ ...update,
176
+ installed: true,
177
+ installation: {
178
+ ...removal,
179
+ application: FAILED_INSTALL_STATES.has(removal?.application) ? removal.application : 'failed',
180
+ errorMessage: operationMessage(removal),
181
+ },
182
+ }
183
+ }
184
+ removed = !failedOperation(removal)
185
+ }
186
+
187
+ let installation
188
+ try {
189
+ installation = await api.plugins.installBundle(installSpec, installOptions)
190
+ } catch (error) {
191
+ installation = { application: 'failed', error }
192
+ }
193
+
194
+ let errorMessage = ''
195
+ if (failedOperation(installation) && removed) {
196
+ const previousSpec = typeof existing.source === 'string' && existing.source
197
+ ? existing.source
198
+ : `${PLUGIN_PACKAGE_NAME}@${currentVersion}`
199
+ try {
200
+ const recovery = await api.plugins.installBundle(previousSpec, {
201
+ ...(typeof existing.enabled === 'boolean' ? { enabled: existing.enabled } : {}),
202
+ })
203
+ errorMessage = failedOperation(recovery)
204
+ ? `Update failed: ${operationMessage(installation)}. Restoring the previous version also failed: ${operationMessage(recovery)}`
205
+ : `Update failed: ${operationMessage(installation)}. The previous version was restored.`
206
+ installation = { ...installation, recovery }
207
+ } catch (error) {
208
+ errorMessage = `Update failed: ${operationMessage(installation)}. Restoring the previous version also failed: ${formatPluginInstallError(error)}`
209
+ }
210
+ }
211
+
212
+ const detail = errorMessage || (installation?.error ? formatPluginInstallError(installation.error) : '')
120
213
  return {
121
214
  ...update,
122
215
  installed: true,
123
- installation: errorMessage && installation && typeof installation === 'object'
124
- ? { ...installation, errorMessage }
216
+ installation: detail && installation && typeof installation === 'object'
217
+ ? { ...installation, errorMessage: detail }
125
218
  : installation,
126
219
  }
127
220
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-weapp-gateway",
3
- "version": "0.8.13",
3
+ "version": "0.8.14",
4
4
  "description": "Authenticated mobile gateway for DSH with Session V4 history, live permissions, inbox-based queue sync, built-in WeChat ClawBot, plugin management, LAN and Cloudflare Tunnel access, device pairing, per-session Agent presets, relay access, and speech input",
5
5
  "main": "lib/index.mjs",
6
6
  "files": [