chatluna-plugin-onebot-toolbox 0.0.0-stage → 0.0.2
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/LICENSE +21 -0
- package/README.md +50 -2
- package/lib/config.d.ts +40 -0
- package/lib/config.d.ts.map +1 -0
- package/lib/index.d.ts +8 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +809 -0
- package/lib/index.js.map +1 -0
- package/lib/logger.d.ts +9 -0
- package/lib/logger.d.ts.map +1 -0
- package/lib/onebot.d.ts +35 -0
- package/lib/onebot.d.ts.map +1 -0
- package/lib/requests.d.ts +21 -0
- package/lib/requests.d.ts.map +1 -0
- package/lib/tools.d.ts +33 -0
- package/lib/tools.d.ts.map +1 -0
- package/lib/variables.d.ts +12 -0
- package/lib/variables.d.ts.map +1 -0
- package/package.json +78 -3
package/lib/index.js
ADDED
|
@@ -0,0 +1,809 @@
|
|
|
1
|
+
import { Schema } from "@chatluna/schema";
|
|
2
|
+
import { fileRefTableOf } from "@chatluna/message";
|
|
3
|
+
// external/onebot-toolbox/src/logger.ts
|
|
4
|
+
function createLogger(ctx, debugLogging) {
|
|
5
|
+
const logger = ctx.logger;
|
|
6
|
+
return {
|
|
7
|
+
debug: (format, ...args) => debugLogging ? logger.info(format, ...args) : logger.debug(format, ...args),
|
|
8
|
+
info: (format, ...args) => logger.info(format, ...args),
|
|
9
|
+
warn: (format, ...args) => logger.warn(format, ...args)
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
// external/onebot-toolbox/src/config.ts
|
|
13
|
+
/** 原生工具的名称与描述固定,全部注册;启用范围交给 v2 的工具管理。 */
|
|
14
|
+
const NATIVE_TOOLS = {
|
|
15
|
+
setSelfProfile: {
|
|
16
|
+
toolName: "set_self_profile",
|
|
17
|
+
description: "修改自己的 QQ 资料,可设置昵称、签名和性别。"
|
|
18
|
+
},
|
|
19
|
+
setQQAvatar: {
|
|
20
|
+
toolName: "set_qq_avatar",
|
|
21
|
+
description: "修改机器人 QQ 头像。imageUrl 填会话里图片的 ref(`<img ref=\"i…\"/>` 的 ref 属性)、HTTP/HTTPS 链接或 OneBot 端可读的本地路径;会话图片与 HTTP/HTTPS 图片会先由插件读取并转为 OneBot base64 文件,避免临时 URL 过期或 OneBot 端无法访问。"
|
|
22
|
+
},
|
|
23
|
+
setGroupCard: {
|
|
24
|
+
toolName: "set_group_card",
|
|
25
|
+
description: "修改群成员的群昵称。"
|
|
26
|
+
},
|
|
27
|
+
searchGroupMember: {
|
|
28
|
+
toolName: "search_group_member",
|
|
29
|
+
description: "根据 qqid 精确查询群成员的群昵称与 QQ 昵称,或根据群昵称/QQ 昵称搜索 qqid。默认查询当前群聊,可通过 groupId 查询指定群。"
|
|
30
|
+
},
|
|
31
|
+
leaveGroup: {
|
|
32
|
+
toolName: "set_group_leave",
|
|
33
|
+
description: "让机器人退出当前群聊。"
|
|
34
|
+
},
|
|
35
|
+
handleRequest: {
|
|
36
|
+
toolName: "handle_request",
|
|
37
|
+
description: "根据 OneBot 申请 flag 同意或拒绝好友申请、加群申请或群邀请。"
|
|
38
|
+
},
|
|
39
|
+
setGroupKick: {
|
|
40
|
+
toolName: "set_group_kick",
|
|
41
|
+
description: "将指定成员踢出群聊。"
|
|
42
|
+
},
|
|
43
|
+
setGroupSpecialTitle: {
|
|
44
|
+
toolName: "set_group_special_title",
|
|
45
|
+
description: "修改或清除群成员的 QQ 群专属头衔。"
|
|
46
|
+
},
|
|
47
|
+
deleteMessage: {
|
|
48
|
+
toolName: "delete_msg",
|
|
49
|
+
description: "根据消息 ID 撤回指定消息。"
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const NATIVE_TOOL_KEYS = Object.keys(NATIVE_TOOLS);
|
|
53
|
+
const DEFAULT_MEMBER_INFO_ITEMS = [
|
|
54
|
+
"nickname",
|
|
55
|
+
"userId",
|
|
56
|
+
"role",
|
|
57
|
+
"level",
|
|
58
|
+
"title",
|
|
59
|
+
"gender",
|
|
60
|
+
"age",
|
|
61
|
+
"area",
|
|
62
|
+
"joinTime",
|
|
63
|
+
"lastSentTime"
|
|
64
|
+
];
|
|
65
|
+
const DEFAULT_GROUP_INFO_ITEMS = [
|
|
66
|
+
"groupName",
|
|
67
|
+
"groupId",
|
|
68
|
+
"memberCount",
|
|
69
|
+
"ownerList",
|
|
70
|
+
"adminList"
|
|
71
|
+
];
|
|
72
|
+
/** 变量名须满足 v2 模板的变量命名规则(`@chatluna/prompt` 的 `variable`),留空表示不注册。 */
|
|
73
|
+
const VARIABLE_NAME = /^([a-z_][a-z0-9_]*)?$/i;
|
|
74
|
+
const memberItems = (subject) => Schema.array(Schema.union([
|
|
75
|
+
Schema.const("userId").description(`${subject} ID`),
|
|
76
|
+
Schema.const("nickname").description("群昵称"),
|
|
77
|
+
Schema.const("role").description("群内身份"),
|
|
78
|
+
Schema.const("level").description("群等级"),
|
|
79
|
+
Schema.const("title").description("群头衔"),
|
|
80
|
+
Schema.const("gender").description("性别"),
|
|
81
|
+
Schema.const("age").description("年龄"),
|
|
82
|
+
Schema.const("area").description("地区"),
|
|
83
|
+
Schema.const("joinTime").description("入群时间"),
|
|
84
|
+
Schema.const("lastSentTime").description("最后发言时间")
|
|
85
|
+
])).role("checkbox").default([...DEFAULT_MEMBER_INFO_ITEMS]);
|
|
86
|
+
const variableName = (name) => Schema.string().default(name).pattern(VARIABLE_NAME).extra("label", "变量名称").description("在预设中以`{变量名称}`引用,留空时不注册");
|
|
87
|
+
const basicFields = {
|
|
88
|
+
oneBotProtocol: Schema.union([
|
|
89
|
+
Schema.const("napcat").description("NapCat"),
|
|
90
|
+
Schema.const("llbot").description("LLBot"),
|
|
91
|
+
Schema.const("snowluma").description("SnowLuma")
|
|
92
|
+
]).default("napcat").role("radio").extra("label", "OneBot 协议").description("按所选实现调整请求参数:LLBot 不支持设置性别,群禁言列表的群号类型也不同;SnowLuma 同意好友申请后另行设置备注"),
|
|
93
|
+
requestNotice: Schema.boolean().default(true).extra("label", "申请通知").description("收到好友申请、加群申请或群邀请时唤醒对应会话,把申请 flag 告诉模型,供`handle_request`工具处理。好友申请与群邀请发往申请人的私聊,加群申请发往所申请的群;该工具在工具管理中禁用时不唤醒")
|
|
94
|
+
};
|
|
95
|
+
const variableFields = {
|
|
96
|
+
userInfo: Schema.object({
|
|
97
|
+
variableName: variableName("userInfo"),
|
|
98
|
+
items: memberItems("用户").extra("label", "信息项").description("选择用户信息变量中显示的详细信息项")
|
|
99
|
+
}).description("用户信息变量").collapse(),
|
|
100
|
+
botInfo: Schema.object({
|
|
101
|
+
variableName: variableName("botInfo"),
|
|
102
|
+
items: memberItems("机器人").extra("label", "信息项").description("选择机器人信息变量中显示的详细信息项")
|
|
103
|
+
}).description("机器人信息变量").collapse(),
|
|
104
|
+
groupInfo: Schema.object({
|
|
105
|
+
variableName: variableName("groupInfo"),
|
|
106
|
+
items: Schema.array(Schema.union([
|
|
107
|
+
Schema.const("groupName").description("群名称"),
|
|
108
|
+
Schema.const("groupId").description("群号"),
|
|
109
|
+
Schema.const("memberCount").description("成员数量"),
|
|
110
|
+
Schema.const("createTime").description("创建时间(仅部分实现支持)"),
|
|
111
|
+
Schema.const("ownerList").description("群主名单"),
|
|
112
|
+
Schema.const("adminList").description("管理员名单")
|
|
113
|
+
])).role("checkbox").default([...DEFAULT_GROUP_INFO_ITEMS]).extra("label", "信息项").description("选择群信息变量中显示的群信息项")
|
|
114
|
+
}).description("群信息变量").collapse(),
|
|
115
|
+
groupShutList: Schema.object({ variableName: variableName("groupShutList") }).description("群禁言列表变量").collapse()
|
|
116
|
+
};
|
|
117
|
+
const otherFields = { debugLogging: Schema.boolean().default(false).extra("label", "输出调试日志").description("以 info 级别输出工具调用与变量解析的调试信息") };
|
|
118
|
+
/**
|
|
119
|
+
* 插件配置,直接在 v2 的插件设置页编辑。顶层是分组的 intersect,SchemaForm 把每组的 description 渲染成小节标题;
|
|
120
|
+
* 每个字段都写 `extra('label')`,缺 label 时 SchemaForm 会把配置键当标题显示。
|
|
121
|
+
*/
|
|
122
|
+
const Config = Schema.intersect([
|
|
123
|
+
Schema.object(basicFields).description("基础设置"),
|
|
124
|
+
Schema.object(variableFields).description("变量"),
|
|
125
|
+
Schema.object(otherFields).description("其他设置")
|
|
126
|
+
]);
|
|
127
|
+
// external/onebot-toolbox/src/requests.ts
|
|
128
|
+
/**
|
|
129
|
+
* 申请发往的会话:好友申请与群邀请发往申请人的私聊,加群申请发往所申请的群。
|
|
130
|
+
* 用路由身份而不是事件 Session 唤醒:申请事件不是消息,申请人与机器人之间往往还没有会话,
|
|
131
|
+
* `chat.wake` 会按身份新开一个。
|
|
132
|
+
*/
|
|
133
|
+
function requestTarget(session, kind) {
|
|
134
|
+
if (!session.userId) return void 0;
|
|
135
|
+
if (kind === "add") {
|
|
136
|
+
if (!session.guildId) return void 0;
|
|
137
|
+
return {
|
|
138
|
+
platform: session.platform,
|
|
139
|
+
selfId: session.selfId,
|
|
140
|
+
isDirect: false,
|
|
141
|
+
guildId: session.guildId,
|
|
142
|
+
channelId: session.guildId,
|
|
143
|
+
userId: session.userId
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
platform: session.platform,
|
|
148
|
+
selfId: session.selfId,
|
|
149
|
+
isDirect: true,
|
|
150
|
+
channelId: `private:${session.userId}`,
|
|
151
|
+
userId: session.userId
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/** 唤醒原因即模型看到的通知,写明 flag 与调用方式,模型据此调用处理申请工具。 */
|
|
155
|
+
function requestReason(session, kind, toolName) {
|
|
156
|
+
const comment = session.content?.trim() ? `, message: ${JSON.stringify(session.content.trim())}` : "";
|
|
157
|
+
const flag = `flag: ${JSON.stringify(session.messageId ?? "")}`;
|
|
158
|
+
if (kind === "friend") return `user ${session.userId} sent you a friend request (${flag}${comment}). Call ${toolName} with requestType "friend" and userId "${session.userId}" to approve or reject it`;
|
|
159
|
+
if (kind === "add") return `user ${session.userId} requested to join group ${session.guildId} (${flag}${comment}). Call ${toolName} with requestType "group" and subType "add" to approve or reject it`;
|
|
160
|
+
return `user ${session.userId} invited you to join group ${session.guildId} (${flag}${comment}). Call ${toolName} with requestType "group" and subType "invite" to approve or reject it`;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* v2 不把 OneBot 的申请事件交给模型,`handle_request` 拿不到 flag 就无从调用,
|
|
164
|
+
* 所以把申请转成一次唤醒;工具在 v2 工具管理里被禁用时跟着不唤醒。会话所在路由关闭或没选模型时 `wake` 会报错,只记调试日志。
|
|
165
|
+
*/
|
|
166
|
+
function registerRequestNotices(ctx, config, log) {
|
|
167
|
+
if (!config.requestNotice) return;
|
|
168
|
+
const toolName = NATIVE_TOOLS.handleRequest.toolName;
|
|
169
|
+
ctx.inject(["chat"], (inner) => {
|
|
170
|
+
const notify = (session, kind) => {
|
|
171
|
+
if (session.platform !== "onebot") return;
|
|
172
|
+
if (!session.messageId) return log.debug("%s request without flag ignored", kind);
|
|
173
|
+
if (!inner.tools.list().some((tool) => tool.name === toolName)) return log.debug("%s request ignored: %s is disabled", kind, toolName);
|
|
174
|
+
const target = requestTarget(session, kind);
|
|
175
|
+
if (!target) return;
|
|
176
|
+
inner.chat.wake(target, {
|
|
177
|
+
reason: requestReason(session, kind, toolName),
|
|
178
|
+
by: "onebot-toolbox"
|
|
179
|
+
}).then(() => log.debug("%s request %s delivered", kind, session.messageId)).catch((error) => log.debug("%s request %s not delivered: %s", kind, session.messageId, String(error)));
|
|
180
|
+
};
|
|
181
|
+
inner.on("friend-request", (session) => notify(session, "friend"));
|
|
182
|
+
inner.on("guild-member-request", (session) => notify(session, "add"));
|
|
183
|
+
inner.on("guild-request", (session) => notify(session, "invite"));
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
// external/onebot-toolbox/src/onebot.ts
|
|
187
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
188
|
+
/** 会话所在的 OneBot 机器人;不是 OneBot 会话或找不到机器人时为 undefined。 */
|
|
189
|
+
function oneBotOf(ctx, platform, selfId) {
|
|
190
|
+
if (platform !== "onebot" || selfId === void 0) return void 0;
|
|
191
|
+
return ctx.satori.bots.find((bot) => bot.platform === "onebot" && bot.selfId === selfId);
|
|
192
|
+
}
|
|
193
|
+
/** 调用 OneBot action 并返回 data,按响应包的 status 与 retcode 判定业务失败。 */
|
|
194
|
+
async function callOneBot(bot, action, params) {
|
|
195
|
+
const result = await bot.internal._request(action, params);
|
|
196
|
+
if (!isRecord(result) || typeof result.status !== "string" || typeof result.retcode !== "number") throw new Error("Invalid OneBot response envelope");
|
|
197
|
+
if (result.status === "ok" && result.retcode === 0) return result.data;
|
|
198
|
+
const reason = result.wording || result.message || result.status || "OneBot returned failed";
|
|
199
|
+
throw new Error(`${String(reason)} (retcode: ${result.retcode})`);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* 整数形式的 ID 转成数字,其余原样保留;OneBot 实现普遍要求数字形式的群号、QQ 号与消息 ID。
|
|
203
|
+
* SnowLuma 的 message_id 是 int32 哈希,常为负数,所以带负号的整数同样转换。
|
|
204
|
+
*/
|
|
205
|
+
function numericId(value) {
|
|
206
|
+
return /^-?\d+$/.test(value) ? Number(value) : value;
|
|
207
|
+
}
|
|
208
|
+
/** 取第一个非空值(去掉首尾空白后不为空字符串),保留原始类型。 */
|
|
209
|
+
function pickFirst(...values) {
|
|
210
|
+
for (const value of values) {
|
|
211
|
+
if (value === null || value === void 0) continue;
|
|
212
|
+
if (!String(value).trim()) continue;
|
|
213
|
+
return typeof value === "number" ? value : String(value);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function pickText(...values) {
|
|
217
|
+
return String(pickFirst(...values) ?? "").trim();
|
|
218
|
+
}
|
|
219
|
+
/** OneBot 使用秒,Satori 使用毫秒,统一成毫秒;空值与 0 视为没有。 */
|
|
220
|
+
function normalizeTimestamp(raw) {
|
|
221
|
+
if (raw === null || raw === void 0 || raw === "") return void 0;
|
|
222
|
+
const numeric = Number(raw);
|
|
223
|
+
if (!Number.isFinite(numeric) || numeric === 0) return void 0;
|
|
224
|
+
return numeric < 1e11 ? numeric * 1e3 : numeric;
|
|
225
|
+
}
|
|
226
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
227
|
+
function formatDate(value) {
|
|
228
|
+
if (value === void 0) return "";
|
|
229
|
+
const date = new Date(value);
|
|
230
|
+
if (Number.isNaN(date.valueOf())) return "";
|
|
231
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
232
|
+
}
|
|
233
|
+
function formatDateTime(value) {
|
|
234
|
+
const day = formatDate(value);
|
|
235
|
+
if (!day || value === void 0) return day;
|
|
236
|
+
const date = new Date(value);
|
|
237
|
+
return `${day} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
238
|
+
}
|
|
239
|
+
function asMembers(value) {
|
|
240
|
+
return Array.isArray(value) ? value.filter(isRecord) : [];
|
|
241
|
+
}
|
|
242
|
+
function asRecord(value) {
|
|
243
|
+
return isRecord(value) ? value : void 0;
|
|
244
|
+
}
|
|
245
|
+
// external/onebot-toolbox/src/tools.ts
|
|
246
|
+
const MISSING_GROUP = "Missing groupId. Provide groupId explicitly or run inside a group session.";
|
|
247
|
+
const MAX_MEMBER_RESULTS = 10;
|
|
248
|
+
const groupIdArg = () => Schema.string().required(false).description("Target group ID. Defaults to current session group.");
|
|
249
|
+
function groupOf(scope, groupId) {
|
|
250
|
+
const resolved = groupId?.trim() || scope.guildId?.trim();
|
|
251
|
+
if (!resolved) throw new Error(MISSING_GROUP);
|
|
252
|
+
return resolved;
|
|
253
|
+
}
|
|
254
|
+
function required(value, name) {
|
|
255
|
+
const trimmed = value.trim();
|
|
256
|
+
if (!trimmed) throw new Error(`${name} is required.`);
|
|
257
|
+
return trimmed;
|
|
258
|
+
}
|
|
259
|
+
const GENDERS = {
|
|
260
|
+
unknown: "0",
|
|
261
|
+
male: "1",
|
|
262
|
+
female: "2"
|
|
263
|
+
};
|
|
264
|
+
const STORAGE_PREFIX = "storage://";
|
|
265
|
+
/** v2 给模型的文件占位符 `<img ref="i…"/>`:`i` 加内容哈希的最短唯一前缀(至少 6 位)。 */
|
|
266
|
+
const FILE_REF = /^i[0-9a-f]{6,}$/;
|
|
267
|
+
const base64File = (buffer) => `base64://${buffer.toString("base64")}`;
|
|
268
|
+
/** HTTP/HTTPS 图片先读成 base64:OneBot 端常访问不到临时 URL,或 URL 在提交前就已过期。 */
|
|
269
|
+
async function remoteFile(url) {
|
|
270
|
+
try {
|
|
271
|
+
const response = await fetch(url);
|
|
272
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
273
|
+
return base64File(Buffer.from(await response.arrayBuffer()));
|
|
274
|
+
} catch (error) {
|
|
275
|
+
throw new Error(`无法读取头像图片:${error instanceof Error ? error.message : String(error)}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/** 入站图片由 v2 下载进 `chatluna_storage`,24 小时后过期;没装存储插件时会话里只有 url 或 data 块。 */
|
|
279
|
+
async function storageFile(ctx, reference) {
|
|
280
|
+
const storage = ctx.get("chatluna_storage");
|
|
281
|
+
if (!storage) throw new Error(`Cannot read ${reference}: the chatluna_storage service is not loaded.`);
|
|
282
|
+
const file = await storage.getTempFile(reference.slice(10));
|
|
283
|
+
if (!file) throw new Error(`${reference} is not in storage (it may have expired).`);
|
|
284
|
+
return base64File(await file.data);
|
|
285
|
+
}
|
|
286
|
+
function dataFile(data) {
|
|
287
|
+
if (!data.startsWith("data:")) return `base64://${data}`;
|
|
288
|
+
const comma = data.indexOf(",");
|
|
289
|
+
const body = data.slice(comma + 1);
|
|
290
|
+
return data.slice(0, comma).endsWith(";base64") ? `base64://${body}` : base64File(Buffer.from(decodeURIComponent(body)));
|
|
291
|
+
}
|
|
292
|
+
function messagesOf(agent) {
|
|
293
|
+
const session = agent && "session" in agent ? agent.session : void 0;
|
|
294
|
+
if (session === null || typeof session !== "object" || !("messages" in session) || typeof session.messages !== "function") return void 0;
|
|
295
|
+
return session.messages();
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* v2 不把聊天图片的地址交给模型,只给 `<img ref="i…"/>`,ref 要在发起调用的 agent 的会话日志里按内容哈希解析
|
|
299
|
+
* (与 v2 自带的 `look` 工具、出站 `<img ref>` 同一张表)。
|
|
300
|
+
*/
|
|
301
|
+
async function sessionImageFile(scope, ref) {
|
|
302
|
+
const messages = messagesOf(scope.agent);
|
|
303
|
+
if (!messages) throw new Error(`Image ref ${ref} can only be resolved inside a conversation.`);
|
|
304
|
+
const block = fileRefTableOf(messages).resolve(ref);
|
|
305
|
+
if (!block) throw new Error(`Image ref ${ref} is not in this conversation.`);
|
|
306
|
+
if (!block.mediaType.startsWith("image/")) throw new Error(`ref ${ref} is ${block.mediaType}, not an image.`);
|
|
307
|
+
const { data } = block;
|
|
308
|
+
if (data.type === "data") return dataFile(data.data);
|
|
309
|
+
if (data.type === "url") return remoteFile(data.url);
|
|
310
|
+
if (data.type === "reference" && data.reference.startsWith(STORAGE_PREFIX)) return storageFile(scope.ctx, data.reference);
|
|
311
|
+
throw new Error(`Image ref ${ref} cannot be read.`);
|
|
312
|
+
}
|
|
313
|
+
/** 头像来源:会话图片 ref、`storage://`、HTTP/HTTPS 图片都先读成 base64 再提交,其余(OneBot 端可读的本地路径)原样提交。 */
|
|
314
|
+
async function avatarFile(scope, image) {
|
|
315
|
+
if (FILE_REF.test(image)) return sessionImageFile(scope, image);
|
|
316
|
+
if (image.startsWith(STORAGE_PREFIX)) return storageFile(scope.ctx, image);
|
|
317
|
+
if (/^https?:\/\//i.test(image)) return remoteFile(image);
|
|
318
|
+
return image;
|
|
319
|
+
}
|
|
320
|
+
function formatMember(index, member) {
|
|
321
|
+
return `${index}. qqid:${pickText(member.user_id) || "未知"},群昵称:${pickText(member.card) || "无"},QQ昵称:${pickText(member.nickname) || "无"}`;
|
|
322
|
+
}
|
|
323
|
+
const NATIVE_TOOL_IMPLEMENTATIONS = {
|
|
324
|
+
setSelfProfile: {
|
|
325
|
+
args: Schema.args({
|
|
326
|
+
nickname: Schema.string().min(1).description("The new nickname for the bot."),
|
|
327
|
+
signature: Schema.string().required(false).description("Optional: the new personal signature."),
|
|
328
|
+
gender: Schema.union([
|
|
329
|
+
"unknown",
|
|
330
|
+
"male",
|
|
331
|
+
"female"
|
|
332
|
+
]).required(false).description("Optional: the new gender.")
|
|
333
|
+
}),
|
|
334
|
+
effect: "write",
|
|
335
|
+
async run(scope, args) {
|
|
336
|
+
await callOneBot(scope.bot, "set_qq_profile", {
|
|
337
|
+
nickname: args.nickname,
|
|
338
|
+
...args.signature ? { personal_note: args.signature } : {},
|
|
339
|
+
...args.gender && scope.protocol !== "llbot" ? { sex: GENDERS[args.gender] } : {}
|
|
340
|
+
});
|
|
341
|
+
return "机器人资料已更新。";
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
setQQAvatar: {
|
|
345
|
+
args: Schema.args({ imageUrl: Schema.string().min(1).description("The image: the ref attribute of an `<img ref=\"i…\"/>` in this conversation, an HTTP/HTTPS URL, or a local file path readable by the OneBot side.") }),
|
|
346
|
+
effect: "write",
|
|
347
|
+
async run(scope, args) {
|
|
348
|
+
const file = await avatarFile(scope, required(args.imageUrl, "imageUrl"));
|
|
349
|
+
await callOneBot(scope.bot, "set_qq_avatar", { file });
|
|
350
|
+
return "QQ 头像已更新。";
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
setGroupCard: {
|
|
354
|
+
args: Schema.args({
|
|
355
|
+
groupId: groupIdArg(),
|
|
356
|
+
userId: Schema.string().min(1).description("Target member user ID."),
|
|
357
|
+
card: Schema.string().min(1).description("New group card for the member.")
|
|
358
|
+
}),
|
|
359
|
+
effect: "write",
|
|
360
|
+
async run(scope, args) {
|
|
361
|
+
const groupId = groupOf(scope, args.groupId);
|
|
362
|
+
const userId = required(args.userId, "userId");
|
|
363
|
+
const card = required(args.card, "card");
|
|
364
|
+
await callOneBot(scope.bot, "set_group_card", {
|
|
365
|
+
group_id: groupId,
|
|
366
|
+
user_id: userId,
|
|
367
|
+
card
|
|
368
|
+
});
|
|
369
|
+
return `群昵称已更新:${userId} -> ${card}`;
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
searchGroupMember: {
|
|
373
|
+
args: Schema.args({
|
|
374
|
+
groupId: groupIdArg(),
|
|
375
|
+
query: Schema.string().min(1).description("qqid, group card, or QQ nickname to search."),
|
|
376
|
+
mode: Schema.union([
|
|
377
|
+
"auto",
|
|
378
|
+
"byName",
|
|
379
|
+
"byQQ"
|
|
380
|
+
]).required(false).description("Search mode. Defaults to auto.")
|
|
381
|
+
}),
|
|
382
|
+
effect: "read",
|
|
383
|
+
async run(scope, args) {
|
|
384
|
+
const query = required(args.query, "query");
|
|
385
|
+
const mode = args.mode ?? "auto";
|
|
386
|
+
const groupId = groupOf(scope, args.groupId);
|
|
387
|
+
const members = asMembers(await callOneBot(scope.bot, "get_group_member_list", { group_id: numericId(groupId) }));
|
|
388
|
+
const byQQ = mode === "byQQ" || mode === "auto" && /^\d+$/.test(query);
|
|
389
|
+
const keyword = query.toLowerCase();
|
|
390
|
+
const matches = members.filter((member) => byQQ ? pickText(member.user_id) === query : [pickText(member.card), pickText(member.nickname)].some((name) => name && name.toLowerCase().includes(keyword)));
|
|
391
|
+
if (!matches.length) return "未找到匹配的群成员。";
|
|
392
|
+
return [matches.length > MAX_MEMBER_RESULTS ? `仅显示前 ${MAX_MEMBER_RESULTS} 条,共 ${matches.length} 条匹配:` : `找到 ${matches.length} 个匹配成员:`, ...matches.slice(0, MAX_MEMBER_RESULTS).map((member, index) => formatMember(index + 1, member))].join("\n");
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
leaveGroup: {
|
|
396
|
+
args: Schema.args({ groupId: groupIdArg() }),
|
|
397
|
+
effect: "write",
|
|
398
|
+
async run(scope, args) {
|
|
399
|
+
const groupId = groupOf(scope, args.groupId);
|
|
400
|
+
await callOneBot(scope.bot, "set_group_leave", { group_id: groupId });
|
|
401
|
+
return `已退出群 ${groupId}。`;
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
handleRequest: {
|
|
405
|
+
args: Schema.args({
|
|
406
|
+
requestType: Schema.union(["friend", "group"]).description("Request type: friend or group."),
|
|
407
|
+
flag: Schema.string().min(1).description("OneBot request flag from the event."),
|
|
408
|
+
approve: Schema.boolean().description("true to approve the request, false to reject it."),
|
|
409
|
+
subType: Schema.union(["add", "invite"]).required(false).description("Required for group requests: add or invite."),
|
|
410
|
+
remark: Schema.string().required(false).description("Optional friend remark when approving a friend request."),
|
|
411
|
+
userId: Schema.string().required(false).description("Requester user ID of a friend request; needed to apply the remark on SnowLuma."),
|
|
412
|
+
reason: Schema.string().required(false).description("Optional rejection reason for a group request.")
|
|
413
|
+
}),
|
|
414
|
+
effect: "write",
|
|
415
|
+
async run(scope, args) {
|
|
416
|
+
const flag = required(args.flag, "request flag");
|
|
417
|
+
const verb = args.approve ? "同意" : "拒绝";
|
|
418
|
+
if (args.requestType === "friend") {
|
|
419
|
+
const remark = args.remark?.trim() ?? "";
|
|
420
|
+
await callOneBot(scope.bot, "set_friend_add_request", {
|
|
421
|
+
flag,
|
|
422
|
+
approve: args.approve,
|
|
423
|
+
remark
|
|
424
|
+
});
|
|
425
|
+
const message = `已${verb}好友申请 ${flag}。`;
|
|
426
|
+
if (scope.protocol !== "snowluma" || !args.approve || !remark) return message;
|
|
427
|
+
const userId = args.userId?.trim();
|
|
428
|
+
if (!userId) return `${message}未提供申请人 userId,备注未设置。`;
|
|
429
|
+
await callOneBot(scope.bot, "set_friend_remark", {
|
|
430
|
+
user_id: numericId(userId),
|
|
431
|
+
remark
|
|
432
|
+
});
|
|
433
|
+
return `${message}备注已设置为 ${remark}。`;
|
|
434
|
+
}
|
|
435
|
+
if (!args.subType) throw new Error("subType is required for group requests; use add or invite.");
|
|
436
|
+
await callOneBot(scope.bot, "set_group_add_request", {
|
|
437
|
+
flag,
|
|
438
|
+
sub_type: args.subType,
|
|
439
|
+
approve: args.approve,
|
|
440
|
+
reason: args.reason?.trim() ?? ""
|
|
441
|
+
});
|
|
442
|
+
return `已${verb}${args.subType === "invite" ? "群邀请" : "加群申请"} ${flag}。`;
|
|
443
|
+
}
|
|
444
|
+
},
|
|
445
|
+
setGroupKick: {
|
|
446
|
+
args: Schema.args({
|
|
447
|
+
groupId: groupIdArg(),
|
|
448
|
+
userId: Schema.string().min(1).description("Target member user ID.")
|
|
449
|
+
}),
|
|
450
|
+
effect: "write",
|
|
451
|
+
async run(scope, args) {
|
|
452
|
+
const groupId = groupOf(scope, args.groupId);
|
|
453
|
+
const userId = required(args.userId, "userId");
|
|
454
|
+
await callOneBot(scope.bot, "set_group_kick", {
|
|
455
|
+
group_id: groupId,
|
|
456
|
+
user_id: userId
|
|
457
|
+
});
|
|
458
|
+
return `已将用户 ${userId} 移出群 ${groupId}。`;
|
|
459
|
+
}
|
|
460
|
+
},
|
|
461
|
+
setGroupSpecialTitle: {
|
|
462
|
+
args: Schema.args({
|
|
463
|
+
groupId: groupIdArg(),
|
|
464
|
+
userId: Schema.string().min(1).description("Target member user ID."),
|
|
465
|
+
title: Schema.string().description("New special title. Use an empty string to clear it.")
|
|
466
|
+
}),
|
|
467
|
+
effect: "write",
|
|
468
|
+
async run(scope, args) {
|
|
469
|
+
const groupId = groupOf(scope, args.groupId);
|
|
470
|
+
const userId = required(args.userId, "userId");
|
|
471
|
+
const title = args.title.trim();
|
|
472
|
+
await callOneBot(scope.bot, "set_group_special_title", {
|
|
473
|
+
group_id: groupId,
|
|
474
|
+
user_id: userId,
|
|
475
|
+
special_title: title
|
|
476
|
+
});
|
|
477
|
+
return title ? `群专属头衔已更新:${userId} -> ${title}` : `群专属头衔已清除:${userId}`;
|
|
478
|
+
}
|
|
479
|
+
},
|
|
480
|
+
deleteMessage: {
|
|
481
|
+
args: Schema.args({ message_id: Schema.string().min(1).description("Specific message ID to delete.") }),
|
|
482
|
+
effect: "write",
|
|
483
|
+
async run(scope, args) {
|
|
484
|
+
const messageId = required(args.message_id, "message_id");
|
|
485
|
+
await callOneBot(scope.bot, "delete_msg", { message_id: numericId(messageId) });
|
|
486
|
+
return `Message deleted by ID ${messageId}.`;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
/** 全部原生工具的定义,名称与描述统一取自 NATIVE_TOOLS。 */
|
|
491
|
+
function nativeToolDefinitions(ctx, config, log) {
|
|
492
|
+
return NATIVE_TOOL_KEYS.map((key) => {
|
|
493
|
+
const tool = NATIVE_TOOL_IMPLEMENTATIONS[key];
|
|
494
|
+
const { toolName: name, description } = NATIVE_TOOLS[key];
|
|
495
|
+
return {
|
|
496
|
+
name,
|
|
497
|
+
description,
|
|
498
|
+
args: tool.args,
|
|
499
|
+
traits: { effect: tool.effect },
|
|
500
|
+
async run(args, call) {
|
|
501
|
+
const location = call.location;
|
|
502
|
+
const bot = oneBotOf(ctx, location?.platform, location?.selfId);
|
|
503
|
+
if (!bot) throw new Error("This tool only supports OneBot platform.");
|
|
504
|
+
try {
|
|
505
|
+
const result = await tool.run({
|
|
506
|
+
ctx,
|
|
507
|
+
bot,
|
|
508
|
+
guildId: location?.isDirect ? void 0 : location?.guild,
|
|
509
|
+
protocol: config.oneBotProtocol,
|
|
510
|
+
agent: call.agent
|
|
511
|
+
}, args);
|
|
512
|
+
log.debug("%s: %s", name, result);
|
|
513
|
+
return result;
|
|
514
|
+
} catch (error) {
|
|
515
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
516
|
+
log.warn("%s failed: %s", name, message);
|
|
517
|
+
throw new Error(`${name} failed: ${message}`);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* 注册原生工具。工具对所有会话全局可见(v2 的工具管理页因此能列出并开关它们),
|
|
525
|
+
* 再给不在 OneBot 上的 agent 挂一层排除过滤。
|
|
526
|
+
*/
|
|
527
|
+
function registerNativeTools(ctx, config, log) {
|
|
528
|
+
const definitions = nativeToolDefinitions(ctx, config, log);
|
|
529
|
+
for (const definition of definitions) ctx.tools.define(definition);
|
|
530
|
+
const exclude = definitions.map((definition) => definition.name);
|
|
531
|
+
log.info("native tools registered: %s", exclude.join(", "));
|
|
532
|
+
ctx.inject(["agents"], (inner) => {
|
|
533
|
+
const scope = (agent) => {
|
|
534
|
+
if (agent.options.location?.platform !== "onebot") inner.tools.filter({ exclude }, agent);
|
|
535
|
+
};
|
|
536
|
+
inner.on("chatluna/agent-open", ({ agent }) => scope(agent));
|
|
537
|
+
for (const agent of inner.agents.list()) scope(agent);
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
// external/onebot-toolbox/src/variables.ts
|
|
541
|
+
const FALLBACK_MEMBER_ITEMS = ["nickname", "userId"];
|
|
542
|
+
const SHUT_LIST_TIMEOUT_MS = 200;
|
|
543
|
+
const SHUT_LIST_CACHE_TTL_MS = 15e3;
|
|
544
|
+
function translateGender(value) {
|
|
545
|
+
if (value === "male") return "男";
|
|
546
|
+
if (value === "female") return "女";
|
|
547
|
+
return "";
|
|
548
|
+
}
|
|
549
|
+
function normalizeRole(value) {
|
|
550
|
+
if (value === "owner") return "群主";
|
|
551
|
+
if (value === "admin") return "管理员";
|
|
552
|
+
return !value || value === "member" ? "群员" : value;
|
|
553
|
+
}
|
|
554
|
+
/** 会话所在的 Satori 机器人,任意平台;预览等没有真实机器人的环境里为 undefined。 */
|
|
555
|
+
function botOf(ctx, env) {
|
|
556
|
+
return ctx.satori.bots.find((bot) => bot.platform === env.platform && bot.selfId === env.selfId);
|
|
557
|
+
}
|
|
558
|
+
/** 查询群成员信息;私聊、找不到机器人或查询失败时为 undefined,变量照常按已知信息渲染。 */
|
|
559
|
+
async function fetchMember(ctx, env, userId) {
|
|
560
|
+
const bot = botOf(ctx, env);
|
|
561
|
+
if (!bot || !env.guildId) return void 0;
|
|
562
|
+
try {
|
|
563
|
+
if (bot.platform === "onebot") return asRecord(await callOneBot(bot, "get_group_member_info", {
|
|
564
|
+
group_id: Number(env.guildId),
|
|
565
|
+
user_id: Number(userId),
|
|
566
|
+
no_cache: false
|
|
567
|
+
}));
|
|
568
|
+
const member = await bot.getGuildMember(env.guildId, userId);
|
|
569
|
+
return {
|
|
570
|
+
user_id: member.user?.id,
|
|
571
|
+
card: pickText(member.nick, member.name),
|
|
572
|
+
nickname: member.user?.name,
|
|
573
|
+
role: member.roles?.[0]?.id,
|
|
574
|
+
title: member.title,
|
|
575
|
+
join_time: member.joinedAt
|
|
576
|
+
};
|
|
577
|
+
} catch {
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
/** 用户信息与机器人信息共用的渲染:按配置的信息项逐条输出,取不到的项省略。 */
|
|
582
|
+
function renderMember(member, items, id, fallbackName) {
|
|
583
|
+
const values = [];
|
|
584
|
+
for (const item of items) switch (item) {
|
|
585
|
+
case "nickname": {
|
|
586
|
+
const name = pickText(member?.card, member?.nickname, fallbackName, id);
|
|
587
|
+
if (name) values.push(`name:${name}`);
|
|
588
|
+
break;
|
|
589
|
+
}
|
|
590
|
+
case "userId":
|
|
591
|
+
if (id) values.push(`id:${id}`);
|
|
592
|
+
break;
|
|
593
|
+
case "role":
|
|
594
|
+
values.push(`群内身份:${normalizeRole(pickText(member?.role))}`);
|
|
595
|
+
break;
|
|
596
|
+
case "level": {
|
|
597
|
+
const level = pickFirst(member?.level);
|
|
598
|
+
if (level !== void 0) values.push(`群等级:${level}`);
|
|
599
|
+
break;
|
|
600
|
+
}
|
|
601
|
+
case "title": {
|
|
602
|
+
const title = pickFirst(member?.title);
|
|
603
|
+
if (title !== void 0) values.push(`头衔:${title}`);
|
|
604
|
+
break;
|
|
605
|
+
}
|
|
606
|
+
case "gender": {
|
|
607
|
+
const gender = translateGender(member?.sex);
|
|
608
|
+
if (gender) values.push(`性别:${gender}`);
|
|
609
|
+
break;
|
|
610
|
+
}
|
|
611
|
+
case "age": {
|
|
612
|
+
const age = Number(member?.age);
|
|
613
|
+
if (Number.isFinite(age) && age > 0) values.push(`年龄:${age}`);
|
|
614
|
+
break;
|
|
615
|
+
}
|
|
616
|
+
case "area": {
|
|
617
|
+
const area = pickFirst(member?.area);
|
|
618
|
+
if (area !== void 0) values.push(`地区:${area}`);
|
|
619
|
+
break;
|
|
620
|
+
}
|
|
621
|
+
case "joinTime": {
|
|
622
|
+
const joinTime = formatDate(normalizeTimestamp(member?.join_time));
|
|
623
|
+
if (joinTime) values.push(`入群:${joinTime}`);
|
|
624
|
+
break;
|
|
625
|
+
}
|
|
626
|
+
case "lastSentTime": {
|
|
627
|
+
const lastSentTime = formatDateTime(normalizeTimestamp(member?.last_sent_time));
|
|
628
|
+
if (lastSentTime) values.push(`活跃:${lastSentTime}`);
|
|
629
|
+
break;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return values;
|
|
633
|
+
}
|
|
634
|
+
const itemsOf = (items) => items.length ? items : FALLBACK_MEMBER_ITEMS;
|
|
635
|
+
function userInfoProvider(ctx, config) {
|
|
636
|
+
return async ({ env }) => {
|
|
637
|
+
const userId = (env.userId ?? env.message?.senderId)?.trim();
|
|
638
|
+
if (!userId) return "未知用户";
|
|
639
|
+
const values = renderMember(await fetchMember(ctx, env, userId), itemsOf(config.userInfo.items), userId, env.userName ?? env.message?.senderName);
|
|
640
|
+
return values.length ? values.join(", ") : `id:${userId}`;
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
function botInfoProvider(ctx, config) {
|
|
644
|
+
return async ({ env }) => {
|
|
645
|
+
const botId = env.selfId?.trim();
|
|
646
|
+
if (!botId) return "未知机器人";
|
|
647
|
+
const values = renderMember(await fetchMember(ctx, env, botId), itemsOf(config.botInfo.items), botId, void 0);
|
|
648
|
+
return values.length ? values.join(", ") : botId;
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
async function fetchRoles(bot, guildId, log) {
|
|
652
|
+
try {
|
|
653
|
+
const members = asMembers(await callOneBot(bot, "get_group_member_list", { group_id: Number(guildId) }));
|
|
654
|
+
if (!members.length) return void 0;
|
|
655
|
+
const owners = [];
|
|
656
|
+
const admins = [];
|
|
657
|
+
for (const member of members) {
|
|
658
|
+
const role = member.role;
|
|
659
|
+
const userId = pickText(member.user_id);
|
|
660
|
+
const name = pickText(member.card, member.nickname, userId);
|
|
661
|
+
const label = userId ? `${name}(${userId})` : name;
|
|
662
|
+
if (role === "owner") owners.push(label);
|
|
663
|
+
else if (role === "admin") admins.push(label);
|
|
664
|
+
}
|
|
665
|
+
return {
|
|
666
|
+
owners,
|
|
667
|
+
admins
|
|
668
|
+
};
|
|
669
|
+
} catch (error) {
|
|
670
|
+
log.debug("group roles lookup failed: %s", String(error));
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
function renderGroupInfo(group, items, roles) {
|
|
675
|
+
const id = pickText(group.group_id);
|
|
676
|
+
const name = pickText(group.group_name);
|
|
677
|
+
const memberCount = group.member_count;
|
|
678
|
+
const createTime = formatDate(normalizeTimestamp(group.create_time ?? group.group_create_time));
|
|
679
|
+
const values = [];
|
|
680
|
+
for (const item of items) switch (item) {
|
|
681
|
+
case "groupName":
|
|
682
|
+
if (name) values.push(`群名:${name}`);
|
|
683
|
+
break;
|
|
684
|
+
case "groupId":
|
|
685
|
+
if (id) values.push(`群号:${id}`);
|
|
686
|
+
break;
|
|
687
|
+
case "memberCount":
|
|
688
|
+
if (memberCount !== void 0 && memberCount !== null) values.push(`成员数量:${String(memberCount)}`);
|
|
689
|
+
break;
|
|
690
|
+
case "createTime":
|
|
691
|
+
if (createTime) values.push(`创建时间:${createTime}`);
|
|
692
|
+
break;
|
|
693
|
+
case "ownerList":
|
|
694
|
+
if (roles) values.push(`群主:${roles.owners.length ? roles.owners.join("、") : "无"}`);
|
|
695
|
+
break;
|
|
696
|
+
case "adminList": if (roles) values.push(`管理员:${roles.admins.length ? roles.admins.join("、") : "无"}`);
|
|
697
|
+
}
|
|
698
|
+
return values.length ? values.join("\n") : id || name || "未能获取当前群信息。";
|
|
699
|
+
}
|
|
700
|
+
function groupInfoProvider(ctx, config, log) {
|
|
701
|
+
return async ({ env }) => {
|
|
702
|
+
if (!env.guildId) return "";
|
|
703
|
+
if (env.platform !== "onebot") return "当前平台暂不支持查询群信息。";
|
|
704
|
+
const bot = oneBotOf(ctx, env.platform, env.selfId);
|
|
705
|
+
if (!bot) return "暂无群信息。";
|
|
706
|
+
const items = config.groupInfo.items.length ? config.groupInfo.items : DEFAULT_GROUP_INFO_ITEMS;
|
|
707
|
+
const group = await callOneBot(bot, "get_group_info", {
|
|
708
|
+
group_id: Number(env.guildId),
|
|
709
|
+
no_cache: false
|
|
710
|
+
}).then(asRecord, () => void 0);
|
|
711
|
+
if (!group) return "未能获取当前群信息。";
|
|
712
|
+
return renderGroupInfo(group, items, items.includes("ownerList") || items.includes("adminList") ? await fetchRoles(bot, env.guildId, log) : void 0);
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
var ShutListTimeout = class extends Error {};
|
|
716
|
+
function withTimeout(promise, ms) {
|
|
717
|
+
let timer;
|
|
718
|
+
return Promise.race([promise, new Promise((_, reject) => {
|
|
719
|
+
timer = setTimeout(() => reject(new ShutListTimeout("group shut list request timed out")), ms);
|
|
720
|
+
})]).finally(() => clearTimeout(timer));
|
|
721
|
+
}
|
|
722
|
+
async function fetchShutList(bot, guildId, protocol) {
|
|
723
|
+
const data = await withTimeout(callOneBot(bot, "get_group_shut_list", { group_id: protocol === "llbot" ? guildId : Number(guildId) }), SHUT_LIST_TIMEOUT_MS);
|
|
724
|
+
return Array.isArray(data) ? data : [];
|
|
725
|
+
}
|
|
726
|
+
function renderShutItem(item) {
|
|
727
|
+
const record = asRecord(item);
|
|
728
|
+
if (!record) return void 0;
|
|
729
|
+
const userId = pickText(record.uin, record.user_id);
|
|
730
|
+
if (!userId) return void 0;
|
|
731
|
+
return `id:${userId},name:${pickText(record.cardName, record.remark, record.nick, record.nickname, userId)},禁言截至:${formatDateTime(normalizeTimestamp(pickFirst(record.shutUpTime, record.shut_up_time))) || "未知"}`;
|
|
732
|
+
}
|
|
733
|
+
function groupShutListProvider(ctx, config, log) {
|
|
734
|
+
const cache = /* @__PURE__ */ new Map();
|
|
735
|
+
return async ({ env }) => {
|
|
736
|
+
if (!env.guildId) return "";
|
|
737
|
+
if (env.platform !== "onebot") return "当前平台暂不支持查询群禁言列表。";
|
|
738
|
+
const bot = oneBotOf(ctx, env.platform, env.selfId);
|
|
739
|
+
if (!bot) return "暂无群禁言列表。";
|
|
740
|
+
const key = `${env.selfId}:${env.guildId}`;
|
|
741
|
+
const cached = cache.get(key);
|
|
742
|
+
if (cached && cached.expiresAt > Date.now()) return cached.value;
|
|
743
|
+
try {
|
|
744
|
+
const lines = (await fetchShutList(bot, env.guildId, config.oneBotProtocol)).flatMap((item) => renderShutItem(item) ?? []);
|
|
745
|
+
const value = lines.length ? lines.join("\n") : "当前群暂无禁言成员。";
|
|
746
|
+
cache.set(key, {
|
|
747
|
+
value,
|
|
748
|
+
expiresAt: Date.now() + SHUT_LIST_CACHE_TTL_MS
|
|
749
|
+
});
|
|
750
|
+
return value;
|
|
751
|
+
} catch (error) {
|
|
752
|
+
log.debug("group shut list failed: %s", String(error));
|
|
753
|
+
return error instanceof ShutListTimeout ? "当前群暂无禁言成员。" : "获取群禁言列表失败。";
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
/** 按配置得到要注册的变量;变量名留空的不注册。 */
|
|
758
|
+
function variableDefinitions(ctx, config, log) {
|
|
759
|
+
return [
|
|
760
|
+
[
|
|
761
|
+
config.userInfo.variableName,
|
|
762
|
+
"Current user info in this group",
|
|
763
|
+
userInfoProvider(ctx, config)
|
|
764
|
+
],
|
|
765
|
+
[
|
|
766
|
+
config.botInfo.variableName,
|
|
767
|
+
"Bot info in this group",
|
|
768
|
+
botInfoProvider(ctx, config)
|
|
769
|
+
],
|
|
770
|
+
[
|
|
771
|
+
config.groupInfo.variableName,
|
|
772
|
+
"Current group info",
|
|
773
|
+
groupInfoProvider(ctx, config, log)
|
|
774
|
+
],
|
|
775
|
+
[
|
|
776
|
+
config.groupShutList.variableName,
|
|
777
|
+
"Muted members of this group",
|
|
778
|
+
groupShutListProvider(ctx, config, log)
|
|
779
|
+
]
|
|
780
|
+
].filter(([name]) => name.trim()).map(([name, description, provider]) => [{
|
|
781
|
+
name: name.trim(),
|
|
782
|
+
description,
|
|
783
|
+
mutable: true
|
|
784
|
+
}, provider]);
|
|
785
|
+
}
|
|
786
|
+
function registerVariables(ctx, config, log) {
|
|
787
|
+
for (const [declaration, provider] of variableDefinitions(ctx, config, log)) try {
|
|
788
|
+
ctx.prompt.variable(declaration, provider);
|
|
789
|
+
} catch (error) {
|
|
790
|
+
log.warn("variable %s not registered: %s", declaration.name, error instanceof Error ? error.message : String(error));
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
// external/onebot-toolbox/src/index.ts
|
|
794
|
+
const name = "chatluna-onebot-toolbox";
|
|
795
|
+
/** 过滤工具用的 agents、唤醒用的 chat 与读取会话图片用的 chatluna_storage 是可选服务。 */
|
|
796
|
+
const inject = [
|
|
797
|
+
"satori",
|
|
798
|
+
"tools",
|
|
799
|
+
"prompt"
|
|
800
|
+
];
|
|
801
|
+
function apply(ctx, config) {
|
|
802
|
+
const log = createLogger(ctx, config.debugLogging);
|
|
803
|
+
registerVariables(ctx, config, log);
|
|
804
|
+
registerNativeTools(ctx, config, log);
|
|
805
|
+
registerRequestNotices(ctx, config, log);
|
|
806
|
+
}
|
|
807
|
+
export { Config, DEFAULT_GROUP_INFO_ITEMS, DEFAULT_MEMBER_INFO_ITEMS, NATIVE_TOOLS, NATIVE_TOOL_KEYS, apply, inject, name };
|
|
808
|
+
|
|
809
|
+
//# sourceMappingURL=index.js.map
|