alemonjs-aichat 1.0.34 → 1.0.37

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/config.js CHANGED
@@ -3,6 +3,8 @@ import fs from 'fs';
3
3
  import path from 'path';
4
4
  import { redis } from './redis.js';
5
5
 
6
+ const TOOL_LOOP_TTL_SECONDS = 600;
7
+ const TOOL_LOOP_STALE_MS = TOOL_LOOP_TTL_SECONDS * 1000;
6
8
  class db {
7
9
  redis;
8
10
  systemPrompt = "";
@@ -427,9 +429,50 @@ class db {
427
429
  for (const key of keys) {
428
430
  await this.redis.del(key);
429
431
  }
432
+ const contextStatsKeys = await this.redis.keys(`ai:context_stats:*`);
433
+ if (contextStatsKeys.length > 0) {
434
+ await this.redis.del(...contextStatsKeys);
435
+ }
436
+ await this.clearAITransientState();
430
437
  return;
431
438
  }
432
- await this.redis.del(`ai:history:${guid}`);
439
+ await this.clearAITransientState(guid);
440
+ await this.redis.del(`ai:history:${guid}`, `ai:context_stats:${guid}`);
441
+ }
442
+ /** 记录当前会话上下文和 token 使用情况 */
443
+ async recordAIContextStats(guid, input) {
444
+ const current = await this.getAIContextStats(guid);
445
+ const promptTokens = input.promptTokens ?? 0;
446
+ const completionTokens = input.completionTokens ?? 0;
447
+ const totalTokens = input.totalTokens ?? promptTokens + completionTokens;
448
+ const cachedTokens = input.cachedTokens ?? 0;
449
+ const cacheMissTokens = input.cacheMissTokens ?? Math.max(promptTokens - cachedTokens, 0);
450
+ const nextStats = {
451
+ ...input,
452
+ promptTokens: input.promptTokens ?? null,
453
+ completionTokens: input.completionTokens ?? null,
454
+ totalTokens: input.totalTokens ?? null,
455
+ cachedTokens: input.cachedTokens ?? null,
456
+ cacheMissTokens: input.cacheMissTokens ?? null,
457
+ guid,
458
+ requestCount: (current?.requestCount ?? 0) + 1,
459
+ updatedAt: Date.now(),
460
+ totalPromptTokens: (current?.totalPromptTokens ?? 0) + promptTokens,
461
+ totalCompletionTokens: (current?.totalCompletionTokens ?? 0) + completionTokens,
462
+ totalTokensUsed: (current?.totalTokensUsed ?? 0) + totalTokens,
463
+ totalCachedTokens: (current?.totalCachedTokens ?? 0) + cachedTokens,
464
+ totalCacheMissTokens: (current?.totalCacheMissTokens ?? 0) + cacheMissTokens,
465
+ };
466
+ await this.redis.set(`ai:context_stats:${guid}`, JSON.stringify(nextStats));
467
+ return nextStats;
468
+ }
469
+ /** 获取当前会话上下文和 token 使用情况 */
470
+ async getAIContextStats(guid) {
471
+ const statsStr = await this.redis.get(`ai:context_stats:${guid}`);
472
+ if (!statsStr) {
473
+ return null;
474
+ }
475
+ return JSON.parse(statsStr);
433
476
  }
434
477
  /** 清理最后的N条聊天记录 */
