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.
@@ -1,4 +1,4 @@
1
- import { useMessage, Text, Audio, ImageURL, ImageFile, Mention } from 'alemonjs';
1
+ import { useMessage, Audio, Text, ImageURL, ImageFile, Mention } from 'alemonjs';
2
2
  import { log } from 'console';
3
3
  import OpenAi from 'openai';
4
4
  import { TTSClient } from '../../api/tts.js';
@@ -6,7 +6,7 @@ import { availableTools } from '../../api/aitools.js';
6
6
  import redisClient, { DEFAULT_CAPI_CONTEXT_LIMIT } from '../../config.js';
7
7
  import { getChatConfig, shouldSkipAIReply, buildUserMessage, getDeepThoughtReasoning } from './getChatConfig.js';
8
8
  import { createTTSMessage } from './tts.js';
9
- import { parseAIReply } from './tools.js';
9
+ import { parseAIReply, sendAIReplyToOutput } from './tools.js';
10
10
 
11
11
  const selects = onSelects(["message.create", "private.message.create"]);
12
12
  let currentModel = "派蒙-默认"; // 默认音色
@@ -16,6 +16,9 @@ const AUTO_SUMMARY_DETAIL_MAX_LENGTH = 2000;
16
16
  const sendAIReply = async (reply, cfg, e, message) => {
17
17
  const aireply = await parseAIReply(reply, cfg, e, message);
18
18
  console.log("aireply", aireply);
19
+ if (await sendAIReplyToOutput(message, aireply)) {
20
+ return aireply;
21
+ }
19
22
  if (aireply?.ttsMessages) {
20
23
  for (const ttsMsg of aireply.ttsMessages) {
21
24
  const ttsMessage = await createTTSMessage(ttsMsg.text, new TTSClient(), ttsMsg.model ?? currentModel);
@@ -31,6 +34,12 @@ const sendAIReply = async (reply, cfg, e, message) => {
31
34
  }
32
35
  return aireply;
33
36
  };
37
+ const sendOutputText = async (message, text) => {
38
+ if (typeof message?.sendText === "function") {
39
+ return await message.sendText(text);
40
+ }
41
+ return await message.send(format(Text(text)));
42
+ };
34
43
  const appendPendingGuidanceMessages = async (guid, messages) => {
35
44
  const queuedMessages = await redisClient.drainAIGuidanceMessages(guid);
36
45
  if (!queuedMessages.length) {
@@ -46,6 +55,30 @@ const appendPendingGuidanceMessages = async (guid, messages) => {
46
55
  await redisClient.addAIChatHistoryBatch(guid, guidanceMessages);
47
56
  return guidanceMessages;
48
57
  };
58
+ const extractTextValue = (value) => {
59
+ if (typeof value === "string") {
60
+ return value;
61
+ }
62
+ if (Array.isArray(value)) {
63
+ return value
64
+ .map((item) => extractTextValue(item))
65
+ .filter((text) => text.trim() !== "")
66
+ .join("\n");
67
+ }
68
+ if (value && typeof value === "object") {
69
+ return extractTextValue(value.text ?? value.content ?? value.summary ?? "");
70
+ }
71
+ return "";
72
+ };
73
+ const getToolCallDisplayContent = (message) => {
74
+ const content = extractTextValue(message?.content);
75
+ if (content.trim() !== "") {
76
+ return content;
77
+ }
78
+ return extractTextValue(message?.reasoning_content ??
79
+ message?.reasoningContent ??
80
+ message?.reasoning);
81
+ };
49
82
  const getTextLength = (value) => {
50
83
  if (typeof value === "string") {
51
84
  return value.length;
@@ -159,7 +192,7 @@ const recordCapiContextStats = async (guid, model, messages, completion) => {
159
192
  usageAvailable: Boolean(usage),
160
193
  });
161
194
  };
162
- const CApiReply = async (e) => {
195
+ const CApiReply = async (e, outputMessage) => {
163
196
  console.log("e.UserId", e.UserId, e.bot);
164
197
  const cfg = await getChatConfig(e);
165
198
  if (await shouldSkipAIReply(e, cfg)) {
@@ -168,7 +201,7 @@ const CApiReply = async (e) => {
168
201
  };
169
202
  }
170
203
  // 创建消息
171
- const [message] = useMessage(e);
204
+ const [message] = outputMessage ? [outputMessage] : useMessage(e);
172
205
  const usermessage = await buildUserMessage(e, cfg, "capi");
173
206
  try {
174
207
  const openai = new OpenAi({
@@ -176,6 +209,79 @@ const CApiReply = async (e) => {
176
209
  apiKey: cfg.config.key,
177
210
  timeout: 300000,
178
211
  });
212
+ const shouldStreamToOutput = typeof outputMessage?.streamTextDelta === "function";
213
+ const requestChatCompletion = async (params, requestMessages) => {
214
+ if (!shouldStreamToOutput) {
215
+ const completion = await openai.chat.completions.create(params);
216
+ await recordCapiContextStats(e.guid, cfg.config.model, requestMessages, completion);
217
+ return completion;
218
+ }
219
+ const streamParams = {
220
+ ...params,
221
+ stream: true,
222
+ };
223
+ const stream = (await openai.chat.completions.create(streamParams));
224
+ let content = "";
225
+ let reasoningContent = "";
226
+ const toolCalls = [];
227
+ const ensureToolCall = (index) => {
228
+ if (!toolCalls[index]) {
229
+ toolCalls[index] = {
230
+ id: "",
231
+ type: "function",
232
+ function: {
233
+ name: "",
234
+ arguments: "",
235
+ },
236
+ };
237
+ }
238
+ return toolCalls[index];
239
+ };
240
+ for await (const chunk of stream) {
241
+ const delta = chunk?.choices?.[0]?.delta || {};
242
+ const text = extractTextValue(delta.content ?? "");
243
+ if (text) {
244
+ content += text;
245
+ await outputMessage.streamTextDelta(text);
246
+ }
247
+ const reasoningDelta = extractTextValue(delta.reasoning_content ?? delta.reasoningContent ?? "");
248
+ if (reasoningDelta) {
249
+ reasoningContent += reasoningDelta;
250
+ }
251
+ if (Array.isArray(delta.tool_calls)) {
252
+ for (const item of delta.tool_calls) {
253
+ const indexValue = Number(item.index);
254
+ const index = Number.isFinite(indexValue) ? indexValue : 0;
255
+ const toolCall = ensureToolCall(index);
256
+ if (item.id)
257
+ toolCall.id = item.id;
258
+ if (item.type)
259
+ toolCall.type = item.type;
260
+ if (item.function?.name) {
261
+ toolCall.function.name += item.function.name;
262
+ }
263
+ if (item.function?.arguments) {
264
+ toolCall.function.arguments += item.function.arguments;
265
+ }
266
+ }
267
+ }
268
+ }
269
+ const normalizedToolCalls = toolCalls.filter((item) => item?.function?.name);
270
+ return {
271
+ choices: [
272
+ {
273
+ message: {
274
+ role: "assistant",
275
+ content,
276
+ reasoning_content: reasoningContent,
277
+ tool_calls: normalizedToolCalls.length
278
+ ? normalizedToolCalls
279
+ : undefined,
280
+ },
281
+ },
282
+ ],
283
+ };
284
+ };
179
285
  const systemMessage = {
180
286
  role: "system",
181
287
  content: cfg.systemPrompt + `\n性格约束:${cfg.config.systemPrompt}`,
@@ -196,18 +302,17 @@ const CApiReply = async (e) => {
196
302
  }
197
303
  const deepThoughtReasoning = getDeepThoughtReasoning(cfg);
198
304
  Object.assign(createParams, deepThoughtReasoning);
199
- createParams["stream"] = false;
305
+ if (!shouldStreamToOutput) {
306
+ createParams["stream"] = false;
307
+ }
200
308
  // log("请求AI,参数:", createParams);
201
- const stream = (await openai.chat.completions.create(createParams));
202
- await recordCapiContextStats(e.guid, cfg.config.model, messages, stream);
309
+ const stream = await requestChatCompletion(createParams, messages);
203
310
  log("AI回复原始数据:\n", JSON.stringify(stream, null, 2));
204
311
  let fullContent = "";
205
312
  let toolCalls = [];
206
313
  const completion = stream;
207
314
  const aiReplyMessage = completion?.choices?.[0]?.message;
208
- if (aiReplyMessage?.content) {
209
- fullContent = aiReplyMessage.content;
210
- }
315
+ fullContent = getToolCallDisplayContent(aiReplyMessage);
211
316
  if (Array.isArray(aiReplyMessage?.tool_calls)) {
212
317
  toolCalls = aiReplyMessage.tool_calls.map((tc) => ({
213
318
  id: tc.id || "",
@@ -261,8 +366,7 @@ const CApiReply = async (e) => {
261
366
  params.tool_choice = "auto";
262
367
  }
263
368
  Object.assign(params, getDeepThoughtReasoning(cfg));
264
- res = await openai.chat.completions.create(params);
265
- await recordCapiContextStats(e.guid, cfg.config.model, messages, res);
369
+ res = await requestChatCompletion(params, messages);
266
370
  if (!res.choices || res.choices.length === 0) {
267
371
  log("AI未返回内容");
268
372
  return;
@@ -274,7 +378,7 @@ const CApiReply = async (e) => {
274
378
  log("AI返回工具调用,内容:-----", res.choices[0]);
275
379
  // 先处理对话
276
380
  if (toolRound >= 10 && toolRound % 10 === 0) {
277
- await message.send(format(Text(`AI正在连续调用工具,第 ${toolRound} 轮处理中,请稍等`)));
381
+ await sendOutputText(message, `AI正在连续调用工具,第 ${toolRound} 轮处理中,请稍等`);
278
382
  }
279
383
  messages.push(responseMessage);
280
384
  log("模型决定调用工具:", responseMessage.tool_calls);
@@ -282,11 +386,14 @@ const CApiReply = async (e) => {
282
386
  const useToolsMessage = responseMessage.tool_calls.map((toolCall) => {
283
387
  return `工具调用: ${toolCall.function.name}(${cfg.toolConfig.toolPromptArgsSwitch === "1" ? toolCall.function.arguments : ""})`;
284
388
  });
285
- const msgId = await message.send(format(Text(useToolsMessage.join("\n"))));
389
+ const msgId = await sendOutputText(message, useToolsMessage.join("\n"));
286
390
  if (cfg.toolConfig.toolPromptRevokeSwitch === "1") {
287
391
  // 10秒后撤回工具调用提示
288
392
  setTimeout(() => {
289
- message.delete({ messageId: msgId[0].data.message_id });
393
+ const messageId = msgId?.[0]?.data?.message_id;
394
+ if (messageId && typeof message?.delete === "function") {
395
+ message.delete({ messageId });
396
+ }
290
397
  }, 10000);
291
398
  }
292
399
  }
@@ -333,6 +440,9 @@ const CApiReply = async (e) => {
333
440
  };
334
441
  }
335
442
  }
443
+ if (typeof message?.sendToolResult === "function") {
444
+ await message.sendToolResult(functionName, functionResult);
445
+ }
336
446
  const toolContent = JSON.stringify(typeof functionResult === "undefined"
337
447
  ? { ok: true, result: null }
338
448
  : functionResult);
@@ -366,8 +476,7 @@ const CApiReply = async (e) => {
366
476
  Object.assign(params, deepThoughtReasoning);
367
477
  // log("重新请求AI,参数:", params);
368
478
  // 重新请求
369
- res = await openai.chat.completions.create(params);
370
- await recordCapiContextStats(e.guid, cfg.config.model, messages, res);
479
+ res = await requestChatCompletion(params, messages);
371
480
  // 检查是否还有工具调用需要处理
372
481
  if (!res.choices || res.choices.length === 0) {
373
482
  log("AI未返回内容");
@@ -400,7 +509,7 @@ const CApiReply = async (e) => {
400
509
  catch (error) {
401
510
  await redisClient.setAIToolLoopProcessing(e.guid, false);
402
511
  console.error("AI回复出错", error);
403
- message.send(format(Text("AI回复出错了\n" + error)));
512
+ await sendOutputText(message, "AI回复出错了\n" + error);
404
513
  return {
405
514
  next: true,
406
515
  };
@@ -15,14 +15,14 @@ const drainPendingGuidanceInputs = async (guid) => {
15
15
  .map((item) => item?.rapiMessage || item)
16
16
  .filter(Boolean);
17
17
  };
18
- const RApiReply = async (e) => {
18
+ const RApiReply = async (e, outputMessage) => {
19
19
  const cfg = await getChatConfig(e);
20
20
  if (await shouldSkipAIReply(e, cfg)) {
21
21
  return {
22
22
  next: true,
23
23
  };
24
24
  }
25
- const [message] = useMessage();
25
+ const [message] = outputMessage ? [outputMessage] : useMessage(e);
26
26
  const openai = new OpenAi({
27
27
  baseURL: cfg.config.host,
28
28
  apiKey: cfg.config.key,
@@ -58,6 +58,9 @@ const RApiReply = async (e) => {
58
58
  if (event.type === "response.output_text.delta") {
59
59
  const text = event.delta ?? "";
60
60
  fullText += text;
61
+ if (text && typeof message?.streamTextDelta === "function") {
62
+ await message.streamTextDelta(text);
63
+ }
61
64
  }
62
65
  // 更稳妥:从 output_item.done 读取完整 function_call
63
66
  if (event.type === "response.output_item.done" &&
@@ -157,7 +160,11 @@ const RApiReply = async (e) => {
157
160
  let newText = "";
158
161
  for await (const event of toolResponse) {
159
162
  if (event.type === "response.output_text.delta") {
160
- newText += event.delta;
163
+ const text = event.delta ?? "";
164
+ newText += text;
165
+ if (text && typeof message?.streamTextDelta === "function") {
166
+ await message.streamTextDelta(text);
167
+ }
161
168
  }
162
169
  if (event.type === "response.completed") {
163
170
  previousResponseId = event.response.id;
@@ -1,4 +1,4 @@
1
- import { format, Audio, ImageURL, ImageFile, Text, Mention } from 'alemonjs';
1
+ import { Audio, ImageURL, ImageFile, Text, format, Mention } from 'alemonjs';
2
2
  import redisClient from '../../config.js';
3
3
  import { TTSClient } from '../../api/tts.js';
4
4
  import { createTTSMessage } from './tts.js';
@@ -128,6 +128,12 @@ const sendReplyMessages = async (replyMessages, e, cfg, message) => {
128
128
  await message.send(format(...components));
129
129
  }
130
130
  };
131
+ const sendParsedReplyToCustomOutput = async (message, aireply) => {
132
+ if (typeof message?.sendAIReply !== "function")
133
+ return false;
134
+ await message.sendAIReply(aireply);
135
+ return true;
136
+ };
131
137
  const parseAIReply = async (newText, cfg, e, message) => {
132
138
  const replyMessages = newText.split(new RegExp(`${cfg.botName}:`, "g"));
133
139
  const replyMessagesResult = [];
@@ -199,7 +205,7 @@ const parseAIOutput = (raw) => {
199
205
  const imgs = text.match(/!\[[^\]]+\]\([^\)]+\)/g) || [];
200
206
  if (imgs.length) {
201
207
  imgs.forEach((img) => {
202
- const urlMatch = img.match(/!\[[^\]]+\]\([^\)]+\)/);
208
+ const urlMatch = img.match(/!\[[^\]]+\]\(([^)]+)\)/);
203
209
  if (urlMatch) {
204
210
  result.push({ type: "image", url: urlMatch[1] });
205
211
  }
@@ -240,6 +246,10 @@ const parseAIOutput = (raw) => {
240
246
  return result;
241
247
  };
242
248
  const renderMessages = async (items, message) => {
249
+ if (typeof message?.sendRapiItems === "function") {
250
+ await message.sendRapiItems(items);
251
+ return;
252
+ }
243
253
  const components = [];
244
254
  for (const item of items) {
245
255
  switch (item.type) {
@@ -259,5 +269,6 @@ const renderMessages = async (items, message) => {
259
269
  }
260
270
  await message.send(format(...components));
261
271
  };
272
+ const sendAIReplyToOutput = sendParsedReplyToCustomOutput;
262
273
 
263
- export { handleTTS, parseAIOutput, parseAIReply, renderMessages, sendReplyMessages };
274
+ export { handleTTS, parseAIOutput, parseAIReply, renderMessages, sendAIReplyToOutput, sendReplyMessages };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alemonjs-aichat",
3
- "version": "1.0.39",
3
+ "version": "1.0.40",
4
4
  "description": "alemonjs-aichat",
5
5
  "author": "suancaixianyu",
6
6
  "license": "MIT",
@@ -24,7 +24,6 @@
24
24
  "@types/ssh2": "^1.15.5",
25
25
  "alemonjs": "2.1.65",
26
26
  "cssnano": "^7",
27
- "jsxp": "^1.4.0",
28
27
  "lvyjs": "^0.2.25",
29
28
  "pm2": "^5",
30
29
  "tailwindcss": "3"
@@ -49,11 +48,18 @@
49
48
  "@aws-sdk/client-s3": "^3.975.0",
50
49
  "@aws-sdk/s3-request-presigner": "^3.975.0",
51
50
  "ai": "^6.0.105",
51
+ "jsxp": "^1.4.0",
52
+ "koa": "^3.2.0",
53
+ "koa-router": "^14.0.0",
54
+ "koa-send": "^5.0.1",
55
+ "koa-static": "^5.0.0",
52
56
  "mime-types": "^3.0.2",
53
57
  "node-fetch": "^3.3.2",
54
58
  "openai": "^6.34.0",
59
+ "react": "^19.2.5",
60
+ "react-dom": "^19.2.5",
55
61
  "sharp": "^0.34.5",
56
- "zod": "^4.3.6",
57
- "ssh2": "^1.17.0"
62
+ "ssh2": "^1.17.0",
63
+ "zod": "^4.3.6"
58
64
  }
59
65
  }