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