dsh-plugin-update 0.3.0 → 0.4.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/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 同字面,不另起字面;#38 加 updateChangelog)。 */
12
+ export declare const HTTP_PHONE_ACTIONS: readonly ["updateStatus", "updateCheck", "updateInstall", "updateChangelog"];
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,301 @@
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", "updateChangelog"];
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
+ out.add("updateChangelog");
46
+ return out;
47
+ }
48
+ function assertHttpRoutes(routes, prefix, batchPrefix) {
49
+ if (!routes) return;
50
+ const allowed = allowedRouteKeys(prefix, batchPrefix);
51
+ for (const key of Object.keys(routes)) {
52
+ if (!allowed.has(key)) {
53
+ throw new Error("[dsh-plugin-update] routes \u672A\u77E5\u952E\u62D2\u7EDD\uFF1A" + JSON.stringify(key));
54
+ }
55
+ const value = routes[key];
56
+ if (typeof value !== "string" || !value) {
57
+ throw new Error("[dsh-plugin-update] routes \u503C\u975E\u6CD5\uFF1A" + JSON.stringify(key));
58
+ }
59
+ }
60
+ }
61
+ function resolveHttpPath(options, phoneName) {
62
+ if (typeof phoneName !== "string" || !phoneName) {
63
+ throw new Error("[dsh-plugin-update] \u7535\u8BDD\u540D\u975E\u6CD5\uFF1A\u987B\u4E3A\u975E\u7A7A\u5B57\u7B26\u4E32");
64
+ }
65
+ const routes = options.routes ?? {};
66
+ assertHttpRoutes(routes, options.prefix, options.batchPrefix);
67
+ const short = shortActionOf(phoneName);
68
+ const hit = routes[phoneName] ?? routes[short];
69
+ if (hit !== void 0) {
70
+ if (isAbsoluteUrl(hit)) return hit;
71
+ return joinBase(options.baseUrl, hit);
72
+ }
73
+ return joinBase(options.baseUrl, phoneName);
74
+ }
75
+ function httpTransportFailed(reason, detail) {
76
+ const clean = sanitizeDetail(String(detail ?? reason ?? ""));
77
+ const err = new Error("[dsh-plugin-update] http-transport-failed: " + reason);
78
+ err.code = HTTP_TRANSPORT_FAILED;
79
+ err.detail = clean;
80
+ return err;
81
+ }
82
+ function isRecord(value) {
83
+ return !!value && typeof value === "object" && !Array.isArray(value);
84
+ }
85
+ function pickHttpDiag(raw) {
86
+ if (!isRecord(raw)) return void 0;
87
+ const out = {};
88
+ let kept = 0;
89
+ for (const key of DIAG_KEYS) {
90
+ const value = raw[key];
91
+ if (typeof value === "string" || typeof value === "number") {
92
+ out[key] = key === "detail" ? sanitizeDetail(String(value)) : value;
93
+ kept++;
94
+ }
95
+ }
96
+ return kept > 0 ? out : void 0;
97
+ }
98
+ function shapeHttpReply(parsed) {
99
+ if (!isRecord(parsed)) {
100
+ throw httpTransportFailed("\u56DE\u5305\u5F62\u72B6\u975E\u6CD5", String(parsed).slice(0, 120));
101
+ }
102
+ const out = { ...parsed };
103
+ if ("diagnostic" in out) delete out["diagnostic"];
104
+ if ("diag" in out) {
105
+ const kept = pickHttpDiag(out["diag"]);
106
+ if (kept) out["diag"] = kept;
107
+ else delete out["diag"];
108
+ }
109
+ if (Array.isArray(out["rows"])) {
110
+ out["rows"] = out["rows"].map((row) => {
111
+ if (!isRecord(row)) return row;
112
+ const copy = { ...row };
113
+ if ("diagnostic" in copy) delete copy["diagnostic"];
114
+ if ("diag" in copy) {
115
+ const kept = pickHttpDiag(copy["diag"]);
116
+ if (kept) copy["diag"] = kept;
117
+ else delete copy["diag"];
118
+ }
119
+ return copy;
120
+ });
121
+ }
122
+ return out;
123
+ }
124
+ function timeoutForPhone(phoneName, timeout) {
125
+ if (typeof timeout === "number" && Number.isFinite(timeout) && timeout > 0) return timeout;
126
+ const t = timeout ?? {};
127
+ if (phoneName.endsWith(".updateInstall") || phoneName.endsWith(".batchInstall")) {
128
+ if (typeof t.installMs === "number" && Number.isFinite(t.installMs) && t.installMs > 0) return t.installMs;
129
+ return DEFAULT_HTTP_INSTALL_TIMEOUT_MS;
130
+ }
131
+ if (phoneName.endsWith(".updateCheck") || phoneName.endsWith(".batchCheck") || phoneName.endsWith(".updateChangelog")) {
132
+ if (typeof t.checkMs === "number" && Number.isFinite(t.checkMs) && t.checkMs > 0) return t.checkMs;
133
+ return DEFAULT_HTTP_CHECK_TIMEOUT_MS;
134
+ }
135
+ if (typeof t.statusMs === "number" && Number.isFinite(t.statusMs) && t.statusMs > 0) return t.statusMs;
136
+ if (typeof t.checkMs === "number" && Number.isFinite(t.checkMs) && t.checkMs > 0) return t.checkMs;
137
+ return DEFAULT_HTTP_STATUS_TIMEOUT_MS;
138
+ }
139
+ function resolveFetchImpl(options) {
140
+ const impl = options.fetch ?? options.fetchImpl ?? globalThis.fetch;
141
+ if (typeof impl !== "function") {
142
+ 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");
143
+ }
144
+ return impl;
145
+ }
146
+ function createHttpCall(options) {
147
+ if (!options || typeof options !== "object") {
148
+ 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");
149
+ }
150
+ const prefix = options.prefix;
151
+ if (typeof prefix !== "string" || !prefix) {
152
+ throw new Error("[dsh-plugin-update] \u7535\u8BDD\u540D\u524D\u7F00 prefix \u975E\u6CD5\uFF1A\u987B\u4E3A\u975E\u7A7A\u5B57\u7B26\u4E32");
153
+ }
154
+ if (prefix.includes(".") || prefix.includes("/") || prefix.includes("\\") || /\s/.test(prefix)) {
155
+ 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");
156
+ }
157
+ const baseUrl = options.baseUrl;
158
+ if (typeof baseUrl !== "string" || !baseUrl.trim()) {
159
+ throw new Error("[dsh-plugin-update] \u7F51\u5173\u6839 baseUrl \u5FC5\u586B\uFF1A\u987B\u4E3A\u975E\u7A7A\u5B57\u7B26\u4E32");
160
+ }
161
+ const routes = options.routes ?? {};
162
+ assertHttpRoutes(routes, prefix, options.batchPrefix);
163
+ const fetchImpl = resolveFetchImpl(options);
164
+ const timeout = options.timeout ?? options.timeoutMs;
165
+ return async function httpCall(phoneName, args) {
166
+ if (typeof phoneName !== "string" || !phoneName) {
167
+ throw new Error("[dsh-plugin-update] \u7535\u8BDD\u540D\u975E\u6CD5\uFF1A\u987B\u4E3A\u975E\u7A7A\u5B57\u7B26\u4E32");
168
+ }
169
+ const url = resolveHttpPath({ prefix, batchPrefix: options.batchPrefix, baseUrl, routes }, phoneName);
170
+ const safeArgs = args && typeof args === "object" ? args : {};
171
+ const timeoutMs = timeoutForPhone(phoneName, timeout);
172
+ const body = JSON.stringify(stripSignal(safeArgs));
173
+ const AbortCtor = globalThis.AbortController;
174
+ const controller = typeof AbortCtor === "function" ? new AbortCtor() : null;
175
+ const callerSignal = safeArgs["signal"];
176
+ const onCallerAbort = () => {
177
+ try {
178
+ controller?.abort();
179
+ } catch {
180
+ }
181
+ };
182
+ if (callerSignal && typeof callerSignal.addEventListener === "function") {
183
+ try {
184
+ callerSignal.addEventListener.call(callerSignal, "abort", onCallerAbort, { once: true });
185
+ } catch {
186
+ }
187
+ }
188
+ let res;
189
+ const fetchPromise = fetchImpl(url, {
190
+ method: "POST",
191
+ headers: { "content-type": "application/json" },
192
+ body,
193
+ ...controller ? { signal: controller.signal } : {}
194
+ });
195
+ let timeoutTimer = null;
196
+ void fetchPromise.catch(() => {
197
+ });
198
+ try {
199
+ res = await Promise.race([
200
+ fetchPromise,
201
+ new Promise((_, reject) => {
202
+ timeoutTimer = setTimeout(() => {
203
+ try {
204
+ controller?.abort();
205
+ } catch {
206
+ }
207
+ reject(httpTransportFailed("\u8D85\u65F6", "\u6293\u53D6\u8D85\u65F6\uFF08" + String(timeoutMs) + "ms\uFF09\uFF1A" + phoneName));
208
+ }, timeoutMs);
209
+ })
210
+ ]);
211
+ } catch (e) {
212
+ if (timeoutTimer) clearTimeout(timeoutTimer);
213
+ if (e && typeof e === "object" && e.code === HTTP_TRANSPORT_FAILED) throw e;
214
+ throw httpTransportFailed(isAbortError(e) ? "\u8D85\u65F6\u6216\u53D6\u6D88" : "\u65AD\u7F51\u6216\u6293\u53D6\u5931\u8D25", e instanceof Error ? e.message : String(e));
215
+ }
216
+ if (timeoutTimer) clearTimeout(timeoutTimer);
217
+ const ok = res.ok === true;
218
+ if (!ok) {
219
+ const status = res.status;
220
+ const text2 = await readReplyText(res).catch(() => "");
221
+ throw httpTransportFailed("\u975E 2xx\uFF08" + String(status ?? "unknown") + "\uFF09", "HTTP " + String(status ?? "?") + ": " + String(text2).slice(0, 120));
222
+ }
223
+ const text = await readReplyText(res);
224
+ let parsed;
225
+ try {
226
+ parsed = JSON.parse(text);
227
+ } catch {
228
+ throw httpTransportFailed("JSON \u975E\u6CD5", String(text).slice(0, 120));
229
+ }
230
+ return shapeHttpReply(parsed);
231
+ };
232
+ }
233
+ function stripSignal(args) {
234
+ if (!("signal" in args)) return args;
235
+ const out = { ...args };
236
+ delete out["signal"];
237
+ return out;
238
+ }
239
+ function isAbortError(e) {
240
+ const name = e.name;
241
+ return name === "AbortError" || name === "TimeoutError";
242
+ }
243
+ async function readReplyText(res) {
244
+ const textFn = res.text;
245
+ if (typeof textFn === "function") return String(await textFn.call(res));
246
+ const jsonFn = res.json;
247
+ if (typeof jsonFn === "function") return JSON.stringify(await jsonFn.call(res));
248
+ throw httpTransportFailed("\u56DE\u5305\u5F62\u72B6\u975E\u6CD5", "\u6293\u53D6\u56DE\u5305\u65E2\u65E0 text() \u4E5F\u65E0 json()");
249
+ }
250
+ function mountUpdatePanelHttp(container, options) {
251
+ if (!options || typeof options !== "object") {
252
+ 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");
253
+ }
254
+ const prefix = options.prefix ?? DEFAULT_PREFIX;
255
+ const call = createHttpCall({ prefix, baseUrl: options.baseUrl, routes: options.routes, fetch: options.fetch, fetchImpl: options.fetchImpl, timeout: options.timeout, timeoutMs: options.timeoutMs });
256
+ const { baseUrl: _b, routes: _r, fetch: _f, fetchImpl: _fi, timeout: _t, timeoutMs: _tm, prefix: _p, ...panelRest } = options;
257
+ return mountUpdatePanel(container, { ...panelRest, pluginId: options.pluginId, prefix, call });
258
+ }
259
+ function mountUpdateEntryHttp(container, options) {
260
+ if (!options || typeof options !== "object") {
261
+ 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");
262
+ }
263
+ const prefix = options.prefix ?? DEFAULT_PREFIX;
264
+ const rawCall = createHttpCall({ prefix, baseUrl: options.baseUrl, routes: options.routes, fetch: options.fetch, fetchImpl: options.fetchImpl, timeout: options.timeout, timeoutMs: options.timeoutMs });
265
+ const guarded = async (name, args) => {
266
+ if (shortActionOf(name) === "updateInstall") {
267
+ throw new Error("[dsh-plugin-update] \u5165\u53E3\u4EF6\u53EA\u5141\u8BB8\u53EA\u8BFB\u7535\u8BDD\uFF0CupdateInstall \u7981\u6B62\uFF08\u6E90\u7801\u7EA7\uFF09");
268
+ }
269
+ return rawCall(name, args);
270
+ };
271
+ const { baseUrl: _b, routes: _r, fetch: _f, fetchImpl: _fi, timeout: _t, timeoutMs: _tm, prefix: _p, ...entryRest } = options;
272
+ return mountUpdateEntry(container, { ...entryRest, pluginId: options.pluginId, prefix, call: guarded });
273
+ }
274
+ function mountUpdateBatchPanelHttp(container, options) {
275
+ if (!options || typeof options !== "object") {
276
+ 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");
277
+ }
278
+ const batchPrefix = assertPrefix(options.batchPrefix, "\u6279\u91CF\u7535\u8BDD\u524D\u7F00 batchPrefix");
279
+ const kernelPrefix = typeof options.prefix === "string" && options.prefix ? assertPrefix(options.prefix, "\u7535\u8BDD\u540D\u524D\u7F00 prefix") : batchPrefix;
280
+ const call = createHttpCall({ prefix: kernelPrefix, batchPrefix, baseUrl: options.baseUrl, routes: options.routes, fetch: options.fetch, fetchImpl: options.fetchImpl, timeout: options.timeout, timeoutMs: options.timeoutMs });
281
+ const { baseUrl: _b, routes: _r, fetch: _f, fetchImpl: _fi, timeout: _t, timeoutMs: _tm, prefix: _p, batchPrefix: _bp, ...batchRest } = options;
282
+ return mountUpdateBatchPanel(container, { ...batchRest, prefix: batchPrefix, call });
283
+ }
284
+ export {
285
+ DEFAULT_HTTP_CHECK_TIMEOUT_MS,
286
+ DEFAULT_HTTP_INSTALL_TIMEOUT_MS,
287
+ DEFAULT_HTTP_STATUS_TIMEOUT_MS,
288
+ HTTP_BATCH_ACTIONS,
289
+ HTTP_PHONE_ACTIONS,
290
+ HTTP_TRANSPORT_FAILED,
291
+ assertHttpRoutes,
292
+ assertPollInterval,
293
+ createHttpCall,
294
+ httpTransportFailed,
295
+ mountUpdateBatchPanelHttp,
296
+ mountUpdateEntryHttp,
297
+ mountUpdatePanelHttp,
298
+ pickHttpDiag,
299
+ resolveHttpPath,
300
+ shapeHttpReply
301
+ };
@@ -44,6 +44,11 @@ export interface BatchPanelOptions {
44
44
  onRestartRequested?: () => void | Promise<void>;
45
45
  /** 复制文本的出口(与单插件面板同一口径):不传即试浏览器剪贴板,都没有也不抛错。 */
46
46
  copyText?: (text: string) => void | Promise<void>;
47
+ /**
48
+ * 详情行自动取日志(#38):默认开。展开行有新版时调该行自己的更新日志电话按需取;
49
+ * 老宿主没给行电话名即回中性提示;false 关闭。
50
+ */
51
+ autoChangelog?: boolean;
47
52
  }
