@coze-arch/cli 0.0.13 → 0.0.14-alpha.c52ee4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/__templates__/expo/AGENTS.md +15 -7
- package/lib/__templates__/expo/README.md +15 -7
- package/lib/__templates__/expo/client/eslint.config.mjs +3 -0
- package/lib/__templates__/expo/eslint-plugins/expo/index.js +9 -0
- package/lib/__templates__/expo/eslint-plugins/expo/rule.js +105 -0
- package/lib/__templates__/expo/eslint-plugins/expo/tech.md +108 -0
- package/lib/__templates__/nextjs/AGENTS.md +9 -0
- package/lib/__templates__/nextjs/eslint.config.mjs +15 -0
- package/lib/__templates__/pi-agent/.coze +10 -0
- package/lib/__templates__/pi-agent/AGENTS.md +150 -0
- package/lib/__templates__/pi-agent/README.md +155 -0
- package/lib/__templates__/pi-agent/_gitignore +3 -0
- package/lib/__templates__/pi-agent/docs/project-overview.md +273 -0
- package/lib/__templates__/pi-agent/docs/user/getting-started.md +46 -0
- package/lib/__templates__/pi-agent/package.json +52 -0
- package/lib/__templates__/pi-agent/pnpm-lock.yaml +7840 -0
- package/lib/__templates__/pi-agent/scripts/dev.sh +14 -0
- package/lib/__templates__/pi-agent/scripts/prepare.sh +2 -0
- package/lib/__templates__/pi-agent/src/agent.ts +367 -0
- package/lib/__templates__/pi-agent/src/channels/feishu/index.ts +760 -0
- package/lib/__templates__/pi-agent/src/channels/feishu/streaming-card.ts +297 -0
- package/lib/__templates__/pi-agent/src/channels/wechat/index.ts +171 -0
- package/lib/__templates__/pi-agent/src/config.ts +596 -0
- package/lib/__templates__/pi-agent/src/core.ts +218 -0
- package/lib/__templates__/pi-agent/src/dashboard/api/channels.ts +148 -0
- package/lib/__templates__/pi-agent/src/dashboard/api/docs.ts +204 -0
- package/lib/__templates__/pi-agent/src/dashboard/api/models.ts +141 -0
- package/lib/__templates__/pi-agent/src/dashboard/api/overview.ts +33 -0
- package/lib/__templates__/pi-agent/src/dashboard/config-store.ts +64 -0
- package/lib/__templates__/pi-agent/src/dashboard/index.ts +39 -0
- package/lib/__templates__/pi-agent/src/dashboard/server.ts +622 -0
- package/lib/__templates__/pi-agent/src/dashboard/types.ts +25 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/index.html +13 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/postcss.config.cjs +7 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/app-layout.tsx +186 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/page-title.tsx +17 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/alert.tsx +22 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/badge.tsx +25 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/button.tsx +40 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/card.tsx +29 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/input.tsx +18 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/label.tsx +8 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/select.tsx +80 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/separator.tsx +23 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/hooks/use-fetch.ts +32 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/hooks/use-local-storage-state.ts +23 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/main.tsx +30 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/channels-page.tsx +188 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/chat-page.tsx +451 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/docs-page.tsx +65 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/models-page.tsx +122 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/overview-page.tsx +134 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/services/chat-ws-service.ts +167 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/styles.css +294 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/utils/index.ts +11 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/tsconfig.json +13 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/vite.config.ts +17 -0
- package/lib/__templates__/pi-agent/src/index.ts +123 -0
- package/lib/__templates__/pi-agent/src/session-store.ts +223 -0
- package/lib/__templates__/pi-agent/template.config.js +45 -0
- package/lib/__templates__/pi-agent/tests/config.test.ts +292 -0
- package/lib/__templates__/pi-agent/tests/dashboard-docs-api.test.ts +125 -0
- package/lib/__templates__/pi-agent/tests/dashboard-models-api.test.ts +171 -0
- package/lib/__templates__/pi-agent/tests/feishu-channel.test.ts +149 -0
- package/lib/__templates__/pi-agent/tests/feishu-streaming-card.test.ts +15 -0
- package/lib/__templates__/pi-agent/tests/session-store.test.ts +61 -0
- package/lib/__templates__/pi-agent/tests/smoke/run-smoke.ts +275 -0
- package/lib/__templates__/pi-agent/tsconfig.json +20 -0
- package/lib/__templates__/pi-agent/types/larksuiteoapi-node-sdk.d.ts +113 -0
- package/lib/__templates__/taro/pnpm-lock.yaml +24 -14
- package/lib/__templates__/taro/server/package.json +0 -2
- package/lib/__templates__/taro/src/presets/dev-debug.ts +2 -2
- package/lib/__templates__/templates.json +24 -0
- package/lib/__templates__/vite/AGENTS.md +5 -0
- package/lib/cli.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { createServer as createHttpServer, type IncomingMessage, type Server as HttpServer } from "node:http";
|
|
5
|
+
import express from "express";
|
|
6
|
+
import { createServer as createViteServer, type ViteDevServer } from "vite";
|
|
7
|
+
import { WebSocketServer, type RawData, type WebSocket } from "ws";
|
|
8
|
+
import type { DashboardServer, DashboardServerOptions } from "./types.js";
|
|
9
|
+
import { readDocsResponse } from "./api/docs.js";
|
|
10
|
+
import { buildOverviewResponse } from "./api/overview.js";
|
|
11
|
+
import { readChannelsResponse, saveChannelsRequest } from "./api/channels.js";
|
|
12
|
+
import { ValidationError, readModelsResponse, saveModelsRequest } from "./api/models.js";
|
|
13
|
+
import type { ChannelName } from "../core.js";
|
|
14
|
+
import { createBotMessage, getSessionKey } from "../core.js";
|
|
15
|
+
|
|
16
|
+
const DEFAULT_HOST = "127.0.0.1";
|
|
17
|
+
const DEFAULT_PORT = <%= port %>;
|
|
18
|
+
|
|
19
|
+
function readPort(explicitPort: number | undefined): number {
|
|
20
|
+
if (explicitPort !== undefined) {
|
|
21
|
+
return explicitPort;
|
|
22
|
+
}
|
|
23
|
+
const rawPort = process.env.PI_BOT_DASHBOARD_PORT;
|
|
24
|
+
if (!rawPort) return DEFAULT_PORT;
|
|
25
|
+
const parsed = Number.parseInt(rawPort, 10);
|
|
26
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
27
|
+
throw new Error(`Invalid PI_BOT_DASHBOARD_PORT value: ${rawPort}`);
|
|
28
|
+
}
|
|
29
|
+
return parsed;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readHost(explicitHost: string | undefined): string {
|
|
33
|
+
return explicitHost ?? process.env.PI_BOT_DASHBOARD_HOST ?? DEFAULT_HOST;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const DASHBOARD_DIR = dirname(fileURLToPath(import.meta.url));
|
|
37
|
+
const PROJECT_ROOT = resolve(DASHBOARD_DIR, "..", "..");
|
|
38
|
+
const USER_DOCS_DIR = resolve(PROJECT_ROOT, "docs", "user");
|
|
39
|
+
const WEB_ROOT = resolve(DASHBOARD_DIR, "web");
|
|
40
|
+
const WEB_DIST = resolve(WEB_ROOT, "dist");
|
|
41
|
+
|
|
42
|
+
type ChatSessionIdentity = {
|
|
43
|
+
channel?: ChannelName;
|
|
44
|
+
isDirectMessage?: boolean;
|
|
45
|
+
senderId?: string;
|
|
46
|
+
conversationId?: string;
|
|
47
|
+
threadId?: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
type WsClientFrame =
|
|
51
|
+
| { type: "chat.send"; runId: string; session?: ChatSessionIdentity; sessionKey?: string; text?: string }
|
|
52
|
+
| { type: "chat.abort"; runId?: string; session?: ChatSessionIdentity; sessionKey?: string }
|
|
53
|
+
| { type: "ping" };
|
|
54
|
+
|
|
55
|
+
type WsServerFrame =
|
|
56
|
+
| { type: "ack"; runId: string; status: "started" | "in_flight" }
|
|
57
|
+
| { type: "meta"; runId: string; sessionKey: string }
|
|
58
|
+
| { type: "delta"; runId: string; sessionKey: string; delta: string }
|
|
59
|
+
| { type: "done"; runId: string; sessionKey: string; text: string }
|
|
60
|
+
| { type: "error"; runId: string; sessionKey?: string; error: string };
|
|
61
|
+
|
|
62
|
+
function writeSse(res: express.Response, event: string, data: unknown) {
|
|
63
|
+
res.write(`event: ${event}\n`);
|
|
64
|
+
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseBoolQuery(value: unknown, defaultValue: boolean): boolean {
|
|
68
|
+
if (value === undefined) return defaultValue;
|
|
69
|
+
if (typeof value === "string") {
|
|
70
|
+
const v = value.trim().toLowerCase();
|
|
71
|
+
if (v === "true" || v === "1" || v === "yes") return true;
|
|
72
|
+
if (v === "false" || v === "0" || v === "no") return false;
|
|
73
|
+
}
|
|
74
|
+
return Boolean(value);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function parseSessionKey(sessionKey: string): ChatSessionIdentity {
|
|
78
|
+
const raw = sessionKey.trim();
|
|
79
|
+
const parts = raw.split(":");
|
|
80
|
+
const channel = parts[0] as ChannelName | undefined;
|
|
81
|
+
const kind = parts[1];
|
|
82
|
+
|
|
83
|
+
if (!channel || (channel !== "dashboard" && channel !== "feishu" && channel !== "wechat")) {
|
|
84
|
+
throw new Error(`Invalid sessionKey (channel): ${sessionKey}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (kind === "dm") {
|
|
88
|
+
const senderId = parts.slice(2).join(":");
|
|
89
|
+
if (!senderId) throw new Error(`Invalid sessionKey (dm senderId): ${sessionKey}`);
|
|
90
|
+
return { channel, isDirectMessage: true, senderId, conversationId: "dashboard" };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (kind === "group") {
|
|
94
|
+
const conversationId = parts.slice(2).join(":");
|
|
95
|
+
if (!conversationId) throw new Error(`Invalid sessionKey (group conversationId): ${sessionKey}`);
|
|
96
|
+
return { channel, isDirectMessage: false, senderId: "unknown", conversationId };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (kind === "thread") {
|
|
100
|
+
// format: channel:thread:<conversationId>:<threadId>
|
|
101
|
+
if (parts.length < 4) throw new Error(`Invalid sessionKey (thread): ${sessionKey}`);
|
|
102
|
+
const conversationId = parts[2] ?? "";
|
|
103
|
+
const threadId = parts.slice(3).join(":");
|
|
104
|
+
if (!conversationId || !threadId) throw new Error(`Invalid sessionKey (thread ids): ${sessionKey}`);
|
|
105
|
+
return { channel, isDirectMessage: false, senderId: "unknown", conversationId, threadId };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
throw new Error(`Invalid sessionKey (kind): ${sessionKey}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function resolveSessionIdentity(req: express.Request): { identity: ChatSessionIdentity; sessionKey: string } {
|
|
112
|
+
const sessionKeyRaw = typeof req.query.sessionKey === "string" ? req.query.sessionKey : undefined;
|
|
113
|
+
if (sessionKeyRaw && sessionKeyRaw.trim()) {
|
|
114
|
+
const identity = parseSessionKey(sessionKeyRaw);
|
|
115
|
+
// Prefer the provided sessionKey verbatim for lookups; it matches getSessionKey() format.
|
|
116
|
+
return { identity, sessionKey: sessionKeyRaw.trim() };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const channel = (typeof req.query.channel === "string" ? req.query.channel : "dashboard") as ChannelName;
|
|
120
|
+
const isDirectMessage = parseBoolQuery(req.query.isDirectMessage, true);
|
|
121
|
+
const senderId = typeof req.query.senderId === "string" ? req.query.senderId : "dashboard-user";
|
|
122
|
+
const conversationId = typeof req.query.conversationId === "string" ? req.query.conversationId : "dashboard";
|
|
123
|
+
const threadId = typeof req.query.threadId === "string" ? req.query.threadId : undefined;
|
|
124
|
+
|
|
125
|
+
const identity: ChatSessionIdentity = {
|
|
126
|
+
channel,
|
|
127
|
+
isDirectMessage,
|
|
128
|
+
senderId,
|
|
129
|
+
conversationId,
|
|
130
|
+
threadId,
|
|
131
|
+
};
|
|
132
|
+
const keyMsg = createBotMessage({
|
|
133
|
+
channel: identity.channel ?? "dashboard",
|
|
134
|
+
isDirectMessage: Boolean(identity.isDirectMessage ?? true),
|
|
135
|
+
senderId: identity.senderId ?? "dashboard-user",
|
|
136
|
+
conversationId: identity.conversationId ?? "dashboard",
|
|
137
|
+
threadId: identity.threadId,
|
|
138
|
+
text: "",
|
|
139
|
+
mentions: [],
|
|
140
|
+
});
|
|
141
|
+
const sessionKey = getSessionKey(keyMsg);
|
|
142
|
+
return { identity, sessionKey };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function extractTextFromAgentMessage(message: unknown): string {
|
|
146
|
+
if (!message || typeof message !== "object") return "";
|
|
147
|
+
const content = (message as { content?: unknown }).content;
|
|
148
|
+
if (typeof content === "string") return content;
|
|
149
|
+
if (!Array.isArray(content)) return "";
|
|
150
|
+
return content
|
|
151
|
+
.flatMap((part) => {
|
|
152
|
+
if (!part || typeof part !== "object") return [];
|
|
153
|
+
const typedPart = part as { type?: unknown; text?: unknown };
|
|
154
|
+
return typedPart.type === "text" && typeof typedPart.text === "string" ? [typedPart.text] : [];
|
|
155
|
+
})
|
|
156
|
+
.join("");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function createDashboardServer(options: DashboardServerOptions): DashboardServer {
|
|
160
|
+
const host = readHost(options.host);
|
|
161
|
+
const port = readPort(options.port);
|
|
162
|
+
const url = `http://localhost:${port}`;
|
|
163
|
+
const isProd = process.env.NODE_ENV === "production";
|
|
164
|
+
|
|
165
|
+
let started = false;
|
|
166
|
+
let httpServer: HttpServer | null = null;
|
|
167
|
+
let vite: ViteDevServer | null = null;
|
|
168
|
+
let wss: WebSocketServer | null = null;
|
|
169
|
+
|
|
170
|
+
async function start() {
|
|
171
|
+
if (started) return;
|
|
172
|
+
|
|
173
|
+
const app = express();
|
|
174
|
+
app.disable("x-powered-by");
|
|
175
|
+
app.use(express.json({ limit: "1mb" }));
|
|
176
|
+
|
|
177
|
+
app.get("/api/overview", (_req, res) => {
|
|
178
|
+
res.json(
|
|
179
|
+
buildOverviewResponse({
|
|
180
|
+
dashboardUrl: url,
|
|
181
|
+
dashboardStarted: started,
|
|
182
|
+
appName: options.runtime.appName,
|
|
183
|
+
agentMode: options.runtime.agentMode,
|
|
184
|
+
workspaceDir: options.runtime.workspaceDir,
|
|
185
|
+
agentDir: options.runtime.agentDir,
|
|
186
|
+
enabledChannels: options.runtime.enabledChannels,
|
|
187
|
+
}),
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
app.get("/api/channels", (_req, res) => {
|
|
192
|
+
try {
|
|
193
|
+
res.json(readChannelsResponse({ configStore: options.configStore }));
|
|
194
|
+
} catch (err) {
|
|
195
|
+
res.status(500).json({ ok: false, error: String(err) });
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
app.post("/api/channels", (req, res) => {
|
|
200
|
+
try {
|
|
201
|
+
saveChannelsRequest({
|
|
202
|
+
configStore: options.configStore,
|
|
203
|
+
body: req.body as Parameters<typeof saveChannelsRequest>[0]["body"],
|
|
204
|
+
});
|
|
205
|
+
res.json({ ok: true });
|
|
206
|
+
} catch (err) {
|
|
207
|
+
res.status(500).json({ ok: false, error: String(err) });
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
app.get("/api/models", (_req, res) => {
|
|
212
|
+
try {
|
|
213
|
+
res.json(readModelsResponse({ configStore: options.configStore }));
|
|
214
|
+
} catch (err) {
|
|
215
|
+
res.status(500).json({ ok: false, error: String(err) });
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
app.post("/api/models", (req, res) => {
|
|
220
|
+
try {
|
|
221
|
+
saveModelsRequest({
|
|
222
|
+
configStore: options.configStore,
|
|
223
|
+
body: req.body as Parameters<typeof saveModelsRequest>[0]["body"],
|
|
224
|
+
});
|
|
225
|
+
res.json({ ok: true });
|
|
226
|
+
} catch (err) {
|
|
227
|
+
const status = err instanceof ValidationError ? err.statusCode : 500;
|
|
228
|
+
res.status(status).json({ ok: false, error: String(err) });
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
app.get("/api/docs", (req, res) => {
|
|
233
|
+
try {
|
|
234
|
+
const slug = typeof req.query.slug === "string" ? req.query.slug : undefined;
|
|
235
|
+
res.json(readDocsResponse({ docsDir: USER_DOCS_DIR, slug }));
|
|
236
|
+
} catch (err) {
|
|
237
|
+
res.status(500).json({ ok: false, error: String(err) });
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
app.get("/api/chat/sessions", (_req, res) => {
|
|
242
|
+
try {
|
|
243
|
+
const sessions = options.agentRuntime.listSessionKeys();
|
|
244
|
+
res.json({ ok: true, sessions });
|
|
245
|
+
} catch (err) {
|
|
246
|
+
res.status(500).json({ ok: false, error: String(err) });
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
app.get("/api/chat/history", async (req, res) => {
|
|
251
|
+
try {
|
|
252
|
+
const { sessionKey } = resolveSessionIdentity(req);
|
|
253
|
+
const session = await options.agentRuntime.ensureSessionLoaded(sessionKey);
|
|
254
|
+
if (!session) {
|
|
255
|
+
res.json({ ok: true, sessionKey, messages: [] });
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const messages = session.state.messages
|
|
260
|
+
.filter((m) => {
|
|
261
|
+
const role = (m as { role?: unknown }).role;
|
|
262
|
+
return role === "user" || role === "assistant";
|
|
263
|
+
})
|
|
264
|
+
.map((m) => {
|
|
265
|
+
const role = (m as { role?: unknown }).role as "user" | "assistant";
|
|
266
|
+
const text = extractTextFromAgentMessage(m);
|
|
267
|
+
const timestamp = typeof (m as { timestamp?: unknown }).timestamp === "number"
|
|
268
|
+
? ((m as { timestamp?: number }).timestamp as number)
|
|
269
|
+
: undefined;
|
|
270
|
+
return { role, text, timestamp };
|
|
271
|
+
})
|
|
272
|
+
.filter((m) => m.text.trim().length > 0);
|
|
273
|
+
|
|
274
|
+
res.json({ ok: true, sessionKey, messages });
|
|
275
|
+
} catch (err) {
|
|
276
|
+
res.status(500).json({ ok: false, error: String(err) });
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
app.post("/api/chat/reset", async (req, res) => {
|
|
282
|
+
try {
|
|
283
|
+
const body = (req.body ?? {}) as { session?: ChatSessionIdentity; sessionKey?: string };
|
|
284
|
+
const explicitKey = typeof body.sessionKey === "string" ? body.sessionKey.trim() : "";
|
|
285
|
+
const sessionKey =
|
|
286
|
+
explicitKey
|
|
287
|
+
? explicitKey
|
|
288
|
+
: getSessionKey(
|
|
289
|
+
createBotMessage({
|
|
290
|
+
channel: body.session?.channel ?? "dashboard",
|
|
291
|
+
isDirectMessage: Boolean(body.session?.isDirectMessage ?? true),
|
|
292
|
+
senderId: body.session?.senderId ?? "dashboard-user",
|
|
293
|
+
conversationId: body.session?.conversationId ?? "dashboard",
|
|
294
|
+
threadId: body.session?.threadId,
|
|
295
|
+
text: "",
|
|
296
|
+
mentions: [],
|
|
297
|
+
}),
|
|
298
|
+
);
|
|
299
|
+
await options.agentRuntime.resetSession(sessionKey);
|
|
300
|
+
res.json({ ok: true, sessionKey });
|
|
301
|
+
} catch (err) {
|
|
302
|
+
res.status(500).json({ ok: false, error: String(err) });
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
app.post("/api/chat/stream", async (req, res) => {
|
|
307
|
+
const body = (req.body ?? {}) as { session?: ChatSessionIdentity; text?: string };
|
|
308
|
+
const sessionIdentity = body.session ?? {};
|
|
309
|
+
const text = typeof body.text === "string" ? body.text : "";
|
|
310
|
+
|
|
311
|
+
const msg = createBotMessage({
|
|
312
|
+
channel: sessionIdentity.channel ?? "dashboard",
|
|
313
|
+
isDirectMessage: Boolean(sessionIdentity.isDirectMessage ?? true),
|
|
314
|
+
senderId: sessionIdentity.senderId ?? "dashboard-user",
|
|
315
|
+
conversationId: sessionIdentity.conversationId ?? "dashboard",
|
|
316
|
+
threadId: sessionIdentity.threadId,
|
|
317
|
+
text,
|
|
318
|
+
mentions: [],
|
|
319
|
+
});
|
|
320
|
+
const sessionKey = getSessionKey(msg);
|
|
321
|
+
|
|
322
|
+
// Enforce one in-flight stream per session.
|
|
323
|
+
const existing = options.agentRuntime.getSessionIfExists(sessionKey);
|
|
324
|
+
if (existing?.isStreaming) {
|
|
325
|
+
res.status(409).json({ ok: false, error: `Session is busy: ${sessionKey}` });
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
res.status(200);
|
|
330
|
+
res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
|
|
331
|
+
res.setHeader("Cache-Control", "no-cache, no-transform");
|
|
332
|
+
res.setHeader("Connection", "keep-alive");
|
|
333
|
+
(res as unknown as { flushHeaders?: () => void }).flushHeaders?.();
|
|
334
|
+
|
|
335
|
+
// Best-effort keepalive so browsers/proxies keep the connection open.
|
|
336
|
+
const keepalive = setInterval(() => {
|
|
337
|
+
res.write(":keepalive\n\n");
|
|
338
|
+
}, 15000);
|
|
339
|
+
|
|
340
|
+
let closed = false;
|
|
341
|
+
req.on("close", () => {
|
|
342
|
+
closed = true;
|
|
343
|
+
clearInterval(keepalive);
|
|
344
|
+
void options.agentRuntime.abortSession(sessionKey);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
await options.agentRuntime.stream(msg, {
|
|
349
|
+
onMeta(meta) {
|
|
350
|
+
writeSse(res, "meta", meta);
|
|
351
|
+
},
|
|
352
|
+
onDelta(delta) {
|
|
353
|
+
if (closed) return;
|
|
354
|
+
writeSse(res, "delta", { delta });
|
|
355
|
+
},
|
|
356
|
+
onError(error) {
|
|
357
|
+
if (closed) return;
|
|
358
|
+
writeSse(res, "error", { error });
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
if (!closed) {
|
|
363
|
+
// Final snapshot for clients that only render on done.
|
|
364
|
+
const session = options.agentRuntime.getSessionIfExists(sessionKey);
|
|
365
|
+
const lastAssistant = session?.getLastAssistantText?.() ?? undefined;
|
|
366
|
+
writeSse(res, "done", { text: lastAssistant ?? "" });
|
|
367
|
+
}
|
|
368
|
+
} catch (err) {
|
|
369
|
+
if (!closed) {
|
|
370
|
+
writeSse(res, "error", { error: String(err) });
|
|
371
|
+
}
|
|
372
|
+
} finally {
|
|
373
|
+
clearInterval(keepalive);
|
|
374
|
+
if (!closed) {
|
|
375
|
+
res.end();
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// WebSocket chat streaming (preferred).
|
|
381
|
+
// Client connects to ws(s)://<host>/api/chat/ws and sends { type:"chat.send", session, text }.
|
|
382
|
+
const wsPath = "/api/chat/ws";
|
|
383
|
+
|
|
384
|
+
if (!isProd) {
|
|
385
|
+
vite = await createViteServer({
|
|
386
|
+
root: WEB_ROOT,
|
|
387
|
+
server: { middlewareMode: true },
|
|
388
|
+
appType: "custom",
|
|
389
|
+
});
|
|
390
|
+
app.use(vite.middlewares);
|
|
391
|
+
|
|
392
|
+
app.use("*", async (req, res) => {
|
|
393
|
+
try {
|
|
394
|
+
const template = readFileSync(resolve(WEB_ROOT, "index.html"), "utf-8");
|
|
395
|
+
const html = await vite!.transformIndexHtml(req.originalUrl, template);
|
|
396
|
+
res.status(200).setHeader("Content-Type", "text/html").end(html);
|
|
397
|
+
} catch (e) {
|
|
398
|
+
vite?.ssrFixStacktrace(e as Error);
|
|
399
|
+
res.status(500).end(String(e));
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
} else {
|
|
403
|
+
app.use(express.static(WEB_DIST));
|
|
404
|
+
app.use("*", (_req, res) => {
|
|
405
|
+
res.sendFile(resolve(WEB_DIST, "index.html"));
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
await new Promise<void>((resolveListen, rejectListen) => {
|
|
410
|
+
const server = createHttpServer(app);
|
|
411
|
+
server.once("error", rejectListen);
|
|
412
|
+
server.listen(port, host, () => resolveListen());
|
|
413
|
+
httpServer = server;
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
// Attach WS server after HTTP server exists.
|
|
417
|
+
wss = new WebSocketServer({ server: httpServer!, path: wsPath });
|
|
418
|
+
wss.on("connection", (socket: WebSocket, req: IncomingMessage) => {
|
|
419
|
+
console.log("[dashboard][ws] connection", {
|
|
420
|
+
url: req.url,
|
|
421
|
+
remoteAddress: req.socket.remoteAddress
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
let activeSessionKey: string | null = null;
|
|
425
|
+
let activeRunId: string | null = null;
|
|
426
|
+
let closed = false;
|
|
427
|
+
const queue: Array<{ runId: string; msg: ReturnType<typeof createBotMessage>; sessionKey: string }> = [];
|
|
428
|
+
let draining = false;
|
|
429
|
+
|
|
430
|
+
const send = (frame: WsServerFrame) => {
|
|
431
|
+
if (closed) return;
|
|
432
|
+
try {
|
|
433
|
+
socket.send(JSON.stringify(frame));
|
|
434
|
+
} catch (err) {
|
|
435
|
+
console.warn("[dashboard][ws] send failed", {
|
|
436
|
+
activeRunId,
|
|
437
|
+
activeSessionKey,
|
|
438
|
+
frameType: frame.type,
|
|
439
|
+
error: String(err)
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
const resolveSessionKeyFromIdentity = (sessionIdentity: ChatSessionIdentity | undefined) => {
|
|
445
|
+
const keyMsg = createBotMessage({
|
|
446
|
+
channel: sessionIdentity?.channel ?? "dashboard",
|
|
447
|
+
isDirectMessage: Boolean(sessionIdentity?.isDirectMessage ?? true),
|
|
448
|
+
senderId: sessionIdentity?.senderId ?? "dashboard-user",
|
|
449
|
+
conversationId: sessionIdentity?.conversationId ?? "dashboard",
|
|
450
|
+
threadId: sessionIdentity?.threadId,
|
|
451
|
+
text: "",
|
|
452
|
+
mentions: [],
|
|
453
|
+
});
|
|
454
|
+
return getSessionKey(keyMsg);
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
const abortActive = async (sessionKey?: string) => {
|
|
458
|
+
const key = sessionKey ?? activeSessionKey;
|
|
459
|
+
if (!key) return;
|
|
460
|
+
await options.agentRuntime.abortSession(key);
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
socket.on("close", () => {
|
|
464
|
+
closed = true;
|
|
465
|
+
console.log("[dashboard][ws] close", {
|
|
466
|
+
activeRunId,
|
|
467
|
+
activeSessionKey
|
|
468
|
+
});
|
|
469
|
+
void abortActive();
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
// Keep WS alive across proxies. Browser will ignore ping frames; ws lib handles pong.
|
|
473
|
+
const pingTimer = setInterval(() => {
|
|
474
|
+
try {
|
|
475
|
+
socket.ping();
|
|
476
|
+
} catch {
|
|
477
|
+
// ignore
|
|
478
|
+
}
|
|
479
|
+
}, 15000);
|
|
480
|
+
pingTimer.unref?.();
|
|
481
|
+
|
|
482
|
+
const drainQueue = async () => {
|
|
483
|
+
if (draining || closed) return;
|
|
484
|
+
draining = true;
|
|
485
|
+
try {
|
|
486
|
+
while (!closed) {
|
|
487
|
+
const next = queue.shift();
|
|
488
|
+
if (!next) break;
|
|
489
|
+
|
|
490
|
+
const { runId, msg, sessionKey } = next;
|
|
491
|
+
activeRunId = runId;
|
|
492
|
+
activeSessionKey = sessionKey;
|
|
493
|
+
|
|
494
|
+
const existing = options.agentRuntime.getSessionIfExists(sessionKey);
|
|
495
|
+
if (existing?.isStreaming) {
|
|
496
|
+
send({ type: "ack", runId, status: "in_flight" });
|
|
497
|
+
send({ type: "error", runId, sessionKey, error: `Session is busy: ${sessionKey}` });
|
|
498
|
+
activeRunId = null;
|
|
499
|
+
activeSessionKey = null;
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
send({ type: "ack", runId, status: "started" });
|
|
504
|
+
send({ type: "meta", runId, sessionKey });
|
|
505
|
+
|
|
506
|
+
try {
|
|
507
|
+
const res = await options.agentRuntime.stream(msg, {
|
|
508
|
+
onMeta(meta) {
|
|
509
|
+
// repeat meta for robustness
|
|
510
|
+
send({ type: "meta", runId, sessionKey: meta.sessionKey });
|
|
511
|
+
},
|
|
512
|
+
onDelta(delta) {
|
|
513
|
+
send({ type: "delta", runId, sessionKey, delta });
|
|
514
|
+
},
|
|
515
|
+
onError(error) {
|
|
516
|
+
send({ type: "error", runId, sessionKey, error });
|
|
517
|
+
},
|
|
518
|
+
});
|
|
519
|
+
send({ type: "done", runId, sessionKey, text: res.finalText });
|
|
520
|
+
} catch (err) {
|
|
521
|
+
send({ type: "error", runId, sessionKey, error: String(err) });
|
|
522
|
+
} finally {
|
|
523
|
+
activeRunId = null;
|
|
524
|
+
activeSessionKey = null;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
} finally {
|
|
528
|
+
draining = false;
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
socket.on("message", async (raw: RawData) => {
|
|
533
|
+
let parsed: WsClientFrame | null = null;
|
|
534
|
+
try {
|
|
535
|
+
parsed = JSON.parse(String(raw)) as WsClientFrame;
|
|
536
|
+
} catch {
|
|
537
|
+
send({ type: "error", runId: "unknown", error: "invalid json" });
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (!parsed || typeof parsed !== "object" || typeof (parsed as { type?: unknown }).type !== "string") {
|
|
542
|
+
send({ type: "error", runId: "unknown", error: "invalid frame" });
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (parsed.type === "ping") {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (parsed.type === "chat.abort") {
|
|
551
|
+
const key =
|
|
552
|
+
typeof parsed.sessionKey === "string"
|
|
553
|
+
? parsed.sessionKey
|
|
554
|
+
: resolveSessionKeyFromIdentity(parsed.session);
|
|
555
|
+
// If abort targets current run, abort immediately; otherwise best-effort abort by sessionKey.
|
|
556
|
+
await abortActive(key);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (parsed.type !== "chat.send") {
|
|
561
|
+
send({ type: "error", runId: "unknown", error: `unknown frame type: ${(parsed as { type: string }).type}` });
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const runId = typeof parsed.runId === "string" ? parsed.runId : "";
|
|
566
|
+
if (!runId.trim()) {
|
|
567
|
+
send({ type: "error", runId: "unknown", error: "missing runId" });
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const text = typeof parsed.text === "string" ? parsed.text : "";
|
|
572
|
+
const sessionIdentity =
|
|
573
|
+
typeof parsed.sessionKey === "string" && parsed.sessionKey.trim()
|
|
574
|
+
? parseSessionKey(parsed.sessionKey)
|
|
575
|
+
: (parsed.session ?? {});
|
|
576
|
+
const msg = createBotMessage({
|
|
577
|
+
channel: sessionIdentity.channel ?? "dashboard",
|
|
578
|
+
isDirectMessage: Boolean(sessionIdentity.isDirectMessage ?? true),
|
|
579
|
+
senderId: sessionIdentity.senderId ?? "dashboard-user",
|
|
580
|
+
conversationId: sessionIdentity.conversationId ?? "dashboard",
|
|
581
|
+
threadId: sessionIdentity.threadId,
|
|
582
|
+
text,
|
|
583
|
+
mentions: [],
|
|
584
|
+
});
|
|
585
|
+
const sessionKey = getSessionKey(msg);
|
|
586
|
+
|
|
587
|
+
queue.push({ runId, msg, sessionKey });
|
|
588
|
+
void drainQueue();
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
socket.once("close", () => clearInterval(pingTimer));
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
started = true;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
async function stop() {
|
|
598
|
+
if (!started) return;
|
|
599
|
+
if (wss) {
|
|
600
|
+
await new Promise<void>((resolveClose) => {
|
|
601
|
+
wss!.close(() => resolveClose());
|
|
602
|
+
});
|
|
603
|
+
wss = null;
|
|
604
|
+
}
|
|
605
|
+
await new Promise<void>((resolveClose, rejectClose) => {
|
|
606
|
+
httpServer?.close((err) => (err ? rejectClose(err) : resolveClose()));
|
|
607
|
+
});
|
|
608
|
+
httpServer = null;
|
|
609
|
+
if (vite) {
|
|
610
|
+
await vite.close();
|
|
611
|
+
vite = null;
|
|
612
|
+
}
|
|
613
|
+
started = false;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
return {
|
|
617
|
+
start,
|
|
618
|
+
stop,
|
|
619
|
+
isStarted: () => started,
|
|
620
|
+
getUrl: () => url,
|
|
621
|
+
};
|
|
622
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { PiAgentRuntime } from "../agent.js";
|
|
2
|
+
import type { ConfigStore } from "./config-store.js";
|
|
3
|
+
|
|
4
|
+
export interface DashboardServerOptions {
|
|
5
|
+
runtime: DashboardRuntime;
|
|
6
|
+
agentRuntime: PiAgentRuntime;
|
|
7
|
+
configStore: ConfigStore;
|
|
8
|
+
host?: string;
|
|
9
|
+
port?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface DashboardServer {
|
|
13
|
+
start(): Promise<void>;
|
|
14
|
+
stop(): Promise<void>;
|
|
15
|
+
isStarted(): boolean;
|
|
16
|
+
getUrl(): string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type DashboardRuntime = {
|
|
20
|
+
appName: string;
|
|
21
|
+
agentMode: string;
|
|
22
|
+
workspaceDir: string;
|
|
23
|
+
agentDir: string;
|
|
24
|
+
enabledChannels: { feishu: boolean; wechat: boolean };
|
|
25
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>pi-bot dashboard</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
13
|
+
|