koishi-plugin-qxgl-satori 0.0.1
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 +14 -0
- package/lib/index.js +975 -0
- package/package.json +20 -0
- package/readme.md +5 -0
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
declare var __defProp: <T>(o: T, p: PropertyKey, attributes: PropertyDescriptor & ThisType<any>) => T;
|
|
2
|
+
declare var __name: (target: any, value: any) => any;
|
|
3
|
+
declare var koishi: any;
|
|
4
|
+
declare var Schema: any, Logger: any, h: any;
|
|
5
|
+
declare var logger: any;
|
|
6
|
+
declare var inject: string[];
|
|
7
|
+
declare var pluginName: string;
|
|
8
|
+
declare var fs: any;
|
|
9
|
+
declare var fsPromises: any;
|
|
10
|
+
declare var path: any;
|
|
11
|
+
declare var pathToFileURL: any;
|
|
12
|
+
declare var Config: any;
|
|
13
|
+
declare var usage: string;
|
|
14
|
+
declare function apply(ctx: any, config: any): Promise<void>;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,975 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
4
|
+
|
|
5
|
+
// src/index.ts
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
var koishi = require("koishi");
|
|
8
|
+
var { Schema, Logger, h } = require("koishi");
|
|
9
|
+
var logger = new Logger("qxgl-satori");
|
|
10
|
+
var inject = ["database"];
|
|
11
|
+
var pluginName = "qxgl-satori";
|
|
12
|
+
var fs = require("node:fs");
|
|
13
|
+
var fsPromises = require("node:fs/promises");
|
|
14
|
+
var path = require("node:path");
|
|
15
|
+
var { pathToFileURL } = require("node:url");
|
|
16
|
+
var Config = Schema.intersect([
|
|
17
|
+
Schema.object({
|
|
18
|
+
enableAuthSystem: Schema.boolean().default(true).description("启用授权系统(关闭时所有群可用,但仍记录群信息)"),
|
|
19
|
+
autoLeaveOnMute: Schema.boolean().default(true).description("被禁言时自动退群"),
|
|
20
|
+
autoLeaveOnExpiry: Schema.boolean().default(true).description("授权到期时自动退群"),
|
|
21
|
+
commandAuthority: Schema.number().default(3).description("授权管理指令所需权限等级"),
|
|
22
|
+
pictureStrategy: Schema.union([
|
|
23
|
+
Schema.const("auto").description("自动检测(Satori用路径,OneBot用file://)"),
|
|
24
|
+
Schema.const("http").description("强制HTTP服务(需配置对外可访问地址)")
|
|
25
|
+
]).default("auto").description("图片发送策略"),
|
|
26
|
+
httpServePath: Schema.string().default("http://localhost:5140/qxgl-satori").description("图片HTTP服务基础地址(使用HTTP策略时)")
|
|
27
|
+
}).description("授权系统设置"),
|
|
28
|
+
Schema.object({
|
|
29
|
+
defaultImageExtension: Schema.union(["jpg", "png", "gif"]).default("png").description("图片保存后缀"),
|
|
30
|
+
addKeywordTime: Schema.number().role("slider").min(1).max(30).step(1).default(5).description("添加回复输入时限(分钟)"),
|
|
31
|
+
Treat_all_as_lowercase: Schema.boolean().default(true).description("英文关键词匹配无视大小写"),
|
|
32
|
+
Delete_Branch_Only: Schema.boolean().default(true).description("删除多段回复时必须指定序号"),
|
|
33
|
+
HandleDuplicateKeywords: Schema.union([
|
|
34
|
+
Schema.const("1").description("直接替换/覆盖"),
|
|
35
|
+
Schema.const("2").description("并列添加(随机选择)"),
|
|
36
|
+
Schema.const("3").description("禁止重复")
|
|
37
|
+
]).default("2").description("重复关键词处理"),
|
|
38
|
+
MultisegmentAdditionRecoveryEffect: Schema.union([
|
|
39
|
+
Schema.const("1").description("原版输入,原版输出(多段消息)"),
|
|
40
|
+
Schema.const("2").description("合为图文消息/多行消息(一次发出)"),
|
|
41
|
+
Schema.const("3").description("合为图文消息并合并转发"),
|
|
42
|
+
Schema.const("4").description("原版输入,合并转发发送")
|
|
43
|
+
]).default("2").description("多段回复效果"),
|
|
44
|
+
Frequency_limitation: Schema.number().default(0).description("同一问答最小触发间隔(秒,0为不限制)"),
|
|
45
|
+
Type_of_restriction: Schema.union([
|
|
46
|
+
Schema.const("1").description("对同一个问题(全部对象)"),
|
|
47
|
+
Schema.const("2").description("仅对同一个频道(不同频道独立)")
|
|
48
|
+
]).default("2").description("最小间隔限制对象"),
|
|
49
|
+
Search_Range: Schema.union([
|
|
50
|
+
Schema.const("1").description("仅在当前频道"),
|
|
51
|
+
Schema.const("2").description("搜索全部频道"),
|
|
52
|
+
Schema.const("3").description("当前频道+全局")
|
|
53
|
+
]).default("3").description("搜索范围"),
|
|
54
|
+
Find_Return_Preset: Schema.union([
|
|
55
|
+
Schema.const("1").description("仅返回问答内容"),
|
|
56
|
+
Schema.const("2").description("仅返回位置"),
|
|
57
|
+
Schema.const("3").description("返回内容和位置")
|
|
58
|
+
]).default("1").description("查找返回信息"),
|
|
59
|
+
Return_Limit: Schema.union([
|
|
60
|
+
Schema.const("1").description("返回全部"),
|
|
61
|
+
Schema.const("2").description("仅返回一条")
|
|
62
|
+
]).default("2").description("返回限制"),
|
|
63
|
+
MatchPatternForExit: Schema.union([
|
|
64
|
+
Schema.const("1").description("仅接受一次性输入"),
|
|
65
|
+
Schema.const("2").description("完全匹配结束词退出"),
|
|
66
|
+
Schema.const("3").description("包含结束词退出"),
|
|
67
|
+
Schema.const("4").description("完全匹配或包含均可退出")
|
|
68
|
+
]).default("4").description("退出添加方式"),
|
|
69
|
+
AlwayPrompt: Schema.union([
|
|
70
|
+
Schema.const("1").description("不返回提示"),
|
|
71
|
+
Schema.const("2").description("仅返回一次"),
|
|
72
|
+
Schema.const("3").description("每次输入都返回")
|
|
73
|
+
]).default("2").description("提示方式"),
|
|
74
|
+
Prompt: Schema.string().role("textarea", { rows: [2, 4] }).default("请输入回复内容(输入 取消添加 以取消,输入 结束添加 以结束):").description("添加提示文字"),
|
|
75
|
+
KeywordOfEsc: Schema.string().default("取消添加").description("取消添加关键词"),
|
|
76
|
+
KeywordOfEnd: Schema.string().default("结束添加").description("结束添加关键词")
|
|
77
|
+
}).description("关键词问答设置"),
|
|
78
|
+
Schema.object({
|
|
79
|
+
admin_list: Schema.array(Schema.object({
|
|
80
|
+
adminID: Schema.string().description("管理员用户ID"),
|
|
81
|
+
allowcommand: Schema.array(Schema.union([
|
|
82
|
+
"添加",
|
|
83
|
+
"全局添加",
|
|
84
|
+
"删除",
|
|
85
|
+
"全局删除",
|
|
86
|
+
"修改",
|
|
87
|
+
"全局修改",
|
|
88
|
+
"查找关键词",
|
|
89
|
+
"查看关键词列表",
|
|
90
|
+
"群授权",
|
|
91
|
+
"私聊授权",
|
|
92
|
+
"到期时间",
|
|
93
|
+
"查询到期",
|
|
94
|
+
"全局延期",
|
|
95
|
+
"全局减少",
|
|
96
|
+
"更新名称",
|
|
97
|
+
"列出已记录群",
|
|
98
|
+
"取消授权",
|
|
99
|
+
"更换授权",
|
|
100
|
+
"测试授权",
|
|
101
|
+
"取消测试授权",
|
|
102
|
+
"测试同步",
|
|
103
|
+
"测试发布",
|
|
104
|
+
"测试添加",
|
|
105
|
+
"测试删除",
|
|
106
|
+
"测试修改"
|
|
107
|
+
])).default(["到期时间"]).description("可使用的指令")
|
|
108
|
+
})).role("table").description("管理员列表(0代表所有用户)").default([{ adminID: "0" }]),
|
|
109
|
+
channel_admin_auth: Schema.boolean().default(false).description("开启后自动允许群主/管理员使用全部指令(需适配器支持)")
|
|
110
|
+
}).description("权限设置"),
|
|
111
|
+
Schema.object({
|
|
112
|
+
loggerinfo: Schema.boolean().default(false).description("日志调试模式")
|
|
113
|
+
}).description("调试设置")
|
|
114
|
+
]);
|
|
115
|
+
var usage = `<!DOCTYPE html>
|
|
116
|
+
<html lang="zh">
|
|
117
|
+
<head>
|
|
118
|
+
<meta charset="UTF-8">
|
|
119
|
+
<title>qxgl-satori 插件使用说明</title>
|
|
120
|
+
</head>
|
|
121
|
+
<body>
|
|
122
|
+
<h1>插件功能说明</h1>
|
|
123
|
+
<p>qxgl-satori 整合了群/私聊授权管理和关键词问答功能,支持多账号共享授权数据,Satori 原生适配。</p>
|
|
124
|
+
<h2>授权管理指令</h2>
|
|
125
|
+
<ul>
|
|
126
|
+
<li><strong>群授权 <群号> <±月数> [授权人]</strong> - 授权/修改群到期时间(支持负数减少)</li>
|
|
127
|
+
<li><strong>私聊授权 <用户ID> <±月数> [授权人]</strong> - 私聊授权</li>
|
|
128
|
+
<li><strong>取消授权 <目标ID></strong> - 删除授权记录</li>
|
|
129
|
+
<li><strong>更换授权 <原ID> <新ID></strong> - 转移授权</li>
|
|
130
|
+
<li><strong>到期时间</strong> - 查询当前群/私聊到期时间</li>
|
|
131
|
+
<li><strong>查询到期 [目标ID]</strong> - 查询指定目标(无参数查当前)</li>
|
|
132
|
+
<li><strong>全局延期 <天数></strong> - 所有群统一延期</li>
|
|
133
|
+
<li><strong>全局减少 <天数></strong> - 所有群统一减少</li>
|
|
134
|
+
<li><strong>列出已记录群</strong> - 显示所有群授权信息</li>
|
|
135
|
+
<li><strong>更新名称</strong> - 刷新所有群名称缓存</li>
|
|
136
|
+
</ul>
|
|
137
|
+
<h2>关键词指令</h2>
|
|
138
|
+
<ul>
|
|
139
|
+
<li><strong>添加 <关键词> [-x] [-f]</strong> - 添加关键词(-x正则,-f指定回复方式1-4)</li>
|
|
140
|
+
<li><strong>全局添加 <关键词> [-x] [-f]</strong> - 添加全局关键词</li>
|
|
141
|
+
<li><strong>删除 <关键词> [-q <序号>]</strong> - 删除关键词(-q删指定分支)</li>
|
|
142
|
+
<li><strong>全局删除 <关键词> [-q <序号>]</strong> - 删除全局关键词</li>
|
|
143
|
+
<li><strong>修改 <关键词> [-q <序号>]</strong> - 修改回复</li>
|
|
144
|
+
<li><strong>全局修改 <关键词> [-q <序号>]</strong> - 修改全局回复</li>
|
|
145
|
+
<li><strong>查找关键词 <关键词></strong> - 模糊搜索</li>
|
|
146
|
+
<li><strong>查看关键词列表</strong> - 显示图文关键词列表</li>
|
|
147
|
+
</ul>
|
|
148
|
+
<h2>测试环境指令</h2>
|
|
149
|
+
<ul>
|
|
150
|
+
<li><strong>测试授权 <目标ID...></strong> - 将群/私聊加入测试名单</li>
|
|
151
|
+
<li><strong>取消测试授权 <目标ID...></strong> - 移出测试名单</li>
|
|
152
|
+
<li><strong>测试同步</strong> - 全局问答复制到测试环境</li>
|
|
153
|
+
<li><strong>测试发布</strong> - 测试问答发布到全局(自动备份)</li>
|
|
154
|
+
<li><strong>测试添加/删除/修改</strong> - 同正式指令,仅操作测试数据</li>
|
|
155
|
+
</ul>
|
|
156
|
+
</body>
|
|
157
|
+
</html>`;
|
|
158
|
+
async function apply(ctx, config) {
|
|
159
|
+
const root = path.join(ctx.baseDir, "data", "qxgl-satori");
|
|
160
|
+
if (!fs.existsSync(root)) {
|
|
161
|
+
fs.mkdirSync(root, { recursive: true });
|
|
162
|
+
}
|
|
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
|
+
let lastTriggerTimes = {};
|
|
168
|
+
function logInfo(message) {
|
|
169
|
+
if (config.loggerinfo) logger.info(message);
|
|
170
|
+
}
|
|
171
|
+
__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
|
+
ctx.model.extend("qxgl_satori_auth", {
|
|
177
|
+
platform: "string",
|
|
178
|
+
channelId: "string",
|
|
179
|
+
expiryDate: "date",
|
|
180
|
+
isblockedchannel: "boolean",
|
|
181
|
+
authorizer: "string",
|
|
182
|
+
channelName: "string",
|
|
183
|
+
updateDate: "date",
|
|
184
|
+
testChannels: "list"
|
|
185
|
+
}, {
|
|
186
|
+
primary: ["platform", "channelId"]
|
|
187
|
+
// 多账号共享关键:移除 selfId
|
|
188
|
+
});
|
|
189
|
+
async function updateChannelName(session) {
|
|
190
|
+
if (!session.channelId || session.channelId.startsWith("private:")) return "私聊";
|
|
191
|
+
let name = "未知群聊";
|
|
192
|
+
try {
|
|
193
|
+
if (session.bot && typeof session.bot.getGuild === "function") {
|
|
194
|
+
const info = await session.bot.getGuild(session.channelId);
|
|
195
|
+
name = info?.name || name;
|
|
196
|
+
}
|
|
197
|
+
} catch (e) {
|
|
198
|
+
logInfo(`获取群名失败: ${e.message}`);
|
|
199
|
+
}
|
|
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
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
__name(hasPermission, "hasPermission");
|
|
227
|
+
async function authorizeGroup(session, channelId, months, authorizer) {
|
|
228
|
+
if (!channelId || !months) return "请提供群号和月数";
|
|
229
|
+
const monthsNum = parseInt(months);
|
|
230
|
+
if (isNaN(monthsNum)) return "月数必须是数字";
|
|
231
|
+
const channelName = await updateChannelName({ ...session, channelId });
|
|
232
|
+
const currentDate = /* @__PURE__ */ new Date();
|
|
233
|
+
const [record] = await ctx.database.get("qxgl_satori_auth", {
|
|
234
|
+
platform: session.platform,
|
|
235
|
+
channelId
|
|
236
|
+
});
|
|
237
|
+
let newExpiryDate;
|
|
238
|
+
if (!record) {
|
|
239
|
+
newExpiryDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + monthsNum, currentDate.getDate());
|
|
240
|
+
await ctx.database.create("qxgl_satori_auth", {
|
|
241
|
+
platform: session.platform,
|
|
242
|
+
channelId,
|
|
243
|
+
expiryDate: newExpiryDate,
|
|
244
|
+
isblockedchannel: false,
|
|
245
|
+
authorizer: authorizer || "蒙面人",
|
|
246
|
+
channelName,
|
|
247
|
+
updateDate: currentDate,
|
|
248
|
+
testChannels: []
|
|
249
|
+
});
|
|
250
|
+
} else {
|
|
251
|
+
const currentExpiry = record.expiryDate ? new Date(record.expiryDate) : currentDate;
|
|
252
|
+
newExpiryDate = new Date(currentExpiry.getFullYear(), currentExpiry.getMonth() + monthsNum, currentExpiry.getDate());
|
|
253
|
+
await ctx.database.set(
|
|
254
|
+
"qxgl_satori_auth",
|
|
255
|
+
{ platform: session.platform, channelId },
|
|
256
|
+
{
|
|
257
|
+
expiryDate: newExpiryDate,
|
|
258
|
+
isblockedchannel: false,
|
|
259
|
+
authorizer: authorizer || "蒙面人",
|
|
260
|
+
channelName,
|
|
261
|
+
updateDate: currentDate
|
|
262
|
+
}
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
return `群号 "${channelId}" 的授权已${monthsNum > 0 ? "增加" : "减少"} ${Math.abs(monthsNum)} 个月
|
|
266
|
+
新的到期时间:${formatDate(newExpiryDate)}
|
|
267
|
+
开启人:${authorizer || "蒙面人"}
|
|
268
|
+
更新日期:${formatDate(currentDate)}`;
|
|
269
|
+
}
|
|
270
|
+
__name(authorizeGroup, "authorizeGroup");
|
|
271
|
+
async function authorizePrivate(session, userId, months, authorizer) {
|
|
272
|
+
if (!userId || !months) return "请提供用户ID和月数";
|
|
273
|
+
const monthsNum = parseInt(months);
|
|
274
|
+
if (isNaN(monthsNum)) return "月数必须是数字";
|
|
275
|
+
const channelId = `private:${userId}`;
|
|
276
|
+
const currentDate = /* @__PURE__ */ new Date();
|
|
277
|
+
const [record] = await ctx.database.get("qxgl_satori_auth", {
|
|
278
|
+
platform: session.platform,
|
|
279
|
+
channelId
|
|
280
|
+
});
|
|
281
|
+
let newExpiryDate;
|
|
282
|
+
if (!record) {
|
|
283
|
+
newExpiryDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + monthsNum, currentDate.getDate());
|
|
284
|
+
await ctx.database.create("qxgl_satori_auth", {
|
|
285
|
+
platform: session.platform,
|
|
286
|
+
channelId,
|
|
287
|
+
expiryDate: newExpiryDate,
|
|
288
|
+
isblockedchannel: false,
|
|
289
|
+
authorizer: authorizer || "蒙面人",
|
|
290
|
+
channelName: "私聊",
|
|
291
|
+
updateDate: currentDate,
|
|
292
|
+
testChannels: []
|
|
293
|
+
});
|
|
294
|
+
} else {
|
|
295
|
+
const currentExpiry = record.expiryDate ? new Date(record.expiryDate) : currentDate;
|
|
296
|
+
newExpiryDate = new Date(currentExpiry.getFullYear(), currentExpiry.getMonth() + monthsNum, currentExpiry.getDate());
|
|
297
|
+
await ctx.database.set(
|
|
298
|
+
"qxgl_satori_auth",
|
|
299
|
+
{ platform: session.platform, channelId },
|
|
300
|
+
{
|
|
301
|
+
expiryDate: newExpiryDate,
|
|
302
|
+
authorizer: authorizer || "蒙面人",
|
|
303
|
+
updateDate: currentDate
|
|
304
|
+
}
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
return `用户ID "${userId}" 的授权已${monthsNum > 0 ? "增加" : "减少"} ${Math.abs(monthsNum)} 个月
|
|
308
|
+
新的到期时间:${formatDate(newExpiryDate)}`;
|
|
309
|
+
}
|
|
310
|
+
__name(authorizePrivate, "authorizePrivate");
|
|
311
|
+
async function cancelAuthorization(session, targetId) {
|
|
312
|
+
if (!targetId) return "请提供目标ID";
|
|
313
|
+
const channelId = targetId.startsWith("private:") ? targetId : targetId;
|
|
314
|
+
const [record] = await ctx.database.get("qxgl_satori_auth", {
|
|
315
|
+
platform: session.platform,
|
|
316
|
+
channelId
|
|
317
|
+
});
|
|
318
|
+
if (!record) return `未找到授权记录:${targetId}`;
|
|
319
|
+
await ctx.database.remove("qxgl_satori_auth", {
|
|
320
|
+
platform: session.platform,
|
|
321
|
+
channelId
|
|
322
|
+
});
|
|
323
|
+
return `授权已取消:${targetId}`;
|
|
324
|
+
}
|
|
325
|
+
__name(cancelAuthorization, "cancelAuthorization");
|
|
326
|
+
async function changeAuthorization(session, sourceId, targetId) {
|
|
327
|
+
if (!sourceId || !targetId) return "请提供原ID和新ID";
|
|
328
|
+
const sourceChannelId = sourceId.startsWith("private:") ? sourceId : sourceId;
|
|
329
|
+
const targetChannelId = targetId.startsWith("private:") ? targetId : targetId;
|
|
330
|
+
const [sourceRecord] = await ctx.database.get("qxgl_satori_auth", {
|
|
331
|
+
platform: session.platform,
|
|
332
|
+
channelId: sourceChannelId
|
|
333
|
+
});
|
|
334
|
+
if (!sourceRecord) return `未找到原授权记录:${sourceId}`;
|
|
335
|
+
const [targetRecord] = await ctx.database.get("qxgl_satori_auth", {
|
|
336
|
+
platform: session.platform,
|
|
337
|
+
channelId: targetChannelId
|
|
338
|
+
});
|
|
339
|
+
if (targetRecord) return `新目标已存在授权记录:${targetId}`;
|
|
340
|
+
await ctx.database.create("qxgl_satori_auth", {
|
|
341
|
+
platform: session.platform,
|
|
342
|
+
channelId: targetChannelId,
|
|
343
|
+
expiryDate: sourceRecord.expiryDate,
|
|
344
|
+
isblockedchannel: sourceRecord.isblockedchannel,
|
|
345
|
+
authorizer: sourceRecord.authorizer,
|
|
346
|
+
channelName: "新授权",
|
|
347
|
+
updateDate: /* @__PURE__ */ new Date(),
|
|
348
|
+
testChannels: sourceRecord.testChannels || []
|
|
349
|
+
});
|
|
350
|
+
await ctx.database.remove("qxgl_satori_auth", {
|
|
351
|
+
platform: session.platform,
|
|
352
|
+
channelId: sourceChannelId
|
|
353
|
+
});
|
|
354
|
+
return `授权已从 ${sourceId} 更换到 ${targetId}`;
|
|
355
|
+
}
|
|
356
|
+
__name(changeAuthorization, "changeAuthorization");
|
|
357
|
+
async function checkExpiryTime(session) {
|
|
358
|
+
const [record] = await ctx.database.get("qxgl_satori_auth", {
|
|
359
|
+
platform: session.platform,
|
|
360
|
+
channelId: session.channelId
|
|
361
|
+
});
|
|
362
|
+
if (!record || !record.expiryDate) {
|
|
363
|
+
return `未设置到期时间`;
|
|
364
|
+
} else {
|
|
365
|
+
return `本次使用由《${record.authorizer || "蒙面人"}》激活
|
|
366
|
+
到期时间:${formatDate(record.expiryDate)}`;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
__name(checkExpiryTime, "checkExpiryTime");
|
|
370
|
+
async function queryExpiryTime(session, targetId) {
|
|
371
|
+
if (!targetId) {
|
|
372
|
+
return await checkExpiryTime(session);
|
|
373
|
+
}
|
|
374
|
+
const channelId = targetId.startsWith("private:") ? targetId : targetId;
|
|
375
|
+
const [record] = await ctx.database.get("qxgl_satori_auth", {
|
|
376
|
+
platform: session.platform,
|
|
377
|
+
channelId
|
|
378
|
+
});
|
|
379
|
+
if (!record || !record.expiryDate) {
|
|
380
|
+
return `未找到授权记录:${targetId}`;
|
|
381
|
+
} else {
|
|
382
|
+
return `授权到期时间:${formatDate(record.expiryDate)}
|
|
383
|
+
开启人:${record.authorizer || "蒙面人"}`;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
__name(queryExpiryTime, "queryExpiryTime");
|
|
387
|
+
async function globalDelayExpiry(session, days) {
|
|
388
|
+
const daysNum = parseInt(days);
|
|
389
|
+
if (isNaN(daysNum) || daysNum <= 0) return "请输入有效的天数(正整数)";
|
|
390
|
+
const records = await ctx.database.get("qxgl_satori_auth");
|
|
391
|
+
const currentDate = /* @__PURE__ */ new Date();
|
|
392
|
+
let updatedCount = 0;
|
|
393
|
+
for (const record of records) {
|
|
394
|
+
if (record.expiryDate) {
|
|
395
|
+
const currentExpiry = new Date(record.expiryDate);
|
|
396
|
+
if (currentExpiry <= currentDate) continue;
|
|
397
|
+
const newExpiry = new Date(currentExpiry.getTime() + daysNum * 24 * 60 * 60 * 1e3);
|
|
398
|
+
await ctx.database.set(
|
|
399
|
+
"qxgl_satori_auth",
|
|
400
|
+
{ platform: record.platform, channelId: record.channelId },
|
|
401
|
+
{ expiryDate: newExpiry, updateDate: currentDate }
|
|
402
|
+
);
|
|
403
|
+
updatedCount++;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return `已为 ${updatedCount} 个群聊延期 ${daysNum} 天`;
|
|
407
|
+
}
|
|
408
|
+
__name(globalDelayExpiry, "globalDelayExpiry");
|
|
409
|
+
async function globalReduceExpiry(session, days) {
|
|
410
|
+
const daysNum = parseInt(days);
|
|
411
|
+
if (isNaN(daysNum) || daysNum <= 0) return "请输入有效的天数(正整数)";
|
|
412
|
+
const records = await ctx.database.get("qxgl_satori_auth");
|
|
413
|
+
const currentDate = /* @__PURE__ */ new Date();
|
|
414
|
+
let updatedCount = 0;
|
|
415
|
+
for (const record of records) {
|
|
416
|
+
if (record.expiryDate) {
|
|
417
|
+
const currentExpiry = new Date(record.expiryDate);
|
|
418
|
+
if (currentExpiry <= currentDate) continue;
|
|
419
|
+
const newExpiry = new Date(currentExpiry.getTime() - daysNum * 24 * 60 * 60 * 1e3);
|
|
420
|
+
await ctx.database.set(
|
|
421
|
+
"qxgl_satori_auth",
|
|
422
|
+
{ platform: record.platform, channelId: record.channelId },
|
|
423
|
+
{ expiryDate: newExpiry, updateDate: currentDate }
|
|
424
|
+
);
|
|
425
|
+
updatedCount++;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return `已为 ${updatedCount} 个群聊减少 ${daysNum} 天`;
|
|
429
|
+
}
|
|
430
|
+
__name(globalReduceExpiry, "globalReduceExpiry");
|
|
431
|
+
function escapeRegExp(string) {
|
|
432
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
433
|
+
}
|
|
434
|
+
__name(escapeRegExp, "escapeRegExp");
|
|
435
|
+
async function downloadImage(url, rootDir, isGlobal, session) {
|
|
436
|
+
try {
|
|
437
|
+
const buf = await ctx.http.get(url, { responseType: "arraybuffer" }).then((r) => Buffer.from(r));
|
|
438
|
+
const folder = path.join(rootDir, isGlobal ? "global" : `${session.platform}_${session.channelId}`);
|
|
439
|
+
await fsPromises.mkdir(folder, { recursive: true });
|
|
440
|
+
const fileName = `${Date.now()}_${Math.random().toString(36).slice(2)}.${config.defaultImageExtension}`;
|
|
441
|
+
const localPath = path.join(folder, fileName);
|
|
442
|
+
await fsPromises.writeFile(localPath, buf);
|
|
443
|
+
return localPath;
|
|
444
|
+
} catch (e) {
|
|
445
|
+
logger.error(`下载图片失败: ${e.message}`);
|
|
446
|
+
throw new Error(`图片下载失败,请更换链接`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
__name(downloadImage, "downloadImage");
|
|
450
|
+
async function parseReplyContent(reply, rootDir, session, options) {
|
|
451
|
+
const elements = h.parse(reply);
|
|
452
|
+
const results = [];
|
|
453
|
+
for (const element of elements) {
|
|
454
|
+
let item = null;
|
|
455
|
+
if (element.type === "img" || element.type === "image") {
|
|
456
|
+
const localPath = await downloadImage(element.attrs.src, rootDir, options.global, session);
|
|
457
|
+
item = {
|
|
458
|
+
type: "image",
|
|
459
|
+
text: localPath,
|
|
460
|
+
fileSize: element.attrs.fileSize || "",
|
|
461
|
+
replyway: options.forward || config.MultisegmentAdditionRecoveryEffect
|
|
462
|
+
};
|
|
463
|
+
} else if (element.type === "text") {
|
|
464
|
+
item = {
|
|
465
|
+
type: "text",
|
|
466
|
+
text: element.attrs.content,
|
|
467
|
+
replyway: options.forward || config.MultisegmentAdditionRecoveryEffect
|
|
468
|
+
};
|
|
469
|
+
} else if (element.type === "at") {
|
|
470
|
+
item = {
|
|
471
|
+
type: "at",
|
|
472
|
+
text: element.attrs.id,
|
|
473
|
+
replyway: options.forward || config.MultisegmentAdditionRecoveryEffect
|
|
474
|
+
};
|
|
475
|
+
} else if (element.type === "audio") {
|
|
476
|
+
item = { type: "audio", text: element.attrs.path || element.attrs.url, replyway: options.forward || config.MultisegmentAdditionRecoveryEffect };
|
|
477
|
+
} else if (element.type === "video") {
|
|
478
|
+
item = { type: "video", text: element.attrs.src, replyway: options.forward || config.MultisegmentAdditionRecoveryEffect };
|
|
479
|
+
}
|
|
480
|
+
if (item) results.push(item);
|
|
481
|
+
}
|
|
482
|
+
return results;
|
|
483
|
+
}
|
|
484
|
+
__name(parseReplyContent, "parseReplyContent");
|
|
485
|
+
async function formatReply(reply, session) {
|
|
486
|
+
if (reply.type === "image") {
|
|
487
|
+
if (config.pictureStrategy === "http") {
|
|
488
|
+
const fileName = path.basename(reply.text);
|
|
489
|
+
const url = `${config.httpServePath}/${reply.text.includes("/global/") ? "global/" : ""}${fileName}`;
|
|
490
|
+
return h.image(url);
|
|
491
|
+
} else {
|
|
492
|
+
if (isSatoriAdapter(session)) {
|
|
493
|
+
return h.image(reply.text);
|
|
494
|
+
} else {
|
|
495
|
+
if (!reply.text.startsWith("http") && !reply.text.startsWith("data:")) {
|
|
496
|
+
return h.image(pathToFileURL(reply.text).href);
|
|
497
|
+
}
|
|
498
|
+
return h.image(reply.text);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
} else if (reply.type === "text") {
|
|
502
|
+
return h.text(reply.text);
|
|
503
|
+
} else if (reply.type === "at") {
|
|
504
|
+
return h.at(reply.text);
|
|
505
|
+
} else if (reply.type === "audio") {
|
|
506
|
+
return h.audio(reply.text);
|
|
507
|
+
} else if (reply.type === "video") {
|
|
508
|
+
return h.video(reply.text);
|
|
509
|
+
}
|
|
510
|
+
return h.text(String(reply.text));
|
|
511
|
+
}
|
|
512
|
+
__name(formatReply, "formatReply");
|
|
513
|
+
async function addKeywordReply(session, filePath, keyword, options) {
|
|
514
|
+
let data = {};
|
|
515
|
+
if (fs.existsSync(filePath)) {
|
|
516
|
+
try {
|
|
517
|
+
data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
518
|
+
} catch (e) {
|
|
519
|
+
data = {};
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
if (config.Treat_all_as_lowercase) keyword = keyword.toLowerCase();
|
|
523
|
+
const key = options.regex ? `regex:${keyword}` : keyword;
|
|
524
|
+
if (!data[key]) data[key] = [];
|
|
525
|
+
if (data[key].length > 0 && config.HandleDuplicateKeywords === "3") {
|
|
526
|
+
return `关键词 "${keyword}" 已存在,请先删除`;
|
|
527
|
+
}
|
|
528
|
+
if (config.HandleDuplicateKeywords === "1") data[key] = [];
|
|
529
|
+
if (config.AlwayPrompt === "2" || config.AlwayPrompt === "3") {
|
|
530
|
+
await session.send(config.Prompt);
|
|
531
|
+
}
|
|
532
|
+
const replies = [];
|
|
533
|
+
if (config.MatchPatternForExit === "1") {
|
|
534
|
+
const timeout = config.addKeywordTime * 6e4;
|
|
535
|
+
const reply = await session.prompt(timeout);
|
|
536
|
+
if (!reply) return "输入超时";
|
|
537
|
+
if (reply.includes(config.KeywordOfEsc)) return "已取消";
|
|
538
|
+
const parsed = await parseReplyContent(reply, root, session, options);
|
|
539
|
+
replies.push(parsed);
|
|
540
|
+
} else {
|
|
541
|
+
while (true) {
|
|
542
|
+
if (config.AlwayPrompt === "3") await session.send(config.Prompt);
|
|
543
|
+
const timeout = config.addKeywordTime * 6e4;
|
|
544
|
+
const reply = await session.prompt(timeout);
|
|
545
|
+
if (!reply) return "输入超时";
|
|
546
|
+
if (reply.includes(config.KeywordOfEsc)) return "已取消";
|
|
547
|
+
if (config.MatchPatternForExit === "2" && reply === config.KeywordOfEnd) break;
|
|
548
|
+
if (config.MatchPatternForExit === "3" && reply.includes(config.KeywordOfEnd)) break;
|
|
549
|
+
if (config.MatchPatternForExit === "4" && (reply === config.KeywordOfEnd || reply.includes(config.KeywordOfEnd))) break;
|
|
550
|
+
const parsed = await parseReplyContent(reply, root, session, options);
|
|
551
|
+
replies.push(...parsed);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
data[key].push(replies);
|
|
555
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
556
|
+
return `关键词 "${keyword}" 的回复已添加`;
|
|
557
|
+
}
|
|
558
|
+
__name(addKeywordReply, "addKeywordReply");
|
|
559
|
+
async function deleteKeywordReply(session, filePath, keyword, specifiedIndex) {
|
|
560
|
+
if (!fs.existsSync(filePath)) return `关键词 "${keyword}" 不存在`;
|
|
561
|
+
let data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
562
|
+
const searchKey = config.Treat_all_as_lowercase ? keyword.toLowerCase() : keyword;
|
|
563
|
+
let foundKey = null;
|
|
564
|
+
for (const k in data) {
|
|
565
|
+
const checkKey = config.Treat_all_as_lowercase ? k.toLowerCase() : k;
|
|
566
|
+
const realKey = checkKey.startsWith("regex:") ? checkKey.slice(6) : checkKey;
|
|
567
|
+
if (realKey === searchKey) {
|
|
568
|
+
foundKey = k;
|
|
569
|
+
break;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if (!foundKey) return `关键词 "${keyword}" 不存在`;
|
|
573
|
+
const replies = data[foundKey];
|
|
574
|
+
if (config.Delete_Branch_Only && replies.length > 1) {
|
|
575
|
+
if (!specifiedIndex) {
|
|
576
|
+
return `该关键词有 ${replies.length} 个回复,请使用 -q 指定删除序号(1-${replies.length})`;
|
|
577
|
+
}
|
|
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
|
+
}
|
|
589
|
+
}
|
|
590
|
+
__name(deleteKeywordReply, "deleteKeywordReply");
|
|
591
|
+
async function getTestChannels(session) {
|
|
592
|
+
const records = await ctx.database.get("qxgl_satori_auth", { platform: session.platform });
|
|
593
|
+
if (records.length > 0 && records[0].testChannels) {
|
|
594
|
+
return records[0].testChannels;
|
|
595
|
+
}
|
|
596
|
+
return [];
|
|
597
|
+
}
|
|
598
|
+
__name(getTestChannels, "getTestChannels");
|
|
599
|
+
async function setTestChannels(session, channels) {
|
|
600
|
+
await ctx.database.set(
|
|
601
|
+
"qxgl_satori_auth",
|
|
602
|
+
{ platform: session.platform },
|
|
603
|
+
{ testChannels: channels }
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
__name(setTestChannels, "setTestChannels");
|
|
607
|
+
ctx.before("command", (session, command) => {
|
|
608
|
+
const whiteList = [
|
|
609
|
+
"qxgl-satori.群授权",
|
|
610
|
+
"qxgl-satori.私聊授权",
|
|
611
|
+
"qxgl-satori.取消授权",
|
|
612
|
+
"qxgl-satori.更换授权",
|
|
613
|
+
"qxgl-satori.到期时间",
|
|
614
|
+
"qxgl-satori.查询到期",
|
|
615
|
+
"qxgl-satori.全局延期",
|
|
616
|
+
"qxgl-satori.全局减少",
|
|
617
|
+
"qxgl-satori.列出已记录群",
|
|
618
|
+
"qxgl-satori.更新名称"
|
|
619
|
+
];
|
|
620
|
+
if (whiteList.includes(command.name)) {
|
|
621
|
+
session.skipAuthCheck = true;
|
|
622
|
+
}
|
|
623
|
+
return true;
|
|
624
|
+
}, true);
|
|
625
|
+
ctx.middleware(async (session, next) => {
|
|
626
|
+
if (!session.channelId) return next();
|
|
627
|
+
const [record] = await ctx.database.get("qxgl_satori_auth", {
|
|
628
|
+
platform: session.platform,
|
|
629
|
+
channelId: session.channelId
|
|
630
|
+
});
|
|
631
|
+
if (!record) {
|
|
632
|
+
const channelName = await updateChannelName(session);
|
|
633
|
+
await ctx.database.create("qxgl_satori_auth", {
|
|
634
|
+
platform: session.platform,
|
|
635
|
+
channelId: session.channelId,
|
|
636
|
+
expiryDate: config.enableAuthSystem ? null : /* @__PURE__ */ new Date("2099-12-31"),
|
|
637
|
+
isblockedchannel: false,
|
|
638
|
+
authorizer: "",
|
|
639
|
+
channelName,
|
|
640
|
+
updateDate: /* @__PURE__ */ new Date(),
|
|
641
|
+
testChannels: []
|
|
642
|
+
});
|
|
643
|
+
logInfo(`自动记录新群聊: ${session.channelId}`);
|
|
644
|
+
}
|
|
645
|
+
return next();
|
|
646
|
+
}, true);
|
|
647
|
+
ctx.middleware(async (session, next) => {
|
|
648
|
+
if (session.skipAuthCheck) return next();
|
|
649
|
+
if (!config.enableAuthSystem) return next();
|
|
650
|
+
const [record] = await ctx.database.get("qxgl_satori_auth", {
|
|
651
|
+
platform: session.platform,
|
|
652
|
+
channelId: session.channelId
|
|
653
|
+
});
|
|
654
|
+
if (record?.expiryDate) {
|
|
655
|
+
const expiry = new Date(record.expiryDate);
|
|
656
|
+
const now = /* @__PURE__ */ new Date();
|
|
657
|
+
if (now > expiry) {
|
|
658
|
+
if (config.autoLeaveOnExpiry && session.platform === "onebot") {
|
|
659
|
+
await session.bot?.leaveGroup?.(session.channelId);
|
|
660
|
+
}
|
|
661
|
+
await ctx.database.set(
|
|
662
|
+
"qxgl_satori_auth",
|
|
663
|
+
{ platform: session.platform, channelId: session.channelId },
|
|
664
|
+
{ isblockedchannel: true }
|
|
665
|
+
);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
if (record?.isblockedchannel) return;
|
|
670
|
+
return next();
|
|
671
|
+
});
|
|
672
|
+
ctx.command(`${pluginName}/群授权 <channelId> <months> [authorizer]`, "群授权", { authority: config.commandAuthority }).action(async ({ session }, channelId, months, authorizer) => {
|
|
673
|
+
return await authorizeGroup(session, channelId, months, authorizer);
|
|
674
|
+
});
|
|
675
|
+
ctx.command(`${pluginName}/私聊授权 <userId> <months> [authorizer]`, "私聊授权", { authority: config.commandAuthority }).action(async ({ session }, userId, months, authorizer) => {
|
|
676
|
+
return await authorizePrivate(session, userId, months, authorizer);
|
|
677
|
+
});
|
|
678
|
+
ctx.command(`${pluginName}/取消授权 <targetId>`, "取消授权", { authority: config.commandAuthority }).action(async ({ session }, targetId) => {
|
|
679
|
+
return await cancelAuthorization(session, targetId);
|
|
680
|
+
});
|
|
681
|
+
ctx.command(`${pluginName}/更换授权 <sourceId> <targetId>`, "更换授权", { authority: config.commandAuthority }).action(async ({ session }, sourceId, targetId) => {
|
|
682
|
+
return await changeAuthorization(session, sourceId, targetId);
|
|
683
|
+
});
|
|
684
|
+
ctx.command(`${pluginName}/到期时间`, "查询本群/私聊到期时间", { authority: 0 }).action(async ({ session }) => {
|
|
685
|
+
return await checkExpiryTime(session);
|
|
686
|
+
});
|
|
687
|
+
ctx.command(`${pluginName}/查询到期 [targetId]`, "查询指定群/私聊到期时间", { authority: config.commandAuthority }).action(async ({ session }, targetId) => {
|
|
688
|
+
return await queryExpiryTime(session, targetId);
|
|
689
|
+
});
|
|
690
|
+
ctx.command(`${pluginName}/全局延期 <days>`, "为所有群统一延期", { authority: config.commandAuthority }).action(async ({ session }, days) => {
|
|
691
|
+
return await globalDelayExpiry(session, days);
|
|
692
|
+
});
|
|
693
|
+
ctx.command(`${pluginName}/全局减少 <days>`, "为所有群统一减少", { authority: config.commandAuthority }).action(async ({ session }, days) => {
|
|
694
|
+
return await globalReduceExpiry(session, days);
|
|
695
|
+
});
|
|
696
|
+
ctx.command(`${pluginName}/列出已记录群`, "列出所有已记录的群", { authority: config.commandAuthority }).action(async ({ session }) => {
|
|
697
|
+
const records = await ctx.database.get("qxgl_satori_auth");
|
|
698
|
+
const groups = records.filter((r) => !r.channelId.startsWith("private:"));
|
|
699
|
+
if (!groups.length) return "暂无群聊记录";
|
|
700
|
+
return groups.map(
|
|
701
|
+
(r) => `群名:${r.channelName}
|
|
702
|
+
群号:${r.channelId}
|
|
703
|
+
到期:${formatDate(r.expiryDate)}
|
|
704
|
+
开启人:${r.authorizer || "蒙面人"}
|
|
705
|
+
更新:${formatDate(r.updateDate)}`
|
|
706
|
+
).join("\n\n");
|
|
707
|
+
});
|
|
708
|
+
ctx.command(`${pluginName}/更新名称`, "刷新所有群名称缓存", { authority: config.commandAuthority }).action(async ({ session }) => {
|
|
709
|
+
const records = await ctx.database.get("qxgl_satori_auth");
|
|
710
|
+
let count = 0;
|
|
711
|
+
for (const record of records) {
|
|
712
|
+
if (record.channelId.startsWith("private:")) continue;
|
|
713
|
+
const name = await updateChannelName({ ...session, channelId: record.channelId });
|
|
714
|
+
await ctx.database.set(
|
|
715
|
+
"qxgl_satori_auth",
|
|
716
|
+
{ platform: record.platform, channelId: record.channelId },
|
|
717
|
+
{ channelName: name }
|
|
718
|
+
);
|
|
719
|
+
count++;
|
|
720
|
+
}
|
|
721
|
+
return `已更新 ${count} 个群聊名称`;
|
|
722
|
+
});
|
|
723
|
+
ctx.command(`${pluginName}/添加 <keyword>`, "添加关键词", { authority: config.commandAuthority }).option("regex", "-x 使用正则匹配").option("forward", "-f <type> 指定回复方式(1-4)").action(async ({ session, options }, keyword) => {
|
|
724
|
+
if (!hasPermission(session, "添加")) return "权限不足";
|
|
725
|
+
if (!keyword) return "请提供关键词";
|
|
726
|
+
const filePath = path.join(root, `${session.platform}_${session.channelId}.json`);
|
|
727
|
+
return await addKeywordReply(session, filePath, keyword, { ...options, global: false });
|
|
728
|
+
});
|
|
729
|
+
ctx.command(`${pluginName}/全局添加 <keyword>`, "添加全局关键词", { authority: config.commandAuthority }).option("regex", "-x 使用正则匹配").option("forward", "-f <type> 指定回复方式(1-4)").action(async ({ session, options }, keyword) => {
|
|
730
|
+
if (!hasPermission(session, "全局添加")) return "权限不足";
|
|
731
|
+
if (!keyword) return "请提供关键词";
|
|
732
|
+
return await addKeywordReply(session, globalFile, keyword, { ...options, global: true });
|
|
733
|
+
});
|
|
734
|
+
ctx.command(`${pluginName}/删除 <keyword>`, "删除关键词", { authority: config.commandAuthority }).option("question", "-q <index> 指定删除序号").action(async ({ session, options }, keyword) => {
|
|
735
|
+
if (!hasPermission(session, "删除")) return "权限不足";
|
|
736
|
+
if (!keyword) return "请提供关键词";
|
|
737
|
+
const filePath = path.join(root, `${session.platform}_${session.channelId}.json`);
|
|
738
|
+
return await deleteKeywordReply(session, filePath, keyword, options.question);
|
|
739
|
+
});
|
|
740
|
+
ctx.command(`${pluginName}/全局删除 <keyword>`, "删除全局关键词", { authority: config.commandAuthority }).option("question", "-q <index> 指定删除序号").action(async ({ session, options }, keyword) => {
|
|
741
|
+
if (!hasPermission(session, "全局删除")) return "权限不足";
|
|
742
|
+
if (!keyword) return "请提供关键词";
|
|
743
|
+
return await deleteKeywordReply(session, globalFile, keyword, options.question);
|
|
744
|
+
});
|
|
745
|
+
ctx.command(`${pluginName}/修改 <keyword>`, "修改关键词回复", { authority: config.commandAuthority }).option("question", "-q <index> 指定回复序号").action(async ({ session, options }, keyword) => {
|
|
746
|
+
if (!hasPermission(session, "修改")) return "权限不足";
|
|
747
|
+
if (!keyword) return "请提供关键词";
|
|
748
|
+
const filePath = path.join(root, `${session.platform}_${session.channelId}.json`);
|
|
749
|
+
if (!fs.existsSync(filePath)) return "未找到数据";
|
|
750
|
+
let data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
751
|
+
const key = config.Treat_all_as_lowercase ? keyword.toLowerCase() : keyword;
|
|
752
|
+
if (!data[key]) return `关键词 "${keyword}" 不存在`;
|
|
753
|
+
const idx = (parseInt(options.question) || 1) - 1;
|
|
754
|
+
if (idx < 0 || idx >= data[key].length) return "序号无效";
|
|
755
|
+
let current = "";
|
|
756
|
+
for (const item of data[key][idx]) {
|
|
757
|
+
current += await formatReply(item, session);
|
|
758
|
+
}
|
|
759
|
+
await session.send(`正在修改【${keyword}】的第 ${idx + 1} 条回复:
|
|
760
|
+
${current}`);
|
|
761
|
+
await session.send(`请一次性输入新内容(${config.KeywordOfEsc} 取消):`);
|
|
762
|
+
const reply = await session.prompt(config.addKeywordTime * 6e4);
|
|
763
|
+
if (!reply || reply.includes(config.KeywordOfEsc)) return "已取消";
|
|
764
|
+
data[key][idx] = await parseReplyContent(reply, root, session, { global: false });
|
|
765
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
766
|
+
return `已修改 "${keyword}" 的第 ${idx + 1} 条回复`;
|
|
767
|
+
});
|
|
768
|
+
ctx.command(`${pluginName}/全局修改 <keyword>`, "修改全局关键词", { authority: config.commandAuthority }).option("question", "-q <index> 指定回复序号").action(async ({ session, options }, keyword) => {
|
|
769
|
+
if (!hasPermission(session, "全局修改")) return "权限不足";
|
|
770
|
+
if (!keyword) return "请提供关键词";
|
|
771
|
+
if (!fs.existsSync(globalFile)) return "未找到全局数据";
|
|
772
|
+
let data = JSON.parse(fs.readFileSync(globalFile, "utf-8"));
|
|
773
|
+
const key = config.Treat_all_as_lowercase ? keyword.toLowerCase() : keyword;
|
|
774
|
+
if (!data[key]) return `全局关键词 "${keyword}" 不存在`;
|
|
775
|
+
const idx = (parseInt(options.question) || 1) - 1;
|
|
776
|
+
if (idx < 0 || idx >= data[key].length) return "序号无效";
|
|
777
|
+
let current = "";
|
|
778
|
+
for (const item of data[key][idx]) {
|
|
779
|
+
current += await formatReply(item, session);
|
|
780
|
+
}
|
|
781
|
+
await session.send(`正在修改全局【${keyword}】的第 ${idx + 1} 条回复:
|
|
782
|
+
${current}`);
|
|
783
|
+
await session.send(`请一次性输入新内容(${config.KeywordOfEsc} 取消):`);
|
|
784
|
+
const reply = await session.prompt(config.addKeywordTime * 6e4);
|
|
785
|
+
if (!reply || reply.includes(config.KeywordOfEsc)) return "已取消";
|
|
786
|
+
data[key][idx] = await parseReplyContent(reply, root, session, { global: true });
|
|
787
|
+
fs.writeFileSync(globalFile, JSON.stringify(data, null, 2));
|
|
788
|
+
return `已修改全局 "${keyword}" 的第 ${idx + 1} 条回复`;
|
|
789
|
+
});
|
|
790
|
+
ctx.command(`${pluginName}/查找关键词 <keyword>`, "查找关键词", { authority: config.commandAuthority }).action(async ({ session }, keyword) => {
|
|
791
|
+
if (!hasPermission(session, "查找关键词")) return "权限不足";
|
|
792
|
+
if (!keyword) return "请提供关键词";
|
|
793
|
+
const files = [];
|
|
794
|
+
if (config.Search_Range === "1" || config.Search_Range === "3") {
|
|
795
|
+
files.push(path.join(root, `${session.platform}_${session.channelId}.json`));
|
|
796
|
+
}
|
|
797
|
+
if (config.Search_Range === "2" || config.Search_Range === "3") {
|
|
798
|
+
files.push(globalFile);
|
|
799
|
+
const allFiles = fs.readdirSync(root).filter((f) => f.endsWith(".json") && f !== "test.json" && f !== "global.json");
|
|
800
|
+
files.push(...allFiles.map((f) => path.join(root, f)));
|
|
801
|
+
}
|
|
802
|
+
const results = [];
|
|
803
|
+
for (const file of [...new Set(files)]) {
|
|
804
|
+
if (!fs.existsSync(file)) continue;
|
|
805
|
+
const data = JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
806
|
+
for (const key in data) {
|
|
807
|
+
const checkKey = config.Treat_all_as_lowercase ? key.toLowerCase() : key;
|
|
808
|
+
const searchKey = config.Treat_all_as_lowercase ? keyword.toLowerCase() : keyword;
|
|
809
|
+
if (checkKey.includes(searchKey)) {
|
|
810
|
+
const channelId = path.basename(file, ".json").replace(`${session.platform}_`, "");
|
|
811
|
+
const content = data[key].map(
|
|
812
|
+
(group, i) => `回复${i + 1}: ${group.map((g) => g.type === "text" ? g.text : `[${g.type}]`).join(", ")}`
|
|
813
|
+
).join("\n");
|
|
814
|
+
if (config.Find_Return_Preset === "1") results.push(`关键词:${key}
|
|
815
|
+
${content}`);
|
|
816
|
+
else if (config.Find_Return_Preset === "2") results.push(`位置:${channelId}`);
|
|
817
|
+
else results.push(`位置:${channelId}
|
|
818
|
+
关键词:${key}
|
|
819
|
+
${content}`);
|
|
820
|
+
if (config.Return_Limit === "2") break;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
if (!results.length) return `未找到 "${keyword}"`;
|
|
825
|
+
return results.join("\n\n");
|
|
826
|
+
});
|
|
827
|
+
ctx.command(`${pluginName}/查看关键词列表`, "查看关键词列表", { authority: config.commandAuthority }).action(async ({ session }) => {
|
|
828
|
+
if (!hasPermission(session, "查看关键词列表")) return "权限不足";
|
|
829
|
+
const filePath = path.join(root, `${session.platform}_${session.channelId}.json`);
|
|
830
|
+
const globalData = fs.existsSync(globalFile) ? JSON.parse(fs.readFileSync(globalFile, "utf-8")) : {};
|
|
831
|
+
const localData = fs.existsSync(filePath) ? JSON.parse(fs.readFileSync(filePath, "utf-8")) : {};
|
|
832
|
+
const keys = [.../* @__PURE__ */ new Set([...Object.keys(globalData), ...Object.keys(localData)])];
|
|
833
|
+
if (!keys.length) return "暂无关键词";
|
|
834
|
+
return `关键词列表(共${keys.length}个):
|
|
835
|
+
` + keys.join("\n");
|
|
836
|
+
});
|
|
837
|
+
ctx.command(`${pluginName}/测试授权 <...targets>`, "将群/私聊加入测试名单", { authority: config.commandAuthority }).action(async ({ session }, ...targets) => {
|
|
838
|
+
if (!targets.length) return "请提供至少一个目标ID";
|
|
839
|
+
const channels = await getTestChannels(session);
|
|
840
|
+
const set = new Set(channels);
|
|
841
|
+
const added = [];
|
|
842
|
+
for (const t of targets) {
|
|
843
|
+
const cid = t.startsWith("private:") ? t : t;
|
|
844
|
+
if (!set.has(cid)) {
|
|
845
|
+
set.add(cid);
|
|
846
|
+
added.push(t);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
if (added.length) await setTestChannels(session, Array.from(set));
|
|
850
|
+
return added.length ? `已加入测试名单:${added.join("、")}` : "指定目标已在测试名单中";
|
|
851
|
+
});
|
|
852
|
+
ctx.command(`${pluginName}/取消测试授权 <...targets>`, "将群/私聊移出测试名单", { authority: config.commandAuthority }).action(async ({ session }, ...targets) => {
|
|
853
|
+
if (!targets.length) return "请提供至少一个目标ID";
|
|
854
|
+
const channels = await getTestChannels(session);
|
|
855
|
+
const set = new Set(channels);
|
|
856
|
+
const removed = [];
|
|
857
|
+
for (const t of targets) {
|
|
858
|
+
const cid = t.startsWith("private:") ? t : t;
|
|
859
|
+
if (set.has(cid)) {
|
|
860
|
+
set.delete(cid);
|
|
861
|
+
removed.push(t);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
if (removed.length) await setTestChannels(session, Array.from(set));
|
|
865
|
+
return removed.length ? `已移出测试名单:${removed.join("、")}` : "指定目标不在测试名单中";
|
|
866
|
+
});
|
|
867
|
+
ctx.command(`${pluginName}/测试同步`, "全局问答复制到测试环境", { authority: config.commandAuthority }).action(async ({ session }) => {
|
|
868
|
+
if (!fs.existsSync(globalFile)) return "全局问答文件不存在";
|
|
869
|
+
fs.copyFileSync(globalFile, testFile);
|
|
870
|
+
const data = JSON.parse(fs.readFileSync(testFile, "utf-8"));
|
|
871
|
+
return `测试同步完成,共 ${Object.keys(data).length} 条问答已复制`;
|
|
872
|
+
});
|
|
873
|
+
ctx.command(`${pluginName}/测试发布`, "测试问答发布到全局", { authority: config.commandAuthority }).alias("测试转正").alias("全局发布").action(async ({ session }) => {
|
|
874
|
+
if (!fs.existsSync(testFile)) return "测试问答文件不存在";
|
|
875
|
+
const backupFile = path.join(root, "global.json.pub.bak");
|
|
876
|
+
if (fs.existsSync(globalFile)) fs.copyFileSync(globalFile, backupFile);
|
|
877
|
+
fs.copyFileSync(testFile, globalFile);
|
|
878
|
+
const data = JSON.parse(fs.readFileSync(globalFile, "utf-8"));
|
|
879
|
+
return `测试发布完成,共 ${Object.keys(data).length} 条问答已发布,原全局已备份`;
|
|
880
|
+
});
|
|
881
|
+
ctx.command(`${pluginName}/测试添加 <keyword>`, "添加测试关键词", { authority: config.commandAuthority }).option("regex", "-x 使用正则匹配").option("forward", "-f <type> 指定回复方式(1-4)").action(async ({ session, options }, keyword) => {
|
|
882
|
+
if (!hasPermission(session, "测试添加")) return "权限不足";
|
|
883
|
+
return await addKeywordReply(session, testFile, keyword, { ...options, global: false });
|
|
884
|
+
});
|
|
885
|
+
ctx.command(`${pluginName}/测试删除 <keyword>`, "删除测试关键词", { authority: config.commandAuthority }).option("question", "-q <index> 指定删除序号").action(async ({ session, options }, keyword) => {
|
|
886
|
+
if (!hasPermission(session, "测试删除")) return "权限不足";
|
|
887
|
+
return await deleteKeywordReply(session, testFile, keyword, options.question);
|
|
888
|
+
});
|
|
889
|
+
ctx.command(`${pluginName}/测试修改 <keyword>`, "修改测试关键词", { authority: config.commandAuthority }).option("question", "-q <index> 指定回复序号").action(async ({ session, options }, keyword) => {
|
|
890
|
+
if (!hasPermission(session, "测试修改")) return "权限不足";
|
|
891
|
+
if (!fs.existsSync(testFile)) return "测试文件不存在";
|
|
892
|
+
return "测试修改功能待完善";
|
|
893
|
+
});
|
|
894
|
+
ctx.middleware(async (session, next) => {
|
|
895
|
+
if (!session.content) return next();
|
|
896
|
+
let content = session.content;
|
|
897
|
+
if (config.Treat_all_as_lowercase) content = content.toLowerCase();
|
|
898
|
+
const testChannels = await getTestChannels(session);
|
|
899
|
+
const isTest = testChannels.includes(session.channelId) || testChannels.includes(`private:${session.userId}`);
|
|
900
|
+
const files = [];
|
|
901
|
+
if (isTest) {
|
|
902
|
+
files.push(testFile);
|
|
903
|
+
} else {
|
|
904
|
+
if (config.Search_Range === "3" || config.Search_Range === "2") files.push(globalFile);
|
|
905
|
+
if (config.Search_Range === "1" || config.Search_Range === "3") {
|
|
906
|
+
files.push(path.join(root, `${session.platform}_${session.channelId}.json`));
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
for (const file of files) {
|
|
910
|
+
if (!fs.existsSync(file)) continue;
|
|
911
|
+
const data = JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
912
|
+
const keys = Object.keys(data).sort((a, b) => {
|
|
913
|
+
const lenA = a.startsWith("regex:") ? a.slice(6).length : a.length;
|
|
914
|
+
const lenB = b.startsWith("regex:") ? b.slice(6).length : b.length;
|
|
915
|
+
return lenB - lenA;
|
|
916
|
+
});
|
|
917
|
+
for (const key of keys) {
|
|
918
|
+
let isMatch = false;
|
|
919
|
+
let replies = data[key];
|
|
920
|
+
if (key.startsWith("regex:")) {
|
|
921
|
+
const pattern = key.slice(6);
|
|
922
|
+
isMatch = new RegExp(pattern).test(content);
|
|
923
|
+
} else {
|
|
924
|
+
isMatch = content === key;
|
|
925
|
+
}
|
|
926
|
+
if (isMatch) {
|
|
927
|
+
const limitKey = config.Type_of_restriction === "2" ? `${key}:${session.channelId}` : key;
|
|
928
|
+
const now = Date.now();
|
|
929
|
+
if (config.Frequency_limitation > 0 && lastTriggerTimes[limitKey] && now - lastTriggerTimes[limitKey] < config.Frequency_limitation * 1e3) {
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
lastTriggerTimes[limitKey] = now;
|
|
933
|
+
const group = replies[Math.floor(Math.random() * replies.length)];
|
|
934
|
+
const replyway = group[0]?.replyway || config.MultisegmentAdditionRecoveryEffect;
|
|
935
|
+
if (replyway === "1") {
|
|
936
|
+
for (const item of group) {
|
|
937
|
+
await session.send(await formatReply(item, session));
|
|
938
|
+
}
|
|
939
|
+
} else if (replyway === "2") {
|
|
940
|
+
let combined = "";
|
|
941
|
+
for (const item of group) combined += await formatReply(item, session);
|
|
942
|
+
await session.send(combined);
|
|
943
|
+
} else if (replyway === "3" || replyway === "4") {
|
|
944
|
+
const figure = h("figure");
|
|
945
|
+
for (const item of group) {
|
|
946
|
+
const formatted = await formatReply(item, session);
|
|
947
|
+
figure.children.push(replyway === "4" ? h("message", {}, formatted) : formatted);
|
|
948
|
+
}
|
|
949
|
+
await session.send(figure);
|
|
950
|
+
}
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
return next();
|
|
956
|
+
});
|
|
957
|
+
ctx.on("guild-member", async (session) => {
|
|
958
|
+
if (!config.autoLeaveOnMute) return;
|
|
959
|
+
if (session.subtype !== "ban") return;
|
|
960
|
+
if (session.selfId !== session.userId) return;
|
|
961
|
+
const [record] = await ctx.database.get("qxgl_satori_auth", {
|
|
962
|
+
platform: session.platform,
|
|
963
|
+
channelId: session.guildId
|
|
964
|
+
});
|
|
965
|
+
if (record && session.bot?.leaveGroup) {
|
|
966
|
+
await session.bot.leaveGroup(session.guildId);
|
|
967
|
+
}
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
__name(apply, "apply");
|
|
971
|
+
exports.apply = apply;
|
|
972
|
+
exports.Config = Config;
|
|
973
|
+
exports.name = pluginName;
|
|
974
|
+
exports.usage = usage;
|
|
975
|
+
exports.inject = inject;
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "koishi-plugin-qxgl-satori",
|
|
3
|
+
"description": "开发中",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"typings": "lib/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"lib",
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"chatbot",
|
|
14
|
+
"koishi",
|
|
15
|
+
"plugin"
|
|
16
|
+
],
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"koishi": "4.18.10"
|
|
19
|
+
}
|
|
20
|
+
}
|