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.
- package/README.md +15 -1
- package/data/awesome-categories.json +1199 -0
- package/data/extra-plugins.json +152 -112
- package/data/registry-snapshot.json +11941 -7636
- package/lib/categorize.mjs +131 -0
- package/lib/client.js +191 -18
- package/lib/client.js.map +1 -1
- package/lib/github-topic.mjs +94 -80
- package/lib/index.js +136 -25
- package/lib/index.js.map +1 -1
- package/package.json +76 -76
- package/plugins.json +11941 -7636
- package/scripts/build.mjs +101 -101
- package/scripts/classify-categories.mjs +43 -0
- package/scripts/refresh-snapshot.mjs +122 -93
- package/scripts/repair-bash-terminal.mjs +65 -0
- package/scripts/restart-dsh.mjs +123 -0
|
@@ -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: "
|
|
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([
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
if (
|
|
264
|
-
|
|
265
|
-
if (job.
|
|
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
|
-
|
|
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
|
|
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,
|
|
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.
|
|
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 (
|
|
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
|
-
|
|
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", {}, "
|
|
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);
|