koishi-plugin-satori-ai 1.2.6 → 1.2.7

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.
@@ -0,0 +1,52 @@
1
+ import { Session, Context } from 'koishi';
2
+ declare module 'koishi' {
3
+ interface Events {
4
+ 'game-result'(session: Session, result: gameResult): void;
5
+ }
6
+ }
7
+ export interface gameResult {
8
+ message: string;
9
+ gameName: string;
10
+ }
11
+ /**
12
+ * 单局游戏的抽象基类,提供通用生命周期方法
13
+ */
14
+ export declare abstract class abstractGameSingleGame {
15
+ protected session: Session;
16
+ protected disposeListener: () => boolean;
17
+ constructor(disposeListener: () => boolean, session: Session);
18
+ startGame(): string;
19
+ endGame(): {
20
+ message: string;
21
+ gameName: string;
22
+ };
23
+ processInput(str: string): Promise<string>;
24
+ }
25
+ type Constructor<T> = new (...args: any[]) => T;
26
+ /**
27
+ * 游戏管理抽象基类,管理多个频道的游戏实例
28
+ */
29
+ export declare abstract class abstractGame<T extends abstractGameSingleGame> {
30
+ protected gameClass: Constructor<T>;
31
+ protected channelGames: Map<string, T>;
32
+ constructor(GameClass: Constructor<T>);
33
+ /**
34
+ * 生成消息监听器,用于处理玩家输入
35
+ * @param userID 玩家ID
36
+ * @param guildID 服务器ID
37
+ * @returns 监听函数,将输入转发给对应频道的游戏实例
38
+ */
39
+ protected listener: (userID: string, guildID: string) => ((session: Session) => void);
40
+ /**
41
+ * 启动游戏实例
42
+ * @param session 会话上下文
43
+ * @param ctx Koishi 上下文
44
+ * @param args 启动参数(如难度等级)
45
+ */
46
+ startGame(session: Session, ctx: Context, args: string[]): abstractGameSingleGame | null;
47
+ /**
48
+ * 结束游戏实例,触发结果事件
49
+ */
50
+ endGame(session: Session, ctx: Context): string;
51
+ }
52
+ export {};
package/lib/game.d.ts CHANGED
@@ -1,11 +1,17 @@
1
1
  import { Session, Context } from 'koishi';
2
+ import { Sat } from './types';
3
+ /**
4
+ * 游戏总控类,管理所有可用游戏和命令
5
+ */
2
6
  export declare class Game {
3
7
  private GAMES;
4
8
  private channelGames;
5
- private goBang;
6
- constructor(ctx: Context);
9
+ private availableGames;
10
+ private context;
11
+ private config;
12
+ constructor(ctx: Context, cfg: Sat.Config);
7
13
  private registerCommands;
8
- startGame(session: Session, gameName: string, args: string[]): Promise<string>;
9
- endGame(session: Session): Promise<"当前频道没有游戏在进行中" | "游戏已结束">;
14
+ startGame(session: Session, gameName: string, args: string[]): Promise<"当前频道已经有游戏在进行中" | "没有这个游戏哦">;
15
+ endGame(session: Session): Promise<string>;
10
16
  private selectGame;
11
17
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,43 @@
1
+ import { Session, Context } from 'koishi';
2
+ import { abstractGame, abstractGameSingleGame, gameResult } from './abstractGame';
3
+ export declare enum winFlag {
4
+ win = 1,
5
+ lose = 2,
6
+ draw = 3,
7
+ pending = 4
8
+ }
9
+ export interface goBangGameResult extends gameResult {
10
+ win: winFlag;
11
+ }
12
+ /**
13
+ * 五子棋单局实现类,继承自 abstractGameSingleGame
14
+ */
15
+ declare class goBangSingleGame extends abstractGameSingleGame {
16
+ private playerFlag;
17
+ private winningFlag;
18
+ level: number;
19
+ private board;
20
+ constructor(disposeListener: () => boolean, session: Session);
21
+ startGame: () => string;
22
+ endGame: () => {
23
+ message: string;
24
+ win: winFlag;
25
+ gameName: string;
26
+ };
27
+ /**
28
+ * 处理玩家输入(落子坐标)
29
+ * @param str 输入内容,格式为 "x y"
30
+ */
31
+ processInput(str: string): Promise<string>;
32
+ private checkWin;
33
+ private checkDirection;
34
+ private printBoard;
35
+ }
36
+ /**
37
+ * 五子棋管理类,继承自 abstractGame
38
+ */
39
+ export declare class goBang extends abstractGame<goBangSingleGame> {
40
+ constructor();
41
+ startGame(session: Session, ctx: Context, args: string[]): goBangSingleGame;
42
+ }
43
+ export {};
package/lib/index.d.ts CHANGED
@@ -7,6 +7,7 @@ export declare class SAT extends Sat {
7
7
  private portraitManager;
8
8
  private ChannelParallelCount;
9
9
  private onlineUsers;
10
+ private game;
10
11
  constructor(ctx: Context, config: Sat.Config);
11
12
  private getAPIConfig;
12
13
  private getMemoryConfig;
package/lib/index.js CHANGED
@@ -33,7 +33,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
33
33
  // src/locales/zh.yml
34
34
  var require_zh = __commonJS({
35
35
  "src/locales/zh.yml"(exports, module2) {
36
- module2.exports = { commands: { sat: { description: "AI聊天", messages: { "no-prompt": "你好。使用说明可以看我的空间哦。", tooManyEnglishLetters: "请不要用这么多英文......", err: "寄!{0}", tooLong: "你的话太多了。", content_tooLong: "这个问题有点复杂,你还是去问别人吧。", "duplicate-dialogue": "这个刚刚说过了吧......", online: "思考中,请再等一会!", "no-response": "我不知道你在说什么……", update_level_succeed: "用户等级已更新为{0}", exceeds: "已经差不多够了吧?请明天再来。", usage: "今日次数{0}/{1}", hatMan: "叮!帽子先生保护了你的好感度,快说谢谢帽子先生吧!", block1: "我讨厌你!", block2: "我讨厌你们!" } }, "sat.clear": { description: "清空当前频道会话", messages: { clean: "已清空当前会话。", Allclean: "已清空所有会话。" } }, "sat.common_sense": { description: "添加常识", messages: { "no-prompt": "你想让我知道什么?", succeed: "我知道了,{0}" } } } };
36
+ module2.exports = { commands: { sat: { description: "AI聊天", messages: { "no-prompt": "你好。使用说明可以看我的空间哦。", tooManyEnglishLetters: "请不要用这么多英文......", err: "寄!{0}", tooLong: "你的话太多了。", content_tooLong: "这个问题有点复杂,你还是去问别人吧。", "duplicate-dialogue": "这个刚刚说过了吧......", online: "思考中,请再等一会!", "no-response": "我不知道你在说什么……", update_level_succeed: "用户等级已更新为{0}", exceeds: "已经差不多够了吧?请明天再来。", usage: "今日次数{0}/{1}", hatMan: "叮!帽子先生保护了你的好感度,快说谢谢帽子先生吧!", rockBottom: "叮!谷底小石保护了你的好感度,快说谢谢谷底小石吧!", block1: "我讨厌你!", block2: "我讨厌你们!" } }, "sat.clear": { description: "清空当前频道会话", messages: { clean: "已清空当前会话。", Allclean: "已清空所有会话。" } }, "sat.common_sense": { description: "添加常识", messages: { "no-prompt": "你想让我知道什么?", succeed: "我知道了,{0}" } } } };
37
37
  }
38
38
  });
39
39
 
@@ -135,7 +135,10 @@ function processPrompt(prompt) {
135
135
  return "";
136
136
  if (prompt.includes(":poke"))
137
137
  return "戳戳";
138
- return prompt.replace(/<[^>]*?name="([^\"]*)"[^>]*>/g, (_, name) => `@${name}`);
138
+ prompt = prompt.replace(/<[^>]*?img[^>]*>/g, "[图片]");
139
+ prompt = prompt.replace(/<[^>]*?avatar[^>]*>/g, "");
140
+ prompt = prompt.replace(/<[^>]*?name="([^\"]*)"[^>]*>/g, (_, name) => `@${name}`);
141
+ return prompt;
139
142
  }
140
143
  __name(processPrompt, "processPrompt");
141
144
  function filterResponse(prompt, words) {
@@ -146,7 +149,11 @@ function filterResponse(prompt, words) {
146
149
  }
147
150
  return part;
148
151
  }).join("");
149
- const trimmedResult = filtered.trim();
152
+ const regex = /<think>[\s\S]*?<\/think>/g;
153
+ const filteredThink = filtered.replace(regex, "");
154
+ const regex2 = /<think>|<\/think>/g;
155
+ const filtered2 = filteredThink.replace(regex2, "");
156
+ const trimmedResult = filtered2.trim();
150
157
  return trimmedResult === "" ? "……" : trimmedResult;
151
158
  }
