opencode-oncall 0.1.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/LICENSE +151 -0
- package/README.md +50 -0
- package/dist/common-settings-actions.d.ts +15 -0
- package/dist/common-settings-actions.js +48 -0
- package/dist/common-settings-store.d.ts +1 -0
- package/dist/common-settings-store.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/plugin-hooks.d.ts +51 -0
- package/dist/plugin-hooks.js +288 -0
- package/dist/plugin.d.ts +10 -0
- package/dist/plugin.js +115 -0
- package/dist/settings-store.d.ts +50 -0
- package/dist/settings-store.js +214 -0
- package/dist/store-paths.d.ts +16 -0
- package/dist/store-paths.js +61 -0
- package/dist/ui/wechat-menu.d.ts +26 -0
- package/dist/ui/wechat-menu.js +90 -0
- package/dist/wechat/bind-flow.d.ts +29 -0
- package/dist/wechat/bind-flow.js +207 -0
- package/dist/wechat/bridge.d.ts +136 -0
- package/dist/wechat/bridge.js +1059 -0
- package/dist/wechat/broker-client.d.ts +23 -0
- package/dist/wechat/broker-client.js +274 -0
- package/dist/wechat/broker-endpoint.d.ts +21 -0
- package/dist/wechat/broker-endpoint.js +78 -0
- package/dist/wechat/broker-entry.d.ts +123 -0
- package/dist/wechat/broker-entry.js +1321 -0
- package/dist/wechat/broker-launcher.d.ts +37 -0
- package/dist/wechat/broker-launcher.js +418 -0
- package/dist/wechat/broker-mutation-queue.d.ts +93 -0
- package/dist/wechat/broker-mutation-queue.js +126 -0
- package/dist/wechat/broker-server.d.ts +86 -0
- package/dist/wechat/broker-server.js +1340 -0
- package/dist/wechat/broker-state-store.d.ts +335 -0
- package/dist/wechat/broker-state-store.js +1964 -0
- package/dist/wechat/command-parser.d.ts +18 -0
- package/dist/wechat/command-parser.js +58 -0
- package/dist/wechat/compat/jiti-loader.d.ts +27 -0
- package/dist/wechat/compat/jiti-loader.js +118 -0
- package/dist/wechat/compat/openclaw-account-helpers.d.ts +29 -0
- package/dist/wechat/compat/openclaw-account-helpers.js +60 -0
- package/dist/wechat/compat/openclaw-bind-helpers.d.ts +29 -0
- package/dist/wechat/compat/openclaw-bind-helpers.js +169 -0
- package/dist/wechat/compat/openclaw-guided-smoke.d.ts +180 -0
- package/dist/wechat/compat/openclaw-guided-smoke.js +1134 -0
- package/dist/wechat/compat/openclaw-public-entry.d.ts +33 -0
- package/dist/wechat/compat/openclaw-public-entry.js +62 -0
- package/dist/wechat/compat/openclaw-public-helpers.d.ts +70 -0
- package/dist/wechat/compat/openclaw-public-helpers.js +68 -0
- package/dist/wechat/compat/openclaw-qr-gateway.d.ts +15 -0
- package/dist/wechat/compat/openclaw-qr-gateway.js +39 -0
- package/dist/wechat/compat/openclaw-smoke.d.ts +48 -0
- package/dist/wechat/compat/openclaw-smoke.js +100 -0
- package/dist/wechat/compat/openclaw-sync-buf.d.ts +24 -0
- package/dist/wechat/compat/openclaw-sync-buf.js +80 -0
- package/dist/wechat/compat/openclaw-updates-send.d.ts +47 -0
- package/dist/wechat/compat/openclaw-updates-send.js +38 -0
- package/dist/wechat/compat/qrcode-terminal-loader.d.ts +12 -0
- package/dist/wechat/compat/qrcode-terminal-loader.js +16 -0
- package/dist/wechat/compat/slash-guard.d.ts +11 -0
- package/dist/wechat/compat/slash-guard.js +24 -0
- package/dist/wechat/dead-letter-store.d.ts +48 -0
- package/dist/wechat/dead-letter-store.js +224 -0
- package/dist/wechat/debug-bundle-collector.d.ts +49 -0
- package/dist/wechat/debug-bundle-collector.js +580 -0
- package/dist/wechat/debug-bundle-flow.d.ts +37 -0
- package/dist/wechat/debug-bundle-flow.js +180 -0
- package/dist/wechat/debug-bundle-redaction.d.ts +14 -0
- package/dist/wechat/debug-bundle-redaction.js +339 -0
- package/dist/wechat/handle.d.ts +10 -0
- package/dist/wechat/handle.js +57 -0
- package/dist/wechat/ipc-auth.d.ts +6 -0
- package/dist/wechat/ipc-auth.js +39 -0
- package/dist/wechat/latest-account-state-store.d.ts +8 -0
- package/dist/wechat/latest-account-state-store.js +38 -0
- package/dist/wechat/notification-dispatcher.d.ts +34 -0
- package/dist/wechat/notification-dispatcher.js +266 -0
- package/dist/wechat/notification-format.d.ts +15 -0
- package/dist/wechat/notification-format.js +196 -0
- package/dist/wechat/notification-store.d.ts +72 -0
- package/dist/wechat/notification-store.js +807 -0
- package/dist/wechat/notification-types.d.ts +37 -0
- package/dist/wechat/notification-types.js +1 -0
- package/dist/wechat/openclaw-account-adapter.d.ts +30 -0
- package/dist/wechat/openclaw-account-adapter.js +60 -0
- package/dist/wechat/operator-store.d.ts +9 -0
- package/dist/wechat/operator-store.js +69 -0
- package/dist/wechat/protocol.d.ts +150 -0
- package/dist/wechat/protocol.js +197 -0
- package/dist/wechat/question-interaction.d.ts +24 -0
- package/dist/wechat/question-interaction.js +180 -0
- package/dist/wechat/request-store.d.ts +108 -0
- package/dist/wechat/request-store.js +669 -0
- package/dist/wechat/session-digest.d.ts +50 -0
- package/dist/wechat/session-digest.js +167 -0
- package/dist/wechat/state-paths.d.ts +26 -0
- package/dist/wechat/state-paths.js +92 -0
- package/dist/wechat/status-format.d.ts +26 -0
- package/dist/wechat/status-format.js +616 -0
- package/dist/wechat/token-store.d.ts +20 -0
- package/dist/wechat/token-store.js +193 -0
- package/dist/wechat/wechat-status-runtime.d.ts +89 -0
- package/dist/wechat/wechat-status-runtime.js +518 -0
- package/package.json +74 -0
|
@@ -0,0 +1,1134 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { loadOpenClawWeixinPublicHelpers, } from "./openclaw-public-helpers.js";
|
|
6
|
+
import { createOpenClawSmokeHarness, runOpenClawSmoke, sanitizeOpenClawEvidenceSample } from "./openclaw-smoke.js";
|
|
7
|
+
import { STAGE_A_SLASH_ONLY_MESSAGE } from "./slash-guard.js";
|
|
8
|
+
export const GUIDED_SMOKE_EVIDENCE_FILES = {
|
|
9
|
+
preflight: "001-preflight.md",
|
|
10
|
+
qrStart: "002-qr-start.md",
|
|
11
|
+
loginSuccess: "003-login-success.md",
|
|
12
|
+
statusCommand: "004-status-command.json",
|
|
13
|
+
replyCommand: "005-reply-command.json",
|
|
14
|
+
allowCommand: "006-allow-command.json",
|
|
15
|
+
};
|
|
16
|
+
const DEFAULT_QR_WAIT_TIMEOUT_MS = 480_000;
|
|
17
|
+
const SLASH_SAMPLE_STEPS = [
|
|
18
|
+
{
|
|
19
|
+
command: "status",
|
|
20
|
+
input: "/status",
|
|
21
|
+
evidenceFile: GUIDED_SMOKE_EVIDENCE_FILES.statusCommand,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
command: "reply",
|
|
25
|
+
input: "/reply smoke",
|
|
26
|
+
evidenceFile: GUIDED_SMOKE_EVIDENCE_FILES.replyCommand,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
command: "allow",
|
|
30
|
+
input: "/allow once",
|
|
31
|
+
evidenceFile: GUIDED_SMOKE_EVIDENCE_FILES.allowCommand,
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
const DEFAULT_EVIDENCE_BASE_DIR = fileURLToPath(new URL("../../../docs/superpowers/wechat-stage-a/evidence", import.meta.url));
|
|
35
|
+
const DEFAULT_API_SAMPLES_DOC_PATH = fileURLToPath(new URL("../../../docs/superpowers/wechat-stage-a/api-samples-sanitized.md", import.meta.url));
|
|
36
|
+
const DEFAULT_GO_NO_GO_DOC_PATH = fileURLToPath(new URL("../../../docs/superpowers/wechat-stage-a/go-no-go.md", import.meta.url));
|
|
37
|
+
const FIXED_NON_SLASH_WARNING_TEXT = STAGE_A_SLASH_ONLY_MESSAGE;
|
|
38
|
+
export const NON_SLASH_WARNING_TEXT_FOR_TEST = FIXED_NON_SLASH_WARNING_TEXT;
|
|
39
|
+
const DEFAULT_KEY_FIELDS_CHECK = {
|
|
40
|
+
login: { status: "known-unknown" },
|
|
41
|
+
getupdates: { status: "known-unknown" },
|
|
42
|
+
slashInbound: { status: "known-unknown" },
|
|
43
|
+
warningReply: { status: "known-unknown" },
|
|
44
|
+
};
|
|
45
|
+
const DEFAULT_SLASH_CAPTURE_WAIT_TIMEOUT_MS = 180_000;
|
|
46
|
+
const DEFAULT_SLASH_CAPTURE_POLL_INTERVAL_MS = 2_000;
|
|
47
|
+
const DEFAULT_PUBLIC_GET_UPDATES_LONG_POLL_TIMEOUT_MS = 35_000;
|
|
48
|
+
async function writeLineDefault(line) {
|
|
49
|
+
process.stdout.write(`${line}\n`);
|
|
50
|
+
}
|
|
51
|
+
async function printGuidedPrompt(message, writeLine) {
|
|
52
|
+
await writeLine(message);
|
|
53
|
+
}
|
|
54
|
+
function createRunId() {
|
|
55
|
+
return new Date().toISOString().replace(/[:.]/g, "-");
|
|
56
|
+
}
|
|
57
|
+
function resolveDependencyVersionsFromPackageJson() {
|
|
58
|
+
const packageJsonPath = fileURLToPath(new URL("../../../package.json", import.meta.url));
|
|
59
|
+
const packageJsonRaw = readFileSync(packageJsonPath, "utf8");
|
|
60
|
+
const packageJson = JSON.parse(packageJsonRaw);
|
|
61
|
+
return {
|
|
62
|
+
"@tencent-weixin/openclaw-weixin": packageJson.dependencies?.["@tencent-weixin/openclaw-weixin"] ?? "unknown",
|
|
63
|
+
openclaw: packageJson.dependencies?.openclaw ?? "unknown",
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
async function runGuidedSelfTestDefault() {
|
|
67
|
+
try {
|
|
68
|
+
const results = await runOpenClawSmoke("self-test");
|
|
69
|
+
const loaded = results.some((item) => item.route === "public-self-test" && item.status === "loaded");
|
|
70
|
+
if (!loaded) {
|
|
71
|
+
return { ok: false, reason: "self-test missing public-self-test loaded result" };
|
|
72
|
+
}
|
|
73
|
+
return { ok: true };
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export function createGuidedSmokeRun(options = {}) {
|
|
83
|
+
const runId = options.runId ?? createRunId();
|
|
84
|
+
const cwd = options.cwd ?? process.cwd();
|
|
85
|
+
const evidenceBaseDir = options.evidenceBaseDir ?? DEFAULT_EVIDENCE_BASE_DIR;
|
|
86
|
+
const evidenceDir = path.join(evidenceBaseDir, runId);
|
|
87
|
+
return {
|
|
88
|
+
runId,
|
|
89
|
+
cwd,
|
|
90
|
+
evidenceBaseDir,
|
|
91
|
+
evidenceDir,
|
|
92
|
+
status: "running",
|
|
93
|
+
conclusion: "known-unknown",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
export function failGuidedSmoke(run, reason) {
|
|
97
|
+
run.status = "blocked";
|
|
98
|
+
run.conclusion = "known-unknown";
|
|
99
|
+
return {
|
|
100
|
+
runId: run.runId,
|
|
101
|
+
status: run.status,
|
|
102
|
+
conclusion: run.conclusion,
|
|
103
|
+
evidenceDir: run.evidenceDir,
|
|
104
|
+
reason,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
export function getGuidedSmokeExitCode(result) {
|
|
108
|
+
return result.status === "blocked" ? 1 : 0;
|
|
109
|
+
}
|
|
110
|
+
export async function writePreflightEvidence(run, data) {
|
|
111
|
+
const lines = [
|
|
112
|
+
"# Guided Smoke Preflight",
|
|
113
|
+
"",
|
|
114
|
+
`- run id: \`${data.runId}\``,
|
|
115
|
+
`- cwd: \`${data.cwd}\``,
|
|
116
|
+
`- node version: \`${data.nodeVersion}\``,
|
|
117
|
+
`- @tencent-weixin/openclaw-weixin: \`${data.dependencyVersions["@tencent-weixin/openclaw-weixin"]}\``,
|
|
118
|
+
`- openclaw: \`${data.dependencyVersions.openclaw}\``,
|
|
119
|
+
`- dependency versions: \`${data.dependencyVersionResolution.status}\` (${data.dependencyVersionResolution.detail})`,
|
|
120
|
+
`- public entry load: \`${data.publicEntryLoad.status}\` (${data.publicEntryLoad.detail})`,
|
|
121
|
+
`- evidence directory creation: \`${data.evidenceDirectoryCreation.status}\` (${data.evidenceDirectoryCreation.detail})`,
|
|
122
|
+
`- self-test: \`${data.selfTest.status}\` (${data.selfTest.detail})`,
|
|
123
|
+
"",
|
|
124
|
+
];
|
|
125
|
+
const filePath = path.join(run.evidenceDir, GUIDED_SMOKE_EVIDENCE_FILES.preflight);
|
|
126
|
+
await writeFile(filePath, lines.join("\n"), "utf8");
|
|
127
|
+
}
|
|
128
|
+
async function writeQrStartEvidence(run, status, detail, waitTimeoutMs) {
|
|
129
|
+
const filePath = path.join(run.evidenceDir, GUIDED_SMOKE_EVIDENCE_FILES.qrStart);
|
|
130
|
+
const lines = [
|
|
131
|
+
"# Guided Smoke QR Start",
|
|
132
|
+
"",
|
|
133
|
+
`- status: \`${status}\``,
|
|
134
|
+
`- wait timeout ms: \`${waitTimeoutMs}\``,
|
|
135
|
+
`- detail: ${detail}`,
|
|
136
|
+
"",
|
|
137
|
+
];
|
|
138
|
+
await writeFile(filePath, lines.join("\n"), "utf8");
|
|
139
|
+
}
|
|
140
|
+
async function writeLoginSuccessEvidence(run, status, waitTimeoutMs) {
|
|
141
|
+
const filePath = path.join(run.evidenceDir, GUIDED_SMOKE_EVIDENCE_FILES.loginSuccess);
|
|
142
|
+
const lines = [
|
|
143
|
+
"# Guided Smoke Login Wait",
|
|
144
|
+
"",
|
|
145
|
+
`- status: \`${status}\``,
|
|
146
|
+
`- wait timeout ms: \`${waitTimeoutMs}\``,
|
|
147
|
+
"",
|
|
148
|
+
];
|
|
149
|
+
await writeFile(filePath, lines.join("\n"), "utf8");
|
|
150
|
+
}
|
|
151
|
+
async function writeFinalStatusEvidence(run, input) {
|
|
152
|
+
const filePath = path.join(run.evidenceDir, "999-final-status.md");
|
|
153
|
+
const lines = [
|
|
154
|
+
"# Guided Smoke Final Status",
|
|
155
|
+
"",
|
|
156
|
+
`- stage: \`${input.stage}\``,
|
|
157
|
+
`- status: \`${input.status}\``,
|
|
158
|
+
`- conclusion: \`${input.conclusion}\``,
|
|
159
|
+
`- detail: ${input.detail}`,
|
|
160
|
+
"",
|
|
161
|
+
];
|
|
162
|
+
await writeFile(filePath, lines.join("\n"), "utf8");
|
|
163
|
+
}
|
|
164
|
+
function buildNonSlashEvidenceFileName(index) {
|
|
165
|
+
const sequence = String(index + 1).padStart(2, "0");
|
|
166
|
+
const order = String(7 + index).padStart(3, "0");
|
|
167
|
+
return `${order}-nonslash-warning-${sequence}.json`;
|
|
168
|
+
}
|
|
169
|
+
function detectSensitiveResidue(input) {
|
|
170
|
+
const patterns = [
|
|
171
|
+
/"contextToken"\s*:\s*"(?!\[REDACTED_)[^"]+"/i,
|
|
172
|
+
/"context_token"\s*:\s*"(?!\[REDACTED_)[^"]+"/i,
|
|
173
|
+
/"bot_token"\s*:\s*"(?!\[REDACTED_)[^"]+"/i,
|
|
174
|
+
/"authorization"\s*:\s*"(?!Bearer \[REDACTED_AUTHORIZATION\])[^"]+"/i,
|
|
175
|
+
/"userId"\s*:\s*"(?!\[REDACTED_)[^"]+"/i,
|
|
176
|
+
/"botId"\s*:\s*"(?!\[REDACTED_)[^"]+"/i,
|
|
177
|
+
/"qrCode"\s*:\s*"(?!\[REDACTED_)[^"]+"/i,
|
|
178
|
+
/"deviceId"\s*:\s*"(?!\[REDACTED_)[^"]+"/i,
|
|
179
|
+
/"messageId"\s*:\s*"(?!\[REDACTED_)[^"]+"/i,
|
|
180
|
+
/"requestId"\s*:\s*"(?!\[REDACTED_)[^"]+"/i,
|
|
181
|
+
];
|
|
182
|
+
for (const pattern of patterns) {
|
|
183
|
+
const match = input.match(pattern);
|
|
184
|
+
if (match) {
|
|
185
|
+
return match[0];
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
async function maybeUpdateGoNoGoDoc(run, goNoGoDocPath, input) {
|
|
191
|
+
if (!goNoGoDocPath) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
await updateGoNoGoDoc(run, goNoGoDocPath, input);
|
|
195
|
+
}
|
|
196
|
+
async function writeNonSlashAttemptEvidence(run, input) {
|
|
197
|
+
const fileName = buildNonSlashEvidenceFileName(input.index);
|
|
198
|
+
const payload = {
|
|
199
|
+
evidenceFile: fileName,
|
|
200
|
+
evidenceId: `nonslash-warning-${String(input.index + 1).padStart(2, "0")}`,
|
|
201
|
+
timestamp: new Date().toISOString(),
|
|
202
|
+
input: input.attempt.input ?? null,
|
|
203
|
+
routeResult: "guard-reject-warning",
|
|
204
|
+
inbound: input.attempt.inbound ?? null,
|
|
205
|
+
warningReply: input.attempt.warningReply ?? null,
|
|
206
|
+
persisted: input.attempt.persisted === true,
|
|
207
|
+
};
|
|
208
|
+
const raw = JSON.stringify(payload, null, 2);
|
|
209
|
+
const sanitized = sanitizeOpenClawEvidenceSample(raw);
|
|
210
|
+
const filePath = path.join(run.evidenceDir, fileName);
|
|
211
|
+
await writeFile(filePath, sanitized, "utf8");
|
|
212
|
+
const written = await readFile(filePath, "utf8");
|
|
213
|
+
return {
|
|
214
|
+
fileName,
|
|
215
|
+
sensitiveResidue: detectSensitiveResidue(written),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function validateNonSlashAttempt(attempt) {
|
|
219
|
+
const hasInbound = Boolean(attempt.inbound && typeof attempt.inbound === "object");
|
|
220
|
+
if (!hasInbound) {
|
|
221
|
+
return "missing real inbound capture for non-slash attempt";
|
|
222
|
+
}
|
|
223
|
+
const warningOk = attempt.warningReply?.ok === true;
|
|
224
|
+
const warningTextMatched = attempt.warningReply?.text === FIXED_NON_SLASH_WARNING_TEXT;
|
|
225
|
+
if (!warningOk || !warningTextMatched) {
|
|
226
|
+
return "fixed warning reply validation failed";
|
|
227
|
+
}
|
|
228
|
+
if (attempt.persisted !== true) {
|
|
229
|
+
return "non-slash attempt is not persisted";
|
|
230
|
+
}
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
async function writeKeyFieldsCheckEvidence(run, input) {
|
|
234
|
+
const filePath = path.join(run.evidenceDir, "090-key-fields-check.md");
|
|
235
|
+
const lines = [
|
|
236
|
+
"# Guided Smoke Key Fields Check",
|
|
237
|
+
"",
|
|
238
|
+
`- login fields: \`${input.login.status}\`${input.login.detail ? ` (${input.login.detail})` : ""}`,
|
|
239
|
+
`- getupdates fields: \`${input.getupdates.status}\`${input.getupdates.detail ? ` (${input.getupdates.detail})` : ""}`,
|
|
240
|
+
`- slash inbound fields: \`${input.slashInbound.status}\`${input.slashInbound.detail ? ` (${input.slashInbound.detail})` : ""}`,
|
|
241
|
+
`- warning reply fields: \`${input.warningReply.status}\`${input.warningReply.detail ? ` (${input.warningReply.detail})` : ""}`,
|
|
242
|
+
"",
|
|
243
|
+
];
|
|
244
|
+
await writeFile(filePath, lines.join("\n"), "utf8");
|
|
245
|
+
}
|
|
246
|
+
async function updateGoNoGoDoc(run, filePath, input) {
|
|
247
|
+
const previous = await readFile(filePath, "utf8");
|
|
248
|
+
const normalizedPrevious = previous.trimEnd();
|
|
249
|
+
const failedChecks = input.nonSlash.failedChecks.length > 0 ? input.nonSlash.failedChecks.join(",") : "none";
|
|
250
|
+
const section = [
|
|
251
|
+
`## Guided Smoke Run (${run.runId})`,
|
|
252
|
+
`- 运行状态:\`${run.status}\``,
|
|
253
|
+
`- 最终结论:\`${run.conclusion}\``,
|
|
254
|
+
`- 证据目录:\`${run.evidenceDir}\``,
|
|
255
|
+
`- 非 slash 计数:\`${input.nonSlash.passed}/${input.nonSlash.total}\``,
|
|
256
|
+
`- 非 slash 失败项:\`${failedChecks}\``,
|
|
257
|
+
"- 关键字段检查:",
|
|
258
|
+
` - login: \`${input.keyFields.login.status}\``,
|
|
259
|
+
` - getupdates: \`${input.keyFields.getupdates.status}\``,
|
|
260
|
+
` - slash inbound: \`${input.keyFields.slashInbound.status}\``,
|
|
261
|
+
` - warning reply: \`${input.keyFields.warningReply.status}\``,
|
|
262
|
+
"- 关键字段证据:`090-key-fields-check.md`",
|
|
263
|
+
"",
|
|
264
|
+
].join("\n");
|
|
265
|
+
await writeFile(filePath, `${normalizedPrevious}\n\n${section}`, "utf8");
|
|
266
|
+
}
|
|
267
|
+
function pickFirstString(source, keys) {
|
|
268
|
+
if (!source || typeof source !== "object") {
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
const record = source;
|
|
272
|
+
for (const key of keys) {
|
|
273
|
+
const value = record[key];
|
|
274
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
275
|
+
return value;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
function extractTextFromPublicWeixinMessage(message) {
|
|
281
|
+
for (const item of message.item_list ?? []) {
|
|
282
|
+
if (item?.type === 1 && typeof item.text_item?.text === "string" && item.text_item.text.trim().length > 0) {
|
|
283
|
+
return item.text_item.text;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return "";
|
|
287
|
+
}
|
|
288
|
+
function normalizeSlashInboundSample(input) {
|
|
289
|
+
return {
|
|
290
|
+
command: input.command,
|
|
291
|
+
input: input.input,
|
|
292
|
+
messageId: input.message.message_id ?? null,
|
|
293
|
+
fromUserId: input.message.from_user_id ?? null,
|
|
294
|
+
contextToken: input.message.context_token ?? null,
|
|
295
|
+
createdAtMs: input.message.create_time_ms ?? null,
|
|
296
|
+
text: extractTextFromPublicWeixinMessage(input.message),
|
|
297
|
+
itemTypes: (input.message.item_list ?? []).map((item) => item?.type).filter((item) => typeof item === "number"),
|
|
298
|
+
normalizedBy: "guided-smoke-public-structure",
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
export const normalizeSlashInboundSampleForTest = normalizeSlashInboundSample;
|
|
302
|
+
function normalizeNonSlashInboundSample(message) {
|
|
303
|
+
return {
|
|
304
|
+
messageId: message.message_id ?? null,
|
|
305
|
+
fromUserId: message.from_user_id ?? null,
|
|
306
|
+
contextToken: message.context_token ?? null,
|
|
307
|
+
createdAtMs: message.create_time_ms ?? null,
|
|
308
|
+
text: extractTextFromPublicWeixinMessage(message),
|
|
309
|
+
itemTypes: (message.item_list ?? []).map((item) => item?.type).filter((item) => typeof item === "number"),
|
|
310
|
+
normalizedBy: "guided-smoke-public-structure",
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
function sleep(ms) {
|
|
314
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
315
|
+
}
|
|
316
|
+
function matchesSlashStep(message, input) {
|
|
317
|
+
const text = extractTextFromPublicWeixinMessage(message).trim();
|
|
318
|
+
return text === input.trim();
|
|
319
|
+
}
|
|
320
|
+
function matchesNonSlashStep(message, expectedText) {
|
|
321
|
+
const text = extractTextFromPublicWeixinMessage(message).trim();
|
|
322
|
+
if (text.length === 0 || text.startsWith("/")) {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
if (expectedText) {
|
|
326
|
+
return text === expectedText.trim();
|
|
327
|
+
}
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
async function captureSlashInboundFromPublicMessages(input) {
|
|
331
|
+
const deadline = Date.now() + (input.waitTimeoutMs ?? DEFAULT_SLASH_CAPTURE_WAIT_TIMEOUT_MS);
|
|
332
|
+
const seen = new Set();
|
|
333
|
+
while (Date.now() <= deadline) {
|
|
334
|
+
const messages = await input.getMessages();
|
|
335
|
+
for (const message of messages) {
|
|
336
|
+
const key = `${message.message_id ?? "unknown"}:${message.create_time_ms ?? "unknown"}`;
|
|
337
|
+
if (seen.has(key)) {
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
seen.add(key);
|
|
341
|
+
if (matchesSlashStep(message, input.input)) {
|
|
342
|
+
const remaining = messages.filter((candidate) => {
|
|
343
|
+
const candidateKey = `${candidate.message_id ?? "unknown"}:${candidate.create_time_ms ?? "unknown"}`;
|
|
344
|
+
return candidateKey !== key && !seen.has(candidateKey);
|
|
345
|
+
});
|
|
346
|
+
if (remaining.length > 0) {
|
|
347
|
+
input.stashMessages?.(remaining);
|
|
348
|
+
}
|
|
349
|
+
return normalizeSlashInboundSample({
|
|
350
|
+
command: input.command,
|
|
351
|
+
input: input.input,
|
|
352
|
+
message,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
await sleep(input.pollIntervalMs ?? DEFAULT_SLASH_CAPTURE_POLL_INTERVAL_MS);
|
|
357
|
+
}
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
export const captureSlashInboundFromPublicMessagesForTest = captureSlashInboundFromPublicMessages;
|
|
361
|
+
async function captureNonSlashInboundFromPublicMessages(input) {
|
|
362
|
+
const deadline = Date.now() + (input.waitTimeoutMs ?? DEFAULT_SLASH_CAPTURE_WAIT_TIMEOUT_MS);
|
|
363
|
+
const seen = new Set();
|
|
364
|
+
while (Date.now() <= deadline) {
|
|
365
|
+
const messages = await input.getMessages();
|
|
366
|
+
for (const message of messages) {
|
|
367
|
+
const key = `${message.message_id ?? "unknown"}:${message.create_time_ms ?? "unknown"}`;
|
|
368
|
+
if (seen.has(key)) {
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
seen.add(key);
|
|
372
|
+
if (matchesNonSlashStep(message, input.expectedText)) {
|
|
373
|
+
const remaining = messages.filter((candidate) => {
|
|
374
|
+
const candidateKey = `${candidate.message_id ?? "unknown"}:${candidate.create_time_ms ?? "unknown"}`;
|
|
375
|
+
return candidateKey !== key && !seen.has(candidateKey);
|
|
376
|
+
});
|
|
377
|
+
if (remaining.length > 0) {
|
|
378
|
+
input.stashMessages?.(remaining);
|
|
379
|
+
}
|
|
380
|
+
return normalizeNonSlashInboundSample(message);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
await sleep(input.pollIntervalMs ?? DEFAULT_SLASH_CAPTURE_POLL_INTERVAL_MS);
|
|
384
|
+
}
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
async function runQrLoginDefault(input) {
|
|
388
|
+
const qrGateway = input.qrGateway;
|
|
389
|
+
if (!qrGateway) {
|
|
390
|
+
throw new Error("missing qrGateway helper");
|
|
391
|
+
}
|
|
392
|
+
const startResult = await Promise.resolve(qrGateway.loginWithQrStart({
|
|
393
|
+
accountId: undefined,
|
|
394
|
+
force: false,
|
|
395
|
+
timeoutMs: input.waitTimeoutMs,
|
|
396
|
+
verbose: false,
|
|
397
|
+
}));
|
|
398
|
+
const qrTerminal = pickFirstString(startResult, ["qrTerminal"]);
|
|
399
|
+
const qrUrl = pickFirstString(startResult, ["qrDataUrl"]);
|
|
400
|
+
const sessionKey = pickFirstString(startResult, ["sessionKey"]);
|
|
401
|
+
const qrStartMessage = pickFirstString(startResult, ["message", "detail", "reason"]);
|
|
402
|
+
if (!sessionKey) {
|
|
403
|
+
throw new Error("missing sessionKey from qr start");
|
|
404
|
+
}
|
|
405
|
+
if (qrTerminal) {
|
|
406
|
+
await input.writeLine(qrTerminal);
|
|
407
|
+
}
|
|
408
|
+
else if (qrUrl) {
|
|
409
|
+
await input.writeLine(`QR URL fallback: ${qrUrl}`);
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
throw new Error(qrStartMessage || "invalid qr login result: missing qr code or qr url");
|
|
413
|
+
}
|
|
414
|
+
const waitResult = await Promise.resolve(qrGateway.loginWithQrWait({ timeoutMs: input.waitTimeoutMs, sessionKey }));
|
|
415
|
+
if (waitResult && typeof waitResult === "object" && "status" in waitResult && String(waitResult.status) === "timeout") {
|
|
416
|
+
return { status: "timeout", qrPrinted: Boolean(qrTerminal), qrUrl };
|
|
417
|
+
}
|
|
418
|
+
return {
|
|
419
|
+
status: "success",
|
|
420
|
+
connected: waitResult && typeof waitResult === "object" && "connected" in waitResult ? waitResult.connected === true : undefined,
|
|
421
|
+
qrPrinted: Boolean(qrTerminal),
|
|
422
|
+
qrUrl,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
async function writeSlashCommandEvidence(run, input) {
|
|
426
|
+
const filePath = path.join(run.evidenceDir, input.evidenceFile);
|
|
427
|
+
const payload = {
|
|
428
|
+
evidenceFile: input.evidenceFile,
|
|
429
|
+
timestamp: new Date().toISOString(),
|
|
430
|
+
input: input.text,
|
|
431
|
+
command: input.command,
|
|
432
|
+
inbound: input.inbound,
|
|
433
|
+
routeResult: input.routeResult,
|
|
434
|
+
completed: input.completed,
|
|
435
|
+
outbound: {
|
|
436
|
+
mode: "none",
|
|
437
|
+
detail: "guided smoke slash sampling has no real outbound",
|
|
438
|
+
},
|
|
439
|
+
};
|
|
440
|
+
const raw = JSON.stringify(payload, null, 2);
|
|
441
|
+
const sanitized = sanitizeOpenClawEvidenceSample(raw);
|
|
442
|
+
await writeFile(filePath, sanitized, "utf8");
|
|
443
|
+
}
|
|
444
|
+
async function captureSlashInboundDefault(command, deps = {}) {
|
|
445
|
+
let lastObservation = null;
|
|
446
|
+
const loader = deps.loadOpenClawWeixinPublicHelpers ?? loadOpenClawWeixinPublicHelpers;
|
|
447
|
+
const helpers = await loader(deps.publicHelpersOptions);
|
|
448
|
+
const accountState = helpers.latestAccountState;
|
|
449
|
+
if (!accountState) {
|
|
450
|
+
return {
|
|
451
|
+
command,
|
|
452
|
+
synthetic: true,
|
|
453
|
+
reason: "missing logged-in weixin account state",
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
const publicHelpers = { getUpdates: helpers.getUpdates };
|
|
457
|
+
const state = deps.state ?? {};
|
|
458
|
+
let getUpdatesBuf = typeof state.getUpdatesBuf === "string"
|
|
459
|
+
? state.getUpdatesBuf
|
|
460
|
+
: (typeof accountState.getUpdatesBuf === "string" ? accountState.getUpdatesBuf : "");
|
|
461
|
+
const inbound = await captureSlashInboundFromPublicMessages({
|
|
462
|
+
command,
|
|
463
|
+
input: `/${command === "reply" ? "reply smoke" : command === "allow" ? "allow once" : "status"}`,
|
|
464
|
+
waitTimeoutMs: deps.waitTimeoutMs,
|
|
465
|
+
pollIntervalMs: deps.pollIntervalMs,
|
|
466
|
+
getMessages: async () => {
|
|
467
|
+
if (Array.isArray(state.pendingMessages) && state.pendingMessages.length > 0) {
|
|
468
|
+
const queued = state.pendingMessages;
|
|
469
|
+
state.pendingMessages = [];
|
|
470
|
+
return queued;
|
|
471
|
+
}
|
|
472
|
+
const response = await publicHelpers.getUpdates({
|
|
473
|
+
baseUrl: accountState.baseUrl,
|
|
474
|
+
token: accountState.token,
|
|
475
|
+
get_updates_buf: getUpdatesBuf,
|
|
476
|
+
timeoutMs: DEFAULT_PUBLIC_GET_UPDATES_LONG_POLL_TIMEOUT_MS,
|
|
477
|
+
});
|
|
478
|
+
if (typeof response.get_updates_buf === "string") {
|
|
479
|
+
getUpdatesBuf = response.get_updates_buf;
|
|
480
|
+
state.getUpdatesBuf = response.get_updates_buf;
|
|
481
|
+
}
|
|
482
|
+
const messages = Array.isArray(response.msgs) ? response.msgs : [];
|
|
483
|
+
lastObservation = {
|
|
484
|
+
msgCount: messages.length,
|
|
485
|
+
getUpdatesBuf: typeof response.get_updates_buf === "string" ? response.get_updates_buf : getUpdatesBuf,
|
|
486
|
+
texts: messages.map((message) => extractTextFromPublicWeixinMessage(message)).filter((text) => text.length > 0),
|
|
487
|
+
};
|
|
488
|
+
return messages;
|
|
489
|
+
},
|
|
490
|
+
stashMessages: (messages) => {
|
|
491
|
+
state.pendingMessages = messages;
|
|
492
|
+
},
|
|
493
|
+
});
|
|
494
|
+
return inbound ?? {
|
|
495
|
+
command,
|
|
496
|
+
synthetic: true,
|
|
497
|
+
reason: `timed out waiting for real ${command} inbound`,
|
|
498
|
+
getUpdatesObservation: lastObservation,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
export const captureSlashInboundDefaultForTest = captureSlashInboundDefault;
|
|
502
|
+
async function runDefaultNonSlashVerification(input) {
|
|
503
|
+
const total = input.inputs?.length ?? 10;
|
|
504
|
+
const attempts = [];
|
|
505
|
+
const failedChecks = [];
|
|
506
|
+
const keyFieldsCheck = {
|
|
507
|
+
login: { status: "pass" },
|
|
508
|
+
getupdates: { status: "known-unknown" },
|
|
509
|
+
slashInbound: { status: "pass" },
|
|
510
|
+
warningReply: { status: "known-unknown" },
|
|
511
|
+
};
|
|
512
|
+
const loader = input.loadOpenClawWeixinPublicHelpers ?? loadOpenClawWeixinPublicHelpers;
|
|
513
|
+
let helpers;
|
|
514
|
+
try {
|
|
515
|
+
helpers = await loader(input.publicHelpersOptions);
|
|
516
|
+
}
|
|
517
|
+
catch (error) {
|
|
518
|
+
return {
|
|
519
|
+
passed: 0,
|
|
520
|
+
total,
|
|
521
|
+
failedChecks: [error instanceof Error ? error.message : String(error)],
|
|
522
|
+
attempts,
|
|
523
|
+
keyFieldsCheck,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
const accountState = helpers.latestAccountState;
|
|
527
|
+
if (!accountState) {
|
|
528
|
+
return {
|
|
529
|
+
passed: 0,
|
|
530
|
+
total,
|
|
531
|
+
failedChecks: ["missing logged-in weixin account state"],
|
|
532
|
+
attempts,
|
|
533
|
+
keyFieldsCheck,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
const publicHelpers = { getUpdates: helpers.getUpdates };
|
|
537
|
+
const sendHelper = { sendMessageWeixin: helpers.sendMessageWeixin };
|
|
538
|
+
const state = input.state ?? {};
|
|
539
|
+
let getUpdatesBuf = typeof state.getUpdatesBuf === "string"
|
|
540
|
+
? state.getUpdatesBuf
|
|
541
|
+
: (typeof accountState.getUpdatesBuf === "string" ? accountState.getUpdatesBuf : "");
|
|
542
|
+
const harness = createOpenClawSmokeHarness({ mode: "real-account" });
|
|
543
|
+
for (let index = 0; index < total; index += 1) {
|
|
544
|
+
const expectedText = input.inputs?.[index];
|
|
545
|
+
const inbound = await captureNonSlashInboundFromPublicMessages({
|
|
546
|
+
expectedText,
|
|
547
|
+
waitTimeoutMs: input.waitTimeoutMs,
|
|
548
|
+
pollIntervalMs: input.pollIntervalMs,
|
|
549
|
+
getMessages: async () => {
|
|
550
|
+
if (Array.isArray(state.pendingMessages) && state.pendingMessages.length > 0) {
|
|
551
|
+
const queued = state.pendingMessages;
|
|
552
|
+
state.pendingMessages = [];
|
|
553
|
+
return queued;
|
|
554
|
+
}
|
|
555
|
+
const response = await publicHelpers.getUpdates({
|
|
556
|
+
baseUrl: accountState.baseUrl,
|
|
557
|
+
token: accountState.token,
|
|
558
|
+
get_updates_buf: getUpdatesBuf,
|
|
559
|
+
timeoutMs: DEFAULT_PUBLIC_GET_UPDATES_LONG_POLL_TIMEOUT_MS,
|
|
560
|
+
});
|
|
561
|
+
if (typeof response.get_updates_buf === "string") {
|
|
562
|
+
getUpdatesBuf = response.get_updates_buf;
|
|
563
|
+
state.getUpdatesBuf = response.get_updates_buf;
|
|
564
|
+
}
|
|
565
|
+
return Array.isArray(response.msgs) ? response.msgs : [];
|
|
566
|
+
},
|
|
567
|
+
stashMessages: (messages) => {
|
|
568
|
+
state.pendingMessages = messages;
|
|
569
|
+
},
|
|
570
|
+
});
|
|
571
|
+
if (!inbound) {
|
|
572
|
+
keyFieldsCheck.getupdates = { status: "known-unknown", detail: "missing real inbound capture for non-slash attempt" };
|
|
573
|
+
keyFieldsCheck.warningReply = { status: "known-unknown" };
|
|
574
|
+
failedChecks.push("missing real inbound capture for non-slash attempt");
|
|
575
|
+
attempts.push({
|
|
576
|
+
input: expectedText ?? undefined,
|
|
577
|
+
inbound: null,
|
|
578
|
+
warningReply: { ok: false, text: "" },
|
|
579
|
+
persisted: false,
|
|
580
|
+
});
|
|
581
|
+
break;
|
|
582
|
+
}
|
|
583
|
+
keyFieldsCheck.getupdates = { status: "pass" };
|
|
584
|
+
const route = await harness.handleIncomingText(String(inbound.text ?? ""));
|
|
585
|
+
const warningText = route.route === "guard-reject" ? FIXED_NON_SLASH_WARNING_TEXT : "";
|
|
586
|
+
let warningReply;
|
|
587
|
+
try {
|
|
588
|
+
const response = await sendHelper.sendMessageWeixin({
|
|
589
|
+
to: String(inbound.fromUserId ?? ""),
|
|
590
|
+
text: warningText,
|
|
591
|
+
opts: {
|
|
592
|
+
baseUrl: accountState.baseUrl,
|
|
593
|
+
token: accountState.token,
|
|
594
|
+
contextToken: typeof inbound.contextToken === "string" ? inbound.contextToken : undefined,
|
|
595
|
+
},
|
|
596
|
+
});
|
|
597
|
+
warningReply = {
|
|
598
|
+
ok: route.route === "guard-reject" && warningText === FIXED_NON_SLASH_WARNING_TEXT,
|
|
599
|
+
text: warningText,
|
|
600
|
+
messageId: response.messageId,
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
catch (error) {
|
|
604
|
+
warningReply = {
|
|
605
|
+
ok: false,
|
|
606
|
+
text: warningText,
|
|
607
|
+
error: error instanceof Error ? error.message : String(error),
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
attempts.push({
|
|
611
|
+
input: String(inbound.text ?? expectedText ?? ""),
|
|
612
|
+
inbound,
|
|
613
|
+
warningReply,
|
|
614
|
+
persisted: warningReply?.ok === true,
|
|
615
|
+
});
|
|
616
|
+
const validationError = validateNonSlashAttempt(attempts.at(-1) ?? null);
|
|
617
|
+
if (validationError) {
|
|
618
|
+
keyFieldsCheck.warningReply = { status: "known-unknown", detail: validationError };
|
|
619
|
+
failedChecks.push(validationError);
|
|
620
|
+
break;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
const passed = attempts.filter((attempt) => validateNonSlashAttempt(attempt) === null).length;
|
|
624
|
+
if (passed === total && failedChecks.length === 0) {
|
|
625
|
+
keyFieldsCheck.warningReply = { status: "pass" };
|
|
626
|
+
}
|
|
627
|
+
return {
|
|
628
|
+
passed,
|
|
629
|
+
total,
|
|
630
|
+
failedChecks,
|
|
631
|
+
attempts,
|
|
632
|
+
keyFieldsCheck,
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
export const runDefaultNonSlashVerificationForTest = runDefaultNonSlashVerification;
|
|
636
|
+
async function runSlashSampling(run, captureSlashInbound, writeLine) {
|
|
637
|
+
const harness = createOpenClawSmokeHarness({ mode: "real-account" });
|
|
638
|
+
for (const step of SLASH_SAMPLE_STEPS) {
|
|
639
|
+
const inbound = await captureSlashInbound(step.command);
|
|
640
|
+
const route = await harness.handleIncomingText(step.input);
|
|
641
|
+
const routeResult = route.route;
|
|
642
|
+
const hasRealInbound = Boolean(inbound) && (inbound?.synthetic !== true);
|
|
643
|
+
const completed = hasRealInbound && routeResult === "stub";
|
|
644
|
+
await writeSlashCommandEvidence(run, {
|
|
645
|
+
command: step.command,
|
|
646
|
+
text: step.input,
|
|
647
|
+
evidenceFile: step.evidenceFile,
|
|
648
|
+
inbound,
|
|
649
|
+
routeResult,
|
|
650
|
+
completed,
|
|
651
|
+
});
|
|
652
|
+
if (!completed) {
|
|
653
|
+
return {
|
|
654
|
+
ok: false,
|
|
655
|
+
reason: `slash sampling incomplete: ${step.input}`,
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
if (step.command === "status") {
|
|
659
|
+
await printGuidedPrompt("已收到 `/status`,下一步请发送 `/reply smoke`", writeLine);
|
|
660
|
+
}
|
|
661
|
+
else if (step.command === "reply") {
|
|
662
|
+
await printGuidedPrompt("已收到 `/reply smoke`,下一步请发送 `/allow once`", writeLine);
|
|
663
|
+
}
|
|
664
|
+
else {
|
|
665
|
+
await printGuidedPrompt("已收到 `/allow once`,下一步开始发送 10 条普通文本", writeLine);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return { ok: true };
|
|
669
|
+
}
|
|
670
|
+
async function updateApiSamplesSanitizedDoc(run, filePath) {
|
|
671
|
+
let previous = "";
|
|
672
|
+
try {
|
|
673
|
+
previous = await readFile(filePath, "utf8");
|
|
674
|
+
}
|
|
675
|
+
catch {
|
|
676
|
+
previous = "# WeChat Stage A API 脱敏样本\n";
|
|
677
|
+
}
|
|
678
|
+
const normalizedPrevious = previous.trimEnd();
|
|
679
|
+
const evidenceRelativePath = path.relative(path.dirname(filePath), run.evidenceDir).split(path.sep).join("/");
|
|
680
|
+
const section = [
|
|
681
|
+
`## slash 采样更新(${run.runId})`,
|
|
682
|
+
`- 证据目录:\`${evidenceRelativePath}\``,
|
|
683
|
+
"- 命令样本:`/status`、`/reply smoke`、`/allow once`",
|
|
684
|
+
`- 引用文件:\`${GUIDED_SMOKE_EVIDENCE_FILES.statusCommand}\`、\`${GUIDED_SMOKE_EVIDENCE_FILES.replyCommand}\`、\`${GUIDED_SMOKE_EVIDENCE_FILES.allowCommand}\``,
|
|
685
|
+
"- outbound:`none`(无真实出站)",
|
|
686
|
+
"",
|
|
687
|
+
].join("\n");
|
|
688
|
+
const nextContent = `${normalizedPrevious}\n\n${section}`;
|
|
689
|
+
await writeFile(filePath, nextContent, "utf8");
|
|
690
|
+
}
|
|
691
|
+
function normalizeQrLoginResult(result) {
|
|
692
|
+
if (!result || typeof result !== "object") {
|
|
693
|
+
return null;
|
|
694
|
+
}
|
|
695
|
+
const candidate = result;
|
|
696
|
+
if (candidate.status !== "success" && candidate.status !== "timeout") {
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
699
|
+
if (candidate.status === "success") {
|
|
700
|
+
if (candidate.connected !== true) {
|
|
701
|
+
return null;
|
|
702
|
+
}
|
|
703
|
+
const hasPrintedQr = candidate.qrPrinted === true;
|
|
704
|
+
const qrUrl = typeof candidate.qrDataUrl === "string" && candidate.qrDataUrl.trim().length > 0
|
|
705
|
+
? candidate.qrDataUrl
|
|
706
|
+
: undefined;
|
|
707
|
+
const hasQrUrl = Boolean(qrUrl);
|
|
708
|
+
if (!hasPrintedQr && !hasQrUrl) {
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
const normalizedQrUrl = typeof candidate.qrDataUrl === "string" && candidate.qrDataUrl.trim().length > 0
|
|
713
|
+
? candidate.qrDataUrl
|
|
714
|
+
: undefined;
|
|
715
|
+
return {
|
|
716
|
+
status: candidate.status,
|
|
717
|
+
connected: candidate.connected === true ? true : undefined,
|
|
718
|
+
qrPrinted: candidate.qrPrinted === true,
|
|
719
|
+
qrUrl: normalizedQrUrl,
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
export const normalizeQrLoginResultForTest = normalizeQrLoginResult;
|
|
723
|
+
function normalizeNonSlashVerificationResult(result) {
|
|
724
|
+
if (!result || typeof result !== "object") {
|
|
725
|
+
return {
|
|
726
|
+
passed: 0,
|
|
727
|
+
total: 10,
|
|
728
|
+
failedChecks: ["non-slash verification not implemented"],
|
|
729
|
+
attempts: [],
|
|
730
|
+
keyFieldsCheck: DEFAULT_KEY_FIELDS_CHECK,
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
const candidate = result;
|
|
734
|
+
const passed = Number(candidate.passed);
|
|
735
|
+
const total = Number(candidate.total);
|
|
736
|
+
const failedChecks = Array.isArray(candidate.failedChecks) ? candidate.failedChecks.filter((item) => typeof item === "string") : [];
|
|
737
|
+
const attempts = Array.isArray(candidate.attempts)
|
|
738
|
+
? candidate.attempts.filter((item) => Boolean(item) && typeof item === "object")
|
|
739
|
+
: [];
|
|
740
|
+
const keyFieldsCandidate = candidate.keyFieldsCheck;
|
|
741
|
+
const keyFieldsCheck = {
|
|
742
|
+
login: keyFieldsCandidate?.login?.status ? { status: keyFieldsCandidate.login.status, detail: keyFieldsCandidate.login.detail } : DEFAULT_KEY_FIELDS_CHECK.login,
|
|
743
|
+
getupdates: keyFieldsCandidate?.getupdates?.status
|
|
744
|
+
? { status: keyFieldsCandidate.getupdates.status, detail: keyFieldsCandidate.getupdates.detail }
|
|
745
|
+
: DEFAULT_KEY_FIELDS_CHECK.getupdates,
|
|
746
|
+
slashInbound: keyFieldsCandidate?.slashInbound?.status
|
|
747
|
+
? { status: keyFieldsCandidate.slashInbound.status, detail: keyFieldsCandidate.slashInbound.detail }
|
|
748
|
+
: DEFAULT_KEY_FIELDS_CHECK.slashInbound,
|
|
749
|
+
warningReply: keyFieldsCandidate?.warningReply?.status
|
|
750
|
+
? { status: keyFieldsCandidate.warningReply.status, detail: keyFieldsCandidate.warningReply.detail }
|
|
751
|
+
: DEFAULT_KEY_FIELDS_CHECK.warningReply,
|
|
752
|
+
};
|
|
753
|
+
if (!Number.isFinite(passed) || !Number.isFinite(total)) {
|
|
754
|
+
return {
|
|
755
|
+
passed: 0,
|
|
756
|
+
total: 10,
|
|
757
|
+
failedChecks: ["non-slash verification not implemented"],
|
|
758
|
+
attempts,
|
|
759
|
+
keyFieldsCheck,
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
return {
|
|
763
|
+
passed,
|
|
764
|
+
total,
|
|
765
|
+
failedChecks,
|
|
766
|
+
attempts,
|
|
767
|
+
keyFieldsCheck,
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
async function failWithFinalEvidence(run, input, goNoGoDocPath) {
|
|
771
|
+
run.status = "blocked";
|
|
772
|
+
run.conclusion = "known-unknown";
|
|
773
|
+
if (goNoGoDocPath) {
|
|
774
|
+
try {
|
|
775
|
+
await maybeUpdateGoNoGoDoc(run, goNoGoDocPath, {
|
|
776
|
+
nonSlash: input.nonSlash,
|
|
777
|
+
keyFields: input.keyFields,
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
catch {
|
|
781
|
+
// fall through and always write final status evidence
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
await writeFinalStatusEvidence(run, {
|
|
785
|
+
stage: input.stage,
|
|
786
|
+
status: "blocked",
|
|
787
|
+
conclusion: "known-unknown",
|
|
788
|
+
detail: input.reason,
|
|
789
|
+
});
|
|
790
|
+
return failGuidedSmoke(run, input.reason);
|
|
791
|
+
}
|
|
792
|
+
async function completeWithNoGoFinalEvidence(run, input, goNoGoDocPath) {
|
|
793
|
+
run.status = "completed";
|
|
794
|
+
run.conclusion = "no-go";
|
|
795
|
+
if (goNoGoDocPath) {
|
|
796
|
+
await maybeUpdateGoNoGoDoc(run, goNoGoDocPath, {
|
|
797
|
+
nonSlash: input.nonSlash,
|
|
798
|
+
keyFields: input.keyFields,
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
await writeFinalStatusEvidence(run, {
|
|
802
|
+
stage: input.stage,
|
|
803
|
+
status: "completed",
|
|
804
|
+
conclusion: run.conclusion,
|
|
805
|
+
detail: input.reason,
|
|
806
|
+
});
|
|
807
|
+
return {
|
|
808
|
+
runId: run.runId,
|
|
809
|
+
status: run.status,
|
|
810
|
+
conclusion: run.conclusion,
|
|
811
|
+
evidenceDir: run.evidenceDir,
|
|
812
|
+
reason: input.reason,
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
export async function runGuidedSmoke(options = {}) {
|
|
816
|
+
const run = createGuidedSmokeRun(options);
|
|
817
|
+
const waitTimeoutMs = options.qrWaitTimeoutMs ?? DEFAULT_QR_WAIT_TIMEOUT_MS;
|
|
818
|
+
const writeLine = options.writeLine ?? writeLineDefault;
|
|
819
|
+
const slashCaptureState = {};
|
|
820
|
+
const loadOpenClawPublicHelpers = options.loadOpenClawWeixinPublicHelpers ?? loadOpenClawWeixinPublicHelpers;
|
|
821
|
+
let cachedPublicHelpers = null;
|
|
822
|
+
const getPublicHelpers = async () => {
|
|
823
|
+
if (cachedPublicHelpers) {
|
|
824
|
+
return cachedPublicHelpers;
|
|
825
|
+
}
|
|
826
|
+
cachedPublicHelpers = await loadOpenClawPublicHelpers({
|
|
827
|
+
...options.publicHelpersOptions,
|
|
828
|
+
});
|
|
829
|
+
return cachedPublicHelpers;
|
|
830
|
+
};
|
|
831
|
+
const loadPublicEntry = options.loadPublicEntry ?? (async () => {
|
|
832
|
+
const helpers = await getPublicHelpers();
|
|
833
|
+
return helpers.entry;
|
|
834
|
+
});
|
|
835
|
+
const runSelfTest = options.runSelfTest ?? runGuidedSelfTestDefault;
|
|
836
|
+
const getDependencyVersions = options.getDependencyVersions ?? resolveDependencyVersionsFromPackageJson;
|
|
837
|
+
const runQrLogin = options.runQrLogin ?? (async (input) => {
|
|
838
|
+
const helpers = await getPublicHelpers();
|
|
839
|
+
return runQrLoginDefault({
|
|
840
|
+
...input,
|
|
841
|
+
qrGateway: helpers.qrGateway,
|
|
842
|
+
writeLine,
|
|
843
|
+
});
|
|
844
|
+
});
|
|
845
|
+
const captureSlashInbound = options.captureSlashInbound ?? ((command) => captureSlashInboundDefault(command, {
|
|
846
|
+
state: slashCaptureState,
|
|
847
|
+
loadOpenClawWeixinPublicHelpers: async () => getPublicHelpers(),
|
|
848
|
+
waitTimeoutMs: options.slashCaptureWaitTimeoutMs,
|
|
849
|
+
pollIntervalMs: options.slashCapturePollIntervalMs,
|
|
850
|
+
}));
|
|
851
|
+
const runNonSlashVerification = options.runNonSlashVerification ?? (() => runDefaultNonSlashVerification({
|
|
852
|
+
state: slashCaptureState,
|
|
853
|
+
loadOpenClawWeixinPublicHelpers: async () => getPublicHelpers(),
|
|
854
|
+
}));
|
|
855
|
+
const apiSamplesDocPath = options.apiSamplesDocPath ??
|
|
856
|
+
(run.evidenceBaseDir === DEFAULT_EVIDENCE_BASE_DIR ? DEFAULT_API_SAMPLES_DOC_PATH : undefined);
|
|
857
|
+
const goNoGoDocPath = options.goNoGoDocPath ??
|
|
858
|
+
(run.evidenceBaseDir === DEFAULT_EVIDENCE_BASE_DIR ? DEFAULT_GO_NO_GO_DOC_PATH : undefined);
|
|
859
|
+
const blockedDefaults = {
|
|
860
|
+
nonSlash: { passed: 0, total: 10, failedChecks: [] },
|
|
861
|
+
keyFields: DEFAULT_KEY_FIELDS_CHECK,
|
|
862
|
+
};
|
|
863
|
+
let evidenceDirectoryCreation;
|
|
864
|
+
try {
|
|
865
|
+
await mkdir(run.evidenceDir, { recursive: true });
|
|
866
|
+
evidenceDirectoryCreation = { status: "pass", detail: run.evidenceDir };
|
|
867
|
+
}
|
|
868
|
+
catch (error) {
|
|
869
|
+
evidenceDirectoryCreation = {
|
|
870
|
+
status: "fail",
|
|
871
|
+
detail: `evidence directory creation failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
let publicEntryLoad;
|
|
875
|
+
try {
|
|
876
|
+
const entry = await loadPublicEntry();
|
|
877
|
+
publicEntryLoad = { status: "pass", detail: entry.entryRelativePath };
|
|
878
|
+
}
|
|
879
|
+
catch (error) {
|
|
880
|
+
publicEntryLoad = {
|
|
881
|
+
status: "fail",
|
|
882
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
let selfTestResult;
|
|
886
|
+
try {
|
|
887
|
+
selfTestResult = await runSelfTest();
|
|
888
|
+
}
|
|
889
|
+
catch (error) {
|
|
890
|
+
selfTestResult = {
|
|
891
|
+
ok: false,
|
|
892
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
let dependencyVersions;
|
|
896
|
+
let dependencyVersionResolution;
|
|
897
|
+
try {
|
|
898
|
+
dependencyVersions = getDependencyVersions();
|
|
899
|
+
dependencyVersionResolution = { status: "pass", detail: "dependency versions resolved" };
|
|
900
|
+
}
|
|
901
|
+
catch (error) {
|
|
902
|
+
dependencyVersions = {
|
|
903
|
+
"@tencent-weixin/openclaw-weixin": "unknown",
|
|
904
|
+
openclaw: "unknown",
|
|
905
|
+
};
|
|
906
|
+
dependencyVersionResolution = {
|
|
907
|
+
status: "fail",
|
|
908
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
if (evidenceDirectoryCreation.status === "fail") {
|
|
912
|
+
return failGuidedSmoke(run, evidenceDirectoryCreation.detail);
|
|
913
|
+
}
|
|
914
|
+
await writePreflightEvidence(run, {
|
|
915
|
+
runId: run.runId,
|
|
916
|
+
cwd: run.cwd,
|
|
917
|
+
nodeVersion: process.versions.node,
|
|
918
|
+
dependencyVersions,
|
|
919
|
+
dependencyVersionResolution,
|
|
920
|
+
publicEntryLoad,
|
|
921
|
+
evidenceDirectoryCreation,
|
|
922
|
+
selfTest: {
|
|
923
|
+
status: selfTestResult.ok ? "pass" : "fail",
|
|
924
|
+
detail: selfTestResult.reason ?? (selfTestResult.ok ? "self-test passed" : "self-test failed"),
|
|
925
|
+
},
|
|
926
|
+
});
|
|
927
|
+
const preflightFailed = publicEntryLoad.status !== "pass" ||
|
|
928
|
+
evidenceDirectoryCreation.status !== "pass" ||
|
|
929
|
+
dependencyVersionResolution.status !== "pass" ||
|
|
930
|
+
!selfTestResult.ok;
|
|
931
|
+
if (preflightFailed) {
|
|
932
|
+
return failGuidedSmoke(run, publicEntryLoad.status !== "pass"
|
|
933
|
+
? publicEntryLoad.detail
|
|
934
|
+
: dependencyVersionResolution.status !== "pass"
|
|
935
|
+
? dependencyVersionResolution.detail
|
|
936
|
+
: selfTestResult.reason);
|
|
937
|
+
}
|
|
938
|
+
try {
|
|
939
|
+
const qrResult = await runQrLogin({ waitTimeoutMs });
|
|
940
|
+
await writeQrStartEvidence(run, "pass", "loginWithQrStart succeeded", waitTimeoutMs);
|
|
941
|
+
const normalizedQrResult = normalizeQrLoginResult(qrResult);
|
|
942
|
+
if (!normalizedQrResult) {
|
|
943
|
+
const qrFailureDetail = pickFirstString(qrResult, ["message", "detail", "reason"]);
|
|
944
|
+
return failWithFinalEvidence(run, {
|
|
945
|
+
stage: "qr-login",
|
|
946
|
+
reason: qrFailureDetail || "invalid qr login result: missing qr code or qr url",
|
|
947
|
+
nonSlash: blockedDefaults.nonSlash,
|
|
948
|
+
keyFields: blockedDefaults.keyFields,
|
|
949
|
+
}, goNoGoDocPath);
|
|
950
|
+
}
|
|
951
|
+
if (normalizedQrResult.status === "timeout") {
|
|
952
|
+
await writeLoginSuccessEvidence(run, "timeout", waitTimeoutMs);
|
|
953
|
+
return failWithFinalEvidence(run, {
|
|
954
|
+
stage: "qr-login",
|
|
955
|
+
reason: "loginWithQrWait timeout",
|
|
956
|
+
nonSlash: blockedDefaults.nonSlash,
|
|
957
|
+
keyFields: blockedDefaults.keyFields,
|
|
958
|
+
}, goNoGoDocPath);
|
|
959
|
+
}
|
|
960
|
+
await writeLoginSuccessEvidence(run, "success", waitTimeoutMs);
|
|
961
|
+
await printGuidedPrompt("二维码登录成功,下一步请发送 `/status`", writeLine);
|
|
962
|
+
}
|
|
963
|
+
catch (error) {
|
|
964
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
965
|
+
await writeQrStartEvidence(run, "fail", detail, waitTimeoutMs);
|
|
966
|
+
return failWithFinalEvidence(run, {
|
|
967
|
+
stage: "qr-login",
|
|
968
|
+
reason: detail,
|
|
969
|
+
nonSlash: blockedDefaults.nonSlash,
|
|
970
|
+
keyFields: blockedDefaults.keyFields,
|
|
971
|
+
}, goNoGoDocPath);
|
|
972
|
+
}
|
|
973
|
+
let slashSampling;
|
|
974
|
+
try {
|
|
975
|
+
slashSampling = await runSlashSampling(run, captureSlashInbound, writeLine);
|
|
976
|
+
}
|
|
977
|
+
catch (error) {
|
|
978
|
+
return failWithFinalEvidence(run, {
|
|
979
|
+
stage: "slash-sampling",
|
|
980
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
981
|
+
nonSlash: blockedDefaults.nonSlash,
|
|
982
|
+
keyFields: blockedDefaults.keyFields,
|
|
983
|
+
}, goNoGoDocPath);
|
|
984
|
+
}
|
|
985
|
+
if (!slashSampling.ok) {
|
|
986
|
+
return failWithFinalEvidence(run, {
|
|
987
|
+
stage: "slash-sampling",
|
|
988
|
+
reason: slashSampling.reason ?? "slash sampling incomplete",
|
|
989
|
+
nonSlash: blockedDefaults.nonSlash,
|
|
990
|
+
keyFields: blockedDefaults.keyFields,
|
|
991
|
+
}, goNoGoDocPath);
|
|
992
|
+
}
|
|
993
|
+
if (apiSamplesDocPath) {
|
|
994
|
+
try {
|
|
995
|
+
await updateApiSamplesSanitizedDoc(run, apiSamplesDocPath);
|
|
996
|
+
}
|
|
997
|
+
catch (error) {
|
|
998
|
+
return failWithFinalEvidence(run, {
|
|
999
|
+
stage: "documentation-update",
|
|
1000
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1001
|
+
nonSlash: blockedDefaults.nonSlash,
|
|
1002
|
+
keyFields: blockedDefaults.keyFields,
|
|
1003
|
+
}, goNoGoDocPath);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
let nonSlashVerificationRaw;
|
|
1007
|
+
try {
|
|
1008
|
+
nonSlashVerificationRaw = await runNonSlashVerification();
|
|
1009
|
+
}
|
|
1010
|
+
catch (error) {
|
|
1011
|
+
return failWithFinalEvidence(run, {
|
|
1012
|
+
stage: "non-slash-verification",
|
|
1013
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1014
|
+
nonSlash: blockedDefaults.nonSlash,
|
|
1015
|
+
keyFields: blockedDefaults.keyFields,
|
|
1016
|
+
}, goNoGoDocPath);
|
|
1017
|
+
}
|
|
1018
|
+
const nonSlashVerification = normalizeNonSlashVerificationResult(nonSlashVerificationRaw);
|
|
1019
|
+
const attempts = nonSlashVerification.attempts.length > 0
|
|
1020
|
+
? nonSlashVerification.attempts
|
|
1021
|
+
: [{
|
|
1022
|
+
input: "(missing non-slash attempt evidence)",
|
|
1023
|
+
inbound: null,
|
|
1024
|
+
warningReply: { ok: false, text: "" },
|
|
1025
|
+
persisted: false,
|
|
1026
|
+
}];
|
|
1027
|
+
for (let index = 0; index < attempts.length; index += 1) {
|
|
1028
|
+
const writeResult = await writeNonSlashAttemptEvidence(run, {
|
|
1029
|
+
index,
|
|
1030
|
+
attempt: attempts[index],
|
|
1031
|
+
});
|
|
1032
|
+
if (writeResult.sensitiveResidue) {
|
|
1033
|
+
return failWithFinalEvidence(run, {
|
|
1034
|
+
stage: "non-slash-verification",
|
|
1035
|
+
reason: `sensitive residue detected in ${writeResult.fileName}: ${writeResult.sensitiveResidue}`,
|
|
1036
|
+
nonSlash: {
|
|
1037
|
+
passed: nonSlashVerification.passed,
|
|
1038
|
+
total: nonSlashVerification.total,
|
|
1039
|
+
failedChecks: nonSlashVerification.failedChecks,
|
|
1040
|
+
},
|
|
1041
|
+
keyFields: nonSlashVerification.keyFieldsCheck,
|
|
1042
|
+
}, goNoGoDocPath);
|
|
1043
|
+
}
|
|
1044
|
+
const validationError = validateNonSlashAttempt(attempts[index]);
|
|
1045
|
+
if (validationError) {
|
|
1046
|
+
return completeWithNoGoFinalEvidence(run, {
|
|
1047
|
+
stage: "non-slash-verification",
|
|
1048
|
+
reason: validationError,
|
|
1049
|
+
nonSlash: {
|
|
1050
|
+
passed: nonSlashVerification.passed,
|
|
1051
|
+
total: nonSlashVerification.total,
|
|
1052
|
+
failedChecks: nonSlashVerification.failedChecks,
|
|
1053
|
+
},
|
|
1054
|
+
keyFields: nonSlashVerification.keyFieldsCheck,
|
|
1055
|
+
}, goNoGoDocPath);
|
|
1056
|
+
}
|
|
1057
|
+
await printGuidedPrompt(`普通文本验证进度:${index + 1}/${attempts.length}`, writeLine);
|
|
1058
|
+
}
|
|
1059
|
+
const nonSlashPassed = nonSlashVerification.passed === 10 && nonSlashVerification.total === 10;
|
|
1060
|
+
if (!nonSlashPassed) {
|
|
1061
|
+
const reason = nonSlashVerification.failedChecks[0] ?? `non-slash verification incomplete: ${nonSlashVerification.passed}/${nonSlashVerification.total}`;
|
|
1062
|
+
return completeWithNoGoFinalEvidence(run, {
|
|
1063
|
+
stage: "non-slash-verification",
|
|
1064
|
+
reason,
|
|
1065
|
+
nonSlash: {
|
|
1066
|
+
passed: nonSlashVerification.passed,
|
|
1067
|
+
total: nonSlashVerification.total,
|
|
1068
|
+
failedChecks: nonSlashVerification.failedChecks,
|
|
1069
|
+
},
|
|
1070
|
+
keyFields: nonSlashVerification.keyFieldsCheck,
|
|
1071
|
+
}, goNoGoDocPath);
|
|
1072
|
+
}
|
|
1073
|
+
const keyFields = nonSlashVerification.keyFieldsCheck;
|
|
1074
|
+
await writeKeyFieldsCheckEvidence(run, keyFields);
|
|
1075
|
+
const keyFieldsAllPassed = keyFields.login.status === "pass" &&
|
|
1076
|
+
keyFields.getupdates.status === "pass" &&
|
|
1077
|
+
keyFields.slashInbound.status === "pass" &&
|
|
1078
|
+
keyFields.warningReply.status === "pass";
|
|
1079
|
+
run.status = "completed";
|
|
1080
|
+
run.conclusion = keyFieldsAllPassed ? "go" : "no-go";
|
|
1081
|
+
if (goNoGoDocPath) {
|
|
1082
|
+
try {
|
|
1083
|
+
await updateGoNoGoDoc(run, goNoGoDocPath, {
|
|
1084
|
+
nonSlash: {
|
|
1085
|
+
passed: nonSlashVerification.passed,
|
|
1086
|
+
total: nonSlashVerification.total,
|
|
1087
|
+
failedChecks: nonSlashVerification.failedChecks,
|
|
1088
|
+
},
|
|
1089
|
+
keyFields,
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
catch (error) {
|
|
1093
|
+
return failWithFinalEvidence(run, {
|
|
1094
|
+
stage: "documentation-update",
|
|
1095
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1096
|
+
nonSlash: {
|
|
1097
|
+
passed: nonSlashVerification.passed,
|
|
1098
|
+
total: nonSlashVerification.total,
|
|
1099
|
+
failedChecks: nonSlashVerification.failedChecks,
|
|
1100
|
+
},
|
|
1101
|
+
keyFields,
|
|
1102
|
+
}, goNoGoDocPath);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
await writeFinalStatusEvidence(run, {
|
|
1106
|
+
stage: "completed",
|
|
1107
|
+
status: "completed",
|
|
1108
|
+
conclusion: run.conclusion,
|
|
1109
|
+
detail: "guided smoke completed",
|
|
1110
|
+
});
|
|
1111
|
+
return {
|
|
1112
|
+
runId: run.runId,
|
|
1113
|
+
status: run.status,
|
|
1114
|
+
conclusion: run.conclusion,
|
|
1115
|
+
evidenceDir: run.evidenceDir,
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
async function runGuidedSmokeCli() {
|
|
1119
|
+
const result = await runGuidedSmoke();
|
|
1120
|
+
const output = {
|
|
1121
|
+
route: "guided-smoke",
|
|
1122
|
+
...result,
|
|
1123
|
+
evidenceFiles: GUIDED_SMOKE_EVIDENCE_FILES,
|
|
1124
|
+
};
|
|
1125
|
+
process.stdout.write(`${JSON.stringify([output], null, 2)}\n`);
|
|
1126
|
+
process.exitCode = getGuidedSmokeExitCode(result);
|
|
1127
|
+
}
|
|
1128
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
1129
|
+
runGuidedSmokeCli().catch((error) => {
|
|
1130
|
+
const message = error instanceof Error ? error.stack ?? error.message : String(error);
|
|
1131
|
+
process.stderr.write(`${message}\n`);
|
|
1132
|
+
process.exitCode = 1;
|
|
1133
|
+
});
|
|
1134
|
+
}
|