alemonjs-aichat 1.0.37 → 1.0.39

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
@@ -5,6 +5,7 @@ import { redis } from './redis.js';
5
5
 
6
6
  const TOOL_LOOP_TTL_SECONDS = 600;
7
7
  const TOOL_LOOP_STALE_MS = TOOL_LOOP_TTL_SECONDS * 1000;
8
+ const DEFAULT_CAPI_CONTEXT_LIMIT = 256000;
8
9
  class db {
9
10
  redis;
10
11
  systemPrompt = "";
@@ -207,6 +208,19 @@ class db {
207
208
  }
208
209
  /** --------------------------------------------------------- */
209
210
  /** 工具相关配置 */
211
+ /** 获取 CAPI 自动压缩上下文长度 */
212
+ async getCAPIContextLimit(guid) {
213
+ const raw = await this.redis.get(`ai:capi_context_limit:${guid}`);
214
+ const limit = Number(raw);
215
+ return Number.isFinite(limit) && limit > 0
216
+ ? Math.floor(limit)
217
+ : DEFAULT_CAPI_CONTEXT_LIMIT;
218
+ }
219
+ /** 设置 CAPI 自动压缩上下文长度 */
220
+ async setCAPIContextLimit(guid, limit) {
221
+ const normalized = Math.max(1, Math.floor(limit));
222
+ await this.redis.set(`ai:capi_context_limit:${guid}`, String(normalized));
223
+ }
210
224
  /** 获取工具开关状态 */
211
225
  async getToolSwitch(guid) {
212
226
  return (await this.redis.get(`ai:tool:switch:${guid}`)) || "1";
@@ -239,6 +253,14 @@ class db {
239
253
  async setToolPromptArgsSwitch(guid, enable) {
240
254
  await this.redis.set(`ai:tool_prompt_args:switch:${guid}`, enable ? "1" : "0");
241
255
  }
256
+ /** 工具调用前是否发送模型返回的 content */
257
+ async getToolCallContentSwitch(guid) {
258
+ return (await this.redis.get(`ai:tool_call_content:switch:${guid}`)) || "0";
259
+ }
260
+ /** 设置工具调用前 content 发送开关状态 */
261
+ async setToolCallContentSwitch(guid, enable) {
262
+ await this.redis.set(`ai:tool_call_content:switch:${guid}`, enable ? "1" : "0");
263
+ }
242
264
  /** --------------------------------------------------------- */
243
265
  /** 获取复杂输出开关状态 */
244
266
  async getComplexOutput(guid) {
@@ -1189,4 +1211,4 @@ class db {
1189
1211
  }
1190
1212
  const redisClient = new db(redis);
1191
1213
 
1192
- export { redisClient as default };
1214
+ export { DEFAULT_CAPI_CONTEXT_LIMIT, redisClient as default };
@@ -1,7 +1,7 @@
1
1
  var title = "IA聊天插件";
2
2
  var desc = "一个基于AI的聊天插件,支持多种AI模型和丰富的功能配置,适用于各种聊天场景。";
3
3
  var name = "alemonjs-aichat";
4
- var version = "v1.0.32";
4
+ var version = "v1.0.39";
5
5
  var by = "AlemonJS";
6
6
  var list = [
7
7
  {
@@ -136,6 +136,14 @@ var list = [
136
136
  {
137
137
  cmd: "/清空对话",
138
138
  desc: "清空当前页面的对话记录。"
139
+ },
140
+ {
141
+ cmd: "/设置AI上下文长度 [256k|1m]",
142
+ desc: "设置 CAPI 模式自动压缩上下文的长度,默认 256k。"
143
+ },
144
+ {
145
+ cmd: "/撤回",
146
+ desc: "引用一条消息发送撤回指令,自动撤回被引用的消息。"
139
147
  }
140
148
  ]
141
149
  }
@@ -200,6 +208,10 @@ var list = [
200
208
  {
201
209
  cmd: "/[开启|关闭]工具提示详情",
202
210
  desc: "开启或关闭工具调用提示详情功能。开启后工具调用的提示消息会包含更多的详情信息。"
211
+ },
212
+ {
213
+ cmd: "/[开启|关闭]工具调用内容",
214
+ desc: "开启或关闭工具调用前模型返回内容的发送。开启后会发送工具调用响应中 content 的内容。"
203
215
  }
204
216
  ]
205
217
  }
@@ -9,6 +9,8 @@ const cmds = [
9
9
  { cmd: "/切换模型 <模型名称>", desc: "切换当前使用的AI模型" },
10
10
  { cmd: "/<开启|关闭>仅艾特触发", desc: "开启或关闭仅艾特触发功能" },
11
11
  { cmd: "/<开启|关闭>工具", desc: "开启或关闭AI工具" },
12
+ { cmd: "/<开启|关闭>工具调用内容", desc: "开启或关闭工具调用前内容发送" },
13
+ { cmd: "/设置AI上下文长度 256k", desc: "设置CAPI自动压缩上下文长度" },
12
14
  { cmd: "/<开启|关闭>复杂输出", desc: "开启或关闭复杂输出" },
13
15
  { cmd: "/<开启|关闭>好感度", desc: "开启或关闭好感度系统" },
14
16
  { cmd: "/清空对话", desc: "清空当前对话历史" },
@@ -76,7 +78,11 @@ function App(data) {
76
78
  React.createElement(StatusItem, { label: "\u5DE5\u5177\u603B\u5F00\u5173", value: data.tools }),
77
79
  React.createElement(StatusItem, { label: "\u5DE5\u5177\u63D0\u793A", value: data.toolPromptSwitch }),
78
80
  React.createElement(StatusItem, { label: "\u5DE5\u5177\u63D0\u793A\u64A4\u56DE", value: data.toolPromptRevokeSwitch }),
79
- React.createElement(StatusItem, { label: "\u5DE5\u5177\u63D0\u793A\u4F20\u53C2", value: data.toolPromptArgsSwitch })),
81
+ React.createElement(StatusItem, { label: "\u5DE5\u5177\u63D0\u793A\u4F20\u53C2", value: data.toolPromptArgsSwitch }),
82
+ React.createElement(StatusItem, { label: "\u5DE5\u5177\u8C03\u7528\u5185\u5BB9", value: data.toolCallContentSwitch }),
83
+ React.createElement("div", { className: "flex items-center justify-between py-2 px-2 rounded-lg bg-white/5" },
84
+ React.createElement("span", { className: " text-white/70" }, "CAPI\u4E0A\u4E0B\u6587\u957F\u5EA6"),
85
+ React.createElement("span", { className: "px-3 py-0.5 rounded-full text-xs font-mono font-medium border bg-cyan-500/20 text-cyan-300 border-cyan-500/30" }, data.capiContextLimit))),
80
86
  React.createElement("div", { className: "pt-2 border-t border-white/10" },
81
87
  React.createElement("div", { className: "flex items-center justify-between py-2" },
82
88
  React.createElement("span", { className: " text-white/60" }, "\u5F53\u524D\u4F7F\u7528\u6A21\u578B"),
@@ -177,6 +177,7 @@ var mw = onMiddleware(selects, async (event, next) => {
177
177
  // 处理回复消息
178
178
  const replyData = event.value.message.find((item) => item.type === "reply");
179
179
  if (replyData) {
180
+ event["replyMessageId"] = String(replyData.data.id);
180
181
  const msg = await onebotClient.getMsg({
181
182
  message_id: Number(replyData.data.id),
182
183
  });
@@ -233,6 +234,7 @@ var mw = onMiddleware(selects, async (event, next) => {
233
234
  })
234
235
  .join("");
235
236
  if (event.replyId && event.replyId !== "-1") {
237
+ event["replyMessageId"] = String(event.replyId);
236
238
  const replyMsg = await client.getMessage(event.replyId);
237
239
  console.log("获取回复", replyMsg);
238
240
  if (replyMsg) {
@@ -276,6 +278,7 @@ var mw = onMiddleware(selects, async (event, next) => {
276
278
  ? [CDN_URL + event.value.fileMeta.url]
277
279
  : [];
278
280
  if (event.value.replyToId) {
281
+ event["replyMessageId"] = String(event.value.replyToId);
279
282
  const channelMessages = await client.request({
280
283
  method: "GET",
281
284
  url: `/channels/${event.ChannelId}/messages/${event.value.replyToId}`,
@@ -54,44 +54,60 @@ const archiveCurrentConversation = async (guid, reason, aiConfig) => {
54
54
  reason,
55
55
  });
56
56
  };
57
- const formatTokenValue = (value) => {
57
+ const statusNumberFormatter = new Intl.NumberFormat("zh-CN");
58
+ const formatNumberValue = (value) => {
58
59
  return typeof value === "number" && Number.isFinite(value)
59
- ? String(value)
60
+ ? statusNumberFormatter.format(value)
60
61
  : "未知";
61
62
  };
62
- const formatAIContextStatus = (stats) => {
63
+ const formatTokenValue = (value) => {
64
+ return formatNumberValue(value);
65
+ };
66
+ const formatContextLimit = (tokens) => {
67
+ if (tokens >= 1000000 && tokens % 1000000 === 0) {
68
+ return `${tokens / 1000000}m`;
69
+ }
70
+ if (tokens >= 1000 && tokens % 1000 === 0) {
71
+ return `${tokens / 1000}k`;
72
+ }
73
+ return `${tokens}`;
74
+ };
75
+ const formatAIContextStatus = (stats, capiContextLimit) => {
63
76
  if (!stats) {
64
- return "当前会话还没有 CAPI 上下文统计。";
77
+ return `当前会话还没有 CAPI 上下文统计。\nCAPI上下文长度: ${formatContextLimit(capiContextLimit)}`;
65
78
  }
66
79
  const updatedAt = new Date(stats.updatedAt).toLocaleString("zh-CN", {
67
80
  hour12: false,
68
81
  });
69
82
  const usageNote = stats.usageAvailable
70
83
  ? ""
71
- : "\n提示: 最近一次请求未返回 usage, 实际 token 统计可能不完整。";
84
+ : "提示: 最近一次请求未返回 usage, 实际 token 统计可能不完整。";
85
+ const divider = "················";
72
86
  return [
73
87
  "当前AI状态:",
88
+ divider,
74
89
  `模式: ${stats.mode.toUpperCase()}`,
75
90
  `模型: ${stats.model || "未知"}`,
76
91
  `更新时间: ${updatedAt}`,
77
- `请求次数: ${stats.requestCount}`,
78
- "",
92
+ `请求次数: ${formatNumberValue(stats.requestCount)}`,
93
+ divider,
79
94
  "最近一次上下文:",
80
- `消息数: ${stats.contextMessageCount}`,
81
- `上下文字符数: ${stats.contextCharLength}`,
82
- `上下文估算token: ${stats.estimatedContextTokens}`,
95
+ `CAPI上下文长度: ${formatContextLimit(capiContextLimit)}`,
96
+ `消息数: ${formatNumberValue(stats.contextMessageCount)}`,
97
+ `上下文字符数: ${formatNumberValue(stats.contextCharLength)}`,
98
+ `上下文估算token: ${formatTokenValue(stats.estimatedContextTokens)}`,
83
99
  `输入token: ${formatTokenValue(stats.promptTokens)}`,
84
100
  `输出token: ${formatTokenValue(stats.completionTokens)}`,
85
101
  `总token: ${formatTokenValue(stats.totalTokens)}`,
86
102
  `缓存命中token: ${formatTokenValue(stats.cachedTokens)}`,
87
103
  `缓存未命中token: ${formatTokenValue(stats.cacheMissTokens)}`,
88
- "",
104
+ divider,
89
105
  "当前会话累计:",
90
- `输入token: ${stats.totalPromptTokens}`,
91
- `输出token: ${stats.totalCompletionTokens}`,
92
- `总token: ${stats.totalTokensUsed}`,
93
- `缓存命中token: ${stats.totalCachedTokens}`,
94
- `缓存未命中token: ${stats.totalCacheMissTokens}`,
106
+ `输入token: ${formatTokenValue(stats.totalPromptTokens)}`,
107
+ `输出token: ${formatTokenValue(stats.totalCompletionTokens)}`,
108
+ `总token: ${formatTokenValue(stats.totalTokensUsed)}`,
109
+ `缓存命中token: ${formatTokenValue(stats.totalCachedTokens)}`,
110
+ `缓存未命中token: ${formatTokenValue(stats.totalCacheMissTokens)}`,
95
111
  usageNote,
96
112
  ]
97
113
  .filter((line) => line !== "")
@@ -104,7 +120,8 @@ var res = onResponse(selects, async (e, next) => {
104
120
  // 查看AI状态
105
121
  if (/^(\/|#)ai状态$/i.test(e.msg)) {
106
122
  const stats = await redisClient.getAIContextStats(e.guid);
107
- message.send(format(Text(formatAIContextStatus(stats))));
123
+ const capiContextLimit = await redisClient.getCAPIContextLimit(e.guid);
124
+ message.send(format(Text(formatAIContextStatus(stats, capiContextLimit))));
108
125
  }
109
126
  // 查看AI配置
110
127
  if (/^(\/|#)(ai配置|当前提示词|查看提示词)$/i.test(e.msg)) {
@@ -121,6 +138,8 @@ var res = onResponse(selects, async (e, next) => {
121
138
  const toolPromptSwitch = await redisClient.getToolPromptSwitch(e.guid); // 工具提示开关
122
139
  const toolPromptRevokeSwitch = await redisClient.getToolPromptRevokeSwitch(e.guid); // 工具提示撤回开关
123
140
  const toolPromptArgsSwitch = await redisClient.getToolPromptArgsSwitch(e.guid); // 工具提示传参开关
141
+ const toolCallContentSwitch = await redisClient.getToolCallContentSwitch(e.guid); // 工具调用内容发送开关
142
+ const capiContextLimit = await redisClient.getCAPIContextLimit(e.guid);
124
143
  // 发送消息
125
144
  try {
126
145
  const scale = await redisClient.getRenderPrecision(e.guid);
@@ -141,6 +160,8 @@ var res = onResponse(selects, async (e, next) => {
141
160
  toolPromptSwitch: toolPromptSwitch == "1",
142
161
  toolPromptRevokeSwitch: toolPromptRevokeSwitch == "1",
143
162
  toolPromptArgsSwitch: toolPromptArgsSwitch == "1",
163
+ toolCallContentSwitch: toolCallContentSwitch == "1",
164
+ capiContextLimit: formatContextLimit(capiContextLimit),
144
165
  }, {
145
166
  playwright: {
146
167
  context: { deviceScaleFactor: scale },
@@ -150,11 +171,11 @@ var res = onResponse(selects, async (e, next) => {
150
171
  message.send(format(Image(img)));
151
172
  }
152
173
  else {
153
- message.send(format(Text(`当前AI配置:\n`), Text(`总开关: ${config.model != "" ? "开启" : "关闭"}\n`), Text(`工具: ${toolsIsOpen == "1" ? "开启" : "关闭"}\n`), Text(`复杂输出: ${complexResponse == "1" ? "开启" : "关闭"}\n`), Text(`好感度开关: ${affectionIsOpen == "1" ? "开启" : "关闭"}\n`), Text(`TTS回复开关: ${isOpenTTSReply == "1" ? "开启" : "关闭"}\n`), Text(`深度思考: ${deepThinkingIsOpen == "1" ? "默认" : "关闭"}\n`), Text(`仅艾特触发: ${atTriggerSwitch == "1" ? "开启" : "关闭"}\n`), Text(`模型: ${config.model || "未设置"}\n`), Text(`提示词: ${config.systemPrompt || "未设置"}\n`), Text(`当前AI: ${currentAI || "未设置"}\n`), Text(`AI数量: ${aiList.length ? aiList.length : "暂无AI配置"}`)));
174
+ message.send(format(Text(`当前AI配置:\n`), Text(`总开关: ${config.model != "" ? "开启" : "关闭"}\n`), Text(`工具: ${toolsIsOpen == "1" ? "开启" : "关闭"}\n`), Text(`工具调用内容: ${toolCallContentSwitch == "1" ? "开启" : "关闭"}\n`), Text(`CAPI上下文长度: ${formatContextLimit(capiContextLimit)}\n`), Text(`复杂输出: ${complexResponse == "1" ? "开启" : "关闭"}\n`), Text(`好感度开关: ${affectionIsOpen == "1" ? "开启" : "关闭"}\n`), Text(`TTS回复开关: ${isOpenTTSReply == "1" ? "开启" : "关闭"}\n`), Text(`深度思考: ${deepThinkingIsOpen == "1" ? "默认" : "关闭"}\n`), Text(`仅艾特触发: ${atTriggerSwitch == "1" ? "开启" : "关闭"}\n`), Text(`模型: ${config.model || "未设置"}\n`), Text(`提示词: ${config.systemPrompt || "未设置"}\n`), Text(`当前AI: ${currentAI || "未设置"}\n`), Text(`AI数量: ${aiList.length ? aiList.length : "暂无AI配置"}`)));
154
175
  }
155
176
  }
156
177
  catch (error) {
157
- message.send(format(Text(`当前AI配置:\n`), Text(`总开关: ${config.model != "" ? "开启" : "关闭"}\n`), Text(`工具: ${toolsIsOpen == "1" ? "开启" : "关闭"}\n`), Text(`复杂输出: ${complexResponse == "1" ? "开启" : "关闭"}\n`), Text(`好感度开关: ${affectionIsOpen == "1" ? "开启" : "关闭"}\n`), Text(`TTS回复开关: ${isOpenTTSReply == "1" ? "开启" : "关闭"}\n`), Text(`深度思考: ${deepThinkingIsOpen == "1" ? "默认" : "关闭"}\n`), Text(`仅艾特触发: ${atTriggerSwitch == "1" ? "开启" : "关闭"}\n`), Text(`模型: ${config.model || "未设置"}\n`), Text(`提示词: ${config.systemPrompt || "未设置"}\n`), Text(`当前AI: ${currentAI || "未设置"}\n`), Text(`AI数量: ${aiList.length ? aiList.length : "暂无AI配置"}`)));
178
+ message.send(format(Text(`当前AI配置:\n`), Text(`总开关: ${config.model != "" ? "开启" : "关闭"}\n`), Text(`工具: ${toolsIsOpen == "1" ? "开启" : "关闭"}\n`), Text(`工具调用内容: ${toolCallContentSwitch == "1" ? "开启" : "关闭"}\n`), Text(`CAPI上下文长度: ${formatContextLimit(capiContextLimit)}\n`), Text(`复杂输出: ${complexResponse == "1" ? "开启" : "关闭"}\n`), Text(`好感度开关: ${affectionIsOpen == "1" ? "开启" : "关闭"}\n`), Text(`TTS回复开关: ${isOpenTTSReply == "1" ? "开启" : "关闭"}\n`), Text(`深度思考: ${deepThinkingIsOpen == "1" ? "默认" : "关闭"}\n`), Text(`仅艾特触发: ${atTriggerSwitch == "1" ? "开启" : "关闭"}\n`), Text(`模型: ${config.model || "未设置"}\n`), Text(`提示词: ${config.systemPrompt || "未设置"}\n`), Text(`当前AI: ${currentAI || "未设置"}\n`), Text(`AI数量: ${aiList.length ? aiList.length : "暂无AI配置"}`)));
158
179
  }
159
180
  }
160
181
  // 查看AI列表
@@ -0,0 +1,34 @@
1
+ import { useMessage, Text } from 'alemonjs';
2
+
3
+ const selects = onSelects(["message.create", "private.message.create"]);
4
+ const normalizeMessageId = (value) => {
5
+ if (value === undefined || value === null)
6
+ return undefined;
7
+ const text = String(value).trim();
8
+ return text === "" || text === "-1" ? undefined : text;
9
+ };
10
+ const getReplyMessageId = (e) => {
11
+ const replySegment = Array.isArray(e.value?.message)
12
+ ? e.value.message.find((item) => item?.type === "reply")
13
+ : undefined;
14
+ return (normalizeMessageId(e.replyMessageId) ||
15
+ normalizeMessageId(replySegment?.data?.id) ||
16
+ normalizeMessageId(e.replyId) ||
17
+ normalizeMessageId(e.value?.replyToId) ||
18
+ normalizeMessageId(e.reply?.message_id) ||
19
+ normalizeMessageId(e.reply?.id));
20
+ };
21
+ var res = onResponse(selects, async (e) => {
22
+ const [message] = useMessage(e);
23
+ const targetMessageId = getReplyMessageId(e);
24
+ if (!targetMessageId) {
25
+ await message.send(format(Text("请引用一条消息后再发送 /撤回")));
26
+ return;
27
+ }
28
+ const result = await message.delete({ messageId: targetMessageId });
29
+ if (result.code !== 2000) {
30
+ await message.send(format(Text("撤回失败,可能是平台不支持或机器人没有权限")));
31
+ }
32
+ });
33
+
34
+ export { res as default, selects };
@@ -8,6 +8,21 @@ const value = getConfigValue().aiChat || {};
8
8
  const selects = onSelects(["message.create", "private.message.create"]);
9
9
  // inline regex checks are used in-place; keep `test` as shorthand
10
10
  const test = /(\/|#)测试$/i;
11
+ const capiContextLimitCommand = /^(#|\/)设置(AI)?上下文(长度)? ?([0-9]+)(k|m)$/i;
12
+ const formatContextLimit = (tokens) => {
13
+ if (tokens >= 1000000 && tokens % 1000000 === 0) {
14
+ return `${tokens / 1000000}m`;
15
+ }
16
+ if (tokens >= 1000 && tokens % 1000 === 0) {
17
+ return `${tokens / 1000}k`;
18
+ }
19
+ return `${tokens}`;
20
+ };
21
+ const parseContextLimit = (value, unit) => {
22
+ const count = Number(value);
23
+ const multiplier = unit.toLowerCase() === "m" ? 1000000 : 1000;
24
+ return Math.floor(count * multiplier);
25
+ };
11
26
  var res = onResponse(selects, async (e, next) => {
12
27
  // 创建
13
28
  const [message] = useMessage(e);
@@ -281,6 +296,23 @@ var res = onResponse(selects, async (e, next) => {
281
296
  message.send(format(Text(`已将当前 AI 的模型修改为 ${modelName.trim()} !`)));
282
297
  return;
283
298
  }
299
+ // 设置 CAPI 自动压缩上下文长度
300
+ if (capiContextLimitCommand.test(e.msg)) {
301
+ const match = e.msg.match(capiContextLimitCommand);
302
+ if (!match) {
303
+ message.send(format(Text("格式错误,请按照 格式:/设置AI上下文长度 256k 进行设置")));
304
+ return;
305
+ }
306
+ const [, , , , value, unit] = match;
307
+ const limit = parseContextLimit(value, unit);
308
+ if (!Number.isFinite(limit) || limit <= 0) {
309
+ message.send(format(Text("上下文长度必须大于 0,例如:/设置AI上下文长度 256k")));
310
+ return;
311
+ }
312
+ await redisClient.setCAPIContextLimit(e.guid, limit);
313
+ message.send(format(Text(`已设置 CAPI 上下文长度为 ${formatContextLimit(limit)},接近 90% 时会自动压缩上下文。`)));
314
+ return;
315
+ }
284
316
  // 设置复杂输出开关
285
317
  if (/(\/|#)(关闭|开启)复杂(输出|回复)$/i.test(e.msg)) {
286
318
  const match = e.msg.match(/(\/|#)(关闭|开启)复杂(输出|回复)$/i);
@@ -493,6 +525,22 @@ var res = onResponse(selects, async (e, next) => {
493
525
  message.send(format(Text(`已${enable ? "开启" : "关闭"}工具提示详情功能 !`)));
494
526
  return;
495
527
  }
528
+ // 控制工具调用前 content 发送开关
529
+ if (/(\/|#)(开启|关闭)工具调用内容$/i.test(e.msg)) {
530
+ if (!e.IsMaster) {
531
+ return;
532
+ }
533
+ const match = e.msg.match(/(\/|#)(开启|关闭)工具调用内容$/i);
534
+ if (!match) {
535
+ message.send(format(Text("格式错误,请按照 格式:/开启工具调用内容 或 /关闭工具调用内容 进行设置")));
536
+ return;
537
+ }
538
+ const [, , action] = match;
539
+ const enable = action === "开启";
540
+ await redisClient.setToolCallContentSwitch(e.guid, enable);
541
+ message.send(format(Text(`已${enable ? "开启" : "关闭"}工具调用内容发送功能 !`)));
542
+ return;
543
+ }
496
544
  // 控制工具开关
497
545
  if (/(\/|#)(开启|关闭)工具(.*)$/i.test(e.msg)) {
498
546
  if (!e.IsMaster) {
@@ -3,13 +3,16 @@ import { log } from 'console';
3
3
  import OpenAi from 'openai';
4
4
  import { TTSClient } from '../../api/tts.js';
5
5
  import { availableTools } from '../../api/aitools.js';
6
- import redisClient from '../../config.js';
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
9
  import { parseAIReply } from './tools.js';
10
10
 
11
11
  const selects = onSelects(["message.create", "private.message.create"]);
12
12
  let currentModel = "派蒙-默认"; // 默认音色
13
+ const CAPI_CONTEXT_COMPRESSION_RATIO = 0.9;
14
+ const CAPI_CONTEXT_RETAIN_RATIO = 0.35;
15
+ const AUTO_SUMMARY_DETAIL_MAX_LENGTH = 2000;
13
16
  const sendAIReply = async (reply, cfg, e, message) => {
14
17
  const aireply = await parseAIReply(reply, cfg, e, message);
15
18
  console.log("aireply", aireply);
@@ -55,6 +58,69 @@ const getTextLength = (value) => {
55
58
  }
56
59
  return 0;
57
60
  };
61
+ const estimateContextTokens = (value) => {
62
+ return Math.ceil(getTextLength(value) / 4);
63
+ };
64
+ const trimText = (text, maxLength) => {
65
+ const normalized = text.replace(/\s+/g, " ").trim();
66
+ if (normalized.length <= maxLength) {
67
+ return normalized;
68
+ }
69
+ return `${normalized.slice(0, maxLength)}...`;
70
+ };
71
+ const takeTailMessagesWithinTokenBudget = (history, tokenBudget) => {
72
+ const kept = [];
73
+ for (let index = history.length - 1; index >= 0; index -= 1) {
74
+ kept.unshift(history[index]);
75
+ if (estimateContextTokens(kept) > tokenBudget) {
76
+ kept.shift();
77
+ break;
78
+ }
79
+ }
80
+ return kept;
81
+ };
82
+ const buildAutoSummaryMessage = (record) => {
83
+ const summary = record?.summary || "较早对话已自动归档";
84
+ const detail = trimText(record?.detail || record?.transcript || "无详细摘要", AUTO_SUMMARY_DETAIL_MAX_LENGTH);
85
+ return {
86
+ role: "system",
87
+ content: [
88
+ "【自动压缩上下文】",
89
+ `较早对话已归档为 ${record?.id || "未知ID"},当前上下文只保留摘要和最近消息。`,
90
+ `摘要: ${summary}`,
91
+ `详细信息: ${detail}`,
92
+ "如需更完整内容,可按归档ID或关键词调用 MemoryOperation 检索历史归档。",
93
+ ].join("\n"),
94
+ };
95
+ };
96
+ const prepareCapiMessagesForRequest = async (guid, systemMessage, currentMessages) => {
97
+ const limit = await redisClient.getCAPIContextLimit(guid);
98
+ const threshold = Math.floor(limit * CAPI_CONTEXT_COMPRESSION_RATIO);
99
+ const estimatedTokens = estimateContextTokens(currentMessages);
100
+ if (estimatedTokens < threshold) {
101
+ return currentMessages;
102
+ }
103
+ const history = await redisClient.getAIChatHistory(guid);
104
+ if (history.length === 0) {
105
+ return currentMessages;
106
+ }
107
+ const archived = await redisClient.archiveAIChatHistory(guid, {
108
+ reason: "auto_context_compress",
109
+ });
110
+ if (!archived) {
111
+ return currentMessages;
112
+ }
113
+ const minRetainBudget = Math.min(Math.floor(DEFAULT_CAPI_CONTEXT_LIMIT * 0.05), Math.floor(limit * 0.5));
114
+ const retainBudget = Math.max(Math.floor(limit * CAPI_CONTEXT_RETAIN_RATIO), minRetainBudget);
115
+ const keptHistory = takeTailMessagesWithinTokenBudget(history, retainBudget);
116
+ const compressedHistory = [buildAutoSummaryMessage(archived), ...keptHistory];
117
+ await redisClient.setAIChatHistory(guid, compressedHistory);
118
+ const rebuiltMessages = [systemMessage, ...compressedHistory];
119
+ const pendingMessages = currentMessages.slice(Math.max(1 + history.length, 1));
120
+ rebuiltMessages.push(...pendingMessages);
121
+ log(`CAPI上下文接近上限,已自动压缩:${estimatedTokens}/${limit} tokens,归档ID:${archived.id}`);
122
+ return rebuiltMessages;
123
+ };
58
124
  const getNumber = (value) => {
59
125
  return typeof value === "number" && Number.isFinite(value) ? value : null;
60
126
  };
@@ -84,7 +150,7 @@ const recordCapiContextStats = async (guid, model, messages, completion) => {
84
150
  model,
85
151
  contextMessageCount: messages.length,
86
152
  contextCharLength,
87
- estimatedContextTokens: Math.ceil(contextCharLength / 4),
153
+ estimatedContextTokens: estimateContextTokens(messages),
88
154
  promptTokens,
89
155
  completionTokens,
90
156
  totalTokens: getNumber(usage?.total_tokens),
@@ -110,14 +176,16 @@ const CApiReply = async (e) => {
110
176
  apiKey: cfg.config.key,
111
177
  timeout: 300000,
112
178
  });
179
+ const systemMessage = {
180
+ role: "system",
181
+ content: cfg.systemPrompt + `\n性格约束:${cfg.config.systemPrompt}`,
182
+ };
113
183
  let messages = [
114
- {
115
- role: "system",
116
- content: cfg.systemPrompt + `\n性格约束:${cfg.config.systemPrompt}`,
117
- },
184
+ systemMessage,
118
185
  ...cfg.historyMessages,
119
186
  usermessage,
120
187
  ];
188
+ messages = await prepareCapiMessagesForRequest(e.guid, systemMessage, messages);
121
189
  const createParams = {
122
190
  model: cfg.config.model,
123
191
  messages: messages,
@@ -165,7 +233,9 @@ const CApiReply = async (e) => {
165
233
  return;
166
234
  }
167
235
  await redisClient.addAIChatHistory(e.guid, usermessage);
168
- if (res.choices[0].message?.tool_calls && fullContent.trim() !== "") {
236
+ if (cfg.toolConfig.toolCallContentSwitch === "1" &&
237
+ res.choices[0].message?.tool_calls &&
238
+ fullContent.trim() !== "") {
169
239
  await sendAIReply(fullContent, cfg, e, message);
170
240
  }
171
241
  // 检查是否有工具调用需要处理
@@ -180,6 +250,7 @@ const CApiReply = async (e) => {
180
250
  if (pendingGuidanceMessages.length === 0) {
181
251
  break;
182
252
  }
253
+ messages = await prepareCapiMessagesForRequest(e.guid, systemMessage, messages);
183
254
  log("工具调用完成后收到补充引导,继续请求模型:", pendingGuidanceMessages.length);
184
255
  const params = {
185
256
  model: cfg.config.model,
@@ -284,6 +355,7 @@ const CApiReply = async (e) => {
284
355
  if (guidanceMessages.length > 0) {
285
356
  log("工具调用期间收到补充引导:", guidanceMessages.length);
286
357
  }
358
+ messages = await prepareCapiMessagesForRequest(e.guid, systemMessage, messages);
287
359
  const params = {
288
360
  model: cfg.config.model,
289
361
  messages: messages,
@@ -60,6 +60,8 @@ const getChatConfig = async (e) => {
60
60
  const atTriggerSwitch = await redisClient.getAtTriggerSwitch(e.guid);
61
61
  /** 历史消息 */
62
62
  const historyMessages = await redisClient.getAIChatHistory(e.guid);
63
+ /** CAPI 自动压缩上下文长度 */
64
+ const capiContextLimit = await redisClient.getCAPIContextLimit(e.guid);
63
65
  /** 好感度信息 */
64
66
  const affections = await redisClient.getAffectionLevelAll(e.guid);
65
67
  /** 工具相关配置 */
@@ -75,6 +77,8 @@ const getChatConfig = async (e) => {
75
77
  toolPromptRevokeSwitch: await redisClient.getToolPromptRevokeSwitch(e.guid),
76
78
  /** 工具提示传参开关 */
77
79
  toolPromptArgsSwitch: await redisClient.getToolPromptArgsSwitch(e.guid),
80
+ /** 工具调用前 content 发送开关 */
81
+ toolCallContentSwitch: await redisClient.getToolCallContentSwitch(e.guid),
78
82
  };
79
83
  /** 技能列表 */
80
84
  const skills = loadSkills();
@@ -83,33 +87,34 @@ const getChatConfig = async (e) => {
83
87
  /** 机器人昵称 */
84
88
  const botName = e.bot.nickname || "小咸鱼";
85
89
  const memoryPrompt = relevantMemoryContext
86
- ? `
87
- ## 系统预检索记忆
88
- 以下内容是系统根据当前话题自动检索到的相关长期记忆和历史归档, 回答时优先参考与当前话题直接相关的部分; 如果与用户本轮最新表述冲突, 以用户本轮消息为准
89
- ${relevantMemoryContext}
90
+ ? `
91
+ ## 系统预检索记忆
92
+ 以下内容是系统根据当前话题自动检索到的相关长期记忆和历史归档, 回答时优先参考与当前话题直接相关的部分; 如果与用户本轮最新表述冲突, 以用户本轮消息为准
93
+ ${relevantMemoryContext}
90
94
  `
91
95
  : "";
92
96
  /** 系统提示词 */
93
- const systemPrompt = `
94
- # AI助手回复规范
95
- 请严格按照以下规范进行回复, 不要添加任何不必要的内容, 也不要删除或修改规范中的任何内容, 以确保回复能够被正确解析和处理
96
-
97
- ## 技能
98
- 在遇到用户需要执行特定操作时,先获取对应的技能,如果有可用技能,获取该技能,并严格按照技能文档要求的格式调用, 不要添加任何多余的内容, 也不要删除或修改规范中的任何内容, 以确保技能能够被正确解析和处理
99
- 当没有技能可以应对需求时, 就自行挑选合适的工具函数来完成用户的需求
100
- 如果用户提到本地仓库、知识库、参考项目、之前克隆过的项目,或者你准备说"本地没有"、"仓库里没有"、"没找到"之类的话,必须先检查 public/{guid} 内现有内容,优先搜索 knowledge/ 目录;至少先调用一次 AgentSearchFiles、AgentListFiles 或读取相关技能后再下结论
101
- 如果你不知道之前项目所在的 guid,先调用 AgentListWorkspaces 找到 public/ 下已有工作目录,再继续用 AgentListFiles、AgentSearchFiles 或 AgentReadFileLines;不要用 exec 查 public/{guid},因为 exec 的默认工作目录不是那里
102
- 如果用户要查看分支、确认当前分支、查看远端或切换分支,优先使用 AgentGitOperation,不要自己拼 git commit、push、pull、merge、rebase、reset 等命令
103
- 如果用户在继续之前的话题、问你记不记得、提到上次/刚才/之前聊过的内容,先参考“系统预检索记忆”;如果系统预检索内容还不够,再调用 MemoryOperation,优先使用 chatHistory search:关键词 或 guid:id 去补充历史上下文,然后再回答
104
-
105
- 调用执行脚本类型的工具时, 务必确保脚本安全, 不得执行对服务器有伤害的脚本
106
- ---
107
- 技能列表:
108
- ${skills.map((skill) => `- ${skill.name}: ${skill.description}`).join("\n")}
109
- ---
110
- ${memoryPrompt}
111
-
112
- ## 关于回复格式:
97
+ const systemPrompt = `
98
+ # AI助手回复规范
99
+ 请严格按照以下规范进行回复, 不要添加任何不必要的内容, 也不要删除或修改规范中的任何内容, 以确保回复能够被正确解析和处理
100
+
101
+ ## 技能
102
+ 在遇到用户需要执行特定操作时,先获取对应的技能,如果有可用技能,获取该技能,并严格按照技能文档要求的格式调用, 不要添加任何多余的内容, 也不要删除或修改规范中的任何内容, 以确保技能能够被正确解析和处理
103
+ 当没有技能可以应对需求时, 就自行挑选合适的工具函数来完成用户的需求
104
+ 如果用户提到本地仓库、知识库、参考项目、之前克隆过的项目,或者你准备说"本地没有"、"仓库里没有"、"没找到"之类的话,必须先检查 public/{guid} 内现有内容,优先搜索 knowledge/ 目录;至少先调用一次 AgentSearchFiles、AgentListFiles 或读取相关技能后再下结论
105
+ 如果你不知道之前项目所在的 guid,先调用 AgentListWorkspaces 找到 public/ 下已有工作目录,再继续用 AgentListFiles、AgentSearchFiles 或 AgentReadFileLines;不要用 exec 查 public/{guid},因为 exec 的默认工作目录不是那里
106
+ 如果用户需要直接执行服务器运维指令、查看服务状态、管理 Docker/PM2/systemctl、查看日志或部署服务,使用 RunServerCommand;它会自动审核指令安全,不要改用 exec。调用 RunServerCommand 时 reviewGuid 必填,必须传下方环境信息里的“当前群号(reviewGuid)”,不要传 public/{guid} 工作目录ID
107
+ 如果用户要查看分支、确认当前分支、查看远端或切换分支,优先使用 AgentGitOperation,不要自己拼 git commit、push、pull、merge、rebase、reset 等命令
108
+ 如果用户在继续之前的话题、问你记不记得、提到上次/刚才/之前聊过的内容,先参考“系统预检索记忆”;如果系统预检索内容还不够,再调用 MemoryOperation,优先使用 chatHistory 的 search:关键词 或 guid:id 去补充历史上下文,然后再回答
109
+
110
+ 调用执行脚本类型的工具时, 务必确保脚本安全, 不得执行对服务器有伤害的脚本
111
+ ---
112
+ 技能列表:
113
+ ${skills.map((skill) => `- ${skill.name}: ${skill.description}`).join("\n")}
114
+ ---
115
+ ${memoryPrompt}
116
+
117
+ ## 关于回复格式:
113
118
  ### 用户发言
114
119
  - 格式: [私聊]用户昵称(用户id)(发送时间):消息内容
115
120
  当场景为私聊时, 用户昵称前方会出现[私聊]标识
@@ -158,15 +163,15 @@ const getChatConfig = async (e) => {
158
163
  #### 基础状态信息:
159
164
  这里的信息会实时变化,根据需要进行获取使用
160
165
  当前群时间:${getGroupTimeString()}
161
- 当前群号:${e.guid}
166
+ 当前群号(reviewGuid):${e.guid}
162
167
  当前群名称:${e.GroupName || "无"}
163
168
  当前聊天平台:${e.Platform}
164
169
  当前框架:alemonjs-aichat
165
170
  ${e.ClientError ? `当前错误信息:${e.ClientError}` : ""}
166
171
  `;
167
- const RapiSystemPrompt = `
168
- 请严格按照以下规范进行回复, 以确保回复能够被正确解析和处理
169
- 1. 文本消息:
172
+ const RapiSystemPrompt = `
173
+ 请严格按照以下规范进行回复, 以确保回复能够被正确解析和处理
174
+ 1. 文本消息:
170
175
  ${botName}::text<<<EOF
171
176
  文本内容
172
177
  EOF
@@ -185,14 +190,14 @@ ${botName}::audio voice=音色 内容
185
190
  =====================
186
191
 
187
192
  - 不允许使用任何其他格式
188
- - 不允许输出解释说明
189
- - 不允许夹杂 markdown
190
- - tool调用前可以先输出一行说明
191
- - 每一条输出必须独立一行
192
- - 允许多段输出叠加, 例如:
193
- ${botName}::text 这是第一条消息
194
- ${botName}::text 这是第二条消息
195
- ${memoryPrompt ? `\n【系统预检索记忆】\n${relevantMemoryContext}\n` : ""}
193
+ - 不允许输出解释说明
194
+ - 不允许夹杂 markdown
195
+ - tool调用前可以先输出一行说明
196
+ - 每一条输出必须独立一行
197
+ - 允许多段输出叠加, 例如:
198
+ ${botName}::text 这是第一条消息
199
+ ${botName}::text 这是第二条消息
200
+ ${memoryPrompt ? `\n【系统预检索记忆】\n${relevantMemoryContext}\n` : ""}
196
201
  `;
197
202
  return {
198
203
  /** AI配置 */
@@ -220,6 +225,8 @@ ${memoryPrompt ? `\n【系统预检索记忆】\n${relevantMemoryContext}\n` : "
220
225
  skills,
221
226
  /** 历史消息 */
222
227
  historyMessages,
228
+ /** CAPI 自动压缩上下文长度 */
229
+ capiContextLimit,
223
230
  /** 好感度信息 */
224
231
  affections,
225
232
  /** 工具配置 */
@@ -1,4 +1,4 @@
1
- import { format, Audio, Mention, Text, ImageURL, ImageFile } from 'alemonjs';
1
+ import { format, Audio, ImageURL, ImageFile, Text, 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';