435
478
  async clearLastNChatHistory(guid, n) {
@@ -464,17 +507,72 @@ class db {
464
507
  await this.redis.set(this.getArchiveRecordKey(guid, id), JSON.stringify(record));
465
508
  return record;
466
509
  }
510
+ /** 清理工具循环等临时会话状态 */
511
+ async clearAITransientState(guid) {
512
+ if (!guid) {
513
+ const transientPrefixes = [
514
+ "ai:tool_loop_processing:",
515
+ "ai:guidance:",
516
+ "ai:session:",
517
+ ];
518
+ for (const prefix of transientPrefixes) {
519
+ const keys = await this.redis.keys(`${prefix}*`);
520
+ if (keys.length > 0) {
521
+ await this.redis.del(...keys);
522
+ }
523
+ }
524
+ return;
525
+ }
526
+ await this.redis.del(`ai:tool_loop_processing:${guid}`, `ai:guidance:${guid}`, `ai:session:${guid}`);
527
+ }
467
528
  /** 设置工具调用中的处理状态 */
468
529
  async setAIToolLoopProcessing(guid, processing) {
469
530
  if (processing) {
470
- await this.redis.set(`ai:tool_loop_processing:${guid}`, "1");
531
+ const now = Date.now();
532
+ const state = {
533
+ processing: true,
534
+ startedAt: now,
535
+ updatedAt: now,
536
+ };
537
+ await this.redis.set(`ai:tool_loop_processing:${guid}`, JSON.stringify(state), "EX", TOOL_LOOP_TTL_SECONDS);
471
538
  return;
472
539
  }
473
540
  await this.redis.del(`ai:tool_loop_processing:${guid}`);
474
541
  }
475
542
  /** 获取工具调用中的处理状态 */
476
543
  async getAIToolLoopProcessing(guid) {
477
- return (await this.redis.get(`ai:tool_loop_processing:${guid}`)) || "0";
544
+ const key = `ai:tool_loop_processing:${guid}`;
545
+ const processing = await this.redis.get(key);
546
+ if (!processing) {
547
+ return "0";
548
+ }
549
+ let state = null;
550
+ if (processing !== "1") {
551
+ try {
552
+ const parsed = JSON.parse(processing);
553
+ if (parsed?.processing === true) {
554
+ state = parsed;
555
+ }
556
+ }
557
+ catch {
558
+ await this.clearAITransientState(guid);
559
+ return "0";
560
+ }
561
+ const updatedAt = Number(state?.updatedAt || state?.startedAt || 0);
562
+ if (!updatedAt || Date.now() - updatedAt > TOOL_LOOP_STALE_MS) {
563
+ await this.clearAITransientState(guid);
564
+ return "0";
565
+ }
566
+ }
567
+ const ttl = await this.redis.ttl(key);
568
+ if (ttl === -1) {
569
+ if (processing === "1") {
570
+ await this.clearAITransientState(guid);
571
+ return "0";
572
+ }
573
+ await this.redis.expire(key, TOOL_LOOP_TTL_SECONDS);
574
+ }
575
+ return "1";
478
576
  }
479
577
  /** 获取工具调用期间追加的引导消息 */
480
578
  async getAIGuidanceMessages(guid) {
@@ -482,13 +580,20 @@ class db {
482
580
  if (!queueStr) {
483
581
  return [];
484
582
  }
485
- return JSON.parse(queueStr);
583
+ try {
584
+ const queue = JSON.parse(queueStr);
585
+ return Array.isArray(queue) ? queue : [];
586
+ }
587
+ catch {
588
+ await this.clearAIGuidanceMessages(guid);
589
+ return [];
590
+ }
486
591
  }
487
592
  /** 追加工具调用期间的引导消息 */
488
593
  async addAIGuidanceMessage(guid, data) {
489
594
  const queue = await this.getAIGuidanceMessages(guid);
490
595
  queue.push(data);
491
- await this.redis.set(`ai:guidance:${guid}`, JSON.stringify(queue));
596
+ await this.redis.set(`ai:guidance:${guid}`, JSON.stringify(queue), "EX", TOOL_LOOP_TTL_SECONDS);
492
597
  return queue;
493
598
  }
494
599
  /** 提取并清空工具调用期间的引导消息 */
@@ -1,7 +1,7 @@
1
1
  import redisClient from '../../config.js';
2
2
  import { loadSkills } from '../../api/loadSkill.js';
3
3
  import App from '../../image/conponent/AiConfig.js';
4
- import { useMessage, Image, Text } from 'alemonjs';
4
+ import { useMessage, Text, Image } from 'alemonjs';
5
5
  import { renderComponentIsHtmlToBuffer } from 'jsxp';
6
6
  import OpenAi from 'openai';
7
7
 
@@ -54,10 +54,58 @@ const archiveCurrentConversation = async (guid, reason, aiConfig) => {
54
54
  reason,
55
55
  });
56
56
  };
