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.
- package/client/client.js +377 -336
- package/client/client.js.map +1 -1
- package/lib/http/routes.js +38 -2
- package/lib/services/install/task-queue.js +32 -15
- package/lib/services/loader.js +67 -21
- package/lib/types/http/routes.d.ts +5 -0
- package/lib/types/services/install/install-types.d.ts +8 -1
- package/lib/types/services/install/task-queue.d.ts +5 -4
- package/lib/types/services/loader.d.ts +45 -7
- package/package.json +1 -1
- package/src/client/components/PluginHubSection.tsx +59 -11
- package/src/client/components/modals/NotificationsModal.tsx +2 -5
- package/src/client/components/modals/modals.tsx +34 -22
- package/src/client/hooks/useTaskQueue.ts +16 -11
- package/src/client/locales.ts +17 -4
- package/src/client/types.ts +1 -1
- package/src/server/http/routes.ts +39 -2
- package/src/server/services/install/install-types.ts +8 -1
- package/src/server/services/install/task-queue.ts +36 -20
- package/src/server/services/loader.ts +77 -23
|
@@ -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
|
|
7
|
-
*
|
|
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
|
-
* 返回
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
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
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
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 ? '
|
|
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
|
-
|
|
160
|
-
|
|
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 {
|