dsh-plugin-update 0.2.0 → 0.3.1
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 +54 -0
- package/README.md +511 -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/http.d.ts +127 -0
- package/dist/http.js +300 -0
- 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 +31 -2
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { assertPollInterval } from './client.js';
|
|
2
|
+
import { type UpdatePanelContainer, type UpdatePanelController, type UpdatePanelOptions } from './panel.js';
|
|
3
|
+
import { type UpdateEntryController, type UpdateEntryOptions } from './entry.js';
|
|
4
|
+
import { type BatchPanelContainer, type BatchPanelController, type BatchPanelOptions } from './panel-batch.js';
|
|
5
|
+
/** 传输失败码:永不进面板 14 稳定码分支,只走抛异常通道。 */
|
|
6
|
+
export declare const HTTP_TRANSPORT_FAILED = "http-transport-failed";
|
|
7
|
+
/** 抓取超时默认:查 15s / 装 16min(#30 冻结,调用方可覆写 + AbortSignal 透入)。 */
|
|
8
|
+
export declare const DEFAULT_HTTP_CHECK_TIMEOUT_MS = 15000;
|
|
9
|
+
export declare const DEFAULT_HTTP_INSTALL_TIMEOUT_MS: number;
|
|
10
|
+
export declare const DEFAULT_HTTP_STATUS_TIMEOUT_MS = 15000;
|
|
11
|
+
/** 单插件三电话动作名(与 src/config.ts PHONE_ACTIONS 同字面,不另起字面)。 */
|
|
12
|
+
export declare const HTTP_PHONE_ACTIONS: readonly ["updateStatus", "updateCheck", "updateInstall"];
|
|
13
|
+
export type HttpPhoneAction = (typeof HTTP_PHONE_ACTIONS)[number];
|
|
14
|
+
/** 批量五电话动作名(与 src/panel-batch.ts buildBatchPhoneNames 同字面,不另起字面;同一内核)。 */
|
|
15
|
+
export declare const HTTP_BATCH_ACTIONS: readonly ["batchStatus", "batchCheck", "batchInstall", "batchResume", "batchCancel"];
|
|
16
|
+
export type HttpBatchAction = (typeof HTTP_BATCH_ACTIONS)[number];
|
|
17
|
+
/** 逐电话覆写表:键为全电话名(prefix.动作)或短动作名;未知键拒绝。 */
|
|
18
|
+
export type HttpRoutes = Record<string, string>;
|
|
19
|
+
/** 抓取函数形状:与 globalThis.fetch 同形(测试给内存假件)。 */
|
|
20
|
+
export type HttpFetch = (url: string, init: {
|
|
21
|
+
method: string;
|
|
22
|
+
headers: Record<string, string>;
|
|
23
|
+
body: string;
|
|
24
|
+
signal?: unknown;
|
|
25
|
+
}) => Promise<unknown>;
|
|
26
|
+
export interface HttpTimeouts {
|
|
27
|
+
/** 查/状态超时毫秒(默认 15s)。 */
|
|
28
|
+
checkMs?: number;
|
|
29
|
+
/** 装超时毫秒(默认 16min)。 */
|
|
30
|
+
installMs?: number;
|
|
31
|
+
/** 状态超时毫秒(默认 15s,与查同值)。 */
|
|
32
|
+
statusMs?: number;
|
|
33
|
+
}
|
|
34
|
+
export interface CreateHttpCallOptions {
|
|
35
|
+
/** 单插件电话前缀(与宿主侧一致;电话名从它算出,不写字面量)。 */
|
|
36
|
+
prefix: string;
|
|
37
|
+
/** 批量电话前缀(独立于单插件前缀;给了即同时校验批量五电话键,同一内核)。 */
|
|
38
|
+
batchPrefix?: string;
|
|
39
|
+
/** 网关根:默认路径 = 网关根 + 全电话名(首尾斜杠归一)。 */
|
|
40
|
+
baseUrl: string;
|
|
41
|
+
/** 电话级覆写表(routes[全电话名] ?? routes[短动作] ?? 默认拼接)。 */
|
|
42
|
+
routes?: HttpRoutes;
|
|
43
|
+
/** 抓取注入点(不传即 globalThis.fetch;测试给假件)。 */
|
|
44
|
+
fetch?: HttpFetch;
|
|
45
|
+
/** 同上别名(与 fetch 二选一,fetch 优先)。 */
|
|
46
|
+
fetchImpl?: HttpFetch;
|
|
47
|
+
/** 超时覆写:数字即全电话统一;对象即按电话分设。 */
|
|
48
|
+
timeout?: number | HttpTimeouts;
|
|
49
|
+
/** 同上别名(与 timeout 二选一,timeout 优先)。 */
|
|
50
|
+
timeoutMs?: number | HttpTimeouts;
|
|
51
|
+
}
|
|
52
|
+
/** 覆写表未知键拒绝(创建即抛, fail-fast)。 */
|
|
53
|
+
export declare function assertHttpRoutes(routes: HttpRoutes | undefined, prefix: string, batchPrefix?: string): void;
|
|
54
|
+
/** 电话名 → POST 路径(默认拼接 + 电话级覆写 + 缺键回落 + 斜杠归一)。 */
|
|
55
|
+
export declare function resolveHttpPath(options: {
|
|
56
|
+
prefix: string;
|
|
57
|
+
batchPrefix?: string;
|
|
58
|
+
baseUrl: string;
|
|
59
|
+
routes?: HttpRoutes;
|
|
60
|
+
}, phoneName: string): string;
|
|
61
|
+
/** 传输失败:断网/非 2xx/JSON 非法/超时,永不进稳定码分支。 */
|
|
62
|
+
export declare function httpTransportFailed(reason: string, detail?: unknown): Error & {
|
|
63
|
+
code: string;
|
|
64
|
+
detail: string;
|
|
65
|
+
};
|
|
66
|
+
/** diag 宽容读:只留 16 键(DIAG_KEYS),detail 脱敏;diagnostic 原话丢弃。 */
|
|
67
|
+
export declare function pickHttpDiag(raw: unknown): Record<string, unknown> | undefined;
|
|
68
|
+
/** 回包整形:diagnostic 丢弃,diag 过滤,其余(queue/env/session/rows/progress/未知键)原样透传。
|
|
69
|
+
*
|
|
70
|
+
* 批量行内同理:rows[i].diagnostic 丢弃,rows[i].diag 只留 16 键且 detail 脱敏;
|
|
71
|
+
* 行内 queue/manual/snapshot/phoneNames 等原样透传(排队/续跑语义不裁剪)。 */
|
|
72
|
+
export declare function shapeHttpReply(parsed: unknown): Record<string, unknown>;
|
|
73
|
+
/** 建 HTTP 通话函数:(phoneName, args) => Promise<reply>,回包全透传,失败抛异常。 */
|
|
74
|
+
export declare function createHttpCall(options: CreateHttpCallOptions): (phoneName: string, args: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
|
75
|
+
export interface UpdatePanelHttpOptions extends Omit<UpdatePanelOptions, "call" | "prefix"> {
|
|
76
|
+
/** 单插件电话前缀(与宿主侧一致)。 */
|
|
77
|
+
prefix?: string;
|
|
78
|
+
/** 网关根(必填)。 */
|
|
79
|
+
baseUrl: string;
|
|
80
|
+
/** 电话级覆写表(可选)。 */
|
|
81
|
+
routes?: HttpRoutes;
|
|
82
|
+
/** 抓取注入点(可选)。 */
|
|
83
|
+
fetch?: HttpFetch;
|
|
84
|
+
fetchImpl?: HttpFetch;
|
|
85
|
+
/** 超时覆写(可选)。 */
|
|
86
|
+
timeout?: number | HttpTimeouts;
|
|
87
|
+
timeoutMs?: number | HttpTimeouts;
|
|
88
|
+
}
|
|
89
|
+
/** 面板整组件 HTTP 直挂:只传三样即跑,内核复用 mountUpdatePanel。 */
|
|
90
|
+
export declare function mountUpdatePanelHttp(container: UpdatePanelContainer, options: UpdatePanelHttpOptions): UpdatePanelController;
|
|
91
|
+
export interface UpdateEntryHttpOptions extends Omit<UpdateEntryOptions, "call" | "prefix"> {
|
|
92
|
+
/** 单插件电话前缀(与宿主侧一致)。 */
|
|
93
|
+
prefix?: string;
|
|
94
|
+
/** 网关根(必填)。 */
|
|
95
|
+
baseUrl: string;
|
|
96
|
+
/** 电话级覆写表(可选)。 */
|
|
97
|
+
routes?: HttpRoutes;
|
|
98
|
+
/** 抓取注入点(可选)。 */
|
|
99
|
+
fetch?: HttpFetch;
|
|
100
|
+
fetchImpl?: HttpFetch;
|
|
101
|
+
/** 超时覆写(可选)。 */
|
|
102
|
+
timeout?: number | HttpTimeouts;
|
|
103
|
+
timeoutMs?: number | HttpTimeouts;
|
|
104
|
+
}
|
|
105
|
+
/** 入口件 HTTP 直挂:内部源码级禁安装电话,内核复用 mountUpdateEntry。 */
|
|
106
|
+
export declare function mountUpdateEntryHttp(container: UpdatePanelContainer, options: UpdateEntryHttpOptions): UpdateEntryController;
|
|
107
|
+
export interface UpdateBatchPanelHttpOptions extends Omit<BatchPanelOptions, "call" | "prefix"> {
|
|
108
|
+
/** 批量电话前缀(与宿主侧一致;五电话名从它算出,不写字面量)。 */
|
|
109
|
+
batchPrefix: string;
|
|
110
|
+
/** 单插件电话前缀(可选;给了即同一内核同时认单三电话,便于同网关共用一张 routes 表)。 */
|
|
111
|
+
prefix?: string;
|
|
112
|
+
/** 网关根(必填)。 */
|
|
113
|
+
baseUrl: string;
|
|
114
|
+
/** 电话级覆写表(可选;键为全电话名或短动作名,未知键拒绝)。 */
|
|
115
|
+
routes?: HttpRoutes;
|
|
116
|
+
/** 抓取注入点(可选)。 */
|
|
117
|
+
fetch?: HttpFetch;
|
|
118
|
+
fetchImpl?: HttpFetch;
|
|
119
|
+
/** 超时覆写(可选;batchInstall 走装时限 16min,其余走查/状态 15s)。 */
|
|
120
|
+
timeout?: number | HttpTimeouts;
|
|
121
|
+
timeoutMs?: number | HttpTimeouts;
|
|
122
|
+
}
|
|
123
|
+
/** 批量面板 HTTP 直挂:只传批量前缀加网关根即跑,内核复用 mountUpdateBatchPanel。
|
|
124
|
+
*
|
|
125
|
+
* 取消整批走 batchCancel 电话而不走中止等待(语义不混淆);卸载只停轮询,宿主侧照跑。 */
|
|
126
|
+
export declare function mountUpdateBatchPanelHttp(container: BatchPanelContainer, options: UpdateBatchPanelHttpOptions): BatchPanelController;
|
|
127
|
+
export { assertPollInterval };
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
// AUTO-GENERATED by node build.mjs — DO NOT EDIT. Source: src/http.ts
|
|
2
|
+
import { DEFAULT_PREFIX, assertPrefix } from "./config.js";
|
|
3
|
+
import { assertPollInterval } from "./client.js";
|
|
4
|
+
import { DIAG_KEYS } from "./diag.js";
|
|
5
|
+
import { sanitizeDetail } from "./redaction.js";
|
|
6
|
+
import { mountUpdatePanel } from "./panel.js";
|
|
7
|
+
import { mountUpdateEntry } from "./entry.js";
|
|
8
|
+
import { mountUpdateBatchPanel } from "./panel-batch.js";
|
|
9
|
+
const HTTP_TRANSPORT_FAILED = "http-transport-failed";
|
|
10
|
+
const DEFAULT_HTTP_CHECK_TIMEOUT_MS = 15e3;
|
|
11
|
+
const DEFAULT_HTTP_INSTALL_TIMEOUT_MS = 16 * 6e4;
|
|
12
|
+
const DEFAULT_HTTP_STATUS_TIMEOUT_MS = 15e3;
|
|
13
|
+
const HTTP_PHONE_ACTIONS = ["updateStatus", "updateCheck", "updateInstall"];
|
|
14
|
+
const HTTP_BATCH_ACTIONS = ["batchStatus", "batchCheck", "batchInstall", "batchResume", "batchCancel"];
|
|
15
|
+
function isAbsoluteUrl(value) {
|
|
16
|
+
return /^https?:\/\//i.test(value);
|
|
17
|
+
}
|
|
18
|
+
function stripRightSlash(value) {
|
|
19
|
+
return String(value ?? "").replace(/\/+$/, "");
|
|
20
|
+
}
|
|
21
|
+
function joinBase(baseUrl, rel) {
|
|
22
|
+
const b = stripRightSlash(baseUrl);
|
|
23
|
+
let r = String(rel ?? "");
|
|
24
|
+
if (!r) return b;
|
|
25
|
+
if (!r.startsWith("/")) r = "/" + r;
|
|
26
|
+
return b + r;
|
|
27
|
+
}
|
|
28
|
+
function shortActionOf(phoneName) {
|
|
29
|
+
const name = String(phoneName ?? "");
|
|
30
|
+
const at = name.lastIndexOf(".");
|
|
31
|
+
return at >= 0 ? name.slice(at + 1) : name;
|
|
32
|
+
}
|
|
33
|
+
function allowedRouteKeys(prefix, batchPrefix) {
|
|
34
|
+
const out = /* @__PURE__ */ new Set();
|
|
35
|
+
for (const a of HTTP_PHONE_ACTIONS) {
|
|
36
|
+
out.add(a);
|
|
37
|
+
out.add(prefix + "." + a);
|
|
38
|
+
}
|
|
39
|
+
if (batchPrefix) {
|
|
40
|
+
for (const a of HTTP_BATCH_ACTIONS) {
|
|
41
|
+
out.add(a);
|
|
42
|
+
out.add(batchPrefix + "." + a);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
function assertHttpRoutes(routes, prefix, batchPrefix) {
|
|
48
|
+
if (!routes) return;
|
|
49
|
+
const allowed = allowedRouteKeys(prefix, batchPrefix);
|
|
50
|
+
for (const key of Object.keys(routes)) {
|
|
51
|
+
if (!allowed.has(key)) {
|
|
52
|
+
throw new Error("[dsh-plugin-update] routes \u672A\u77E5\u952E\u62D2\u7EDD\uFF1A" + JSON.stringify(key));
|
|
53
|
+
}
|
|
54
|
+
const value = routes[key];
|
|
55
|
+
if (typeof value !== "string" || !value) {
|
|
56
|
+
throw new Error("[dsh-plugin-update] routes \u503C\u975E\u6CD5\uFF1A" + JSON.stringify(key));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function resolveHttpPath(options, phoneName) {
|
|
61
|
+
if (typeof phoneName !== "string" || !phoneName) {
|
|
62
|
+
throw new Error("[dsh-plugin-update] \u7535\u8BDD\u540D\u975E\u6CD5\uFF1A\u987B\u4E3A\u975E\u7A7A\u5B57\u7B26\u4E32");
|
|
63
|
+
}
|
|
64
|
+
const routes = options.routes ?? {};
|
|
65
|
+
assertHttpRoutes(routes, options.prefix, options.batchPrefix);
|
|
66
|
+
const short = shortActionOf(phoneName);
|
|
67
|
+
const hit = routes[phoneName] ?? routes[short];
|
|
68
|
+
if (hit !== void 0) {
|
|
69
|
+
if (isAbsoluteUrl(hit)) return hit;
|
|
70
|
+
return joinBase(options.baseUrl, hit);
|
|
71
|
+
}
|
|
72
|
+
return joinBase(options.baseUrl, phoneName);
|
|
73
|
+
}
|
|
74
|
+
function httpTransportFailed(reason, detail) {
|
|
75
|
+
const clean = sanitizeDetail(String(detail ?? reason ?? ""));
|
|
76
|
+
const err = new Error("[dsh-plugin-update] http-transport-failed: " + reason);
|
|
77
|
+
err.code = HTTP_TRANSPORT_FAILED;
|
|
78
|
+
err.detail = clean;
|
|
79
|
+
return err;
|
|
80
|
+
}
|
|
81
|
+
function isRecord(value) {
|
|
82
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
83
|
+
}
|
|
84
|
+
function pickHttpDiag(raw) {
|
|
85
|
+
if (!isRecord(raw)) return void 0;
|
|
86
|
+
const out = {};
|
|
87
|
+
let kept = 0;
|
|
88
|
+
for (const key of DIAG_KEYS) {
|
|
89
|
+
const value = raw[key];
|
|
90
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
91
|
+
out[key] = key === "detail" ? sanitizeDetail(String(value)) : value;
|
|
92
|
+
kept++;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return kept > 0 ? out : void 0;
|
|
96
|
+
}
|
|
97
|
+
function shapeHttpReply(parsed) {
|
|
98
|
+
if (!isRecord(parsed)) {
|
|
99
|
+
throw httpTransportFailed("\u56DE\u5305\u5F62\u72B6\u975E\u6CD5", String(parsed).slice(0, 120));
|
|
100
|
+
}
|
|
101
|
+
const out = { ...parsed };
|
|
102
|
+
if ("diagnostic" in out) delete out["diagnostic"];
|
|
103
|
+
if ("diag" in out) {
|
|
104
|
+
const kept = pickHttpDiag(out["diag"]);
|
|
105
|
+
if (kept) out["diag"] = kept;
|
|
106
|
+
else delete out["diag"];
|
|
107
|
+
}
|
|
108
|
+
if (Array.isArray(out["rows"])) {
|
|
109
|
+
out["rows"] = out["rows"].map((row) => {
|
|
110
|
+
if (!isRecord(row)) return row;
|
|
111
|
+
const copy = { ...row };
|
|
112
|
+
if ("diagnostic" in copy) delete copy["diagnostic"];
|
|
113
|
+
if ("diag" in copy) {
|
|
114
|
+
const kept = pickHttpDiag(copy["diag"]);
|
|
115
|
+
if (kept) copy["diag"] = kept;
|
|
116
|
+
else delete copy["diag"];
|
|
117
|
+
}
|
|
118
|
+
return copy;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
function timeoutForPhone(phoneName, timeout) {
|
|
124
|
+
if (typeof timeout === "number" && Number.isFinite(timeout) && timeout > 0) return timeout;
|
|
125
|
+
const t = timeout ?? {};
|
|
126
|
+
if (phoneName.endsWith(".updateInstall") || phoneName.endsWith(".batchInstall")) {
|
|
127
|
+
if (typeof t.installMs === "number" && Number.isFinite(t.installMs) && t.installMs > 0) return t.installMs;
|
|
128
|
+
return DEFAULT_HTTP_INSTALL_TIMEOUT_MS;
|
|
129
|
+
}
|
|
130
|
+
if (phoneName.endsWith(".updateCheck") || phoneName.endsWith(".batchCheck")) {
|
|
131
|
+
if (typeof t.checkMs === "number" && Number.isFinite(t.checkMs) && t.checkMs > 0) return t.checkMs;
|
|
132
|
+
return DEFAULT_HTTP_CHECK_TIMEOUT_MS;
|
|
133
|
+
}
|
|
134
|
+
if (typeof t.statusMs === "number" && Number.isFinite(t.statusMs) && t.statusMs > 0) return t.statusMs;
|
|
135
|
+
if (typeof t.checkMs === "number" && Number.isFinite(t.checkMs) && t.checkMs > 0) return t.checkMs;
|
|
136
|
+
return DEFAULT_HTTP_STATUS_TIMEOUT_MS;
|
|
137
|
+
}
|
|
138
|
+
function resolveFetchImpl(options) {
|
|
139
|
+
const impl = options.fetch ?? options.fetchImpl ?? globalThis.fetch;
|
|
140
|
+
if (typeof impl !== "function") {
|
|
141
|
+
throw new Error("[dsh-plugin-update] HTTP \u4F20\u8F93\u7F3A\u5C11\u6293\u53D6\u51FD\u6570\uFF1A\u8BF7\u4F20 fetch \u6216\u5728\u6709 globalThis.fetch \u7684\u73AF\u5883\u8FD0\u884C");
|
|
142
|
+
}
|
|
143
|
+
return impl;
|
|
144
|
+
}
|
|
145
|
+
function createHttpCall(options) {
|
|
146
|
+
if (!options || typeof options !== "object") {
|
|
147
|
+
throw new Error("[dsh-plugin-update] \u5EFA HTTP \u4F20\u8F93\u7F3A\u5C11\u914D\u7F6E\uFF1A\u524D\u7F00 prefix \u4E0E\u7F51\u5173\u6839 baseUrl \u5FC5\u586B");
|
|
148
|
+
}
|
|
149
|
+
const prefix = options.prefix;
|
|
150
|
+
if (typeof prefix !== "string" || !prefix) {
|
|
151
|
+
throw new Error("[dsh-plugin-update] \u7535\u8BDD\u540D\u524D\u7F00 prefix \u975E\u6CD5\uFF1A\u987B\u4E3A\u975E\u7A7A\u5B57\u7B26\u4E32");
|
|
152
|
+
}
|
|
153
|
+
if (prefix.includes(".") || prefix.includes("/") || prefix.includes("\\") || /\s/.test(prefix)) {
|
|
154
|
+
throw new Error("[dsh-plugin-update] \u7535\u8BDD\u540D\u524D\u7F00 prefix \u975E\u6CD5\uFF1A\u4E0D\u5F97\u542B\u6709\u70B9\u3001\u8DEF\u5F84\u5206\u9694\u7B26\u6216\u7A7A\u767D");
|
|
155
|
+
}
|
|
156
|
+
const baseUrl = options.baseUrl;
|
|
157
|
+
if (typeof baseUrl !== "string" || !baseUrl.trim()) {
|
|
158
|
+
throw new Error("[dsh-plugin-update] \u7F51\u5173\u6839 baseUrl \u5FC5\u586B\uFF1A\u987B\u4E3A\u975E\u7A7A\u5B57\u7B26\u4E32");
|
|
159
|
+
}
|
|
160
|
+
const routes = options.routes ?? {};
|
|
161
|
+
assertHttpRoutes(routes, prefix, options.batchPrefix);
|
|
162
|
+
const fetchImpl = resolveFetchImpl(options);
|
|
163
|
+
const timeout = options.timeout ?? options.timeoutMs;
|
|
164
|
+
return async function httpCall(phoneName, args) {
|
|
165
|
+
if (typeof phoneName !== "string" || !phoneName) {
|
|
166
|
+
throw new Error("[dsh-plugin-update] \u7535\u8BDD\u540D\u975E\u6CD5\uFF1A\u987B\u4E3A\u975E\u7A7A\u5B57\u7B26\u4E32");
|
|
167
|
+
}
|
|
168
|
+
const url = resolveHttpPath({ prefix, batchPrefix: options.batchPrefix, baseUrl, routes }, phoneName);
|
|
169
|
+
const safeArgs = args && typeof args === "object" ? args : {};
|
|
170
|
+
const timeoutMs = timeoutForPhone(phoneName, timeout);
|
|
171
|
+
const body = JSON.stringify(stripSignal(safeArgs));
|
|
172
|
+
const AbortCtor = globalThis.AbortController;
|
|
173
|
+
const controller = typeof AbortCtor === "function" ? new AbortCtor() : null;
|
|
174
|
+
const callerSignal = safeArgs["signal"];
|
|
175
|
+
const onCallerAbort = () => {
|
|
176
|
+
try {
|
|
177
|
+
controller?.abort();
|
|
178
|
+
} catch {
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
if (callerSignal && typeof callerSignal.addEventListener === "function") {
|
|
182
|
+
try {
|
|
183
|
+
callerSignal.addEventListener.call(callerSignal, "abort", onCallerAbort, { once: true });
|
|
184
|
+
} catch {
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
let res;
|
|
188
|
+
const fetchPromise = fetchImpl(url, {
|
|
189
|
+
method: "POST",
|
|
190
|
+
headers: { "content-type": "application/json" },
|
|
191
|
+
body,
|
|
192
|
+
...controller ? { signal: controller.signal } : {}
|
|
193
|
+
});
|
|
194
|
+
let timeoutTimer = null;
|
|
195
|
+
void fetchPromise.catch(() => {
|
|
196
|
+
});
|
|
197
|
+
try {
|
|
198
|
+
res = await Promise.race([
|
|
199
|
+
fetchPromise,
|
|
200
|
+
new Promise((_, reject) => {
|
|
201
|
+
timeoutTimer = setTimeout(() => {
|
|
202
|
+
try {
|
|
203
|
+
controller?.abort();
|
|
204
|
+
} catch {
|
|
205
|
+
}
|
|
206
|
+
reject(httpTransportFailed("\u8D85\u65F6", "\u6293\u53D6\u8D85\u65F6\uFF08" + String(timeoutMs) + "ms\uFF09\uFF1A" + phoneName));
|
|
207
|
+
}, timeoutMs);
|
|
208
|
+
})
|
|
209
|
+
]);
|
|
210
|
+
} catch (e) {
|
|
211
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
212
|
+
if (e && typeof e === "object" && e.code === HTTP_TRANSPORT_FAILED) throw e;
|
|
213
|
+
throw httpTransportFailed(isAbortError(e) ? "\u8D85\u65F6\u6216\u53D6\u6D88" : "\u65AD\u7F51\u6216\u6293\u53D6\u5931\u8D25", e instanceof Error ? e.message : String(e));
|
|
214
|
+
}
|
|
215
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
216
|
+
const ok = res.ok === true;
|
|
217
|
+
if (!ok) {
|
|
218
|
+
const status = res.status;
|
|
219
|
+
const text2 = await readReplyText(res).catch(() => "");
|
|
220
|
+
throw httpTransportFailed("\u975E 2xx\uFF08" + String(status ?? "unknown") + "\uFF09", "HTTP " + String(status ?? "?") + ": " + String(text2).slice(0, 120));
|
|
221
|
+
}
|
|
222
|
+
const text = await readReplyText(res);
|
|
223
|
+
let parsed;
|
|
224
|
+
try {
|
|
225
|
+
parsed = JSON.parse(text);
|
|
226
|
+
} catch {
|
|
227
|
+
throw httpTransportFailed("JSON \u975E\u6CD5", String(text).slice(0, 120));
|
|
228
|
+
}
|
|
229
|
+
return shapeHttpReply(parsed);
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function stripSignal(args) {
|
|
233
|
+
if (!("signal" in args)) return args;
|
|
234
|
+
const out = { ...args };
|
|
235
|
+
delete out["signal"];
|
|
236
|
+
return out;
|
|
237
|
+
}
|
|
238
|
+
function isAbortError(e) {
|
|
239
|
+
const name = e.name;
|
|
240
|
+
return name === "AbortError" || name === "TimeoutError";
|
|
241
|
+
}
|
|
242
|
+
async function readReplyText(res) {
|
|
243
|
+
const textFn = res.text;
|
|
244
|
+
if (typeof textFn === "function") return String(await textFn.call(res));
|
|
245
|
+
const jsonFn = res.json;
|
|
246
|
+
if (typeof jsonFn === "function") return JSON.stringify(await jsonFn.call(res));
|
|
247
|
+
throw httpTransportFailed("\u56DE\u5305\u5F62\u72B6\u975E\u6CD5", "\u6293\u53D6\u56DE\u5305\u65E2\u65E0 text() \u4E5F\u65E0 json()");
|
|
248
|
+
}
|
|
249
|
+
function mountUpdatePanelHttp(container, options) {
|
|
250
|
+
if (!options || typeof options !== "object") {
|
|
251
|
+
throw new Error("[dsh-plugin-update] \u6302 HTTP \u9762\u677F\u7F3A\u5C11\u914D\u7F6E\uFF1A\u63D2\u4EF6\u6807\u8BC6 pluginId \u4E0E\u7F51\u5173\u6839 baseUrl \u5FC5\u586B");
|
|
252
|
+
}
|
|
253
|
+
const prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
254
|
+
const call = createHttpCall({ prefix, baseUrl: options.baseUrl, routes: options.routes, fetch: options.fetch, fetchImpl: options.fetchImpl, timeout: options.timeout, timeoutMs: options.timeoutMs });
|
|
255
|
+
const { baseUrl: _b, routes: _r, fetch: _f, fetchImpl: _fi, timeout: _t, timeoutMs: _tm, prefix: _p, ...panelRest } = options;
|
|
256
|
+
return mountUpdatePanel(container, { ...panelRest, pluginId: options.pluginId, prefix, call });
|
|
257
|
+
}
|
|
258
|
+
function mountUpdateEntryHttp(container, options) {
|
|
259
|
+
if (!options || typeof options !== "object") {
|
|
260
|
+
throw new Error("[dsh-plugin-update] \u6302 HTTP \u5165\u53E3\u4EF6\u7F3A\u5C11\u914D\u7F6E\uFF1A\u63D2\u4EF6\u6807\u8BC6 pluginId \u4E0E\u7F51\u5173\u6839 baseUrl \u5FC5\u586B");
|
|
261
|
+
}
|
|
262
|
+
const prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
263
|
+
const rawCall = createHttpCall({ prefix, baseUrl: options.baseUrl, routes: options.routes, fetch: options.fetch, fetchImpl: options.fetchImpl, timeout: options.timeout, timeoutMs: options.timeoutMs });
|
|
264
|
+
const guarded = async (name, args) => {
|
|
265
|
+
if (shortActionOf(name) === "updateInstall") {
|
|
266
|
+
throw new Error("[dsh-plugin-update] \u5165\u53E3\u4EF6\u53EA\u5141\u8BB8\u53EA\u8BFB\u7535\u8BDD\uFF0CupdateInstall \u7981\u6B62\uFF08\u6E90\u7801\u7EA7\uFF09");
|
|
267
|
+
}
|
|
268
|
+
return rawCall(name, args);
|
|
269
|
+
};
|
|
270
|
+
const { baseUrl: _b, routes: _r, fetch: _f, fetchImpl: _fi, timeout: _t, timeoutMs: _tm, prefix: _p, ...entryRest } = options;
|
|
271
|
+
return mountUpdateEntry(container, { ...entryRest, pluginId: options.pluginId, prefix, call: guarded });
|
|
272
|
+
}
|
|
273
|
+
function mountUpdateBatchPanelHttp(container, options) {
|
|
274
|
+
if (!options || typeof options !== "object") {
|
|
275
|
+
throw new Error("[dsh-plugin-update] \u6302 HTTP \u6279\u91CF\u9762\u677F\u7F3A\u5C11\u914D\u7F6E\uFF1A\u6279\u91CF\u524D\u7F00 batchPrefix \u4E0E\u7F51\u5173\u6839 baseUrl \u5FC5\u586B");
|
|
276
|
+
}
|
|
277
|
+
const batchPrefix = assertPrefix(options.batchPrefix, "\u6279\u91CF\u7535\u8BDD\u524D\u7F00 batchPrefix");
|
|
278
|
+
const kernelPrefix = typeof options.prefix === "string" && options.prefix ? assertPrefix(options.prefix, "\u7535\u8BDD\u540D\u524D\u7F00 prefix") : batchPrefix;
|
|
279
|
+
const call = createHttpCall({ prefix: kernelPrefix, batchPrefix, baseUrl: options.baseUrl, routes: options.routes, fetch: options.fetch, fetchImpl: options.fetchImpl, timeout: options.timeout, timeoutMs: options.timeoutMs });
|
|
280
|
+
const { baseUrl: _b, routes: _r, fetch: _f, fetchImpl: _fi, timeout: _t, timeoutMs: _tm, prefix: _p, batchPrefix: _bp, ...batchRest } = options;
|
|
281
|
+
return mountUpdateBatchPanel(container, { ...batchRest, prefix: batchPrefix, call });
|
|
282
|
+
}
|
|
283
|
+
export {
|
|
284
|
+
DEFAULT_HTTP_CHECK_TIMEOUT_MS,
|
|
285
|
+
DEFAULT_HTTP_INSTALL_TIMEOUT_MS,
|
|
286
|
+
DEFAULT_HTTP_STATUS_TIMEOUT_MS,
|
|
287
|
+
HTTP_BATCH_ACTIONS,
|
|
288
|
+
HTTP_PHONE_ACTIONS,
|
|
289
|
+
HTTP_TRANSPORT_FAILED,
|
|
290
|
+
assertHttpRoutes,
|
|
291
|
+
assertPollInterval,
|
|
292
|
+
createHttpCall,
|
|
293
|
+
httpTransportFailed,
|
|
294
|
+
mountUpdateBatchPanelHttp,
|
|
295
|
+
mountUpdateEntryHttp,
|
|
296
|
+
mountUpdatePanelHttp,
|
|
297
|
+
pickHttpDiag,
|
|
298
|
+
resolveHttpPath,
|
|
299
|
+
shapeHttpReply
|
|
300
|
+
};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { type UpdatePanelTheme } from './panel.js';
|
|
2
|
+
/** 一行要画的东西(宿主回包里的形状)。 */
|
|
3
|
+
export interface BatchRowView {
|
|
4
|
+
key: string;
|
|
5
|
+
title: string;
|
|
6
|
+
/** 会话相位:pending/checking/ready/installing/current/done/failed/skipped(认不出按 pending)。 */
|
|
7
|
+
phase: string;
|
|
8
|
+
targetVersion: string | null;
|
|
9
|
+
restartRequired: boolean;
|
|
10
|
+
error: string | null;
|
|
11
|
+
/** 该行的单插件快照(展开详情与「安装这家」按钮的门控都用它)。 */
|
|
12
|
+
snapshot: unknown;
|
|
13
|
+
manual?: string | null;
|
|
14
|
+
queue?: unknown;
|
|
15
|
+
profileName?: string | null;
|
|
16
|
+
/** 该行的插件标识(跳过存储与诊断文本用;缺省用 key)。宿主侧后补字段,缺了照样跑。 */
|
|
17
|
+
pluginId?: string | null;
|
|
18
|
+
/** 该行的诊断详情(宿主给就带上;没有就不带)。 */
|
|
19
|
+
diag?: unknown;
|
|
20
|
+
/** 宿主种类(进诊断文本;缺省说人话)。 */
|
|
21
|
+
hostKind?: string | null;
|
|
22
|
+
/** 该家单插件三电话的名字(取消排队要用它自己的安装电话;老宿主不给就不给取消入口)。 */
|
|
23
|
+
phoneNames?: unknown;
|
|
24
|
+
}
|
|
25
|
+
/** 摆放形态:默认内嵌,切弹窗走同一参数(与单插件面板同一套写法)。 */
|
|
26
|
+
export type BatchPanelMode = 'embedded' | 'dialog';
|
|
27
|
+
/** 批量面板的挂载参数。 */
|
|
28
|
+
export interface BatchPanelOptions {
|
|
29
|
+
/** 批量电话前缀(与宿主一致)。 */
|
|
30
|
+
prefix: string;
|
|
31
|
+
/** 调宿主电话:(phoneName, args) => Promise<reply>。 */
|
|
32
|
+
call: (name: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
33
|
+
/** 主题:与单插件面板同一套(缺省 default)。 */
|
|
34
|
+
theme?: UpdatePanelTheme;
|
|
35
|
+
/** 摆放形态:内嵌或弹窗(缺省 embedded)。 */
|
|
36
|
+
mode?: BatchPanelMode;
|
|
37
|
+
/** 轮询间隔(毫秒,缺省 1500,下限 250)。 */
|
|
38
|
+
pollMs?: number;
|
|
39
|
+
/** 可选:键 -> 中文名覆盖。 */
|
|
40
|
+
titles?: Record<string, string>;
|
|
41
|
+
/**
|
|
42
|
+
* 「重启宿主」的落地(与单插件面板同一口径):不传就只提示手动重启,不假装能重启。
|
|
43
|
+
*/
|
|
44
|
+
onRestartRequested?: () => void | Promise<void>;
|
|
45
|
+
/** 复制文本的出口(与单插件面板同一口径):不传即试浏览器剪贴板,都没有也不抛错。 */
|
|
46
|
+
copyText?: (text: string) => void | Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
/** 面板可点的动作(HTML 上 data-act 一一对应;测试走同一条路)。 */
|
|
49
|
+
export type BatchPanelActionKind = 'check' | 'install' | 'resume' | 'cancel' | 'row-install' | 'row-skip' | 'row-resume-skip' | 'row-cancel-queue' | 'row-copy-manual' | 'row-copy-diag' | 'toggle-details' | 'restart' | 'close';
|
|
50
|
+
/** 挂载点:只要有 innerHTML 的容器即可(浏览器元素或测试替身都行)。 */
|
|
51
|
+
export interface BatchPanelContainer {
|
|
52
|
+
innerHTML: string;
|
|
53
|
+
addEventListener?: (type: string, listener: (ev: unknown) => void) => void;
|
|
54
|
+
removeEventListener?: (type: string, listener: (ev: unknown) => void) => void;
|
|
55
|
+
}
|
|
56
|
+
/** 批量面板控制器(与契约桩同名同义;act 多收一个可选 key,供行内动作指认某一家)。 */
|
|
57
|
+
export interface BatchPanelController {
|
|
58
|
+
refresh(): Promise<void>;
|
|
59
|
+
act(action: BatchPanelActionKind, key?: string): Promise<void>;
|
|
60
|
+
setTheme(theme: UpdatePanelTheme): void;
|
|
61
|
+
setMode(mode: BatchPanelMode): void;
|
|
62
|
+
unmount(): void;
|
|
63
|
+
}
|
|
64
|
+
/** 批量五个电话名(与宿主侧 MultiHostUpdate.phoneNames 同一形状)。 */
|
|
65
|
+
export interface BatchPhoneNames {
|
|
66
|
+
status: string;
|
|
67
|
+
check: string;
|
|
68
|
+
install: string;
|
|
69
|
+
resume: string;
|
|
70
|
+
cancel: string;
|
|
71
|
+
}
|
|
72
|
+
/** 批量面板轮询口径(默认 1.5 秒、下限 250 毫秒,与单插件面板同一套下限)。 */
|
|
73
|
+
export declare const BATCH_PANEL_POLL: {
|
|
74
|
+
readonly defaultMs: 1500;
|
|
75
|
+
readonly minMs: 250;
|
|
76
|
+
};
|
|
77
|
+
/** 批量电话名拼法:前缀 + 五个固定动作名(前缀形状校验与单插件面板同一套)。 */
|
|
78
|
+
export declare function buildBatchPhoneNames(prefix: string): BatchPhoneNames;
|
|
79
|
+
export interface BatchLedgerCounts {
|
|
80
|
+
/** phase=ready:有新版、还没安装。 */
|
|
81
|
+
updatable: number;
|
|
82
|
+
/** phase=installing:正在安装。 */
|
|
83
|
+
installing: number;
|
|
84
|
+
/** phase=pending/checking:还没轮到/正在查。 */
|
|
85
|
+
pending: number;
|
|
86
|
+
/** 安装好了但要重启宿主才生效(终态之外的一档,单独数)。 */
|
|
87
|
+
restart: number;
|
|
88
|
+
/** phase=failed。 */
|
|
89
|
+
failed: number;
|
|
90
|
+
/** phase=skipped。 */
|
|
91
|
+
skipped: number;
|
|
92
|
+
/** phase=current/done 且不用重启:真的没事了。 */
|
|
93
|
+
settled: number;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* 从行上数分类账(每行恰好进一档,七档之和 === 行数):
|
|
97
|
+
* 失败优先(失败行永远算失败,不算待重启);其余按相位落档;不用重启的终态才算「已最新」。
|
|
98
|
+
* 例外一处:「忙失败占位」(phase=failed 但 error=update-busy)其实是**排队**不是失败——
|
|
99
|
+
* 宿主忙时先写占位再回 update-busy,面板把它翻回「可更新」,免得总账把它报成失败。
|
|
100
|
+
*/
|
|
101
|
+
export declare function batchLedgerCounts(rows: readonly BatchRowView[]): BatchLedgerCounts;
|
|
102
|
+
/** 分类计数 → 一句话总账(只出现非零档,顺序固定:可更新 · 安装中 · 待查 · 待重启 · 失败 · 已跳过 · 已最新)。 */
|
|
103
|
+
export declare function batchLedgerText(counts: BatchLedgerCounts): string;
|
|
104
|
+
/**
|
|
105
|
+
* 排队中的状态词:只回答「还要等多久」。位置从该行的队列视图来。
|
|
106
|
+
* queue.ts 的 position 是 waiting 顺位(队首为 1、0 = 自己正在装),而正在装的那一家也压在这一家前面,
|
|
107
|
+
* 所以「前方 N 个」= position N(队首那家前方正好 1 个:正在装的拥有者)。
|
|
108
|
+
* 位置取不到(宿主只记了忙、没给占位读数)就说「等前面安装完」,不猜数字。
|
|
109
|
+
*/
|
|
110
|
+
export declare function batchQueuedStatus(position: number | null): string;
|
|
111
|
+
/**
|
|
112
|
+
* 一行的状态词:只回答一个问题——这家的**下一步**是什么。
|
|
113
|
+
* 中文可执行,不写相位英文(相位只留在 data-phase 属性上,给人看的这句永远是动作)。
|
|
114
|
+
*/
|
|
115
|
+
export declare function batchRowStatus(row: BatchRowView, skippedVersion?: string | null): string;
|
|
116
|
+
export interface BatchPanelRenderInput {
|
|
117
|
+
rows?: readonly BatchRowView[];
|
|
118
|
+
theme?: UpdatePanelTheme;
|
|
119
|
+
mode?: BatchPanelMode;
|
|
120
|
+
/** 当前展开的那一家(键);null 即全部收起。 */
|
|
121
|
+
expandedKey?: string | null;
|
|
122
|
+
/** 批量电话本身失败的稳定码(ok:false 或抛错)。 */
|
|
123
|
+
lastError?: string | null;
|
|
124
|
+
/** 本地提示(重启入口、取消结果之类)。 */
|
|
125
|
+
notice?: string | null;
|
|
126
|
+
/** 面板正有一次电话在飞(本地忙守卫的一半)。 */
|
|
127
|
+
inFlight?: boolean;
|
|
128
|
+
/** 是否已经拿到过第一次回包(决定总账那句是不是「正在读取…」)。 */
|
|
129
|
+
loaded?: boolean;
|
|
130
|
+
titles?: Record<string, string>;
|
|
131
|
+
/** 键 -> 该家被跳过的版本(挂载侧从跳过存储读出;纯渲染函数不读存储)。 */
|
|
132
|
+
skippedVersions?: Record<string, string | null>;
|
|
133
|
+
/** notice 归哪一家:等于某行键时那条回执画在该家详情里(否则画在面板底部)。 */
|
|
134
|
+
noticeKey?: string | null;
|
|
135
|
+
}
|
|
136
|
+
/** 整面板 HTML(含样式;重绘即整体替换 innerHTML,故每次都带 style 也只留一份)。 */
|
|
137
|
+
export declare function renderBatchPanelHTML(input: BatchPanelRenderInput): string;
|
|
138
|
+
export declare const UPDATE_BATCH_PANEL_CSS: string;
|
|
139
|
+
/**
|
|
140
|
+
* 批量面板的 D5 皮肤(#20 同一套口径:只换颜色/字体/间距,不改顺序、不藏东西)。
|
|
141
|
+
* 卷宗抬头右侧给大印章留 120px(与单插件面板档案头同一处理)。
|
|
142
|
+
*/
|
|
143
|
+
export declare const UPDATE_BATCH_PANEL_D5_CSS: string;
|
|
144
|
+
/**
|
|
145
|
+
* 挂批量面板(调用方一行即跑):
|
|
146
|
+
* ```js
|
|
147
|
+
* const panel = mountUpdateBatchPanel(document.getElementById('upd'), { prefix: 'life', call: host.call })
|
|
148
|
+
* // …离开时 panel.unmount()(只停轮询,批量更新在宿主侧继续跑)
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
export declare function mountUpdateBatchPanel(container: BatchPanelContainer, options: BatchPanelOptions): BatchPanelController;
|