dsh-update-center 0.8.0 → 0.8.2

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.
@@ -0,0 +1,131 @@
1
+ /**
2
+ * 按功能给市场条目分类(awesome-dsh-plugin 的分类体系)。
3
+ *
4
+ * 两条规则按优先级生效:
5
+ * 1. awesome-dsh-plugin 精选清单的仓库 → 分类映射(data/awesome-categories.json,
6
+ * 键为小写 owner/name,值为清单小节名);
7
+ * 2. 名称 + 中英文描述的有序关键词匹配(靠前的分类更特定,先命中先得);
8
+ * 3. 都不中则归入 other。
9
+ */
10
+
11
+ /** 分类键 → 中英文标签(下拉顺序即此对象的键序)。 */
12
+ export const CATEGORY_LABELS = {
13
+ ui: { en: 'UI Enhancements', zh: '界面增强' },
14
+ billing: { en: 'Usage & Billing', zh: '用量与计费' },
15
+ themes: { en: 'Themes & Appearance', zh: '主题与外观' },
16
+ models: { en: 'Models & Providers', zh: '模型与供应商' },
17
+ sessions: { en: 'Sessions & Messages', zh: '会话与消息' },
18
+ memory: { en: 'Memory', zh: '记忆' },
19
+ tools: { en: 'Tools & Capabilities', zh: '工具与能力' },
20
+ vision: { en: 'Vision & Multimodal', zh: '视觉与多模态' },
21
+ skills: { en: 'Skills', zh: '技能' },
22
+ workflow: { en: 'Workflow & Automation', zh: '工作流与自动化' },
23
+ notifications: { en: 'Notifications & Integrations', zh: '通知与集成' },
24
+ dev: { en: 'Development & Runtime', zh: '开发与运行时' },
25
+ markets: { en: 'Plugin Markets & Managers', zh: '插件市场与管理' },
26
+ fun: { en: 'Just for Fun', zh: '趣味' },
27
+ other: { en: 'Other', zh: '其他' },
28
+ }
29
+
30
+ /** awesome 清单小节名 → 分类键。 */
31
+ const SECTION_TO_KEY = {
32
+ 'UI Enhancements': 'ui',
33
+ 'Usage & Billing': 'billing',
34
+ 'Themes & Appearance': 'themes',
35
+ 'Models & Providers': 'models',
36
+ 'Sessions & Messages': 'sessions',
37
+ Memory: 'memory',
38
+ 'Tools & Capabilities': 'tools',
39
+ 'Vision & Multimodal': 'vision',
40
+ Skills: 'skills',
41
+ 'Workflow & Automation': 'workflow',
42
+ 'Notifications & Integrations': 'notifications',
43
+ 'Development & Runtime': 'dev',
44
+ 'Plugin Markets & Managers': 'markets',
45
+ 'Just for Fun': 'fun',
46
+ }
47
+
48
+ /** 个别仓库的关键词覆盖不到,显式指定(小写 owner/name → 分类键)。 */
49
+ const MANUAL_OVERRIDES = {
50
+ 'deepseek-ai/deepseek-harness': 'dev',
51
+ 'britneycode/dsh-update-center': 'markets',
52
+ }
53
+
54
+ /**
55
+ * 关键词规则(有序,先命中先得;英文匹配小写子串,中文直接子串)。
56
+ * 无意义的通用词(dsh、plugin、插件)不参与匹配。
57
+ */
58
+ const KEYWORD_RULES = [
59
+ ['markets', ['market', '市场', 'registry', '注册表', 'plugin manager', '插件管理', 'plugin store', '插件商店', 'installer', '安装器', 'updater', '更新器', '商店', '插件生态', 'oh-my', 'update-center', 'update center', '更新中心', 'healthcheck', '健康检测', '木马', 'malware']],
60
+ ['billing', ['billing', '计费', 'usage', '用量', 'cost', '费用', 'spend', '开销', 'quota', '配额', '账单', 'invoice', '价格监控', 'price', 'subscription', '订阅', 'balance', '余额']],
61
+ ['vision', ['vision', '视觉', 'ocr', '文字识别', 'multimodal', '多模态', 'screenshot', '截图', '看图', '识图', 'image', '图像', '图片', 'photo', '照片', 'video', '视频', 'camera', '摄像头', 'pdf', 'chart recog', '图表识别']],
62
+ ['memory', ['memory', '记忆', 'knowledge base', '知识库', 'knowledge graph', '知识图谱', 'rag', 'embedding', '向量', 'retrieval', '检索增强', 'long-term', '长期记忆', 'flashback', '回忆']],
63
+ ['notifications', ['notify', 'notification', '通知', '提醒', 'telegram', 'slack', 'discord', '钉钉', 'dingtalk', '飞书', 'feishu', 'lark', '微信', 'wechat', 'email', '邮件', 'smtp', 'webhook', '推送', 'bark', '提示音', 'alert', '警报', 'integration', '集成', 'qq', '机器人', 'bot', '频道', 'channel', '兼容', 'compatib', 'tavern']],
64
+ ['themes', ['theme', '主题', 'appearance', '外观', 'color scheme', '配色', '色彩', 'font', '字体', 'icon', '图标', '美化', 'beautif', 'skin', '皮肤', 'splash', '启动画面', 'wallpaper', '壁纸']],
65
+ ['models', ['provider', '供应商', 'model switch', '模型切换', '模型路由', 'model rout', 'llm', 'anthropic', 'openai', 'gpt', 'claude', 'gemini', 'ollama', 'vllm', 'llama', 'qwen', '通义', 'deepseek api', 'openrouter', 'one-api', 'new-api', '中转', 'api key', '模型管理', 'models manage', 'inference', '推理引擎', '免费模型', 'free model', '模型对比', '模型评测', 'reasoning-effort', '思考强度', 'model config', 'model-config', '模型配置']],
66
+ ['skills', ['skill', '技能', 'slash command', '斜杠命令', 'slash-command', 'prompt librar', '提示词', 'persona', '人设', '角色扮演', 'roleplay', '角色', 'clarify', '澄清', 'awesome', '精选', 'curated', 'command palette', '命令面板', 'system prompt', '系统提示']],
67
+ ['workflow', ['workflow', '工作流', 'automation', '自动化', 'cron', '定时', 'schedul', '计划任务', 'pipeline', '流水线', '编排', 'orchestr', 'multi-agent', '多智能体', '多 agent', 'agent team', 'swarm', 'hook', '钩子', '触发', 'trigger', '联动', '一键', 'one-click', 'batch', '批处理', 'macro', '宏', 'permission', '权限', 'auto-approve', '自动批准', 'flow', '流程', '协作', 'collaborat', 'team ']],
68
+ ['sessions', ['session', '会话', 'conversation', '对话', 'chat history', '聊天历史', '历史记录', 'message', '消息', 'export', '导出', 'archive', '归档', 'transcript', 'summar', '总结', '摘要', 'fork', '会话分支', 'pin', '置顶', '标签页', 'chat search', '搜索对话', 'rename', '重命名', 'resume', '恢复会话', 'compact', '压缩上下文', 'attachment', '附件']],
69
+ ['dev', ['debug', '调试', 'runtime', '运行时', 'develop', '开发', 'lint', 'eslint', 'test', '测试', 'coverage', '覆盖率', 'build', '构建', 'bundle', 'repl', 'terminal', '终端', 'shell', 'bash', 'zsh', 'docker', 'kubernetes', 'k8s', 'ci/cd', 'cicd', 'deploy', '部署', 'server', '服务端', 'sdk', 'log', '日志', 'monitor', '监控', 'profiler', '性能分析', 'hot reload', '热重载', 'devtool', '开发工具', 'scaffold', '脚手架', 'git ', '代码审查', 'code review', 'refactor', '重构', '类型', 'typescript', 'bridge', '桥接', 'abi', 'safety', '安全', 'security', 'guardrail', 'sandbox', '沙箱', 'code execution', '代码执行', 'coding agent', '编程', '逆向', 'reverse engineer', 'protocol', '协议', 'vscode', 'ide', 'gateway', '网关', '远程', 'injector', '注入', 'opencode', 'data backend', '数据后端', '后端']],
70
+ ['fun', ['game', '游戏', 'fun', '趣味', '彩蛋', 'easter', 'meme', '表情包', 'emoji', '宠物', 'pet', 'music', '音乐', '闲聊', '无聊', '小说', 'novel', '彩铃', 'pixel', '像素', 'ascii art', '文字画', 'toy']],
71
+ ['ui', ['ui', '界面', 'statusline', '状态栏', 'status bar', 'sidebar', '侧边栏', '侧栏', 'menu', '菜单', 'layout', '布局', 'shortcut', '快捷键', 'keybind', '键位', 'keyboard', 'vim', 'neovim', 'emacs', 'editor', '编辑器', 'input', '输入框', 'render', '渲染', 'markdown', 'panel', '面板', 'fullscreen', '全屏', '动画', 'animation', 'tooltip', 'progress', '进度', 'tui', 'window', '窗口', 'display', '展示', '预览', 'preview', 'tree', '树状', '滚动', 'scroll', 'zen mode', '专注模式', 'desktop', '桌面', '客户端', 'client', 'mobile', '移动端', 'gui', 'activity', '活动状态', '思考状态', 'chain of thought', '思维链', '思考过程']],
72
+ ['tools', ['tool', '工具', 'search', '搜索', 'translate', '翻译', '词典', 'dictionary', 'calc', '计算', 'convert', '转换', '格式化', 'format', 'weather', '天气', '汇率', 'currency', 'qr', '二维码', 'clipboard', '剪贴板', 'file', '文件', '加密', 'encrypt', 'password', '密码', 'generator', '生成器', '爬虫', 'crawler', 'fetch', '抓取', 'download', '下载', 'upload', '上传', 'mcp', '浏览器', 'browser', '数据库', 'database', 'sql', '表格', 'spreadsheet', '地图', 'map', '内容发现', 'rss', '广告', 'ad block', 'anti-ads', '拦截', 'wireframe', '线框', '原型']],
73
+ ]
74
+
75
+ const NOISE_WORDS = new Set(['dsh', 'plugin', '插件', 'deepseek', 'harness', 'extension', '扩展'])
76
+
77
+ /**
78
+ * 单个条目 → 分类键。awesomeMap 为可选的预加载映射
79
+ * (小写 owner/name → 清单小节名)。
80
+ */
81
+ export function classifyPlugin(plugin, awesomeMap) {
82
+ if (plugin?.owner && plugin?.name) {
83
+ const key = `${String(plugin.owner).toLowerCase()}/${String(plugin.name).toLowerCase()}`
84
+ if (MANUAL_OVERRIDES[key]) return MANUAL_OVERRIDES[key]
85
+ const section = awesomeMap?.[key]
86
+ if (section && SECTION_TO_KEY[section]) return SECTION_TO_KEY[section]
87
+ }
88
+ const desc = plugin?.description ?? {}
89
+ const haystack = `${plugin?.name ?? ''} ${desc.en ?? ''} ${desc.zh ?? ''}`.toLowerCase()
90
+ if (!haystack.trim()) return 'other'
91
+ for (const [key, words] of KEYWORD_RULES) {
92
+ for (const word of words) {
93
+ if (!word || NOISE_WORDS.has(word)) continue
94
+ if (haystack.includes(word)) return key
95
+ }
96
+ }
97
+ return 'other'
98
+ }
99
+
100
+ /**
101
+ * 给整个 registry 数据重算分类(原地修改):每个条目的 category 字段 +
102
+ * 顶层的 categories 标签表。返回 { counts, bySource } 供日志/预览。
103
+ */
104
+ export function applyCategories(data, awesomeMap) {
105
+ const counts = {}
106
+ const bySource = { awesome: 0, keyword: 0, other: 0 }
107
+ if (!data || !Array.isArray(data.plugins)) return { counts, bySource }
108
+ for (const plugin of data.plugins) {
109
+ const key = `${String(plugin?.owner ?? '').toLowerCase()}/${String(plugin?.name ?? '').toLowerCase()}`
110
+ const inAwesome = !!(awesomeMap && awesomeMap[key])
111
+ const cat = classifyPlugin(plugin, awesomeMap)
112
+ plugin.category = cat
113
+ counts[cat] = (counts[cat] ?? 0) + 1
114
+ if (inAwesome) bySource.awesome += 1
115
+ else if (cat === 'other') bySource.other += 1
116
+ else bySource.keyword += 1
117
+ }
118
+ data.categories = { ...CATEGORY_LABELS }
119
+ return { counts, bySource }
120
+ }
121
+
122
+ /** 从 awesome 清单映射 JSON(data/awesome-categories.json 的内容)展开成查找表。 */
123
+ export function flattenAwesomeMap(awesomeData) {
124
+ const flat = {}
125
+ if (!awesomeData || typeof awesomeData !== 'object') return flat
126
+ for (const [section, repos] of Object.entries(awesomeData)) {
127
+ if (!Array.isArray(repos)) continue
128
+ for (const repo of repos) flat[String(repo).toLowerCase()] = section
129
+ }
130
+ return flat
131
+ }
package/lib/client.js CHANGED
@@ -1,5 +1,5 @@
1
1
  window.__ModuleLoader__.load({
2
- id: "@dsh-external/dsh-update-center",
2
+ id: "dsh-update-center",
3
3
  factory: (require) => {
4
4
  var module = { exports: {} };
5
5
  var exports = module.exports;
@@ -15,6 +15,7 @@ window.__ModuleLoader__.load({
15
15
  */
16
16
  const inject = ["slots"];
17
17
  const API = "/update-center/api";
18
+ const MARKET_PAGE_SIZE = 500;
18
19
  function el(tag, cls, text) {
19
20
  const element = document.createElement(tag);
20
21
  if (cls) element.className = cls;
@@ -39,7 +40,7 @@ window.__ModuleLoader__.load({
39
40
  .uc-meta{gap:6px;color:var(--dsw-alias-label-tertiary);font-size:11px;min-width:0;flex-wrap:wrap}
40
41
  .uc-path{display:block;min-width:0;max-width:460px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
41
42
  .uc-actions{gap:8px;justify-content:flex-end;flex-wrap:wrap}
42
- .uc-btn{min-height:32px;border:1px solid transparent;border-radius:6px;padding:6px 12px;background:#2878d0;color:#fff;font:inherit;font-size:12px;cursor:pointer;white-space:nowrap}
43
+ .uc-btn{min-height:32px;border:1px solid transparent;border-radius:6px;padding:6px 12px;background:#2878d0;color:#fff;font:inherit;font-size:12px;cursor:pointer;white-space:nowrap;text-decoration:none}
43
44
  .uc-btn.secondary{background:transparent;border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary)}
44
45
  .uc-btn.danger{background:transparent;border-color:rgba(210,58,58,.45);color:#d23a3a}
45
46
  .uc-btn:disabled{opacity:.45;cursor:not-allowed}
@@ -57,6 +58,8 @@ window.__ModuleLoader__.load({
57
58
  .uc-desc{color:var(--dsw-alias-label-secondary);font-size:12px;margin-top:3px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
58
59
  .uc-tags{display:flex;gap:5px;align-items:center;flex-wrap:wrap;margin-top:3px}
59
60
  .uc-tag{background:var(--dsw-alias-bg-multi-select);color:var(--dsw-alias-label-secondary)}
61
+ .uc-tag.cat{border:none;font:inherit;cursor:pointer;box-shadow:none}
62
+ .uc-tag.cat:hover{background:var(--dsw-alias-bg-layer-3)}
60
63
  .uc-tag.npm{background:rgba(40,120,208,.12);color:#2878d0}
61
64
  .uc-tag.link{background:rgba(39,174,96,.12);color:#28945a}
62
65
  .uc-tag.preset{background:rgba(151,89,182,.13);color:#9858b4}
@@ -67,6 +70,7 @@ window.__ModuleLoader__.load({
67
70
  .uc-input{flex:1 1 200px;min-width:160px}
68
71
  .uc-select{flex:0 0 auto}
69
72
  .uc-market-meta{width:100%;color:var(--dsw-alias-label-tertiary);font-size:11px}
73
+ .uc-load-more{display:flex;justify-content:center;padding:16px 0;border-top:1px solid var(--dsw-alias-border-l2)}
70
74
  .uc-msg{margin-top:14px;padding:10px 12px;border-left:3px solid var(--dsw-alias-border-l3);background:var(--dsw-alias-bg-layer-2);white-space:pre-wrap;max-height:260px;overflow:auto;font-size:12px}
71
75
  .uc-msg.ok{border-color:#28945a}
72
76
  .uc-msg.err{border-color:#d23a3a}
@@ -110,8 +114,11 @@ window.__ModuleLoader__.load({
110
114
  const updateAllButton = el("button", "uc-btn", "全部更新");
111
115
  updateAllButton.type = "button";
112
116
  updateAllButton.disabled = true;
117
+ const restartButton = el("button", "uc-btn secondary", "重启 dsh");
118
+ restartButton.type = "button";
119
+ restartButton.title = "更新完成后一键重启 dsh web(当前会话会中断)";
113
120
  const toolbarActions = el("div", "uc-actions");
114
- toolbarActions.append(checkButton, updateAllButton);
121
+ toolbarActions.append(checkButton, updateAllButton, restartButton);
115
122
  toolbar.append(heading, toolbarActions);
116
123
  const tabs = el("div", "uc-tabs");
117
124
  const installedTab = el("button", "uc-tab active", "已安装");
@@ -162,13 +169,19 @@ window.__ModuleLoader__.load({
162
169
  const message = el("div", "uc-msg");
163
170
  message.style.display = "none";
164
171
  page.append(toolbar, tabs, installedView, marketView, message);
165
- const buttons = new Set([checkButton, updateAllButton]);
172
+ const buttons = new Set([
173
+ checkButton,
174
+ updateAllButton,
175
+ restartButton
176
+ ]);
166
177
  let checked = false;
178
+ let lastCheckData = null;
167
179
  let activeJobId = "";
168
180
  let marketData = null;
169
181
  let marketQuery = "";
170
182
  let marketCategory = "";
171
183
  let marketSort = "stars";
184
+ let marketVisibleLimit = MARKET_PAGE_SIZE;
172
185
  let lastStatusData = null;
173
186
  /** 已安装插件里有多少项可更新(用于“全部更新”按钮态)。 */
174
187
  function pluginUpdateCount(plugins) {
@@ -177,6 +190,59 @@ window.__ModuleLoader__.load({
177
190
  return plugin.kind === "npm" && plugin.latest && plugin.latest !== plugin.version || (plugin.kind === "link" || plugin.kind === "preset") && Number(git.behind) > 0 && !git.dirty;
178
191
  }).length;
179
192
  }
193
+ /** 更新任务完成后合成状态:/status 提供最新的本地版本号(新装/更新后的真实
194
+ * 版本、新插件条目),上次 /check 提供尚未更新插件的远端信息(npm latest、
195
+ * git behind)。刚更新成功的插件标记为已最新。这样更新完一个插件后其余
196
+ * 插件的更新按钮仍然可用,不必重新“检查更新”。 */
197
+ function mergeStatusAfterJob(fresh, updatedTargets) {
198
+ if (!lastCheckData) return fresh;
199
+ const checkPlugins = Array.isArray(lastCheckData.plugins) ? lastCheckData.plugins : [];
200
+ const checkByName = new Map(checkPlugins.map((plugin) => [String(plugin.name), plugin]));
201
+ const plugins = (Array.isArray(fresh.plugins) ? fresh.plugins : []).map((plugin) => {
202
+ const check = checkByName.get(String(plugin.name));
203
+ if (!check) {
204
+ if (plugin.kind === "npm" && plugin.version) return {
205
+ ...plugin,
206
+ latest: plugin.version
207
+ };
208
+ return plugin;
209
+ }
210
+ const merged = { ...plugin };
211
+ if (updatedTargets.has(String(plugin.name))) {
212
+ if (plugin.kind === "npm") if (plugin.version) merged.latest = plugin.version;
213
+ else delete merged.latest;
214
+ if (merged.git) merged.git = {
215
+ ...merged.git,
216
+ behind: 0
217
+ };
218
+ } else {
219
+ if (check.latest != null) merged.latest = check.latest;
220
+ if (check.git) merged.git = check.git;
221
+ if (check.upstream) merged.upstream = check.upstream;
222
+ }
223
+ return merged;
224
+ });
225
+ const repo = { ...fresh.repo };
226
+ const checkRepo = lastCheckData.repo ?? {};
227
+ if (checkRepo.fetchOk !== void 0) {
228
+ repo.fetchOk = checkRepo.fetchOk;
229
+ repo.fetchErr = checkRepo.fetchErr;
230
+ }
231
+ const repoBehind = Number(repo?.git?.behind ?? 0);
232
+ const pluginUpdates = pluginUpdateCount(plugins);
233
+ return {
234
+ ...fresh,
235
+ repo,
236
+ plugins,
237
+ summary: {
238
+ repoUpdates: repoBehind,
239
+ pluginUpdates,
240
+ totalUpdates: (repoBehind > 0 ? 1 : 0) + pluginUpdates,
241
+ failedChecks: 0
242
+ },
243
+ ts: lastCheckData.ts ?? Date.now()
244
+ };
245
+ }
180
246
  /** 市场清单里与某个已安装依赖对应的条目(npm 名或仓库名匹配)。 */
181
247
  function marketEntryFor(name) {
182
248
  return (Array.isArray(marketData?.plugins) ? marketData.plugins : []).find((entry) => entry.name === name || entry.npm && entry.npm === name) ?? null;
@@ -258,14 +324,29 @@ window.__ModuleLoader__.load({
258
324
  return;
259
325
  }
260
326
  activeJobId = "";
261
- checked = false;
262
- return refreshStatus(false, true).then(() => {
263
- if (marketData) loadMarket(false, true);
264
- setBusy(false, activeButton);
265
- if (job.status === "completed") {
327
+ const updatedTargets = /* @__PURE__ */ new Set();
328
+ if (job.status === "completed") {
329
+ if (Array.isArray(job.results)) {
330
+ for (const entry of job.results) if (entry?.ok) updatedTargets.add(String(entry.target));
331
+ } else if ((job.action === "npm" || job.action === "link") && job.target) updatedTargets.add(String(job.target));
332
+ }
333
+ const done = (status) => {
334
+ if (status === "completed") {
266
335
  const suffix = job.restartRequired ? "\n\n更新已核验完成,现在可以重启 dsh web。" : "";
267
336
  say(jobText(job) + suffix, "ok");
268
337
  } else say(jobText(job), "err");
338
+ };
339
+ return fetchJson("/status?fresh=1").then((fresh) => {
340
+ if (!fresh?.ok) throw new Error(fresh?.error || "状态读取失败");
341
+ const merged = checked && lastCheckData ? mergeStatusAfterJob(fresh, updatedTargets) : fresh;
342
+ if (merged !== fresh) lastCheckData = merged;
343
+ setBusy(false, activeButton);
344
+ render(merged);
345
+ if (marketData) loadMarket(false, true);
346
+ done(job.status === "completed" ? "completed" : "failed");
347
+ }).catch((error) => {
348
+ setBusy(false, activeButton);
349
+ say(jobText(job) + `\n\n(列表状态刷新失败:${String(error)},可稍后点“检查更新”重新获取。)`, "info");
269
350
  });
270
351
  }).catch((error) => {
271
352
  if (!page.isConnected) return;
@@ -450,18 +531,20 @@ window.__ModuleLoader__.load({
450
531
  });
451
532
  if (marketSort === "stars") filtered.sort((a, b) => Number(b.stars ?? 0) - Number(a.stars ?? 0));
452
533
  else if (marketSort === "added") filtered.sort((a, b) => (Date.parse(String(b.added ?? "")) || 0) - (Date.parse(String(a.added ?? "")) || 0));
453
- marketMeta.textContent = `来源:${{
534
+ const sourceLabel = {
454
535
  registry: "本仓库 plugins.json",
455
536
  github: "GitHub topic:dsh-plugin",
456
537
  disk: "本地缓存",
457
538
  snapshot: "内置快照",
458
539
  network: "网络"
459
- }[String(marketData.source)] ?? marketData.source} · 扫描于 ${String(marketData.updated || "未知")} · 共 ${plugins.length} 个,筛选出 ${filtered.length} 个${filtered.length > 300 ? "(仅显示前 300)" : ""}`;
540
+ }[String(marketData.source)] ?? marketData.source;
541
+ const visibleCount = Math.min(filtered.length, marketVisibleLimit);
542
+ marketMeta.textContent = `来源:${sourceLabel} · 扫描于 ${String(marketData.updated || "未知")} · 共 ${plugins.length} 个,筛选出 ${filtered.length} 个 · 已显示 ${visibleCount} 个`;
460
543
  if (!filtered.length) {
461
544
  marketList.append(el("li", "uc-empty", "没有匹配的插件"));
462
545
  return;
463
546
  }
464
- for (const entry of filtered.slice(0, 300)) {
547
+ for (const entry of filtered.slice(0, marketVisibleLimit)) {
465
548
  const installed = installedSet.has(String(entry.name)) || entry.npm && installedSet.has(String(entry.npm));
466
549
  const item = el("li", "uc-item");
467
550
  const identity = el("div");
@@ -472,8 +555,21 @@ window.__ModuleLoader__.load({
472
555
  const description = entry.description?.zh || entry.description?.en || "";
473
556
  if (description) identity.append(el("div", "uc-desc", String(description)));
474
557
  const tags = el("div", "uc-tags");
558
+ if (entry.category && marketData?.categories?.[entry.category]) {
559
+ const catLabel = marketData.categories[entry.category];
560
+ const catTag = el("button", "uc-tag cat", String(catLabel?.zh || catLabel?.en || entry.category));
561
+ catTag.type = "button";
562
+ catTag.title = "按此分类筛选";
563
+ catTag.addEventListener("click", () => {
564
+ marketCategory = String(entry.category);
565
+ categorySelect.value = marketCategory;
566
+ renderMarket();
567
+ });
568
+ tags.append(catTag);
569
+ }
475
570
  if (entry.owner) tags.append(el("span", "uc-meta", String(entry.owner)));
476
- if (entry.npm) tags.append(el("span", "uc-tag npm", "npm"));
571
+ if (entry.installMode === "manual") tags.append(el("span", "uc-tag preset", "套装"));
572
+ else if (entry.npm) tags.append(el("span", "uc-tag npm", "npm"));
477
573
  else tags.append(el("span", "uc-tag", "github"));
478
574
  identity.append(tags);
479
575
  const status = el("div", "uc-status");
@@ -482,7 +578,14 @@ window.__ModuleLoader__.load({
482
578
  const statusPlugin = (lastStatusData?.plugins ?? []).find((plugin) => plugin.name === entry.npm || plugin.name === entry.name);
483
579
  const updatable = statusPlugin && (statusPlugin.kind === "npm" && statusPlugin.latest && statusPlugin.latest !== statusPlugin.version || Number(statusPlugin.git?.behind) > 0);
484
580
  const actions = el("div", "uc-actions");
485
- if (installed) {
581
+ if (entry.installMode === "manual") {
582
+ status.append(badge("手动安装", "muted"));
583
+ const guideLink = el("a", "uc-btn", "安装说明");
584
+ guideLink.href = String(entry.url);
585
+ guideLink.target = "_blank";
586
+ guideLink.rel = "noreferrer";
587
+ actions.append(guideLink);
588
+ } else if (installed) {
486
589
  status.append(updatable ? badge("可更新", "update") : badge("已安装", "ok"));
487
590
  const removeButton = actionButton("卸载", "danger");
488
591
  bindAction(removeButton, "/uninstall", { name: entry.npm || entry.name }, `从 profile 卸载 ${entry.npm || entry.name}?任务完成后再重启,是否继续?`, "正在创建后台卸载任务…");
@@ -495,14 +598,27 @@ window.__ModuleLoader__.load({
495
598
  item.append(identity, status, actions);
496
599
  marketList.append(item);
497
600
  }
601
+ if (filtered.length > marketVisibleLimit) {
602
+ const remaining = filtered.length - marketVisibleLimit;
603
+ const loadMoreItem = el("li", "uc-load-more");
604
+ const loadMoreButton = el("button", "uc-btn secondary", `显示更多(剩余 ${remaining} 个)`);
605
+ loadMoreButton.type = "button";
606
+ loadMoreButton.addEventListener("click", () => {
607
+ marketVisibleLimit += MARKET_PAGE_SIZE;
608
+ renderMarket();
609
+ });
610
+ loadMoreItem.append(loadMoreButton);
611
+ marketList.append(loadMoreItem);
612
+ }
498
613
  }
499
614
  async function loadMarket(force, silent) {
500
615
  if (force) setBusy(true, refreshButton, "刷新中…");
501
616
  if (!silent) say("正在加载插件市场清单…", "info");
502
617
  try {
503
- const data = await fetchJson(force ? "/market/refresh" : "/market");
618
+ const data = await fetchJson(force ? "/market/refresh" : "/market", force ? { method: "POST" } : void 0);
504
619
  if (!data?.ok) throw new Error(data?.error || "清单加载失败");
505
620
  marketData = data;
621
+ marketVisibleLimit = MARKET_PAGE_SIZE;
506
622
  renderCategorySelect();
507
623
  renderMarket();
508
624
  if (lastStatusData) renderPlugins(lastStatusData.plugins ?? []);
@@ -526,12 +642,17 @@ window.__ModuleLoader__.load({
526
642
  renderRepo(data?.repo);
527
643
  renderPlugins(Array.isArray(data?.plugins) ? data.plugins : []);
528
644
  updateSummary(data);
529
- updateAllButton.disabled = !checked || pluginUpdateCount(data?.plugins ?? []) === 0;
645
+ const job = data?.job ?? {};
646
+ const jobRunning = job.status === "queued" || job.status === "running";
647
+ updateAllButton.disabled = jobRunning;
648
+ updateAllButton.title = jobRunning ? "有更新任务正在执行,请等待完成后再批量更新" : "先检查远端,再一键更新所有可更新的插件(不含 dsh 本体)";
649
+ restartButton.disabled = jobRunning;
650
+ restartButton.title = restartButton.disabled ? "有更新任务正在执行,请等待完成后再重启" : "更新完成后一键重启 dsh web(当前会话会中断)";
530
651
  }
531
652
  function refreshStatus(showJob = true, fresh = false) {
532
653
  return fetchJson(fresh ? "/status?fresh=1" : "/status").then((data) => {
533
654
  if (!data?.ok) throw new Error(data?.error || "状态读取失败");
534
- render(data);
655
+ render(checked && lastCheckData && !data.summary ? mergeStatusAfterJob(data, /* @__PURE__ */ new Set()) : data);
535
656
  if (showJob) {
536
657
  resumeJob(data.job);
537
658
  if (data?.job?.status === "completed") say(jobText(data.job), "ok");
@@ -543,18 +664,69 @@ window.__ModuleLoader__.load({
543
664
  marketTab.addEventListener("click", () => switchTab("market"));
544
665
  searchInput.addEventListener("input", () => {
545
666
  marketQuery = searchInput.value;
667
+ marketVisibleLimit = MARKET_PAGE_SIZE;
546
668
  renderMarket();
547
669
  });
548
670
  categorySelect.addEventListener("change", () => {
549
671
  marketCategory = categorySelect.value;
672
+ marketVisibleLimit = MARKET_PAGE_SIZE;
550
673
  renderMarket();
551
674
  });
552
675
  sortSelect.addEventListener("change", () => {
553
676
  marketSort = sortSelect.value;
677
+ marketVisibleLimit = MARKET_PAGE_SIZE;
554
678
  renderMarket();
555
679
  });
556
680
  refreshButton.addEventListener("click", () => void loadMarket(true, false));
557
- bindAction(updateAllButton, "/update-all", {}, "把所有可更新的插件(不含 dsh 本体)排成一个后台批量任务串行执行,单个失败不影响其余。任务完成后再重启,是否继续?", "正在检查并创建批量更新任务…");
681
+ bindAction(updateAllButton, "/update-all", {}, "将先自动检查远端更新,再把所有可更新的插件(不含 dsh 本体)排成一个后台批量任务串行执行,单个失败不影响其余。检查可能需要十几秒。任务完成后再重启,是否继续?", "正在检查并创建批量更新任务,可能需要十几秒…");
682
+ /** 重启下发后等待服务恢复:先看到请求失败(旧进程已停),再看到成功(新
683
+ * 进程就绪),然后自动刷新页面。期间保持全部按钮禁用,避免把请求打进
684
+ * 一个正在关闭/正在启动的服务。超时则恢复按钮并提示手动处理。 */
685
+ function waitForRestart() {
686
+ const deadline = Date.now() + 18e4;
687
+ let sawDown = false;
688
+ const tick = () => {
689
+ if (!page.isConnected) return;
690
+ if (Date.now() > deadline) {
691
+ setBusy(false, restartButton);
692
+ say("等待 dsh 重启超时。请手动刷新页面;若一直打不开,可查看 ~/.dsh/update-center/dsh-web.stderr.log 或用 Start-DSH 启动并查看控制台输出。", "err");
693
+ return;
694
+ }
695
+ fetchJson("/status").then(() => {
696
+ if (sawDown) {
697
+ say("dsh 已重启完成,正在刷新页面…", "ok");
698
+ window.location.reload();
699
+ return;
700
+ }
701
+ window.setTimeout(tick, 1e3);
702
+ }).catch(() => {
703
+ sawDown = true;
704
+ window.setTimeout(tick, 2e3);
705
+ });
706
+ };
707
+ window.setTimeout(tick, 1500);
708
+ }
709
+ restartButton.addEventListener("click", () => {
710
+ const job = lastStatusData?.job ?? {};
711
+ if (job.status === "queued" || job.status === "running") {
712
+ say("有更新任务正在执行,请等待任务完成后再重启(重启会中断后台任务)。", "err");
713
+ return;
714
+ }
715
+ if (!window.confirm("重启 dsh web?当前会话会中断,重启完成后页面会自动刷新。")) return;
716
+ setBusy(true, restartButton, "重启中…");
717
+ say("正在下发重启指令…", "info");
718
+ fetchJson("/restart", {
719
+ method: "POST",
720
+ body: "{}"
721
+ }).then((data) => {
722
+ if (!data?.ok) throw new Error(data?.result || data?.error || "重启失败");
723
+ say("重启指令已下发,dsh 即将重启;恢复后面板会自动刷新。", "info");
724
+ waitForRestart();
725
+ }).catch((error) => {
726
+ setBusy(false, restartButton);
727
+ say("重启请求失败:" + String(error), "err");
728
+ });
729
+ });
558
730
  checkButton.addEventListener("click", () => {
559
731
  setBusy(true, checkButton, "检查中…");
560
732
  say("正在检查 dsh 和插件的远端状态…", "info");
@@ -564,6 +736,7 @@ window.__ModuleLoader__.load({
564
736
  }).then((data) => {
565
737
  if (!data?.ok) throw new Error(data?.error || "检查失败");
566
738
  checked = true;
739
+ lastCheckData = data;
567
740
  render(data);
568
741
  if (marketData) renderMarket();
569
742
  const total = Number(data?.summary?.totalUpdates ?? 0);