koishi-plugin-satori-ai 1.4.0-beta.2 → 1.4.2

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/index.d.ts CHANGED
@@ -63,5 +63,6 @@ export declare class SAT extends Sat {
63
63
  handleRandomMiddleware(session: Session): Promise<string>;
64
64
  handleNickNameMiddleware(session: Session, prompt: string): Promise<string>;
65
65
  handleChannelMemoryManager(session: Session): Promise<void>;
66
+ dispose(): Promise<void>;
66
67
  }
67
68
  export default SAT;
package/lib/index.js CHANGED
@@ -272,20 +272,20 @@ function filterResponse(prompt, words, options) {
272
272
  const applyTagFilter = options?.applyTagFilter ?? true;
273
273
  let working = prompt;
274
274
  if (applyBracketFilter) {
275
- const parts = working.split(/([(\[【《(][^))]*[)\]】》)])/g);
275
+ const parts = working.split(/([(\[【《(][^))]{0,500}[)\]】》)])/g);
276
276
  const filtered = parts.map((part) => {
277
277
  if (part.startsWith("(") && part.endsWith(")") || part.startsWith("(") && part.endsWith(")") || part.startsWith("[") && part.endsWith("]") || part.startsWith("【") && part.endsWith("】") || part.startsWith("《") && part.endsWith("》")) {
278
278
  return words.some((word) => part.includes(word)) ? "" : part;
279
279
  }
280
280
  return part;
281
- }).join("");
281
+ }).join("…");
282
282
  working = filtered.replace(/\s+/g, "…");
283
283
  if (!working) {
284
284
  working = prompt;
285
285
  }
286
286
  }
287
287
  if (!applyTagFilter) {
288
- return working ? { content: working, error: false } : { content: "有点问题,请重置对话", error: true };
288
+ return working ? { content: working, error: false } : { content: "返回内容为空,请重置对话", error: true };
289
289
  }
290
290
  const pTagRegex = /<p>[\s\S]*?<\/p>/g;
291
291
  const doubaoRegex = /<doubaothinking>[\s\S]*?<\/doubaothinking>/g;
@@ -302,11 +302,11 @@ function filterResponse(prompt, words, options) {
302
302
  combined = answerTagMatches.map((s) => s.replace(/<answer>/i, "").replace(/<\/answer>/i, "")).join(",");
303
303
  }
304
304
  if (!combined) {
305
- return { content: "有点问题,请重置对话", error: true };
305
+ return { content: "内容抽取失败,请重置对话", error: true };
306
306
  }
307
307
  const cleanupRegex = /<p>|<\/p>|<doubaothinking>|<\/doubaothinking>|<answer>|<\/answer>|<br>|<\/br>|<br\/>/gi;
308
308
  const cleanedContent = combined.replace(cleanupRegex, "").trim();
309
- return cleanedContent ? { content: cleanedContent, error: false } : { content: "有点问题,请重置对话", error: true };
309
+ return cleanedContent ? { content: cleanedContent, error: false } : { content: "标签过滤失败,请重置对话", error: true };
310
310
  }
311
311
  __name(filterResponse, "filterResponse");
