dsh-plugin-pack 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +537 -3
- package/bin/dsh-plugin-pack.js +200 -0
- package/client.js +2320 -0
- package/cordis.patch.yml +24 -0
- package/hotload.js +230 -0
- package/lib/export.js +444 -0
- package/lib/home.js +75 -0
- package/lib/import-plugin.js +356 -0
- package/lib/import-tgz.js +115 -0
- package/lib/import.js +831 -0
- package/lib/inbox.js +194 -0
- package/lib/index.js +725 -0
- package/lib/jobs.js +208 -0
- package/lib/patch-text.js +672 -0
- package/lib/scan.js +488 -0
- package/lib/server.js +926 -0
- package/lib/tar.js +241 -0
- package/lib/util.js +333 -0
- package/lib/zip-stream.js +166 -0
- package/lib/zip.js +514 -0
- package/package.json +72 -6
package/lib/jobs.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 后台任务管理器。
|
|
3
|
+
*
|
|
4
|
+
* 解决的问题:导出/导入要跑几十秒,用户切走画面(客户端断开)时
|
|
5
|
+
* 任务在宿主里照样跑完,但结果丢了 —— 切回来什么都看不到。
|
|
6
|
+
*
|
|
7
|
+
* 做法:
|
|
8
|
+
* 1. 每个长任务登记成一条记录,有 id、状态、进度、结果
|
|
9
|
+
* 2. 任务本身在宿主里同步执行(本来就不受客户端断开影响)
|
|
10
|
+
* 3. 客户端可以通过 /plugin-pack/jobs 随时查:进行中、已完成、失败
|
|
11
|
+
* 4. 完成的结果保留一段时间,切回画面还能看到
|
|
12
|
+
*
|
|
13
|
+
* 记录存在内存里(进程重启即失,这是合理的:任务本身也随进程结束)。
|
|
14
|
+
*
|
|
15
|
+
* @module dsh-plugin-pack/jobs
|
|
16
|
+
*/
|
|
17
|
+
import { randomUUID } from 'node:crypto';
|
|
18
|
+
|
|
19
|
+
/** 已完成任务保留多久(毫秒)。 */
|
|
20
|
+
const KEEP_DONE_MS = 30 * 60 * 1000;
|
|
21
|
+
/** 最多保留多少条。 */
|
|
22
|
+
const MAX_JOBS = 40;
|
|
23
|
+
|
|
24
|
+
/** @type {Map<string, object>} */
|
|
25
|
+
const jobs = new Map();
|
|
26
|
+
|
|
27
|
+
/** 当前的进度回调栈(支持嵌套调用)。 */
|
|
28
|
+
const progressStack = [];
|
|
29
|
+
|
|
30
|
+
/** 单调递增序号:同一毫秒内创建的任务也能稳定排序。 */
|
|
31
|
+
let sequence = 0;
|
|
32
|
+
|
|
33
|
+
/** 产出一个短 id。 */
|
|
34
|
+
function newId() {
|
|
35
|
+
return randomUUID().slice(0, 8);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 清理过旧/过多的记录。 */
|
|
39
|
+
function prune() {
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
for (const [id, job] of jobs) {
|
|
42
|
+
const finished = job.status === 'done' || job.status === 'failed';
|
|
43
|
+
if (finished && now - job.finishedAt > KEEP_DONE_MS) jobs.delete(id);
|
|
44
|
+
}
|
|
45
|
+
// 超过上限时,先删最老的已完成任务
|
|
46
|
+
if (jobs.size > MAX_JOBS) {
|
|
47
|
+
const finished = [...jobs.values()]
|
|
48
|
+
.filter((job) => job.status === 'done' || job.status === 'failed')
|
|
49
|
+
.sort((a, b) => a.seq - b.seq);
|
|
50
|
+
for (const job of finished) {
|
|
51
|
+
if (jobs.size <= MAX_JOBS) break;
|
|
52
|
+
jobs.delete(job.id);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 跑一个后台任务(支持同步或异步 task)。
|
|
59
|
+
*
|
|
60
|
+
* 异步 task 是必须的:导出要做几秒到几十秒的压缩,
|
|
61
|
+
* 用同步 API 会卡死事件循环,把 dsh 的对话一起拖住。
|
|
62
|
+
*
|
|
63
|
+
* @param {object} options
|
|
64
|
+
* @param {'export'|'import'|'rollback'|'import-plugin'} options.kind
|
|
65
|
+
* @param {string} options.title 人读的标题(如 "导出 5 个插件")
|
|
66
|
+
* @param {object} [options.meta] 任意附加信息(参数摘要等)
|
|
67
|
+
* @param {(report: (message: string, percent?: number) => void) => any | Promise<any>} options.task
|
|
68
|
+
* @returns {Promise<{ id: string, result: any, job: object }>}
|
|
69
|
+
*/
|
|
70
|
+
export async function runJob({ kind, title, meta = {}, task }) {
|
|
71
|
+
prune();
|
|
72
|
+
const id = newId();
|
|
73
|
+
const job = {
|
|
74
|
+
id,
|
|
75
|
+
kind,
|
|
76
|
+
title,
|
|
77
|
+
meta,
|
|
78
|
+
seq: ++sequence,
|
|
79
|
+
status: 'running',
|
|
80
|
+
createdAt: Date.now(),
|
|
81
|
+
finishedAt: null,
|
|
82
|
+
progress: [],
|
|
83
|
+
percent: 0,
|
|
84
|
+
result: null,
|
|
85
|
+
error: null,
|
|
86
|
+
};
|
|
87
|
+
jobs.set(id, job);
|
|
88
|
+
|
|
89
|
+
const report = (message, percent) => {
|
|
90
|
+
job.progress.push({ at: Date.now(), message: String(message) });
|
|
91
|
+
if (job.progress.length > 200) job.progress.shift();
|
|
92
|
+
if (Number.isFinite(percent)) job.percent = Math.max(0, Math.min(100, Math.round(percent)));
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
progressStack.push(report);
|
|
96
|
+
try {
|
|
97
|
+
const result = await task(report);
|
|
98
|
+
job.status = 'done';
|
|
99
|
+
job.percent = 100;
|
|
100
|
+
job.finishedAt = Date.now();
|
|
101
|
+
job.result = summarizeResult(kind, result);
|
|
102
|
+
job.durationMs = job.finishedAt - job.createdAt;
|
|
103
|
+
return { id, result, job };
|
|
104
|
+
} catch (error) {
|
|
105
|
+
job.status = 'failed';
|
|
106
|
+
job.finishedAt = Date.now();
|
|
107
|
+
job.error = String(error?.message ?? error);
|
|
108
|
+
job.durationMs = job.finishedAt - job.createdAt;
|
|
109
|
+
throw error;
|
|
110
|
+
} finally {
|
|
111
|
+
progressStack.pop();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** 取当前正在跑的任务的上报函数(导出/导入内部想上报进度时用)。 */
|
|
116
|
+
export function currentReporter() {
|
|
117
|
+
return progressStack[progressStack.length - 1] ?? (() => {});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** 把任务结果压成可序列化的摘要(不要把整个 manifest 回传)。 */
|
|
121
|
+
function summarizeResult(kind, result) {
|
|
122
|
+
if (!result || typeof result !== 'object') return result ?? null;
|
|
123
|
+
const base = {
|
|
124
|
+
ok: result.ok !== false,
|
|
125
|
+
kind: result.kind ?? kind,
|
|
126
|
+
file: result.file ?? null,
|
|
127
|
+
bytes: result.bytes ?? null,
|
|
128
|
+
entries: result.entries ?? null,
|
|
129
|
+
plugins: Array.isArray(result.plugins)
|
|
130
|
+
? result.plugins.map((plugin) => ({
|
|
131
|
+
name: plugin.name,
|
|
132
|
+
version: plugin.version,
|
|
133
|
+
action: plugin.action ?? null,
|
|
134
|
+
files: plugin.files ?? null,
|
|
135
|
+
bytes: plugin.bytes ?? null,
|
|
136
|
+
}))
|
|
137
|
+
: null,
|
|
138
|
+
backup: result.backup ?? null,
|
|
139
|
+
warnings: Array.isArray(result.warnings) ? result.warnings.slice(0, 10) : [],
|
|
140
|
+
restartRequired: Boolean(result.restartRequired),
|
|
141
|
+
};
|
|
142
|
+
return base;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 列出任务。
|
|
147
|
+
*
|
|
148
|
+
* @param {{ limit?: number, includeDone?: boolean }} [options]
|
|
149
|
+
* @returns {object[]} 最新的在前
|
|
150
|
+
*/
|
|
151
|
+
export function listJobs(options = {}) {
|
|
152
|
+
prune();
|
|
153
|
+
const limit = Number.isFinite(options.limit) && options.limit > 0 ? Math.min(100, options.limit) : 20;
|
|
154
|
+
const includeDone = options.includeDone !== false;
|
|
155
|
+
return [...jobs.values()]
|
|
156
|
+
.filter((job) => includeDone || job.status === 'running')
|
|
157
|
+
.sort((a, b) => b.seq - a.seq)
|
|
158
|
+
.slice(0, limit)
|
|
159
|
+
.map(publicJob);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** 取单个任务。 */
|
|
163
|
+
export function getJob(id) {
|
|
164
|
+
const job = jobs.get(String(id ?? ''));
|
|
165
|
+
return job ? publicJob(job) : null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** 是否有正在跑的任务。 */
|
|
169
|
+
export function runningJobs() {
|
|
170
|
+
return [...jobs.values()].filter((job) => job.status === 'running').map(publicJob);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** 转成对外结构(去掉内部字段)。 */
|
|
174
|
+
function publicJob(job) {
|
|
175
|
+
return {
|
|
176
|
+
id: job.id,
|
|
177
|
+
kind: job.kind,
|
|
178
|
+
title: job.title,
|
|
179
|
+
meta: job.meta,
|
|
180
|
+
status: job.status,
|
|
181
|
+
createdAt: job.createdAt,
|
|
182
|
+
finishedAt: job.finishedAt,
|
|
183
|
+
durationMs: job.durationMs ?? null,
|
|
184
|
+
percent: job.percent,
|
|
185
|
+
// 只回最近 12 条进度,避免响应过大
|
|
186
|
+
progress: job.progress.slice(-12),
|
|
187
|
+
result: job.result,
|
|
188
|
+
error: job.error,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** 清空已完成任务(界面上的"清空记录"用)。 */
|
|
193
|
+
export function clearFinished() {
|
|
194
|
+
let removed = 0;
|
|
195
|
+
for (const [id, job] of jobs) {
|
|
196
|
+
if (job.status === 'done' || job.status === 'failed') {
|
|
197
|
+
jobs.delete(id);
|
|
198
|
+
removed += 1;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return removed;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** 测试用:清空全部。 */
|
|
205
|
+
export function resetJobs() {
|
|
206
|
+
jobs.clear();
|
|
207
|
+
progressStack.length = 0;
|
|
208
|
+
}
|