mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-11 08:10:48 +08:00
重构主控编排与运行时预热链路,统一工作区提示词/专家调度协议并补齐 wiki 记忆注入与写回闭环。
同时收敛启动与运维脚本默认行为(含 wiki worker)、更新 Admin 可观测性与相关测试,降低首轮时延并提高运行稳定性。 Made-with: Cursor
This commit is contained in:
parent
4a23b715a2
commit
dbbe3add6a
14438 changed files with 2693620 additions and 2546 deletions
247
openclaw/scripts/e2e/mcp-channels-docker-client.ts
Normal file
247
openclaw/scripts/e2e/mcp-channels-docker-client.ts
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
import { randomUUID } from "node:crypto";
|
||||
import {
|
||||
assert,
|
||||
ClaudeChannelNotificationSchema,
|
||||
ClaudePermissionNotificationSchema,
|
||||
connectGateway,
|
||||
connectMcpClient,
|
||||
extractTextFromGatewayPayload,
|
||||
type ClaudeChannelNotification,
|
||||
maybeApprovePendingBridgePairing,
|
||||
waitFor,
|
||||
} from "./mcp-channels-harness.ts";
|
||||
|
||||
async function main() {
|
||||
const gatewayUrl = process.env.GW_URL?.trim();
|
||||
const gatewayToken = process.env.GW_TOKEN?.trim();
|
||||
assert(gatewayUrl, "missing GW_URL");
|
||||
assert(gatewayToken, "missing GW_TOKEN");
|
||||
|
||||
const gateway = await connectGateway({ url: gatewayUrl, token: gatewayToken });
|
||||
let mcpHandle = await connectMcpClient({
|
||||
gatewayUrl,
|
||||
gatewayToken,
|
||||
});
|
||||
let mcp = mcpHandle.client;
|
||||
|
||||
try {
|
||||
if (await maybeApprovePendingBridgePairing(gateway)) {
|
||||
await Promise.allSettled([mcp.close(), mcpHandle.transport.close()]);
|
||||
mcpHandle = await connectMcpClient({
|
||||
gatewayUrl,
|
||||
gatewayToken,
|
||||
});
|
||||
mcp = mcpHandle.client;
|
||||
}
|
||||
|
||||
const listed = (await mcp.callTool({
|
||||
name: "conversations_list",
|
||||
arguments: {},
|
||||
})) as {
|
||||
structuredContent?: { conversations?: Array<Record<string, unknown>> };
|
||||
};
|
||||
const conversation = listed.structuredContent?.conversations?.find(
|
||||
(entry) => entry.sessionKey === "agent:main:main",
|
||||
);
|
||||
assert(conversation, "expected seeded conversation in conversations_list");
|
||||
assert(conversation.channel === "imessage", "expected seeded channel");
|
||||
assert(conversation.to === "+15551234567", "expected seeded target");
|
||||
|
||||
const fetched = (await mcp.callTool({
|
||||
name: "conversation_get",
|
||||
arguments: { session_key: "agent:main:main" },
|
||||
})) as {
|
||||
structuredContent?: { conversation?: Record<string, unknown> };
|
||||
isError?: boolean;
|
||||
};
|
||||
assert(!fetched.isError, "conversation_get should succeed");
|
||||
assert(
|
||||
fetched.structuredContent?.conversation?.sessionKey === "agent:main:main",
|
||||
"conversation_get returned wrong session",
|
||||
);
|
||||
|
||||
const history = (await mcp.callTool({
|
||||
name: "messages_read",
|
||||
arguments: { session_key: "agent:main:main", limit: 10 },
|
||||
})) as {
|
||||
structuredContent?: { messages?: Array<Record<string, unknown>> };
|
||||
};
|
||||
const messages = history.structuredContent?.messages ?? [];
|
||||
assert(messages.length >= 2, "expected seeded transcript messages");
|
||||
const attachmentMessage = messages.find((entry) => {
|
||||
const raw = entry.__openclaw;
|
||||
return raw && typeof raw === "object" && (raw as { id?: unknown }).id === "msg-attachment";
|
||||
});
|
||||
assert(attachmentMessage, "expected seeded attachment message");
|
||||
|
||||
const attachments = (await mcp.callTool({
|
||||
name: "attachments_fetch",
|
||||
arguments: { session_key: "agent:main:main", message_id: "msg-attachment" },
|
||||
})) as {
|
||||
structuredContent?: { attachments?: Array<Record<string, unknown>> };
|
||||
isError?: boolean;
|
||||
};
|
||||
assert(!attachments.isError, "attachments_fetch should succeed");
|
||||
assert(
|
||||
(attachments.structuredContent?.attachments?.length ?? 0) === 1,
|
||||
"expected one seeded attachment",
|
||||
);
|
||||
|
||||
const waited = (await Promise.all([
|
||||
mcp.callTool({
|
||||
name: "events_wait",
|
||||
arguments: {
|
||||
session_key: "agent:main:main",
|
||||
after_cursor: 0,
|
||||
timeout_ms: 10_000,
|
||||
},
|
||||
}) as Promise<{
|
||||
structuredContent?: { event?: Record<string, unknown> };
|
||||
}>,
|
||||
gateway.request("chat.inject", {
|
||||
sessionKey: "agent:main:main",
|
||||
message: "assistant live event",
|
||||
}),
|
||||
]).then(([result]) => result)) as {
|
||||
structuredContent?: { event?: Record<string, unknown> };
|
||||
};
|
||||
const assistantEvent = waited.structuredContent?.event;
|
||||
assert(assistantEvent, "expected events_wait result");
|
||||
assert(assistantEvent.type === "message", "expected message event");
|
||||
assert(assistantEvent.role === "assistant", "expected assistant event role");
|
||||
assert(assistantEvent.text === "assistant live event", "expected assistant event text");
|
||||
const assistantCursor = typeof assistantEvent.cursor === "number" ? assistantEvent.cursor : 0;
|
||||
|
||||
const polled = (await mcp.callTool({
|
||||
name: "events_poll",
|
||||
arguments: { session_key: "agent:main:main", after_cursor: 0, limit: 10 },
|
||||
})) as {
|
||||
structuredContent?: { events?: Array<Record<string, unknown>> };
|
||||
};
|
||||
assert(
|
||||
(polled.structuredContent?.events ?? []).some(
|
||||
(entry) => entry.text === "assistant live event",
|
||||
),
|
||||
"expected assistant event in events_poll",
|
||||
);
|
||||
|
||||
const channelMessage = `hello from docker ${randomUUID()}`;
|
||||
const userEvent = (await Promise.all([
|
||||
mcp.callTool({
|
||||
name: "events_wait",
|
||||
arguments: {
|
||||
session_key: "agent:main:main",
|
||||
after_cursor: assistantCursor,
|
||||
timeout_ms: 10_000,
|
||||
},
|
||||
}) as Promise<{
|
||||
structuredContent?: { event?: Record<string, unknown> };
|
||||
}>,
|
||||
gateway.request("chat.send", {
|
||||
sessionKey: "agent:main:main",
|
||||
message: channelMessage,
|
||||
idempotencyKey: randomUUID(),
|
||||
}),
|
||||
]).then(([result]) => result)) as {
|
||||
structuredContent?: { event?: Record<string, unknown> };
|
||||
};
|
||||
const rawGatewayUserMessage = await waitFor("raw gateway user session.message", () =>
|
||||
gateway.events.find(
|
||||
(entry) =>
|
||||
entry.event === "session.message" &&
|
||||
entry.payload.sessionKey === "agent:main:main" &&
|
||||
extractTextFromGatewayPayload(entry.payload) === channelMessage,
|
||||
),
|
||||
);
|
||||
if (userEvent.structuredContent?.event?.text !== channelMessage) {
|
||||
throw new Error(
|
||||
`expected user event after chat.send: ${JSON.stringify(
|
||||
{
|
||||
userEvent: userEvent.structuredContent?.event ?? null,
|
||||
rawGatewayUserMessage: rawGatewayUserMessage ?? null,
|
||||
recentGatewayEvents: gateway.events.slice(-10).map((entry) => ({
|
||||
event: entry.event,
|
||||
sessionKey: entry.payload.sessionKey,
|
||||
text: extractTextFromGatewayPayload(entry.payload),
|
||||
})),
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
assert(rawGatewayUserMessage, "expected raw gateway session.message after chat.send");
|
||||
|
||||
let helpNotification: ClaudeChannelNotification;
|
||||
try {
|
||||
helpNotification = await waitFor(
|
||||
"Claude channel notification",
|
||||
() =>
|
||||
mcpHandle.rawMessages
|
||||
.map((entry) => ClaudeChannelNotificationSchema.safeParse(entry))
|
||||
.find(
|
||||
(entry) =>
|
||||
entry.success &&
|
||||
entry.data.params.meta.session_key === "agent:main:main" &&
|
||||
entry.data.params.content === channelMessage,
|
||||
)?.data.params,
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`timeout waiting for Claude channel notification: ${JSON.stringify(
|
||||
{
|
||||
rawMessages: mcpHandle.rawMessages.slice(-10),
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}`,
|
||||
{ cause: error },
|
||||
);
|
||||
}
|
||||
assert(helpNotification.content === channelMessage, "expected Claude channel content");
|
||||
|
||||
await mcp.notification({
|
||||
method: "notifications/claude/channel/permission_request",
|
||||
params: {
|
||||
request_id: "abcde",
|
||||
tool_name: "Bash",
|
||||
description: "run npm test",
|
||||
input_preview: '{"cmd":"npm test"}',
|
||||
},
|
||||
});
|
||||
|
||||
await gateway.request("chat.send", {
|
||||
sessionKey: "agent:main:main",
|
||||
message: "yes abcde",
|
||||
idempotencyKey: randomUUID(),
|
||||
});
|
||||
const permission = await waitFor(
|
||||
"Claude permission notification",
|
||||
() =>
|
||||
mcpHandle.rawMessages
|
||||
.map((entry) => ClaudePermissionNotificationSchema.safeParse(entry))
|
||||
.find((entry) => entry.success && entry.data.params.request_id === "abcde")?.data.params,
|
||||
);
|
||||
assert(permission.behavior === "allow", "expected allow permission reply");
|
||||
|
||||
process.stdout.write(
|
||||
JSON.stringify(
|
||||
{
|
||||
ok: true,
|
||||
sessionKey: "agent:main:main",
|
||||
rawNotifications: mcpHandle.rawMessages.filter(
|
||||
(entry) =>
|
||||
ClaudeChannelNotificationSchema.safeParse(entry).success ||
|
||||
ClaudePermissionNotificationSchema.safeParse(entry).success,
|
||||
).length,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
) + "\n",
|
||||
);
|
||||
} finally {
|
||||
await Promise.allSettled([mcp.close(), mcpHandle.transport.close(), gateway.close()]);
|
||||
}
|
||||
}
|
||||
|
||||
await main();
|
||||
Loading…
Add table
Add a link
Reference in a new issue