312
312
  function addOutputCensor(session, word, baseURL) {
@@ -417,7 +417,16 @@ async function wrapInHTML(str, width = 20) {
417
417
  <div class="satori-text">${str.replaceAll(/\n/g, "<br/>")}</div>
418
418
  </body>
419
419
  </html>`;
420
- return puppeteer.render(html);
420
+ try {
421
+ const renderPromise = puppeteer.render(html);
422
+ const timeoutPromise = new Promise(
423
+ (_, reject) => setTimeout(() => reject(new Error("Puppeteer 渲染超时")), 3e4)
424
+ );
425
+ return await Promise.race([renderPromise, timeoutPromise]);
426
+ } catch (error) {
427
+ logger.error(`Puppeteer 渲染失败: ${error}`);
428
+ return "渲染失败,请联系管理员";
429
+ }
421
430
  }
422
431
  __name(wrapInHTML, "wrapInHTML");
423
432
 
@@ -539,6 +548,7 @@ var APIClient = class {
539
548
  messages,
540
549
  temperature: this.config.temperature,
541
550
  top_p: 1,
551
+ max_tokens: this.config.max_output_tokens,
542
552
  frequency_penalty: this.config.frequency_penalty,
543
553
  presence_penalty: this.config.presence_penalty
544
554
  };
@@ -570,11 +580,8 @@ var APIClient = class {
570
580
  if (reasoning_content != "无" && content && content.length > reasoning_content.length) {
571
581
  logger2.warn("返回内容疑似包含推理内容");
572
582
  }
573
- const responseMsg = { role: "assistant", content };
574
- if (payload.messages.some((msg) => msg === responseMsg) && content.length > 5) {
575
- const lastMsg = payload.messages[payload.messages.length - 1];
576
- payload.messages[payload.messages.length - 1].content = lastMsg.content + "(注意不要重复之前的内容)";
577
- logger2.warn(`返回内容与之前内容相同,重试(第${i}次)中...`);
583
+ if (response.usage.completion_tokens > this.config.max_output_tokens) {
584
+ logger2.warn("返回内容过长导致被截断");
578
585
  continue;
579
586
  }
580
587
  return { content, error: false, reasoning_content };
@@ -587,7 +594,7 @@ var APIClient = class {
587
594
  continue;
588
595
  }
589
596
  }
590
- return { content: "unreachable", error: true };
597
+ return { content: "请求失败,请重试", error: true };
591
598
  }
592
599
  // 生成请求头
593
600
  createHeaders(keys) {
@@ -658,7 +665,6 @@ var MemoryManager = class {
658
665
  channelDialogues = /* @__PURE__ */ new Map();
659
666
  charactersToRemove = ["的", "一", "是", "了", "什", "么", "我", "谁", "不", "人", "在", "他", "有", "这", "个", "上", "们", "来", "到", "时", "大", "地", "为", "子", "中", "你", "说", "生", "国", "年", "着", "就", "那", "和", "要", "她", "出", "也", "得", "里", "后", "自", "以", "会", "id="];
660
667
  MAX_MEMORY_LENGTH = 5e3;
661
- userMemoryCache = /* @__PURE__ */ new Map();
662
668
  // 更新记忆
663
669
  async updateMemories(session, prompt, config, response) {
664
670
  if (response.error)
@@ -792,10 +798,10 @@ var MemoryManager = class {
792
798
  if (updated.length > this.MAX_MEMORY_LENGTH) {
793
799
  updated = updated.slice(-this.MAX_MEMORY_LENGTH);
794
800
  }
795
- fs2.writeFileSync(filePath, JSON.stringify(updated, null, 2));
801
+ await fs2.promises.writeFile(filePath, JSON.stringify(updated, null, 2), "utf-8");
796
802
  }
797
803
  // 记忆检索
798
- async searchMemories(session, prompt, type = "user") {
804
+ async searchMemories(session, type = "user") {
799
805
  const filePathMap = {
800
806
  "user": this.getUserMemoryPath(session.userId),
801
807
  "common": path2.join(this.config.dataDir, "common_sense.txt"),
@@ -811,14 +817,16 @@ var MemoryManager = class {
811
817
  if (!fs2.existsSync(filePath)) {
812
818
  return "";
813
819
  }
814
- const keywords = prompt.split("").filter((word) => !this.charactersToRemove.includes(word));
820
+ const channelId = this.config.personal_memory ? session.userId : session.channelId;
821
+ const recentMemories = this.getChannelContext(channelId);
822
+ const userDialogues = recentMemories.filter((entry) => entry.role === "user").map((entry) => entry.content).join("");
823
+ const keywords = userDialogues.split("").filter((word) => !this.charactersToRemove.includes(word));
815
824
  let entries = await this.loadMemoryFile(filePath);
816
825
  const matched = this.findBestMatches(entries, keywords).slice(0, topNMap[type] * 5);
817
- this.userMemoryCache.set(session.userId, matched);
818
826
  if (type === "user") {
819
827
  const remainingEntries = entries.filter((entry) => !matched.includes(entry));
820
828
  let updatedEntries = [...remainingEntries, ...matched];
821
- fs2.writeFileSync(filePath, JSON.stringify(updatedEntries, null, 2));
829
+ await fs2.promises.writeFile(filePath, JSON.stringify(updatedEntries, null, 2), "utf-8");
822
830
  }
823
831
  const result = this.formatMatches(matched, type, topNMap[type]);
824
832
  return result;
@@ -885,15 +893,16 @@ ${matched.map((entry) => entry.content).join("\n")}
885
893
  }
886
894
  // 确保记忆文件存在
887
895
  async ensureMemoryFile(filePath) {
888
- fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
896
+ await fs2.promises.mkdir(path2.dirname(filePath), { recursive: true });
889
897
  if (!fs2.existsSync(filePath)) {
890
- fs2.writeFileSync(filePath, "[]", "utf-8");
898
+ await fs2.promises.writeFile(filePath, "[]", "utf-8");
891
899
  }
892
900
  }
893
901
  // 加载记忆文件
894
902
  async loadMemoryFile(filePath) {
895
903
  try {
896
- return JSON.parse(fs2.readFileSync(filePath, "utf-8"));
904
+ const content = await fs2.promises.readFile(filePath, "utf-8");
905
+ return JSON.parse(content);
897
906
  } catch {
898
907
  return [];
899
908
  }
@@ -902,10 +911,6 @@ ${matched.map((entry) => entry.content).join("\n")}
902
911
  getChannelContext(channelId) {
903
912
  return this.channelMemories.get(channelId)?.dialogues || [];
904
913
  }
905
- // 获取用户缓存记忆
906
- getUserCachedMemory(userId) {
907
- return this.userMemoryCache.get(userId) || [];
908
- }
909
914
  };
910
915
 
911
916
  // src/fixed-dialogues.ts
@@ -1265,6 +1270,7 @@ var Sat = class extends import_koishi5.Service {
1265
1270
  authority: import_koishi5.Schema.number().role("slider").min(0).max(5).step(1).description("允许使用的最低权限").default(1),
1266
1271
  no_system_prompt: import_koishi5.Schema.boolean().default(false).description("是否将系统提示的system替换为user(用于具有思维链的模型)"),
1267
1272
  max_tokens: import_koishi5.Schema.number().description("最大请求长度(字符数)").default(100),
1273
+ max_output_tokens: import_koishi5.Schema.number().description("最大返回长度(不含思维链)(token数)").default(4096),
1268
1274
  message_max_length: import_koishi5.Schema.number().description("最大频道上下文长度(条数)").default(10),
1269
1275
  temperature: import_koishi5.Schema.number().role("slider").min(0).max(2).step(0.01).default(0.5).description("温度"),
1270
1276
  frequency_penalty: import_koishi5.Schema.number().default(0).description("频率惩罚"),
@@ -1463,8 +1469,7 @@ var UserPortraitManager = class {
1463
1469
  1. 基本信息(性别、年龄、生日等)
1464
1470
  2. 生活习惯(作息等)
1465
1471
  3. 反感点(如有)
1466
- 4. 对AI的期望(从对话历史中推断)
1467
- 5. 用户希望记住的信息(如有)
1472
+ 4. 近期可能需要记住的信息(如有)
1468
1473
 
1469
1474
  用户名:${user.usersname}
1470
1475
  你与用户的关系:${level}
@@ -1490,8 +1495,7 @@ ${history}
1490
1495
  1. 基本信息(性别、年龄、生日等,不要遗漏或改动已有的信息)
1491
1496
  2. 生活习惯(作息等)
1492
1497
  3. 反感点(如有)
1493
- 4. 对AI的期望(从对话历史中推断)
1494
- 5. 用户希望记住的信息(如有)
1498
+ 4. 近期可能需要记住的信息(如有)
1495
1499
 
1496
1500
  用户名:${user.usersname}
1497
1501
  你与用户的关系:${level}
@@ -1506,6 +1510,7 @@ ${NewPortrait}
1506
1510
  ·根据已有画像,补充新信息,避免重复,并且不要遗漏你认为重要的信息
1507
1511
  ·新信息中声称未知的内容,如果已有画像中有相关信息,请优先保留已有画像的信息,尤其是基本信息部分
1508
1512
  ·你认为不太重要的信息可以删除
1513
+ ·近期可能需要记住的信息最多保留最后三条
1509
1514
  ·使用尽可能简洁的表达
1510
1515
  ·保留不确定性的表述(如"可能"、"似乎"、"用户自称")
1511
1516
  ·保持中立和客观,避免带有个人情感色彩的描述,不要添加评价或建议
@@ -3422,11 +3427,13 @@ var Galgame = class {
3422
3427
  return JSON.parse(fs6.readFileSync(filePath, "utf-8"));
3423
3428
  }
3424
3429
  async sendStoryContent(session, content) {
3430
+ const username = session.username || "你";
3431
+ const storyText = String(content.text).replace(/\{?_USERNAME_\}?/g, username);
3425
3432
  if (content.isNarration) {
3426
- const wrappedText = await wrapInHTML(content.text, 40);
3433
+ const wrappedText = await wrapInHTML(storyText, 25);
3427
3434
  await session.send(wrappedText);
3428
3435
  } else {
3429
- await session.send(content.text);
3436
+ await session.send(storyText);
3430
3437
  }
3431
3438
  if (content.imageName) {
3432
3439
  const imagePath = path6.resolve(this.config.dataDir, "event", content.imageName);
@@ -3434,6 +3441,7 @@ var Galgame = class {
3434
3441
  const buffer = fs6.readFileSync(imagePath);
3435
3442
  const ext = path6.extname(imagePath).toLowerCase();
3436
3443
  const mime = ext === ".png" ? "image/png" : ext === ".webp" ? "image/webp" : "image/jpeg";
3444
+ await new Promise((resolve3) => setTimeout(resolve3, 1e3));
3437
3445
  await session.send(import_koishi14.h.image(buffer, mime));
3438
3446
  }
3439
3447
  }
@@ -3443,9 +3451,9 @@ var Galgame = class {
3443
3451
  // 然后在阅读时间上加上额外 3 秒缓冲,最终向上取整并转为毫秒。
3444
3452
  // 对中英文混合文本:中文按汉字计数,拉丁文按空格分词计数。
3445
3453
  computeAutoAdvanceDelay(text) {
3446
- const readingWpm = 200;
3454
+ const readingWpm = 500;
3447
3455
  if (!text)
3448
- return 4e3;
3456
+ return 1e3;
3449
3457
  const cjkMatch = text.match(/[\u4e00-\u9fff]/g) || [];
3450
3458
  const cjkCount = cjkMatch.length;
3451
3459
  const withoutCjk = text.replace(/[\u4e00-\u9fff]/g, "");
@@ -3453,7 +3461,7 @@ var Galgame = class {
3453
3461
  const effectiveWords = cjkCount + latinWords;
3454
3462
  const wordsPerSecond = readingWpm / 60;
3455
3463
  const readingSeconds = effectiveWords / Math.max(wordsPerSecond, 1e-3);
3456
- const delaySeconds = Math.max(1, Math.ceil(readingSeconds) + 3);
3464
+ const delaySeconds = Math.max(1, Math.ceil(readingSeconds) + 2);
3457
3465
  return delaySeconds * 1e3;
3458
3466
  }
3459
3467
  async handleEventProgress(session, event) {
@@ -3471,9 +3479,19 @@ var Galgame = class {
3471
3479
  delete this.ongoingEvents[userId];
3472
3480
  delete this.currentId[userId];
3473
3481
  this.completedEvents[userId] = event.eventName;
3482
+ try {
3483
+ const portraitPath = path6.join(this.config.dataDir, "UserPortrait", `${userId}.txt`);
3484
+ if (fs6.existsSync(portraitPath)) {
3485
+ const appendText = `
3486
+ 近期你和他进行了约会:${event.eventName}`;
3487
+ fs6.appendFileSync(portraitPath, appendText, "utf-8");
3488
+ }
3489
+ } catch (e) {
3490
+ }
3474
3491
  return;
3475
3492
  }
3476
3493
  if (content.options) {
3494
+ await new Promise((resolve3) => setTimeout(resolve3, 1e3));
3477
3495
  const optionsText = content.options.map((opt, idx) => `选项 ${idx + 1}: ${opt.text}`).join("\n");
3478
3496
  await session.send(optionsText + '\n发送格式类似 "选项 1" 来进行选择');
3479
3497
  delete this.ongoingEvents[userId];
@@ -3489,10 +3507,10 @@ var Galgame = class {
3489
3507
  async handleEnding(session, ending) {
3490
3508
  const rewardParts = [];
3491
3509
  if (ending.favorabilityReward) {
3492
- rewardParts.push(`好感度 +${ending.favorabilityReward}`);
3510
+ rewardParts.push(`好感度 ${ending.favorabilityReward}`);
3493
3511
  }
3494
3512
  if (ending.pReward) {
3495
- rewardParts.push(`P点 +${ending.pReward}`);
3513
+ rewardParts.push(`P点 ${ending.pReward}`);
3496
3514
  }
3497
3515
  if (ending.itemRewards) {
3498
3516
  for (const [name, qty] of Object.entries(ending.itemRewards)) {
@@ -3503,6 +3521,7 @@ var Galgame = class {
3503
3521
 
3504
3522
  获得奖励:
3505
3523
  ${rewardParts.join("\n")}` : "";
3524
+ await new Promise((resolve3) => setTimeout(resolve3, 1e3));
3506
3525
  await session.send(ending.text + rewardText);
3507
3526
  if (ending.image) {
3508
3527
  const imagePath = path6.resolve(this.config.dataDir, "event", ending.image);
@@ -3510,6 +3529,7 @@ ${rewardParts.join("\n")}` : "";
3510
3529
  const buffer = fs6.readFileSync(imagePath);
3511
3530
  const ext = path6.extname(imagePath).toLowerCase();
3512
3531
  const mime = ext === ".png" ? "image/png" : ext === ".webp" ? "image/webp" : "image/jpeg";
3532
+ await new Promise((resolve3) => setTimeout(resolve3, 1e3));
3513
3533
  await session.send(import_koishi14.h.image(buffer, mime));
3514
3534
  }
3515
3535
  }
@@ -3560,7 +3580,7 @@ ${rewardParts.join("\n")}` : "";
3560
3580
  }
3561
3581
  const events = fs6.readdirSync(eventDir).filter((file) => file.endsWith(".json")).map((file) => this.loadEvent(file.replace(".json", ""))).filter((event2) => event2 && user.favorability >= event2.favorabilityThreshold).map((event2) => event2.eventName);
3562
3582
  const result = "当前可用的约会事件有:\n" + events.join("\n") + "\n请输入命令:约会 [事件名] 来开始对应的事件。";
3563
- const wrappedText = await wrapInHTML(result);
3583
+ const wrappedText = await wrapInHTML(result, 25);
3564
3584
  await session.send(wrappedText);
3565
3585
  return;
3566
3586
  }
@@ -3661,7 +3681,7 @@ story 为有序的段落数组,每项至少需要 id 和 text。
3661
3681
  },
