alemonjs-aichat 1.0.39 → 1.0.40

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.
@@ -0,0 +1,441 @@
1
+ import React from 'react';
2
+ import { renderToString } from 'react-dom/server';
3
+ import Koa from 'koa';
4
+ import KoaStatic from 'koa-static';
5
+ import Router from 'koa-router';
6
+ import send from 'koa-send';
7
+ import { existsSync } from 'fs';
8
+ import { readFile } from 'fs/promises';
9
+ import { normalize, extname, dirname, join } from 'path';
10
+ import { processHtmlPaths } from 'jsxp';
11
+ import redisClient from '../config.js';
12
+ import panelConfig from './config.js';
13
+ import { resolvePanelChatIdentity, clearPanelChatMessages, sendPanelChatMessageStream, sendPanelChatMessage } from './chat.js';
14
+ import { addPanelGroupConfig, updatePanelGroupConfig } from './groupConfig.js';
15
+
16
+ const textTypes = new Set(["css", "js", "mjs", "html", "svg", "json"]);
17
+ const mimeTypes = {
18
+ css: "text/css",
19
+ js: "application/javascript",
20
+ mjs: "application/javascript",
21
+ html: "text/html",
22
+ svg: "image/svg+xml",
23
+ json: "application/json",
24
+ };
25
+ const loadConfig = async () => panelConfig;
26
+ const rewriteServerUrls = (content, dir, prefix) => content.replace(/url\(["']?([^"')]+)["']?\)/g, (full, rawPath) => {
27
+ if (/^(https?:|data:|\/)/.test(rawPath))
28
+ return full;
29
+ const filePath = normalize(join(dir, rawPath));
30
+ return `url(${prefix}/_jsxp_file?path=${encodeURIComponent(filePath)})`;
31
+ });
32
+ const renderRoute = async (options, ctx) => {
33
+ let html = "";
34
+ if ("html" in options && options.html) {
35
+ html = options.html;
36
+ }
37
+ else if ("element" in options && options.element) {
38
+ const props = options.propsCall ? await options.propsCall(ctx) : {};
39
+ html = `<!DOCTYPE html>${renderToString(React.createElement(options.element, props))}`;
40
+ }
41
+ else {
42
+ html = `<!DOCTYPE html>${renderToString(options.component)}`;
43
+ }
44
+ return processHtmlPaths(html, "server");
45
+ };
46
+ const getQueryText = (value) => {
47
+ if (Array.isArray(value))
48
+ return String(value[0] || "").trim();
49
+ return String(value || "").trim();
50
+ };
51
+ const readFormBody = async (ctx) => {
52
+ const state = ctx.state;
53
+ if (state.panelFormBody)
54
+ return state.panelFormBody;
55
+ const chunks = [];
56
+ await new Promise((resolve, reject) => {
57
+ ctx.req.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
58
+ ctx.req.on("end", resolve);
59
+ ctx.req.on("error", reject);
60
+ });
61
+ state.panelFormBody = new URLSearchParams(Buffer.concat(chunks).toString());
62
+ return state.panelFormBody;
63
+ };
64
+ const getActionText = async (ctx, key) => {
65
+ if (ctx.method.toUpperCase() === "POST") {
66
+ const body = await readFormBody(ctx);
67
+ return String(body.get(key) || "").trim();
68
+ }
69
+ return getQueryText(ctx.query[key]);
70
+ };
71
+ const getPanelGuid = async (queryGuid) => {
72
+ if (queryGuid)
73
+ return queryGuid;
74
+ const keys = await redisClient.redis.keys("ai:currentAI:*");
75
+ const firstKey = keys[0];
76
+ if (!firstKey)
77
+ return "global";
78
+ return firstKey.replace("ai:currentAI:", "") || "global";
79
+ };
80
+ const getActionUiState = async (ctx) => ({
81
+ groupListOpen: await getActionText(ctx, "panelGroupListOpen"),
82
+ openGroups: await getActionText(ctx, "panelOpenGroups"),
83
+ aiListOpen: await getActionText(ctx, "panelAiListOpen"),
84
+ openRows: await getActionText(ctx, "panelOpenRows"),
85
+ scrollY: await getActionText(ctx, "panelScrollY"),
86
+ });
87
+ const redirectPanel = async (ctx, status, message) => {
88
+ const state = await getActionUiState(ctx);
89
+ const params = new URLSearchParams({
90
+ panelStatus: status,
91
+ panelMessage: message,
92
+ });
93
+ if (state.groupListOpen)
94
+ params.set("panelGroupListOpen", state.groupListOpen);
95
+ if (state.openGroups)
96
+ params.set("panelOpenGroups", state.openGroups);
97
+ if (state.aiListOpen)
98
+ params.set("panelAiListOpen", state.aiListOpen);
99
+ if (state.openRows)
100
+ params.set("panelOpenRows", state.openRows);
101
+ if (state.scrollY)
102
+ params.set("panelScrollY", state.scrollY);
103
+ ctx.redirect(`/panel/config?${params.toString()}`);
104
+ };
105
+ const redirectPanelChat = async (ctx, status, message, guid, userId) => {
106
+ const chatConfigOpen = await getActionText(ctx, "chatConfigOpen");
107
+ const nickname = await getActionText(ctx, "nickname");
108
+ const params = new URLSearchParams({
109
+ panelStatus: status,
110
+ panelMessage: message,
111
+ guid,
112
+ userId,
113
+ });
114
+ if (chatConfigOpen)
115
+ params.set("chatConfigOpen", chatConfigOpen);
116
+ if (nickname)
117
+ params.set("nickname", nickname);
118
+ ctx.redirect(`/panel/chat?${params.toString()}`);
119
+ };
120
+ const handleUseAI = async (ctx) => {
121
+ const name = await getActionText(ctx, "name");
122
+ if (!name) {
123
+ await redirectPanel(ctx, "error", "缺少AI名称");
124
+ return;
125
+ }
126
+ const guid = await getPanelGuid(await getActionText(ctx, "guid"));
127
+ const aiConfig = await redisClient.switchAI(guid, name);
128
+ if (!aiConfig) {
129
+ await redirectPanel(ctx, "error", `切换失败,未找到AI:${name}`);
130
+ return;
131
+ }
132
+ await redirectPanel(ctx, "success", `已将 ${guid} 切换到 ${name}`);
133
+ };
134
+ const handleDeleteAI = async (ctx) => {
135
+ const name = await getActionText(ctx, "name");
136
+ if (!name) {
137
+ await redirectPanel(ctx, "error", "缺少AI名称");
138
+ return;
139
+ }
140
+ const deleted = await redisClient.deleteAI(name);
141
+ if (!deleted) {
142
+ await redirectPanel(ctx, "error", `删除失败,未找到AI:${name}`);
143
+ return;
144
+ }
145
+ await redirectPanel(ctx, "success", `已删除AI:${name}`);
146
+ };
147
+ const getActionFlag = async (ctx, key) => {
148
+ if (ctx.method.toUpperCase() === "POST") {
149
+ const body = await readFormBody(ctx);
150
+ return body.has(key) && body.get(key) !== "0";
151
+ }
152
+ const value = getQueryText(ctx.query[key]);
153
+ return value === "1" || value === "true" || value === "on";
154
+ };
155
+ const handleUpdateGroupConfig = async (ctx) => {
156
+ const guid = await getActionText(ctx, "guid");
157
+ const userId = await getActionText(ctx, "userId");
158
+ const source = await getActionText(ctx, "source");
159
+ const currentAI = await getActionText(ctx, "currentAI");
160
+ const capiContextLimit = Number(await getActionText(ctx, "capiContextLimit"));
161
+ const redirectResult = (status, message, targetGuid = guid) => {
162
+ if (source === "chat") {
163
+ return redirectPanelChat(ctx, status, message, targetGuid, userId || "webuser");
164
+ }
165
+ return redirectPanel(ctx, status, message);
166
+ };
167
+ if (!guid) {
168
+ await redirectResult("error", "缺少群号 guid", "global");
169
+ return;
170
+ }
171
+ try {
172
+ await updatePanelGroupConfig({
173
+ guid,
174
+ currentAI,
175
+ capiContextLimit,
176
+ switches: {
177
+ chatSwitch: await getActionFlag(ctx, "chatSwitch"),
178
+ toolSwitch: await getActionFlag(ctx, "toolSwitch"),
179
+ toolCallContentSwitch: await getActionFlag(ctx, "toolCallContentSwitch"),
180
+ complexOutput: await getActionFlag(ctx, "complexOutput"),
181
+ affectionSwitch: await getActionFlag(ctx, "affectionSwitch"),
182
+ ttsResponseSwitch: await getActionFlag(ctx, "ttsResponseSwitch"),
183
+ deepThoughtSwitch: await getActionFlag(ctx, "deepThoughtSwitch"),
184
+ r18Switch: await getActionFlag(ctx, "r18Switch"),
185
+ atTriggerSwitch: await getActionFlag(ctx, "atTriggerSwitch"),
186
+ toolPromptSwitch: await getActionFlag(ctx, "toolPromptSwitch"),
187
+ toolPromptRevokeSwitch: await getActionFlag(ctx, "toolPromptRevokeSwitch"),
188
+ toolPromptArgsSwitch: await getActionFlag(ctx, "toolPromptArgsSwitch"),
189
+ proactiveSwitch: await getActionFlag(ctx, "proactiveSwitch"),
190
+ },
191
+ });
192
+ await redirectResult("success", `已保存群 ${guid} 的配置`);
193
+ }
194
+ catch (error) {
195
+ await redirectResult("error", error instanceof Error ? error.message : String(error));
196
+ }
197
+ };
198
+ const parseJsonArray = (value) => {
199
+ if (!value)
200
+ return [];
201
+ try {
202
+ const parsed = JSON.parse(value);
203
+ if (!Array.isArray(parsed))
204
+ return [];
205
+ return parsed.map((item) => String(item || "").trim()).filter(Boolean);
206
+ }
207
+ catch {
208
+ return [];
209
+ }
210
+ };
211
+ const handleAddGroupConfig = async (ctx) => {
212
+ const guid = await getActionText(ctx, "guid");
213
+ const currentAI = await getActionText(ctx, "currentAI");
214
+ const chatSwitch = await getActionFlag(ctx, "chatSwitch");
215
+ if (!guid) {
216
+ await redirectPanel(ctx, "error", "缺少群号 guid");
217
+ return;
218
+ }
219
+ try {
220
+ await addPanelGroupConfig({
221
+ guid,
222
+ currentAI,
223
+ chatSwitch,
224
+ });
225
+ const state = await getActionUiState(ctx);
226
+ const params = new URLSearchParams({
227
+ panelStatus: "success",
228
+ panelMessage: `已添加群 ${guid}`,
229
+ panelGroupListOpen: "1",
230
+ panelOpenGroups: JSON.stringify([...new Set([...parseJsonArray(state.openGroups), guid])]),
231
+ });
232
+ if (state.aiListOpen)
233
+ params.set("panelAiListOpen", state.aiListOpen);
234
+ if (state.openRows)
235
+ params.set("panelOpenRows", state.openRows);
236
+ if (state.scrollY)
237
+ params.set("panelScrollY", state.scrollY);
238
+ ctx.redirect(`/panel/config?${params.toString()}`);
239
+ }
240
+ catch (error) {
241
+ await redirectPanel(ctx, "error", error instanceof Error ? error.message : String(error));
242
+ }
243
+ };
244
+ const handlePanelChat = async (ctx) => {
245
+ const identity = await resolvePanelChatIdentity(await getActionText(ctx, "guid"), await getActionText(ctx, "userId"), await getActionText(ctx, "nickname"));
246
+ const text = await getActionText(ctx, "message");
247
+ if (!text) {
248
+ await redirectPanelChat(ctx, "error", "消息不能为空", identity.guid, identity.userId);
249
+ return;
250
+ }
251
+ await sendPanelChatMessage(identity.guid, identity.userId, identity.nickname, text);
252
+ await redirectPanelChat(ctx, "success", "消息已发送", identity.guid, identity.userId);
253
+ };
254
+ const handlePanelChatStream = async (ctx) => {
255
+ const identity = await resolvePanelChatIdentity(await getActionText(ctx, "guid"), await getActionText(ctx, "userId"), await getActionText(ctx, "nickname"));
256
+ const text = await getActionText(ctx, "message");
257
+ ctx.respond = false;
258
+ ctx.res.writeHead(200, {
259
+ "Content-Type": "application/x-ndjson; charset=utf-8",
260
+ "Cache-Control": "no-cache, no-transform",
261
+ Connection: "keep-alive",
262
+ "X-Accel-Buffering": "no",
263
+ });
264
+ const writeEvent = async (event) => {
265
+ ctx.res.write(`${JSON.stringify(event)}\n`);
266
+ };
267
+ try {
268
+ await sendPanelChatMessageStream(identity.guid, identity.userId, identity.nickname, text, writeEvent);
269
+ }
270
+ catch (error) {
271
+ await writeEvent({
272
+ type: "error",
273
+ message: error instanceof Error ? error.message : String(error),
274
+ });
275
+ await writeEvent({ type: "done" });
276
+ }
277
+ finally {
278
+ ctx.res.end();
279
+ }
280
+ };
281
+ const handleClearPanelChat = async (ctx) => {
282
+ const identity = await resolvePanelChatIdentity(await getActionText(ctx, "guid"), await getActionText(ctx, "userId"), await getActionText(ctx, "nickname"));
283
+ await clearPanelChatMessages(identity.guid, identity.userId);
284
+ await redirectPanelChat(ctx, "success", "聊天记录已清空", identity.guid, identity.userId);
285
+ };
286
+ const handleChatUseAI = async (ctx) => {
287
+ const identity = await resolvePanelChatIdentity(await getActionText(ctx, "guid"), await getActionText(ctx, "userId"), await getActionText(ctx, "nickname"));
288
+ const name = await getActionText(ctx, "name");
289
+ if (!name) {
290
+ await redirectPanelChat(ctx, "error", "缺少AI配置名称", identity.guid, identity.userId);
291
+ return;
292
+ }
293
+ const aiConfig = await redisClient.switchAI(identity.guid, name);
294
+ if (!aiConfig) {
295
+ await redirectPanelChat(ctx, "error", `切换失败,未找到AI:${name}`, identity.guid, identity.userId);
296
+ return;
297
+ }
298
+ await redirectPanelChat(ctx, "success", `已将 ${identity.guid} 切换到 ${name}`, identity.guid, identity.userId);
299
+ };
300
+ const handleChatSwitchModel = async (ctx) => {
301
+ const identity = await resolvePanelChatIdentity(await getActionText(ctx, "guid"), await getActionText(ctx, "userId"), await getActionText(ctx, "nickname"));
302
+ const model = await getActionText(ctx, "model");
303
+ if (!model) {
304
+ await redirectPanelChat(ctx, "error", "缺少模型名称", identity.guid, identity.userId);
305
+ return;
306
+ }
307
+ const aiConfig = await redisClient.getAIConfig(identity.guid);
308
+ if (!aiConfig.host.trim() || !aiConfig.key.trim()) {
309
+ await redirectPanelChat(ctx, "error", `群号 ${identity.guid} 还没有可用 AI 配置`, identity.guid, identity.userId);
310
+ return;
311
+ }
312
+ await redisClient.setAIConfig(identity.guid, {
313
+ ...aiConfig,
314
+ model,
315
+ });
316
+ await redirectPanelChat(ctx, "success", `已切换模型:${model}`, identity.guid, identity.userId);
317
+ };
318
+ const handleChatSwitchContextLimit = async (ctx) => {
319
+ const identity = await resolvePanelChatIdentity(await getActionText(ctx, "guid"), await getActionText(ctx, "userId"), await getActionText(ctx, "nickname"));
320
+ const capiContextLimit = Number(await getActionText(ctx, "capiContextLimit"));
321
+ if (!Number.isFinite(capiContextLimit) || capiContextLimit <= 0) {
322
+ await redirectPanelChat(ctx, "error", "请输入有效的上下文长度", identity.guid, identity.userId);
323
+ return;
324
+ }
325
+ await redisClient.setCAPIContextLimit(identity.guid, capiContextLimit);
326
+ await redirectPanelChat(ctx, "success", `已更新上下文长度:${capiContextLimit}`, identity.guid, identity.userId);
327
+ };
328
+ const handleChatSwitchPrompt = async (ctx) => {
329
+ const identity = await resolvePanelChatIdentity(await getActionText(ctx, "guid"), await getActionText(ctx, "userId"), await getActionText(ctx, "nickname"));
330
+ const prompt = await getActionText(ctx, "prompt");
331
+ const result = await redisClient.switchPrompt(identity.guid, prompt);
332
+ if (prompt && result === null) {
333
+ await redirectPanelChat(ctx, "error", `切换失败,未找到提示词:${prompt}`, identity.guid, identity.userId);
334
+ return;
335
+ }
336
+ await redirectPanelChat(ctx, "success", prompt ? `已切换提示词:${prompt}` : "已恢复默认提示词", identity.guid, identity.userId);
337
+ };
338
+ let panelServer = null;
339
+ let panelServerPromise = null;
340
+ const startPanelServer = async () => {
341
+ const config = await loadConfig();
342
+ const app = new Koa();
343
+ const prefix = config?.prefix ?? "";
344
+ const router = new Router({ prefix });
345
+ const cwd = process.cwd();
346
+ const port = config?.port ?? 8080;
347
+ const configStatics = config?.statics ?? "public";
348
+ const staticRoots = Array.isArray(configStatics) ? configStatics : [configStatics];
349
+ const allowedFileRoots = [cwd, ...staticRoots].map((item) => normalize(item).toLowerCase());
350
+ console.log("_______jsxp-panel_______");
351
+ router.get("/panel/action/use-ai", handleUseAI);
352
+ router.get("/panel/action/delete-ai", handleDeleteAI);
353
+ router.get("/panel/action/clear-chat", handleClearPanelChat);
354
+ router.post("/panel/action/add-group-config", handleAddGroupConfig);
355
+ router.post("/panel/action/update-group-config", handleUpdateGroupConfig);
356
+ router.post("/panel/action/use-ai", handleUseAI);
357
+ router.post("/panel/action/delete-ai", handleDeleteAI);
358
+ router.post("/panel/action/chat/use-ai", handleChatUseAI);
359
+ router.post("/panel/action/chat/switch-model", handleChatSwitchModel);
360
+ router.post("/panel/action/chat/switch-context-limit", handleChatSwitchContextLimit);
361
+ router.post("/panel/action/chat/switch-prompt", handleChatSwitchPrompt);
362
+ router.post("/panel/action/chat/stream", handlePanelChatStream);
363
+ router.post("/panel/action/chat", handlePanelChat);
364
+ router.post("/panel/action/clear-chat", handleClearPanelChat);
365
+ router.get("/_jsxp_file", async (ctx) => {
366
+ const raw = getQueryText(ctx.query.path);
367
+ const filePath = normalize(decodeURIComponent(raw));
368
+ if (!filePath) {
369
+ ctx.status = 400;
370
+ ctx.body = { error: 'Missing "path" query parameter' };
371
+ return;
372
+ }
373
+ const normalizedFilePath = normalize(filePath).toLowerCase();
374
+ if (!allowedFileRoots.some((root) => normalizedFilePath.startsWith(root))) {
375
+ ctx.status = 403;
376
+ ctx.body = { error: "Access denied" };
377
+ return;
378
+ }
379
+ if (!existsSync(filePath)) {
380
+ ctx.status = 404;
381
+ ctx.body = { error: "File not found" };
382
+ return;
383
+ }
384
+ const ext = extname(filePath).slice(1).toLowerCase();
385
+ if (textTypes.has(ext)) {
386
+ const content = await readFile(filePath, "utf-8");
387
+ ctx.type = mimeTypes[ext] || "text/plain";
388
+ ctx.body = rewriteServerUrls(content, dirname(filePath), prefix);
389
+ return;
390
+ }
391
+ await send(ctx, filePath, { root: "/" });
392
+ });
393
+ const routes = config?.routes ?? {};
394
+ for (const url in routes) {
395
+ console.log(`http://${config?.host ?? "127.0.0.1"}:${port}${prefix + url}`);
396
+ router.get(url, async (ctx) => {
397
+ const latestConfig = await loadConfig();
398
+ const latestRoute = latestConfig?.routes?.[url];
399
+ if (!latestRoute) {
400
+ ctx.status = 404;
401
+ return;
402
+ }
403
+ ctx.body = await renderRoute(latestRoute, ctx);
404
+ });
405
+ }
406
+ for (const item of staticRoots) {
407
+ app.use(KoaStatic(item));
408
+ }
409
+ app.use(router.routes());
410
+ await new Promise((resolve, reject) => {
411
+ const server = app.listen(port);
412
+ panelServer = server;
413
+ server.once("listening", () => {
414
+ console.log(`Server is running on port ${port}`);
415
+ console.log("自行调整默认浏览器尺寸 800 X 1280 100%");
416
+ console.log("_______jsxp-panel_______");
417
+ resolve();
418
+ });
419
+ server.once("error", (error) => {
420
+ panelServer = null;
421
+ if (error.code === "EADDRINUSE") {
422
+ console.warn(`网页控制台端口 ${port} 已被占用,跳过本次启动。`);
423
+ resolve();
424
+ return;
425
+ }
426
+ reject(error);
427
+ });
428
+ });
429
+ };
430
+ const createPanelServer = async () => {
431
+ if (panelServer || panelServerPromise) {
432
+ return panelServerPromise ?? Promise.resolve();
433
+ }
434
+ panelServerPromise = startPanelServer().catch((error) => {
435
+ panelServerPromise = null;
436
+ throw error;
437
+ });
438
+ return panelServerPromise;
439
+ };
440
+
441
+ export { createPanelServer };