





















@@ -3,6 +3,10 @@ import fs from "node:fs/promises";
33import path from "node:path";
44import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
55import { ensureRepoBoundDirectory, resolveRepoRelativeOutputDir } from "../cli-paths.js";
6+import {
7+acquireQaCredentialLease,
8+startQaCredentialLeaseHeartbeat,
9+} from "../live-transports/shared/credential-lease.runtime.js";
610711export type MantisSlackDesktopSmokeOptions = {
812alternateModel?: string;
@@ -49,6 +53,17 @@ type CommandRunner = (
4953options: SpawnOptions,
5054) => Promise<CommandResult>;
515556+type SlackGatewayCredentialPayload = {
57+channelId: string;
58+sutAppToken: string;
59+sutBotToken: string;
60+};
61+62+type SlackGatewayCredentialLease = Awaited<
63+ReturnType<typeof acquireQaCredentialLease<SlackGatewayCredentialPayload>>
64+>;
65+type SlackGatewayCredentialHeartbeat = ReturnType<typeof startQaCredentialLeaseHeartbeat>;
66+5267type CrabboxInspect = {
5368host?: string;
5469id?: string;
@@ -194,12 +209,110 @@ function buildCrabboxEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
194209if (!trimToValue(next.OPENCLAW_MANTIS_SLACK_BOT_TOKEN) && trimToValue(next.SLACK_BOT_TOKEN)) {
195210next.OPENCLAW_MANTIS_SLACK_BOT_TOKEN = next.SLACK_BOT_TOKEN;
196211}
212+if (
213+!trimToValue(next.OPENCLAW_MANTIS_SLACK_BOT_TOKEN) &&
214+trimToValue(next.OPENCLAW_QA_SLACK_SUT_BOT_TOKEN)
215+) {
216+next.OPENCLAW_MANTIS_SLACK_BOT_TOKEN = next.OPENCLAW_QA_SLACK_SUT_BOT_TOKEN;
217+}
197218if (!trimToValue(next.OPENCLAW_MANTIS_SLACK_APP_TOKEN) && trimToValue(next.SLACK_APP_TOKEN)) {
198219next.OPENCLAW_MANTIS_SLACK_APP_TOKEN = next.SLACK_APP_TOKEN;
199220}
221+if (
222+!trimToValue(next.OPENCLAW_MANTIS_SLACK_APP_TOKEN) &&
223+trimToValue(next.OPENCLAW_QA_SLACK_SUT_APP_TOKEN)
224+) {
225+next.OPENCLAW_MANTIS_SLACK_APP_TOKEN = next.OPENCLAW_QA_SLACK_SUT_APP_TOKEN;
226+}
227+if (
228+!trimToValue(next.OPENCLAW_MANTIS_SLACK_CHANNEL_ID) &&
229+trimToValue(next.OPENCLAW_QA_SLACK_CHANNEL_ID)
230+) {
231+next.OPENCLAW_MANTIS_SLACK_CHANNEL_ID = next.OPENCLAW_QA_SLACK_CHANNEL_ID;
232+}
200233return next;
201234}
202235236+function resolveSlackGatewayEnvPayload(env: NodeJS.ProcessEnv): SlackGatewayCredentialPayload {
237+const channelId = trimToValue(env.OPENCLAW_QA_SLACK_CHANNEL_ID);
238+const sutBotToken = trimToValue(env.OPENCLAW_QA_SLACK_SUT_BOT_TOKEN);
239+const sutAppToken = trimToValue(env.OPENCLAW_QA_SLACK_SUT_APP_TOKEN);
240+if (!channelId || !sutBotToken || !sutAppToken) {
241+throw new Error(
242+"Gateway setup requires OPENCLAW_QA_SLACK_CHANNEL_ID, OPENCLAW_QA_SLACK_SUT_BOT_TOKEN, and OPENCLAW_QA_SLACK_SUT_APP_TOKEN when using --credential-source env.",
243+);
244+}
245+return {
246+ channelId,
247+ sutAppToken,
248+ sutBotToken,
249+};
250+}
251+252+function parseSlackGatewayCredentialPayload(payload: unknown): SlackGatewayCredentialPayload {
253+if (!payload || typeof payload !== "object") {
254+throw new Error("Slack credential payload must be an object.");
255+}
256+const candidate = payload as Record<string, unknown>;
257+const channelId =
258+typeof candidate.channelId === "string" ? trimToValue(candidate.channelId) : undefined;
259+const sutBotToken =
260+typeof candidate.sutBotToken === "string" ? trimToValue(candidate.sutBotToken) : undefined;
261+const sutAppToken =
262+typeof candidate.sutAppToken === "string" ? trimToValue(candidate.sutAppToken) : undefined;
263+if (!channelId || !sutBotToken || !sutAppToken) {
264+throw new Error(
265+"Slack credential payload must include channelId, sutBotToken, and sutAppToken.",
266+);
267+}
268+return {
269+ channelId,
270+ sutAppToken,
271+ sutBotToken,
272+};
273+}
274+275+async function prepareGatewayCredentialEnv(params: {
276+credentialRole: string;
277+credentialSource: string;
278+env: NodeJS.ProcessEnv;
279+gatewaySetup: boolean;
280+}) {
281+if (!params.gatewaySetup) {
282+return {};
283+}
284+if (
285+trimToValue(params.env.OPENCLAW_MANTIS_SLACK_BOT_TOKEN) &&
286+trimToValue(params.env.OPENCLAW_MANTIS_SLACK_APP_TOKEN)
287+) {
288+return {};
289+}
290+const credentialLease = await acquireQaCredentialLease<SlackGatewayCredentialPayload>({
291+env: params.env,
292+kind: "slack",
293+source: params.credentialSource,
294+role: params.credentialRole,
295+resolveEnvPayload: () => resolveSlackGatewayEnvPayload(params.env),
296+parsePayload: parseSlackGatewayCredentialPayload,
297+});
298+const leaseHeartbeat = startQaCredentialLeaseHeartbeat(credentialLease);
299+const payload = credentialLease.payload;
300+params.env.OPENCLAW_MANTIS_SLACK_BOT_TOKEN = payload.sutBotToken;
301+params.env.OPENCLAW_MANTIS_SLACK_APP_TOKEN = payload.sutAppToken;
302+params.env.OPENCLAW_MANTIS_SLACK_CHANNEL_ID =
303+trimToValue(params.env.OPENCLAW_MANTIS_SLACK_CHANNEL_ID) ?? payload.channelId;
304+params.env.OPENCLAW_QA_SLACK_CHANNEL_ID =
305+trimToValue(params.env.OPENCLAW_QA_SLACK_CHANNEL_ID) ?? payload.channelId;
306+params.env.OPENCLAW_QA_SLACK_SUT_BOT_TOKEN =
307+trimToValue(params.env.OPENCLAW_QA_SLACK_SUT_BOT_TOKEN) ?? payload.sutBotToken;
308+params.env.OPENCLAW_QA_SLACK_SUT_APP_TOKEN =
309+trimToValue(params.env.OPENCLAW_QA_SLACK_SUT_APP_TOKEN) ?? payload.sutAppToken;
310+return {
311+ credentialLease,
312+ leaseHeartbeat,
313+};
314+}
315+203316function extractLeaseId(output: string) {
204317return output.match(/\b(?:cbx_[a-f0-9]+|tbx_[A-Za-z0-9_-]+)\b/u)?.[0];
205318}
@@ -656,13 +769,23 @@ export async function runMantisSlackDesktopSmoke(
656769const remoteOutputDir = `/tmp/openclaw-mantis-slack-desktop-${startedAt
657770 .toISOString()
658771 .replace(/[^0-9A-Za-z]/gu, "-")}`;
772+let credentialLease: SlackGatewayCredentialLease | undefined;
773+let leaseHeartbeat: SlackGatewayCredentialHeartbeat | undefined;
659774let leaseId = explicitLeaseId;
660775let summary: MantisSlackDesktopSmokeSummary | undefined;
661776let screenshotPath: string | undefined;
662777let slackQaDir: string | undefined;
663778let videoPath: string | undefined;
664779665780try {
781+const preparedCredentialEnv = await prepareGatewayCredentialEnv({
782+ credentialRole,
783+ credentialSource,
784+ env,
785+ gatewaySetup,
786+});
787+credentialLease = preparedCredentialEnv.credentialLease;
788+leaseHeartbeat = preparedCredentialEnv.leaseHeartbeat;
666789leaseId =
667790leaseId ??
668791(await warmupCrabbox({
@@ -718,6 +841,7 @@ export async function runMantisSlackDesktopSmoke(
718841remoteRunError = error;
719842return { stdout: "", stderr: "" };
720843});
844+leaseHeartbeat?.throwIfFailed();
721845await copyRemoteArtifacts({
722846cwd: repoRoot,
723847 env,
@@ -814,5 +938,15 @@ export async function runMantisSlackDesktopSmoke(
814938if (summary?.status === "pass" && createdLease && leaseId && !keepLease) {
815939await stopCrabbox({ crabboxBin, cwd: repoRoot, env, leaseId, provider, runner });
816940}
941+if (leaseHeartbeat) {
942+await leaseHeartbeat.stop().catch((error: unknown) => {
943+console.warn(`Slack credential heartbeat cleanup failed: ${formatErrorMessage(error)}`);
944+});
945+}
946+if (credentialLease) {
947+await credentialLease.release().catch((error: unknown) => {
948+console.warn(`Slack credential release failed: ${formatErrorMessage(error)}`);
949+});
950+}
817951}
818952}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。