48
53
  /** 面板可点的动作(HTML 上 data-act 一一对应;测试走同一条路)。 */
49
54
  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';
@@ -125,6 +130,8 @@ export interface BatchPanelRenderInput {
125
130
  notice?: string | null;
126
131
  /** 面板正有一次电话在飞(本地忙守卫的一半)。 */
127
132
  inFlight?: boolean;
133
+ /** 「取消这一批」已点过一次:按钮换确认文案,再点一次才真取消(防误触)。 */
134
+ confirmCancel?: boolean;
128
135
  /** 是否已经拿到过第一次回包(决定总账那句是不是「正在读取…」)。 */
129
136
  loaded?: boolean;
130
137
  titles?: Record<string, string>;
@@ -132,6 +139,8 @@ export interface BatchPanelRenderInput {
132
139
  skippedVersions?: Record<string, string | null>;
133
140
  /** notice 归哪一家:等于某行键时那条回执画在该家详情里(否则画在面板底部)。 */
134
141
  noticeKey?: string | null;
142
+ /** 键 -> 该行已取到的日志全文(只传有文本的;取不到与没展开即中性提示,不挡安装)。 */
143
+ changelogs?: Record<string, string | null>;
135
144
  }
136
145
  /** 整面板 HTML(含样式;重绘即整体替换 innerHTML,故每次都带 style 也只留一份)。 */
137
146
  export declare function renderBatchPanelHTML(input: BatchPanelRenderInput): string;