dsh-plugin-pack 1.0.3 → 1.0.4
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.js +101 -3
- package/lib/jobs.js +103 -5
- package/lib/server.js +79 -3
- package/package.json +1 -1
package/client.js
CHANGED
|
@@ -483,6 +483,10 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
|
|
|
483
483
|
/** 后台任务列表(进行中 + 已完成)。 */
|
|
484
484
|
const getJobs = () => api('/plugin-pack/jobs?limit=20');
|
|
485
485
|
const clearJobs = () => api('/plugin-pack/jobs/clear', { method: 'POST', body: '{}' });
|
|
486
|
+
/** 待决策任务(导入遇到重名时登记的):切回来捡弹窗用。 */
|
|
487
|
+
const getWaiting = () => api('/plugin-pack/waiting');
|
|
488
|
+
const cancelWaiting = (id, reason) =>
|
|
489
|
+
api('/plugin-pack/waiting/cancel', { method: 'POST', body: JSON.stringify({ id, reason }) });
|
|
486
490
|
|
|
487
491
|
/** 上传一个 zip,返回宿主落盘后的绝对路径。 */
|
|
488
492
|
async function uploadZip(file) {
|
|
@@ -1434,8 +1438,15 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
|
|
|
1434
1438
|
const [pickerDir, setPickerDir] = useState('');
|
|
1435
1439
|
const [pickerMode, setPickerMode] = useState('file');
|
|
1436
1440
|
const [importPluginResult, setImportPluginResult] = useState(null);
|
|
1437
|
-
/**
|
|
1441
|
+
/**
|
|
1442
|
+
* 待确认的重名冲突:{ jobId, kind, file, conflicts, plugins }。非空时弹窗。
|
|
1443
|
+
*
|
|
1444
|
+
* 这个状态来自服务端的"待决策"任务,不是纯前端内存 ——
|
|
1445
|
+
* 切走画面再回来能捡回弹窗,否则会留下文件已上传但没人处理的悬空状态。
|
|
1446
|
+
*/
|
|
1438
1447
|
const [pendingImport, setPendingImport] = useState(null);
|
|
1448
|
+
/** 当前弹窗对应的服务端任务 id(取消时要通知服务端)。 */
|
|
1449
|
+
const pendingJobId = useRef(null);
|
|
1439
1450
|
/** 后台任务列表:切走画面回来靠它恢复 */
|
|
1440
1451
|
const [jobs, setJobs] = useState([]);
|
|
1441
1452
|
/** 体积上限(MiB),0 = 不限制。默认跟着服务端配置走。 */
|
|
@@ -1509,6 +1520,64 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
|
|
|
1509
1520
|
}
|
|
1510
1521
|
}, []);
|
|
1511
1522
|
|
|
1523
|
+
/* 挂载时捡回"待决策"的导入。
|
|
1524
|
+
*
|
|
1525
|
+
* 场景:用户在冲突弹窗里没做选择就切走了画面。组件卸载后前端 state 丢了,
|
|
1526
|
+
* 但服务端还记着这个待决策任务 —— 回来时把它捡回来重开弹窗,
|
|
1527
|
+
* 而不是让那次导入悬在半空(文件传上去了,没人处理)。
|
|
1528
|
+
*/
|
|
1529
|
+
useEffect(() => {
|
|
1530
|
+
let alive = true;
|
|
1531
|
+
(async () => {
|
|
1532
|
+
try {
|
|
1533
|
+
const data = await getWaiting();
|
|
1534
|
+
if (!alive) return;
|
|
1535
|
+
const list = Array.isArray(data?.waiting) ? data.waiting : [];
|
|
1536
|
+
const first = list[0];
|
|
1537
|
+
if (!first) return;
|
|
1538
|
+
pendingJobId.current = first.id;
|
|
1539
|
+
setPendingImport({
|
|
1540
|
+
kind: 'bundle',
|
|
1541
|
+
jobId: first.id,
|
|
1542
|
+
file: first.payload?.file ?? null,
|
|
1543
|
+
conflicts: first.payload?.conflicts ?? [],
|
|
1544
|
+
plugins: first.payload?.plugins ?? [],
|
|
1545
|
+
resumed: true,
|
|
1546
|
+
});
|
|
1547
|
+
} catch {
|
|
1548
|
+
/* 服务端还没这个接口就静默跳过 */
|
|
1549
|
+
}
|
|
1550
|
+
})();
|
|
1551
|
+
return () => {
|
|
1552
|
+
alive = false;
|
|
1553
|
+
};
|
|
1554
|
+
}, []);
|
|
1555
|
+
|
|
1556
|
+
/* 组件卸载 = 用户切走画面/关页面。
|
|
1557
|
+
*
|
|
1558
|
+
* 此时如果弹窗还开着、用户没做选择,就通知服务端取消这次导入,
|
|
1559
|
+
* 避免留下"文件已上传但不处理"的悬空任务。
|
|
1560
|
+
*/
|
|
1561
|
+
useEffect(
|
|
1562
|
+
() => () => {
|
|
1563
|
+
const id = pendingJobId.current;
|
|
1564
|
+
if (!id) return;
|
|
1565
|
+
pendingJobId.current = null;
|
|
1566
|
+
// 卸载时不能等结果,用 keepalive 尽量送达
|
|
1567
|
+
try {
|
|
1568
|
+
fetch(new URL('/plugin-pack/waiting/cancel', window.location.origin).toString(), {
|
|
1569
|
+
method: 'POST',
|
|
1570
|
+
headers: { 'content-type': 'application/json' },
|
|
1571
|
+
body: JSON.stringify({ id, reason: '用户切走了画面,未做选择' }),
|
|
1572
|
+
keepalive: true,
|
|
1573
|
+
}).catch(() => {});
|
|
1574
|
+
} catch {
|
|
1575
|
+
/* 忽略 */
|
|
1576
|
+
}
|
|
1577
|
+
},
|
|
1578
|
+
[],
|
|
1579
|
+
);
|
|
1580
|
+
|
|
1512
1581
|
const clearJobHistory = async () => {
|
|
1513
1582
|
try {
|
|
1514
1583
|
await clearJobs();
|
|
@@ -1558,8 +1627,11 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
|
|
|
1558
1627
|
const preview = await doImport({ file: uploaded.file, dryRun: true });
|
|
1559
1628
|
const conflicts = Array.isArray(preview?.conflicts) ? preview.conflicts : [];
|
|
1560
1629
|
if (conflicts.length) {
|
|
1630
|
+
// 服务端在 dryRun 时已登记了"待决策"任务,带 jobId 回来
|
|
1631
|
+
pendingJobId.current = preview?.jobId ?? null;
|
|
1561
1632
|
setPendingImport({
|
|
1562
1633
|
kind: 'bundle',
|
|
1634
|
+
jobId: preview?.jobId ?? null,
|
|
1563
1635
|
file: uploaded.file,
|
|
1564
1636
|
conflicts,
|
|
1565
1637
|
plugins: preview.plugins ?? [],
|
|
@@ -1605,6 +1677,9 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
|
|
|
1605
1677
|
const confirmImport = async (decisions) => {
|
|
1606
1678
|
const pending = pendingImport;
|
|
1607
1679
|
if (!pending) return;
|
|
1680
|
+
const jobId = pending.jobId ?? pendingJobId.current;
|
|
1681
|
+
// 明确了选择,就别让卸载钩子把它当"未决策"取消掉
|
|
1682
|
+
pendingJobId.current = null;
|
|
1608
1683
|
setPendingImport(null);
|
|
1609
1684
|
setBusy(true);
|
|
1610
1685
|
setImportResult(null);
|
|
@@ -1615,6 +1690,8 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
|
|
|
1615
1690
|
restoreData: true,
|
|
1616
1691
|
linkNodeModules: true,
|
|
1617
1692
|
decisions,
|
|
1693
|
+
// 复用同一个任务记录,界面上是同一条从"待决策"变成"完成"
|
|
1694
|
+
...(jobId ? { jobId } : {}),
|
|
1618
1695
|
});
|
|
1619
1696
|
setImportResult(result);
|
|
1620
1697
|
await load();
|
|
@@ -1626,6 +1703,20 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
|
|
|
1626
1703
|
}
|
|
1627
1704
|
};
|
|
1628
1705
|
|
|
1706
|
+
/** 用户点了取消 / 按了 Esc / 点了遮罩:视为放弃这次导入。 */
|
|
1707
|
+
const cancelImport = async () => {
|
|
1708
|
+
const id = pendingJobId.current ?? pendingImport?.jobId ?? null;
|
|
1709
|
+
pendingJobId.current = null;
|
|
1710
|
+
setPendingImport(null);
|
|
1711
|
+
if (!id) return;
|
|
1712
|
+
try {
|
|
1713
|
+
await cancelWaiting(id, '用户取消了导入');
|
|
1714
|
+
await refreshJobs();
|
|
1715
|
+
} catch {
|
|
1716
|
+
/* 取消失败不阻塞界面 */
|
|
1717
|
+
}
|
|
1718
|
+
};
|
|
1719
|
+
|
|
1629
1720
|
const runExport = async () => {
|
|
1630
1721
|
if (!chosen.length) {
|
|
1631
1722
|
setExportResult({ error: translate('export.empty') });
|
|
@@ -2173,7 +2264,14 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
|
|
|
2173
2264
|
.then((preview) => {
|
|
2174
2265
|
const conflicts = Array.isArray(preview?.conflicts) ? preview.conflicts : [];
|
|
2175
2266
|
if (conflicts.length) {
|
|
2176
|
-
|
|
2267
|
+
pendingJobId.current = preview?.jobId ?? null;
|
|
2268
|
+
setPendingImport({
|
|
2269
|
+
kind: 'bundle',
|
|
2270
|
+
jobId: preview?.jobId ?? null,
|
|
2271
|
+
file: picked,
|
|
2272
|
+
conflicts,
|
|
2273
|
+
plugins: preview?.plugins ?? [],
|
|
2274
|
+
});
|
|
2177
2275
|
return;
|
|
2178
2276
|
}
|
|
2179
2277
|
return doImport({ file: picked, dryRun: false, mode: 'merge', restoreData: true, linkNodeModules: true }).then(
|
|
@@ -2201,7 +2299,7 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
|
|
|
2201
2299
|
pending: pendingImport,
|
|
2202
2300
|
t,
|
|
2203
2301
|
onConfirm: confirmImport,
|
|
2204
|
-
onCancel:
|
|
2302
|
+
onCancel: cancelImport,
|
|
2205
2303
|
}),
|
|
2206
2304
|
);
|
|
2207
2305
|
}
|
package/lib/jobs.js
CHANGED
|
@@ -39,13 +39,13 @@ function newId() {
|
|
|
39
39
|
function prune() {
|
|
40
40
|
const now = Date.now();
|
|
41
41
|
for (const [id, job] of jobs) {
|
|
42
|
-
const finished = job.status === 'done' || job.status === 'failed';
|
|
42
|
+
const finished = job.status === 'done' || job.status === 'failed' || job.status === 'cancelled';
|
|
43
43
|
if (finished && now - job.finishedAt > KEEP_DONE_MS) jobs.delete(id);
|
|
44
44
|
}
|
|
45
45
|
// 超过上限时,先删最老的已完成任务
|
|
46
46
|
if (jobs.size > MAX_JOBS) {
|
|
47
47
|
const finished = [...jobs.values()]
|
|
48
|
-
.filter((job) => job.status === 'done' || job.status === 'failed')
|
|
48
|
+
.filter((job) => job.status === 'done' || job.status === 'failed' || job.status === 'cancelled')
|
|
49
49
|
.sort((a, b) => a.seq - b.seq);
|
|
50
50
|
for (const job of finished) {
|
|
51
51
|
if (jobs.size <= MAX_JOBS) break;
|
|
@@ -54,6 +54,94 @@ function prune() {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* 登记一个「等用户决策」的任务。
|
|
59
|
+
*
|
|
60
|
+
* 解决的问题:导入遇到重名时要弹窗问「覆盖还是跳过」,但弹窗状态原本只在
|
|
61
|
+
* 前端内存里 —— 用户切走画面再回来,组件卸载,弹窗就没了,导入悬在半空:
|
|
62
|
+
* 文件传上去了但没人处理。
|
|
63
|
+
*
|
|
64
|
+
* 所以要有一个服务端的"待决策"位,让界面切回来能捡回来。用户不决策直接
|
|
65
|
+
* 切走/关页面,就走 cancelWaiting 视为取消。
|
|
66
|
+
*
|
|
67
|
+
* @param {object} options
|
|
68
|
+
* @param {'import'} options.kind
|
|
69
|
+
* @param {string} options.title
|
|
70
|
+
* @param {object} [options.meta]
|
|
71
|
+
* @param {object} options.payload 给界面用的数据(冲突列表等)
|
|
72
|
+
* @returns {string} 任务 id
|
|
73
|
+
*/
|
|
74
|
+
export function openWaiting({ kind, title, meta = {}, payload = {} }) {
|
|
75
|
+
prune();
|
|
76
|
+
const id = newId();
|
|
77
|
+
const job = {
|
|
78
|
+
id,
|
|
79
|
+
kind,
|
|
80
|
+
title,
|
|
81
|
+
meta,
|
|
82
|
+
seq: ++sequence,
|
|
83
|
+
status: 'waiting',
|
|
84
|
+
createdAt: Date.now(),
|
|
85
|
+
finishedAt: null,
|
|
86
|
+
progress: [{ at: Date.now(), message: '等待选择覆盖或跳过' }],
|
|
87
|
+
percent: 0,
|
|
88
|
+
result: null,
|
|
89
|
+
error: null,
|
|
90
|
+
payload,
|
|
91
|
+
};
|
|
92
|
+
jobs.set(id, job);
|
|
93
|
+
return id;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 取一个待决策任务的完整数据(含 payload)。 */
|
|
97
|
+
export function getWaiting(id) {
|
|
98
|
+
const job = jobs.get(String(id ?? ''));
|
|
99
|
+
if (!job || job.status !== 'waiting') return null;
|
|
100
|
+
return { ...publicJob(job), payload: job.payload ?? {} };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 列出所有待决策任务(界面切回来要捡的那种)。 */
|
|
104
|
+
export function listWaiting() {
|
|
105
|
+
prune();
|
|
106
|
+
return [...jobs.values()]
|
|
107
|
+
.filter((job) => job.status === 'waiting')
|
|
108
|
+
.sort((a, b) => b.seq - a.seq)
|
|
109
|
+
.map((job) => ({ ...publicJob(job), payload: job.payload ?? {} }));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 取消一个待决策任务(用户切走、关页面、点取消都走这里)。
|
|
114
|
+
*
|
|
115
|
+
* @param {string} id
|
|
116
|
+
* @param {string} [reason]
|
|
117
|
+
* @returns {boolean} 是否真的取消了(不存在或已决策返回 false)
|
|
118
|
+
*/
|
|
119
|
+
export function cancelWaiting(id, reason = '已取消') {
|
|
120
|
+
const job = jobs.get(String(id ?? ''));
|
|
121
|
+
if (!job || job.status !== 'waiting') return false;
|
|
122
|
+
job.status = 'cancelled';
|
|
123
|
+
job.finishedAt = Date.now();
|
|
124
|
+
job.durationMs = job.finishedAt - job.createdAt;
|
|
125
|
+
job.error = reason;
|
|
126
|
+
job.payload = null;
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 从等待转为执行中(用户做了决策)。
|
|
132
|
+
*
|
|
133
|
+
* @param {string} id
|
|
134
|
+
* @returns {boolean}
|
|
135
|
+
*/
|
|
136
|
+
export function resumeWaiting(id) {
|
|
137
|
+
const job = jobs.get(String(id ?? ''));
|
|
138
|
+
if (!job || job.status !== 'waiting') return false;
|
|
139
|
+
job.status = 'running';
|
|
140
|
+
job.payload = null;
|
|
141
|
+
job.progress.push({ at: Date.now(), message: '开始导入' });
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
57
145
|
/**
|
|
58
146
|
* 跑一个后台任务(支持同步或异步 task)。
|
|
59
147
|
*
|
|
@@ -64,13 +152,15 @@ function prune() {
|
|
|
64
152
|
* @param {'export'|'import'|'rollback'|'import-plugin'} options.kind
|
|
65
153
|
* @param {string} options.title 人读的标题(如 "导出 5 个插件")
|
|
66
154
|
* @param {object} [options.meta] 任意附加信息(参数摘要等)
|
|
155
|
+
* @param {string} [options.jobId] 复用既有任务记录(决策后继续用同一个 id)
|
|
67
156
|
* @param {(report: (message: string, percent?: number) => void) => any | Promise<any>} options.task
|
|
68
157
|
* @returns {Promise<{ id: string, result: any, job: object }>}
|
|
69
158
|
*/
|
|
70
|
-
export async function runJob({ kind, title, meta = {}, task }) {
|
|
159
|
+
export async function runJob({ kind, title, meta = {}, task, jobId }) {
|
|
71
160
|
prune();
|
|
72
|
-
const id = newId();
|
|
73
|
-
const
|
|
161
|
+
const id = jobId ?? newId();
|
|
162
|
+
const existing = jobId ? jobs.get(String(jobId)) : null;
|
|
163
|
+
const job = existing ?? {
|
|
74
164
|
id,
|
|
75
165
|
kind,
|
|
76
166
|
title,
|
|
@@ -84,6 +174,14 @@ export async function runJob({ kind, title, meta = {}, task }) {
|
|
|
84
174
|
result: null,
|
|
85
175
|
error: null,
|
|
86
176
|
};
|
|
177
|
+
if (existing) {
|
|
178
|
+
existing.status = 'running';
|
|
179
|
+
existing.payload = null;
|
|
180
|
+
existing.error = null;
|
|
181
|
+
}
|
|
182
|
+
job.status = 'running';
|
|
183
|
+
job.payload = null;
|
|
184
|
+
job.error = null;
|
|
87
185
|
jobs.set(id, job);
|
|
88
186
|
|
|
89
187
|
const report = (message, percent) => {
|
package/lib/server.js
CHANGED
|
@@ -29,7 +29,17 @@ import { reloadClientPackage, clientStatus, localVerdict } from '../hotload.js';
|
|
|
29
29
|
import { importLocalPlugin } from './import-plugin.js';
|
|
30
30
|
import { importTarball, isTarball } from './import-tgz.js';
|
|
31
31
|
import { parseMultipart, boundaryOf, saveIncomingZip, saveIncomingDir, pruneInbox } from './inbox.js';
|
|
32
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
runJob,
|
|
34
|
+
listJobs,
|
|
35
|
+
getJob,
|
|
36
|
+
runningJobs,
|
|
37
|
+
clearFinished,
|
|
38
|
+
openWaiting,
|
|
39
|
+
getWaiting,
|
|
40
|
+
listWaiting,
|
|
41
|
+
cancelWaiting,
|
|
42
|
+
} from './jobs.js';
|
|
33
43
|
import { dshHome as resolveDshHome, profileDir as resolveProfileDir, profileName as resolveProfileName, humanBytes } from './home.js';
|
|
34
44
|
import { readJson, writeJson, isDir, isFile } from './util.js';
|
|
35
45
|
|
|
@@ -585,6 +595,7 @@ export function mountRoutes(ctx, config = {}) {
|
|
|
585
595
|
return;
|
|
586
596
|
}
|
|
587
597
|
const dryRun = Boolean(body.dryRun);
|
|
598
|
+
const decisions = body.decisions && typeof body.decisions === 'object' ? body.decisions : {};
|
|
588
599
|
const doImportBundle = (report) =>
|
|
589
600
|
importBundle({
|
|
590
601
|
file: body.file,
|
|
@@ -595,17 +606,40 @@ export function mountRoutes(ctx, config = {}) {
|
|
|
595
606
|
linkNodeModules: body.linkNodeModules !== false,
|
|
596
607
|
only: Array.isArray(body.only) && body.only.length ? body.only : null,
|
|
597
608
|
// 逐插件决策:界面弹窗后传进来,优先级高于全局 mode
|
|
598
|
-
decisions
|
|
609
|
+
decisions,
|
|
599
610
|
onProgress: report,
|
|
600
611
|
});
|
|
601
612
|
if (dryRun) {
|
|
602
|
-
|
|
613
|
+
const preview = doImportBundle(() => {});
|
|
614
|
+
const conflicts = Array.isArray(preview?.conflicts) ? preview.conflicts : [];
|
|
615
|
+
/* 有冲突且用户还没决策 → 登记成"待决策"任务。
|
|
616
|
+
* 这样用户切走画面再回来,界面能从 /plugin-pack/waiting 捡回这个弹窗;
|
|
617
|
+
* 一直不决策就走 cancel,不会留下"文件传了但没人处理"的悬空状态。 */
|
|
618
|
+
if (conflicts.length && !Object.keys(decisions).length) {
|
|
619
|
+
const jobId = openWaiting({
|
|
620
|
+
kind: 'import',
|
|
621
|
+
title: `导入整合包(${conflicts.length} 个重名待决定)`,
|
|
622
|
+
meta: { file: body.file },
|
|
623
|
+
payload: {
|
|
624
|
+
file: body.file,
|
|
625
|
+
conflicts,
|
|
626
|
+
plugins: preview.plugins ?? [],
|
|
627
|
+
restoreData: body.restoreData !== false,
|
|
628
|
+
linkNodeModules: body.linkNodeModules !== false,
|
|
629
|
+
},
|
|
630
|
+
});
|
|
631
|
+
sendJson(response, 200, { ...preview, jobId });
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
sendJson(response, 200, preview);
|
|
603
635
|
return;
|
|
604
636
|
}
|
|
605
637
|
const { id, result } = await runJob({
|
|
606
638
|
kind: 'import',
|
|
607
639
|
title: `导入整合包`,
|
|
608
640
|
meta: { file: body.file },
|
|
641
|
+
// 决策后继续用同一个任务 id,界面上就是同一条记录从"待决策"变成"完成"
|
|
642
|
+
jobId: body.jobId ? String(body.jobId) : undefined,
|
|
609
643
|
task: (report) => doImportBundle(report),
|
|
610
644
|
});
|
|
611
645
|
sendJson(response, 200, { ...result, jobId: id });
|
|
@@ -614,6 +648,48 @@ export function mountRoutes(ctx, config = {}) {
|
|
|
614
648
|
'plugin-pack: import',
|
|
615
649
|
);
|
|
616
650
|
|
|
651
|
+
/* 待决策任务:导入遇到重名时登记,界面切走再回来靠它捡回弹窗。
|
|
652
|
+
* GET /plugin-pack/waiting 列出待决策任务(含冲突列表)
|
|
653
|
+
* POST /plugin-pack/waiting/cancel 取消(用户切走/关页面/点取消)
|
|
654
|
+
*/
|
|
655
|
+
register(
|
|
656
|
+
{
|
|
657
|
+
kind: 'exact',
|
|
658
|
+
path: `${PREFIX}/waiting`,
|
|
659
|
+
handler: async (request, response) => {
|
|
660
|
+
if (request.method !== 'GET') {
|
|
661
|
+
sendJson(response, 405, { ok: false, error: '只支持 GET' });
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
const url = new URL(request.url ?? '/', 'http://localhost');
|
|
665
|
+
const id = url.searchParams.get('id');
|
|
666
|
+
if (id) {
|
|
667
|
+
sendJson(response, 200, { ok: true, waiting: getWaiting(id) });
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
sendJson(response, 200, { ok: true, waiting: listWaiting() });
|
|
671
|
+
},
|
|
672
|
+
},
|
|
673
|
+
'plugin-pack: waiting',
|
|
674
|
+
);
|
|
675
|
+
|
|
676
|
+
register(
|
|
677
|
+
{
|
|
678
|
+
kind: 'exact',
|
|
679
|
+
path: `${PREFIX}/waiting/cancel`,
|
|
680
|
+
handler: async (request, response) => {
|
|
681
|
+
if (request.method !== 'POST') {
|
|
682
|
+
sendJson(response, 405, { ok: false, error: '只支持 POST' });
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
const body = await readJsonBody(request);
|
|
686
|
+
const cancelled = cancelWaiting(body.id, body.reason || '用户取消了导入');
|
|
687
|
+
sendJson(response, 200, { ok: true, cancelled });
|
|
688
|
+
},
|
|
689
|
+
},
|
|
690
|
+
'plugin-pack: waiting-cancel',
|
|
691
|
+
);
|
|
692
|
+
|
|
617
693
|
register(
|
|
618
694
|
{
|
|
619
695
|
kind: 'exact',
|