@coze-arch/cli 0.0.15-alpha.a0f5b9 → 0.0.15-alpha.dfd4c4

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.
Files changed (103) hide show
  1. package/lib/__templates__/expo/.coze +1 -0
  2. package/lib/__templates__/expo/.cozeproj/scripts/validate.sh +8 -0
  3. package/lib/__templates__/expo/package.json +2 -1
  4. package/lib/__templates__/nextjs/.coze +1 -0
  5. package/lib/__templates__/nextjs/package.json +3 -1
  6. package/lib/__templates__/nextjs/scripts/validate.sh +10 -0
  7. package/lib/__templates__/nuxt-vue/.coze +1 -0
  8. package/lib/__templates__/nuxt-vue/eslint.config.mjs +25 -0
  9. package/lib/__templates__/nuxt-vue/package.json +9 -2
  10. package/lib/__templates__/nuxt-vue/pnpm-lock.yaml +790 -10
  11. package/lib/__templates__/nuxt-vue/scripts/validate.sh +10 -0
  12. package/lib/__templates__/taro/.coze +1 -0
  13. package/lib/__templates__/taro/.cozeproj/scripts/validate.sh +8 -0
  14. package/lib/__templates__/taro/package.json +1 -1
  15. package/lib/__templates__/templates.json +0 -24
  16. package/lib/__templates__/vite/.coze +1 -0
  17. package/lib/__templates__/vite/package.json +3 -1
  18. package/lib/__templates__/vite/scripts/validate.sh +10 -0
  19. package/lib/cli.js +330 -97
  20. package/package.json +1 -1
  21. package/lib/__templates__/pi-agent/.coze +0 -10
  22. package/lib/__templates__/pi-agent/AGENTS.md +0 -140
  23. package/lib/__templates__/pi-agent/README.md +0 -172
  24. package/lib/__templates__/pi-agent/_gitignore +0 -3
  25. package/lib/__templates__/pi-agent/_npmrc +0 -23
  26. package/lib/__templates__/pi-agent/docs/project-overview.md +0 -356
  27. package/lib/__templates__/pi-agent/docs/user/getting-started.md +0 -47
  28. package/lib/__templates__/pi-agent/package.json +0 -60
  29. package/lib/__templates__/pi-agent/pi-resources/SYSTEM.md +0 -15
  30. package/lib/__templates__/pi-agent/pi-resources/extensions/preference-memory/index.ts +0 -355
  31. package/lib/__templates__/pi-agent/pi-resources/extensions/test-ping.ts +0 -19
  32. package/lib/__templates__/pi-agent/pi-resources/prompts/test-prompt.md +0 -11
  33. package/lib/__templates__/pi-agent/pi-resources/skills/coze-asr/SKILL.md +0 -36
  34. package/lib/__templates__/pi-agent/pi-resources/skills/coze-asr/scripts/asr.mjs +0 -9
  35. package/lib/__templates__/pi-agent/pi-resources/skills/coze-image-gen/SKILL.md +0 -41
  36. package/lib/__templates__/pi-agent/pi-resources/skills/coze-image-gen/scripts/gen.mjs +0 -9
  37. package/lib/__templates__/pi-agent/pi-resources/skills/coze-tts/SKILL.md +0 -85
  38. package/lib/__templates__/pi-agent/pi-resources/skills/coze-tts/scripts/tts.mjs +0 -9
  39. package/lib/__templates__/pi-agent/pi-resources/skills/coze-video-gen/SKILL.md +0 -53
  40. package/lib/__templates__/pi-agent/pi-resources/skills/coze-video-gen/scripts/gen.mjs +0 -9
  41. package/lib/__templates__/pi-agent/pnpm-lock.yaml +0 -8285
  42. package/lib/__templates__/pi-agent/scripts/dev.sh +0 -14
  43. package/lib/__templates__/pi-agent/scripts/prepare.sh +0 -2
  44. package/lib/__templates__/pi-agent/src/agent.ts +0 -363
  45. package/lib/__templates__/pi-agent/src/channels/feishu/index.ts +0 -760
  46. package/lib/__templates__/pi-agent/src/channels/feishu/streaming-card.ts +0 -297
  47. package/lib/__templates__/pi-agent/src/channels/wechat/index.ts +0 -171
  48. package/lib/__templates__/pi-agent/src/config.ts +0 -596
  49. package/lib/__templates__/pi-agent/src/core.ts +0 -218
  50. package/lib/__templates__/pi-agent/src/dashboard/api/channels.ts +0 -148
  51. package/lib/__templates__/pi-agent/src/dashboard/api/docs.ts +0 -204
  52. package/lib/__templates__/pi-agent/src/dashboard/api/models.ts +0 -141
  53. package/lib/__templates__/pi-agent/src/dashboard/api/overview.ts +0 -33
  54. package/lib/__templates__/pi-agent/src/dashboard/config-store.ts +0 -64
  55. package/lib/__templates__/pi-agent/src/dashboard/index.ts +0 -39
  56. package/lib/__templates__/pi-agent/src/dashboard/server.ts +0 -622
  57. package/lib/__templates__/pi-agent/src/dashboard/types.ts +0 -25
  58. package/lib/__templates__/pi-agent/src/dashboard/web/index.html +0 -13
  59. package/lib/__templates__/pi-agent/src/dashboard/web/postcss.config.cjs +0 -7
  60. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/app-layout.tsx +0 -186
  61. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/page-title.tsx +0 -17
  62. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/alert.tsx +0 -22
  63. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/badge.tsx +0 -25
  64. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/button.tsx +0 -40
  65. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/card.tsx +0 -29
  66. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/input.tsx +0 -18
  67. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/label.tsx +0 -8
  68. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/select.tsx +0 -80
  69. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/separator.tsx +0 -23
  70. package/lib/__templates__/pi-agent/src/dashboard/web/src/hooks/use-fetch.ts +0 -32
  71. package/lib/__templates__/pi-agent/src/dashboard/web/src/hooks/use-local-storage-state.ts +0 -23
  72. package/lib/__templates__/pi-agent/src/dashboard/web/src/main.tsx +0 -30
  73. package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/channels-page.tsx +0 -188
  74. package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/chat-page.tsx +0 -451
  75. package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/docs-page.tsx +0 -65
  76. package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/models-page.tsx +0 -122
  77. package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/overview-page.tsx +0 -134
  78. package/lib/__templates__/pi-agent/src/dashboard/web/src/services/chat-ws-service.ts +0 -167
  79. package/lib/__templates__/pi-agent/src/dashboard/web/src/styles.css +0 -294
  80. package/lib/__templates__/pi-agent/src/dashboard/web/src/utils/index.ts +0 -11
  81. package/lib/__templates__/pi-agent/src/dashboard/web/tsconfig.json +0 -13
  82. package/lib/__templates__/pi-agent/src/dashboard/web/vite.config.ts +0 -17
  83. package/lib/__templates__/pi-agent/src/index.ts +0 -123
  84. package/lib/__templates__/pi-agent/src/pi-resources.ts +0 -125
  85. package/lib/__templates__/pi-agent/src/session-store.ts +0 -223
  86. package/lib/__templates__/pi-agent/src/tools/common/format-coze-error.ts +0 -12
  87. package/lib/__templates__/pi-agent/src/tools/index.ts +0 -2
  88. package/lib/__templates__/pi-agent/src/tools/web-fetch/index.ts +0 -195
  89. package/lib/__templates__/pi-agent/src/tools/web-search/index.ts +0 -206
  90. package/lib/__templates__/pi-agent/template.config.js +0 -45
  91. package/lib/__templates__/pi-agent/tests/config.test.ts +0 -315
  92. package/lib/__templates__/pi-agent/tests/dashboard-docs-api.test.ts +0 -125
  93. package/lib/__templates__/pi-agent/tests/dashboard-models-api.test.ts +0 -171
  94. package/lib/__templates__/pi-agent/tests/feishu-channel.test.ts +0 -149
  95. package/lib/__templates__/pi-agent/tests/feishu-streaming-card.test.ts +0 -15
  96. package/lib/__templates__/pi-agent/tests/pi-resources.test.ts +0 -73
  97. package/lib/__templates__/pi-agent/tests/preference-memory.test.ts +0 -43
  98. package/lib/__templates__/pi-agent/tests/session-store.test.ts +0 -61
  99. package/lib/__templates__/pi-agent/tests/smoke/run-smoke.ts +0 -275
  100. package/lib/__templates__/pi-agent/tests/web-fetch.test.ts +0 -157
  101. package/lib/__templates__/pi-agent/tests/web-search.test.ts +0 -208
  102. package/lib/__templates__/pi-agent/tsconfig.json +0 -21
  103. package/lib/__templates__/pi-agent/types/larksuiteoapi-node-sdk.d.ts +0 -113
@@ -1,622 +0,0 @@
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
- }
@@ -1,25 +0,0 @@
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
- };
@@ -1,13 +0,0 @@
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
-
@@ -1,7 +0,0 @@
1
- module.exports = {
2
- plugins: {
3
- "@tailwindcss/postcss": {},
4
- autoprefixer: {},
5
- },
6
- };
7
-