























暂无文章
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
22import path from "node:path";
33import { setTimeout as sleep } from "node:timers/promises";
44import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
5+import { scanDirectReplyTranscriptSentinels } from "./gateway-log-sentinel.js";
56import { liveTurnTimeoutMs } from "./suite-runtime-agent-common.js";
67import type {
78QaRawSessionStoreEntry,
@@ -16,6 +17,11 @@ type QaGatewayCallEnv = Pick<
16171718const SESSION_STORE_LOCK_RETRY_DELAYS_MS = [1_000, 3_000, 5_000] as const;
181920+type QaSessionTranscriptSummary = {
21+finalText: string;
22+hasDirectReplySelfMessage: boolean;
23+};
24+1925function isSessionStoreLockTimeout(error: unknown) {
2026const text = formatErrorMessage(error);
2127return (
@@ -25,6 +31,73 @@ function isSessionStoreLockTimeout(error: unknown) {
2531);
2632}
273334+function isRecord(value: unknown): value is Record<string, unknown> {
35+return Boolean(value) && typeof value === "object" && !Array.isArray(value);
36+}
37+38+function readNonEmptyString(value: unknown): string | undefined {
39+return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
40+}
41+42+function extractSessionTranscriptText(message: Record<string, unknown>) {
43+const rawContent = message.content;
44+if (typeof rawContent === "string") {
45+return rawContent.trim();
46+}
47+if (!Array.isArray(rawContent)) {
48+return "";
49+}
50+const parts: string[] = [];
51+for (const block of rawContent) {
52+if (typeof block === "string") {
53+if (block.trim()) {
54+parts.push(block.trim());
55+}
56+continue;
57+}
58+if (!isRecord(block)) {
59+continue;
60+}
61+const text = readNonEmptyString(block.text);
62+if (text) {
63+parts.push(text);
64+continue;
65+}
66+const content = readNonEmptyString(block.content);
67+if (
68+content &&
69+(block.type === "output_text" || block.type === "text" || block.type === "message")
70+) {
71+parts.push(content);
72+}
73+}
74+return parts.join("\n").trim();
75+}
76+77+function extractFinalAssistantTextFromTranscript(transcriptBytes: string) {
78+let finalText = "";
79+for (const line of transcriptBytes.split(/\r?\n/u)) {
80+const trimmed = line.trim();
81+if (!trimmed) {
82+continue;
83+}
84+try {
85+const parsed = JSON.parse(trimmed) as unknown;
86+const message = isRecord(parsed) && isRecord(parsed.message) ? parsed.message : undefined;
87+if (!message || message.role !== "assistant") {
88+continue;
89+}
90+const text = extractSessionTranscriptText(message);
91+if (text) {
92+finalText = text;
93+}
94+} catch {
95+// Ignore malformed transcript rows and keep QA summary checks deterministic.
96+}
97+}
98+return finalText;
99+}
100+28101async function callGatewayWithSessionStoreLockRetry<T>(
29102env: QaGatewayCallEnv,
30103method: string,
@@ -106,6 +179,18 @@ async function readSkillStatus(env: QaGatewayCallEnv, agentId = "qa") {
106179return payload.skills ?? [];
107180}
108181182+function resolveQaSessionTranscriptFile(params: {
183+sessionsDir: string;
184+sessionId: string;
185+sessionFile?: string;
186+}) {
187+const explicit = readNonEmptyString(params.sessionFile);
188+if (explicit) {
189+return path.isAbsolute(explicit) ? explicit : path.join(params.sessionsDir, explicit);
190+}
191+return path.join(params.sessionsDir, `${params.sessionId}.jsonl`);
192+}
193+109194async function readRawQaSessionStore(env: Pick<QaSuiteRuntimeEnv, "gateway">) {
110195const storePath = path.join(
111196env.gateway.tempRoot,
@@ -126,4 +211,40 @@ async function readRawQaSessionStore(env: Pick<QaSuiteRuntimeEnv, "gateway">) {
126211}
127212}
128213129-export { createSession, readEffectiveTools, readRawQaSessionStore, readSkillStatus };
214+async function readSessionTranscriptSummary(
215+env: Pick<QaSuiteRuntimeEnv, "gateway">,
216+sessionKey: string,
217+): Promise<QaSessionTranscriptSummary> {
218+const normalizedSessionKey = sessionKey.trim();
219+if (!normalizedSessionKey) {
220+throw new Error("readSessionTranscriptSummary requires a session key");
221+}
222+const store = await readRawQaSessionStore(env);
223+const entry = store[normalizedSessionKey];
224+const sessionId = readNonEmptyString(entry?.sessionId);
225+if (!sessionId) {
226+throw new Error(`session transcript entry not found for ${normalizedSessionKey}`);
227+}
228+const sessionsDir = path.join(env.gateway.tempRoot, "state", "agents", "qa", "sessions");
229+const transcriptPath = resolveQaSessionTranscriptFile({
230+ sessionsDir,
231+ sessionId,
232+sessionFile: entry?.sessionFile,
233+});
234+const transcriptBytes = await fs.readFile(transcriptPath, "utf8");
235+if (!transcriptBytes.trim()) {
236+throw new Error(`session transcript is empty for ${normalizedSessionKey}`);
237+}
238+return {
239+finalText: extractFinalAssistantTextFromTranscript(transcriptBytes),
240+hasDirectReplySelfMessage: scanDirectReplyTranscriptSentinels(transcriptBytes).length > 0,
241+};
242+}
243+244+export {
245+createSession,
246+readEffectiveTools,
247+readRawQaSessionStore,
248+readSessionTranscriptSummary,
249+readSkillStatus,
250+};
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。