weapp-ide-cli 6.1.9 → 6.2.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/README.md +18 -0
- package/dist/automator-BkZZsGtn.js +1 -0
- package/dist/automator-CRpB6_pA.js +576 -0
- package/dist/automator-session-gWEsSs_A.js +558 -0
- package/dist/{cli-2sY1sAVq.js → cli-ChMQqi5w.js} +1219 -52
- package/dist/cli.js +3 -3
- package/dist/commands-C1PF1B4P.js +528 -0
- package/dist/commands-CZVPfkE9.js +1 -0
- package/dist/devtoolsProjectOwnership-yDD6h0dO.js +1726 -0
- package/dist/index.d.ts +314 -54
- package/dist/index.js +8 -5
- package/dist/managed-Sq8y8FzP.js +212 -0
- package/dist/run-mcp-CPv8cesh.js +1 -0
- package/dist/{run-mcp-LqaWlQJ_.js → run-mcp-Dl_1TH1x.js} +18 -9
- package/dist/sessionStore-zdzy2UFM.js +605 -0
- package/package.json +12 -11
- package/dist/automator-session-fbanL2eW.js +0 -1362
- package/dist/commands-DpMG1w04.js +0 -2
- package/dist/commands-__RJ552L.js +0 -1359
- package/dist/run-mcp-C2SQ_hRJ.js +0 -2
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
import { D as logger, E as colors } from "./sessionStore-zdzy2UFM.js";
|
|
2
|
+
import { c as getAutomatorProtocolTimeoutMethod, d as isAutomatorProtocolTimeoutError, f as isAutomatorWsConnectError, g as resolveAutomatorSessionOptions, i as resolveProjectAutomatorPort, l as isAutomatorLoginError, m as isDevtoolsHttpPortError, n as launchAutomator, p as isDevtoolsExtensionContextInvalidatedError, s as formatAutomatorLoginError, t as connectOpenedAutomator, u as isAutomatorPortInUseError } from "./automator-CRpB6_pA.js";
|
|
3
|
+
import { acquireSharedMiniProgram, closeSharedMiniProgram, getSharedMiniProgramSessionCount, releaseSharedMiniProgram, withMiniProgram } from "@weapp-vite/devtools-runtime";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { OperationLifecycle } from "@weapp-vite/miniprogram-automator/operation";
|
|
6
|
+
import { emitKeypressEvents } from "node:readline";
|
|
7
|
+
//#region src/i18n.ts
|
|
8
|
+
let currentLocale = "zh";
|
|
9
|
+
/**
|
|
10
|
+
* @description 设置当前语言。
|
|
11
|
+
*/
|
|
12
|
+
function setLocale(locale) {
|
|
13
|
+
currentLocale = normalizeLocale(locale);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* @description 从 argv 和环境变量中解析语言并设置。
|
|
17
|
+
*/
|
|
18
|
+
function configureLocaleFromArgv(argv, fallbackLocale) {
|
|
19
|
+
const localeFromArgv = readLangOption(argv);
|
|
20
|
+
if (localeFromArgv) {
|
|
21
|
+
setLocale(localeFromArgv);
|
|
22
|
+
return currentLocale;
|
|
23
|
+
}
|
|
24
|
+
if (fallbackLocale) {
|
|
25
|
+
setLocale(fallbackLocale);
|
|
26
|
+
return currentLocale;
|
|
27
|
+
}
|
|
28
|
+
const localeFromEnv = process.env.WEAPP_IDE_CLI_LANG || process.env.LANG;
|
|
29
|
+
if (localeFromEnv) {
|
|
30
|
+
setLocale(localeFromEnv);
|
|
31
|
+
return currentLocale;
|
|
32
|
+
}
|
|
33
|
+
setLocale("zh");
|
|
34
|
+
return currentLocale;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* @description 根据当前语言选择文案。
|
|
38
|
+
*/
|
|
39
|
+
function i18nText(zh, en) {
|
|
40
|
+
return currentLocale === "en" ? en : zh;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* @description 校验 --lang 参数是否合法。
|
|
44
|
+
*/
|
|
45
|
+
function validateLocaleOption(argv) {
|
|
46
|
+
const localeFromArgv = readLangOption(argv);
|
|
47
|
+
if (!localeFromArgv) return;
|
|
48
|
+
if (!isLocaleTokenSupported(localeFromArgv)) throw new Error(`不支持的语言: ${localeFromArgv},仅支持 zh 或 en`);
|
|
49
|
+
}
|
|
50
|
+
function normalizeLocale(raw) {
|
|
51
|
+
if (!raw) return "zh";
|
|
52
|
+
const normalized = raw.trim().toLowerCase();
|
|
53
|
+
if (normalized === "en" || normalized.startsWith("en_") || normalized.startsWith("en-")) return "en";
|
|
54
|
+
return "zh";
|
|
55
|
+
}
|
|
56
|
+
function isLocaleTokenSupported(raw) {
|
|
57
|
+
const normalized = raw.trim().toLowerCase();
|
|
58
|
+
return normalized === "zh" || normalized === "en" || normalized.startsWith("zh_") || normalized.startsWith("zh-") || normalized.startsWith("en_") || normalized.startsWith("en-");
|
|
59
|
+
}
|
|
60
|
+
function readLangOption(argv) {
|
|
61
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
62
|
+
const token = argv[index];
|
|
63
|
+
if (!token) continue;
|
|
64
|
+
if (token === "--lang") {
|
|
65
|
+
const value = argv[index + 1];
|
|
66
|
+
return typeof value === "string" ? value : void 0;
|
|
67
|
+
}
|
|
68
|
+
if (token.startsWith("--lang=")) return token.slice(7);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region src/cli/inputCoordinator.ts
|
|
73
|
+
const sharedSessions = /* @__PURE__ */ new Set();
|
|
74
|
+
const exclusiveKeypressStack = [];
|
|
75
|
+
const EXCLUSIVE_KEYPRESS_CARRYOVER_GUARD_MS = 500;
|
|
76
|
+
let initialized = false;
|
|
77
|
+
let rawModeEnabled = false;
|
|
78
|
+
let dataListenerAttached = false;
|
|
79
|
+
let keypressListenerAttached = false;
|
|
80
|
+
let lastExclusiveResolvedKeypress;
|
|
81
|
+
function hasInteractiveStdin() {
|
|
82
|
+
return Boolean(process.stdin?.isTTY);
|
|
83
|
+
}
|
|
84
|
+
function canSetRawMode() {
|
|
85
|
+
return typeof process.stdin?.setRawMode === "function";
|
|
86
|
+
}
|
|
87
|
+
function updateTerminalState() {
|
|
88
|
+
const shouldKeepInputActive = Array.from(sharedSessions).some((session) => !session.closed && !session.suspended) || exclusiveKeypressStack.length > 0;
|
|
89
|
+
if (canSetRawMode() && rawModeEnabled !== shouldKeepInputActive) {
|
|
90
|
+
process.stdin.setRawMode(shouldKeepInputActive);
|
|
91
|
+
rawModeEnabled = shouldKeepInputActive;
|
|
92
|
+
}
|
|
93
|
+
if (shouldKeepInputActive) {
|
|
94
|
+
process.stdin.resume();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
process.stdin.pause();
|
|
98
|
+
}
|
|
99
|
+
function handleData(chunk) {
|
|
100
|
+
if (exclusiveKeypressStack.length > 0) return;
|
|
101
|
+
for (const session of sharedSessions) {
|
|
102
|
+
if (session.closed || session.suspended || !session.options.onData) continue;
|
|
103
|
+
session.options.onData(chunk);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function handleKeypress(str, key) {
|
|
107
|
+
const activeExclusive = exclusiveKeypressStack[exclusiveKeypressStack.length - 1];
|
|
108
|
+
if (activeExclusive) {
|
|
109
|
+
const now = Date.now();
|
|
110
|
+
const signature = `${key?.ctrl ? "ctrl+" : ""}${key?.name ?? str}`;
|
|
111
|
+
if (lastExclusiveResolvedKeypress && lastExclusiveResolvedKeypress.signature === signature && now - lastExclusiveResolvedKeypress.timestamp <= EXCLUSIVE_KEYPRESS_CARRYOVER_GUARD_MS) {
|
|
112
|
+
lastExclusiveResolvedKeypress.timestamp = now;
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (now < activeExclusive.ignoreUntil) return;
|
|
116
|
+
const nextValue = activeExclusive.onKeypress(str, key);
|
|
117
|
+
if (nextValue !== void 0) {
|
|
118
|
+
clearTimeout(activeExclusive.timeout);
|
|
119
|
+
exclusiveKeypressStack.pop();
|
|
120
|
+
lastExclusiveResolvedKeypress = {
|
|
121
|
+
signature,
|
|
122
|
+
timestamp: now
|
|
123
|
+
};
|
|
124
|
+
updateTerminalState();
|
|
125
|
+
activeExclusive.resolve(nextValue);
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
for (const session of sharedSessions) {
|
|
130
|
+
if (session.closed || session.suspended || !session.options.onKeypress) continue;
|
|
131
|
+
session.options.onKeypress(str, key);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function ensureCoordinatorInitialized() {
|
|
135
|
+
if (initialized || !hasInteractiveStdin()) return;
|
|
136
|
+
emitKeypressEvents(process.stdin);
|
|
137
|
+
if (!dataListenerAttached) {
|
|
138
|
+
process.stdin.on("data", handleData);
|
|
139
|
+
dataListenerAttached = true;
|
|
140
|
+
}
|
|
141
|
+
if (!keypressListenerAttached) {
|
|
142
|
+
process.stdin.on("keypress", handleKeypress);
|
|
143
|
+
keypressListenerAttached = true;
|
|
144
|
+
}
|
|
145
|
+
initialized = true;
|
|
146
|
+
}
|
|
147
|
+
function createSharedInputSession(options) {
|
|
148
|
+
if (!hasInteractiveStdin()) return;
|
|
149
|
+
ensureCoordinatorInitialized();
|
|
150
|
+
const session = {
|
|
151
|
+
closed: false,
|
|
152
|
+
options,
|
|
153
|
+
suspended: false
|
|
154
|
+
};
|
|
155
|
+
sharedSessions.add(session);
|
|
156
|
+
updateTerminalState();
|
|
157
|
+
return {
|
|
158
|
+
close() {
|
|
159
|
+
if (session.closed) return;
|
|
160
|
+
session.closed = true;
|
|
161
|
+
sharedSessions.delete(session);
|
|
162
|
+
updateTerminalState();
|
|
163
|
+
},
|
|
164
|
+
resume() {
|
|
165
|
+
if (session.closed) return;
|
|
166
|
+
session.suspended = false;
|
|
167
|
+
updateTerminalState();
|
|
168
|
+
},
|
|
169
|
+
suspend() {
|
|
170
|
+
if (session.closed) return;
|
|
171
|
+
session.suspended = true;
|
|
172
|
+
updateTerminalState();
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
async function waitForExclusiveKeypress(options) {
|
|
177
|
+
options.signal?.throwIfAborted();
|
|
178
|
+
if (!hasInteractiveStdin()) return "timeout";
|
|
179
|
+
ensureCoordinatorInitialized();
|
|
180
|
+
return await new Promise((resolve, reject) => {
|
|
181
|
+
const normalizedTimeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs && options.timeoutMs > 0 ? options.timeoutMs : 3e4;
|
|
182
|
+
const normalizedIgnoreInitialMs = Number.isFinite(options.ignoreInitialMs) && options.ignoreInitialMs && options.ignoreInitialMs > 0 ? options.ignoreInitialMs : 0;
|
|
183
|
+
let onAbort;
|
|
184
|
+
const record = {
|
|
185
|
+
ignoreUntil: Date.now() + normalizedIgnoreInitialMs,
|
|
186
|
+
onKeypress: options.onKeypress,
|
|
187
|
+
resolve: (value) => {
|
|
188
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
189
|
+
resolve(value);
|
|
190
|
+
},
|
|
191
|
+
timeout: setTimeout(() => {
|
|
192
|
+
const index = exclusiveKeypressStack.lastIndexOf(record);
|
|
193
|
+
if (index >= 0) exclusiveKeypressStack.splice(index, 1);
|
|
194
|
+
updateTerminalState();
|
|
195
|
+
record.resolve("timeout");
|
|
196
|
+
}, normalizedTimeoutMs)
|
|
197
|
+
};
|
|
198
|
+
onAbort = () => {
|
|
199
|
+
clearTimeout(record.timeout);
|
|
200
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
201
|
+
const index = exclusiveKeypressStack.indexOf(record);
|
|
202
|
+
if (index >= 0) exclusiveKeypressStack.splice(index, 1);
|
|
203
|
+
updateTerminalState();
|
|
204
|
+
reject(options.signal?.reason);
|
|
205
|
+
};
|
|
206
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
207
|
+
exclusiveKeypressStack.push(record);
|
|
208
|
+
updateTerminalState();
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
async function runWithSuspendedSharedInput(runner) {
|
|
212
|
+
const previousStates = /* @__PURE__ */ new Map();
|
|
213
|
+
for (const session of sharedSessions) {
|
|
214
|
+
if (session.closed) continue;
|
|
215
|
+
previousStates.set(session, session.suspended);
|
|
216
|
+
session.suspended = true;
|
|
217
|
+
}
|
|
218
|
+
if (canSetRawMode() && rawModeEnabled) {
|
|
219
|
+
process.stdin.setRawMode(false);
|
|
220
|
+
rawModeEnabled = false;
|
|
221
|
+
}
|
|
222
|
+
process.stdin.resume();
|
|
223
|
+
try {
|
|
224
|
+
return await runner();
|
|
225
|
+
} finally {
|
|
226
|
+
for (const [session, suspended] of previousStates) if (!session.closed) session.suspended = suspended;
|
|
227
|
+
updateTerminalState();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
//#endregion
|
|
231
|
+
//#region src/cli/retry.ts
|
|
232
|
+
const RETRY_PROMPT_INITIAL_IGNORE_MS = 300;
|
|
233
|
+
const RETRY_CONFIRM_KEYS = ["y"];
|
|
234
|
+
const RETRY_CANCEL_KEYS = [
|
|
235
|
+
"q",
|
|
236
|
+
"Esc",
|
|
237
|
+
"Ctrl+C"
|
|
238
|
+
];
|
|
239
|
+
const LOGIN_REQUIRED_PATTERNS = [
|
|
240
|
+
/code\s*[:=]\s*10/i,
|
|
241
|
+
/需要重新登录/,
|
|
242
|
+
/need\s+re-?login/i,
|
|
243
|
+
/re-?login/i
|
|
244
|
+
];
|
|
245
|
+
/**
|
|
246
|
+
* @description 提取执行错误文本,便于统一匹配与提示。
|
|
247
|
+
*/
|
|
248
|
+
function extractExecutionErrorText(error) {
|
|
249
|
+
if (!error || typeof error !== "object") return "";
|
|
250
|
+
const parts = [];
|
|
251
|
+
const candidate = error;
|
|
252
|
+
for (const field of [
|
|
253
|
+
candidate.message,
|
|
254
|
+
candidate.shortMessage,
|
|
255
|
+
candidate.stderr,
|
|
256
|
+
candidate.stdout
|
|
257
|
+
]) if (typeof field === "string" && field.trim()) parts.push(field);
|
|
258
|
+
return parts.join("\n");
|
|
259
|
+
}
|
|
260
|
+
function extractLoginRequiredMessage(text) {
|
|
261
|
+
if (!text) return "";
|
|
262
|
+
if (/需要重新登录/.test(text)) return "需要重新登录";
|
|
263
|
+
const englishMatch = text.match(/need\s+re-?login|re-?login/i);
|
|
264
|
+
if (englishMatch?.[0]) return englishMatch[0].toLowerCase();
|
|
265
|
+
const firstLine = text.split(/\r?\n/).map((line) => line.trim()).find((line) => Boolean(line) && !line.startsWith("at "));
|
|
266
|
+
if (!firstLine) return "";
|
|
267
|
+
return firstLine.replace(/^\[error\]\s*/i, "").replace(/^error\s*:\s*/i, "").slice(0, 120);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* @description 判断是否为微信开发者工具登录失效错误。
|
|
271
|
+
*/
|
|
272
|
+
function isWechatIdeLoginRequiredError(error) {
|
|
273
|
+
const text = extractExecutionErrorText(error);
|
|
274
|
+
if (!text) return false;
|
|
275
|
+
return LOGIN_REQUIRED_PATTERNS.some((pattern) => pattern.test(text));
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* @description 将登录失效错误格式化为更易读的摘要。
|
|
279
|
+
*/
|
|
280
|
+
function formatWechatIdeLoginRequiredError(error) {
|
|
281
|
+
const text = extractExecutionErrorText(error);
|
|
282
|
+
const code = text.match(/code\s*[:=]\s*(\d+)/i)?.[1];
|
|
283
|
+
const message = extractLoginRequiredMessage(text);
|
|
284
|
+
const lines = ["微信开发者工具返回登录错误:"];
|
|
285
|
+
if (code) lines.push(`- code: ${code}`);
|
|
286
|
+
if (message) lines.push(`- message: ${message}`);
|
|
287
|
+
if (!code && !message) lines.push("- message: 需要重新登录");
|
|
288
|
+
return lines.join("\n");
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* @description 创建登录失效专用错误,并携带退出码语义。
|
|
292
|
+
*/
|
|
293
|
+
function createWechatIdeLoginRequiredExitError(error, reason) {
|
|
294
|
+
const summary = formatWechatIdeLoginRequiredError(error);
|
|
295
|
+
const message = reason ? `${reason}\n${summary}` : summary;
|
|
296
|
+
const loginError = new Error(message, { cause: error });
|
|
297
|
+
loginError.name = "WechatIdeLoginRequiredError";
|
|
298
|
+
loginError.code = 10;
|
|
299
|
+
loginError.exitCode = 10;
|
|
300
|
+
return loginError;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* @description 判断错误是否为登录失效专用退出错误。
|
|
304
|
+
*/
|
|
305
|
+
function isWechatIdeLoginRequiredExitError(error) {
|
|
306
|
+
if (!error || typeof error !== "object") return false;
|
|
307
|
+
const candidate = error;
|
|
308
|
+
return candidate.name === "WechatIdeLoginRequiredError" || candidate.code === 10 || candidate.exitCode === 10;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* @description 交互等待用户按 y 重试,按 q 或 Ctrl+C 取消。
|
|
312
|
+
*/
|
|
313
|
+
async function waitForRetryKeypress(options = {}) {
|
|
314
|
+
const { timeoutMs = 3e4 } = options;
|
|
315
|
+
if (!process.stdin.isTTY) return "cancel";
|
|
316
|
+
return await waitForExclusiveKeypress({
|
|
317
|
+
signal: options.signal,
|
|
318
|
+
ignoreInitialMs: 300,
|
|
319
|
+
onKeypress: (_str, key) => {
|
|
320
|
+
if (!key) return;
|
|
321
|
+
if (key.ctrl && key.name === "c") return "cancel";
|
|
322
|
+
if (key.name === "y") return "retry";
|
|
323
|
+
if (key.name === "q" || key.name === "escape") return "cancel";
|
|
324
|
+
},
|
|
325
|
+
timeoutMs
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
function highlightHotkey(key) {
|
|
329
|
+
return colors.bold(colors.green(key));
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* @description 生成重试按键提示,并高亮关键热键。
|
|
333
|
+
*/
|
|
334
|
+
function formatRetryHotkeyPrompt(timeoutMs = 3e4) {
|
|
335
|
+
const highlight = (key) => highlightHotkey(key);
|
|
336
|
+
const timeoutSeconds = Math.max(1, Math.ceil(timeoutMs / 1e3));
|
|
337
|
+
const confirmKeys = RETRY_CONFIRM_KEYS.map(highlight).join(" / ");
|
|
338
|
+
const cancelKeys = RETRY_CANCEL_KEYS.map(highlight).join(" / ");
|
|
339
|
+
return i18nText(`按 ${confirmKeys} 重试,按 ${cancelKeys} 退出(${timeoutSeconds}s 内无输入将自动失败)。`, `Press ${confirmKeys} to retry, ${cancelKeys} to cancel (auto fail in ${timeoutSeconds}s).`);
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* @description 输出重试热键提示,并独占等待当前提示对应的按键输入。
|
|
343
|
+
*/
|
|
344
|
+
async function promptRetryKeypress(options) {
|
|
345
|
+
const { logger, timeoutMs = 3e4 } = options;
|
|
346
|
+
logger.info(formatRetryHotkeyPrompt(timeoutMs));
|
|
347
|
+
return await waitForRetryKeypress({
|
|
348
|
+
timeoutMs,
|
|
349
|
+
signal: options.signal
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* @description 统一处理微信开发者工具登录失效场景下的提示与重试交互。
|
|
354
|
+
*/
|
|
355
|
+
async function promptWechatIdeLoginRetry(options) {
|
|
356
|
+
const { allowRetry = true, cancelLevel = "info", error, logger, promptOpenIdeLogin = false, retryTimeoutMs = 3e4 } = options;
|
|
357
|
+
logger.error(i18nText("检测到微信开发者工具登录状态失效,请先登录后重试。", "Wechat DevTools login has expired. Please login and retry."));
|
|
358
|
+
if (promptOpenIdeLogin) logger.warn(i18nText("请先打开微信开发者工具完成登录。", "Please open Wechat DevTools and complete login first."));
|
|
359
|
+
logger.warn(formatWechatIdeLoginRequiredError(error));
|
|
360
|
+
if (!allowRetry) return "cancel";
|
|
361
|
+
const action = await promptRetryKeypress({
|
|
362
|
+
logger,
|
|
363
|
+
timeoutMs: retryTimeoutMs,
|
|
364
|
+
signal: options.signal
|
|
365
|
+
});
|
|
366
|
+
if (action === "timeout") {
|
|
367
|
+
logger.error(i18nText(`等待登录重试输入超时(${retryTimeoutMs}ms),已自动取消。`, `Retry prompt timed out (${retryTimeoutMs}ms), canceled automatically.`));
|
|
368
|
+
return "cancel";
|
|
369
|
+
}
|
|
370
|
+
if (action !== "retry") {
|
|
371
|
+
logger[cancelLevel](i18nText("已取消重试。完成登录后请重新执行当前命令。", "Retry canceled. Please run the command again after login."));
|
|
372
|
+
return "cancel";
|
|
373
|
+
}
|
|
374
|
+
return "retry";
|
|
375
|
+
}
|
|
376
|
+
//#endregion
|
|
377
|
+
//#region src/cli/run-login-executor.ts
|
|
378
|
+
/** 命令、登录提示和有限重试共用一个 deadline,取消信号由执行边界传入 I/O。 */
|
|
379
|
+
async function runRetryableCommand(options) {
|
|
380
|
+
return await new OperationLifecycle(options.timeout ?? 3e4, "IDE command", options.signal).run(async (scope) => {
|
|
381
|
+
const maxAttempts = options.maxAttempts ?? 3;
|
|
382
|
+
for (let retryCount = 0;; retryCount += 1) {
|
|
383
|
+
scope.attempt();
|
|
384
|
+
const result = await scope.step(() => options.execute(scope), {
|
|
385
|
+
stage: "execute",
|
|
386
|
+
waitForExit: true,
|
|
387
|
+
disposeLate: options.disposeLate
|
|
388
|
+
});
|
|
389
|
+
if (!options.isRetryableResult(result)) return result;
|
|
390
|
+
const cause = options.createCancelError(result);
|
|
391
|
+
scope.recordFailure(cause);
|
|
392
|
+
if (retryCount + 1 >= maxAttempts) throw cause;
|
|
393
|
+
const action = await scope.step(() => options.promptRetry(result, retryCount, scope), {
|
|
394
|
+
stage: "login-retry",
|
|
395
|
+
waitForExit: true
|
|
396
|
+
});
|
|
397
|
+
if (options.shouldRetry(action)) {
|
|
398
|
+
scope.throwIfAborted();
|
|
399
|
+
options.onRetry?.();
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
options.onCancel?.(result);
|
|
403
|
+
throw cause;
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
//#endregion
|
|
408
|
+
//#region src/cli/automator-session.ts
|
|
409
|
+
function unwrapAutomatorConnectionError(result) {
|
|
410
|
+
return result.kind === "retryable" ? result.error : result.value;
|
|
411
|
+
}
|
|
412
|
+
async function launchFreshAutomator(options) {
|
|
413
|
+
const shouldUseProjectPort = !options.port && !options.sessionId;
|
|
414
|
+
return await launchAutomator({
|
|
415
|
+
...options,
|
|
416
|
+
...shouldUseProjectPort ? {
|
|
417
|
+
persistAsDefaultSession: true,
|
|
418
|
+
port: resolveProjectAutomatorPort(options.projectPath)
|
|
419
|
+
} : {}
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
function normalizeMiniProgramConnectionError(error, background = false) {
|
|
423
|
+
const diagnostics = background ? {
|
|
424
|
+
error: (message) => logger.debug(message),
|
|
425
|
+
warn: (message) => logger.debug(message)
|
|
426
|
+
} : logger;
|
|
427
|
+
if (isAutomatorLoginError(error)) {
|
|
428
|
+
diagnostics.error(i18nText("检测到微信开发者工具登录状态失效,请先登录后重试。", "Wechat DevTools login has expired. Please login and retry."));
|
|
429
|
+
diagnostics.warn(formatAutomatorLoginError(error));
|
|
430
|
+
return new Error("DEVTOOLS_LOGIN_REQUIRED", { cause: error });
|
|
431
|
+
}
|
|
432
|
+
if (isDevtoolsHttpPortError(error)) {
|
|
433
|
+
diagnostics.error(i18nText("无法连接到微信开发者工具,请确保已开启 HTTP 服务端口。", "Cannot connect to Wechat DevTools. Please ensure HTTP service port is enabled."));
|
|
434
|
+
diagnostics.warn(i18nText("请在微信开发者工具中:设置 -> 安全设置 -> 开启服务端口", "Please enable service port in Wechat DevTools: Settings -> Security -> Service Port"));
|
|
435
|
+
return new Error("DEVTOOLS_HTTP_PORT_ERROR", { cause: error });
|
|
436
|
+
}
|
|
437
|
+
if (isDevtoolsExtensionContextInvalidatedError(error)) {
|
|
438
|
+
diagnostics.error(i18nText("微信开发者工具自动化上下文尚未就绪,通常是刚启动或正在重载。", "Wechat DevTools automation context is not ready yet, usually because the IDE has just started or is still reloading."));
|
|
439
|
+
diagnostics.warn(i18nText("请稍后重试;若持续失败,关闭多余的开发者工具窗口后重试。", "Please retry shortly. If it keeps failing, close extra DevTools windows and try again."));
|
|
440
|
+
return new Error("DEVTOOLS_EXTENSION_CONTEXT_INVALIDATED", { cause: error });
|
|
441
|
+
}
|
|
442
|
+
if (isAutomatorWsConnectError(error) || isAutomatorPortInUseError(error)) {
|
|
443
|
+
diagnostics.error(i18nText("无法连接到当前项目的微信开发者工具自动化 websocket。", "Cannot connect to the Wechat DevTools automation websocket for the current project."));
|
|
444
|
+
diagnostics.warn(i18nText("请确认当前打开的是目标项目;若之前跑过其他 e2e / screenshot 任务,关闭多余的微信开发者工具窗口,或结束残留的 `wechatwebdevtools cli auto --project ...` 进程后重试。", "Please confirm the current DevTools window is the target project. If you recently ran other e2e / screenshot tasks, close extra windows or stop stale `wechatwebdevtools cli auto --project ...` processes and retry."));
|
|
445
|
+
return new Error("DEVTOOLS_WS_CONNECT_ERROR", { cause: error });
|
|
446
|
+
}
|
|
447
|
+
if (isAutomatorProtocolTimeoutError(error)) {
|
|
448
|
+
const method = getAutomatorProtocolTimeoutMethod(error) ?? "unknown";
|
|
449
|
+
diagnostics.error(i18nText(`微信开发者工具在协议调用 ${method} 上超时,未按预期返回结果。`, `Wechat DevTools timed out while executing protocol method ${method} and did not return a result.`));
|
|
450
|
+
diagnostics.warn(i18nText("这通常表示当前 DevTools 自动化会话已卡住、窗口不在目标项目、或当前 DevTools 版本对该协议调用无响应。请重开目标项目窗口后重试;若仍复现,优先记录当前 DevTools 版本与协议方法名继续排查。", "This usually means the current DevTools automation session is stuck, the window is not on the target project, or the current DevTools version is not responding to that protocol method. Reopen the target project window and retry. If it still reproduces, record the current DevTools version and protocol method name for follow-up debugging."));
|
|
451
|
+
return new Error("DEVTOOLS_PROTOCOL_TIMEOUT", { cause: error });
|
|
452
|
+
}
|
|
453
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* @description 建立 automator 会话,并统一处理常见连接错误提示。
|
|
457
|
+
*/
|
|
458
|
+
async function connectMiniProgramWithDiagnostics(options, background) {
|
|
459
|
+
options = await resolveAutomatorSessionOptions(options);
|
|
460
|
+
const result = await runRetryableCommand({
|
|
461
|
+
timeout: options.timeout,
|
|
462
|
+
signal: options.signal,
|
|
463
|
+
disposeLate: (result) => {
|
|
464
|
+
if (result.kind === "result") result.value.disconnect();
|
|
465
|
+
},
|
|
466
|
+
createCancelError: (result) => createWechatIdeLoginRequiredExitError(unwrapAutomatorConnectionError(result), "cancelled"),
|
|
467
|
+
execute: async (operation) => {
|
|
468
|
+
const operationOptions = () => ({
|
|
469
|
+
...options,
|
|
470
|
+
timeout: operation.remainingMs(),
|
|
471
|
+
signal: operation.signal
|
|
472
|
+
});
|
|
473
|
+
if (options.runtimeProvider === "headless" || options.preferOpenedSession === false && options.openedOnly !== true) try {
|
|
474
|
+
return {
|
|
475
|
+
kind: "result",
|
|
476
|
+
value: await launchFreshAutomator(operationOptions())
|
|
477
|
+
};
|
|
478
|
+
} catch (error) {
|
|
479
|
+
if (!isAutomatorLoginError(error)) throw normalizeMiniProgramConnectionError(error, background);
|
|
480
|
+
return {
|
|
481
|
+
error,
|
|
482
|
+
kind: "retryable"
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
try {
|
|
486
|
+
return {
|
|
487
|
+
kind: "result",
|
|
488
|
+
value: await connectOpenedAutomator(operationOptions())
|
|
489
|
+
};
|
|
490
|
+
} catch (error) {
|
|
491
|
+
const normalizedOpenSessionError = normalizeMiniProgramConnectionError(error, background);
|
|
492
|
+
if (options.openedOnly || options.preferOpenedSession !== false && Boolean(options.port || options.sessionId)) throw normalizedOpenSessionError;
|
|
493
|
+
if (normalizedOpenSessionError instanceof Error && ("code" in normalizedOpenSessionError && normalizedOpenSessionError.code === "WECHAT_DEVTOOLS_HOST_IDENTITY_MISMATCH" || normalizedOpenSessionError.message === "DEVTOOLS_PROTOCOL_TIMEOUT" || normalizedOpenSessionError.message.startsWith("DEVTOOLS_INSTALLATION_MISMATCH") || normalizedOpenSessionError.message.startsWith("DEVTOOLS_PORT_IDENTITY"))) throw normalizedOpenSessionError;
|
|
494
|
+
try {
|
|
495
|
+
return {
|
|
496
|
+
kind: "result",
|
|
497
|
+
value: await launchFreshAutomator(operationOptions())
|
|
498
|
+
};
|
|
499
|
+
} catch (launchError) {
|
|
500
|
+
if (!isAutomatorLoginError(launchError)) throw normalizeMiniProgramConnectionError(launchError, background);
|
|
501
|
+
return {
|
|
502
|
+
error: launchError,
|
|
503
|
+
kind: "retryable"
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
},
|
|
508
|
+
isRetryableResult: (result) => result.kind === "retryable",
|
|
509
|
+
onRetry: () => {
|
|
510
|
+
logger.info(i18nText("正在重试连接微信开发者工具...", "Retrying to connect Wechat DevTools..."));
|
|
511
|
+
},
|
|
512
|
+
promptRetry: async (result, _retryCount, operation) => await promptWechatIdeLoginRetry({
|
|
513
|
+
error: unwrapAutomatorConnectionError(result),
|
|
514
|
+
retryTimeoutMs: operation.remainingMs(),
|
|
515
|
+
signal: operation.signal,
|
|
516
|
+
logger,
|
|
517
|
+
promptOpenIdeLogin: true
|
|
518
|
+
}),
|
|
519
|
+
shouldRetry: (action) => action === "retry"
|
|
520
|
+
});
|
|
521
|
+
if (result.kind === "retryable") throw createWechatIdeLoginRequiredExitError(result.error);
|
|
522
|
+
return result.value;
|
|
523
|
+
}
|
|
524
|
+
/** 建立交互式会话,并报告最终连接错误。 */
|
|
525
|
+
async function connectMiniProgram(options) {
|
|
526
|
+
return await connectMiniProgramWithDiagnostics(options, false);
|
|
527
|
+
}
|
|
528
|
+
/** 后台日志桥接的连接探针,不在每次可恢复失败时输出终局错误。 */
|
|
529
|
+
async function connectConsoleMiniProgram(options) {
|
|
530
|
+
return await connectMiniProgramWithDiagnostics(options, true);
|
|
531
|
+
}
|
|
532
|
+
/** 为后台日志桥接保留共享会话所有权和诊断级别。 */
|
|
533
|
+
async function acquireConsoleMiniProgram(options) {
|
|
534
|
+
return await acquireSharedMiniProgram({
|
|
535
|
+
resolveSessionOptions: resolveAutomatorSessionOptions,
|
|
536
|
+
connectMiniProgram: connectConsoleMiniProgram,
|
|
537
|
+
normalizeConnectionError: (error) => normalizeMiniProgramConnectionError(error, true)
|
|
538
|
+
}, options);
|
|
539
|
+
}
|
|
540
|
+
const runtimeHooks = {
|
|
541
|
+
resolveSessionOptions: resolveAutomatorSessionOptions,
|
|
542
|
+
connectMiniProgram,
|
|
543
|
+
normalizeConnectionError: normalizeMiniProgramConnectionError
|
|
544
|
+
};
|
|
545
|
+
/**
|
|
546
|
+
* @description 获取指定项目的共享 automator 会话;若不存在则自动创建。
|
|
547
|
+
*/
|
|
548
|
+
async function acquireSharedMiniProgram$1(options) {
|
|
549
|
+
return await acquireSharedMiniProgram(runtimeHooks, options);
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* @description 统一管理 automator 会话生命周期。
|
|
553
|
+
*/
|
|
554
|
+
async function withMiniProgram$1(options, runner) {
|
|
555
|
+
return await withMiniProgram(runtimeHooks, options, runner);
|
|
556
|
+
}
|
|
557
|
+
//#endregion
|
|
558
|
+
export { runWithSuspendedSharedInput as C, validateLocaleOption as D, i18nText as E, createSharedInputSession as S, configureLocaleFromArgv as T, isWechatIdeLoginRequiredError as _, connectMiniProgram as a, promptWechatIdeLoginRetry as b, withMiniProgram$1 as c, RETRY_CONFIRM_KEYS as d, RETRY_PROMPT_INITIAL_IGNORE_MS as f, formatWechatIdeLoginRequiredError as g, formatRetryHotkeyPrompt as h, connectConsoleMiniProgram as i, runRetryableCommand as l, extractExecutionErrorText as m, acquireSharedMiniProgram$1 as n, getSharedMiniProgramSessionCount as o, createWechatIdeLoginRequiredExitError as p, closeSharedMiniProgram as r, releaseSharedMiniProgram as s, acquireConsoleMiniProgram as t, RETRY_CANCEL_KEYS as u, isWechatIdeLoginRequiredExitError as v, waitForExclusiveKeypress as w, waitForRetryKeypress as x, promptRetryKeypress as y };
|