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 +110 -5
- package/lib/response/config/res.js +49 -1
- package/lib/response/mw.js +2 -2
- package/lib/response/zreply/capi.js +54 -0
- package/lib/routes/commands.js +1 -1
- package/package.json +1 -1
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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"; // 复杂输出
|
package/lib/response/mw.js
CHANGED
|
@@ -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
|
|
3
|
+
return next(); // 普通聊天交给后续路由/文件处理,跳过命令子路由
|
|
4
4
|
}
|
|
5
5
|
else {
|
|
6
|
-
return
|
|
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 {
|
package/lib/routes/commands.js
CHANGED
|
@@ -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 (包含大量子命令)
|