3662
3682
  {
3663
3683
  id: 4,
3664
- text: "现在进入分支环节:请选择你的回应。\n发送格式:选项 1 或 选项 2。",
3684
+ text: "此段展示分支和用户名占位,现在进入分支环节:请选择_USERNAME_的回应。\n发送格式:选项 1 或 选项 2。",
3665
3685
  options: [
3666
3686
  { text: "友好回应", targetId: 5 },
3667
3687
  { text: "冷淡回应", targetId: 6 }
@@ -3747,6 +3767,17 @@ var SAT = class extends Sat {
3747
3767
  this.galgame = new Galgame(ctx, config);
3748
3768
  this.registerGalgameCommands(ctx);
3749
3769
  }
3770
+ process.on("exit", () => {
3771
+ void this.dispose();
3772
+ });
3773
+ process.on("SIGINT", () => {
3774
+ void this.dispose();
3775
+ process.exit();
3776
+ });
3777
+ process.on("SIGTERM", () => {
3778
+ void this.dispose();
3779
+ process.exit();
3780
+ });
3750
3781
  }
3751
3782
  static {
3752
3783
  __name(this, "SAT");
@@ -3807,7 +3838,8 @@ var SAT = class extends Sat {
3807
3838
  temperature: this.config.temperature,
3808
3839
  frequency_penalty: this.config.frequency_penalty,
3809
3840
  presence_penalty: this.config.presence_penalty,
3810
- reasoning_content: this.config.log_reasoning_content
3841
+ reasoning_content: this.config.log_reasoning_content,
3842
+ max_output_tokens: this.config.max_output_tokens
3811
3843
  };
3812
3844
  }
3813
3845
  getMemoryConfig() {
@@ -3934,7 +3966,6 @@ var SAT = class extends Sat {
3934
3966
  const dialogueCountCheck = await this.checkUserDialogueCount(session, user);
3935
3967
  if (dialogueCountCheck)
3936
3968
  return dialogueCountCheck;
3937
- await this.updateChannelParallelCount(session, 1);
3938
3969
  const response = await this.generateResponse(session, processedPrompt);
3939
3970
  const auxiliaryResult = await this.handleAuxiliaryDialogue(session, processedPrompt, response);
3940
3971
  await this.memoryManager.updateMemories(session, processedPrompt, this.getMemoryConfig(), response);
@@ -4032,7 +4063,8 @@ var SAT = class extends Sat {
4032
4063
  }
4033
4064
  // 更新频道并发数
4034
4065
  async updateChannelParallelCount(session, value) {
4035
- this.onlineUsers.push(session.userId);
4066
+ if (value > 0 && !this.onlineUsers.includes(session.userId))
4067
+ this.onlineUsers.push(session.userId);
4036
4068
  this.ChannelParallelCount.set(session.channelId, (this.ChannelParallelCount.get(session.channelId) || 0) + value);
4037
4069
  }
4038
4070
  // 获取频道并发数
@@ -4058,27 +4090,39 @@ var SAT = class extends Sat {
4058
4090
  if (this.getChannelParallelCount(session) > this.config.max_parallel_count) {
4059
4091
  logger13.info(`频道 ${session.channelId} 并发数过高(${this.getChannelParallelCount(session)}),${session.username}等待中...`);
4060
4092
  }
4061
- while (this.getChannelParallelCount(session) > this.config.max_parallel_count) {
4062
- this.updateChannelParallelCount(session, -1);
4093
+ let waitTimes = 0;
4094
+ while (this.getChannelParallelCount(session) >= this.config.max_parallel_count) {
4095
+ waitTimes++;
4096
+ if (waitTimes > 60) {
4097
+ logger13.error(`并发等待超时:channel=${session.channelId}`);
4098
+ return { content: "系统繁忙,请稍后再试", error: true };
4099
+ }
4063
4100
  await new Promise((resolve3) => setTimeout(resolve3, 1e3));
4064
- this.updateChannelParallelCount(session, 1);
4065
4101
  }
4066
- const messages = await this.buildMessages(session, prompt);
4067
- logger13.info(`频道 ${session.channelId} 处理:${session.userId},剩余${this.getChannelParallelCount(session)}并发`);
4068
- const user = await getUser(this.ctx, session.userId);
4069
- let response = await this.getChatResponse(user, messages, prompt);
4070
- if (response.error)
4071
- response = await this.getChatResponse(user, messages, prompt);
4072
- if (response.error)
4073
- updateUserUsage(this.ctx, user, -1);
4074
- return response;
4102
+ await this.updateChannelParallelCount(session, 1);
4103
+ try {
4104
+ logger13.info(`频道 ${session.channelId} 处理:${session.userId},剩余${this.getChannelParallelCount(session)}并发`);
4105
+ const messages = await this.buildMessages(session, prompt);
4106
+ const user = await getUser(this.ctx, session.userId);
4107
+ let response = await this.getChatResponse(user, messages, prompt);
4108
+ if (response.error)
4109
+ response = await this.getChatResponse(user, messages, prompt);
4110
+ if (response.error)
4111
+ updateUserUsage(this.ctx, user, -1);
4112
+ return response;
4113
+ } finally {
4114
+ this.onlineUsers = this.onlineUsers.filter((id) => id !== session.userId);
4115
+ await this.updateChannelParallelCount(session, -1);
4116
+ }
4075
4117
  }
4076
4118
  // 获取聊天回复
4077
4119
  async getChatResponse(user, messages, prompt) {
4078
4120
  const hasTicket = user?.items?.["地灵殿通行证"]?.description && user.items["地灵殿通行证"].description === "on";
4079
4121
  const maxLength = hasTicket ? user?.items?.["地灵殿通行证"]?.metadata?.use_not_reasoner_LLM_length : this.config.use_not_reasoner_LLM_length;
4080
4122
  const useNoReasoner = prompt.length <= maxLength && this.config.enable_reasoner_like;
4123
+ const startTime = Date.now();
4081
4124
  let response = await this.apiClient.chat(user, messages);
4125
+ logger13.info(`API 调用耗时: ${Date.now() - startTime}ms`);
4082
4126
  if (this.config.log_ask_response) {
4083
4127
  if (this.config.enable_favorability && this.config.enable_mood)
4084
4128
  logger13.info(`Satori AI:(心情值:${this.moodManager.getMoodValue(user.userid)})${response.content}`);
@@ -4086,10 +4130,15 @@ var SAT = class extends Sat {
4086
4130
  logger13.info(`Satori AI:${response.content}`);
4087
4131
  }
4088
4132
  if (!response.error) {
4133
+ const filterStartTime = Date.now();
4089
4134
  response.content = filterResponse(response.content, this.config.reasoner_filter_word.split("-"), {
4090
4135
  applyBracketFilter: this.config.reasoner_filter,
4091
4136
  applyTagFilter: useNoReasoner || this.config.enhanceReasoningProtection
4092
4137
  }).content;
4138
+ const filterTime = Date.now() - filterStartTime;
4139
+ if (filterTime > 1e3) {
4140
+ logger13.warn(`filterResponse 耗时异常: ${filterTime}ms, 内容长度: ${response.content?.length || 0}`);
4141
+ }
4093
4142
  }
4094
4143
  return response;
4095
4144
  }
@@ -4126,10 +4175,10 @@ ${payload}`);
4126
4175
  }
4127
4176
  // 构建系统提示
4128
4177
  async buildSystemPrompt(session, prompt) {
4129
- const commonSense = await this.memoryManager.searchMemories(session, prompt, "common");
4130
- const groupSense = await this.memoryManager.searchMemories(session, prompt + session.username, "group");
4178
+ const commonSense = await this.memoryManager.searchMemories(session, "common");
4179
+ const groupSense = await this.memoryManager.searchMemories(session, "group");
4131
4180
  const channelDialogue = await this.memoryManager.getChannelDialogue(session);
4132
- const userMemory = await this.memoryManager.searchMemories(session, prompt, "user");
4181
+ const userMemory = await this.memoryManager.searchMemories(session, "user");
4133
4182
  const weatherPrompt = await this.weatherManager.buildWeatherPrompt(session);
4134
4183
  const user = await getUser(this.ctx, session.userId);
4135
4184
  const moodLevel = this.moodManager.getMoodLevel(user.userid);
@@ -4203,7 +4252,9 @@ ${payload}`);
4203
4252
  #参考信息到此为止,接下来是思考要求
4204
4253
  #请你在回复时先进行分析思考,并且模仿思维链的模式输出思考内容,${reasonerPrompt};
4205
4254
  #你在思考时必须以 "<think>" 开头, "</think>" 结尾。仔细揣摩用户意图,**完整输出思考内容后**再输出正式的回复内容;
4206
- #注意:你的正式回复内容必须使用“<p>”开头,在输出全部回复后使用“</p>”结尾(即最终回答只允许有一组标签),并且无论如何都要把标签输出完整
4255
+ #注意:你的正式回复内容必须使用“<p>”开头,在输出全部回复后使用“</p>”结尾(即最终回答只允许有一组标签),并且无论如何都要把标签输出完整。
4256
+ 例子格式如下:
4257
+ <think>你的思考内容</think><p>你的正式回复内容</p>
4207
4258
  #接下来是对话要求,以下要求仅对最终的回复内容生效,不限制思考过程
4208
4259
  `;
4209
4260
  const promptForReasoner = this.config.enhanceReasoningProtection ? `
@@ -4225,8 +4276,6 @@ ${useNoReasoner ? promptForNoReasoner : promptForReasoner}
4225
4276
  // 处理回复
4226
4277
  async formatResponse(session, response, auxiliaryResult, reasoningContent) {
4227
4278
  const user = await getUser(this.ctx, session.userId);
4228
- this.updateChannelParallelCount(session, -1);
4229
- this.onlineUsers = this.onlineUsers.filter((id) => id !== session.userId);
4230
4279
  if (!response)
4231
4280
  return session.text("commands.sat.messages.no-response");
4232
4281
  const catEar = user?.items?.["猫耳发饰"]?.description && user.items["猫耳发饰"].description == "on";
@@ -4290,8 +4339,18 @@ ${useNoReasoner ? promptForNoReasoner : promptForReasoner}
4290
4339
  logger13.info("Puppeteer 未就绪,正在重新初始化 Puppeteer...");
4291
4340
  this.setPuppeteer(this.ctx.puppeteer);
4292
4341
  }
4293
- if (firstReasoning.length > 15)
4294
- await session.send(await wrapInHTML(firstReasoning));
4342
+ if (firstReasoning.length > 15) {
4343
+ const renderStartTime = Date.now();
4344
+ try {
4345
+ await session.send(await wrapInHTML(firstReasoning));
4346
+ const renderTime = Date.now() - renderStartTime;
4347
+ if (renderTime > 5e3) {
4348
+ logger13.warn(`wrapInHTML 渲染耗时异常: ${renderTime}ms, 内容长度: ${firstReasoning.length}`);
4349
+ }
4350
+ } catch (error) {
4351
+ logger13.error(`wrapInHTML 渲染失败: ${error}`);
4352
+ }
4353
+ }
4295
4354
  if (this.config.sentences_divide && response.length > 10) {
4296
4355
  const sentences = splitSentences(response, this.config.min_sentences_length, this.config.max_sentences_length).map((text) => import_koishi15.h.text(text));
4297
4356
  for (const sentence of sentences) {
@@ -4394,7 +4453,6 @@ ${useNoReasoner ? promptForNoReasoner : promptForReasoner}
4394
4453
  }
4395
4454
  // 随机中间件转接
4396
4455
  async handleRandomMiddleware(session) {
4397
- await this.updateChannelParallelCount(session, 1);
4398
4456
  const response = await this.generateResponse(session, randomPrompt);
4399
4457
  return await this.formatResponse(session, response.content, null);
4400
4458
  }
@@ -4418,6 +4476,18 @@ ${useNoReasoner ? promptForNoReasoner : promptForReasoner}
4418
4476
  this.memoryManager.updateChannelDialogue(session, censored, session.username);
4419
4477
  return null;
4420
4478
  }
4479
+ async dispose() {
4480
+ if (this.puppeteer) {
4481
+ try {
4482
+ await this.puppeteer.stop();
4483
+ logger13.info("Puppeteer 已成功释放");
4484
+ } catch (error) {
4485
+ logger13.warn("释放 Puppeteer 时出错", error);
4486
+ } finally {
4487
+ this.puppeteer = null;
4488
+ }
4489
+ }
4490
+ }
4421
4491
  };
4422
4492
  var src_default = SAT;
4423
4493
  // Annotate the CommonJS export names for ESM import in node:
package/lib/memory.d.ts CHANGED
@@ -7,7 +7,6 @@ export declare class MemoryManager {
7
7
  private channelDialogues;
8
8
  private charactersToRemove;
9
9
  private MAX_MEMORY_LENGTH;
10
- private userMemoryCache;
11
10
  constructor(ctx: Context, config: MemoryConfig);
12
11
  updateMemories(session: Session, prompt: string, config: MemoryConfig, response: {
13
12
  content: string;
@@ -24,7 +23,7 @@ export declare class MemoryManager {
24
23
  clearAllMemories(): void;
25
24
  getChannelMemory(channelId: string): MemoryEntry[];
26
25
  saveLongTermMemory(session: Session, dialogues: MemoryEntry[], filePath?: string): Promise<void>;
27
- searchMemories(session: Session, prompt: string, type?: 'user' | 'common' | 'group'): Promise<string>;
26
+ searchMemories(session: Session, type?: 'user' | 'common' | 'group'): Promise<string>;
28
27
  private findBestMatches;
29
28
  private calculateMatchScore;
30
29
  private formatMatches;
@@ -32,5 +31,4 @@ export declare class MemoryManager {
32
31
  private ensureMemoryFile;
33
32
  private loadMemoryFile;
34
33
  getChannelContext(channelId: string): MemoryEntry[];
35
- getUserCachedMemory(userId: string): MemoryEntry[];
36
34
  }
package/lib/types.d.ts CHANGED
@@ -105,6 +105,7 @@ export interface APIConfig {
105
105
  maxRetryTimes?: number;
106
106
  retry_delay_time?: number;
107
107
  reasoning_content?: boolean;
108
+ max_output_tokens: number;
108
109
  }
109
110
  export interface APIError extends Error {
110
111
  response?: {
@@ -162,6 +163,7 @@ export declare namespace Sat {
162
163
  reasoner_prompt: string;
163
164
  no_system_prompt: boolean;
164
165
  max_tokens: number;
166
+ max_output_tokens: number;
165
167
  message_max_length: number;
166
168
  remember_min_length: number;
167
169
  temperature: number;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-satori-ai",
3
3
  "description": "基于 Satori 适配器的智能对话插件,具备用户画像、长期记忆、常识管理和好感度系统",
4
- "version": "1.4.0-beta.2",
4
+ "version": "1.4.2",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "types": "lib/index.d.ts",