koishi-plugin-qxgl-satori 0.0.1 → 0.0.3

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.js CHANGED
@@ -4,99 +4,49 @@ var __name = (target, value) => __defProp(target, "name", { value, configurable:
4
4
 
5
5
  // src/index.ts
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- var koishi = require("koishi");
8
7
  var { Schema, Logger, h } = require("koishi");
9
8
  var logger = new Logger("qxgl-satori");
10
9
  var inject = ["database"];
11
10
  var pluginName = "qxgl-satori";
12
- var fs = require("node:fs");
13
- var fsPromises = require("node:fs/promises");
11
+ var fs = require("node:fs/promises");
12
+ var fsSync = require("node:fs");
14
13
  var path = require("node:path");
15
14
  var { pathToFileURL } = require("node:url");
16
15
  var Config = Schema.intersect([
17
16
  Schema.object({
18
- enableAuthSystem: Schema.boolean().default(true).description("启用授权系统(关闭时所有群可用,但仍记录群信息)"),
19
- autoLeaveOnMute: Schema.boolean().default(true).description("被禁言时自动退群"),
20
- autoLeaveOnExpiry: Schema.boolean().default(true).description("授权到期时自动退群"),
21
- commandAuthority: Schema.number().default(3).description("授权管理指令所需权限等级"),
22
- pictureStrategy: Schema.union([
23
- Schema.const("auto").description("自动检测(Satori用路径,OneBot用file://)"),
24
- Schema.const("http").description("强制HTTP服务(需配置对外可访问地址)")
25
- ]).default("auto").description("图片发送策略"),
26
- httpServePath: Schema.string().default("http://localhost:5140/qxgl-satori").description("图片HTTP服务基础地址(使用HTTP策略时)")
27
- }).description("授权系统设置"),
17
+ authorizationMode: Schema.union([
18
+ Schema.const("disabled").description("关闭 - 所有群可用,仅记录不拦截"),
19
+ Schema.const("record").description("记录 - 新群自动创建未授权记录,不阻断"),
20
+ Schema.const("strict").description("严格 - 未授权/过期自动拦截消息")
21
+ ]).default("strict").description("授权系统模式"),
22
+ autoLeaveOnExpiry: Schema.boolean().default(false).description("严格模式下,授权到期是否自动退群"),
23
+ autoLeaveOnMute: Schema.boolean().default(false).description("被禁言时是否自动退群(Satori适配器需支持)"),
24
+ expiryReminderDays: Schema.tuple([Number, Number]).default([3, 1]).description("到期前提醒天数(默认3天和1天前)")
25
+ }).description("授权系统控制"),
28
26
  Schema.object({
29
- defaultImageExtension: Schema.union(["jpg", "png", "gif"]).default("png").description("图片保存后缀"),
30
- addKeywordTime: Schema.number().role("slider").min(1).max(30).step(1).default(5).description("添加回复输入时限(分钟)"),
31
- Treat_all_as_lowercase: Schema.boolean().default(true).description("英文关键词匹配无视大小写"),
32
- Delete_Branch_Only: Schema.boolean().default(true).description("删除多段回复时必须指定序号"),
33
- HandleDuplicateKeywords: Schema.union([
34
- Schema.const("1").description("直接替换/覆盖"),
35
- Schema.const("2").description("并列添加(随机选择)"),
36
- Schema.const("3").description("禁止重复")
37
- ]).default("2").description("重复关键词处理"),
38
- MultisegmentAdditionRecoveryEffect: Schema.union([
39
- Schema.const("1").description("原版输入,原版输出(多段消息)"),
40
- Schema.const("2").description("合为图文消息/多行消息(一次发出)"),
41
- Schema.const("3").description("合为图文消息并合并转发"),
42
- Schema.const("4").description("原版输入,合并转发发送")
43
- ]).default("2").description("多段回复效果"),
44
- Frequency_limitation: Schema.number().default(0).description("同一问答最小触发间隔(秒,0为不限制)"),
45
- Type_of_restriction: Schema.union([
46
- Schema.const("1").description("对同一个问题(全部对象)"),
47
- Schema.const("2").description("仅对同一个频道(不同频道独立)")
48
- ]).default("2").description("最小间隔限制对象"),
49
- Search_Range: Schema.union([
50
- Schema.const("1").description("仅在当前频道"),
51
- Schema.const("2").description("搜索全部频道"),
52
- Schema.const("3").description("当前频道+全局")
53
- ]).default("3").description("搜索范围"),
54
- Find_Return_Preset: Schema.union([
55
- Schema.const("1").description("仅返回问答内容"),
56
- Schema.const("2").description("仅返回位置"),
57
- Schema.const("3").description("返回内容和位置")
58
- ]).default("1").description("查找返回信息"),
59
- Return_Limit: Schema.union([
60
- Schema.const("1").description("返回全部"),
61
- Schema.const("2").description("仅返回一条")
62
- ]).default("2").description("返回限制"),
63
- MatchPatternForExit: Schema.union([
64
- Schema.const("1").description("仅接受一次性输入"),
65
- Schema.const("2").description("完全匹配结束词退出"),
66
- Schema.const("3").description("包含结束词退出"),
67
- Schema.const("4").description("完全匹配或包含均可退出")
68
- ]).default("4").description("退出添加方式"),
69
- AlwayPrompt: Schema.union([
70
- Schema.const("1").description("不返回提示"),
71
- Schema.const("2").description("仅返回一次"),
72
- Schema.const("3").description("每次输入都返回")
73
- ]).default("2").description("提示方式"),
74
- Prompt: Schema.string().role("textarea", { rows: [2, 4] }).default("请输入回复内容(输入 取消添加 以取消,输入 结束添加 以结束):").description("添加提示文字"),
75
- KeywordOfEsc: Schema.string().default("取消添加").description("取消添加关键词"),
76
- KeywordOfEnd: Schema.string().default("结束添加").description("结束添加关键词")
77
- }).description("关键词问答设置"),
78
- Schema.object({
79
- admin_list: Schema.array(Schema.object({
80
- adminID: Schema.string().description("管理员用户ID"),
81
- allowcommand: Schema.array(Schema.union([
82
- "添加",
83
- "全局添加",
84
- "删除",
85
- "全局删除",
86
- "修改",
87
- "全局修改",
88
- "查找关键词",
89
- "查看关键词列表",
27
+ commandAuthority: Schema.number().default(3).description("管理类指令所需权限等级"),
28
+ adminList: Schema.array(Schema.object({
29
+ adminId: Schema.string().description("管理员用户ID"),
30
+ allowedCommands: Schema.array(Schema.union([
90
31
  "群授权",
91
32
  "私聊授权",
92
33
  "到期时间",
93
34
  "查询到期",
94
35
  "全局延期",
95
36
  "全局减少",
96
- "更新名称",
97
- "列出已记录群",
98
37
  "取消授权",
99
38
  "更换授权",
39
+ "更新名称",
40
+ "列出已记录群",
41
+ "备份数据",
42
+ "添加",
43
+ "删除",
44
+ "全局添加",
45
+ "全局删除",
46
+ "查找关键词",
47
+ "修改",
48
+ "全局修改",
49
+ "查看关键词列表",
100
50
  "测试授权",
101
51
  "取消测试授权",
102
52
  "测试同步",
@@ -104,870 +54,1020 @@ var Config = Schema.intersect([
104
54
  "测试添加",
105
55
  "测试删除",
106
56
  "测试修改"
107
- ])).default(["到期时间"]).description("可使用的指令")
108
- })).role("table").description("管理员列表(0代表所有用户)").default([{ adminID: "0" }]),
109
- channel_admin_auth: Schema.boolean().default(false).description("开启后自动允许群主/管理员使用全部指令(需适配器支持)")
57
+ ])).default(["到期时间"]).description("允许使用的指令")
58
+ })).role("table").description("管理员列表(ID填0代表所有用户)").default([{ adminId: "0", allowedCommands: ["到期时间"] }]),
59
+ channelAdminAuth: Schema.boolean().default(false).description("开启后自动允许群管理员/群主使用所有指令(需适配器支持角色获取)")
110
60
  }).description("权限设置"),
111
61
  Schema.object({
112
- loggerinfo: Schema.boolean().default(false).description("日志调试模式")
113
- }).description("调试设置")
62
+ triggerPrefix: Schema.string().default("添加").description("触发添加关键词的指令前缀"),
63
+ deletePrefix: Schema.string().default("删除").description("触发删除的指令前缀"),
64
+ cancelKeyword: Schema.string().default("取消添加").description("取消添加流程的关键词"),
65
+ endKeyword: Schema.string().default("结束添加").description("结束添加流程的关键词"),
66
+ globalTriggerPrefix: Schema.string().default("全局添加").description("全局添加指令前缀"),
67
+ globalDeletePrefix: Schema.string().default("全局删除").description("全局删除指令前缀"),
68
+ viewListCommand: Schema.string().default("查看关键词列表").description("查看列表指令"),
69
+ searchCommand: Schema.string().default("查找关键词").description("搜索指令"),
70
+ fixCommand: Schema.string().default("修改").description("修改指令"),
71
+ globalFixCommand: Schema.string().default("全局修改").description("全局修改指令"),
72
+ addTimeout: Schema.number().role("slider").min(1).max(30).step(1).default(5).description("添加回复输入时限(分钟)"),
73
+ defaultImageExt: Schema.union(["jpg", "png", "gif"]).default("png").description("保存图片默认后缀"),
74
+ imageSendMode: Schema.union([
75
+ Schema.const("file").description("本地文件路径(转换为file://URL)"),
76
+ Schema.const("base64").description("Base64 DataURI(兼容性最好)")
77
+ ]).default("file").description("图片发送方式"),
78
+ localPathMapping: Schema.string().default("").description("本地路径映射(如Docker内外路径映射,格式:/host/path:/container/path,可选)")
79
+ }).description("关键词指令设置"),
80
+ Schema.object({
81
+ matchPrefixes: Schema.array(String).role("table").default(["", "/", "#"]).description("关键词匹配前缀配置"),
82
+ treatAsLowercase: Schema.boolean().default(true).description("英文关键词匹配时忽略大小写"),
83
+ deleteBranchOnly: Schema.boolean().default(true).description("多段回复删除时必须指定序号(-q参数),防止误删"),
84
+ handleDuplicate: Schema.union([
85
+ Schema.const("replace").description("直接覆盖"),
86
+ Schema.const("append").description("添加并列回复(随机选择)"),
87
+ Schema.const("reject").description("拒绝添加,提示已存在")
88
+ ]).default("append").description("重复关键词处理方式"),
89
+ alwaysPrompt: Schema.union([
90
+ Schema.const("never").description("不返回提示(仅首次)"),
91
+ Schema.const("once").description("仅首次提示"),
92
+ Schema.const("always").description("每次输入都提示")
93
+ ]).default("once").description("添加流程提示频率"),
94
+ multisegmentMode: Schema.union([
95
+ Schema.const("1").description("多段独立发送(逐条)"),
96
+ Schema.const("2").description("合并为图文消息(单次发送)"),
97
+ Schema.const("3").description("合并转发(需适配器支持)"),
98
+ Schema.const("4").description("原格式合并转发")
99
+ ]).default("2").description("多段回复发送方式"),
100
+ frequencyLimit: Schema.number().default(0).description("同一问答最小触发间隔(秒,0为不限制)"),
101
+ frequencyScope: Schema.union([
102
+ Schema.const("global").description("全局限制"),
103
+ Schema.const("channel").description("按频道独立限制")
104
+ ]).default("channel").description("频率限制范围"),
105
+ searchRange: Schema.union([
106
+ Schema.const("local").description("仅当前频道"),
107
+ Schema.const("all").description("全部频道"),
108
+ Schema.const("mixed").description("当前频道+全局")
109
+ ]).default("mixed").description("查看/搜索的范围"),
110
+ viewListMode: Schema.union([
111
+ Schema.const("text").description("文字列表"),
112
+ Schema.const("image").description("图片列表(需puppeteer)")
113
+ ]).default("text").description("查看列表返回格式")
114
+ }).description("问答行为设置"),
115
+ Schema.object({
116
+ enableTestEnv: Schema.boolean().default(true).description("启用测试环境功能"),
117
+ unifiedAtField: Schema.boolean().default(true).description('统一at消息格式为<at id="xxx"/>(移除name属性防不一致)'),
118
+ debugMode: Schema.boolean().default(false).description("调试日志模式")
119
+ }).description("高级设置")
114
120
  ]);
115
- var usage = `<!DOCTYPE html>
116
- <html lang="zh">
121
+ var usage = `
122
+ <!DOCTYPE html>
123
+ <html lang="zh-CN">
117
124
  <head>
118
125
  <meta charset="UTF-8">
119
126
  <title>qxgl-satori 插件使用说明</title>
