



























@@ -122,6 +122,9 @@ type SlackQaSummary = {
122122startedAt: string;
123123};
124124125+type SlackCredentialLease = Awaited<ReturnType<typeof acquireQaCredentialLease<SlackQaRuntimeEnv>>>;
126+type SlackCredentialHeartbeat = ReturnType<typeof startQaCredentialLeaseHeartbeat>;
127+125128const SLACK_QA_CAPTURE_CONTENT_ENV = "OPENCLAW_QA_SLACK_CAPTURE_CONTENT";
126129const QA_REDACT_PUBLIC_METADATA_ENV = "OPENCLAW_QA_REDACT_PUBLIC_METADATA";
127130const SLACK_QA_ENV_KEYS = [
@@ -212,6 +215,23 @@ function isTruthyOptIn(value: string | undefined) {
212215return normalized === "1" || normalized === "true" || normalized === "yes";
213216}
214217218+function inferSlackCredentialSource(
219+value: string | undefined,
220+env: NodeJS.ProcessEnv = process.env,
221+): "convex" | "env" {
222+const normalized =
223+value?.trim().toLowerCase() || env.OPENCLAW_QA_CREDENTIAL_SOURCE?.trim().toLowerCase();
224+return normalized === "convex" ? "convex" : "env";
225+}
226+227+function inferSlackCredentialRole(value: string | undefined): QaCredentialRole | undefined {
228+const normalized = value?.trim().toLowerCase();
229+if (normalized === "ci" || normalized === "maintainer") {
230+return normalized;
231+}
232+return undefined;
233+}
234+215235function normalizeSlackId(value: string, label: string) {
216236const normalized = value.trim();
217237if (!/^[A-Z][A-Z0-9]+$/.test(normalized)) {
@@ -607,20 +627,8 @@ export async function runSlackQaLive(params: {
607627const alternateModel = params.alternateModel?.trim() || defaultQaModelForMode(providerMode, true);
608628const sutAccountId = params.sutAccountId?.trim() || "sut";
609629const scenarios = findScenario(params.scenarioIds);
610-611-const credentialLease = await acquireQaCredentialLease({
612-kind: "slack",
613-source: params.credentialSource,
614-role: params.credentialRole,
615-resolveEnvPayload: () => resolveSlackQaRuntimeEnv(),
616-parsePayload: parseSlackQaCredentialPayload,
617-});
618-const leaseHeartbeat = startQaCredentialLeaseHeartbeat(credentialLease);
619-const assertLeaseHealthy = () => {
620-leaseHeartbeat.throwIfFailed();
621-};
622-623-const runtimeEnv = credentialLease.payload;
630+const requestedCredentialSource = inferSlackCredentialSource(params.credentialSource);
631+const requestedCredentialRole = inferSlackCredentialRole(params.credentialRole);
624632const redactPublicMetadata = isTruthyOptIn(process.env[QA_REDACT_PUBLIC_METADATA_ENV]);
625633const includeObservedMessageContent = isTruthyOptIn(process.env[SLACK_QA_CAPTURE_CONTENT_ENV]);
626634const startedAt = new Date().toISOString();
@@ -629,18 +637,37 @@ export async function runSlackQaLive(params: {
629637const cleanupIssues: string[] = [];
630638const gatewayDebugDirPath = path.join(outputDir, "gateway-debug");
631639let preservedGatewayDebugArtifacts = false;
640+let credentialLease: SlackCredentialLease | undefined;
641+let leaseHeartbeat: SlackCredentialHeartbeat | undefined;
642+let runtimeEnv: SlackQaRuntimeEnv | undefined;
632643633644try {
645+credentialLease = await acquireQaCredentialLease({
646+kind: "slack",
647+source: params.credentialSource,
648+role: params.credentialRole,
649+resolveEnvPayload: () => resolveSlackQaRuntimeEnv(),
650+parsePayload: parseSlackQaCredentialPayload,
651+});
652+leaseHeartbeat = startQaCredentialLeaseHeartbeat(credentialLease);
653+const assertLeaseHealthy = () => {
654+leaseHeartbeat?.throwIfFailed();
655+};
656+const activeRuntimeEnv = credentialLease.payload;
657+runtimeEnv = activeRuntimeEnv;
658+634659const [driverIdentity, sutIdentity] = await Promise.all([
635-getSlackIdentity(runtimeEnv.driverBotToken),
636-getSlackIdentity(runtimeEnv.sutBotToken),
660+getSlackIdentity(activeRuntimeEnv.driverBotToken),
661+getSlackIdentity(activeRuntimeEnv.sutBotToken),
637662]);
638663if (driverIdentity.userId === sutIdentity.userId) {
639664throw new Error("Slack QA requires two distinct bots for driver and SUT.");
640665}
641666642-const driverClient = createSlackWriteClient(runtimeEnv.driverBotToken, { timeout: 15_000 });
643-const sutReadClient = createSlackWebClient(runtimeEnv.sutBotToken, { timeout: 15_000 });
667+const driverClient = createSlackWriteClient(activeRuntimeEnv.driverBotToken, {
668+timeout: 15_000,
669+});
670+const sutReadClient = createSlackWebClient(activeRuntimeEnv.sutBotToken, { timeout: 15_000 });
644671const gatewayHarness = await startQaLiveLaneGateway({
645672 repoRoot,
646673transport: {
@@ -655,11 +682,11 @@ export async function runSlackQaLive(params: {
655682controlUiEnabled: false,
656683mutateConfig: (cfg) =>
657684buildSlackQaConfig(cfg, {
658-channelId: runtimeEnv.channelId,
685+channelId: activeRuntimeEnv.channelId,
659686driverBotUserId: driverIdentity.userId,
660687 sutAccountId,
661-sutAppToken: runtimeEnv.sutAppToken,
662-sutBotToken: runtimeEnv.sutBotToken,
688+sutAppToken: activeRuntimeEnv.sutAppToken,
689+sutBotToken: activeRuntimeEnv.sutBotToken,
663690}),
664691});
665692try {
@@ -671,13 +698,13 @@ export async function runSlackQaLive(params: {
671698const requestStartedAt = new Date();
672699try {
673700const sent = await sendSlackChannelMessage({
674-channelId: runtimeEnv.channelId,
701+channelId: activeRuntimeEnv.channelId,
675702client: driverClient,
676703text: scenarioRun.input,
677704});
678705if (scenarioRun.expectReply) {
679706const reply = await waitForSlackScenarioReply({
680-channelId: runtimeEnv.channelId,
707+channelId: activeRuntimeEnv.channelId,
681708client: sutReadClient,
682709matchText: scenarioRun.matchText,
683710 observedMessages,
@@ -700,7 +727,7 @@ export async function runSlackQaLive(params: {
700727});
701728} else {
702729await waitForSlackNoReply({
703-channelId: runtimeEnv.channelId,
730+channelId: activeRuntimeEnv.channelId,
704731client: sutReadClient,
705732matchText: scenarioRun.matchText,
706733 observedMessages,
@@ -760,15 +787,19 @@ export async function runSlackQaLive(params: {
760787details: formatErrorMessage(error),
761788});
762789} finally {
763-try {
764-await leaseHeartbeat.stop();
765-} catch (error) {
766-appendLiveLaneIssue(cleanupIssues, "credential heartbeat stop failed", error);
790+if (leaseHeartbeat) {
791+try {
792+await leaseHeartbeat.stop();
793+} catch (error) {
794+appendLiveLaneIssue(cleanupIssues, "credential heartbeat stop failed", error);
795+}
767796}
768-try {
769-await credentialLease.release();
770-} catch (error) {
771-appendLiveLaneIssue(cleanupIssues, "credential release failed", error);
797+if (credentialLease) {
798+try {
799+await credentialLease.release();
800+} catch (error) {
801+appendLiveLaneIssue(cleanupIssues, "credential release failed", error);
802+}
772803}
773804}
774805@@ -779,14 +810,24 @@ export async function runSlackQaLive(params: {
779810const passed = scenarioResults.filter((entry) => entry.status === "pass").length;
780811const failed = scenarioResults.filter((entry) => entry.status === "fail").length;
781812const summary: SlackQaSummary = {
782-credentials: {
783-source: credentialLease.source,
784-kind: credentialLease.kind,
785-role: credentialLease.role,
786-credentialId: redactPublicMetadata ? undefined : credentialLease.credentialId,
787-ownerId: redactPublicMetadata ? undefined : credentialLease.ownerId,
788-},
789-channelId: redactPublicMetadata ? "<redacted>" : runtimeEnv.channelId,
813+credentials: credentialLease
814+ ? {
815+source: credentialLease.source,
816+kind: credentialLease.kind,
817+role: credentialLease.role,
818+credentialId: redactPublicMetadata ? undefined : credentialLease.credentialId,
819+ownerId: redactPublicMetadata ? undefined : credentialLease.ownerId,
820+}
821+ : {
822+source: requestedCredentialSource,
823+kind: "slack",
824+role: requestedCredentialRole,
825+},
826+channelId: runtimeEnv
827+ ? redactPublicMetadata
828+ ? "<redacted>"
829+ : runtimeEnv.channelId
830+ : "<unavailable>",
790831 startedAt,
791832 finishedAt,
792833 cleanupIssues,
@@ -813,9 +854,9 @@ export async function runSlackQaLive(params: {
813854await fs.writeFile(
814855reportPath,
815856`${renderSlackQaMarkdown({
816- channelId: runtimeEnv.channelId,
857+ channelId: runtimeEnv?.channelId ?? "<unavailable>",
817858 cleanupIssues,
818- credentialSource: credentialLease.source,
859+ credentialSource: credentialLease?.source ?? requestedCredentialSource,
819860 finishedAt,
820861 gatewayDebugDirPath: preservedGatewayDebugArtifacts ? gatewayDebugDirPath : undefined,
821862 redactMetadata: redactPublicMetadata,
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。