helloloop 0.8.4 → 0.8.6

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/src/process.mjs CHANGED
@@ -1,591 +1,7 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
1
+ import { runEngineTask } from "./runtime_engine_task.mjs";
2
+ import { runShellCommand, runVerifyCommands } from "./verify_runner.mjs";
3
3
 
4
- import { ensureDir, nowIso, tailText, writeJson, writeText } from "./common.mjs";
5
- import { getEngineDisplayName, normalizeEngineName } from "./engine_metadata.mjs";
6
- import {
7
- buildClaudeArgs,
8
- buildCodexArgs,
9
- buildGeminiArgs,
10
- resolveEngineInvocation,
11
- resolveVerifyInvocation,
12
- runChild,
13
- } from "./engine_process_support.mjs";
14
- import { sendRuntimeStopNotification } from "./email_notification.mjs";
15
- import { loadGlobalConfig } from "./global_config.mjs";
16
- import {
17
- buildEngineHealthProbePrompt,
18
- buildRuntimeRecoveryPrompt,
19
- classifyRuntimeRecoveryFailure,
20
- renderRuntimeRecoverySummary,
21
- resolveRuntimeRecoveryPolicy,
22
- selectRuntimeRecoveryDelayMs,
23
- } from "./runtime_recovery.mjs";
24
-
25
- function sleep(ms) {
26
- return new Promise((resolve) => {
27
- setTimeout(resolve, ms);
28
- });
29
- }
30
-
31
- function createRuntimeStatusWriter(runtimeStatusFile, baseState) {
32
- return function writeRuntimeStatus(status, extra = {}) {
33
- writeJson(runtimeStatusFile, {
34
- ...baseState,
35
- ...extra,
36
- status,
37
- updatedAt: nowIso(),
38
- });
39
- };
40
- }
41
-
42
- function writeEngineRunArtifacts(runDir, prefix, result, finalMessage) {
43
- writeText(path.join(runDir, `${prefix}-stdout.log`), result.stdout);
44
- writeText(path.join(runDir, `${prefix}-stderr.log`), result.stderr);
45
- writeText(path.join(runDir, `${prefix}-summary.txt`), [
46
- `ok=${result.ok}`,
47
- `code=${result.code}`,
48
- `finished_at=${nowIso()}`,
49
- "",
50
- finalMessage,
51
- ].join("\n"));
52
- }
53
-
54
- function resolveEnginePolicy(policy = {}, engine) {
55
- if (engine === "codex") {
56
- return policy.codex || {};
57
- }
58
- if (engine === "claude") {
59
- return policy.claude || {};
60
- }
61
- if (engine === "gemini") {
62
- return policy.gemini || {};
63
- }
64
- return {};
65
- }
66
-
67
- function buildEngineArgs({
68
- engine,
69
- context,
70
- resolvedPolicy,
71
- executionMode,
72
- outputSchemaFile,
73
- ephemeral,
74
- skipGitRepoCheck,
75
- lastMessageFile,
76
- probeMode = false,
77
- }) {
78
- if (engine === "codex") {
79
- return buildCodexArgs({
80
- context,
81
- model: resolvedPolicy.model,
82
- sandbox: resolvedPolicy.sandbox,
83
- dangerouslyBypassSandbox: resolvedPolicy.dangerouslyBypassSandbox,
84
- jsonOutput: probeMode ? false : (resolvedPolicy.jsonOutput !== false),
85
- outputSchemaFile: probeMode ? "" : outputSchemaFile,
86
- ephemeral,
87
- skipGitRepoCheck,
88
- lastMessageFile,
89
- });
90
- }
91
-
92
- if (engine === "claude") {
93
- return buildClaudeArgs({
94
- model: resolvedPolicy.model,
95
- outputSchemaFile: probeMode ? "" : outputSchemaFile,
96
- executionMode: probeMode ? "execute" : executionMode,
97
- policy: resolvedPolicy,
98
- });
99
- }
100
-
101
- return buildGeminiArgs({
102
- model: resolvedPolicy.model,
103
- executionMode: probeMode ? "execute" : executionMode,
104
- policy: resolvedPolicy,
105
- });
106
- }
107
-
108
- function readEngineFinalMessage(engine, lastMessageFile, result) {
109
- if (engine === "codex") {
110
- return fs.existsSync(lastMessageFile)
111
- ? fs.readFileSync(lastMessageFile, "utf8").trim()
112
- : "";
113
- }
114
- return String(result.stdout || "").trim();
115
- }
116
-
117
- async function runEngineAttempt({
118
- engine,
119
- invocation,
120
- context,
121
- prompt,
122
- runDir,
123
- attemptPrefix,
124
- resolvedPolicy,
125
- executionMode,
126
- outputSchemaFile,
127
- env,
128
- recoveryPolicy,
129
- writeRuntimeStatus,
130
- recoveryCount,
131
- recoveryHistory,
132
- ephemeral = false,
133
- skipGitRepoCheck = false,
134
- probeMode = false,
135
- }) {
136
- const attemptPromptFile = path.join(runDir, `${attemptPrefix}-prompt.md`);
137
- const attemptLastMessageFile = path.join(runDir, `${attemptPrefix}-last-message.txt`);
138
-
139
- if (invocation.error) {
140
- const result = {
141
- ok: false,
142
- code: 1,
143
- stdout: "",
144
- stderr: invocation.error,
145
- signal: "",
146
- startedAt: nowIso(),
147
- finishedAt: nowIso(),
148
- idleTimeout: false,
149
- watchdogTriggered: false,
150
- watchdogReason: "",
151
- };
152
- writeText(attemptPromptFile, prompt);
153
- writeEngineRunArtifacts(runDir, attemptPrefix, result, "");
154
- return {
155
- result,
156
- finalMessage: "",
157
- attemptPrefix,
158
- };
159
- }
160
-
161
- const finalArgs = [
162
- ...invocation.argsPrefix,
163
- ...buildEngineArgs({
164
- engine,
165
- context,
166
- resolvedPolicy,
167
- executionMode,
168
- outputSchemaFile,
169
- ephemeral,
170
- skipGitRepoCheck,
171
- lastMessageFile: attemptLastMessageFile,
172
- probeMode,
173
- }),
174
- ];
175
-
176
- writeRuntimeStatus(probeMode ? "probe_running" : (recoveryCount > 0 ? "recovering" : "running"), {
177
- attemptPrefix,
178
- recoveryCount,
179
- recoveryHistory,
180
- });
181
-
182
- const result = await runChild(invocation.command, finalArgs, {
183
- cwd: context.repoRoot,
184
- stdin: prompt,
185
- env,
186
- shell: invocation.shell,
187
- heartbeatIntervalMs: recoveryPolicy.heartbeatIntervalSeconds * 1000,
188
- stallWarningMs: recoveryPolicy.stallWarningSeconds * 1000,
189
- maxIdleMs: recoveryPolicy.maxIdleSeconds * 1000,
190
- killGraceMs: recoveryPolicy.killGraceSeconds * 1000,
191
- onHeartbeat(payload) {
192
- writeRuntimeStatus(payload.status, {
193
- attemptPrefix,
194
- recoveryCount,
195
- recoveryHistory,
196
- heartbeat: payload,
197
- });
198
- },
199
- });
200
- const finalMessage = readEngineFinalMessage(engine, attemptLastMessageFile, result);
201
-
202
- writeText(attemptPromptFile, prompt);
203
- writeEngineRunArtifacts(runDir, attemptPrefix, result, finalMessage);
204
-
205
- return {
206
- result,
207
- finalMessage,
208
- attemptPrefix,
209
- };
210
- }
211
-
212
- async function runEngineHealthProbe({
213
- engine,
214
- invocation,
215
- context,
216
- runDir,
217
- resolvedPolicy,
218
- recoveryPolicy,
219
- writeRuntimeStatus,
220
- recoveryCount,
221
- recoveryHistory,
222
- env,
223
- probeIndex,
224
- }) {
225
- const probePrompt = buildEngineHealthProbePrompt(engine);
226
- const attemptPrefix = `${engine}-probe-${String(probeIndex).padStart(2, "0")}`;
227
- writeRuntimeStatus("probe_waiting", {
228
- attemptPrefix,
229
- recoveryCount,
230
- recoveryHistory,
231
- });
232
- const attempt = await runEngineAttempt({
233
- engine,
234
- invocation,
235
- context,
236
- prompt: probePrompt,
237
- runDir,
238
- attemptPrefix,
239
- resolvedPolicy,
240
- executionMode: "execute",
241
- outputSchemaFile: "",
242
- env,
243
- recoveryPolicy: {
244
- ...recoveryPolicy,
245
- maxIdleSeconds: recoveryPolicy.healthProbeTimeoutSeconds,
246
- },
247
- writeRuntimeStatus,
248
- recoveryCount,
249
- recoveryHistory,
250
- ephemeral: true,
251
- skipGitRepoCheck: true,
252
- probeMode: true,
253
- });
254
-
255
- return {
256
- ...attempt,
257
- failure: classifyRuntimeRecoveryFailure({
258
- result: {
259
- ...attempt.result,
260
- finalMessage: attempt.finalMessage,
261
- },
262
- }),
263
- };
264
- }
265
-
266
- async function maybeSendStopNotification({
267
- context,
268
- runDir,
269
- engine,
270
- executionMode,
271
- failure,
272
- result,
273
- recoveryHistory,
274
- }) {
275
- try {
276
- return await sendRuntimeStopNotification({
277
- globalConfig: loadGlobalConfig(),
278
- context,
279
- engine: getEngineDisplayName(engine),
280
- phase: executionMode === "analyze" ? "分析/复核" : "执行",
281
- failure,
282
- result,
283
- recoveryHistory,
284
- runDir,
285
- });
286
- } catch (error) {
287
- return {
288
- attempted: true,
289
- delivered: false,
290
- reason: String(error?.message || error || "邮件发送失败。"),
291
- };
292
- }
293
- }
294
-
295
- function buildNotificationNote(notificationResult) {
296
- if (!notificationResult) {
297
- return "";
298
- }
299
- if (notificationResult.delivered) {
300
- return `告警邮件已发送:${(notificationResult.recipients || []).join(", ")}`;
301
- }
302
- if (notificationResult.attempted) {
303
- return `告警邮件发送失败:${notificationResult.reason || "未知原因"}`;
304
- }
305
- return `未发送告警邮件:${notificationResult.reason || "未启用"}`;
306
- }
307
-
308
- export async function runEngineTask({
309
- engine = "codex",
310
- context,
311
- prompt,
312
- runDir,
313
- policy = {},
314
- executionMode = "analyze",
315
- outputSchemaFile = "",
316
- outputPrefix = "",
317
- ephemeral = false,
318
- skipGitRepoCheck = false,
319
- env = {},
320
- }) {
321
- ensureDir(runDir);
322
-
323
- const normalizedEngine = normalizeEngineName(engine) || "codex";
324
- const resolvedPolicy = resolveEnginePolicy(policy, normalizedEngine);
325
- const prefix = outputPrefix || normalizedEngine;
326
- const invocation = resolveEngineInvocation(normalizedEngine, resolvedPolicy.executable);
327
- const recoveryPolicy = resolveRuntimeRecoveryPolicy(policy);
328
- const runtimeStatusFile = path.join(runDir, `${prefix}-runtime.json`);
329
- const writeRuntimeStatus = createRuntimeStatusWriter(runtimeStatusFile, {
330
- engine: normalizedEngine,
331
- engineDisplayName: getEngineDisplayName(normalizedEngine),
332
- phase: executionMode,
333
- outputPrefix: prefix,
334
- hardRetryBudget: recoveryPolicy.hardRetryDelaysSeconds.length,
335
- softRetryBudget: recoveryPolicy.softRetryDelaysSeconds.length,
336
- });
337
-
338
- const recoveryHistory = [];
339
- let currentPrompt = prompt;
340
- let currentRecoveryCount = 0;
341
- let activeFailure = null;
342
-
343
- while (true) {
344
- const attemptPrefix = currentRecoveryCount === 0
345
- ? prefix
346
- : `${prefix}-recovery-${String(currentRecoveryCount).padStart(2, "0")}`;
347
- const taskAttempt = await runEngineAttempt({
348
- engine: normalizedEngine,
349
- invocation,
350
- context,
351
- prompt: currentPrompt,
352
- runDir,
353
- attemptPrefix,
354
- resolvedPolicy,
355
- executionMode,
356
- outputSchemaFile,
357
- env,
358
- recoveryPolicy,
359
- writeRuntimeStatus,
360
- recoveryCount: currentRecoveryCount,
361
- recoveryHistory,
362
- ephemeral,
363
- skipGitRepoCheck,
364
- probeMode: false,
365
- });
366
-
367
- const taskFailure = classifyRuntimeRecoveryFailure({
368
- result: {
369
- ...taskAttempt.result,
370
- finalMessage: taskAttempt.finalMessage,
371
- },
372
- });
373
-
374
- if (taskAttempt.result.ok || !recoveryPolicy.enabled) {
375
- const finalRecoverySummary = taskAttempt.result.ok
376
- ? ""
377
- : renderRuntimeRecoverySummary(recoveryHistory, taskFailure);
378
- const notification = taskAttempt.result.ok
379
- ? null
380
- : await maybeSendStopNotification({
381
- context,
382
- runDir,
383
- engine: normalizedEngine,
384
- executionMode,
385
- failure: taskFailure,
386
- result: taskAttempt.result,
387
- recoveryHistory,
388
- });
389
- const notificationNote = taskAttempt.result.ok ? "" : buildNotificationNote(notification);
390
- const finalizedResult = taskAttempt.result.ok
391
- ? taskAttempt.result
392
- : {
393
- ...taskAttempt.result,
394
- stderr: [
395
- taskAttempt.result.stderr,
396
- "",
397
- finalRecoverySummary,
398
- notificationNote,
399
- ].filter(Boolean).join("\n").trim(),
400
- };
401
-
402
- writeText(path.join(runDir, `${prefix}-prompt.md`), currentPrompt);
403
- writeEngineRunArtifacts(runDir, prefix, finalizedResult, taskAttempt.finalMessage);
404
- if (normalizedEngine === "codex" && taskAttempt.finalMessage) {
405
- writeText(path.join(runDir, `${prefix}-last-message.txt`), taskAttempt.finalMessage);
406
- }
407
- writeRuntimeStatus(taskAttempt.result.ok ? "completed" : "paused_manual", {
408
- attemptPrefix,
409
- recoveryCount: recoveryHistory.length,
410
- recoveryHistory,
411
- recoverySummary: finalRecoverySummary,
412
- finalMessage: taskAttempt.finalMessage,
413
- code: finalizedResult.code,
414
- failureCode: taskFailure.code,
415
- failureFamily: taskFailure.family,
416
- failureReason: taskFailure.reason,
417
- notification,
418
- });
419
-
420
- return {
421
- ...finalizedResult,
422
- finalMessage: taskAttempt.finalMessage,
423
- recoveryCount: recoveryHistory.length,
424
- recoveryHistory,
425
- recoverySummary: finalRecoverySummary,
426
- recoveryFailure: taskAttempt.result.ok
427
- ? null
428
- : {
429
- ...taskFailure,
430
- shouldStopTask: true,
431
- exhausted: true,
432
- },
433
- notification,
434
- };
435
- }
436
-
437
- activeFailure = taskFailure;
438
- while (true) {
439
- const nextRecoveryIndex = recoveryHistory.length + 1;
440
- const recoveryPrompt = buildRuntimeRecoveryPrompt({
441
- basePrompt: prompt,
442
- engine: normalizedEngine,
443
- phaseLabel: executionMode === "analyze" ? "分析/复核" : "执行",
444
- failure: activeFailure,
445
- result: {
446
- ...taskAttempt.result,
447
- finalMessage: taskAttempt.finalMessage,
448
- },
449
- nextRecoveryIndex,
450
- maxRecoveries: recoveryPolicy[activeFailure.family === "hard" ? "hardRetryDelaysSeconds" : "softRetryDelaysSeconds"].length,
451
- });
452
- writeText(
453
- path.join(runDir, `${prefix}-auto-recovery-${String(nextRecoveryIndex).padStart(2, "0")}-prompt.md`),
454
- recoveryPrompt,
455
- );
456
- const delayMs = selectRuntimeRecoveryDelayMs(recoveryPolicy, activeFailure.family, nextRecoveryIndex);
457
- if (delayMs < 0) {
458
- const finalRecoverySummary = renderRuntimeRecoverySummary(recoveryHistory, activeFailure);
459
- const notification = await maybeSendStopNotification({
460
- context,
461
- runDir,
462
- engine: normalizedEngine,
463
- executionMode,
464
- failure: activeFailure,
465
- result: taskAttempt.result,
466
- recoveryHistory,
467
- });
468
- const notificationNote = buildNotificationNote(notification);
469
- const finalizedResult = {
470
- ...taskAttempt.result,
471
- stderr: [
472
- taskAttempt.result.stderr,
473
- "",
474
- finalRecoverySummary,
475
- notificationNote,
476
- ].filter(Boolean).join("\n").trim(),
477
- };
478
-
479
- writeText(path.join(runDir, `${prefix}-prompt.md`), currentPrompt);
480
- writeEngineRunArtifacts(runDir, prefix, finalizedResult, taskAttempt.finalMessage);
481
- writeRuntimeStatus("paused_manual", {
482
- attemptPrefix,
483
- recoveryCount: recoveryHistory.length,
484
- recoveryHistory,
485
- recoverySummary: finalRecoverySummary,
486
- finalMessage: taskAttempt.finalMessage,
487
- code: finalizedResult.code,
488
- failureCode: activeFailure.code,
489
- failureFamily: activeFailure.family,
490
- failureReason: activeFailure.reason,
491
- notification,
492
- });
493
-
494
- return {
495
- ...finalizedResult,
496
- finalMessage: taskAttempt.finalMessage,
497
- recoveryCount: recoveryHistory.length,
498
- recoveryHistory,
499
- recoverySummary: finalRecoverySummary,
500
- recoveryFailure: {
501
- ...activeFailure,
502
- shouldStopTask: true,
503
- exhausted: true,
504
- },
505
- notification,
506
- };
507
- }
508
-
509
- writeRuntimeStatus("retry_waiting", {
510
- attemptPrefix,
511
- recoveryCount: nextRecoveryIndex,
512
- recoveryHistory,
513
- nextRetryDelayMs: delayMs,
514
- nextRetryAt: new Date(Date.now() + delayMs).toISOString(),
515
- failureCode: activeFailure.code,
516
- failureFamily: activeFailure.family,
517
- failureReason: activeFailure.reason,
518
- });
519
- if (delayMs > 0) {
520
- await sleep(delayMs);
521
- }
522
-
523
- const probeAttempt = await runEngineHealthProbe({
524
- engine: normalizedEngine,
525
- invocation,
526
- context,
527
- runDir,
528
- resolvedPolicy,
529
- recoveryPolicy,
530
- writeRuntimeStatus,
531
- recoveryCount: nextRecoveryIndex,
532
- recoveryHistory,
533
- env,
534
- probeIndex: nextRecoveryIndex,
535
- });
536
- const recoveryRecord = {
537
- recoveryIndex: nextRecoveryIndex,
538
- family: activeFailure.family,
539
- code: activeFailure.code,
540
- reason: activeFailure.reason,
541
- delaySeconds: Math.floor(delayMs / 1000),
542
- taskStatus: "failed",
543
- taskCode: taskAttempt.result.code,
544
- taskAttemptPrefix: attemptPrefix,
545
- probeStatus: probeAttempt.result.ok ? "ok" : "failed",
546
- probeCode: probeAttempt.result.code,
547
- probeAttemptPrefix: probeAttempt.attemptPrefix,
548
- probeFailureCode: probeAttempt.failure?.code || "",
549
- probeFailureFamily: probeAttempt.failure?.family || "",
550
- probeFailureReason: probeAttempt.failure?.reason || "",
551
- watchdogTriggered: taskAttempt.result.watchdogTriggered === true || probeAttempt.result.watchdogTriggered === true,
552
- };
553
- recoveryHistory.push(recoveryRecord);
554
- writeJson(path.join(
555
- runDir,
556
- `${prefix}-auto-recovery-${String(nextRecoveryIndex).padStart(2, "0")}.json`,
557
- ), {
558
- ...recoveryRecord,
559
- engine: normalizedEngine,
560
- phase: executionMode,
561
- stdoutTail: tailText(taskAttempt.result.stdout, 20),
562
- stderrTail: tailText(taskAttempt.result.stderr, 20),
563
- finalMessageTail: tailText(taskAttempt.finalMessage, 20),
564
- probeStdoutTail: tailText(probeAttempt.result.stdout, 20),
565
- probeStderrTail: tailText(probeAttempt.result.stderr, 20),
566
- probeFinalMessageTail: tailText(probeAttempt.finalMessage, 20),
567
- createdAt: nowIso(),
568
- });
569
-
570
- if (!probeAttempt.result.ok) {
571
- activeFailure = probeAttempt.failure;
572
- writeRuntimeStatus("probe_failed", {
573
- attemptPrefix: probeAttempt.attemptPrefix,
574
- recoveryCount: nextRecoveryIndex,
575
- recoveryHistory,
576
- failureCode: activeFailure.code,
577
- failureFamily: activeFailure.family,
578
- failureReason: activeFailure.reason,
579
- });
580
- continue;
581
- }
582
-
583
- currentPrompt = recoveryPrompt;
584
- currentRecoveryCount = nextRecoveryIndex;
585
- break;
586
- }
587
- }
588
- }
4
+ export { runEngineTask, runShellCommand, runVerifyCommands };
589
5
 
