fix(tasks): return durable assistant output

This commit is contained in:
Haitao Pan 2026-06-27 12:14:01 +08:00
parent 6ec2c10924
commit d396760a4e
8 changed files with 102 additions and 4 deletions

1
dist/index.d.ts vendored
View File

@ -1,3 +1,4 @@
export declare function lastAssistantText(messages: unknown): string | undefined;
declare const plugin: {
id: string;
name: string;

32
dist/index.js vendored
View File

@ -33,6 +33,37 @@ function resolveRunScope(ctx) {
function stringParam(value) {
return typeof value === "string" ? value.trim() : "";
}
export function lastAssistantText(messages) {
if (!Array.isArray(messages))
return undefined;
for (let index = messages.length - 1; index >= 0; index -= 1) {
const message = messages[index];
if (!message || typeof message !== "object")
continue;
const record = message;
if (stringParam(record.role).toLowerCase() !== "assistant")
continue;
const content = record.content;
if (typeof content === "string" && content.trim())
return content.trim();
if (!Array.isArray(content))
continue;
const text = content
.map((block) => {
if (!block || typeof block !== "object")
return "";
const item = block;
const type = stringParam(item.type).toLowerCase();
return type === "text" || type === "output_text" ? stringParam(item.text) : "";
})
.filter(Boolean)
.join("\n")
.trim();
if (text)
return text;
}
return undefined;
}
const plugin = definePluginEntry({
id: "openclaw-multi-session-plugins",
name: "openclaw-multi-session-plugins",
@ -77,6 +108,7 @@ function register(api) {
openclawSessionKey,
runId,
success: event?.success === true,
output: lastAssistantText(event?.messages),
error: event?.error,
});
}

View File

@ -36,6 +36,7 @@ export type XWorkmateRecordedTaskRunV1 = {
startedAt: string;
updatedAt: string;
completedAt?: string;
output?: string;
error?: string;
};
export declare function registerXWorkmateSessionExtension(api: OpenClawPluginApi): void;
@ -55,6 +56,7 @@ export declare function recordXWorkmateTaskRunTerminal(input: {
openclawSessionKey: string;
runId: string;
success: boolean;
output?: unknown;
error?: unknown;
}): Promise<XWorkmateRecordedTaskRunV1>;
export declare function getXWorkmateTaskSnapshot(input: {

14
dist/src/taskState.js vendored
View File

@ -51,6 +51,7 @@ export async function recordXWorkmateTaskRunTerminal(input) {
success: input.success,
updatedAt: now,
completedAt: now,
output: sanitizeTaskRunOutput(input.output),
error: sanitizeTaskRunError(input.error),
});
}
@ -194,8 +195,10 @@ export async function getXWorkmateTaskSnapshot(input) {
completedAt: recordedRun.completedAt,
error: recordedRun.error,
},
output: recordedRun.output,
resultSummary: recordedRun.output,
error: recordedRun.error,
message: recordedRun.error,
message: recordedRun.output ?? recordedRun.error,
expectedArtifactDirs: mapping?.expectedArtifactDirs ?? [],
artifactScope: exported?.artifactScope,
remoteWorkingDirectory: exported?.remoteWorkingDirectory,
@ -295,6 +298,7 @@ async function upsertXWorkmateTaskRun(api, input) {
startedAt: existing?.startedAt ?? input.startedAt ?? input.updatedAt,
updatedAt: input.updatedAt,
completedAt: input.completedAt,
output: input.output,
error: input.error,
});
runs[input.runId] = recorded;
@ -347,11 +351,19 @@ function readTaskRunsFromEntry(entry) {
startedAt: optionalString(raw?.startedAt) || new Date(0).toISOString(),
updatedAt: optionalString(raw?.updatedAt) || new Date(0).toISOString(),
completedAt: optionalString(raw?.completedAt),
output: optionalString(raw?.output),
error: optionalString(raw?.error),
});
}
return result;
}
function sanitizeTaskRunOutput(value) {
const raw = optionalString(value);
if (!raw) {
return undefined;
}
return raw.slice(0, 16 * 1024);
}
function sanitizeTaskRunError(value) {
const raw = optionalString(value);
if (!raw) {

View File

@ -3,7 +3,7 @@ import os from "node:os";
import path from "node:path";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { describe, expect, it } from "vitest";
import plugin from "./index.js";
import plugin, { lastAssistantText } from "./index.js";
import { prepareXWorkmateArtifacts } from "./src/exportArtifacts.js";
type GatewayMethodHandler = Parameters<OpenClawPluginApi["registerGatewayMethod"]>[1];
@ -14,6 +14,12 @@ type GatewayMethodResponse = {
};
describe("plugin registration", () => {
it("extracts only the final assistant display text", () => {
expect(lastAssistantText([
{ role: "user", content: "secret prompt" },
{ role: "assistant", content: [{ type: "tool_call", text: "ignored" }, { type: "text", text: "完成并已保存。" }] },
])).toBe("完成并已保存。");
});
it("declares registered agent tools in the manifest contract", () => {
const manifest = JSON.parse(fs.readFileSync("openclaw.plugin.json", "utf8")) as {
contracts?: { tools?: string[]; sessionScopedTools?: string[] };
@ -264,7 +270,7 @@ describe("plugin registration", () => {
expect(snapshot.payload?.artifacts).toMatchObject([{ relativePath: "reports/final.md" }]);
await hooks.get("agent_end")?.(
{ runId: "turn-1", success: false, error: "401 authentication failed" },
{ runId: "turn-1", success: false, error: "401 authentication failed", messages: [{ role: "assistant", content: [{ type: "text", text: "上游认证失败。" }] }] },
{ sessionKey: "draft:1780636411666238-3", runId: "turn-1" },
);
expect(sessionExtensionPatches.at(-1)).toMatchObject({

View File

@ -86,6 +86,31 @@ function stringParam(value: unknown): string {
return typeof value === "string" ? value.trim() : "";
}
export function lastAssistantText(messages: unknown): string | undefined {
if (!Array.isArray(messages)) return undefined;
for (let index = messages.length - 1; index >= 0; index -= 1) {
const message = messages[index];
if (!message || typeof message !== "object") continue;
const record = message as Record<string, unknown>;
if (stringParam(record.role).toLowerCase() !== "assistant") continue;
const content = record.content;
if (typeof content === "string" && content.trim()) return content.trim();
if (!Array.isArray(content)) continue;
const text = content
.map((block) => {
if (!block || typeof block !== "object") return "";
const item = block as Record<string, unknown>;
const type = stringParam(item.type).toLowerCase();
return type === "text" || type === "output_text" ? stringParam(item.text) : "";
})
.filter(Boolean)
.join("\n")
.trim();
if (text) return text;
}
return undefined;
}
const plugin = definePluginEntry({
id: "openclaw-multi-session-plugins",
name: "openclaw-multi-session-plugins",
@ -139,6 +164,7 @@ function register(api: OpenClawPluginApi) {
openclawSessionKey,
runId,
success: event?.success === true,
output: lastAssistantText(event?.messages),
error: event?.error,
});
} catch (error) {

View File

@ -275,6 +275,7 @@ describe("xworkmate task state mapping", () => {
openclawSessionKey: "agent:main:draft:failed-run",
runId: "turn-failed",
success: false,
output: "任务执行失败前的说明",
error: "401 Authentication Fails, api_key=sk-secret-value",
});
@ -292,6 +293,9 @@ describe("xworkmate task state mapping", () => {
taskStatus: "failed",
terminal: true,
terminalSource: "agent_end",
output: "任务执行失败前的说明",
resultSummary: "任务执行失败前的说明",
message: "任务执行失败前的说明",
task: {
runId: "turn-failed",
status: "failed",

View File

@ -54,6 +54,7 @@ export type XWorkmateRecordedTaskRunV1 = {
startedAt: string;
updatedAt: string;
completedAt?: string;
output?: string;
error?: string;
};
@ -136,6 +137,7 @@ export async function recordXWorkmateTaskRunTerminal(input: {
openclawSessionKey: string;
runId: string;
success: boolean;
output?: unknown;
error?: unknown;
}): Promise<XWorkmateRecordedTaskRunV1> {
const now = new Date().toISOString();
@ -146,6 +148,7 @@ export async function recordXWorkmateTaskRunTerminal(input: {
success: input.success,
updatedAt: now,
completedAt: now,
output: sanitizeTaskRunOutput(input.output),
error: sanitizeTaskRunError(input.error),
});
}
@ -311,8 +314,10 @@ export async function getXWorkmateTaskSnapshot(input: {
completedAt: recordedRun.completedAt,
error: recordedRun.error,
},
output: recordedRun.output,
resultSummary: recordedRun.output,
error: recordedRun.error,
message: recordedRun.error,
message: recordedRun.output ?? recordedRun.error,
expectedArtifactDirs: mapping?.expectedArtifactDirs ?? [],
artifactScope: exported?.artifactScope,
remoteWorkingDirectory: exported?.remoteWorkingDirectory,
@ -427,6 +432,7 @@ async function upsertXWorkmateTaskRun(
startedAt: existing?.startedAt ?? input.startedAt ?? input.updatedAt,
updatedAt: input.updatedAt,
completedAt: input.completedAt,
output: input.output,
error: input.error,
}) as XWorkmateRecordedTaskRunV1;
runs[input.runId] = recorded;
@ -487,12 +493,21 @@ function readTaskRunsFromEntry(entry: SessionEntry | undefined | null): Record<s
startedAt: optionalString(raw?.startedAt) || new Date(0).toISOString(),
updatedAt: optionalString(raw?.updatedAt) || new Date(0).toISOString(),
completedAt: optionalString(raw?.completedAt),
output: optionalString(raw?.output),
error: optionalString(raw?.error),
}) as XWorkmateRecordedTaskRunV1;
}
return result;
}
function sanitizeTaskRunOutput(value: unknown): string | undefined {
const raw = optionalString(value);
if (!raw) {
return undefined;
}
return raw.slice(0, 16 * 1024);
}
function sanitizeTaskRunError(value: unknown): string | undefined {
const raw = optionalString(value);
if (!raw) {