dsh-plugin 1.6.1 → 1.6.3

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.
@@ -3,16 +3,17 @@
3
3
  * Website: https://dsh-plugin.org
4
4
  * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
5
  *
6
- * 运行中 loader 的读取与停用:卸载成功后对匹配条目做 live-disable,
7
- * 使卸载立即生效(刷新页面不再加载已卸载的 client bundle,也无需重启)。
6
+ * 运行中 loader 的读取、停用与热挂载:卸载成功后对匹配条目做 live-disable、
7
+ * 安装成功后把新插件条目热挂进运行中的 loader,让两类操作都尽量立即生效
8
+ * (刷新页面即可,无需重启宿主)。
8
9
  */
9
10
  import { readFileSync } from 'node:fs'
10
11
  import { join } from 'node:path'
11
12
  import { profileDirectory } from './profile/profile.ts'
12
13
 
13
14
  /**
14
- * 运行中 loader 的最小接口:卸载成功后即时移除条目用。
15
- * 对应官方 `ctx.loader`(cordis-plugin-loader 的 Loader 服务)的读取面与移除面。
15
+ * 运行中 loader 的最小接口:卸载即时停用 / 安装即时热挂载用。
16
+ * 对应官方 `ctx.loader`(cordis-plugin-loader 的 Loader 服务)的读取、新建与移除面。
16
17
  * 移除方式:对匹配条目 live-disable(`entry.update({ disabled: true })`),
17
18
  * 而不是 `loader.remove(id)` —— disable 走 Entry.update 的 disabled 分支,不触发
18
19
  * tree.write()(不会把运行态条目烘焙回 cordis.yml),也不依赖嵌套子树的 id 解析。
@@ -27,6 +28,12 @@ export interface LoaderHandle {
27
28
  }>
28
29
  /** 停止并移除一个条目(官方 Loader API:resolve → EntryGroup.remove → tree.write)。 */
29
30
  remove(id: string): Promise<void>
31
+ /** 在运行中 loader 里新建一个条目并启动它。等价于 profile bundle patch 的
32
+ * `- insert: - name: <pkg>` 行;Loader.write() 为空实现,不会写盘(重启后由
33
+ * profile 的 bundles 清单重新挂载,不会重复挂载)。返回新条目的 id。 */
34
+ create(options: { id?: string; name: string }): Promise<string>
35
+ /** 按 id 解析条目:热挂载后确认 fiber 真的起来了(import 失败时条目在、但 fiber 为空)。 */
36
+ resolve(id: string): { id: string; options?: { name?: string; disabled?: boolean | null }; fiber?: unknown; subgroup?: unknown }
30
37
  }
31
38
 
32
39
  /** 运行中 loader 的全部条目(id + options.name),诊断用。 */
@@ -91,14 +98,15 @@ function nameMatches(entryName: string | undefined, target: string): boolean {
91
98
  * `loader.entries()`(含嵌套子树),对每个匹配条目
92
99
  * `entry.update({ disabled: true })` 做 live-disable —— fiber 被 dispose,
93
100
  * client-modules 对账后不再把它写进 `__DSH_BOOT__`,页面刷新即恢复。
94
- * 返回 true = 无需重启(仅当宿主从未加载过它:磁盘已干净);
95
- * 其余一律 false = 需要重启:要么 disable 失败需「待重启清理」兜底,
96
- * 要么 disable 成功但该插件曾在 loader 中存活——带 UI 的插件(侧边栏面板等
97
- * 宿主启动时渲染的槽位)disable 后不会主动摘除,不重启面板一直残留。
101
+ * 返回 `{ ok, live }`:
102
+ * - `{ ok: true, live: false }`:宿主从未加载过它(磁盘已干净),无需刷新/重启;
103
+ * - `{ ok: true, live: true }`:曾在 loader 中存活且已 live-disable —— 服务端即时卸载;
104
+ * 带客户端 UI 的插件面板要页面刷新才摘除(由调用方按 `hadClientUi` 决定 needsReload);
105
+ * - `{ ok: false, live: true }`:disable 失败 / 超时,需宿主重启兜底清理。
98
106
  * 宿主关键包(@deepseek-ai/* 与本插件自身)一律跳过。
99
107
  */
100
- export async function removeLoadedEntry(loader: LoaderHandle, name: string): Promise<boolean> {
101
- if (name === 'dsh-plugin' || name.startsWith('@deepseek-ai/')) return false
108
+ export async function removeLoadedEntry(loader: LoaderHandle, name: string): Promise<{ ok: boolean; live: boolean }> {
109
+ if (name === 'dsh-plugin' || name.startsWith('@deepseek-ai/')) return { ok: false, live: true }
102
110
  // 5s 超时兜底:fiber dispose 卡住时不能让卸载任务(乃至整个队列)被拖死
103
111
  const removal = (async () => {
104
112
  const entries = dumpLoaderEntries(loader)
@@ -116,33 +124,80 @@ export async function removeLoadedEntry(loader: LoaderHandle, name: string): Pro
116
124
  break
117
125
  } catch (error) {
118
126
  console.log(`[hub-uninstall] disable ${name} attempt ${attempt + 1} failed: ${error instanceof Error ? error.message : String(error)}`)
119
- if (attempt === 2) return false
127
+ if (attempt === 2) return { ok: false, live: true }
120
128
  await new Promise((resolvePromise) => setTimeout(resolvePromise, 200))
121
129
  }
122
130
  }
123
131
  }
124
132
  if (!found) {
125
- // 运行中 loader 无该条目:宿主从未加载过它(装完未重启),磁盘已干净,无需重启
133
+ // 运行中 loader 无该条目:宿主从未加载过它(装完未重启),磁盘已干净,无需刷新/重启
126
134
  console.log(`[hub-uninstall] no live entry for ${name}; nothing to disable`)
127
- return true
135
+ return { ok: true, live: false }
128
136
  }
129
137
  // 找到并停用了 live entry:逻辑层已卸载(刷新页面不再加载其 client bundle),
130
- // 但带 UI 的插件(侧边栏面板等)是宿主启动时渲染的槽位,disable 不会主动摘除,
131
- // 不重启面板会一直残留 → 返回「需要重启」,弹窗给出「立即重启」,面板立即消失
132
- console.log(`[hub-uninstall] disabled live entry for ${name}; UI may persist until restart`)
133
- return false
138
+ // 带 UI 的插件面板由调用方按 hadClientUi 决定是否需要刷新摘除
139
+ console.log(`[hub-uninstall] disabled live entry for ${name}`)
140
+ return { ok: true, live: true }
134
141
  })()
135
142
  const result = await Promise.race([
136
143
  removal.catch((error) => {
137
144
  console.log(`[hub-uninstall] disable failed: ${error instanceof Error ? error.message : String(error)}`)
138
- return false
145
+ return { ok: false, live: true }
139
146
  }),
140
- new Promise<boolean>((resolve) => setTimeout(() => resolve(false), 5_000)),
147
+ new Promise<{ ok: boolean; live: boolean }>((resolve) => setTimeout(() => resolve({ ok: false, live: true }), 5_000)),
141
148
  ])
142
- console.log(`[hub-uninstall] disable ${name} -> ${result ? 'ok/clean' : 'timed out or failed'}`)
149
+ console.log(`[hub-uninstall] disable ${name} -> ${result.ok ? (result.live ? 'disabled live entry' : 'nothing live') : 'timed out or failed'}`)
143
150
  return result
144
151
  }
