


























@@ -0,0 +1,215 @@
1+import path from "node:path";
2+import { fileURLToPath } from "node:url";
3+import { createServer, type Plugin, type ViteDevServer } from "vite";
4+import { CONTROL_UI_BOOTSTRAP_CONFIG_PATH } from "../src/gateway/control-ui-contract.js";
5+import {
6+createControlUiMockBootstrapConfig,
7+createControlUiMockGatewayInitScript,
8+type ControlUiMockGatewayScenario,
9+} from "../ui/src/test-helpers/control-ui-e2e.ts";
10+11+type CliOptions = {
12+host: string;
13+port: number;
14+};
15+16+type SessionListOptions = {
17+hasMore: boolean;
18+nextOffset: number | null;
19+offset?: number;
20+totalCount: number;
21+};
22+23+const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
24+const uiRoot = path.join(repoRoot, "ui");
25+26+function parseArgs(args: string[]): CliOptions {
27+const options: CliOptions = { host: "127.0.0.1", port: 5187 };
28+for (let i = 0; i < args.length; i += 1) {
29+const arg = args[i];
30+if (arg === "--host") {
31+options.host = args[++i] ?? options.host;
32+} else if (arg.startsWith("--host=")) {
33+options.host = arg.slice("--host=".length) || options.host;
34+} else if (arg === "--port") {
35+options.port = parsePort(args[++i], options.port);
36+} else if (arg.startsWith("--port=")) {
37+options.port = parsePort(arg.slice("--port=".length), options.port);
38+}
39+}
40+return options;
41+}
42+43+function parsePort(value: string | undefined, fallback: number): number {
44+const parsed = Number(value);
45+return Number.isInteger(parsed) && parsed > 0 && parsed < 65_536 ? parsed : fallback;
46+}
47+48+function sessionRow(key: string, label: string, updatedAt: number) {
49+return {
50+contextTokens: null,
51+displayName: label,
52+hasActiveRun: false,
53+ key,
54+kind: "direct",
55+ label,
56+model: "gpt-5.5",
57+modelProvider: "openai",
58+status: "done",
59+totalTokens: 0,
60+ updatedAt,
61+};
62+}
63+64+function sessionsListResponse(sessions: unknown[], options: SessionListOptions) {
65+return {
66+count: sessions.length,
67+defaults: {
68+contextTokens: null,
69+model: "gpt-5.5",
70+modelProvider: "openai",
71+},
72+hasMore: options.hasMore,
73+limitApplied: 50,
74+nextOffset: options.nextOffset,
75+offset: options.offset ?? 0,
76+path: "",
77+ sessions,
78+totalCount: options.totalCount,
79+ts: Date.now(),
80+};
81+}
82+83+function createChatPickerScenario(): ControlUiMockGatewayScenario {
84+const baseTime = Date.parse("2026-05-22T09:00:00.000Z");
85+return {
86+assistantAgentId: "openclaw-mock",
87+assistantName: "OpenClaw mock",
88+defaultAgentId: "openclaw-mock",
89+historyMessages: [
90+{
91+content: [
92+{
93+text: 'Mock Control UI is running. Open the chat picker, search for "telegram", then use Load more.',
94+type: "text",
95+},
96+],
97+role: "assistant",
98+timestamp: baseTime,
99+},
100+],
101+methodResponses: {
102+"sessions.list": {
103+cases: [
104+{
105+match: { offset: 50, search: "telegram" },
106+response: sessionsListResponse(
107+[
108+sessionRow("agent:telegram-51", "Telegram archive page 51", baseTime - 180_000),
109+sessionRow("agent:telegram-52", "Telegram archive page 52", baseTime - 240_000),
110+],
111+{ hasMore: false, nextOffset: null, offset: 50, totalCount: 4 },
112+),
113+},
114+{
115+match: { search: "telegram" },
116+response: sessionsListResponse(
117+[
118+sessionRow("agent:telegram", "Telegram follow-up", baseTime - 60_000),
119+sessionRow("agent:telegram-mobile", "Telegram mobile handoff", baseTime - 120_000),
120+],
121+{ hasMore: true, nextOffset: 50, totalCount: 4 },
122+),
123+},
124+{
125+match: {},
126+response: sessionsListResponse(
127+[
128+sessionRow("agent:alpha", "Alpha planning", baseTime - 1_000),
129+sessionRow("agent:design", "Design review", baseTime - 30_000),
130+],
131+{ hasMore: true, nextOffset: 50, totalCount: 125 },
132+),
133+},
134+],
135+},
136+},
137+models: [{ id: "gpt-5.5", name: "gpt-5.5", provider: "openai" }],
138+sessionKey: "agent:alpha",
139+};
140+}
141+142+function escapeScriptContent(script: string): string {
143+return script.replaceAll("</script", "<\\/script");
144+}
145+146+function createMockGatewayPlugin(scenario: ControlUiMockGatewayScenario): Plugin {
147+const initScript = escapeScriptContent(createControlUiMockGatewayInitScript(scenario));
148+const bootstrapBody = JSON.stringify(createControlUiMockBootstrapConfig(scenario));
149+return {
150+configureServer(server) {
151+server.middlewares.use(CONTROL_UI_BOOTSTRAP_CONFIG_PATH, (_req, res) => {
152+res.statusCode = 200;
153+res.setHeader("content-type", "application/json");
154+res.end(bootstrapBody);
155+});
156+},
157+name: "openclaw-control-ui-mock-gateway",
158+transformIndexHtml(html) {
159+return html.replace(
160+"</head>",
161+` <script data-openclaw-control-ui-mock-gateway>\n${initScript}\n </script>\n </head>`,
162+);
163+},
164+};
165+}
166+167+function hostForUrl(boundAddress: string, requestedHost: string): string {
168+const host = boundAddress === "0.0.0.0" || boundAddress === "::" ? requestedHost : boundAddress;
169+const reachableHost = host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host;
170+return reachableHost.includes(":") ? `[${reachableHost}]` : reachableHost;
171+}
172+173+function resolveServerUrl(server: ViteDevServer, requestedHost: string): string {
174+const address = server.httpServer?.address();
175+if (!address || typeof address === "string") {
176+throw new Error("Control UI mock server did not expose a TCP port");
177+}
178+return `http://${hostForUrl(address.address, requestedHost)}:${address.port}/chat`;
179+}
180+181+async function waitForShutdown(): Promise<void> {
182+await new Promise<void>((resolve) => {
183+process.once("SIGINT", resolve);
184+process.once("SIGTERM", resolve);
185+});
186+}
187+188+const options = parseArgs(process.argv.slice(2));
189+const scenario = createChatPickerScenario();
190+const server = await createServer({
191+base: "/",
192+cacheDir: path.join(repoRoot, ".artifacts", "control-ui-mock-vite"),
193+clearScreen: false,
194+configFile: false,
195+define: {
196+OPENCLAW_CONTROL_UI_BUILD_ID: JSON.stringify("mock"),
197+},
198+logLevel: "error",
199+optimizeDeps: {
200+include: ["lit/directives/repeat.js"],
201+},
202+plugins: [createMockGatewayPlugin(scenario)],
203+publicDir: path.join(uiRoot, "public"),
204+root: uiRoot,
205+server: {
206+host: options.host,
207+port: options.port,
208+strictPort: false,
209+},
210+});
211+212+await server.listen();
213+console.log(`[control-ui-mock] ${resolveServerUrl(server, options.host)}`);
214+await waitForShutdown();
215+await server.close();
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。