koishi-plugin-qxgl-satori 0.0.4 → 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.d.ts +3 -1
- package/lib/index.js +940 -931
- package/package.json +1 -1
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
deleteBranchOnly: Schema.boolean().default(true).description("
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
Schema.const("
|
|
87
|
-
Schema.const("
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
]).default("
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
Schema.const("
|
|
97
|
-
Schema.const("
|
|
98
|
-
|
|
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("
|
|
107
|
-
Schema.const("
|
|
108
|
-
Schema.const("
|
|
109
|
-
]).default("
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
117
|
-
|
|
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,997 +108,1023 @@ var usage = `
|
|
|
125
108
|
<meta charset="UTF-8">
|
|
126
109
|
<title>qxgl-satori 插件使用说明</title>
|
|
127
110
|
<style>
|
|
128
|
-
body { font-family: -
|
|
129
|
-
h1 { color: #2c3e50; border-bottom:
|
|
130
|
-
h2 { color: #34495e; margin-top:
|
|
131
|
-
|
|
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
|
-
|
|
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
|
|
140
|
-
<p
|
|
121
|
+
<h1>qxgl-satori 插件</h1>
|
|
122
|
+
<p>基于 Satori 协议优化的群管与问答系统,支持多账号共享数据库。</p>
|
|
141
123
|
|
|
142
|
-
<
|
|
143
|
-
<
|
|
144
|
-
|
|
124
|
+
<div class="notice">
|
|
125
|
+
<strong>多账号兼容:</strong>本插件通过 platform + channelId 作为主键,不绑定任何 Bot 账号(selfId),支持多个 Bot 实例共享同一数据库管理相同群聊。
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<h2>授权管理系统</h2>
|
|
145
129
|
<ul>
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
<li><code>备份数据</code> - 导出数据库为JSON</li>
|
|
130
|
+
<li><strong>群授权</strong>:<code>群授权 <群号> <±月数> [授权人]</code> - 增加或减少群组授权时长</li>
|
|
131
|
+
<li><strong>私聊授权</strong>:<code>私聊授权 <用户ID> <±月数> [授权人]</code></li>
|
|
132
|
+
<li><strong>更换授权</strong>:<code>更换授权 <原群号> <新群号></code> - 迁移授权记录</li>
|
|
133
|
+
<li><strong>取消授权</strong>:<code>取消授权 <群号或用户ID></code></li>
|
|
134
|
+
<li><strong>到期时间</strong>:<code>到期时间</code> - 查询当前群/私聊授权状态</li>
|
|
135
|
+
<li><strong>查询到期</strong>:<code>查询到期 <目标ID></code> - 查询指定目标</li>
|
|
136
|
+
<li><strong>全局延期/减少</strong>:<code>全局延期 <天数></code> / <code>全局减少 <天数></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
|
|
160
|
-
<div class="detail">
|
|
141
|
+
<h2>关键词问答系统</h2>
|
|
161
142
|
<ul>
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
143
|
+
<li><strong>添加</strong>:<code>添加 [关键词]</code> - 支持多段输入(图片自动保存为本地文件)</li>
|
|
144
|
+
<li><strong>全局添加</strong>:<code>全局添加 [关键词]</code></li>
|
|
145
|
+
<li><strong>删除</strong>:<code>删除 [关键词] [-q <序号>]</code></li>
|
|
146
|
+
<li><strong>修改</strong>:<code>修改 [关键词] [-q <序号>]</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
|
|
173
|
-
<div class="detail">
|
|
151
|
+
<h2>测试环境</h2>
|
|
174
152
|
<ul>
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
<li><code>测试发布</code>(别名:<code>测试转正</code>)- 将测试数据发布到正式(自动备份)</li>
|
|
179
|
-
<li><code>测试添加/测试删除/测试修改</code> - 独立的测试问答操作</li>
|
|
153
|
+
<li><strong>测试授权</strong>:<code>测试授权 <群号/用户ID></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
|
|
185
|
-
<
|
|
186
|
-
<
|
|
187
|
-
<
|
|
188
|
-
|
|
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 = { "&": "&", "<": "<", ">": ">", """: '"', "'": "'" };
|
|
182
|
+
return str.replace(/&|<|>|"|'/g, (m) => map[m]);
|
|
183
|
+
}
|
|
184
|
+
__name(unescapeHtml, "unescapeHtml");
|
|
192
185
|
async function apply(ctx, config) {
|
|
193
|
-
const
|
|
194
|
-
const
|
|
195
|
-
if (!fsSync.existsSync(
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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.
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
testChannels: "list"
|
|
207
|
+
recordDate: "date",
|
|
208
|
+
updateDate: "date"
|
|
221
209
|
}, {
|
|
222
210
|
primary: ["platform", "channelId"]
|
|
211
|
+
// 复合主键,确保多账号共享同一记录
|
|
223
212
|
});
|
|
224
|
-
function
|
|
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.
|
|
227
|
-
const defaultConfig = config.adminList.find((a) => a.
|
|
228
|
-
if (defaultConfig
|
|
229
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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(
|
|
296
|
-
function
|
|
297
|
-
|
|
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(
|
|
301
|
-
async function downloadImage(url,
|
|
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
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
|
320
|
-
|
|
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
|
-
|
|
364
|
-
|
|
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",
|
|
287
|
+
results.push({ type: "text", text: el.attrs.content });
|
|
375
288
|
} else if (el.type === "at") {
|
|
376
|
-
results.push({ type: "at",
|
|
289
|
+
results.push({ type: "at", text: el.attrs.id });
|
|
377
290
|
} else if (el.type === "audio") {
|
|
378
|
-
results.push({ type: "audio",
|
|
291
|
+
results.push({ type: "audio", text: el.attrs.src || el.attrs.url });
|
|
379
292
|
} else if (el.type === "video") {
|
|
380
|
-
results.push({ type: "video",
|
|
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
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
text = text.replace(/\{authorizer\}/g,
|
|
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
|
|
320
|
+
return formatted;
|
|
536
321
|
}
|
|
537
|
-
__name(
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
return
|
|
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(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/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]", "群授权管理", {
|
|
591
|
-
authority: config.commandAuthority
|
|
592
|
-
}).action(async ({ session }, channelId, months, authorizer = "蒙面人") => {
|
|
593
|
-
if (!channelId || !months) return "格式错误:群授权 <群号> <±月数> [授权人]";
|
|
594
328
|
const monthsNum = parseInt(months);
|
|
595
|
-
if (isNaN(monthsNum))
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
if (session.bot?.getGuild) {
|
|
599
|
-
const guild = await session.bot.getGuild(channelId);
|
|
600
|
-
groupName = guild?.name || groupName;
|
|
601
|
-
}
|
|
602
|
-
} catch (e) {
|
|
329
|
+
if (isNaN(monthsNum)) {
|
|
330
|
+
await session.send("请提供有效的月份数(正整数表示增加,负整数表示减少)");
|
|
331
|
+
return;
|
|
603
332
|
}
|
|
604
|
-
const
|
|
605
|
-
|
|
333
|
+
const platform = session.platform;
|
|
334
|
+
let record = await ctx.database.get("qxgl_satori_auth", { platform, channelId });
|
|
606
335
|
const now = /* @__PURE__ */ new Date();
|
|
607
|
-
let
|
|
336
|
+
let newExpiryDate;
|
|
608
337
|
if (record.length === 0) {
|
|
609
|
-
|
|
338
|
+
newExpiryDate = new Date(now.getFullYear(), now.getMonth() + monthsNum, now.getDate());
|
|
610
339
|
await ctx.database.create("qxgl_satori_auth", {
|
|
611
340
|
platform,
|
|
612
341
|
channelId,
|
|
613
|
-
|
|
614
|
-
|
|
342
|
+
isAuthorized: true,
|
|
343
|
+
expiryDate: newExpiryDate,
|
|
615
344
|
authorizer,
|
|
616
|
-
channelName:
|
|
617
|
-
|
|
618
|
-
|
|
345
|
+
channelName: "",
|
|
346
|
+
recordDate: now,
|
|
347
|
+
updateDate: now
|
|
619
348
|
});
|
|
620
349
|
} else {
|
|
621
|
-
const
|
|
622
|
-
|
|
350
|
+
const currentExpiry = record[0].expiryDate ? new Date(record[0].expiryDate) : now;
|
|
351
|
+
newExpiryDate = new Date(currentExpiry.getFullYear(), currentExpiry.getMonth() + monthsNum, currentExpiry.getDate());
|
|
623
352
|
await ctx.database.set("qxgl_satori_auth", { platform, channelId }, {
|
|
624
|
-
|
|
353
|
+
isAuthorized: true,
|
|
354
|
+
expiryDate: newExpiryDate,
|
|
625
355
|
authorizer,
|
|
626
|
-
|
|
627
|
-
updateDate: now,
|
|
628
|
-
isBlocked: false
|
|
356
|
+
updateDate: now
|
|
629
357
|
});
|
|
630
358
|
}
|
|
631
359
|
const action = monthsNum > 0 ? "增加" : "减少";
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
授权人:${authorizer}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
+
}
|
|
641
370
|
const monthsNum = parseInt(months);
|
|
642
|
-
if (isNaN(monthsNum))
|
|
371
|
+
if (isNaN(monthsNum)) {
|
|
372
|
+
await session.send("请提供有效的月份数");
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
const platform = session.platform;
|
|
643
376
|
const channelId = `private:${userId}`;
|
|
644
|
-
|
|
645
|
-
const record = await ctx.database.get("qxgl_satori_auth", { platform, channelId });
|
|
377
|
+
let record = await ctx.database.get("qxgl_satori_auth", { platform, channelId });
|
|
646
378
|
const now = /* @__PURE__ */ new Date();
|
|
647
|
-
let
|
|
379
|
+
let newExpiryDate;
|
|
648
380
|
if (record.length === 0) {
|
|
649
|
-
|
|
381
|
+
newExpiryDate = new Date(now.getFullYear(), now.getMonth() + monthsNum, now.getDate());
|
|
650
382
|
await ctx.database.create("qxgl_satori_auth", {
|
|
651
383
|
platform,
|
|
652
384
|
channelId,
|
|
653
|
-
|
|
654
|
-
|
|
385
|
+
isAuthorized: true,
|
|
386
|
+
expiryDate: newExpiryDate,
|
|
655
387
|
authorizer,
|
|
656
388
|
channelName: "私聊",
|
|
657
|
-
|
|
658
|
-
|
|
389
|
+
recordDate: now,
|
|
390
|
+
updateDate: now
|
|
659
391
|
});
|
|
660
392
|
} else {
|
|
661
|
-
const
|
|
662
|
-
|
|
393
|
+
const currentExpiry = record[0].expiryDate ? new Date(record[0].expiryDate) : now;
|
|
394
|
+
newExpiryDate = new Date(currentExpiry.getFullYear(), currentExpiry.getMonth() + monthsNum, currentExpiry.getDate());
|
|
663
395
|
await ctx.database.set("qxgl_satori_auth", { platform, channelId }, {
|
|
664
|
-
|
|
396
|
+
isAuthorized: true,
|
|
397
|
+
expiryDate: newExpiryDate,
|
|
665
398
|
authorizer,
|
|
666
|
-
updateDate: now
|
|
667
|
-
isBlocked: false
|
|
399
|
+
updateDate: now
|
|
668
400
|
});
|
|
669
401
|
}
|
|
670
402
|
const action = monthsNum > 0 ? "增加" : "减少";
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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;
|
|
679
413
|
const channelId = targetId.startsWith("private:") ? targetId : targetId;
|
|
680
|
-
const { platform } = session;
|
|
681
414
|
const record = await ctx.database.get("qxgl_satori_auth", { platform, channelId });
|
|
682
|
-
if (record.length === 0)
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
const
|
|
696
|
-
|
|
697
|
-
const
|
|
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
|
+
}
|
|
698
441
|
await ctx.database.create("qxgl_satori_auth", {
|
|
699
|
-
|
|
700
|
-
channelId:
|
|
701
|
-
|
|
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(),
|
|
702
449
|
updateDate: /* @__PURE__ */ new Date()
|
|
703
450
|
});
|
|
704
|
-
await ctx.database.remove("qxgl_satori_auth", { platform, channelId:
|
|
705
|
-
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
const
|
|
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;
|
|
711
458
|
const record = await ctx.database.get("qxgl_satori_auth", { platform, channelId });
|
|
712
459
|
if (record.length === 0 || !record[0].expiryDate) {
|
|
713
|
-
|
|
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)}`);
|
|
714
466
|
}
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
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;
|
|
722
475
|
const channelId = targetId.startsWith("private:") ? targetId : targetId;
|
|
723
|
-
const record = await ctx.database.get("qxgl_satori_auth", {
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
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
|
+
}
|
|
737
491
|
const records = await ctx.database.get("qxgl_satori_auth");
|
|
738
492
|
const now = /* @__PURE__ */ new Date();
|
|
739
493
|
let count = 0;
|
|
740
|
-
for (const
|
|
741
|
-
if (
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
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
|
+
}
|
|
758
516
|
const records = await ctx.database.get("qxgl_satori_auth");
|
|
759
517
|
const now = /* @__PURE__ */ new Date();
|
|
760
518
|
let count = 0;
|
|
761
|
-
for (const
|
|
762
|
-
if (
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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) {
|
|
777
536
|
const records = await ctx.database.get("qxgl_satori_auth");
|
|
778
537
|
let count = 0;
|
|
779
|
-
for (const
|
|
780
|
-
if (
|
|
538
|
+
for (const record of records) {
|
|
539
|
+
if (record.channelId.startsWith("private:")) continue;
|
|
781
540
|
try {
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
count++;
|
|
791
|
-
}
|
|
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++;
|
|
792
549
|
}
|
|
793
550
|
} catch (e) {
|
|
794
551
|
}
|
|
795
552
|
}
|
|
796
|
-
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
}).action(async () => {
|
|
553
|
+
await session.send(`已更新 ${count} 个群聊名称`);
|
|
554
|
+
}
|
|
555
|
+
__name(updateChannelName, "updateChannelName");
|
|
556
|
+
async function listChannels(session) {
|
|
801
557
|
const records = await ctx.database.get("qxgl_satori_auth");
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
return groups.map(
|
|
805
|
-
(r) => `群名:${r.channelName}
|
|
558
|
+
if (!records.length) return "暂无群聊记录";
|
|
559
|
+
const list = records.filter((r) => !r.channelId.startsWith("private:")).map((r) => `群名:${r.channelName || "未知"}
|
|
806
560
|
群号:${r.channelId}
|
|
561
|
+
平台:${r.platform}
|
|
807
562
|
到期:${formatDate(r.expiryDate)}
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
authority: config.commandAuthority
|
|
814
|
-
}).action(async () => {
|
|
563
|
+
授权人:${r.authorizer || "蒙面人"}`).join("\n\n");
|
|
564
|
+
await session.send(list || "暂无群聊记录");
|
|
565
|
+
}
|
|
566
|
+
__name(listChannels, "listChannels");
|
|
567
|
+
async function backupData(session) {
|
|
815
568
|
const records = await ctx.database.get("qxgl_satori_auth");
|
|
816
|
-
const backupPath = path.join(
|
|
569
|
+
const backupPath = path.join(root, `backup_${Date.now()}.json`);
|
|
817
570
|
await fs.writeFile(backupPath, JSON.stringify(records, null, 2));
|
|
818
|
-
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
const filePath = isGlobal ?
|
|
823
|
-
|
|
824
|
-
fsSync.writeFileSync(filePath, "{}");
|
|
825
|
-
}
|
|
826
|
-
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 = {};
|
|
827
577
|
try {
|
|
828
|
-
|
|
578
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
579
|
+
data = JSON.parse(content);
|
|
829
580
|
} catch {
|
|
830
|
-
data = {};
|
|
831
|
-
}
|
|
832
|
-
const key = isRegex ? `regex:${keyword}` : config.treatAsLowercase ? keyword.toLowerCase() : keyword;
|
|
833
|
-
if (data[key] && config.handleDuplicate === "reject") {
|
|
834
|
-
return `关键词 "${keyword}" 已存在`;
|
|
835
|
-
}
|
|
836
|
-
if (!data[key] || config.handleDuplicate === "replace") {
|
|
837
|
-
data[key] = [];
|
|
838
|
-
}
|
|
839
|
-
if (config.alwaysPrompt !== "never") {
|
|
840
|
-
await session.send('请输入回复内容(支持多段,输入"结束添加"完成,"取消添加"放弃):');
|
|
841
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;
|
|
842
588
|
const replies = [];
|
|
843
|
-
const timeout = config.addTimeout * 6e4;
|
|
844
589
|
while (true) {
|
|
845
|
-
if (config.alwaysPrompt === "always") {
|
|
846
|
-
await session.send("请继续输入(结束添加/取消添加):");
|
|
847
|
-
}
|
|
848
590
|
const reply = await session.prompt(timeout);
|
|
849
|
-
if (!reply)
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
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);
|
|
853
605
|
replies.push(...parsed);
|
|
606
|
+
if (config.matchPatternForExit === "1") break;
|
|
607
|
+
}
|
|
608
|
+
if (replies.length === 0) {
|
|
609
|
+
await session.send("未输入有效内容");
|
|
610
|
+
return;
|
|
854
611
|
}
|
|
855
|
-
if (replies.length === 0) return "未输入有效回复";
|
|
856
612
|
data[key].push(replies);
|
|
857
|
-
|
|
858
|
-
|
|
613
|
+
await fs.writeFile(filePath, JSON.stringify(data, null, 2));
|
|
614
|
+
await session.send(`关键词 "${keyword}" 的回复已添加(当前共 ${data[key].length} 条回复)`);
|
|
859
615
|
}
|
|
860
|
-
__name(
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
const
|
|
882
|
-
if (
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
}
|
|
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));
|
|
886
714
|
}
|
|
887
|
-
if (
|
|
888
|
-
|
|
889
|
-
return `该关键词有 ${data[targetKey].length} 条回复,请使用 -q 指定序号删除`;
|
|
715
|
+
if (config.searchRange === "2" || config.searchRange === "3") {
|
|
716
|
+
files.push(globalFile);
|
|
890
717
|
}
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
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 {
|
|
895
725
|
}
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
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 {
|
|
901
760
|
}
|
|
902
|
-
|
|
903
|
-
|
|
761
|
+
}
|
|
762
|
+
if (results.length === 0) {
|
|
763
|
+
await session.send(`未找到包含 "${keyword}" 的关键词`);
|
|
904
764
|
} else {
|
|
905
|
-
|
|
906
|
-
fsSync.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
907
|
-
return `已删除关键词 "${keyword}"`;
|
|
765
|
+
await session.send(results.join("\n\n"));
|
|
908
766
|
}
|
|
909
767
|
}
|
|
910
|
-
__name(
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
if (!hasPermission(session, "删除")) return "权限不足";
|
|
915
|
-
return deleteKeywordLogic(session, keyword, false, options.index);
|
|
916
|
-
});
|
|
917
|
-
ctx.command(pluginName + `/${config.globalDeletePrefix} [keyword]`, "删除全局关键词", {
|
|
918
|
-
authority: config.commandAuthority
|
|
919
|
-
}).option("index", "-q <number> 指定回复序号").action(async ({ session, options }, keyword) => {
|
|
920
|
-
if (!hasPermission(session, "全局删除")) return "权限不足";
|
|
921
|
-
return deleteKeywordLogic(session, keyword, true, options.index);
|
|
922
|
-
});
|
|
923
|
-
async function fixKeywordLogic(session, keyword, isGlobal, idx = 0) {
|
|
924
|
-
if (!keyword) return "请输入关键词";
|
|
925
|
-
const filePath = isGlobal ? getFilePath(null, true) : getFilePath(session.channelId);
|
|
926
|
-
if (!fsSync.existsSync(filePath)) return "文件不存在";
|
|
927
|
-
const data = JSON.parse(fsSync.readFileSync(filePath, "utf-8"));
|
|
928
|
-
const searchKey = config.treatAsLowercase ? keyword.toLowerCase() : keyword;
|
|
929
|
-
let targetKey = null;
|
|
930
|
-
for (const k of Object.keys(data)) {
|
|
931
|
-
const cleanK = k.startsWith("regex:") ? k.slice(6) : k;
|
|
932
|
-
if (cleanK === searchKey) targetKey = k;
|
|
933
|
-
}
|
|
934
|
-
if (!targetKey) return `关键词 "${keyword}" 不存在`;
|
|
935
|
-
if (!data[targetKey][idx]) return "回复序号无效";
|
|
936
|
-
await session.send(`正在修改 "${keyword}" 的第 ${idx + 1} 条回复,当前内容:`);
|
|
937
|
-
await sendFormattedReply(session, data[targetKey][idx], null);
|
|
938
|
-
await session.send("请输入新内容(取消添加 放弃):");
|
|
939
|
-
const reply = await session.prompt(config.addTimeout * 6e4);
|
|
940
|
-
if (!reply || reply.includes(config.cancelKeyword)) return "已取消";
|
|
941
|
-
const parsed = await parseReplyContent(reply, isGlobal ? "global" : session.channelId);
|
|
942
|
-
data[targetKey][idx] = parsed;
|
|
943
|
-
fsSync.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
944
|
-
return "修改完成";
|
|
768
|
+
__name(searchKeyword, "searchKeyword");
|
|
769
|
+
function isTestChannel(platform, channelId) {
|
|
770
|
+
const fullId = `${platform}:${channelId}`;
|
|
771
|
+
return config.testChannels.includes(fullId);
|
|
945
772
|
}
|
|
946
|
-
__name(
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
});
|
|
953
|
-
ctx.command(pluginName + `/${config.globalFixCommand} [keyword]`, "修改全局关键词", {
|
|
954
|
-
authority: config.commandAuthority
|
|
955
|
-
}).option("index", "-q <number> 回复序号").action(async ({ session, options }, keyword) => {
|
|
956
|
-
if (!hasPermission(session, "全局修改")) return "权限不足";
|
|
957
|
-
return fixKeywordLogic(session, keyword, true, (options.index || 1) - 1);
|
|
958
|
-
});
|
|
959
|
-
ctx.command(pluginName + `/${config.searchCommand} [keyword]`, "查找关键词", {
|
|
960
|
-
authority: config.commandAuthority
|
|
961
|
-
}).action(async ({ session }, keyword) => {
|
|
962
|
-
if (!hasPermission(session, "查找关键词")) return "权限不足";
|
|
963
|
-
if (!keyword) return "请输入搜索词";
|
|
964
|
-
let files = [];
|
|
965
|
-
if (config.searchRange === "local") {
|
|
966
|
-
files = [getFilePath(session.channelId)];
|
|
967
|
-
} else if (config.searchRange === "all") {
|
|
968
|
-
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} 加入测试名单`);
|
|
969
779
|
} else {
|
|
970
|
-
|
|
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"/>');
|
|
971
876
|
}
|
|
972
|
-
|
|
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");
|
|
973
881
|
for (const file of files) {
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
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
|
+
}
|
|
979
933
|
}
|
|
934
|
+
} catch (error) {
|
|
935
|
+
if (error.code !== "ENOENT") logger.error(`读取问答文件失败:${error.message}`);
|
|
980
936
|
}
|
|
981
937
|
}
|
|
982
|
-
return
|
|
938
|
+
return next();
|
|
983
939
|
});
|
|
984
|
-
ctx.command(pluginName
|
|
985
|
-
|
|
986
|
-
}).action(async ({ session }) => {
|
|
987
|
-
if (!hasPermission(session, "查看关键词列表")) return "权限不足";
|
|
988
|
-
const filePath = getFilePath(session.channelId);
|
|
989
|
-
const globalPath = getFilePath(null, true);
|
|
990
|
-
let keys = [];
|
|
991
|
-
if (config.searchRange !== "local" && fsSync.existsSync(globalPath)) {
|
|
992
|
-
keys.push(...Object.keys(JSON.parse(fsSync.readFileSync(globalPath, "utf-8"))).map((k) => `[全]${k}`));
|
|
993
|
-
}
|
|
994
|
-
if (fsSync.existsSync(filePath)) {
|
|
995
|
-
keys.push(...Object.keys(JSON.parse(fsSync.readFileSync(filePath, "utf-8"))).map((k) => `[本]${k}`));
|
|
996
|
-
}
|
|
997
|
-
if (keys.length === 0) return "暂无关键词";
|
|
998
|
-
return keys.join("\n");
|
|
940
|
+
ctx.command(pluginName).action(({ session }) => {
|
|
941
|
+
return "使用帮助:请查看插件说明文档";
|
|
999
942
|
});
|
|
1000
|
-
ctx.command(pluginName
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
if (!targets.length) return "请提供目标ID";
|
|
1004
|
-
const allRecords = await ctx.database.get("qxgl_satori_auth");
|
|
1005
|
-
let testChannels = [];
|
|
1006
|
-
if (allRecords.length > 0 && allRecords[0].testChannels) {
|
|
1007
|
-
testChannels = allRecords[0].testChannels;
|
|
1008
|
-
}
|
|
1009
|
-
const added = [];
|
|
1010
|
-
for (const t of targets) {
|
|
1011
|
-
const cid = t.startsWith("private:") ? t : t;
|
|
1012
|
-
if (!testChannels.includes(cid)) {
|
|
1013
|
-
testChannels.push(cid);
|
|
1014
|
-
added.push(t);
|
|
1015
|
-
}
|
|
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 "权限不足";
|
|
1016
946
|
}
|
|
1017
|
-
|
|
1018
|
-
|
|
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 "权限不足";
|
|
1019
952
|
}
|
|
1020
|
-
|
|
953
|
+
await authorizePrivate(session, userId, months, authorizer);
|
|
1021
954
|
});
|
|
1022
|
-
ctx.command(pluginName
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
const allRecords = await ctx.database.get("qxgl_satori_auth");
|
|
1026
|
-
if (!allRecords.length) return "无数据";
|
|
1027
|
-
let testChannels = allRecords[0].testChannels || [];
|
|
1028
|
-
const removed = [];
|
|
1029
|
-
for (const t of targets) {
|
|
1030
|
-
const idx = testChannels.indexOf(t);
|
|
1031
|
-
if (idx > -1) {
|
|
1032
|
-
testChannels.splice(idx, 1);
|
|
1033
|
-
removed.push(t);
|
|
1034
|
-
} else if (t.startsWith("private:")) {
|
|
1035
|
-
const plain = t.replace("private:", "");
|
|
1036
|
-
const idx2 = testChannels.indexOf(plain);
|
|
1037
|
-
if (idx2 > -1) {
|
|
1038
|
-
testChannels.splice(idx2, 1);
|
|
1039
|
-
removed.push(t);
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
955
|
+
ctx.command(`${pluginName}/${config.commandCancelAuth} <targetId>`, "取消授权").authority(config.commandAuthority).action(async ({ session }, targetId) => {
|
|
956
|
+
if (!hasPermission(session, config.commandCancelAuth)) {
|
|
957
|
+
return "权限不足";
|
|
1042
958
|
}
|
|
1043
|
-
await
|
|
1044
|
-
return `已移出:${removed.join("、") || "无"}`;
|
|
959
|
+
await cancelAuthorization(session, targetId);
|
|
1045
960
|
});
|
|
1046
|
-
ctx.command(pluginName
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
fsSync.copyFileSync(globalPath, testFilePath);
|
|
1052
|
-
const count = Object.keys(JSON.parse(fsSync.readFileSync(testFilePath, "utf-8"))).length;
|
|
1053
|
-
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);
|
|
1054
966
|
});
|
|
1055
|
-
ctx.command(pluginName
|
|
1056
|
-
|
|
1057
|
-
}).alias("测试转正").action(async () => {
|
|
1058
|
-
if (!fsSync.existsSync(testFilePath)) return "测试文件不存在";
|
|
1059
|
-
const globalPath = getFilePath(null, true);
|
|
1060
|
-
const backupPath = path.join(dataDir, `global.json.bak_${Date.now()}`);
|
|
1061
|
-
if (fsSync.existsSync(globalPath)) {
|
|
1062
|
-
fsSync.copyFileSync(globalPath, backupPath);
|
|
1063
|
-
}
|
|
1064
|
-
fsSync.copyFileSync(testFilePath, globalPath);
|
|
1065
|
-
const count = Object.keys(JSON.parse(fsSync.readFileSync(globalPath, "utf-8"))).length;
|
|
1066
|
-
return `测试发布完成,共 ${count} 条数据,原数据已备份`;
|
|
967
|
+
ctx.command(`${pluginName}/${config.commandQueryExpiry}`, "查询本群到期时间").action(async ({ session }) => {
|
|
968
|
+
await checkExpiryTime(session);
|
|
1067
969
|
});
|
|
1068
|
-
ctx.command(pluginName
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
if (!hasPermission(session, "测试添加")) return "权限不足";
|
|
1072
|
-
if (!fsSync.existsSync(testFilePath)) fsSync.writeFileSync(testFilePath, "{}");
|
|
1073
|
-
let data = JSON.parse(fsSync.readFileSync(testFilePath, "utf-8"));
|
|
1074
|
-
const key = options.regex ? `regex:${keyword}` : keyword;
|
|
1075
|
-
if (config.alwaysPrompt !== "never") {
|
|
1076
|
-
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 "权限不足";
|
|
1077
973
|
}
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
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}`);
|
|
1087
1124
|
}
|
|
1088
|
-
if (!data[key]) data[key] = [];
|
|
1089
|
-
data[key].push(replies);
|
|
1090
|
-
fsSync.writeFileSync(testFilePath, JSON.stringify(data, null, 2));
|
|
1091
|
-
return `测试关键词 ${keyword} 已添加`;
|
|
1092
1125
|
});
|
|
1093
|
-
if (config.autoLeaveOnMute) {
|
|
1094
|
-
ctx.on("guild-member", async (session) => {
|
|
1095
|
-
if (session.subtype !== "ban" || session.selfId !== session.userId) return;
|
|
1096
|
-
try {
|
|
1097
|
-
await session.bot?.leaveGuild?.(session.guildId);
|
|
1098
|
-
logger.info(`被禁言自动退群:${session.guildId}`);
|
|
1099
|
-
} catch (e) {
|
|
1100
|
-
logger.warn(`退群失败:${e.message}`);
|
|
1101
|
-
}
|
|
1102
|
-
});
|
|
1103
|
-
}
|
|
1104
1126
|
}
|
|
1105
1127
|
__name(apply, "apply");
|
|
1106
|
-
function formatDate(date) {
|
|
1107
|
-
if (!date) return "未设置";
|
|
1108
|
-
const d = new Date(date);
|
|
1109
|
-
const year = d.getFullYear();
|
|
1110
|
-
const month = (d.getMonth() + 1).toString().padStart(2, "0");
|
|
1111
|
-
const day = d.getDate().toString().padStart(2, "0");
|
|
1112
|
-
return `${year}/${month}/${day}`;
|
|
1113
|
-
}
|
|
1114
|
-
__name(formatDate, "formatDate");
|
|
1115
|
-
function escapeRegExp(string) {
|
|
1116
|
-
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1117
|
-
}
|
|
1118
|
-
__name(escapeRegExp, "escapeRegExp");
|
|
1119
1128
|
exports.apply = apply;
|
|
1120
1129
|
exports.Config = Config;
|
|
1121
1130
|
exports.name = pluginName;
|