590
6
  export async function runCodexTask(options) {
591
7
  return runEngineTask({
@@ -594,7 +10,7 @@ export async function runCodexTask(options) {
594
10
  });
595
11
  }
596
12
 
597
- export async function runCodexExec({ context, prompt, runDir, policy }) {
13
+ export async function runCodexExec({ context, prompt, runDir, policy, hostLease = null }) {
598
14
  return runEngineTask({
599
15
  engine: "codex",
600
16
  context,
@@ -603,10 +19,11 @@ export async function runCodexExec({ context, prompt, runDir, policy }) {
603
19
  policy,
604
20
  executionMode: "execute",
605
21
  outputPrefix: "codex",
22
+ hostLease,
606
23
  });
607
24
  }
608
25
 
609
- export async function runEngineExec({ engine, context, prompt, runDir, policy }) {
26
+ export async function runEngineExec({ engine, context, prompt, runDir, policy, hostLease = null }) {
610
27
  return runEngineTask({
611
28
  engine,
612
29
  context,
@@ -615,70 +32,6 @@ export async function runEngineExec({ engine, context, prompt, runDir, policy })
615
32
  policy,
616
33
  executionMode: "execute",
617
34
  outputPrefix: engine,
35
+ hostLease,
618
36
  });
619
37
  }
620
-
621
- export async function runShellCommand(context, commandLine, runDir, index) {
622
- const shellInvocation = resolveVerifyInvocation();
623
- if (shellInvocation.error) {
624
- const result = {
625
- command: commandLine,
626
- ok: false,
627
- code: 1,
628
- stdout: "",
629
- stderr: shellInvocation.error,
630
- };
631
- const prefix = String(index + 1).padStart(2, "0");
632
- writeText(path.join(runDir, `${prefix}-verify-command.txt`), commandLine);
633
- writeText(path.join(runDir, `${prefix}-verify-stdout.log`), result.stdout);
634
- writeText(path.join(runDir, `${prefix}-verify-stderr.log`), result.stderr);
635
- return result;
636
- }
637
-
638
- const result = await runChild(shellInvocation.command, [
639
- ...shellInvocation.argsPrefix,
640
- commandLine,
641
- ], {
642
- cwd: context.repoRoot,
643
- shell: shellInvocation.shell,
644
- });
645
-
646
- const prefix = String(index + 1).padStart(2, "0");
647
- writeText(path.join(runDir, `${prefix}-verify-command.txt`), commandLine);
648
- writeText(path.join(runDir, `${prefix}-verify-stdout.log`), result.stdout);
649
- writeText(path.join(runDir, `${prefix}-verify-stderr.log`), result.stderr);
650
-
651
- return { command: commandLine, ...result };
652
- }
653
-
654
- export async function runVerifyCommands(context, commands, runDir) {
655
- const results = [];
656
-
657
- for (const [index, command] of commands.entries()) {
658
- const result = await runShellCommand(context, command, runDir, index);
659
- results.push(result);
660
- if (!result.ok) {
661
- return {
662
- ok: false,
663
- results,
664
- failed: result,
665
- summary: [
666
- `验证失败:${result.command}`,
667
- "",
668
- "stdout 尾部:",
669
- tailText(result.stdout, 40),
670
- "",
671
- "stderr 尾部:",
672
- tailText(result.stderr, 40),
673
- ].join("\n").trim(),
674
- };
675
- }
676
- }
677
-
678
- return {
679
- ok: true,
680
- results,
681
- failed: null,
682
- summary: "全部验证命令通过。",
683
- };
684
- }