57
+ const formatTokenValue = (value) => {
58
+ return typeof value === "number" && Number.isFinite(value)
59
+ ? String(value)
60
+ : "未知";
61
+ };
62
+ const formatAIContextStatus = (stats) => {
63
+ if (!stats) {
64
+ return "当前会话还没有 CAPI 上下文统计。";
65
+ }
66
+ const updatedAt = new Date(stats.updatedAt).toLocaleString("zh-CN", {
67
+ hour12: false,
68
+ });
69
+ const usageNote = stats.usageAvailable
70
+ ? ""
71
+ : "\n提示: 最近一次请求未返回 usage, 实际 token 统计可能不完整。";
72
+ return [
73
+ "当前AI状态:",
74
+ `模式: ${stats.mode.toUpperCase()}`,
75
+ `模型: ${stats.model || "未知"}`,
76
+ `更新时间: ${updatedAt}`,
77
+ `请求次数: ${stats.requestCount}`,
78
+ "",
79
+ "最近一次上下文:",
80
+ `消息数: ${stats.contextMessageCount}`,
81
+ `上下文字符数: ${stats.contextCharLength}`,
82
+ `上下文估算token: ${stats.estimatedContextTokens}`,
83
+ `输入token: ${formatTokenValue(stats.promptTokens)}`,
84
+ `输出token: ${formatTokenValue(stats.completionTokens)}`,
85
+ `总token: ${formatTokenValue(stats.totalTokens)}`,
86
+ `缓存命中token: ${formatTokenValue(stats.cachedTokens)}`,
87
+ `缓存未命中token: ${formatTokenValue(stats.cacheMissTokens)}`,
88
+ "",
89
+ "当前会话累计:",
90
+ `输入token: ${stats.totalPromptTokens}`,
91
+ `输出token: ${stats.totalCompletionTokens}`,
92
+ `总token: ${stats.totalTokensUsed}`,
93
+ `缓存命中token: ${stats.totalCachedTokens}`,
94
+ `缓存未命中token: ${stats.totalCacheMissTokens}`,
95
+ usageNote,
96
+ ]
97
+ .filter((line) => line !== "")
98
+ .join("\n");
99
+ };
57
100
  var res = onResponse(selects, async (e, next) => {
58
101
  // 创建
59
102
  const [message] = useMessage(e);
60
103
  const config = await redisClient.getAIConfig(e.guid);
104
+ // 查看AI状态
105
+ if (/^(\/|#)ai状态$/i.test(e.msg)) {
106
+ const stats = await redisClient.getAIContextStats(e.guid);
107
+ message.send(format(Text(formatAIContextStatus(stats))));
108
+ }
61
109
  // 查看AI配置
62
110
  if (/^(\/|#)(ai配置|当前提示词|查看提示词)$/i.test(e.msg)) {
63
111
  const complexResponse = (await redisClient.getComplexOutput(e.guid)) || "1"; // 复杂输出
@@ -1,9 +1,9 @@
1
1
  var mw = onMiddleware(onSelects(["message.create", "private.message.create"]), async (event, next) => {
2
2
  if (event.Xianyu) {
3
- return true; // 继续执行后续的处理函数
3
+ return next(); // 普通聊天交给后续路由/文件处理,跳过命令子路由
4
4
  }
5
5
  else {
6
- return next(); // 跳过所有指令,继续执行后续的处理函数
6
+ return true; // 命令消息继续执行当前命令子路由
7
7
  }
8
8
  });
9
9
 
@@ -43,6 +43,56 @@ const appendPendingGuidanceMessages = async (guid, messages) => {
43
43
  await redisClient.addAIChatHistoryBatch(guid, guidanceMessages);
44
44
  return guidanceMessages;
45
45
  };
46
+ const getTextLength = (value) => {
47
+ if (typeof value === "string") {
48
+ return value.length;
49
+ }
50
+ if (Array.isArray(value)) {
51
+ return value.reduce((sum, item) => sum + getTextLength(item), 0);
52
+ }
53
+ if (value && typeof value === "object") {
54
+ return Object.values(value).reduce((sum, item) => sum + getTextLength(item), 0);
55
+ }
56
+ return 0;
57
+ };
58
+ const getNumber = (value) => {
59
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
60
+ };
61
+ const extractCachedTokens = (usage) => {
62
+ return getNumber(usage?.prompt_tokens_details?.cached_tokens ??
63
+ usage?.input_tokens_details?.cached_tokens ??
64
+ usage?.prompt_cache_hit_tokens);
65
+ };
66
+ const extractCacheMissTokens = (usage, promptTokens) => {
67
+ const explicitMissTokens = getNumber(usage?.prompt_cache_miss_tokens);
68
+ if (explicitMissTokens !== null) {
69
+ return explicitMissTokens;
70
+ }
71
+ const cachedTokens = extractCachedTokens(usage);
72
+ if (promptTokens !== null && cachedTokens !== null) {
73
+ return Math.max(promptTokens - cachedTokens, 0);
74
+ }
75
+ return null;
76
+ };
77
+ const recordCapiContextStats = async (guid, model, messages, completion) => {
78
+ const usage = completion?.usage;
79
+ const promptTokens = getNumber(usage?.prompt_tokens ?? usage?.input_tokens);
80
+ const completionTokens = getNumber(usage?.completion_tokens ?? usage?.output_tokens);
81
+ const contextCharLength = getTextLength(messages);
82
+ await redisClient.recordAIContextStats(guid, {
83
+ mode: "capi",
84
+ model,
85
+ contextMessageCount: messages.length,
86
+ contextCharLength,
87
+ estimatedContextTokens: Math.ceil(contextCharLength / 4),
88
+ promptTokens,
89
+ completionTokens,
90
+ totalTokens: getNumber(usage?.total_tokens),
91
+ cachedTokens: extractCachedTokens(usage),
92
+ cacheMissTokens: extractCacheMissTokens(usage, promptTokens),
93
+ usageAvailable: Boolean(usage),
94
+ });
95
+ };
46
96
  const CApiReply = async (e) => {
47
97
  console.log("e.UserId", e.UserId, e.bot);
48
98
  const cfg = await getChatConfig(e);
@@ -81,6 +131,7 @@ const CApiReply = async (e) => {
81
131
  createParams["stream"] = false;
82
132
  // log("请求AI,参数:", createParams);
83
133
  const stream = (await openai.chat.completions.create(createParams));
134
+ await recordCapiContextStats(e.guid, cfg.config.model, messages, stream);
84
135
  log("AI回复原始数据:\n", JSON.stringify(stream, null, 2));
85
136
  let fullContent = "";
86
137
  let toolCalls = [];
@@ -140,6 +191,7 @@ const CApiReply = async (e) => {
140
191
  }
141
192
  Object.assign(params, getDeepThoughtReasoning(cfg));
142
193
  res = await openai.chat.completions.create(params);
194
+ await recordCapiContextStats(e.guid, cfg.config.model, messages, res);
143
195
  if (!res.choices || res.choices.length === 0) {
144
196
  log("AI未返回内容");
145
197
  return;
@@ -243,6 +295,7 @@ const CApiReply = async (e) => {
243
295
  // log("重新请求AI,参数:", params);
244
296
  // 重新请求
245
297
  res = await openai.chat.completions.create(params);
298
+ await recordCapiContextStats(e.guid, cfg.config.model, messages, res);
246
299
  // 检查是否还有工具调用需要处理
247
300
  if (!res.choices || res.choices.length === 0) {
248
301
  log("AI未返回内容");
@@ -273,6 +326,7 @@ const CApiReply = async (e) => {
273
326
  };
274
327
  }
275
328
  catch (error) {
329
+ await redisClient.setAIToolLoopProcessing(e.guid, false);
276
330
  console.error("AI回复出错", error);
277
331
  message.send(format(Text("AI回复出错了\n" + error)));
278
332
  return {
@@ -14,7 +14,7 @@ const children = [
14
14
  },
15
15
  // config
16
16
  {
17
- regular: Regular.or(/^(\/|#)(ai配置|当前提示词|查看提示词)$/i, /^(\/|#)ai列表$/i, /^(\/|#)提示词列表$/i, /^(\/|#)清空对话$/i, /^(\/|#)清空(所有|全部)对话$/i, /^(\/|#)get (.+)$/i, /(\/|#)(开启)?(new|新对话|新会话)$/i),
17
+ regular: Regular.or(/^(\/|#)(ai配置|当前提示词|查看提示词)$/i, /^(\/|#)ai列表$/i, /^(\/|#)提示词列表$/i, /^(\/|#)清空对话$/i, /^(\/|#)清空(所有|全部)对话$/i, /^(\/|#)get (.+)$/i, /(\/|#)(开启)?(new|新对话|新会话)$/i, /^(\/|#)ai状态$/),
18
18
  handler: lazy(() => import('../response/config/res.js')),
19
19
  },
20
20
  // setting (包含大量子命令)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alemonjs-aichat",
3
- "version": "1.0.34",
3
+ "version": "1.0.37",
4
4
  "description": "alemonjs-aichat",
5
5
  "author": "suancaixianyu",
6
6
  "license": "MIT",