145
152
 
153
+ /**
154
+ * 安装成功后把新插件热挂进运行中 loader(等价 profile bundle patch 的
155
+ * `- insert: - name: <pkg>` 行),让插件无需重启宿主即可生效。
156
+ * 做法:在 Loader 根组新建条目并启动 —— Loader.write() 为空实现,不写盘;
157
+ * 重启后由 profile 的 bundles 清单重新挂载,不会重复。
158
+ * `Entry._init` 的 import 失败只 `logger.error` 后 return(不抛),条目会建出来但
159
+ * fiber 为空 / 条目可能被标记 disabled —— 必须用 `loader.resolve(id)` 校验真的起来了。
160
+ * 返回 true = 已在运行中(本次新建或此前已加载)。
161
+ */
162
+ export async function mountLoadedEntry(loader: LoaderHandle, name: string): Promise<boolean> {
163
+ if (isEntryLoaded(loader, name)) return true
164
+ const mounting = (async () => {
165
+ const id = await loader.create({ name })
166
+ const entry = loader.resolve(id)
167
+ if (entry?.fiber !== undefined && entry?.fiber !== null && !entry.options?.disabled) {
168
+ console.log(`[hub-install] hot mounted ${name} (entry ${id})`)
169
+ return true
170
+ }
171
+ console.log(`[hub-install] hot mount ${name}: entry ${id} created but not running (import failed?)`)
172
+ return false
173
+ })()
174
+ // 10s 超时兜底:import 卡住时不能让装完收尾(乃至整个队列)被拖死
175
+ return Promise.race([
176
+ mounting.catch((error) => {
177
+ console.log(`[hub-install] hot mount ${name} failed: ${error instanceof Error ? error.message : String(error)}`)
178
+ return false
179
+ }),
180
+ new Promise<boolean>((resolve) => setTimeout(() => resolve(false), 10_000)),
181
+ ])
182
+ }
183
+
184
+ /**
185
+ * 已安装包是否带客户端 UI(package.json 的 `dsh.client` 声明):client-modules 据它把
186
+ * 客户端 bundle 注入宿主页面。带 UI 的插件热挂载后需要页面重新加载才会渲染,
187
+ * 网页端与桌面端一致 —— `__DSH_BOOT__` 注入清单由宿主服务端按请求现场拼装,
188
+ * 壳不缓存,所以桌面端刷新页面同样能拿到新清单,无需重开 App。
189
+ */
190
+ export function hasClientUi(profile: string, name: string): boolean {
191
+ try {
192
+ const pkg = JSON.parse(readFileSync(join(profileDirectory(profile), 'node_modules', name, 'package.json'), 'utf8')) as {
193
+ dsh?: { client?: unknown }
194
+ }
195
+ return pkg.dsh?.client !== undefined && pkg.dsh?.client !== null
196
+ } catch {
197
+ return false
198
+ }
199
+ }
200
+
146
201
  /**
147
202
  * 判断某已安装包是否是真正的 dsh 插件(宿主会加载 / 值得提示「待重启」):
148
203
  * 1) 包名已写进 profile 的 `dsh.profile.bundles`(宿主启动时按清单加载);
@@ -156,9 +211,8 @@ export function isDshPlugin(profile: string, name: string): boolean {
156
211
  const profilePkg = JSON.parse(readFileSync(join(profileDirectory(profile), 'package.json'), 'utf8')) as {
157
212
  dsh?: { profile?: { bundles?: unknown } }
158
213
  }
159
- if (Array.isArray(profilePkg.dsh?.profile?.bundles)) {
160
- if ((profilePkg.dsh.profile.bundles as unknown[]).includes(name)) return true
161
- }
214
+ const bundles = profilePkg.dsh?.profile?.bundles
215
+ if (Array.isArray(bundles) && bundles.includes(name)) return true
162
216
  } catch { /* profile 读取失败按无 bundles 处理 */ }
163
217
  // 2) 包内声明:dsh 字段(bundle patch / profile 配置)或 dsh 关键字
164
218
  try {