152
159
  __name(filterResponse, "filterResponse");
@@ -415,6 +422,7 @@ __name(updateFavorability, "updateFavorability");
415
422
  async function updateUserLevel(ctx, user, level) {
416
423
  if (!user)
417
424
  return;
425
+ level = level < 5 ? level : 4;
418
426
  await ctx.database.set("p_system", { userid: user.userid }, { userlevel: level });
419
427
  }
420
428
  __name(updateUserLevel, "updateUserLevel");
@@ -483,6 +491,8 @@ var MemoryManager = class {
483
491
  }
484
492
  channelMemories = /* @__PURE__ */ new Map();
485
493
  channelDialogues = /* @__PURE__ */ new Map();
494
+ charactersToRemove = ["的", "一", "是", "了", "什", "么", "我", "谁", "不", "人", "在", "他", "有", "这", "个", "上", "们", "来", "到", "时", "大", "地", "为", "子", "中", "你", "说", "生", "国", "年", "着", "就", "那", "和", "要", "她", "出", "也", "得", "里", "后", "自", "以", "会", "id=", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0"];
495
+ MAX_MEMORY_LENGTH = 5e3;
486
496
  // 更新记忆
487
497
  async updateMemories(session, prompt, config, response) {
488
498
  if (response.error)
@@ -602,7 +612,10 @@ var MemoryManager = class {
602
612
  if (filtered.length === 0)
603
613
  return;
604
614
  const existing = await this.loadMemoryFile(filePath);
605
- const updated = [...existing, ...filtered];
615
+ let updated = [...existing, ...filtered];
616
+ if (updated.length > this.MAX_MEMORY_LENGTH) {
617
+ updated = updated.slice(-this.MAX_MEMORY_LENGTH);
618
+ }
606
619
  fs2.writeFileSync(filePath, JSON.stringify(updated, null, 2));
607
620
  }
608
621
  // 记忆检索
@@ -615,20 +628,24 @@ var MemoryManager = class {
615
628
  "user": this.config.dailogues_topN,
616
629
  "common": this.config.common_topN
617
630
  };
618
- if (!fs2.existsSync(filePathMap[type])) {
619
- logger3.warn(`记忆文件不存在:${filePathMap[type]}`);
631
+ const filePath = filePathMap[type];
632
+ if (!fs2.existsSync(filePath)) {
620
633
  return "";
621
634
  }
622
- const charactersToRemove = ["的", "一", "是", "了", "什", "么", "我", "谁", "不", "人", "在", "他", "有", "这", "个", "上", "们", "来", "到", "时", "大", "地", "为", "子", "中", "你", "说", "生", "国", "年", "着", "就", "那", "和", "要", "她", "出", "也", "得", "里", "后", "自", "以", "会", "id=", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0"];
623
- const keywords = prompt.split("").filter((word) => !charactersToRemove.includes(word));
624
- const entries = await this.loadMemoryFile(filePathMap[type]);
625
- const matched = this.findBestMatches(entries, keywords, topNMap[type]);
626
- const result = this.formatMatches(matched, type);
635
+ const keywords = prompt.split("").filter((word) => !this.charactersToRemove.includes(word));
636
+ let entries = await this.loadMemoryFile(filePath);
637
+ const matched = this.findBestMatches(entries, keywords).slice(0, topNMap[type] * 5);
638
+ if (type === "user") {
639
+ const remainingEntries = entries.filter((entry) => !matched.includes(entry));
640
+ let updatedEntries = [...remainingEntries, ...matched];
641
+ fs2.writeFileSync(filePath, JSON.stringify(updatedEntries, null, 2));
642
+ }
643
+ const result = this.formatMatches(matched, type, topNMap[type]);
627
644
  return result;
628
645
  }
629
646
  // 记忆检索
630
- findBestMatches(entries, keywords, topN = 5) {
631
- return entries.map((entry) => ({ entry, ...this.calculateMatchScore(entry.content, keywords) })).filter(({ count }) => count > 1).sort((a, b) => b.score - a.score).slice(0, topN < entries.length ? topN : entries.length).map(({ entry }) => entry);
647
+ findBestMatches(entries, keywords) {
648
+ return entries.map((entry) => ({ entry, ...this.calculateMatchScore(entry.content, keywords) })).filter(({ count }) => count > 1).sort((a, b) => b.score - a.score).map(({ entry }) => entry);
632
649
  }
633
650
  // 匹配度计算
634
651
  calculateMatchScore(content, keywords) {
@@ -637,6 +654,8 @@ var MemoryManager = class {
637
654
  const escapedKeywords = keywords.map((k) => escapeRegExp(k));
638
655
  const regex = new RegExp(escapedKeywords.join("|"), "gi");
639
656
  const chineseRegex = /[\u4e00-\u9fa5]/g;
657
+ const regex2 = new RegExp(`[${this.charactersToRemove.join("")}]`, "g");
658
+ content = content.replace(regex2, "");
640
659
  const matches = content.match(regex) || [];
641
660
  let count = 0;
642
661
  matches.forEach((match) => {
@@ -651,7 +670,7 @@ var MemoryManager = class {
651
670
  return { score: ratio, count };
652
671
  }
653
672
  // 格式化匹配结果
654
- formatMatches(matched, type) {
673
+ formatMatches(matched, type, topN = 5) {
655
674
  const prefixMap = {
656
675
  "common": "\n这是你可能用到的信息:",
657
676
  "user": "\n以下是较久之前用户说过的话和对话时间:"
@@ -661,6 +680,7 @@ var MemoryManager = class {
661
680
  const date = `
662
681
  当前日期和时间:${(/* @__PURE__ */ new Date()).toLocaleString()} ${time}`;
663
682
  if (matched.length > 0) {
683
+ matched = matched.slice(0, topN < matched.length ? topN : matched.length);
664
684
  if (type === "common") {
665
685
  const result = `${prefixMap[type]}{
666
686
  ${matched.map((entry) => entry.content).join("\n")} ${date}
@@ -917,6 +937,10 @@ async function ensureCensorFileExists(basePath) {
917
937
  }
918
938
  __name(ensureCensorFileExists, "ensureCensorFileExists");
919
939
  async function applyFavorabilityEffect(ctx, user, effect, session) {
940
+ if (effect < 0 && user.items["谷底小石"]?.count > 0) {
941
+ session.send(session.text("commands.sat.messages.rockBottom"));
942
+ return;
943
+ }
920
944
  if (effect < 0 && user.items["帽子先生"]?.count > 0) {
921
945
  user.items["帽子先生"].count--;
922
946
  if (user.items["帽子先生"].count === 0)
@@ -942,9 +966,6 @@ function createMiddleware(ctx, sat, config) {
942
966
  if (config.nick_name && await hasNickName(ctx, session, config)) {
943
967
  return await handleNickNameMessage(sat, session);
944
968
  }
945
- if (config.mention && isMentionTriggered(session)) {
946
- return await handleMentionMessage(sat, session);
947
- }
948
969
  if (shouldRandomTrigger(session, config)) {
949
970
  return await sat.handleRandomMiddleware(session, session.content);
950
971
  }
@@ -983,16 +1004,6 @@ async function handleNickNameMessage(SAT2, session) {
983
1004
  return await SAT2.handleNickNameMiddleware(session, content);
984
1005
  }
985
1006
  __name(handleNickNameMessage, "handleNickNameMessage");
986
- function isMentionTriggered(session) {
987
- return !!session.stripped.appel;
988
- }
989
- __name(isMentionTriggered, "isMentionTriggered");
990
- async function handleMentionMessage(SAT2, session) {
991
- let message = session.elements.slice(1).filter((e) => e.type === "text").map((e) => e.attrs.content).join("").trim();
992
- if (message)
993
- return await SAT2.handleNickNameMiddleware(session, message);
994
- }
995
- __name(handleMentionMessage, "handleMentionMessage");
996
1007
  function shouldRandomTrigger(session, config) {
997
1008
  const { content } = session;
998
1009
  return !isSpecialMessage(session) && content.length >= config.random_min_tokens && content.length < config.max_tokens && probabilisticCheck(config.randnum);
@@ -1070,10 +1081,10 @@ var Sat = class extends import_koishi5.Service {
1070
1081
  import_koishi5.Schema.object({
1071
1082
  max_usage: import_koishi5.Schema.tuple([Number, Number, Number, Number, Number]).default([40, 240, 3e3, 9999, 0]).description("每日最大使用次数(对应用户level0~level4)(0为不限制)"),
1072
1083
  private: import_koishi5.Schema.boolean().default(false).description("开启后私聊AI可触发对话, 不需要使用指令"),
1073
- mention: import_koishi5.Schema.boolean().default(false).description("开启后机器人被提及(at/引用)可触发对话"),
1074
1084
  nick_name: import_koishi5.Schema.boolean().default(true).description("是否使用昵称触发对话(发言中含有昵称时)"),
1075
1085
  nick_name_list: import_koishi5.Schema.array(String).default(["昵称1"]).description("昵称列表"),
1076
1086
  nick_name_block_words: import_koishi5.Schema.array(String).default(["屏蔽词1"]).description("昵称屏蔽词(含有屏蔽词的消息不会触发昵称对话)"),
1087
+ input_prompt: import_koishi5.Schema.string().role("textarea").default("(注意专注对话主题,遵守对话要求,不要复读)").description("每轮对话前的补充提示(可用于强调要求)不需要时可不填"),
1077
1088
  reasoner_filter: import_koishi5.Schema.boolean().default(true).description("是否启用返回内容过滤,开启后在对话时会过滤掉在括号内且含有过滤词的那一句,用于缓解思维链溢出问题"),
1078
1089
  reasoner_filter_word: import_koishi5.Schema.string().role("textarea").default("系统-提示-用户-设定-回复").description("返回内容过滤词,使用“-”分隔,在括号内且含有过滤词的那一句会被过滤,用于缓解思维链溢出问题"),
1079
1090
  duplicateDialogueCheck: import_koishi5.Schema.boolean().default(true).description("是否检查重复对话"),
@@ -1111,7 +1122,7 @@ var Sat = class extends import_koishi5.Service {
1111
1122
  import_koishi5.Schema.object({
1112
1123
  enable_game: import_koishi5.Schema.boolean().default(false).description("是否开启游戏模块"),
1113
1124
  enable_gobang: import_koishi5.Schema.boolean().default(false).description("是否开启五子棋游戏")
1114
- }).description("拓展模块-游戏设置(开发中)")
1125
+ }).description("拓展模块-游戏设置(无效,开发中)")
1115
1126
  ]);
1116
1127
  })(Sat || (Sat = {}));
1117
1128
 
@@ -1150,30 +1161,34 @@ var UserPortraitManager = class {
1150
1161
  const memoryPath = path5.join(this.config.dataDir, "dialogues", `${userId}.txt`);
1151
1162
  if (!fs5.existsSync(memoryPath))
1152
1163
  return [];
1153
- const MemoryContent = await JSON.parse(fs5.readFileSync(memoryPath, "utf-8"));
1154
- const dialoguesContent = MemoryContent.map((msg) => `${msg.content}${msg.role == "user" ? "(未记录时间)" : msg.role}`);
1164
+ const memoryContent = JSON.parse(fs5.readFileSync(memoryPath, "utf-8"));
1165
+ const validEntries = memoryContent.filter((entry) => entry.role !== "user").map((entry) => {
1166
+ const timeMatch = entry.role.match(/对话日期和时间:(\d{4}\/\d{1,2}\/\d{1,2} \d{2}:\d{2}:\d{2})/);
1167
+ if (!timeMatch)
1168
+ return null;
1169
+ return {
1170
+ ...entry,
1171
+ timestamp: new Date(timeMatch[1]).getTime()
1172
+ };
1173
+ }).filter((entry) => entry !== null).sort((a, b) => b.timestamp - a.timestamp);
1155
1174
  const level = user.userlevel < 5 ? user.userlevel : 4;
1156
- const usageLimit = this.config.max_usage[level] == 0 ? this.config.max_portrait_dialogues : this.config.max_usage[level];
1157
- if (dialoguesContent.length >= usageLimit || dialoguesContent.length >= this.config.max_portrait_dialogues) {
1158
- dialoguesContent.splice(0, dialoguesContent.length - Math.min(usageLimit, this.config.max_portrait_dialogues));
1159
- }
1160
- return dialoguesContent;
1175
+ const usageLimit = this.config.max_usage[level] === 0 ? this.config.max_portrait_dialogues : this.config.max_usage[level];
1176
+ const maxEntries = Math.min(usageLimit, this.config.max_portrait_dialogues);
1177
+ return validEntries.slice(0, maxEntries).map((entry) => `${entry.content}${entry.role}`);
1161
1178
  }
1162
1179
  // 生成提示词模板
1163
- buildMessage(level, history, existingPortrait) {
1180
+ buildMessage(level, history, existingPortrait, user) {
1164
1181
  return [{
1165
1182
  role: "user",
1166
- content: `你是一个角色扮演智能体工作流中的一环,请根据以下信息生成用户画像。需包括:
1183
+ content: `你是一个角色扮演智能体工作流中的一环,请根据以下信息更新用户画像。需包括:
1167
1184
  1. 基本信息(性别、年龄、生日等)
1168
- 2. 生活习惯(作息、爱好等)
1169
- 3. 性格特征(用几句话描述)
1170
- 4. 交流偏好(常用语气词、话题倾向)
1171
- 5. 敏感点(明确表示反感的内容)
1172
- 6. 对AI的期望(从对话历史中推断)
1173
- 7. 其他你认为需要记录的信息(如有)
1174
- 8. 用户希望记住的信息(如有)
1175
- 9. 需要短期记忆的信息(如有)
1185
+ 2. 生活习惯(作息等)
1186
+ 3. 反感点(如有)
1187
+ 4. 对AI的期望(从对话历史中推断)
1188
+ 5. 用户希望记住的信息(如有)
1189
+ 6. 需要短期记忆的信息(如有)
1176
1190
 
1191
+ 用户名:${user.usersname}
1177
1192
  你与用户的关系:${level}
1178
1193
  历史画像:
1179
1194
  ${existingPortrait ? "无" : existingPortrait}
@@ -1183,15 +1198,16 @@ ${history}
1183
1198
 
1184
1199
  注意事项:
1185
1200
  ·用户一定是人类,其他情况是角色扮演
1186
- ·若有历史画像则需要根据发言修正历史画像内容,以发言为准
1187
- ·及时删除历史画像中的错误信息
1201
+ ·若有历史画像则需要根据发言修正历史画像内容,以发言为准,但是历史画像中不需要修改的部分可以保留
1202
+ ·除非用户要求,否则尽量不要改动基本信息
1203
+ ·及时删除历史画像中明确错误或多余的信息
1188
1204
  ·尤其注意用户说“记住”的部分,可能是用户希望记录的信息
1189
1205
  ·历史画像中的需要短期记忆的信息不需要保留,只需要删除旧的添加新的
1190
- ·基于事实推断,避免盲目相信或主观臆测
1191
- ·使用简洁的条目式表达,但内容要全面
1206
+ ·因为在角色扮演中,用户可能会说出不符合事实的信息,需要根据事实推断,避免盲目相信或主观臆测
1207
+ ·使用尽可能简洁的表达
1192
1208
  ·保留不确定性的表述(如"可能"、"似乎"、"用户自称")
1193
1209
  ·保持中立和客观,避免带有个人情感色彩的描述,不要添加评价或建议
1194
- ·仅给出画像内容,不要添加额外的描述、建议、评价、注解等
1210
+ ·仅给出画像内容,不要添加额外的描述、建议、评价、注解等任何内容
1195
1211
  ·不使用markdown等标记语言,直接书写即可`
1196
1212
  }];
1197
1213
  }
@@ -1206,7 +1222,7 @@ ${history}
1206
1222
  const dialogues = await this.getDialogues(user);
1207
1223
  const existing = this.readPortrait(user.userid);
1208
1224
  const userlevel = getFavorabilityLevel(user, this.getFavorabilityConfig());
1209
- const messages = this.buildMessage(userlevel, dialogues.join("\n"), existing);
1225
+ const messages = this.buildMessage(userlevel, dialogues.join("\n"), existing, user);
1210
1226
  logger5.info(`用户 ${user.userid} 画像生成中...`);
1211
1227
  try {
1212
1228
  const response = await apiClient.generateUserPortrait(user, messages);
@@ -1241,6 +1257,16 @@ ${history}
1241
1257
  以下是当前用户的补充信息:{${portrait}
1242
1258
  }
1243
1259
  ` : "";
1260
+ }
1261
+ getUserPortraitById(userId) {
1262
+ if (!this.config.enable_favorability)
1263
+ return "当前未启用好感度功能";
1264
+ if (!this.config.enable_user_portrait)
1265
+ return "当前未启用用户画像功能";
1266
+ const portrait = this.readPortrait(userId);
1267
+ return portrait ? `以下是用户${userId}的画像:{
1268
+ ${portrait}
1269
+ }` : `用户${userId}没有画像`;
1244
1270
  }
1245
1271
  getFavorabilityConfig() {
1246
1272
  return {
@@ -1290,6 +1316,7 @@ var SAT = class extends Sat {
1290
1316
  portraitManager;
1291
1317
  ChannelParallelCount = /* @__PURE__ */ new Map();
1292
1318
  onlineUsers = [];
1319
+ game;
1293
1320
  getAPIConfig() {
1294
1321
  return {
1295
1322
  baseURL: this.config.baseURL,
@@ -1327,7 +1354,6 @@ var SAT = class extends Sat {
1327
1354
  getMiddlewareConfig() {
1328
1355
  return {
1329
1356
  private: this.config.private,
1330
- mention: this.config.mention,
1331
1357
  nick_name: this.config.nick_name,
1332
1358
  nick_name_list: this.config.nick_name_list,
1333
1359
  nick_name_block_words: this.config.nick_name_block_words,
@@ -1383,6 +1409,7 @@ var SAT = class extends Sat {
1383
1409
  ctx.command("sat.update_user_level", "更新用户等级", { authority: 2 }).alias("更新用户").option("id", "-i <id:string>", { authority: 4 }).option("level", "-l <level:number>", { authority: 4 }).action(async ({ session, options }) => this.handleUserLevel(session, options));
1384
1410
  ctx.command("sat.user_usage", "查看用户使用次数").alias("查询次数").action(async ({ session }) => this.handleUserUsage(session));
1385
1411
  ctx.command("sat.add_output_censor <text:text>", "添加输出敏感词", { authority: 4 }).alias("添加输出屏蔽词").action(async ({ session }, word) => addOutputCensor(session, word, this.config.dataDir));
1412
+ ctx.command("sat.get_user_portrait <text:text>", "查看用户画像", { authority: 4 }).alias("查看画像").action(async ({}, userId) => this.portraitManager.getUserPortraitById(userId));
1386
1413
  }
1387
1414
  async handleSatCommand(session, prompt) {
1388
1415
  const user = await ensureUserExists(this.ctx, session.userId, session.username);
@@ -1534,9 +1561,11 @@ var SAT = class extends Sat {
1534
1561
  const messages = this.buildMessages(session, prompt);
1535
1562
  logger6.info(`频道 ${session.channelId} 处理:${session.userId},剩余${this.getChannelParallelCount(session)}并发`);
1536
1563
  const user = await getUser(this.ctx, session.userId);
1537
- const response = await this.apiClient.chat(user, await messages);
1564
+ let response = await this.apiClient.chat(user, await messages);
1538
1565
  if (this.config.log_ask_response)
1539
1566
  logger6.info(`Satori AI:${response.content}`);
1567
+ if (this.config.reasoner_filter)
1568
+ response.content = filterResponse(response.content, this.config.reasoner_filter_word.split("-"));
1540
1569
  return response;
1541
1570
  }
1542
1571
  // 构建消息
@@ -1548,17 +1577,13 @@ var SAT = class extends Sat {
1548
1577
  } else {
1549
1578
  messages.push({ role: "system", content: await this.buildSystemPrompt(session, prompt) });
1550
1579
  }
1551
- if (this.config.personal_memory) {
1552
- const userMemory = this.memoryManager.getChannelContext(session.userId);
1553
- messages.push(...userMemory);
1554
- } else {
1555
- const channelMemory = this.memoryManager.getChannelContext(session.channelId);
1556
- messages.push(...channelMemory);
1580
+ const userMemory = this.memoryManager.getChannelContext(this.config.personal_memory ? session.userId : session.channelId);
1581
+ messages.push(...userMemory);
1582
+ if (this.config.input_prompt && messages.length > 2) {
1583
+ messages.push({ role: "user", content: "system:" + this.config.input_prompt });
1584
+ messages.push({ role: "assistant", content: "好的,我会按您的要求进行回复。" });
1557
1585
  }
1558
- messages.push({
1559
- role: "user",
1560
- content: prompt
1561
- });
1586
+ messages.push({ role: "user", content: prompt });
1562
1587
  let payload = messages.map((msg) => msg.role + ":" + msg.content).join("\n");
1563
1588
  if (this.config.log_system_prompt)
1564
1589
  logger6.info(`系统提示:
@@ -1571,10 +1596,17 @@ ${payload}`);
1571
1596
  const commonSense = await this.memoryManager.searchMemories(session, prompt, "common");
1572
1597
  const channelDialogue = await this.memoryManager.getChannelDialogue(session);
1573
1598
  const userMemory = await this.memoryManager.searchMemories(session, prompt);
1599
+ const user = await getUser(this.ctx, session.userId);
1600
+ if (user?.items?.["觉的衣柜"]?.count) {
1601
+ const clothes = user?.items?.["觉的衣柜"]?.metadata?.clothes;
1602
+ if (clothes)
1603
+ systemPrompt += `
1604
+ 你当前的穿着(根据穿着进行对应的行为):${clothes}
1605
+ `;
1606
+ }
1574
1607
  systemPrompt += commonSense;
1575
1608
  systemPrompt += channelDialogue;
1576
1609
  systemPrompt += userMemory;
1577
- const user = await getUser(this.ctx, session.userId);
1578
1610
  systemPrompt += this.portraitManager.getUserPortrait(session);
1579
1611
  const nickName = user.items["情侣合照"]?.metadata?.userNickName;
1580
1612
  systemPrompt += `用户的名字是:${session.username}, id是:${session.userId}`;
@@ -1594,8 +1626,6 @@ ${payload}`);
1594
1626
  this.onlineUsers = this.onlineUsers.filter((id) => id !== session.userId);
1595
1627
  if (!response)
1596
1628
  return session.text("commands.sat.messages.no-response");
1597
- if (this.config.reasoner_filter)
1598
- response = filterResponse(response, this.config.reasoner_filter_word.split("-"));
1599
1629
  const catEar = user?.items?.["猫耳发饰"]?.description && user.items["猫耳发饰"].description == "on";
1600
1630
  const fumo = user?.items?.["觉fumo"]?.description && user.items["觉fumo"].description == "on";
1601
1631
  const ring = user?.items?.["订婚戒指"]?.description && user.items["订婚戒指"].description == "已使用";
@@ -1648,10 +1678,10 @@ ${payload}`);
1648
1678
  // 更新用户等级
1649
1679
  async handleUserLevel(session, options) {
1650
1680
  const userId = options.id || session.userId;
1651
- const level = options.level || 1;
1652
1681
  const user = await ensureUserExists(this.ctx, userId, session.username);
1682
+ const level = options.level || user.userlevel > 1 ? user.userlevel : 1;
1653
1683
  const enableUserKey = user?.items?.["地灵殿通行证"]?.description && user.items["地灵殿通行证"].description == "on";
1654
- if (enableUserKey)
1684
+ if (enableUserKey || level > 3)
1655
1685
  await this.portraitManager.generatePortrait(session, user, this.apiClient);
1656
1686
  await updateUserLevel(this.ctx, user, level);
1657
1687
  return session.text("commands.sat.messages.update_level_succeed", [level]);
@@ -1670,8 +1700,6 @@ ${payload}`);
1670
1700
  result += session.text("commands.sat.messages.usage", [userUsage, maxUsage]) + "\n";
1671
1701
  if (this.portraitManager.hasPortrait(user.userid))
1672
1702
  result += "用户画像生效中\n";
1673
- else
1674
- result += "用户画像未生效\n";
1675
1703
  return result;
1676
1704
  }
1677
1705
  // 随机中间件转接
package/lib/memory.d.ts CHANGED
@@ -5,6 +5,8 @@ export declare class MemoryManager {
5
5
  private config;
6
6
  private channelMemories;
7
7
  private channelDialogues;
8
+ private charactersToRemove;
9
+ private MAX_MEMORY_LENGTH;
8
10
  constructor(ctx: Context, config: MemoryConfig);
9
11
  updateMemories(session: Session, prompt: string, config: MemoryConfig, response: {
10
12
  content: string;
package/lib/types.d.ts CHANGED
@@ -76,7 +76,6 @@ export interface MemoryConfig {
76
76
  }
77
77
  export interface MiddlewareConfig {
78
78
  private: boolean;
79
- mention: boolean;
80
79
  nick_name: boolean;
81
80
  nick_name_list: string[];
82
81
  nick_name_block_words: string[];
@@ -175,10 +174,10 @@ export declare namespace Sat {
175
174
  enable_fixed_dialogues: boolean;
176
175
  max_usage: number[];
177
176
  private: boolean;
178
- mention: boolean;
179
177
  nick_name: boolean;
180
178
  nick_name_list: string[];
181
179
  nick_name_block_words: string[];
180
+ input_prompt: string;
182
181
  reasoner_filter: boolean;
183
182
  reasoner_filter_word: string;
184
183
  duplicateDialogueCheck: boolean;
@@ -213,6 +212,8 @@ export declare namespace Sat {
213
212
  prompt_4: string;
214
213
  maxRetryTimes: number;
215
214
  retry_delay_time: number;
215
+ enable_game: boolean;
216
+ enable_gobang: boolean;
216
217
  }
217
218
  const Config: Schema<Config>;
218
219
  }
@@ -14,5 +14,6 @@ export declare class UserPortraitManager {
14
14
  private savePortrait;
15
15
  private ensurePortraitFile;
16
16
  getUserPortrait(session: Session): string;
17
+ getUserPortraitById(userId: string): string;
17
18
  private getFavorabilityConfig;
18
19
  }
package/lib/utils.d.ts CHANGED
@@ -13,10 +13,14 @@ export declare function isErrorWithMessage(error: unknown): error is {
13
13
  message: string;
14
14
  };
15
15
  /**
16
- * 将包含 `<...name="[xxx]"...>` 格式的文本替换
17
16
  * @param prompt 包含待处理标签的原始字符串
18
17
  * @returns 处理后的字符串,所有匹配标签被替换为对应名称
19
18
  */
20
19
  export declare function processPrompt(prompt: string): string;
20
+ /**
21
+ * @param prompt 包含待处理标签的原始字符串
22
+ * @param words 需要过滤的关键词
23
+ * @returns 处理后的字符串,删除含有关键词的部分
24
+ */
21
25
  export declare function filterResponse(prompt: string, words: string[]): string;
22
26
  export declare function addOutputCensor(session: Session, word: string, baseURL: string): void;
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "koishi-plugin-satori-ai",
3
3
  "description": "基于 Satori 适配器的智能对话插件,具备用户画像、长期记忆、常识管理和好感度系统",
4
- "version": "1.2.6",
4
+ "version": "1.2.7",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "types": "lib/index.d.ts",
8
8
  "files": [
9
9
  "lib",
10
10
  "locales",
11
+ "wasm",
11
12
  "README.md"
12
13
  ],
13
14
  "license": "MIT",
@@ -0,0 +1,582 @@
1
+ // emcc gobang.cpp -O3 -s EXPORTED_FUNCTIONS="['_decideMove', '_malloc', '_free']" -s MODULARIZE=1 -s ENVIRONMENT='node' -o gobang.js
2
+
3
+ #define _CRT_SECURE_NO_WARNINGS
4
+ #include <stdio.h>
5
+ #include <stdlib.h>
6
+ #include <string.h>
7
+ #include <string>
8
+ #define BOARD_SIZE 12 // 棋盘大小
9
+ #define EMPTY 0 // 空位
10
+ #define BLACK 1 // 黑棋
11
+ #define WHITE 2 // 白棋
12
+ #define INF 2147483647 // 正无穷
13
+ #define _INF -2147483647 // 负无穷
14
+ #define DEPTH 5 // 搜索深度
15
+ #define START "START" // 开始游戏
16
+ #define PLACE "PLACE" // 放置棋子
17
+ #define TURN "TURN" // 轮到我方下棋
18
+ #define END "END" // 游戏结束
19
+ const int BOARD_MIDDLE_1 = (BOARD_SIZE + 1) / 2 - 1;
20
+ const int BOARD_MIDDLE_2 = BOARD_SIZE / 2;
21
+ struct chessType {
22
+ int win5; // 连五
23
+ int alive4; // 活4
24
+ int conti4; // 冲4
25
+ int alive3; // 活3
26
+ int conti3; // 眠3
27
+ int jump3; // 跳3
28
+ int alive2; // 活2
29
+ int conti2; // 眠2
30
+ int jump2; // 跳2
31
+ int alive1; // 活1
32
+ int conti1; // 眠1
33
+ };
34
+ // 坐标结构体
35
+ struct coordinate {
36
+ int x = -1;
37
+ int y = -1;
38
+ int score = 0;
39
+ coordinate(int a = 0, int b = 0, int s = 0) :
40
+ x(a), y(b), score(s) {}
41
+ };
42
+ // 棋盘节点结构体
43
+ struct Nude {
44
+ int board[BOARD_SIZE][BOARD_SIZE] = {{0}}; // 棋盘状态
45
+ int state = 0; // 棋盘状态标识
46
+ Nude() {
47
+ // 初始化棋盘,放置初始棋子
48
+ board[BOARD_MIDDLE_1][BOARD_MIDDLE_1] = WHITE;
49
+ board[BOARD_MIDDLE_2][BOARD_MIDDLE_2] = WHITE;
50
+ board[BOARD_MIDDLE_2][BOARD_MIDDLE_1] = BLACK;
51
+ board[BOARD_MIDDLE_1][BOARD_MIDDLE_2] = BLACK;
52
+ }
53
+ void arr_input(int x, int y, int playerround) {
54
+ board[x][y] = playerround; // 在指定位置放置棋子
55
+ }
56
+ };
57
+ class Game {
58
+ public:
59
+ int myFlag;
60
+ int enemyFlag;
61
+ bool draw;
62
+ Nude MAP;
63
+ Game() :
64
+ myFlag(0), enemyFlag(0), draw(0) {}
65
+ };
66
+ // Game game;
67
+ void debug(const char *str) {
68
+ printf("DEBUG %s\n", str);
69
+ fflush(stdout);
70
+ }
71
+ // 判断坐标是否在棋盘范围内
72
+ bool judgeInRange(coordinate temp) {
73
+ if (temp.x < 0)
74
+ return false;
75
+ if (temp.y < 0)
76
+ return false;
77
+ if (temp.x >= BOARD_SIZE)
78
+ return false;
79
+ if (temp.y >= BOARD_SIZE)
80
+ return false;
81
+ return true;
82
+ }
83
+ // 获取指定位置的棋子颜色
84
+ int getColor(coordinate target, const Game &game) {
85
+ if (judgeInRange(target))
86
+ return game.MAP.board[target.x][target.y];
87
+ else
88
+ return game.enemyFlag;
89
+ }
90
+ // 在指定位置放置棋子
91
+ void place(coordinate target, int player, Game &game) {
92
+ game.MAP.board[target.x][target.y] = player;
93
+ }
94
+ // 快速排序函数
95
+ int partition(coordinate *s, int high, int low) {
96
+ int pi = s[high].score;
97
+ int i = low;
98
+ for (int j = low; j <= high - 1; j++) {
99
+ if (s[j].score >= pi) {
100
+ coordinate temp = s[i];
101
+ s[i] = s[j];
102
+ s[j] = temp;
103
+ i++;
104
+ }
105
+ }
106
+ coordinate temp = s[i];
107
+ s[i] = s[high];
108
+ s[high] = temp;
109
+ return i;
110
+ }
111
+ void quickSort(coordinate *s, int high, int low = 0) {
112
+ if (low < high) {
113
+ int pi = partition(s, high, low);
114
+ quickSort(s, pi - 1, low);
115
+ quickSort(s, high, pi + 1);
116
+ }
117
+ }
118
+ // 返回p点dire方向上w距离的点
119
+ coordinate Neighbor(coordinate temp, int dire, int w) {
120
+ const int direction[4][2] = {{1, 0}, {0, 1}, {1, 1}, {1, -1}};
121
+ coordinate neighbor;
122
+ neighbor.x = temp.x + w * direction[dire][0];
123
+ neighbor.y = temp.y + w * direction[dire][1];
124
+ return neighbor;
125
+ }
126
+ // 判断点在d距离内是否有邻居
127
+ int hasNeighbor(coordinate temp, int d, const Game &game) {
128
+ // 找棋盘上有落子点d个距离内
129
+ for (int i = 0; i < 4; i++) {
130
+ for (int j = -d; j <= d; j++) {
131
+ if (j != 0) {
132
+ coordinate neighbor = Neighbor(temp, i, j);
133
+ if (judgeInRange(neighbor) && getColor(neighbor, game))
134
+ return 1;
135
+ }
136
+ }
137
+ }
138
+ return 0;
139
+ }
140
+ // 获取drie方向上的情况
141
+ int analyzeDire(coordinate temp, int dire, int player, char *beside, const Game &game) {
142
+ int length = 1;
143
+ int i;
144
+ for (i = -1;; i--) {
145
+ coordinate neighbor = Neighbor(temp, dire, i);
146
+ if (judgeInRange(neighbor) && player == game.MAP.board[neighbor.x][neighbor.y])
147
+ length++;
148
+ else {
149
+ for (int j = 0; j < 4; j++) {
150
+ neighbor = Neighbor(temp, dire, i - j);
151
+ if (judgeInRange(neighbor))
152
+ beside[j] = game.MAP.board[neighbor.x][neighbor.y];
153
+ else
154
+ beside[j] = 3 - player;
155
+ }
156
+ break;
157
+ }
158
+ }
159
+ for (i = 1;; i++) {
160
+ coordinate neighbor = Neighbor(temp, dire, i);
161
+ if (judgeInRange(neighbor) && player == game.MAP.board[neighbor.x][neighbor.y])
162
+ length++;
163
+ else {
164
+ for (int j = 0; j < 4; j++) {
165
+ neighbor = Neighbor(temp, dire, i + j);
166
+ if (judgeInRange(neighbor))
167
+ beside[4 + j] = game.MAP.board[neighbor.x][neighbor.y];
168
+ else
169
+ beside[4 + j] = 3 - player;
170
+ }
171
+ break;
172
+ }
173
+ }
174
+ return length;
175
+ }
176
+ // 根据连子数目和边缘信息判断棋型
177
+ chessType typeAnalysis(coordinate p, int dire, int player, const Game &game) {
178
+ char b[8] = {0}; // beside
179
+ int length = analyzeDire(p, dire, player, b, game); // 获取p点连子的长度和两边延伸4子的信息
180
+ chessType temp = {0};
181
+ if (length >= 5)
182
+ temp.win5++;
183
+ else if (length == 4) {
184
+ if (b[0] == 0)
185
+ if (b[4] == 0)
186
+ temp.alive4++; // 011110
187
+ else
188
+ temp.conti4++; // 011112
189
+ else if (b[4] == 0)
190
+ temp.conti4++; // 211110
191
+ } else if (length == 3) {
192
+ if (b[0] == 0) {
193
+ if (b[4] == 0) {
194
+ if (b[1] == player) {
195
+ if (b[5] == player)
196
+ temp.conti4 += 2; // 1011101
197
+ else if (b[5] == player)
198
+ temp.conti4++; // 101110x
199
+ } else if (b[5] == player)
200
+ temp.conti4++; // x011101
201
+ else if (b[5] == 0 || b[1] == 0)
202
+ temp.alive3++; // x01110x
203
+ else
204
+ temp.conti3++; // 2011102
205
+ } else if (b[1] == player)
206
+ temp.conti4++; // 101112x
207
+ else if (b[1] == 0)
208
+ temp.conti3++; // 001112x
209
+ } else if (b[4] == 0) {
210
+ if (b[5] == player)
211
+ temp.conti4++; // x211101
212
+ else
213
+ temp.conti3++; // x211100
214
+ }
215
+ } else if (length == 2) {
216
+ if (b[0] == 0) {
217
+ if (b[4] == 0) {
218
+ if (b[1] == player) {
219
+ if (b[5] == player) {
220
+ if (b[2] == player) {
221
+ if (b[6] == player)
222
+ temp.conti4 += 2; // 11011011
223
+ else
224
+ temp.conti4++; // 1101101x
225
+ } else if (b[2] == 0) {
226
+ if (b[6] == player)
227
+ temp.conti4++; // 01011011
228
+ else
229
+ temp.jump3++; // 0101101x
230
+ } else {
231
+ if (b[6] == player)
232
+ temp.conti4++; // 21011011
233
+ else if (b[6] == 0)
234
+ temp.jump3++; // 21011010
235
+ else
236
+ temp.conti3++; // 21011012
237
+ }
238
+ } else {
239
+ if (b[2] == player)
240
+ temp.conti4++; // 110110xx
241
+ else if (b[2] == 0)
242
+ temp.jump3++; // 010110xx
243
+ else
244
+ temp.conti3++; // 210110xx
245
+ }
246
+ } else if (b[1] == 0) {
247
+ if (b[5] == player) {
248
+ if (b[6] == player)
249
+ temp.conti4++; // x0011011
250
+ else if (b[6] == 0)
251
+ temp.jump3++; // x0011010
252
+ else
253
+ temp.conti3++; // x0011012
254
+ } else
255
+ temp.alive2++; // x00110xx
256
+ } else {
257
+ if (b[5] == player) {
258
+ if (b[6] == player)
259
+ temp.conti4++; // x2011011
260
+ else if (b[6] == 0)
261
+ temp.jump3++; // x2011010
262
+ else
263
+ temp.conti3++; // x2011012
264
+ } else if (b[5] == 0)
265
+ temp.alive2++; // x201100x
266
+ }
267
+ } else {
268
+ if (b[1] == player) {
269
+ if (b[2] == player)
270
+ temp.conti4++; // 110112xx
271
+ else if (b[2] == 0)
272
+ temp.conti3++; // 010112xx
273
+ } else if (b[1] == 0) {
274
+ if (b[2] == player)
275
+ temp.conti3++; // 100112xx
276
+ else if (b[2] == 0)
277
+ temp.conti2++; // 000112xx
278
+ }
279
+ }
280
+ } else if (b[4] == 0) {
281
+ if (b[5] == player) {
282
+ if (b[6] == player)
283
+ temp.conti4++; // xx211011
284
+ else if (b[6] == 0)
285
+ temp.conti3++; // xx211010
286
+ } else if (b[5] == 0) {
287
+ if (b[6] == player)
288
+ temp.conti3++; // xx211001
289
+ else if (b[6] == 0)
290
+ temp.conti2++; // xx211000
291
+ }
292
+ }
293
+ } else {
294
+ if (b[0] == 0) {
295
+ if (b[4] == 0) {
296
+ if (b[1] == player) {
297
+ if (b[5] == player) {
298
+ if (b[2] == player) {
299
+ if (b[6] == player) {
300
+ if (b[3] == player) {
301
+ if (b[7] == player)
302
+ temp.conti4 += 2; // 111010111
303
+ else
304
+ temp.conti4++; // 11101011x
305
+ } else if (b[3] == 0) {
306
+ if (b[7] == player || b[7] == 0)
307
+ temp.conti4++; // 011010111/0
308
+ else
309
+ temp.jump3++; // 011010112
310
+ } else {
311
+ if (b[7] == player)
312
+ temp.conti4++; // 211010111
313
+ else if (b[7] == 0)
314
+ temp.jump3++; // 211010110
315
+ else
316
+ temp.conti3++; // 211010112
317
+ }
318
+ } else {
319
+ if (b[3] == player)
320
+ temp.conti4++; // 1110101xx
321
+ else if (b[3] == 0)
322
+ temp.jump3++; // 0110101xx
323
+ else
324
+ temp.conti3++; // 2110101xx
325
+ }
326
+ } else if (b[2] == 0) {
327
+ if (b[6] == player) {
328
+ if (b[7] == player)
329
+ temp.conti4++; // x01010111
330
+ else if (b[7] == 0)
331
+ temp.jump3++; // x01010110
332
+ else
333
+ temp.conti3++; // x01010112
334
+ } else
335
+ temp.jump2++; // x010101xx
336
+ } else {
337
+ if (b[6] == player) {
338
+ if (b[7] == player)
339
+ temp.conti4++; // x21010111
340
+ else if (b[7] == 0)
341
+ temp.jump3++; // x21010110
342
+ else
343
+ temp.conti3++; // x21010112
344
+ } else if (b[6] == 0)
345
+ temp.jump2++; // x2101010x
346
+ else
347
+ temp.conti3++; // x2101012x
348
+ }
349
+ } else {
350
+ if (b[2] == player) {
351
+ if (b[3] == player)
352
+ temp.conti4++; // 1110102xx
353
+ else if (b[3] == 0)
354
+ temp.jump3++; // 0110102xx
355
+ else
356
+ temp.conti3++; // 2110102xx
357
+ } else if (b[2] == 0)
358
+ temp.jump2++; // x010102xx
359
+ else
360
+ temp.conti2++; // x210102xx
361
+ }
362
+ } else {
363
+ if (b[5] == player)
364
+ if (b[6] == player) {
365
+ if (b[7] == player)
366
+ temp.conti4++; // xxx010111
367
+ else if (b[7] == 0)
368
+ temp.jump3++; // xxx010110
369
+ else
370
+ temp.conti3++; // xxx010112
371
+ } else if (b[6] == 0)
372
+ temp.jump2++; // xxx01010x
373
+ else
374
+ temp.conti2++; // xxx01012x
375
+ else
376
+ temp.alive1++; // xxx0102xx
377
+ }
378
+ } else {
379
+ if (b[1] == player) {
380
+ if (b[2] == player) {
381
+ if (b[3] == player)
382
+ temp.conti4++; // 111012xxx
383
+ else if (b[3] == 0)
384
+ temp.conti3++; // 011012xxx
385
+ } else if (b[2] == 0) {
386
+ if (b[3] == player)
387
+ temp.conti3++; // 101012xxx
388
+ else if (b[3] == 0)
389
+ temp.conti2++; // 001012xxx
390
+ }
391
+ } else
392
+ temp.conti1++; // xxx012xxx
393
+ }
394
+ } else {
395
+ if (b[4] == 0) {
396
+ if (b[5] == player) {
397
+ if (b[6] == player) {
398
+ if (b[7] == player)
399
+ temp.conti4++; // xxx210111
400
+ else if (b[7] == 0)
401
+ temp.conti3++; // xxx210110
402
+ } else if (b[6] == 0) {
403
+ if (b[7] == player)
404
+ temp.conti3++; // xxx210101
405
+ else if (b[7] == 0)
406
+ temp.conti2++; // xxx210100
407
+ }
408
+ } else if (b[5] == 0) {
409
+ if (b[6] == player) {
410
+ if (b[7] == player)
411
+ temp.conti3++; // xxx210011
412
+ else if (b[7] == 0)
413
+ temp.conti2++; // xxx210010
414
+ } else if (b[6] == 0) {
415
+ if (b[7] == player)
416
+ temp.conti2++; // xxx210001
417
+ else if (b[7] == 0)
418
+ temp.conti1++; // xxx210000
419
+ }
420
+ }
421
+ }
422
+ }
423
+ }
424
+ return temp;
425
+ }
426
+ // 单点得分
427
+ int singleScore(coordinate p, int player, const Game &game) {
428
+ chessType chesstype = typeAnalysis(p, 0, player, game);
429
+ for (int i = 1; i < 4; i++) {
430
+ chessType temp;
431
+ temp = typeAnalysis(p, i, player, game);
432
+ chesstype.win5 += temp.win5;
433
+ chesstype.alive4 += temp.alive4;
434
+ chesstype.conti4 += temp.conti4;
435
+ chesstype.alive3 += temp.alive3;
436
+ chesstype.conti3 += temp.conti3;
437
+ chesstype.jump3 += temp.jump3;
438
+ chesstype.alive2 += temp.alive2;
439
+ chesstype.conti2 += temp.conti2;
440
+ chesstype.jump2 += temp.jump2;
441
+ chesstype.alive1 += temp.alive1;
442
+ chesstype.conti1 += temp.conti1;
443
+ }
444
+ if (chesstype.win5) // 胜
445
+ return 1048576;
446
+ int score = ((chesstype.conti4 << 12) +
447
+ (chesstype.alive3 << 12) + (chesstype.conti3 << 8) + (chesstype.jump3 << 10) +
448
+ (chesstype.alive2 << 8) + (chesstype.conti2 << 3) + (chesstype.jump2 << 6) +
449
+ (chesstype.alive1 << 3) + chesstype.conti1);
450
+ if (chesstype.alive3 >= 2 || (chesstype.conti4 && chesstype.alive3) || chesstype.alive4 || chesstype.conti4 >= 2) // 必胜?
451
+ score += 65536;
452
+ return score;
453
+ }
454
+ // 棋盘整体局面分
455
+ int wholeScore(int player, const Game &game) {
456
+ int Score = 0;
457
+ for (int i = 0; i < BOARD_SIZE; i++) {
458
+ for (int j = 0; j < BOARD_SIZE; j++) {
459
+ coordinate temp = {i, j, game.MAP.board[i][j]};
460
+ if (temp.score == 0)
461
+ continue;
462
+ if (temp.score == player) {
463
+ if (player == BLACK)
464
+ Score += singleScore(temp, player, game); // 己方落子的单点分相加
465
+ } else
466
+ Score -= singleScore(temp, 3 - player, game); // 对方落子的单点分相加
467
+ }
468
+ }
469
+ return Score; // 己方总分减对方总分 得到当前对己方来说的局势分
470
+ }
471
+ // 启发性搜索
472
+ int inspireSearch(coordinate *scoreBoard, int player, Game &game) {
473
+ int length = 0;
474
+ for (int i = 0; i < BOARD_SIZE; i++) {
475
+ for (int j = 0; j < BOARD_SIZE; j++) {
476
+ if (game.MAP.board[i][j] == 0) {
477
+ coordinate temp = {i, j, 0};
478
+ if (hasNeighbor(temp, 2, game)) {
479
+ scoreBoard[length] = temp;
480
+ scoreBoard[length].score = singleScore(temp, 3 - player, game);
481
+ if (player == BLACK)
482
+ scoreBoard[length].score += singleScore(temp, player, game);
483
+ length++;
484
+ }
485
+ }
486
+ }
487
+ }
488
+ // 对 scoreBoard 进行排序
489
+ quickSort(scoreBoard, length - 1);
490
+ // 找到最高分数
491
+ int maxScore = scoreBoard[0].score;
492
+ if (maxScore < 5)
493
+ game.draw = 1;
494
+ const int threshold = maxScore / 3;
495
+ // 找到分界线
496
+ int boundary = 1;
497
+ for (int i = 1; i < length; i++) {
498
+ if (scoreBoard[i].score <= threshold) {
499
+ boundary = i; // 更新分界线位置
500
+ break; // 分数低于阈值,跳出循环
501
+ }
502
+ }
503
+ // 更新 length 为分界线的位置
504
+ length = boundary;
505
+ // 返回 length,最多不超过
506
+ return length > 10 ? 10 : length;
507
+ }
508
+ // 负极大极小值搜索
509
+ coordinate alphaBeta(int depth, int alpha, int beta, int player, coordinate command, coordinate current, Game &game) {
510
+ coordinate temp = command;
511
+ if (depth == 0) {
512
+ temp.score = wholeScore(player, game);
513
+ return temp;
514
+ }
515
+ coordinate steps[BOARD_SIZE * BOARD_SIZE];
516
+ int length = inspireSearch(steps, player, game); // 搜索可落子点
517
+ if (length > 7 && depth > 1)
518
+ depth--;
519
+ else if (length > 2)
520
+ depth--;
521
+ for (int i = 0; i < length; i++) {
522
+ place(steps[i], player, game); // 模拟落子
523
+ temp = alphaBeta(depth, -beta, -alpha, 3 - player, steps[i], command, game); // 取负值并交换alpha和beta
524
+ temp.score *= -1;
525
+ place(steps[i], 0, game); // 还原落子
526
+ if (temp.score >= beta) {
527
+ temp.score = beta;
528
+ return temp; // 剪枝
529
+ }
530
+ if (temp.score > alpha)
531
+ alpha = temp.score;
532
+ }
533
+ temp.score = alpha;
534
+ return temp;
535
+ }
536
+ // 搜索入口
537
+ coordinate entrance(int depth, int alpha, int beta, int player, coordinate command, coordinate current, Game &game) {
538
+ coordinate steps[BOARD_SIZE * BOARD_SIZE]{};
539
+ coordinate temp;
540
+ coordinate best;
541
+ int length;
542
+ length = inspireSearch(steps, player, game); // 搜索可落子点
543
+ if (length == 1 || game.draw)
544
+ return steps[0];
545
+ for (int i = 0; i < length; i++) {
546
+ place(steps[i], player, game); // 模拟落子
547
+ temp = alphaBeta(depth, -beta, -alpha, 3 - player, steps[i], command, game); // 递归
548
+ temp.score *= -1;
549
+ place(steps[i], 0, game); // 还原落子
550
+ if (temp.score > alpha) {
551
+ alpha = temp.score;
552
+ best = steps[i]; // 记录最佳落子
553
+ }
554
+ }
555
+ best.score = alpha;
556
+ return best;
557
+ }
558
+
559
+ /*
560
+ * @param board: 棋盘状态
561
+ * @param myFlag: 我方棋子颜色
562
+ * @param depth: 搜索深度
563
+ * @return: 返回落子位置
564
+ */
565
+ extern "C" int decideMove(const int board[BOARD_SIZE * BOARD_SIZE], int myFlag, int depth) {
566
+ Game localGame;
567
+ for (int i = 0; i < BOARD_SIZE; i++) {
568
+ for (int j = 0; j < BOARD_SIZE; j++) {
569
+ localGame.MAP.board[i][j] = board[i * BOARD_SIZE + j];
570
+ }
571
+ }
572
+ localGame.myFlag = myFlag;
573
+ localGame.enemyFlag = 3 - myFlag;
574
+ localGame.draw = 0;
575
+
576
+ coordinate cmd, current;
577
+ coordinate result = entrance(depth, _INF, INF, localGame.myFlag, cmd, current, localGame);
578
+ if (localGame.draw) {
579
+ return -1;
580
+ }
581
+ return result.score * 10000 + result.x * 100 + result.y;
582
+ }
package/wasm/gobang.js ADDED
@@ -0,0 +1,21 @@
1
+ var Module = (() => {
2
+ var _scriptName = typeof document != 'undefined' ? document.currentScript?.src : undefined;
3
+ if (typeof __filename != 'undefined') _scriptName = _scriptName || __filename;
4
+ return (
5
+ async function(moduleArg = {}) {
6
+ var moduleRtn;
7
+
8
+ var Module=moduleArg;var readyPromiseResolve,readyPromiseReject;var readyPromise=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject});var ENVIRONMENT_IS_NODE=true;if(ENVIRONMENT_IS_NODE){}var moduleOverrides={...Module};var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("fs");var nodePath=require("path");scriptDirectory=__dirname+"/";readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(!Module["thisProgram"]&&process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else{}var out=Module["print"]||console.log.bind(console);var err=Module["printErr"]||console.error.bind(console);Object.assign(Module,moduleOverrides);moduleOverrides=null;if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];var wasmBinary=Module["wasmBinary"];var wasmMemory;var ABORT=false;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAP64,HEAPU64,HEAPF64;var runtimeInitialized=false;var isFileURI=filename=>filename.startsWith("file://");function updateMemoryViews(){var b=wasmMemory.buffer;Module["HEAP8"]=HEAP8=new Int8Array(b);Module["HEAP16"]=HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);Module["HEAPF64"]=HEAPF64=new Float64Array(b);Module["HEAP64"]=HEAP64=new BigInt64Array(b);Module["HEAPU64"]=HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["c"]()}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}var runDependencies=0;var dependenciesFulfilled=null;function addRunDependency(id){runDependencies++;Module["monitorRunDependencies"]?.(runDependencies)}function removeRunDependency(id){runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(runDependencies==0){if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject(e);throw e}var wasmBinaryFile;function findWasmBinary(){return locateFile("gobang.wasm")}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&typeof WebAssembly.instantiateStreaming=="function"&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){return{a:wasmImports}}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;wasmMemory=wasmExports["b"];updateMemoryViews();removeRunDependency("wasm-instantiate");return wasmExports}addRunDependency("wasm-instantiate");function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(mod,inst)=>{receiveInstance(mod,inst);resolve(mod.exports)})})}wasmBinaryFile??=findWasmBinary();try{var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}catch(e){readyPromiseReject(e);return Promise.reject(e)}}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.unshift(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.unshift(cb);var noExitRuntime=Module["noExitRuntime"]||true;var abortOnCannotGrowMemory=requestedSize=>{abort("OOM")};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;abortOnCannotGrowMemory(requestedSize)};var wasmImports={a:_emscripten_resize_heap};var wasmExports=await createWasm();var ___wasm_call_ctors=wasmExports["c"];var _decideMove=Module["_decideMove"]=wasmExports["d"];var _malloc=Module["_malloc"]=wasmExports["f"];var _free=Module["_free"]=wasmExports["g"];function run(){if(runDependencies>0){dependenciesFulfilled=run;return}preRun();if(runDependencies>0){dependenciesFulfilled=run;return}function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();readyPromiseResolve(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].pop()()}}run();moduleRtn=readyPromise;
9
+
10
+
11
+ return moduleRtn;
12
+ }
13
+ );
14
+ })();
15
+ if (typeof exports === 'object' && typeof module === 'object') {
16
+ module.exports = Module;
17
+ // This default export looks redundant, but it allows TS to import this
18
+ // commonjs style module.
19
+ module.exports.default = Module;
20
+ } else if (typeof define === 'function' && define['amd'])
21
+ define([], () => Module);
Binary file