koishi-plugin-qxgl-satori 0.0.3 → 0.0.5

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