127
+ <style>
128
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; line-height: 1.6; max-width: 900px; margin: 0 auto; padding: 20px; color: #333; }
129
+ h1 { color: #2c3e50; border-bottom: 3px solid #3498db; padding-bottom: 10px; }
130
+ h2 { color: #34495e; margin-top: 30px; border-left: 5px solid #3498db; padding-left: 15px; }
131
+ h3 { color: #7f8c8d; }
132
+ ul { padding-left: 20px; }
133
+ li { margin: 8px 0; }
134
+ code { background: #f4f4f4; padding: 2px 6px; border-radius: 3px; font-family: Consolas, monospace; color: #e74c3c; }
135
+ .detail { background: #ecf0f1; padding: 15px; border-radius: 8px; margin: 10px 0; }
136
+ </style>
120
137
  </head>
121
138
  <body>
122
- <h1>插件功能说明</h1>
123
- <p>qxgl-satori 整合了群/私聊授权管理和关键词问答功能,支持多账号共享授权数据,Satori 原生适配。</p>
124
- <h2>授权管理指令</h2>
139
+ <h1>qxgl-satori 插件使用说明</h1>
140
+ <p>专为 Satori 适配器优化的群管与问答一体化插件,支持多账号共享授权数据。</p>
141
+
142
+ <h2>一、授权管理系统(指令永不过期)</h2>
143
+ <div class="detail">
144
+ <p><strong>核心特性</strong>:所有授权管理指令不受授权状态影响,即使被拉黑或已过期仍可正常使用。</p>
125
145
  <ul>
126
- <li><strong>群授权 &lt;群号&gt; &lt;±月数&gt; [授权人]</strong> - 授权/修改群到期时间(支持负数减少)</li>
127
- <li><strong>私聊授权 &lt;用户ID&gt; &lt;±月数&gt; [授权人]</strong> - 私聊授权</li>
128
- <li><strong>取消授权 &lt;目标ID&gt;</strong> - 删除授权记录</li>
129
- <li><strong>更换授权 &lt;原ID&gt; &lt;新ID&gt;</strong> - 转移授权</li>
130
- <li><strong>到期时间</strong> - 查询当前群/私聊到期时间</li>
131
- <li><strong>查询到期 [目标ID]</strong> - 查询指定目标(无参数查当前)</li>
132
- <li><strong>全局延期 &lt;天数&gt;</strong> - 所有群统一延期</li>
133
- <li><strong>全局减少 &lt;天数&gt;</strong> - 所有群统一减少</li>
134
- <li><strong>列出已记录群</strong> - 显示所有群授权信息</li>
135
- <li><strong>更新名称</strong> - 刷新所有群名称缓存</li>
146
+ <li><code>群授权 &lt;群号&gt; &lt;±月数&gt; [授权人]</code> - 授权群聊,+6为增6月,-6为减6月</li>
147
+ <li><code>私聊授权 &lt;用户ID&gt; &lt;±月数&gt; [授权人]</code> - 授权私聊</li>
148
+ <li><code>取消授权 &lt;目标ID&gt;</code> - 删除授权记录</li>
149
+ <li><code>更换授权 &lt;原ID&gt; &lt;新ID&gt;</code> - 迁移授权数据</li>
150
+ <li><code>到期时间</code> - 查询当前上下文到期时间</li>
151
+ <li><code>查询到期 &lt;目标ID&gt;</code> - 查询指定目标</li>
152
+ <li><code>全局延期 &lt;天数&gt;</code> / <code>全局减少 &lt;天数&gt;</code> - 批量调整所有授权</li>
153
+ <li><code>更新名称</code> - 手动刷新群名缓存</li>
154
+ <li><code>列出已记录群</code> - 查看所有群授权状态</li>
155
+ <li><code>备份数据</code> - 导出数据库为JSON</li>
136
156
  </ul>
137
- <h2>关键词指令</h2>
157
+ </div>
158
+
159
+ <h2>二、关键词问答系统(Satori专精)</h2>
160
+ <div class="detail">
138
161
  <ul>
139
- <li><strong>添加 &lt;关键词&gt; [-x] [-f]</strong> - 添加关键词(-x正则,-f指定回复方式1-4)</li>
140
- <li><strong>全局添加 &lt;关键词&gt; [-x] [-f]</strong> - 添加全局关键词</li>
141
- <li><strong>删除 &lt;关键词&gt; [-q &lt;序号&gt;]</strong> - 删除关键词(-q删指定分支)</li>
142
- <li><strong>全局删除 &lt;关键词&gt; [-q &lt;序号&gt;]</strong> - 删除全局关键词</li>
143
- <li><strong>修改 &lt;关键词&gt; [-q &lt;序号&gt;]</strong> - 修改回复</li>
144
- <li><strong>全局修改 &lt;关键词&gt; [-q &lt;序号&gt;]</strong> - 修改全局回复</li>
145
- <li><strong>查找关键词 &lt;关键词&gt;</strong> - 模糊搜索</li>
146
- <li><strong>查看关键词列表</strong> - 显示图文关键词列表</li>
162
+ <li><code>添加 [关键词] [-x] [-f方式]</code> - 添加关键词,-x为正则,-f指定发送方式(1-4)</li>
163
+ <li><code>全局添加 [关键词] [-x] [-f方式]</code> - 添加全局关键词</li>
164
+ <li><code>删除 [关键词] [-q序号]</code> / <code>全局删除 [关键词] [-q序号]</code> - 删除指定或全部回复</li>
165
+ <li><code>修改 [关键词] [-q序号]</code> / <code>全局修改 [关键词] [-q序号]</code> - 修改指定回复</li>
166
+ <li><code>查找关键词 [关键词]</code> - 模糊搜索</li>
167
+ <li><code>查看关键词列表</code> - 列出当前范围所有关键词</li>
147
168
  </ul>
148
- <h2>测试环境指令</h2>
169
+ <p>支持变量:<code>{expiryDate}</code> <code>{authorizer}</code> <code>{channelId}</code> <code>{updateDate}</code></p>
170
+ </div>
171
+
172
+ <h2>三、测试环境管理</h2>
173
+ <div class="detail">
149
174
  <ul>
150
- <li><strong>测试授权 &lt;目标ID...&gt;</strong> - 将群/私聊加入测试名单</li>
151
- <li><strong>取消测试授权 &lt;目标ID...&gt;</strong> - 移出测试名单</li>
152
- <li><strong>测试同步</strong> - 全局问答复制到测试环境</li>
153
- <li><strong>测试发布</strong> - 测试问答发布到全局(自动备份)</li>
154
- <li><strong>测试添加/删除/修改</strong> - 同正式指令,仅操作测试数据</li>
175
+ <li><code>测试授权 &lt;ID1&gt; [ID2...]</code> - 将群/用户加入测试名单</li>
176
+ <li><code>取消测试授权 &lt;ID...&gt;</code> - 移出测试名单</li>
177
+ <li><code>测试同步</code> - 将正式数据复制到测试环境(覆盖)</li>
178
+ <li><code>测试发布</code>(别名:<code>测试转正</code>)- 将测试数据发布到正式(自动备份)</li>
179
+ <li><code>测试添加/测试删除/测试修改</code> - 独立的测试问答操作</li>
155
180
  </ul>
181
+ <p><strong>注意</strong>:测试名单内的群仅响应测试数据,与正式环境完全隔离。</p>
182
+ </div>
183
+
184
+ <h2>四、多账号共享说明</h2>
185
+ <div class="detail">
186
+ <p>本插件使用 <code>platform</code> + <code>channelId</code> 作为主键,<strong>不区分 Bot 账号</strong>。</p>
187
+ <p>同一平台(如 Satori)下的多个 Bot 账号共用同一套授权数据,更换 Bot 账号无需重新授权。</p>
188
+ </div>
156
189
  </body>
157
- </html>`;
190
+ </html>
191
+ `;
158
192
  async function apply(ctx, config) {
159
- const root = path.join(ctx.baseDir, "data", "qxgl-satori");
160
- if (!fs.existsSync(root)) {
161
- fs.mkdirSync(root, { recursive: true });
193
+ const dataDir = path.join(ctx.baseDir, "data", "qxgl-satori");
194
+ const imagesDir = path.join(dataDir, "images");
195
+ if (!fsSync.existsSync(dataDir)) {
196
+ fsSync.mkdirSync(dataDir, { recursive: true });
197
+ }
198
+ if (!fsSync.existsSync(imagesDir)) {
199
+ fsSync.mkdirSync(imagesDir, { recursive: true });
200
+ }
201
+ const testFilePath = path.join(dataDir, "test.json");
202
+ if (!fsSync.existsSync(testFilePath)) {
203
+ fsSync.writeFileSync(testFilePath, "{}");
162
204
  }
163
- const globalFile = path.join(root, "global.json");
164
- const testFile = path.join(root, "test.json");
165
- if (!fs.existsSync(globalFile)) fs.writeFileSync(globalFile, "{}");
166
- if (!fs.existsSync(testFile)) fs.writeFileSync(testFile, "{}");
167
205
  let lastTriggerTimes = {};
168
206
  function logInfo(message) {
169
- if (config.loggerinfo) logger.info(message);
207
+ if (config.debugMode) {
208
+ logger.info(message);
209
+ }
170
210
  }
171
211
  __name(logInfo, "logInfo");
172
- function isSatoriAdapter(session) {
173
- return session?.bot?.adapter?.name?.includes("satori") || session?.bot?.constructor?.name?.toLowerCase().includes("satori") || false;
174
- }
175
- __name(isSatoriAdapter, "isSatoriAdapter");
176
212
  ctx.model.extend("qxgl_satori_auth", {
177
213
  platform: "string",
178
214
  channelId: "string",
179
215
  expiryDate: "date",
180
- isblockedchannel: "boolean",
181
216
  authorizer: "string",
182
217
  channelName: "string",
183
218
  updateDate: "date",
219
+ isBlocked: "boolean",
184
220
  testChannels: "list"
185
221
  }, {
186
222
  primary: ["platform", "channelId"]
187
- // 多账号共享关键:移除 selfId
188
223
  });
189
- async function updateChannelName(session) {
190
- if (!session.channelId || session.channelId.startsWith("private:")) return "私聊";
191
- let name = "未知群聊";
192
- try {
193
- if (session.bot && typeof session.bot.getGuild === "function") {
194
- const info = await session.bot.getGuild(session.channelId);
195
- name = info?.name || name;
224
+ function hasPermission(session, command) {
225
+ const userId = session.userId;
226
+ const adminConfig = config.adminList.find((a) => a.adminId === userId);
227
+ const defaultConfig = config.adminList.find((a) => a.adminId === "0");
228
+ if (defaultConfig?.allowedCommands.includes(command)) return true;
229
+ if (adminConfig?.allowedCommands.includes(command)) return true;
230
+ if (config.channelAdminAuth) {
231
+ const roles = session.event?.member?.roles || [];
232
+ if (roles.includes("admin") || roles.includes("owner")) {
233
+ if (!adminConfig) return true;
234
+ return adminConfig.allowedCommands.includes(command);
196
235
  }
197
- } catch (e) {
198
- logInfo(`获取群名失败: ${e.message}`);
199
236
  }
200
- return name;
201
- }
202
- __name(updateChannelName, "updateChannelName");
203
- function formatDate(date) {
204
- if (!date) return "未设置";
205
- const d = new Date(date);
206
- const year = d.getFullYear();
207
- const month = (d.getMonth() + 1).toString().padStart(2, "0");
208
- const day = d.getDate().toString().padStart(2, "0");
209
- return `${year}/${month}/${day}`;
210
- }
211
- __name(formatDate, "formatDate");
212
- function isAdmin(session) {
213
- const roles = session.event?.member?.roles || [];
214
- return roles.includes("admin") || roles.includes("owner");
215
- }
216
- __name(isAdmin, "isAdmin");
217
- function hasPermission(session, commandName) {
218
- const userId = session.userId;
219
- const defaultConfig = config.admin_list.find((a) => a.adminID === "0");
220
- if (defaultConfig?.allowcommand?.includes(commandName)) return true;
221
- const userConfig = config.admin_list.find((a) => a.adminID === userId);
222
- if (userConfig?.allowcommand?.includes(commandName)) return true;
223
- if (config.channel_admin_auth && isAdmin(session)) return true;
224
237
  return false;
225
238
  }
226
239
  __name(hasPermission, "hasPermission");
227
- async function authorizeGroup(session, channelId, months, authorizer) {
228
- if (!channelId || !months) return "请提供群号和月数";
229
- const monthsNum = parseInt(months);
230
- if (isNaN(monthsNum)) return "月数必须是数字";
231
- const channelName = await updateChannelName({ ...session, channelId });
232
- const currentDate = /* @__PURE__ */ new Date();
233
- const [record] = await ctx.database.get("qxgl_satori_auth", {
234
- platform: session.platform,
235
- channelId
236
- });
237
- let newExpiryDate;
238
- if (!record) {
239
- newExpiryDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + monthsNum, currentDate.getDate());
240
+ async function ensureChannelRecorded(session) {
241
+ const { platform, channelId } = session;
242
+ if (!channelId || channelId.startsWith("private:")) return;
243
+ const record = await ctx.database.get("qxgl_satori_auth", { platform, channelId });
244
+ if (record.length === 0 && config.authorizationMode !== "disabled") {
245
+ let channelName = "未知群聊";
246
+ try {
247
+ if (session.bot?.getGuild) {
248
+ const guild = await session.bot.getGuild(channelId);
249
+ channelName = guild?.name || channelName;
250
+ }
251
+ } catch (e) {
252
+ logger.warn(`获取群名失败 ${channelId}: ${e.message}`);
253
+ }
240
254
  await ctx.database.create("qxgl_satori_auth", {
241
- platform: session.platform,
255
+ platform,
242
256
  channelId,
243
- expiryDate: newExpiryDate,
244
- isblockedchannel: false,
245
- authorizer: authorizer || "蒙面人",
257
+ expiryDate: null,
258
+ isBlocked: false,
259
+ authorizer: "系统",
246
260
  channelName,
247
- updateDate: currentDate,
261
+ updateDate: /* @__PURE__ */ new Date(),
248
262
  testChannels: []
249
263
  });
250
- } else {
251
- const currentExpiry = record.expiryDate ? new Date(record.expiryDate) : currentDate;
252
- newExpiryDate = new Date(currentExpiry.getFullYear(), currentExpiry.getMonth() + monthsNum, currentExpiry.getDate());
253
- await ctx.database.set(
254
- "qxgl_satori_auth",
255
- { platform: session.platform, channelId },
256
- {
257
- expiryDate: newExpiryDate,
258
- isblockedchannel: false,
259
- authorizer: authorizer || "蒙面人",
260
- channelName,
261
- updateDate: currentDate
262
- }
263
- );
264
+ logInfo(`自动记录群 ${channelId}`);
264
265
  }
265
- return `群号 "${channelId}" 的授权已${monthsNum > 0 ? "增加" : "减少"} ${Math.abs(monthsNum)} 个月
266
- 新的到期时间:${formatDate(newExpiryDate)}
267
- 开启人:${authorizer || "蒙面人"}
268
- 更新日期:${formatDate(currentDate)}`;
269
266
  }
270
- __name(authorizeGroup, "authorizeGroup");
271
- async function authorizePrivate(session, userId, months, authorizer) {
272
- if (!userId || !months) return "请提供用户ID和月数";
273
- const monthsNum = parseInt(months);
274
- if (isNaN(monthsNum)) return "月数必须是数字";
275
- const channelId = `private:${userId}`;
276
- const currentDate = /* @__PURE__ */ new Date();
277
- const [record] = await ctx.database.get("qxgl_satori_auth", {
278
- platform: session.platform,
279
- channelId
280
- });
281
- let newExpiryDate;
282
- if (!record) {
283
- newExpiryDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + monthsNum, currentDate.getDate());
284
- await ctx.database.create("qxgl_satori_auth", {
285
- platform: session.platform,
286
- channelId,
287
- expiryDate: newExpiryDate,
288
- isblockedchannel: false,
289
- authorizer: authorizer || "蒙面人",
290
- channelName: "私聊",
291
- updateDate: currentDate,
292
- testChannels: []
293
- });
294
- } else {
295
- const currentExpiry = record.expiryDate ? new Date(record.expiryDate) : currentDate;
296
- newExpiryDate = new Date(currentExpiry.getFullYear(), currentExpiry.getMonth() + monthsNum, currentExpiry.getDate());
297
- await ctx.database.set(
298
- "qxgl_satori_auth",
299
- { platform: session.platform, channelId },
300
- {
301
- expiryDate: newExpiryDate,
302
- authorizer: authorizer || "蒙面人",
303
- updateDate: currentDate
304
- }
305
- );
267
+ __name(ensureChannelRecorded, "ensureChannelRecorded");
268
+ async function isAuthorized(session) {
269
+ if (config.authorizationMode === "disabled") return true;
270
+ const { platform, channelId } = session;
271
+ const record = await ctx.database.get("qxgl_satori_auth", { platform, channelId });
272
+ if (record.length === 0) {
273
+ if (config.authorizationMode === "strict") return false;
274
+ return true;
306
275
  }
307
- return `用户ID "${userId}" 的授权已${monthsNum > 0 ? "增加" : "减少"} ${Math.abs(monthsNum)} 个月
308
- 新的到期时间:${formatDate(newExpiryDate)}`;
309
- }
310
- __name(authorizePrivate, "authorizePrivate");
311
- async function cancelAuthorization(session, targetId) {
312
- if (!targetId) return "请提供目标ID";
313
- const channelId = targetId.startsWith("private:") ? targetId : targetId;
314
- const [record] = await ctx.database.get("qxgl_satori_auth", {
315
- platform: session.platform,
316
- channelId
317
- });
318
- if (!record) return `未找到授权记录:${targetId}`;
319
- await ctx.database.remove("qxgl_satori_auth", {
320
- platform: session.platform,
321
- channelId
322
- });
323
- return `授权已取消:${targetId}`;
276
+ const auth = record[0];
277
+ if (auth.isBlocked) return false;
278
+ if (!auth.expiryDate) return true;
279
+ return (/* @__PURE__ */ new Date()).getTime() <= new Date(auth.expiryDate).getTime();
324
280
  }
325
- __name(cancelAuthorization, "cancelAuthorization");
326
- async function changeAuthorization(session, sourceId, targetId) {
327
- if (!sourceId || !targetId) return "请提供原ID和新ID";
328
- const sourceChannelId = sourceId.startsWith("private:") ? sourceId : sourceId;
329
- const targetChannelId = targetId.startsWith("private:") ? targetId : targetId;
330
- const [sourceRecord] = await ctx.database.get("qxgl_satori_auth", {
331
- platform: session.platform,
332
- channelId: sourceChannelId
333
- });
334
- if (!sourceRecord) return `未找到原授权记录:${sourceId}`;
335
- const [targetRecord] = await ctx.database.get("qxgl_satori_auth", {
336
- platform: session.platform,
337
- channelId: targetChannelId
338
- });
339
- if (targetRecord) return `新目标已存在授权记录:${targetId}`;
340
- await ctx.database.create("qxgl_satori_auth", {
341
- platform: session.platform,
342
- channelId: targetChannelId,
343
- expiryDate: sourceRecord.expiryDate,
344
- isblockedchannel: sourceRecord.isblockedchannel,
345
- authorizer: sourceRecord.authorizer,
346
- channelName: "新授权",
347
- updateDate: /* @__PURE__ */ new Date(),
348
- testChannels: sourceRecord.testChannels || []
349
- });
350
- await ctx.database.remove("qxgl_satori_auth", {
351
- platform: session.platform,
352
- channelId: sourceChannelId
353
- });
354
- return `授权已从 ${sourceId} 更换到 ${targetId}`;
355
- }
356
- __name(changeAuthorization, "changeAuthorization");
357
- async function checkExpiryTime(session) {
358
- const [record] = await ctx.database.get("qxgl_satori_auth", {
359
- platform: session.platform,
360
- channelId: session.channelId
361
- });
362
- if (!record || !record.expiryDate) {
363
- return `未设置到期时间`;
364
- } else {
365
- return `本次使用由《${record.authorizer || "蒙面人"}》激活
366
- 到期时间:${formatDate(record.expiryDate)}`;
281
+ __name(isAuthorized, "isAuthorized");
282
+ async function checkAndUpdateExpiry(session) {
283
+ const { platform, channelId } = session;
284
+ const record = await ctx.database.get("qxgl_satori_auth", { platform, channelId });
285
+ if (record.length === 0) return;
286
+ const auth = record[0];
287
+ if (!auth.expiryDate) return;
288
+ const daysUntil = Math.floor((new Date(auth.expiryDate).getTime() - (/* @__PURE__ */ new Date()).getTime()) / (1e3 * 60 * 60 * 24));
289
+ const [remind3, remind1] = config.expiryReminderDays;
290
+ if (daysUntil === remind3 || daysUntil === remind1) {
291
+ await session.send(`⚠️ 授权提醒:本群授权将于 ${formatDate(auth.expiryDate)} 到期(剩${daysUntil}天)
292
+ 授权人:${auth.authorizer || "未知"}`);
367
293
  }
368
294
  }
369
- __name(checkExpiryTime, "checkExpiryTime");
370
- async function queryExpiryTime(session, targetId) {
371
- if (!targetId) {
372
- return await checkExpiryTime(session);
373
- }
374
- const channelId = targetId.startsWith("private:") ? targetId : targetId;
375
- const [record] = await ctx.database.get("qxgl_satori_auth", {
376
- platform: session.platform,
377
- channelId
378
- });
379
- if (!record || !record.expiryDate) {
380
- return `未找到授权记录:${targetId}`;
381
- } else {
382
- return `授权到期时间:${formatDate(record.expiryDate)}
383
- 开启人:${record.authorizer || "蒙面人"}`;
384
- }
295
+ __name(checkAndUpdateExpiry, "checkAndUpdateExpiry");
296
+ function getFilePath(channelId, isGlobal = false) {
297
+ if (isGlobal) return path.join(dataDir, "global.json");
298
+ return path.join(dataDir, `${channelId}.json`);
385
299
  }
386
- __name(queryExpiryTime, "queryExpiryTime");
387
- async function globalDelayExpiry(session, days) {
388
- const daysNum = parseInt(days);
389
- if (isNaN(daysNum) || daysNum <= 0) return "请输入有效的天数(正整数)";
390
- const records = await ctx.database.get("qxgl_satori_auth");
391
- const currentDate = /* @__PURE__ */ new Date();
392
- let updatedCount = 0;
393
- for (const record of records) {
394
- if (record.expiryDate) {
395
- const currentExpiry = new Date(record.expiryDate);
396
- if (currentExpiry <= currentDate) continue;
397
- const newExpiry = new Date(currentExpiry.getTime() + daysNum * 24 * 60 * 60 * 1e3);
398
- await ctx.database.set(
399
- "qxgl_satori_auth",
400
- { platform: record.platform, channelId: record.channelId },
401
- { expiryDate: newExpiry, updateDate: currentDate }
402
- );
403
- updatedCount++;
300
+ __name(getFilePath, "getFilePath");
301
+ async function downloadImage(url, scope = "global") {
302
+ try {
303
+ const buffer = await ctx.http.get(url, { responseType: "arraybuffer" }).then((r) => Buffer.from(r));
304
+ const folder = path.join(imagesDir, scope);
305
+ if (!fsSync.existsSync(folder)) {
306
+ fsSync.mkdirSync(folder, { recursive: true });
404
307
  }
308
+ const ext = (url.match(/\.(\w+)(?:\?|$)/)?.[1] || config.defaultImageExt).toLowerCase();
309
+ const fileName = `${Date.now()}_${Math.random().toString(36).slice(2)}.${ext}`;
310
+ const filePath = path.join(folder, fileName);
311
+ await fs.writeFile(filePath, buffer);
312
+ return { filePath, buffer };
313
+ } catch (e) {
314
+ logger.error(`下载图片失败: ${e.message}`);
315
+ throw new Error("图片下载失败");
405
316
  }
406
- return `已为 ${updatedCount} 个群聊延期 ${daysNum} 天`;
407
317
  }
408
- __name(globalDelayExpiry, "globalDelayExpiry");
409
- async function globalReduceExpiry(session, days) {
410
- const daysNum = parseInt(days);
411
- if (isNaN(daysNum) || daysNum <= 0) return "请输入有效的天数(正整数)";
412
- const records = await ctx.database.get("qxgl_satori_auth");
413
- const currentDate = /* @__PURE__ */ new Date();
414
- let updatedCount = 0;
415
- for (const record of records) {
416
- if (record.expiryDate) {
417
- const currentExpiry = new Date(record.expiryDate);
418
- if (currentExpiry <= currentDate) continue;
419
- const newExpiry = new Date(currentExpiry.getTime() - daysNum * 24 * 60 * 60 * 1e3);
420
- await ctx.database.set(
421
- "qxgl_satori_auth",
422
- { platform: record.platform, channelId: record.channelId },
423
- { expiryDate: newExpiry, updateDate: currentDate }
424
- );
425
- updatedCount++;
318
+ __name(downloadImage, "downloadImage");
319
+ function resolveImagePath(filePath) {
320
+ if (config.localPathMapping) {
321
+ const [host, container] = config.localPathMapping.split(":");
322
+ if (host && container && filePath.startsWith(host)) {
323
+ filePath = filePath.replace(host, container);
426
324
  }
427
325
  }
428
- return `已为 ${updatedCount} 个群聊减少 ${daysNum} 天`;
429
- }
430
- __name(globalReduceExpiry, "globalReduceExpiry");
431
- function escapeRegExp(string) {
432
- return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
326
+ return filePath;
433
327
  }
434
- __name(escapeRegExp, "escapeRegExp");
435
- async function downloadImage(url, rootDir, isGlobal, session) {
436
- try {
437
- const buf = await ctx.http.get(url, { responseType: "arraybuffer" }).then((r) => Buffer.from(r));
438
- const folder = path.join(rootDir, isGlobal ? "global" : `${session.platform}_${session.channelId}`);
439
- await fsPromises.mkdir(folder, { recursive: true });
440
- const fileName = `${Date.now()}_${Math.random().toString(36).slice(2)}.${config.defaultImageExtension}`;
441
- const localPath = path.join(folder, fileName);
442
- await fsPromises.writeFile(localPath, buf);
443
- return localPath;
444
- } catch (e) {
445
- logger.error(`下载图片失败: ${e.message}`);
446
- throw new Error(`图片下载失败,请更换链接`);
328
+ __name(resolveImagePath, "resolveImagePath");
329
+ async function formatImageReply(imagePathOrData) {
330
+ if (config.imageSendMode === "base64") {
331
+ let buffer;
332
+ if (imagePathOrData.startsWith("data:")) {
333
+ return h.image(imagePathOrData);
334
+ } else if (imagePathOrData.startsWith("http")) {
335
+ const result = await downloadImage(imagePathOrData);
336
+ buffer = result.buffer;
337
+ } else {
338
+ const realPath = resolveImagePath(imagePathOrData);
339
+ buffer = await fs.readFile(realPath);
340
+ }
341
+ const base64 = buffer.toString("base64");
342
+ return h.image(`data:image/png;base64,${base64}`);
343
+ } else {
344
+ if (imagePathOrData.startsWith("http")) {
345
+ const { filePath } = await downloadImage(imagePathOrData);
346
+ const realPath = resolveImagePath(filePath);
347
+ return h.image(pathToFileURL(realPath).href);
348
+ } else {
349
+ const realPath = resolveImagePath(imagePathOrData);
350
+ if (!fsSync.existsSync(realPath)) {
351
+ return h.text("[图片文件丢失]");
352
+ }
353
+ return h.image(pathToFileURL(realPath).href);
354
+ }
447
355
  }
448
356
  }
449
- __name(downloadImage, "downloadImage");
450
- async function parseReplyContent(reply, rootDir, session, options) {
451
- const elements = h.parse(reply);
357
+ __name(formatImageReply, "formatImageReply");
358
+ async function parseReplyContent(replyContent, scope = "global") {
359
+ const elements = h.parse(replyContent);
452
360
  const results = [];
453
- for (const element of elements) {
454
- let item = null;
455
- if (element.type === "img" || element.type === "image") {
456
- const localPath = await downloadImage(element.attrs.src, rootDir, options.global, session);
457
- item = {
458
- type: "image",
459
- text: localPath,
460
- fileSize: element.attrs.fileSize || "",
461
- replyway: options.forward || config.MultisegmentAdditionRecoveryEffect
462
- };
463
- } else if (element.type === "text") {
464
- item = {
465
- type: "text",
466
- text: element.attrs.content,
467
- replyway: options.forward || config.MultisegmentAdditionRecoveryEffect
468
- };
469
- } else if (element.type === "at") {
470
- item = {
471
- type: "at",
472
- text: element.attrs.id,
473
- replyway: options.forward || config.MultisegmentAdditionRecoveryEffect
474
- };
475
- } else if (element.type === "audio") {
476
- item = { type: "audio", text: element.attrs.path || element.attrs.url, replyway: options.forward || config.MultisegmentAdditionRecoveryEffect };
477
- } else if (element.type === "video") {
478
- item = { type: "video", text: element.attrs.src, replyway: options.forward || config.MultisegmentAdditionRecoveryEffect };
361
+ for (const el of elements) {
362
+ if (el.type === "img" || el.type === "image") {
363
+ try {
364
+ const { filePath } = await downloadImage(el.attrs.src, scope);
365
+ results.push({
366
+ type: "image",
367
+ content: filePath,
368
+ replyMode: config.multisegmentMode
369
+ });
370
+ } catch (e) {
371
+ results.push({ type: "text", content: `[图片下载失败]`, replyMode: config.multisegmentMode });
372
+ }
373
+ } else if (el.type === "text") {
374
+ results.push({ type: "text", content: el.attrs.content, replyMode: config.multisegmentMode });
375
+ } else if (el.type === "at") {
376
+ results.push({ type: "at", content: el.attrs.id, replyMode: config.multisegmentMode });
377
+ } else if (el.type === "audio") {
378
+ results.push({ type: "audio", content: el.attrs.url || el.attrs.path, replyMode: config.multisegmentMode });
379
+ } else if (el.type === "video") {
380
+ results.push({ type: "video", content: el.attrs.src, replyMode: config.multisegmentMode });
479
381
  }
480
- if (item) results.push(item);
481
382
  }
482
383
  return results;
483
384
  }
484
385
  __name(parseReplyContent, "parseReplyContent");
485
- async function formatReply(reply, session) {
486
- if (reply.type === "image") {
487
- if (config.pictureStrategy === "http") {
488
- const fileName = path.basename(reply.text);
489
- const url = `${config.httpServePath}/${reply.text.includes("/global/") ? "global/" : ""}${fileName}`;
490
- return h.image(url);
386
+ async function sendFormattedReply(session, replyGroup, authRecord) {
387
+ const mode = replyGroup[0]?.replyMode || config.multisegmentMode;
388
+ const replaceVars = /* @__PURE__ */ __name((text) => {
389
+ if (!text || typeof text !== "string") return text;
390
+ if (authRecord) {
391
+ text = text.replace(/\{expiryDate\}/g, formatDate(authRecord.expiryDate));
392
+ text = text.replace(/\{expiryTime\}/g, formatDate(authRecord.expiryDate));
393
+ text = text.replace(/\{authorizer\}/g, authRecord.authorizer || "未设置");
394
+ text = text.replace(/\{channelId\}/g, authRecord.channelId);
395
+ text = text.replace(/\{updateDate\}/g, formatDate(authRecord.updateDate));
491
396
  } else {
492
- if (isSatoriAdapter(session)) {
493
- return h.image(reply.text);
494
- } else {
495
- if (!reply.text.startsWith("http") && !reply.text.startsWith("data:")) {
496
- return h.image(pathToFileURL(reply.text).href);
497
- }
498
- return h.image(reply.text);
397
+ text = text.replace(/\{expiryDate\}/g, "未设置");
398
+ text = text.replace(/\{expiryTime\}/g, "未设置");
399
+ text = text.replace(/\{authorizer\}/g, "未设置");
400
+ }
401
+ return text;
402
+ }, "replaceVars");
403
+ if (mode === "1") {
404
+ for (const item of replyGroup) {
405
+ if (item.type === "text") {
406
+ await session.send(h.text(replaceVars(item.content)));
407
+ } else if (item.type === "image") {
408
+ await session.send(await formatImageReply(item.content));
409
+ } else if (item.type === "at") {
410
+ await session.send(h.at(item.content));
411
+ } else if (item.type === "audio") {
412
+ await session.send(h.audio(item.content));
413
+ } else if (item.type === "video") {
414
+ await session.send(h.video(item.content));
499
415
  }
500
416
  }
501
- } else if (reply.type === "text") {
502
- return h.text(reply.text);
503
- } else if (reply.type === "at") {
504
- return h.at(reply.text);
505
- } else if (reply.type === "audio") {
506
- return h.audio(reply.text);
507
- } else if (reply.type === "video") {
508
- return h.video(reply.text);
509
- }
510
- return h.text(String(reply.text));
511
- }
512
- __name(formatReply, "formatReply");
513
- async function addKeywordReply(session, filePath, keyword, options) {
514
- let data = {};
515
- if (fs.existsSync(filePath)) {
516
- try {
517
- data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
518
- } catch (e) {
519
- data = {};
417
+ } else if (mode === "2") {
418
+ let combined = "";
419
+ for (const item of replyGroup) {
420
+ if (item.type === "text") {
421
+ combined += replaceVars(item.content) + "\n";
422
+ } else if (item.type === "image") {
423
+ await session.send(h.text(combined.trim()));
424
+ combined = "";
425
+ await session.send(await formatImageReply(item.content));
426
+ } else if (item.type === "at") {
427
+ combined += `@${item.content} `;
428
+ }
520
429
  }
521
- }
522
- if (config.Treat_all_as_lowercase) keyword = keyword.toLowerCase();
523
- const key = options.regex ? `regex:${keyword}` : keyword;
524
- if (!data[key]) data[key] = [];
525
- if (data[key].length > 0 && config.HandleDuplicateKeywords === "3") {
526
- return `关键词 "${keyword}" 已存在,请先删除`;
527
- }
528
- if (config.HandleDuplicateKeywords === "1") data[key] = [];
529
- if (config.AlwayPrompt === "2" || config.AlwayPrompt === "3") {
530
- await session.send(config.Prompt);
531
- }
532
- const replies = [];
533
- if (config.MatchPatternForExit === "1") {
534
- const timeout = config.addKeywordTime * 6e4;
535
- const reply = await session.prompt(timeout);
536
- if (!reply) return "输入超时";
537
- if (reply.includes(config.KeywordOfEsc)) return "已取消";
538
- const parsed = await parseReplyContent(reply, root, session, options);
539
- replies.push(parsed);
540
- } else {
541
- while (true) {
542
- if (config.AlwayPrompt === "3") await session.send(config.Prompt);
543
- const timeout = config.addKeywordTime * 6e4;
544
- const reply = await session.prompt(timeout);
545
- if (!reply) return "输入超时";
546
- if (reply.includes(config.KeywordOfEsc)) return "已取消";
547
- if (config.MatchPatternForExit === "2" && reply === config.KeywordOfEnd) break;
548
- if (config.MatchPatternForExit === "3" && reply.includes(config.KeywordOfEnd)) break;
549
- if (config.MatchPatternForExit === "4" && (reply === config.KeywordOfEnd || reply.includes(config.KeywordOfEnd))) break;
550
- const parsed = await parseReplyContent(reply, root, session, options);
551
- replies.push(...parsed);
430
+ if (combined.trim()) {
431
+ await session.send(h.text(combined.trim()));
432
+ }
433
+ } else if (mode === "3" || mode === "4") {
434
+ const children = [];
435
+ for (const item of replyGroup) {
436
+ if (item.type === "text") {
437
+ children.push(h.text(replaceVars(item.content)));
438
+ } else if (item.type === "image") {
439
+ children.push(await formatImageReply(item.content));
440
+ } else if (item.type === "at") {
441
+ children.push(h.at(item.content));
442
+ }
552
443
  }
444
+ await session.send(h("figure", {}, children));
553
445
  }
554
- data[key].push(replies);
555
- fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
556
- return `关键词 "${keyword}" 的回复已添加`;
557
446
  }
558
- __name(addKeywordReply, "addKeywordReply");
559
- async function deleteKeywordReply(session, filePath, keyword, specifiedIndex) {
560
- if (!fs.existsSync(filePath)) return `关键词 "${keyword}" 不存在`;
561
- let data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
562
- const searchKey = config.Treat_all_as_lowercase ? keyword.toLowerCase() : keyword;
563
- let foundKey = null;
564
- for (const k in data) {
565
- const checkKey = config.Treat_all_as_lowercase ? k.toLowerCase() : k;
566
- const realKey = checkKey.startsWith("regex:") ? checkKey.slice(6) : checkKey;
567
- if (realKey === searchKey) {
568
- foundKey = k;
569
- break;
447
+ __name(sendFormattedReply, "sendFormattedReply");
448
+ async function matchKeyword(session, content, isTest = false) {
449
+ const searchFiles = isTest ? [path.join(dataDir, "test.json")] : [
450
+ path.join(dataDir, "global.json"),
451
+ path.join(dataDir, `${session.channelId}.json`)
452
+ ];
453
+ for (const filePath of searchFiles) {
454
+ if (!fsSync.existsSync(filePath)) continue;
455
+ const data = JSON.parse(fsSync.readFileSync(filePath, "utf-8"));
456
+ const keys = Object.keys(data).sort((a, b) => {
457
+ const lenA = a.startsWith("regex:") ? a.slice(6).length : a.length;
458
+ const lenB = b.startsWith("regex:") ? b.slice(6).length : b.length;
459
+ return lenB - lenA;
460
+ });
461
+ for (const key of keys) {
462
+ let isMatch = false;
463
+ let cleanKey = config.treatAsLowercase ? key.toLowerCase() : key;
464
+ let cleanContent = config.treatAsLowercase ? content.toLowerCase() : content;
465
+ if (key.startsWith("regex:")) {
466
+ const pattern = key.slice(6);
467
+ try {
468
+ const regex = new RegExp(pattern);
469
+ isMatch = regex.test(content);
470
+ } catch (e) {
471
+ continue;
472
+ }
473
+ } else {
474
+ for (const prefix of config.matchPrefixes) {
475
+ const prefixedKey = prefix + cleanKey;
476
+ if (cleanContent === prefixedKey) {
477
+ isMatch = true;
478
+ break;
479
+ }
480
+ const match = cleanContent.match(new RegExp(`^${escapeRegExp(prefixedKey)}\\s*(\\d+)$`));
481
+ if (match) {
482
+ const idx = parseInt(match[1]);
483
+ if (data[key][idx - 1]) {
484
+ const authRecord = await ctx.database.get("qxgl_satori_auth", {
485
+ platform: session.platform,
486
+ channelId: session.channelId
487
+ }).then((r) => r[0] || null);
488
+ await sendFormattedReply(session, data[key][idx - 1], authRecord);
489
+ return true;
490
+ }
491
+ }
492
+ }
493
+ }
494
+ if (isMatch) {
495
+ const limitKey = config.frequencyScope === "channel" ? `${key}:${session.channelId}` : key;
496
+ const now = Date.now();
497
+ if (lastTriggerTimes[limitKey] && now - lastTriggerTimes[limitKey] < config.frequencyLimit * 1e3) {
498
+ return true;
499
+ }
500
+ lastTriggerTimes[limitKey] = now;
501
+ const authRecord = await ctx.database.get("qxgl_satori_auth", {
502
+ platform: session.platform,
503
+ channelId: session.channelId
504
+ }).then((r) => r[0] || null);
505
+ const replies = data[key];
506
+ const selected = replies[Math.floor(Math.random() * replies.length)];
507
+ await sendFormattedReply(session, selected, authRecord);
508
+ return true;
509
+ }
570
510
  }
571
511
  }
572
- if (!foundKey) return `关键词 "${keyword}" 不存在`;
573
- const replies = data[foundKey];
574
- if (config.Delete_Branch_Only && replies.length > 1) {
575
- if (!specifiedIndex) {
576
- return `该关键词有 ${replies.length} 个回复,请使用 -q 指定删除序号(1-${replies.length})`;
512
+ return false;
513
+ }
514
+ __name(matchKeyword, "matchKeyword");
515
+ const AUTH_COMMANDS = [
516
+ "群授权",
517
+ "私聊授权",
518
+ "取消授权",
519
+ "更换授权",
520
+ "到期时间",
521
+ "查询到期",
522
+ "全局延期",
523
+ "全局减少",
524
+ "更新名称",
525
+ "列出已记录群",
526
+ "备份数据"
527
+ ];
528
+ function isAuthCommand(content) {
529
+ const trimmed = content.trim();
530
+ for (const cmd of AUTH_COMMANDS) {
531
+ if (trimmed.startsWith(cmd) || trimmed.startsWith("/" + cmd) || trimmed.startsWith("#" + cmd)) {
532
+ return true;
577
533
  }
578
- const idx = parseInt(specifiedIndex) - 1;
579
- if (idx < 0 || idx >= replies.length) return "序号无效";
580
- replies.splice(idx, 1);
581
- if (replies.length === 0) delete data[foundKey];
582
- fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
583
- return `已删除 "${keyword}" 的第 ${specifiedIndex} 个回复`;
584
- } else {
585
- delete data[foundKey];
586
- fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
587
- return `关键词 "${keyword}" 已删除`;
588
534
  }
535
+ return false;
589
536
  }
590
- __name(deleteKeywordReply, "deleteKeywordReply");
591
- async function getTestChannels(session) {
592
- const records = await ctx.database.get("qxgl_satori_auth", { platform: session.platform });
593
- if (records.length > 0 && records[0].testChannels) {
594
- return records[0].testChannels;
537
+ __name(isAuthCommand, "isAuthCommand");
538
+ ctx.middleware(async (session, next) => {
539
+ const { platform, channelId } = session;
540
+ if (isAuthCommand(session.content)) {
541
+ return next();
595
542
  }
596
- return [];
597
- }
598
- __name(getTestChannels, "getTestChannels");
599
- async function setTestChannels(session, channels) {
600
- await ctx.database.set(
601
- "qxgl_satori_auth",
602
- { platform: session.platform },
603
- { testChannels: channels }
604
- );
605
- }
606
- __name(setTestChannels, "setTestChannels");
607
- ctx.before("command", (session, command) => {
608
- const whiteList = [
609
- "qxgl-satori.群授权",
610
- "qxgl-satori.私聊授权",
611
- "qxgl-satori.取消授权",
612
- "qxgl-satori.更换授权",
613
- "qxgl-satori.到期时间",
614
- "qxgl-satori.查询到期",
615
- "qxgl-satori.全局延期",
616
- "qxgl-satori.全局减少",
617
- "qxgl-satori.列出已记录群",
618
- "qxgl-satori.更新名称"
619
- ];
620
- if (whiteList.includes(command.name)) {
621
- session.skipAuthCheck = true;
543
+ if (config.authorizationMode !== "disabled") {
544
+ await ensureChannelRecorded(session);
622
545
  }
623
- return true;
624
- }, true);
625
- ctx.middleware(async (session, next) => {
626
- if (!session.channelId) return next();
627
- const [record] = await ctx.database.get("qxgl_satori_auth", {
628
- platform: session.platform,
629
- channelId: session.channelId
630
- });
631
- if (!record) {
632
- const channelName = await updateChannelName(session);
633
- await ctx.database.create("qxgl_satori_auth", {
634
- platform: session.platform,
635
- channelId: session.channelId,
636
- expiryDate: config.enableAuthSystem ? null : /* @__PURE__ */ new Date("2099-12-31"),
637
- isblockedchannel: false,
638
- authorizer: "",
639
- channelName,
640
- updateDate: /* @__PURE__ */ new Date(),
641
- testChannels: []
642
- });
643
- logInfo(`自动记录新群聊: ${session.channelId}`);
546
+ if (config.authorizationMode === "strict") {
547
+ const authorized = await isAuthorized(session);
548
+ if (!authorized) {
549
+ logInfo(`拦截未授权群消息: ${channelId}`);
550
+ const record = await ctx.database.get("qxgl_satori_auth", { platform, channelId });
551
+ if (record.length > 0 && record[0].expiryDate) {
552
+ const expired = (/* @__PURE__ */ new Date()).getTime() > new Date(record[0].expiryDate).getTime();
553
+ if (expired && config.autoLeaveOnExpiry) {
554
+ try {
555
+ await session.bot?.leaveGuild?.(channelId);
556
+ } catch (e) {
557
+ logger.warn(`自动退群失败: ${e.message}`);
558
+ }
559
+ }
560
+ }
561
+ return;
562
+ }
644
563
  }
564
+ await checkAndUpdateExpiry(session);
645
565
  return next();
646
566
  }, true);
647
567
  ctx.middleware(async (session, next) => {
648
- if (session.skipAuthCheck) return next();
649
- if (!config.enableAuthSystem) return next();
650
- const [record] = await ctx.database.get("qxgl_satori_auth", {
568
+ let content = session.content;
569
+ if (config.unifiedAtField) {
570
+ content = content.replace(/<at\s+id="(\d+)"[^>]*>/g, '<at id="$1"/>');
571
+ }
572
+ content = content.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"');
573
+ if (config.treatAsLowercase) {
574
+ content = content.toLowerCase();
575
+ }
576
+ let isTest = false;
577
+ const authRecord = await ctx.database.get("qxgl_satori_auth", {
651
578
  platform: session.platform,
652
579
  channelId: session.channelId
653
580
  });
654
- if (record?.expiryDate) {
655
- const expiry = new Date(record.expiryDate);
656
- const now = /* @__PURE__ */ new Date();
657
- if (now > expiry) {
658
- if (config.autoLeaveOnExpiry && session.platform === "onebot") {
659
- await session.bot?.leaveGroup?.(session.channelId);
660
- }
661
- await ctx.database.set(
662
- "qxgl_satori_auth",
663
- { platform: session.platform, channelId: session.channelId },
664
- { isblockedchannel: true }
665
- );
666
- return;
667
- }
581
+ if (authRecord.length > 0 && authRecord[0].testChannels) {
582
+ const testChannels = authRecord[0].testChannels;
583
+ isTest = testChannels.includes(session.channelId) || testChannels.includes(`private:${session.userId}`);
668
584
  }
669
- if (record?.isblockedchannel) return;
585
+ const matched = await matchKeyword(session, content, isTest);
586
+ if (matched) return;
670
587
  return next();
671
588
  });
672
- ctx.command(`${pluginName}/群授权 <channelId> <months> [authorizer]`, "群授权", { authority: config.commandAuthority }).action(async ({ session }, channelId, months, authorizer) => {
673
- return await authorizeGroup(session, channelId, months, authorizer);
589
+ ctx.command(pluginName);
590
+ ctx.command(pluginName + "/群授权 <channelId> <months> [authorizer]", "群授权管理").authority(config.commandAuthority).action(async ({ session }, channelId, months, authorizer = "蒙面人") => {
591
+ if (!channelId || !months) return "格式错误:群授权 <群号> <±月数> [授权人]";
592
+ const monthsNum = parseInt(months);
593
+ if (isNaN(monthsNum)) return "请输入有效的月份数(正数增加,负数减少)";
594
+ let groupName = "未知群聊";
595
+ try {
596
+ if (session.bot?.getGuild) {
597
+ const guild = await session.bot.getGuild(channelId);
598
+ groupName = guild?.name || groupName;
599
+ }
600
+ } catch (e) {
601
+ }
602
+ const { platform } = session;
603
+ const record = await ctx.database.get("qxgl_satori_auth", { platform, channelId });
604
+ const now = /* @__PURE__ */ new Date();
605
+ let newExpiry;
606
+ if (record.length === 0) {
607
+ newExpiry = new Date(now.getFullYear(), now.getMonth() + monthsNum, now.getDate());
608
+ await ctx.database.create("qxgl_satori_auth", {
609
+ platform,
610
+ channelId,
611
+ expiryDate: newExpiry,
612
+ isBlocked: false,
613
+ authorizer,
614
+ channelName: groupName,
615
+ updateDate: now,
616
+ testChannels: []
617
+ });
618
+ } else {
619
+ const current = record[0].expiryDate ? new Date(record[0].expiryDate) : now;
620
+ newExpiry = new Date(current.getFullYear(), current.getMonth() + monthsNum, current.getDate());
621
+ await ctx.database.set("qxgl_satori_auth", { platform, channelId }, {
622
+ expiryDate: newExpiry,
623
+ authorizer,
624
+ channelName: groupName,
625
+ updateDate: now,
626
+ isBlocked: false
627
+ });
628
+ }
629
+ const action = monthsNum > 0 ? "增加" : "减少";
630
+ return `群号 "${channelId}" 授权已${action} ${Math.abs(monthsNum)} 个月
631
+ 到期时间:${formatDate(newExpiry)}
632
+ 授权人:${authorizer}
633
+ 更新日期:${formatDate(now)}`;
634
+ });
635
+ ctx.command(pluginName + "/私聊授权 <userId> <months> [authorizer]", "私聊授权管理").authority(config.commandAuthority).action(async ({ session }, userId, months, authorizer = "蒙面人") => {
636
+ if (!userId || !months) return "格式错误:私聊授权 <用户ID> <±月数> [授权人]";
637
+ const monthsNum = parseInt(months);
638
+ if (isNaN(monthsNum)) return "请输入有效的月份数";
639
+ const channelId = `private:${userId}`;
640
+ const { platform } = session;
641
+ const record = await ctx.database.get("qxgl_satori_auth", { platform, channelId });
642
+ const now = /* @__PURE__ */ new Date();
643
+ let newExpiry;
644
+ if (record.length === 0) {
645
+ newExpiry = new Date(now.getFullYear(), now.getMonth() + monthsNum, now.getDate());
646
+ await ctx.database.create("qxgl_satori_auth", {
647
+ platform,
648
+ channelId,
649
+ expiryDate: newExpiry,
650
+ isBlocked: false,
651
+ authorizer,
652
+ channelName: "私聊",
653
+ updateDate: now,
654
+ testChannels: []
655
+ });
656
+ } else {
657
+ const current = record[0].expiryDate ? new Date(record[0].expiryDate) : now;
658
+ newExpiry = new Date(current.getFullYear(), current.getMonth() + monthsNum, current.getDate());
659
+ await ctx.database.set("qxgl_satori_auth", { platform, channelId }, {
660
+ expiryDate: newExpiry,
661
+ authorizer,
662
+ updateDate: now,
663
+ isBlocked: false
664
+ });
665
+ }
666
+ const action = monthsNum > 0 ? "增加" : "减少";
667
+ return `用户 "${userId}" 授权已${action} ${Math.abs(monthsNum)} 个月
668
+ 到期时间:${formatDate(newExpiry)}
669
+ 授权人:${authorizer}`;
674
670
  });
675
- ctx.command(`${pluginName}/私聊授权 <userId> <months> [authorizer]`, "私聊授权", { authority: config.commandAuthority }).action(async ({ session }, userId, months, authorizer) => {
676
- return await authorizePrivate(session, userId, months, authorizer);
671
+ ctx.command(pluginName + "/取消授权 <targetId>", "取消指定群/私聊授权").authority(config.commandAuthority).action(async ({ session }, targetId) => {
672
+ if (!targetId) return "请提供目标ID";
673
+ const channelId = targetId.startsWith("private:") ? targetId : targetId;
674
+ const { platform } = session;
675
+ const record = await ctx.database.get("qxgl_satori_auth", { platform, channelId });
676
+ if (record.length === 0) return `未找到授权记录:${targetId}`;
677
+ await ctx.database.remove("qxgl_satori_auth", { platform, channelId });
678
+ return `已取消授权:${targetId}`;
677
679
  });
678
- ctx.command(`${pluginName}/取消授权 <targetId>`, "取消授权", { authority: config.commandAuthority }).action(async ({ session }, targetId) => {
679
- return await cancelAuthorization(session, targetId);
680
+ ctx.command(pluginName + "/更换授权 <sourceId> <targetId>", "迁移授权数据").authority(config.commandAuthority).action(async ({ session }, sourceId, targetId) => {
681
+ if (!sourceId || !targetId) return "格式:更换授权 <原ID> <新ID>";
682
+ const sourceChannel = sourceId.startsWith("private:") ? sourceId : sourceId;
683
+ const targetChannel = targetId.startsWith("private:") ? targetId : targetId;
684
+ const { platform } = session;
685
+ const source = await ctx.database.get("qxgl_satori_auth", { platform, channelId: sourceChannel });
686
+ if (source.length === 0) return `原目标未找到:${sourceId}`;
687
+ const target = await ctx.database.get("qxgl_satori_auth", { platform, channelId: targetChannel });
688
+ if (target.length > 0) return `新目标已存在授权:${targetId}`;
689
+ const data = source[0];
690
+ await ctx.database.create("qxgl_satori_auth", {
691
+ ...data,
692
+ channelId: targetChannel,
693
+ channelName: "新迁移群聊",
694
+ updateDate: /* @__PURE__ */ new Date()
695
+ });
696
+ await ctx.database.remove("qxgl_satori_auth", { platform, channelId: sourceChannel });
697
+ return `授权已从 ${sourceId} 迁移到 ${targetId}`;
680
698
  });
681
- ctx.command(`${pluginName}/更换授权 <sourceId> <targetId>`, "更换授权", { authority: config.commandAuthority }).action(async ({ session }, sourceId, targetId) => {
682
- return await changeAuthorization(session, sourceId, targetId);
699
+ ctx.command(pluginName + "/到期时间", "查询当前授权到期时间").authority(0).action(async ({ session }) => {
700
+ const { platform, channelId } = session;
701
+ const record = await ctx.database.get("qxgl_satori_auth", { platform, channelId });
702
+ if (record.length === 0 || !record[0].expiryDate) {
703
+ return "当前未设置到期时间(永久授权或无授权记录)";
704
+ }
705
+ return `到期时间:${formatDate(record[0].expiryDate)}
706
+ 授权人:${record[0].authorizer || "未知"}
707
+ 更新于:${formatDate(record[0].updateDate)}`;
683
708
  });
684
- ctx.command(`${pluginName}/到期时间`, "查询本群/私聊到期时间", { authority: 0 }).action(async ({ session }) => {
685
- return await checkExpiryTime(session);
709
+ ctx.command(pluginName + "/查询到期 <targetId>", "查询指定目标授权").authority(config.commandAuthority).action(async ({ session }, targetId) => {
710
+ const channelId = targetId.startsWith("private:") ? targetId : targetId;
711
+ const record = await ctx.database.get("qxgl_satori_auth", {
712
+ platform: session.platform,
713
+ channelId
714
+ });
715
+ if (record.length === 0) return `未找到授权记录:${targetId}`;
716
+ return `目标:${targetId}
717
+ 到期:${formatDate(record[0].expiryDate)}
718
+ 授权人:${record[0].authorizer || "未知"}`;
686
719
  });
687
- ctx.command(`${pluginName}/查询到期 [targetId]`, "查询指定群/私聊到期时间", { authority: config.commandAuthority }).action(async ({ session }, targetId) => {
688
- return await queryExpiryTime(session, targetId);
720
+ ctx.command(pluginName + "/全局延期 <days>", "为所有授权增加天数").authority(config.commandAuthority).action(async ({ session }, days) => {
721
+ const d = parseInt(days);
722
+ if (isNaN(d) || d <= 0) return "请输入正整数天数";
723
+ const records = await ctx.database.get("qxgl_satori_auth");
724
+ const now = /* @__PURE__ */ new Date();
725
+ let count = 0;
726
+ for (const r of records) {
727
+ if (!r.expiryDate) continue;
728
+ if (new Date(r.expiryDate).getTime() <= now.getTime()) continue;
729
+ const newDate = new Date(new Date(r.expiryDate).getTime() + d * 864e5);
730
+ await ctx.database.set(
731
+ "qxgl_satori_auth",
732
+ { platform: r.platform, channelId: r.channelId },
733
+ { expiryDate: newDate, updateDate: now }
734
+ );
735
+ count++;
736
+ }
737
+ return `已为 ${count} 个有效授权延期 ${d} 天`;
689
738
  });
690
- ctx.command(`${pluginName}/全局延期 <days>`, "为所有群统一延期", { authority: config.commandAuthority }).action(async ({ session }, days) => {
691
- return await globalDelayExpiry(session, days);
739
+ ctx.command(pluginName + "/全局减少 <days>", "为所有授权减少天数").authority(config.commandAuthority).action(async ({ session }, days) => {
740
+ const d = parseInt(days);
741
+ if (isNaN(d) || d <= 0) return "请输入正整数天数";
742
+ const records = await ctx.database.get("qxgl_satori_auth");
743
+ const now = /* @__PURE__ */ new Date();
744
+ let count = 0;
745
+ for (const r of records) {
746
+ if (!r.expiryDate) continue;
747
+ if (new Date(r.expiryDate).getTime() <= now.getTime()) continue;
748
+ const newDate = new Date(new Date(r.expiryDate).getTime() - d * 864e5);
749
+ await ctx.database.set(
750
+ "qxgl_satori_auth",
751
+ { platform: r.platform, channelId: r.channelId },
752
+ { expiryDate: newDate, updateDate: now }
753
+ );
754
+ count++;
755
+ }
756
+ return `已为 ${count} 个授权减少 ${d} 天`;
692
757
  });
693
- ctx.command(`${pluginName}/全局减少 <days>`, "为所有群统一减少", { authority: config.commandAuthority }).action(async ({ session }, days) => {
694
- return await globalReduceExpiry(session, days);
758
+ ctx.command(pluginName + "/更新名称", "刷新群名称缓存").authority(config.commandAuthority).action(async ({ session }) => {
759
+ const records = await ctx.database.get("qxgl_satori_auth");
760
+ let count = 0;
761
+ for (const r of records) {
762
+ if (r.channelId.startsWith("private:")) continue;
763
+ try {
764
+ if (session.bot?.getGuild) {
765
+ const guild = await session.bot.getGuild(r.channelId);
766
+ if (guild?.name) {
767
+ await ctx.database.set(
768
+ "qxgl_satori_auth",
769
+ { platform: r.platform, channelId: r.channelId },
770
+ { channelName: guild.name }
771
+ );
772
+ count++;
773
+ }
774
+ }
775
+ } catch (e) {
776
+ }
777
+ }
778
+ return `已更新 ${count} 个群名称`;
695
779
  });
696
- ctx.command(`${pluginName}/列出已记录群`, "列出所有已记录的群", { authority: config.commandAuthority }).action(async ({ session }) => {
780
+ ctx.command(pluginName + "/列出已记录群", "列出所有群授权状态").authority(config.commandAuthority).action(async () => {
697
781
  const records = await ctx.database.get("qxgl_satori_auth");
698
782
  const groups = records.filter((r) => !r.channelId.startsWith("private:"));
699
- if (!groups.length) return "暂无群聊记录";
783
+ if (groups.length === 0) return "暂无群记录";
700
784
  return groups.map(
701
785
  (r) => `群名:${r.channelName}
702
786
  群号:${r.channelId}
703
787
  到期:${formatDate(r.expiryDate)}
704
- 开启人:${r.authorizer || "蒙面人"}
705
- 更新:${formatDate(r.updateDate)}`
788
+ 授权:${r.authorizer}
789
+ 状态:${r.isBlocked ? "已拉黑" : "正常"}`
706
790
  ).join("\n\n");
707
791
  });
708
- ctx.command(`${pluginName}/更新名称`, "刷新所有群名称缓存", { authority: config.commandAuthority }).action(async ({ session }) => {
792
+ ctx.command(pluginName + "/备份数据", "备份数据到JSON").authority(config.commandAuthority).action(async () => {
709
793
  const records = await ctx.database.get("qxgl_satori_auth");
710
- let count = 0;
711
- for (const record of records) {
712
- if (record.channelId.startsWith("private:")) continue;
713
- const name = await updateChannelName({ ...session, channelId: record.channelId });
714
- await ctx.database.set(
715
- "qxgl_satori_auth",
716
- { platform: record.platform, channelId: record.channelId },
717
- { channelName: name }
718
- );
719
- count++;
720
- }
721
- return `已更新 ${count} 个群聊名称`;
794
+ const backupPath = path.join(dataDir, `backup_${Date.now()}.json`);
795
+ await fs.writeFile(backupPath, JSON.stringify(records, null, 2));
796
+ return `已备份 ${records.length} 条记录到 ${backupPath}`;
722
797
  });
723
- ctx.command(`${pluginName}/添加 <keyword>`, "添加关键词", { authority: config.commandAuthority }).option("regex", "-x 使用正则匹配").option("forward", "-f <type> 指定回复方式(1-4)").action(async ({ session, options }, keyword) => {
798
+ async function addKeywordLogic(session, keyword, isGlobal = false, isRegex = false) {
799
+ if (!keyword) return "请输入关键词";
800
+ const filePath = isGlobal ? getFilePath(null, true) : getFilePath(session.channelId);
801
+ if (!fsSync.existsSync(filePath)) {
802
+ fsSync.writeFileSync(filePath, "{}");
803
+ }
804
+ let data;
805
+ try {
806
+ data = JSON.parse(fsSync.readFileSync(filePath, "utf-8"));
807
+ } catch {
808
+ data = {};
809
+ }
810
+ const key = isRegex ? `regex:${keyword}` : config.treatAsLowercase ? keyword.toLowerCase() : keyword;
811
+ if (data[key] && config.handleDuplicate === "reject") {
812
+ return `关键词 "${keyword}" 已存在`;
813
+ }
814
+ if (!data[key] || config.handleDuplicate === "replace") {
815
+ data[key] = [];
816
+ }
817
+ if (config.alwaysPrompt !== "never") {
818
+ await session.send('请输入回复内容(支持多段,输入"结束添加"完成,"取消添加"放弃):');
819
+ }
820
+ const replies = [];
821
+ const timeout = config.addTimeout * 6e4;
822
+ while (true) {
823
+ if (config.alwaysPrompt === "always") {
824
+ await session.send("请继续输入(结束添加/取消添加):");
825
+ }
826
+ const reply = await session.prompt(timeout);
827
+ if (!reply) return "输入超时,已取消";
828
+ if (reply.includes(config.cancelKeyword)) return "已取消添加";
829
+ if (reply.includes(config.endKeyword)) break;
830
+ const parsed = await parseReplyContent(reply, isGlobal ? "global" : session.channelId);
831
+ replies.push(...parsed);
832
+ }
833
+ if (replies.length === 0) return "未输入有效回复";
834
+ data[key].push(replies);
835
+ fsSync.writeFileSync(filePath, JSON.stringify(data, null, 2));
836
+ return `关键词 "${keyword}" 添加完成,共 ${data[key].length} 条回复`;
837
+ }
838
+ __name(addKeywordLogic, "addKeywordLogic");
839
+ ctx.command(pluginName + `/${config.triggerPrefix} [keyword]`, "添加关键词").option("regex", "-x 使用正则匹配").authority(config.commandAuthority).action(async ({ session, options }, keyword) => {
724
840
  if (!hasPermission(session, "添加")) return "权限不足";
725
- if (!keyword) return "请提供关键词";
726
- const filePath = path.join(root, `${session.platform}_${session.channelId}.json`);
727
- return await addKeywordReply(session, filePath, keyword, { ...options, global: false });
841
+ return addKeywordLogic(session, keyword, false, options.regex);
728
842
  });
729
- ctx.command(`${pluginName}/全局添加 <keyword>`, "添加全局关键词", { authority: config.commandAuthority }).option("regex", "-x 使用正则匹配").option("forward", "-f <type> 指定回复方式(1-4)").action(async ({ session, options }, keyword) => {
843
+ ctx.command(pluginName + `/${config.globalTriggerPrefix} [keyword]`, "添加全局关键词").option("regex", "-x 使用正则匹配").authority(config.commandAuthority).action(async ({ session, options }, keyword) => {
730
844
  if (!hasPermission(session, "全局添加")) return "权限不足";
731
- if (!keyword) return "请提供关键词";
732
- return await addKeywordReply(session, globalFile, keyword, { ...options, global: true });
845
+ return addKeywordLogic(session, keyword, true, options.regex);
733
846
  });
734
- ctx.command(`${pluginName}/删除 <keyword>`, "删除关键词", { authority: config.commandAuthority }).option("question", "-q <index> 指定删除序号").action(async ({ session, options }, keyword) => {
847
+ async function deleteKeywordLogic(session, keyword, isGlobal, index = null) {
848
+ if (!keyword) return "请输入关键词";
849
+ const filePath = isGlobal ? getFilePath(null, true) : getFilePath(session.channelId);
850
+ if (!fsSync.existsSync(filePath)) return "暂无数据";
851
+ const data = JSON.parse(fsSync.readFileSync(filePath, "utf-8"));
852
+ const searchKey = config.treatAsLowercase ? keyword.toLowerCase() : keyword;
853
+ let targetKey = null;
854
+ for (const k of Object.keys(data)) {
855
+ const cleanK = k.startsWith("regex:") ? k.slice(6) : k;
856
+ if (cleanK === searchKey) {
857
+ targetKey = k;
858
+ break;
859
+ }
860
+ }
861
+ if (!targetKey) return `关键词 "${keyword}" 不存在`;
862
+ if (config.deleteBranchOnly && data[targetKey].length > 1 && index === null) {
863
+ return `该关键词有 ${data[targetKey].length} 条回复,请使用 -q 指定序号删除`;
864
+ }
865
+ if (index !== null) {
866
+ const idx = parseInt(index) - 1;
867
+ if (isNaN(idx) || idx < 0 || idx >= data[targetKey].length) {
868
+ return "无效的回复序号";
869
+ }
870
+ data[targetKey].splice(idx, 1);
871
+ if (data[targetKey].length === 0) {
872
+ delete data[targetKey];
873
+ fsSync.writeFileSync(filePath, JSON.stringify(data, null, 2));
874
+ return `已删除关键词 "${keyword}"`;
875
+ }
876
+ fsSync.writeFileSync(filePath, JSON.stringify(data, null, 2));
877
+ return `已删除 "${keyword}" 的第 ${index} 条回复`;
878
+ } else {
879
+ delete data[targetKey];
880
+ fsSync.writeFileSync(filePath, JSON.stringify(data, null, 2));
881
+ return `已删除关键词 "${keyword}"`;
882
+ }
883
+ }
884
+ __name(deleteKeywordLogic, "deleteKeywordLogic");
885
+ ctx.command(pluginName + `/${config.deletePrefix} [keyword]`, "删除关键词").option("index", "-q <number> 指定回复序号").authority(config.commandAuthority).action(async ({ session, options }, keyword) => {
735
886
  if (!hasPermission(session, "删除")) return "权限不足";
736
- if (!keyword) return "请提供关键词";
737
- const filePath = path.join(root, `${session.platform}_${session.channelId}.json`);
738
- return await deleteKeywordReply(session, filePath, keyword, options.question);
887
+ return deleteKeywordLogic(session, keyword, false, options.index);
739
888
  });
740
- ctx.command(`${pluginName}/全局删除 <keyword>`, "删除全局关键词", { authority: config.commandAuthority }).option("question", "-q <index> 指定删除序号").action(async ({ session, options }, keyword) => {
889
+ ctx.command(pluginName + `/${config.globalDeletePrefix} [keyword]`, "删除全局关键词").option("index", "-q <number> 指定回复序号").authority(config.commandAuthority).action(async ({ session, options }, keyword) => {
741
890
  if (!hasPermission(session, "全局删除")) return "权限不足";
742
- if (!keyword) return "请提供关键词";
743
- return await deleteKeywordReply(session, globalFile, keyword, options.question);
891
+ return deleteKeywordLogic(session, keyword, true, options.index);
744
892
  });
745
- ctx.command(`${pluginName}/修改 <keyword>`, "修改关键词回复", { authority: config.commandAuthority }).option("question", "-q <index> 指定回复序号").action(async ({ session, options }, keyword) => {
893
+ async function fixKeywordLogic(session, keyword, isGlobal, idx = 0) {
894
+ if (!keyword) return "请输入关键词";
895
+ const filePath = isGlobal ? getFilePath(null, true) : getFilePath(session.channelId);
896
+ if (!fsSync.existsSync(filePath)) return "文件不存在";
897
+ const data = JSON.parse(fsSync.readFileSync(filePath, "utf-8"));
898
+ const searchKey = config.treatAsLowercase ? keyword.toLowerCase() : keyword;
899
+ let targetKey = null;
900
+ for (const k of Object.keys(data)) {
901
+ const cleanK = k.startsWith("regex:") ? k.slice(6) : k;
902
+ if (cleanK === searchKey) targetKey = k;
903
+ }
904
+ if (!targetKey) return `关键词 "${keyword}" 不存在`;
905
+ if (!data[targetKey][idx]) return "回复序号无效";
906
+ await session.send(`正在修改 "${keyword}" 的第 ${idx + 1} 条回复,当前内容:`);
907
+ await sendFormattedReply(session, data[targetKey][idx], null);
908
+ await session.send("请输入新内容(取消添加 放弃):");
909
+ const reply = await session.prompt(config.addTimeout * 6e4);
910
+ if (!reply || reply.includes(config.cancelKeyword)) return "已取消";
911
+ const parsed = await parseReplyContent(reply, isGlobal ? "global" : session.channelId);
912
+ data[targetKey][idx] = parsed;
913
+ fsSync.writeFileSync(filePath, JSON.stringify(data, null, 2));
914
+ return "修改完成";
915
+ }
916
+ __name(fixKeywordLogic, "fixKeywordLogic");
917
+ ctx.command(pluginName + `/${config.fixCommand} [keyword]`, "修改关键词").option("index", "-q <number> 回复序号").authority(config.commandAuthority).action(async ({ session, options }, keyword) => {
746
918
  if (!hasPermission(session, "修改")) return "权限不足";
747
- if (!keyword) return "请提供关键词";
748
- const filePath = path.join(root, `${session.platform}_${session.channelId}.json`);
749
- if (!fs.existsSync(filePath)) return "未找到数据";
750
- let data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
751
- const key = config.Treat_all_as_lowercase ? keyword.toLowerCase() : keyword;
752
- if (!data[key]) return `关键词 "${keyword}" 不存在`;
753
- const idx = (parseInt(options.question) || 1) - 1;
754
- if (idx < 0 || idx >= data[key].length) return "序号无效";
755
- let current = "";
756
- for (const item of data[key][idx]) {
757
- current += await formatReply(item, session);
758
- }
759
- await session.send(`正在修改【${keyword}】的第 ${idx + 1} 条回复:
760
- ${current}`);
761
- await session.send(`请一次性输入新内容(${config.KeywordOfEsc} 取消):`);
762
- const reply = await session.prompt(config.addKeywordTime * 6e4);
763
- if (!reply || reply.includes(config.KeywordOfEsc)) return "已取消";
764
- data[key][idx] = await parseReplyContent(reply, root, session, { global: false });
765
- fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
766
- return `已修改 "${keyword}" 的第 ${idx + 1} 条回复`;
919
+ return fixKeywordLogic(session, keyword, false, (options.index || 1) - 1);
767
920
  });
768
- ctx.command(`${pluginName}/全局修改 <keyword>`, "修改全局关键词", { authority: config.commandAuthority }).option("question", "-q <index> 指定回复序号").action(async ({ session, options }, keyword) => {
921
+ ctx.command(pluginName + `/${config.globalFixCommand} [keyword]`, "修改全局关键词").option("index", "-q <number> 回复序号").authority(config.commandAuthority).action(async ({ session, options }, keyword) => {
769
922
  if (!hasPermission(session, "全局修改")) return "权限不足";
770
- if (!keyword) return "请提供关键词";
771
- if (!fs.existsSync(globalFile)) return "未找到全局数据";
772
- let data = JSON.parse(fs.readFileSync(globalFile, "utf-8"));
773
- const key = config.Treat_all_as_lowercase ? keyword.toLowerCase() : keyword;
774
- if (!data[key]) return `全局关键词 "${keyword}" 不存在`;
775
- const idx = (parseInt(options.question) || 1) - 1;
776
- if (idx < 0 || idx >= data[key].length) return "序号无效";
777
- let current = "";
778
- for (const item of data[key][idx]) {
779
- current += await formatReply(item, session);
780
- }
781
- await session.send(`正在修改全局【${keyword}】的第 ${idx + 1} 条回复:
782
- ${current}`);
783
- await session.send(`请一次性输入新内容(${config.KeywordOfEsc} 取消):`);
784
- const reply = await session.prompt(config.addKeywordTime * 6e4);
785
- if (!reply || reply.includes(config.KeywordOfEsc)) return "已取消";
786
- data[key][idx] = await parseReplyContent(reply, root, session, { global: true });
787
- fs.writeFileSync(globalFile, JSON.stringify(data, null, 2));
788
- return `已修改全局 "${keyword}" 的第 ${idx + 1} 条回复`;
923
+ return fixKeywordLogic(session, keyword, true, (options.index || 1) - 1);
789
924
  });
790
- ctx.command(`${pluginName}/查找关键词 <keyword>`, "查找关键词", { authority: config.commandAuthority }).action(async ({ session }, keyword) => {
925
+ ctx.command(pluginName + `/${config.searchCommand} [keyword]`, "查找关键词").authority(config.commandAuthority).action(async ({ session }, keyword) => {
791
926
  if (!hasPermission(session, "查找关键词")) return "权限不足";
792
- if (!keyword) return "请提供关键词";
793
- const files = [];
794
- if (config.Search_Range === "1" || config.Search_Range === "3") {
795
- files.push(path.join(root, `${session.platform}_${session.channelId}.json`));
796
- }
797
- if (config.Search_Range === "2" || config.Search_Range === "3") {
798
- files.push(globalFile);
799
- const allFiles = fs.readdirSync(root).filter((f) => f.endsWith(".json") && f !== "test.json" && f !== "global.json");
800
- files.push(...allFiles.map((f) => path.join(root, f)));
927
+ if (!keyword) return "请输入搜索词";
928
+ let files = [];
929
+ if (config.searchRange === "local") {
930
+ files = [getFilePath(session.channelId)];
931
+ } else if (config.searchRange === "all") {
932
+ files = fsSync.readdirSync(dataDir).filter((f) => f.endsWith(".json")).map((f) => path.join(dataDir, f));
933
+ } else {
934
+ files = [getFilePath(session.channelId), getFilePath(null, true)];
801
935
  }
802
- const results = [];
803
- for (const file of [...new Set(files)]) {
804
- if (!fs.existsSync(file)) continue;
805
- const data = JSON.parse(fs.readFileSync(file, "utf-8"));
806
- for (const key in data) {
807
- const checkKey = config.Treat_all_as_lowercase ? key.toLowerCase() : key;
808
- const searchKey = config.Treat_all_as_lowercase ? keyword.toLowerCase() : keyword;
809
- if (checkKey.includes(searchKey)) {
810
- const channelId = path.basename(file, ".json").replace(`${session.platform}_`, "");
811
- const content = data[key].map(
812
- (group, i) => `回复${i + 1}: ${group.map((g) => g.type === "text" ? g.text : `[${g.type}]`).join(", ")}`
813
- ).join("\n");
814
- if (config.Find_Return_Preset === "1") results.push(`关键词:${key}
815
- ${content}`);
816
- else if (config.Find_Return_Preset === "2") results.push(`位置:${channelId}`);
817
- else results.push(`位置:${channelId}
818
- 关键词:${key}
819
- ${content}`);
820
- if (config.Return_Limit === "2") break;
936
+ let results = [];
937
+ for (const file of files) {
938
+ if (!fsSync.existsSync(file)) continue;
939
+ const data = JSON.parse(fsSync.readFileSync(file, "utf-8"));
940
+ for (const k of Object.keys(data)) {
941
+ if (k.includes(keyword)) {
942
+ results.push(`在 ${path.basename(file, ".json")} 找到:${k}(${data[k].length}条回复)`);
821
943
  }
822
944
  }
823
945
  }
824
- if (!results.length) return `未找到 "${keyword}"`;
825
- return results.join("\n\n");
946
+ return results.length ? results.slice(0, 10).join("\n") : "未找到";
826
947
  });
827
- ctx.command(`${pluginName}/查看关键词列表`, "查看关键词列表", { authority: config.commandAuthority }).action(async ({ session }) => {
948
+ ctx.command(pluginName + `/${config.viewListCommand}`, "查看关键词列表").authority(config.commandAuthority).action(async ({ session }) => {
828
949
  if (!hasPermission(session, "查看关键词列表")) return "权限不足";
829
- const filePath = path.join(root, `${session.platform}_${session.channelId}.json`);
830
- const globalData = fs.existsSync(globalFile) ? JSON.parse(fs.readFileSync(globalFile, "utf-8")) : {};
831
- const localData = fs.existsSync(filePath) ? JSON.parse(fs.readFileSync(filePath, "utf-8")) : {};
832
- const keys = [.../* @__PURE__ */ new Set([...Object.keys(globalData), ...Object.keys(localData)])];
833
- if (!keys.length) return "暂无关键词";
834
- return `关键词列表(共${keys.length}个):
835
- ` + keys.join("\n");
950
+ const filePath = getFilePath(session.channelId);
951
+ const globalPath = getFilePath(null, true);
952
+ let keys = [];
953
+ if (config.searchRange !== "local" && fsSync.existsSync(globalPath)) {
954
+ keys.push(...Object.keys(JSON.parse(fsSync.readFileSync(globalPath, "utf-8"))).map((k) => `[全]${k}`));
955
+ }
956
+ if (fsSync.existsSync(filePath)) {
957
+ keys.push(...Object.keys(JSON.parse(fsSync.readFileSync(filePath, "utf-8"))).map((k) => `[本]${k}`));
958
+ }
959
+ if (keys.length === 0) return "暂无关键词";
960
+ return keys.join("\n");
836
961
  });
837
- ctx.command(`${pluginName}/测试授权 <...targets>`, "将群/私聊加入测试名单", { authority: config.commandAuthority }).action(async ({ session }, ...targets) => {
838
- if (!targets.length) return "请提供至少一个目标ID";
839
- const channels = await getTestChannels(session);
840
- const set = new Set(channels);
962
+ ctx.command(pluginName + "/测试授权 <targetId...>", "加入测试名单").authority(config.commandAuthority).action(async ({ session }, ...targets) => {
963
+ if (!targets.length) return "请提供目标ID";
964
+ const allRecords = await ctx.database.get("qxgl_satori_auth");
965
+ let testChannels = [];
966
+ if (allRecords.length > 0 && allRecords[0].testChannels) {
967
+ testChannels = allRecords[0].testChannels;
968
+ }
841
969
  const added = [];
842
970
  for (const t of targets) {
843
971
  const cid = t.startsWith("private:") ? t : t;
844
- if (!set.has(cid)) {
845
- set.add(cid);
972
+ if (!testChannels.includes(cid)) {
973
+ testChannels.push(cid);
846
974
  added.push(t);
847
975
  }
848
976
  }
849
- if (added.length) await setTestChannels(session, Array.from(set));
850
- return added.length ? `已加入测试名单:${added.join("")}` : "指定目标已在测试名单中";
977
+ if (added.length > 0) {
978
+ await ctx.database.set("qxgl_satori_auth", {}, { testChannels });
979
+ }
980
+ return `已添加测试:${added.join("、") || "无新增"}`;
851
981
  });
852
- ctx.command(`${pluginName}/取消测试授权 <...targets>`, "将群/私聊移出测试名单", { authority: config.commandAuthority }).action(async ({ session }, ...targets) => {
853
- if (!targets.length) return "请提供至少一个目标ID";
854
- const channels = await getTestChannels(session);
855
- const set = new Set(channels);
982
+ ctx.command(pluginName + "/取消测试授权 <targetId...>", "移出测试名单").authority(config.commandAuthority).action(async ({ session }, ...targets) => {
983
+ const allRecords = await ctx.database.get("qxgl_satori_auth");
984
+ if (!allRecords.length) return "无数据";
985
+ let testChannels = allRecords[0].testChannels || [];
856
986
  const removed = [];
857
987
  for (const t of targets) {
858
- const cid = t.startsWith("private:") ? t : t;
859
- if (set.has(cid)) {
860
- set.delete(cid);
988
+ const idx = testChannels.indexOf(t);
989
+ if (idx > -1) {
990
+ testChannels.splice(idx, 1);
861
991
  removed.push(t);
992
+ } else if (t.startsWith("private:")) {
993
+ const plain = t.replace("private:", "");
994
+ const idx2 = testChannels.indexOf(plain);
995
+ if (idx2 > -1) {
996
+ testChannels.splice(idx2, 1);
997
+ removed.push(t);
998
+ }
862
999
  }
863
1000
  }
864
- if (removed.length) await setTestChannels(session, Array.from(set));
865
- return removed.length ? `已移出测试名单:${removed.join("、")}` : "指定目标不在测试名单中";
1001
+ await ctx.database.set("qxgl_satori_auth", {}, { testChannels });
1002
+ return `已移出:${removed.join("、") || ""}`;
866
1003
  });
867
- ctx.command(`${pluginName}/测试同步`, "全局问答复制到测试环境", { authority: config.commandAuthority }).action(async ({ session }) => {
868
- if (!fs.existsSync(globalFile)) return "全局问答文件不存在";
869
- fs.copyFileSync(globalFile, testFile);
870
- const data = JSON.parse(fs.readFileSync(testFile, "utf-8"));
871
- return `测试同步完成,共 ${Object.keys(data).length} 条问答已复制`;
1004
+ ctx.command(pluginName + "/测试同步", "正式→测试数据同步").authority(config.commandAuthority).action(async () => {
1005
+ const globalPath = getFilePath(null, true);
1006
+ if (!fsSync.existsSync(globalPath)) return "全局文件不存在";
1007
+ fsSync.copyFileSync(globalPath, testFilePath);
1008
+ const count = Object.keys(JSON.parse(fsSync.readFileSync(testFilePath, "utf-8"))).length;
1009
+ return `测试同步完成,共 ${count} 条数据`;
872
1010
  });
873
- ctx.command(`${pluginName}/测试发布`, "测试问答发布到全局", { authority: config.commandAuthority }).alias("测试转正").alias("全局发布").action(async ({ session }) => {
874
- if (!fs.existsSync(testFile)) return "测试问答文件不存在";
875
- const backupFile = path.join(root, "global.json.pub.bak");
876
- if (fs.existsSync(globalFile)) fs.copyFileSync(globalFile, backupFile);
877
- fs.copyFileSync(testFile, globalFile);
878
- const data = JSON.parse(fs.readFileSync(globalFile, "utf-8"));
879
- return `测试发布完成,共 ${Object.keys(data).length} 条问答已发布,原全局已备份`;
1011
+ ctx.command(pluginName + "/测试发布", "测试→正式发布(带备份)").alias("测试转正").authority(config.commandAuthority).action(async () => {
1012
+ if (!fsSync.existsSync(testFilePath)) return "测试文件不存在";
1013
+ const globalPath = getFilePath(null, true);
1014
+ const backupPath = path.join(dataDir, `global.json.bak_${Date.now()}`);
1015
+ if (fsSync.existsSync(globalPath)) {
1016
+ fsSync.copyFileSync(globalPath, backupPath);
1017
+ }
1018
+ fsSync.copyFileSync(testFilePath, globalPath);
1019
+ const count = Object.keys(JSON.parse(fsSync.readFileSync(globalPath, "utf-8"))).length;
1020
+ return `测试发布完成,共 ${count} 条数据,原数据已备份`;
880
1021
  });
881
- ctx.command(`${pluginName}/测试添加 <keyword>`, "添加测试关键词", { authority: config.commandAuthority }).option("regex", "-x 使用正则匹配").option("forward", "-f <type> 指定回复方式(1-4)").action(async ({ session, options }, keyword) => {
1022
+ ctx.command(pluginName + "/测试添加 <keyword>", "测试环境添加").option("regex", "-x").authority(config.commandAuthority).action(async ({ session, options }, keyword) => {
882
1023
  if (!hasPermission(session, "测试添加")) return "权限不足";
883
- return await addKeywordReply(session, testFile, keyword, { ...options, global: false });
884
- });
885
- ctx.command(`${pluginName}/测试删除 <keyword>`, "删除测试关键词", { authority: config.commandAuthority }).option("question", "-q <index> 指定删除序号").action(async ({ session, options }, keyword) => {
886
- if (!hasPermission(session, "测试删除")) return "权限不足";
887
- return await deleteKeywordReply(session, testFile, keyword, options.question);
888
- });
889
- ctx.command(`${pluginName}/测试修改 <keyword>`, "修改测试关键词", { authority: config.commandAuthority }).option("question", "-q <index> 指定回复序号").action(async ({ session, options }, keyword) => {
890
- if (!hasPermission(session, "测试修改")) return "权限不足";
891
- if (!fs.existsSync(testFile)) return "测试文件不存在";
892
- return "测试修改功能待完善";
893
- });
894
- ctx.middleware(async (session, next) => {
895
- if (!session.content) return next();
896
- let content = session.content;
897
- if (config.Treat_all_as_lowercase) content = content.toLowerCase();
898
- const testChannels = await getTestChannels(session);
899
- const isTest = testChannels.includes(session.channelId) || testChannels.includes(`private:${session.userId}`);
900
- const files = [];
901
- if (isTest) {
902
- files.push(testFile);
903
- } else {
904
- if (config.Search_Range === "3" || config.Search_Range === "2") files.push(globalFile);
905
- if (config.Search_Range === "1" || config.Search_Range === "3") {
906
- files.push(path.join(root, `${session.platform}_${session.channelId}.json`));
907
- }
1024
+ if (!fsSync.existsSync(testFilePath)) fsSync.writeFileSync(testFilePath, "{}");
1025
+ let data = JSON.parse(fsSync.readFileSync(testFilePath, "utf-8"));
1026
+ const key = options.regex ? `regex:${keyword}` : keyword;
1027
+ if (config.alwaysPrompt !== "never") {
1028
+ await session.send("【测试模式】请输入回复内容(结束添加/取消添加):");
908
1029
  }
909
- for (const file of files) {
910
- if (!fs.existsSync(file)) continue;
911
- const data = JSON.parse(fs.readFileSync(file, "utf-8"));
912
- const keys = Object.keys(data).sort((a, b) => {
913
- const lenA = a.startsWith("regex:") ? a.slice(6).length : a.length;
914
- const lenB = b.startsWith("regex:") ? b.slice(6).length : b.length;
915
- return lenB - lenA;
916
- });
917
- for (const key of keys) {
918
- let isMatch = false;
919
- let replies = data[key];
920
- if (key.startsWith("regex:")) {
921
- const pattern = key.slice(6);
922
- isMatch = new RegExp(pattern).test(content);
923
- } else {
924
- isMatch = content === key;
925
- }
926
- if (isMatch) {
927
- const limitKey = config.Type_of_restriction === "2" ? `${key}:${session.channelId}` : key;
928
- const now = Date.now();
929
- if (config.Frequency_limitation > 0 && lastTriggerTimes[limitKey] && now - lastTriggerTimes[limitKey] < config.Frequency_limitation * 1e3) {
930
- return;
931
- }
932
- lastTriggerTimes[limitKey] = now;
933
- const group = replies[Math.floor(Math.random() * replies.length)];
934
- const replyway = group[0]?.replyway || config.MultisegmentAdditionRecoveryEffect;
935
- if (replyway === "1") {
936
- for (const item of group) {
937
- await session.send(await formatReply(item, session));
938
- }
939
- } else if (replyway === "2") {
940
- let combined = "";
941
- for (const item of group) combined += await formatReply(item, session);
942
- await session.send(combined);
943
- } else if (replyway === "3" || replyway === "4") {
944
- const figure = h("figure");
945
- for (const item of group) {
946
- const formatted = await formatReply(item, session);
947
- figure.children.push(replyway === "4" ? h("message", {}, formatted) : formatted);
948
- }
949
- await session.send(figure);
950
- }
951
- return;
952
- }
953
- }
1030
+ const replies = [];
1031
+ const timeout = config.addTimeout * 6e4;
1032
+ while (true) {
1033
+ const reply = await session.prompt(timeout);
1034
+ if (!reply) return "超时";
1035
+ if (reply.includes(config.cancelKeyword)) return "取消";
1036
+ if (reply.includes(config.endKeyword)) break;
1037
+ const parsed = await parseReplyContent(reply, "test");
1038
+ replies.push(...parsed);
954
1039
  }
955
- return next();
1040
+ if (!data[key]) data[key] = [];
1041
+ data[key].push(replies);
1042
+ fsSync.writeFileSync(testFilePath, JSON.stringify(data, null, 2));
1043
+ return `测试关键词 ${keyword} 已添加`;
956
1044
  });
957
- ctx.on("guild-member", async (session) => {
958
- if (!config.autoLeaveOnMute) return;
959
- if (session.subtype !== "ban") return;
960
- if (session.selfId !== session.userId) return;
961
- const [record] = await ctx.database.get("qxgl_satori_auth", {
962
- platform: session.platform,
963
- channelId: session.guildId
1045
+ if (config.autoLeaveOnMute) {
1046
+ ctx.on("guild-member", async (session) => {
1047
+ if (session.subtype !== "ban" || session.selfId !== session.userId) return;
1048
+ try {
1049
+ await session.bot?.leaveGuild?.(session.guildId);
1050
+ logger.info(`被禁言自动退群:${session.guildId}`);
1051
+ } catch (e) {
1052
+ logger.warn(`退群失败:${e.message}`);
1053
+ }
964
1054
  });
965
- if (record && session.bot?.leaveGroup) {
966
- await session.bot.leaveGroup(session.guildId);
967
- }
968
- });
1055
+ }
969
1056
  }
970
1057
  __name(apply, "apply");
1058
+ function formatDate(date) {
1059
+ if (!date) return "未设置";
1060
+ const d = new Date(date);
1061
+ const year = d.getFullYear();
1062
+ const month = (d.getMonth() + 1).toString().padStart(2, "0");
1063
+ const day = d.getDate().toString().padStart(2, "0");
1064
+ return `${year}/${month}/${day}`;
1065
+ }
1066
+ __name(formatDate, "formatDate");
1067
+ function escapeRegExp(string) {
1068
+ return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1069
+ }
1070
+ __name(escapeRegExp, "escapeRegExp");
971
1071
  exports.apply = apply;
972
1072
  exports.Config = Config;
973
1073
  exports.name = pluginName;