dsh-plugin-update 0.2.0 → 0.3.0
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/CHANGELOG.md +49 -0
- package/README.md +483 -279
- package/dist/batch-run.d.ts +80 -0
- package/dist/batch-run.js +86 -0
- package/dist/batch.d.ts +111 -0
- package/dist/batch.js +183 -0
- package/dist/changelog-io.d.ts +56 -0
- package/dist/changelog-io.js +258 -0
- package/dist/changelog.d.ts +96 -0
- package/dist/changelog.js +287 -0
- package/dist/client.d.ts +16 -0
- package/dist/client.js +75 -1
- package/dist/commands.d.ts +59 -0
- package/dist/commands.js +60 -4
- package/dist/config.d.ts +48 -0
- package/dist/config.js +9 -1
- package/dist/diag.d.ts +125 -0
- package/dist/diag.js +243 -0
- package/dist/entry.d.ts +60 -0
- package/dist/entry.js +305 -0
- package/dist/gate.d.ts +35 -0
- package/dist/host-batch.d.ts +113 -0
- package/dist/host-batch.js +778 -0
- package/dist/host.d.ts +129 -0
- package/dist/host.js +479 -41
- package/dist/panel-batch.d.ts +151 -0
- package/dist/panel-batch.js +927 -0
- package/dist/panel.d.ts +256 -0
- package/dist/panel.js +1300 -0
- package/dist/ports.d.ts +208 -0
- package/dist/queue.d.ts +109 -0
- package/dist/queue.js +182 -0
- package/dist/reader.d.ts +84 -0
- package/dist/reader.js +6 -4
- package/dist/redaction.d.ts +120 -0
- package/dist/redaction.js +205 -0
- package/dist/service.d.ts +72 -0
- package/dist/service.js +100 -6
- package/dist/store.d.ts +229 -0
- package/dist/store.js +251 -12
- package/package.json +27 -2
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { type BatchSession } from './batch.js';
|
|
2
|
+
/** 查一家的结果。 */
|
|
3
|
+
export type BatchCheckOutcome =
|
|
4
|
+
/** 有新版可装。 */
|
|
5
|
+
{
|
|
6
|
+
kind: 'update';
|
|
7
|
+
version: string;
|
|
8
|
+
}
|
|
9
|
+
/** 已是最新,不需要动。 */
|
|
10
|
+
| {
|
|
11
|
+
kind: 'current';
|
|
12
|
+
}
|
|
13
|
+
/** 这一版被用户跳过(不装,但也不算失败)。 */
|
|
14
|
+
| {
|
|
15
|
+
kind: 'skipped';
|
|
16
|
+
version: string | null;
|
|
17
|
+
}
|
|
18
|
+
/** 查失败(稳定码)。 */
|
|
19
|
+
| {
|
|
20
|
+
kind: 'failed';
|
|
21
|
+
error: string;
|
|
22
|
+
};
|
|
23
|
+
/** 装一家的结果。 */
|
|
24
|
+
export type BatchInstallOutcome =
|
|
25
|
+
/** 装完;restartRequired 表示要重启宿主才生效。 */
|
|
26
|
+
{
|
|
27
|
+
kind: 'done';
|
|
28
|
+
restartRequired: boolean;
|
|
29
|
+
}
|
|
30
|
+
/** 装失败(稳定码)。 */
|
|
31
|
+
| {
|
|
32
|
+
kind: 'failed';
|
|
33
|
+
error: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* 已入队:全局锁被别家占着(宿主的 update-busy),本家排上了但还没轮到。
|
|
37
|
+
* **不是失败**——这一行的相位回到 ready(远端版本留着),本轮推动到此为止,
|
|
38
|
+
* 下次推动(或宿主 drain)会从它接着走。这是「一个一个加入队列」那条路的正解:
|
|
39
|
+
* 用户点「加入队列」= 递一个意图进队列,而不是报一次错。
|
|
40
|
+
*/
|
|
41
|
+
| {
|
|
42
|
+
kind: 'queued';
|
|
43
|
+
position?: number | null;
|
|
44
|
+
};
|
|
45
|
+
/** 驱动器要用到的传输与落盘(调用方注入)。 */
|
|
46
|
+
export interface BatchRunDeps {
|
|
47
|
+
/** 查这一家的远端版本。 */
|
|
48
|
+
check: (key: string) => Promise<BatchCheckOutcome>;
|
|
49
|
+
/** 装这一家:**必须**带上会话给的幂等 requestId(重复提交由队列按它去重)。 */
|
|
50
|
+
install: (key: string, requestId: string, version: string) => Promise<BatchInstallOutcome>;
|
|
51
|
+
/** 会话落盘(每一步之后都调一次:这就是「断点续跑」的全部秘密)。 */
|
|
52
|
+
save: (session: BatchSession) => Promise<void> | void;
|
|
53
|
+
now: () => number;
|
|
54
|
+
}
|
|
55
|
+
/** 驱动器读数:给日志、面板与门禁用。 */
|
|
56
|
+
export interface BatchRunStep {
|
|
57
|
+
key: string;
|
|
58
|
+
action: 'check' | 'install';
|
|
59
|
+
/** 这一步把该行推到了哪个相位。 */
|
|
60
|
+
phase: string;
|
|
61
|
+
/** 失败稳定码(成功为 null)。 */
|
|
62
|
+
error: string | null;
|
|
63
|
+
}
|
|
64
|
+
export interface BatchRunResult {
|
|
65
|
+
session: BatchSession;
|
|
66
|
+
steps: BatchRunStep[];
|
|
67
|
+
/** 停下来的原因:做完了/按策略遇错停/步数用完/没活可干/**排上队但还没轮到**。 */
|
|
68
|
+
stoppedBecause: 'finished' | 'stopped-after-failure' | 'max-steps' | 'empty' | 'queued';
|
|
69
|
+
}
|
|
70
|
+
/** 驱动器选项:`maxSteps` 给 1 就是「只推进一步」,便于宿主按自己的节奏推进(drain)。 */
|
|
71
|
+
export interface BatchRunOptions {
|
|
72
|
+
maxSteps?: number;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 把会话往前推,直到:收尾/按 stopOnFailure 遇错停下/步数用完/没活可干。
|
|
76
|
+
* 每一步都先落盘再继续——中途被打断也不会丢进度。
|
|
77
|
+
*/
|
|
78
|
+
export declare function runBatch(session: BatchSession, deps: BatchRunDeps, options?: BatchRunOptions): Promise<BatchRunResult>;
|
|
79
|
+
/** 会话是否已收尾(面板与日志用同一口径)。 */
|
|
80
|
+
export declare function batchRunFinished(session: BatchSession): boolean;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// AUTO-GENERATED by node build.mjs — DO NOT EDIT. Source: src/batch-run.ts
|
|
2
|
+
import {
|
|
3
|
+
batchProgress,
|
|
4
|
+
isTerminalPhase,
|
|
5
|
+
markBatchEntry,
|
|
6
|
+
nextBatchKey
|
|
7
|
+
} from "./batch.js";
|
|
8
|
+
async function runBatch(session, deps, options = {}) {
|
|
9
|
+
const maxSteps = typeof options.maxSteps === "number" && Number.isFinite(options.maxSteps) && options.maxSteps > 0 ? Math.floor(options.maxSteps) : Number.POSITIVE_INFINITY;
|
|
10
|
+
const steps = [];
|
|
11
|
+
let current = session;
|
|
12
|
+
let used = 0;
|
|
13
|
+
const persist = async (next) => {
|
|
14
|
+
current = next;
|
|
15
|
+
await deps.save(next);
|
|
16
|
+
return next;
|
|
17
|
+
};
|
|
18
|
+
const budgetLeft = () => used < maxSteps;
|
|
19
|
+
while (true) {
|
|
20
|
+
const key = nextBatchKey(current);
|
|
21
|
+
if (key === null) {
|
|
22
|
+
return { session: current, steps, stoppedBecause: stoppedBecause(current) };
|
|
23
|
+
}
|
|
24
|
+
const entry = current.entries.find((e) => e.key === key);
|
|
25
|
+
if (!entry) return { session: current, steps, stoppedBecause: "empty" };
|
|
26
|
+
if (!budgetLeft()) return { session: current, steps, stoppedBecause: "max-steps" };
|
|
27
|
+
let version = entry.phase === "ready" && entry.targetVersion ? entry.targetVersion : null;
|
|
28
|
+
if (version === null) {
|
|
29
|
+
await persist(markBatchEntry(current, key, { phase: "checking", error: null }, deps.now()).session);
|
|
30
|
+
const checked = await deps.check(key);
|
|
31
|
+
used += 1;
|
|
32
|
+
if (checked.kind === "failed") {
|
|
33
|
+
await persist(markBatchEntry(current, key, { phase: "failed", error: checked.error }, deps.now()).session);
|
|
34
|
+
steps.push({ key, action: "check", phase: "failed", error: checked.error });
|
|
35
|
+
if (current.stopOnFailure) return { session: current, steps, stoppedBecause: "stopped-after-failure" };
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (checked.kind === "current") {
|
|
39
|
+
await persist(markBatchEntry(current, key, { phase: "current", targetVersion: null }, deps.now()).session);
|
|
40
|
+
steps.push({ key, action: "check", phase: "current", error: null });
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (checked.kind === "skipped") {
|
|
44
|
+
await persist(markBatchEntry(current, key, { phase: "skipped", targetVersion: checked.version }, deps.now()).session);
|
|
45
|
+
steps.push({ key, action: "check", phase: "skipped", error: null });
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
await persist(markBatchEntry(current, key, { phase: "ready", targetVersion: checked.version, error: null }, deps.now()).session);
|
|
49
|
+
steps.push({ key, action: "check", phase: "ready", error: null });
|
|
50
|
+
version = checked.version;
|
|
51
|
+
if (!budgetLeft()) return { session: current, steps, stoppedBecause: "max-steps" };
|
|
52
|
+
}
|
|
53
|
+
if (version === null) continue;
|
|
54
|
+
await persist(markBatchEntry(current, key, { phase: "installing" }, deps.now()).session);
|
|
55
|
+
const installed = await deps.install(key, entry.requestId, version);
|
|
56
|
+
used += 1;
|
|
57
|
+
if (installed.kind === "queued") {
|
|
58
|
+
await persist(markBatchEntry(current, key, { phase: "ready", error: null }, deps.now()).session);
|
|
59
|
+
steps.push({ key, action: "install", phase: "queued", error: null });
|
|
60
|
+
return { session: current, steps, stoppedBecause: "queued" };
|
|
61
|
+
}
|
|
62
|
+
if (installed.kind === "failed") {
|
|
63
|
+
await persist(markBatchEntry(current, key, { phase: "failed", error: installed.error }, deps.now()).session);
|
|
64
|
+
steps.push({ key, action: "install", phase: "failed", error: installed.error });
|
|
65
|
+
if (current.stopOnFailure) return { session: current, steps, stoppedBecause: "stopped-after-failure" };
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
await persist(
|
|
69
|
+
markBatchEntry(current, key, { phase: "done", restartRequired: installed.restartRequired, error: null }, deps.now()).session
|
|
70
|
+
);
|
|
71
|
+
steps.push({ key, action: "install", phase: "done", error: null });
|
|
72
|
+
}
|
|
73
|
+
return { session: current, steps, stoppedBecause: "max-steps" };
|
|
74
|
+
}
|
|
75
|
+
function stoppedBecause(session) {
|
|
76
|
+
if (session.entries.length === 0) return "empty";
|
|
77
|
+
if (session.entries.every((e) => isTerminalPhase(e.phase))) return "finished";
|
|
78
|
+
return "stopped-after-failure";
|
|
79
|
+
}
|
|
80
|
+
function batchRunFinished(session) {
|
|
81
|
+
return batchProgress(session).finished;
|
|
82
|
+
}
|
|
83
|
+
export {
|
|
84
|
+
batchRunFinished,
|
|
85
|
+
runBatch
|
|
86
|
+
};
|
package/dist/batch.d.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/** 会话账本版本号:收到别的数字按空会话处理,不抛错(与队列同一纪律)。 */
|
|
2
|
+
export declare const BATCH_SESSION_VERSION = 1;
|
|
3
|
+
/** 一个目标在一轮批量里的相位。 */
|
|
4
|
+
export type BatchPhase =
|
|
5
|
+
/** 还没轮到你 */
|
|
6
|
+
'pending'
|
|
7
|
+
/** 正在查远端版本 */
|
|
8
|
+
| 'checking'
|
|
9
|
+
/** 查到了、有新版可装,等排队 */
|
|
10
|
+
| 'ready'
|
|
11
|
+
/** 已提交安装,正在进行 */
|
|
12
|
+
| 'installing'
|
|
13
|
+
/** 本家已是最新,不需要动 */
|
|
14
|
+
| 'current'
|
|
15
|
+
/** 装完了(可能还要重启宿主才生效,见 restartRequired) */
|
|
16
|
+
| 'done'
|
|
17
|
+
/** 这一家失败了(error 给稳定码),按策略决定要不要继续下一家 */
|
|
18
|
+
| 'failed'
|
|
19
|
+
/** 这一家被跳过(用户跳过该版本) */
|
|
20
|
+
| 'skipped';
|
|
21
|
+
/** 终态:不会再变化的相位。 */
|
|
22
|
+
export declare function isTerminalPhase(phase: BatchPhase): boolean;
|
|
23
|
+
/** 会话里的一行。 */
|
|
24
|
+
export interface BatchEntry {
|
|
25
|
+
/** 目标稳定键(会话内唯一)。 */
|
|
26
|
+
key: string;
|
|
27
|
+
phase: BatchPhase;
|
|
28
|
+
/** 幂等编号:同一会话同一目标恒定,重复提交不重复装(队列按它占位)。 */
|
|
29
|
+
requestId: string;
|
|
30
|
+
/** 远端最新版(查过才有)。 */
|
|
31
|
+
targetVersion: string | null;
|
|
32
|
+
/** 装完之后是否要重启宿主才生效(pending-restart)。 */
|
|
33
|
+
restartRequired: boolean;
|
|
34
|
+
/** 失败稳定码(认不出给 internal);成功恒为 null。 */
|
|
35
|
+
error: string | null;
|
|
36
|
+
/** 这一行最后一次变化的时刻。 */
|
|
37
|
+
updatedAt: number;
|
|
38
|
+
}
|
|
39
|
+
/** 一轮批量更新的会话账本(落盘形状)。 */
|
|
40
|
+
export interface BatchSession {
|
|
41
|
+
version: 1;
|
|
42
|
+
/** 会话编号(调用方给:时间戳 + 随机串之类,只作标识用)。 */
|
|
43
|
+
id: string;
|
|
44
|
+
/** 「自己」的键:排序时排最后;没有就传 null。 */
|
|
45
|
+
selfKey: string | null;
|
|
46
|
+
/** 一家失败后是否停下(默认 false:继续下一家)。 */
|
|
47
|
+
stopOnFailure: boolean;
|
|
48
|
+
/** 目标处理顺序(自更新安全:selfKey 在最后)。 */
|
|
49
|
+
order: string[];
|
|
50
|
+
entries: BatchEntry[];
|
|
51
|
+
createdAt: number;
|
|
52
|
+
updatedAt: number;
|
|
53
|
+
}
|
|
54
|
+
/** 会话里进度的读数。 */
|
|
55
|
+
export interface BatchProgress {
|
|
56
|
+
total: number;
|
|
57
|
+
done: number;
|
|
58
|
+
failed: number;
|
|
59
|
+
skipped: number;
|
|
60
|
+
current: number;
|
|
61
|
+
pending: number;
|
|
62
|
+
/** 终态齐了就 true(失败也算收尾——不阻塞整轮的结束)。 */
|
|
63
|
+
finished: boolean;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 目标顺序:selfKey 排最后,其余保持传入顺序(稳定排序,不许键乱序导致顺序漂移)。
|
|
67
|
+
* 传进来的键去重后仍为空即返回空数组,调用方按「没有可做的」处理。
|
|
68
|
+
*/
|
|
69
|
+
export declare function orderTargets(keys: readonly string[], selfKey?: string | null): string[];
|
|
70
|
+
/** 会话内某一行的幂等编号:会话 + 目标 ⇒ 恒定(重跑同一会话不会重复装)。 */
|
|
71
|
+
export declare function batchRequestId(sessionId: string, key: string): string;
|
|
72
|
+
/** 开一轮会话:给定目标键与「自己」的键,按自更新安全排好序,每行落一个幂等编号。 */
|
|
73
|
+
export declare function createBatchSession(args: {
|
|
74
|
+
id: string;
|
|
75
|
+
keys: readonly string[];
|
|
76
|
+
selfKey?: string | null;
|
|
77
|
+
stopOnFailure?: boolean;
|
|
78
|
+
now: number;
|
|
79
|
+
}): BatchSession;
|
|
80
|
+
/** 空会话(文件缺失或损坏时的回退:坏账本不能挡更新,与队列同一纪律)。 */
|
|
81
|
+
export declare function emptyBatchSession(): BatchSession;
|
|
82
|
+
/** 把外面读到的值验成可用的会话:错了不抛错,回空会话。 */
|
|
83
|
+
export declare function normalizeBatchSession(raw: unknown): BatchSession;
|
|
84
|
+
/** 按会话顺序取某一行。 */
|
|
85
|
+
export declare function batchEntryOf(session: BatchSession, key: string): BatchEntry | null;
|
|
86
|
+
/**
|
|
87
|
+
* 下一个该处理的目标键:
|
|
88
|
+
* - 按会话顺序找第一个非终态行;
|
|
89
|
+
* - `stopOnFailure` 为真且已有失败行时返回 null(停下,等人处理);
|
|
90
|
+
* - 全部收尾返回 null。
|
|
91
|
+
*/
|
|
92
|
+
export declare function nextBatchKey(session: BatchSession): string | null;
|
|
93
|
+
/** 改一行:返回新会话(不可变),键不存在时原样返回且 changed=false。 */
|
|
94
|
+
export declare function markBatchEntry(session: BatchSession, key: string, patch: Partial<Omit<BatchEntry, 'key' | 'requestId'>>, now: number): {
|
|
95
|
+
session: BatchSession;
|
|
96
|
+
changed: boolean;
|
|
97
|
+
};
|
|
98
|
+
/** 进度读数(面板与门禁共用同一份口径)。 */
|
|
99
|
+
export declare function batchProgress(session: BatchSession): BatchProgress;
|
|
100
|
+
/** 一轮结束:每一行都进了终态(失败也算收尾——失败不该让整轮永远挂着)。 */
|
|
101
|
+
export declare function isBatchFinished(session: BatchSession): boolean;
|
|
102
|
+
/** 要重启才生效的那几家(批量装完给一条总账,别让用户逐家点)。 */
|
|
103
|
+
export declare function needsRestartKeys(session: BatchSession): string[];
|
|
104
|
+
/** 失败的那几家(给人话总账用)。 */
|
|
105
|
+
export declare function failedKeys(session: BatchSession): string[];
|
|
106
|
+
/**
|
|
107
|
+
* 断点续跑:把「进行中」的行退回 pending——重启/重载后不知道它们走到哪了,
|
|
108
|
+
* 由调用方重新查一次真实状态再决定装不装(幂等编号不变,重复提交不会重复装)。
|
|
109
|
+
* 终态行(done/current/failed/skipped)一律保留:**已完成的不重装**。
|
|
110
|
+
*/
|
|
111
|
+
export declare function resumeBatchSession(session: BatchSession, now: number): BatchSession;
|
package/dist/batch.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// AUTO-GENERATED by node build.mjs — DO NOT EDIT. Source: src/batch.ts
|
|
2
|
+
const BATCH_SESSION_VERSION = 1;
|
|
3
|
+
function isTerminalPhase(phase) {
|
|
4
|
+
return phase === "done" || phase === "failed" || phase === "skipped" || phase === "current";
|
|
5
|
+
}
|
|
6
|
+
function asMillis(value, fallback) {
|
|
7
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
8
|
+
}
|
|
9
|
+
function isNonEmptyString(value) {
|
|
10
|
+
return typeof value === "string" && value.length > 0;
|
|
11
|
+
}
|
|
12
|
+
const PHASES = ["pending", "checking", "ready", "installing", "current", "done", "failed", "skipped"];
|
|
13
|
+
function asPhase(value) {
|
|
14
|
+
return typeof value === "string" && PHASES.includes(value) ? value : "pending";
|
|
15
|
+
}
|
|
16
|
+
function orderTargets(keys, selfKey) {
|
|
17
|
+
const seen = /* @__PURE__ */ new Set();
|
|
18
|
+
const others = [];
|
|
19
|
+
let self = null;
|
|
20
|
+
for (const key of keys) {
|
|
21
|
+
if (!isNonEmptyString(key) || seen.has(key)) continue;
|
|
22
|
+
seen.add(key);
|
|
23
|
+
if (selfKey && key === selfKey) self = key;
|
|
24
|
+
else others.push(key);
|
|
25
|
+
}
|
|
26
|
+
return self ? [...others, self] : others;
|
|
27
|
+
}
|
|
28
|
+
function batchRequestId(sessionId, key) {
|
|
29
|
+
return "batch:" + sessionId + ":" + key;
|
|
30
|
+
}
|
|
31
|
+
function createBatchSession(args) {
|
|
32
|
+
const id = isNonEmptyString(args.id) ? args.id : "batch";
|
|
33
|
+
const selfKey = isNonEmptyString(args.selfKey) ? args.selfKey : null;
|
|
34
|
+
const order = orderTargets(args.keys, selfKey);
|
|
35
|
+
const now = asMillis(args.now, 0);
|
|
36
|
+
const entries = order.map((key) => ({
|
|
37
|
+
key,
|
|
38
|
+
phase: "pending",
|
|
39
|
+
requestId: batchRequestId(id, key),
|
|
40
|
+
targetVersion: null,
|
|
41
|
+
restartRequired: false,
|
|
42
|
+
error: null,
|
|
43
|
+
updatedAt: now
|
|
44
|
+
}));
|
|
45
|
+
return {
|
|
46
|
+
version: BATCH_SESSION_VERSION,
|
|
47
|
+
id,
|
|
48
|
+
selfKey,
|
|
49
|
+
stopOnFailure: args.stopOnFailure === true,
|
|
50
|
+
order,
|
|
51
|
+
entries,
|
|
52
|
+
createdAt: now,
|
|
53
|
+
updatedAt: now
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function emptyBatchSession() {
|
|
57
|
+
return {
|
|
58
|
+
version: BATCH_SESSION_VERSION,
|
|
59
|
+
id: "",
|
|
60
|
+
selfKey: null,
|
|
61
|
+
stopOnFailure: false,
|
|
62
|
+
order: [],
|
|
63
|
+
entries: [],
|
|
64
|
+
createdAt: 0,
|
|
65
|
+
updatedAt: 0
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function normalizeBatchSession(raw) {
|
|
69
|
+
try {
|
|
70
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return emptyBatchSession();
|
|
71
|
+
const input = raw;
|
|
72
|
+
if (input["version"] !== BATCH_SESSION_VERSION) return emptyBatchSession();
|
|
73
|
+
const id = isNonEmptyString(input["id"]) ? input["id"] : "";
|
|
74
|
+
const selfKey = isNonEmptyString(input["selfKey"]) ? input["selfKey"] : null;
|
|
75
|
+
const entries = [];
|
|
76
|
+
const seen = /* @__PURE__ */ new Set();
|
|
77
|
+
const rawEntries = input["entries"];
|
|
78
|
+
if (Array.isArray(rawEntries)) {
|
|
79
|
+
for (const item of rawEntries) {
|
|
80
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
|
|
81
|
+
const e = item;
|
|
82
|
+
const key = e["key"];
|
|
83
|
+
if (!isNonEmptyString(key) || seen.has(key)) continue;
|
|
84
|
+
seen.add(key);
|
|
85
|
+
const requestId = isNonEmptyString(e["requestId"]) ? e["requestId"] : batchRequestId(id, key);
|
|
86
|
+
entries.push({
|
|
87
|
+
key,
|
|
88
|
+
phase: asPhase(e["phase"]),
|
|
89
|
+
requestId,
|
|
90
|
+
targetVersion: isNonEmptyString(e["targetVersion"]) ? e["targetVersion"] : null,
|
|
91
|
+
restartRequired: e["restartRequired"] === true,
|
|
92
|
+
error: isNonEmptyString(e["error"]) ? e["error"] : null,
|
|
93
|
+
updatedAt: asMillis(e["updatedAt"], 0)
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const order = entries.map((e) => e.key);
|
|
98
|
+
return {
|
|
99
|
+
version: BATCH_SESSION_VERSION,
|
|
100
|
+
id,
|
|
101
|
+
selfKey: selfKey && seen.has(selfKey) ? selfKey : null,
|
|
102
|
+
stopOnFailure: input["stopOnFailure"] === true,
|
|
103
|
+
order,
|
|
104
|
+
entries,
|
|
105
|
+
createdAt: asMillis(input["createdAt"], 0),
|
|
106
|
+
updatedAt: asMillis(input["updatedAt"], 0)
|
|
107
|
+
};
|
|
108
|
+
} catch {
|
|
109
|
+
return emptyBatchSession();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function batchEntryOf(session, key) {
|
|
113
|
+
return session.entries.find((e) => e.key === key) ?? null;
|
|
114
|
+
}
|
|
115
|
+
function nextBatchKey(session) {
|
|
116
|
+
if (session.stopOnFailure && session.entries.some((e) => e.phase === "failed")) return null;
|
|
117
|
+
for (const key of session.order) {
|
|
118
|
+
const entry = session.entries.find((e) => e.key === key);
|
|
119
|
+
if (entry && !isTerminalPhase(entry.phase)) return key;
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
function markBatchEntry(session, key, patch, now) {
|
|
124
|
+
const at = session.entries.findIndex((e) => e.key === key);
|
|
125
|
+
if (at < 0) return { session, changed: false };
|
|
126
|
+
const next = { ...session.entries[at], ...patch, key: session.entries[at].key, requestId: session.entries[at].requestId, updatedAt: asMillis(now, session.entries[at].updatedAt) };
|
|
127
|
+
const entries = [...session.entries.slice(0, at), next, ...session.entries.slice(at + 1)];
|
|
128
|
+
return { session: { ...session, entries, updatedAt: next.updatedAt }, changed: true };
|
|
129
|
+
}
|
|
130
|
+
function batchProgress(session) {
|
|
131
|
+
const total = session.entries.length;
|
|
132
|
+
let done = 0;
|
|
133
|
+
let failed = 0;
|
|
134
|
+
let skipped = 0;
|
|
135
|
+
let current = 0;
|
|
136
|
+
let pending = 0;
|
|
137
|
+
for (const entry of session.entries) {
|
|
138
|
+
if (entry.phase === "done" || entry.phase === "current") done += 1;
|
|
139
|
+
else if (entry.phase === "failed") failed += 1;
|
|
140
|
+
else if (entry.phase === "skipped") skipped += 1;
|
|
141
|
+
else if (entry.phase === "installing" || entry.phase === "checking" || entry.phase === "ready") current += 1;
|
|
142
|
+
else pending += 1;
|
|
143
|
+
}
|
|
144
|
+
return { total, done, failed, skipped, current, pending, finished: isBatchFinished(session) };
|
|
145
|
+
}
|
|
146
|
+
function isBatchFinished(session) {
|
|
147
|
+
return session.entries.length > 0 && session.entries.every((e) => isTerminalPhase(e.phase));
|
|
148
|
+
}
|
|
149
|
+
function needsRestartKeys(session) {
|
|
150
|
+
return session.entries.filter((e) => e.restartRequired).map((e) => e.key);
|
|
151
|
+
}
|
|
152
|
+
function failedKeys(session) {
|
|
153
|
+
return session.entries.filter((e) => e.phase === "failed").map((e) => e.key);
|
|
154
|
+
}
|
|
155
|
+
function resumeBatchSession(session, now) {
|
|
156
|
+
const at = asMillis(now, session.updatedAt);
|
|
157
|
+
let changed = false;
|
|
158
|
+
const entries = session.entries.map((entry) => {
|
|
159
|
+
if (entry.phase === "checking" || entry.phase === "installing") {
|
|
160
|
+
changed = true;
|
|
161
|
+
return { ...entry, phase: "pending", error: null, updatedAt: at };
|
|
162
|
+
}
|
|
163
|
+
return entry;
|
|
164
|
+
});
|
|
165
|
+
return changed ? { ...session, entries, updatedAt: at } : session;
|
|
166
|
+
}
|
|
167
|
+
export {
|
|
168
|
+
BATCH_SESSION_VERSION,
|
|
169
|
+
batchEntryOf,
|
|
170
|
+
batchProgress,
|
|
171
|
+
batchRequestId,
|
|
172
|
+
createBatchSession,
|
|
173
|
+
emptyBatchSession,
|
|
174
|
+
failedKeys,
|
|
175
|
+
isBatchFinished,
|
|
176
|
+
isTerminalPhase,
|
|
177
|
+
markBatchEntry,
|
|
178
|
+
needsRestartKeys,
|
|
179
|
+
nextBatchKey,
|
|
180
|
+
normalizeBatchSession,
|
|
181
|
+
orderTargets,
|
|
182
|
+
resumeBatchSession
|
|
183
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/changelog-io.ts —— 更新日志的 Node 侧读写(#23)。
|
|
3
|
+
*
|
|
4
|
+
* 纯解析在 `src/changelog.ts`(零导入,两边可进);
|
|
5
|
+
* 本文件只做跑腿(读盘、联网、解包、验 integrity),仅 Node 侧导入,
|
|
6
|
+
* 面板(浏览器闭包)永不导入本文件。
|
|
7
|
+
*
|
|
8
|
+
* 两种读取时机(#11 §4 第 5 条):
|
|
9
|
+
* - 已装版离线读:`node_modules/<目标包>/CHANGELOG.md`(与版本 pin 绑定,零新增联网);
|
|
10
|
+
* - 新版按需取:新版 tarball 内同名文件,复用官方源 + integrity 校验;
|
|
11
|
+
* 取不到一律回 null(调用方渲染中性提示,不挡安装、不写 blockedReason)。
|
|
12
|
+
*/
|
|
13
|
+
/** tarball 下载上限(字节,压缩态):超限直接回落中性提示,不解包、不OOM。 */
|
|
14
|
+
export declare const CHANGELOG_TARBALL_MAX_BYTES: number;
|
|
15
|
+
/**
|
|
16
|
+
* tar 解压后上限(字节):gzip 炸弹墙(#23 对抗复查补记——此前只卡压缩态,
|
|
17
|
+
* 恶意小包可炸出 GB 级 tar 直接 OOM 宿主)。超限回 null,调用方渲染中性提示。
|
|
18
|
+
*/
|
|
19
|
+
export declare const CHANGELOG_TAR_MAX_BYTES: number;
|
|
20
|
+
/** 日志文本上限(字节):超限截断后返回,不断面板。 */
|
|
21
|
+
export declare const CHANGELOG_FETCH_MAX_BYTES: number;
|
|
22
|
+
/** 默认官方源(与 service.NPM_REGISTRY 同值,显式传参优先)。 */
|
|
23
|
+
export declare const CHANGELOG_DEFAULT_REGISTRY = "https://registry.npmjs.org/";
|
|
24
|
+
/**
|
|
25
|
+
* 已装版离线读(best-effort,失败回 null):
|
|
26
|
+
*读 `<目标包目录>/CHANGELOG.md`,空文件回 null(调用方给中性提示)。
|
|
27
|
+
*/
|
|
28
|
+
export declare function readInstalledChangelogText(targetPackageDir: unknown, opts?: {
|
|
29
|
+
maxBytes?: number;
|
|
30
|
+
}): Promise<string | null>;
|
|
31
|
+
export interface ReleaseChangelogRef {
|
|
32
|
+
/** tarball 地址(service.fetchNpmRelease 回的 tarball 原样)。 */
|
|
33
|
+
tarball: unknown;
|
|
34
|
+
/** integrity 串(`sha512-…`,复用官方源校验)。 */
|
|
35
|
+
integrity: unknown;
|
|
36
|
+
/** 该发行版本号(用于严格路径校验,可选)。 */
|
|
37
|
+
version?: unknown;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 从已解压的 tar 包里取同名文件(best-effort,找不到回 null)。
|
|
41
|
+
* 只收普通文件(typeflag `0`/`\0`),目录与特殊文件一律跳过。
|
|
42
|
+
*/
|
|
43
|
+
export declare function extractChangelogFromTar(tarBytes: Uint8Array, maxBytes?: number): string | null;
|
|
44
|
+
/**
|
|
45
|
+
* 新版按需取(best-effort,任何一步失败回 null,调用方渲染中性提示):
|
|
46
|
+
* 取 tarball → 验 integrity(官方源同一信任根)→ gunzip → 取包内同名文件。
|
|
47
|
+
* 同源校验:tarball 与 registryUrl 同 origin、无用户信息、无 search/hash;
|
|
48
|
+
* 有目标包名 + 版本时再要求精确路径(与 service.fetchNpmRelease 同形)。
|
|
49
|
+
*/
|
|
50
|
+
export declare function fetchReleaseChangelogText(fetchImpl: unknown, release: ReleaseChangelogRef | null | undefined, opts?: {
|
|
51
|
+
targetPackageName?: string;
|
|
52
|
+
registryUrl?: string;
|
|
53
|
+
timeoutMs?: number;
|
|
54
|
+
maxTarballBytes?: number;
|
|
55
|
+
maxChangelogBytes?: number;
|
|
56
|
+
}): Promise<string | null>;
|