koishi-plugin-mai-bridge 0.1.2 → 0.1.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/README.md +1 -1
- package/out/bridge/convert.d.ts +1 -0
- package/out/bridge/convert.js +5 -2
- package/out/bridge/group-trigger.d.ts +7 -1
- package/out/bridge/group-trigger.js +10 -9
- package/out/config.js +1 -1
- package/out/service.d.ts +1 -0
- package/out/service.js +26 -13
- package/package.json +1 -1
- package/src/bridge/convert.ts +6 -2
- package/src/bridge/group-trigger.ts +17 -9
- package/src/config.ts +1 -1
- package/src/service.ts +29 -13
package/README.md
CHANGED
|
@@ -68,7 +68,7 @@ groupMessageTriggerCount: 1
|
|
|
68
68
|
- `apiKey` 可以留空。插件会生成并复用运行期密钥。
|
|
69
69
|
- `dockerNetwork` 按你的 Koishi Docker 网络填写。Koishi 需要能通过 `apiHost` 访问 `maimai-ko`。
|
|
70
70
|
- `webuiPublicUrl` 可填写反代或宿主机映射后的 WebUI 地址,用于 Koishi 控制台显示入口。
|
|
71
|
-
- `groupMessageTriggerCount`
|
|
71
|
+
- `groupMessageTriggerCount` 控制群聊累计多少条消息后批量转发并强制触发 maibot 思考。设为 `3` 时,同一群前两条先缓存,第 3 条会连同前两条一起转发;私聊不受影响。
|
|
72
72
|
|
|
73
73
|
## 消息模式
|
|
74
74
|
|
package/out/bridge/convert.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { KoishiRoute, MaimApiMessage } from '../types';
|
|
|
4
4
|
export interface SessionToMaimOptions {
|
|
5
5
|
resolveImage?: (source: string) => Awaitable<string | undefined>;
|
|
6
6
|
replyContext?: ReplyContext;
|
|
7
|
+
forceMention?: boolean;
|
|
7
8
|
}
|
|
8
9
|
export interface ReplyContext {
|
|
9
10
|
targetMessageId: string;
|
package/out/bridge/convert.js
CHANGED
|
@@ -181,6 +181,8 @@ async function sessionToMaimMessage(session, route, apiKey, options = {}) {
|
|
|
181
181
|
const messageId = String(session.messageId || session.id || `koishi-${Date.now()}`);
|
|
182
182
|
const senderInfo = buildSenderInfo(session);
|
|
183
183
|
const atBot = isAtBot(session);
|
|
184
|
+
const forceMention = !!options.forceMention;
|
|
185
|
+
const mentioned = atBot || forceMention;
|
|
184
186
|
const replyContext = options.replyContext;
|
|
185
187
|
return {
|
|
186
188
|
message_info: {
|
|
@@ -202,8 +204,9 @@ async function sessionToMaimMessage(session, route, apiKey, options = {}) {
|
|
|
202
204
|
koishi_reply_to_message_id: replyContext?.targetMessageId,
|
|
203
205
|
koishi_reply_context_count: replyContext?.contextCount,
|
|
204
206
|
koishi_is_direct: !!session.isDirect,
|
|
205
|
-
|
|
206
|
-
|
|
207
|
+
koishi_group_trigger_forced: forceMention,
|
|
208
|
+
at_bot: mentioned,
|
|
209
|
+
is_mentioned: mentioned,
|
|
207
210
|
},
|
|
208
211
|
format_info: {
|
|
209
212
|
content_format: ['text', 'image'],
|
|
@@ -5,10 +5,16 @@ export interface GroupTriggerResult {
|
|
|
5
5
|
count: number;
|
|
6
6
|
threshold: number;
|
|
7
7
|
key?: string;
|
|
8
|
+
entries: GroupTriggerEntry[];
|
|
9
|
+
forceMention?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface GroupTriggerEntry {
|
|
12
|
+
session: Session;
|
|
13
|
+
route: KoishiRoute;
|
|
8
14
|
}
|
|
9
15
|
export declare class GroupMessageTrigger {
|
|
10
16
|
private threshold;
|
|
11
|
-
private
|
|
17
|
+
private pending;
|
|
12
18
|
constructor(threshold: number);
|
|
13
19
|
test(session: Session, route: KoishiRoute): GroupTriggerResult;
|
|
14
20
|
clear(): void;
|
|
@@ -3,26 +3,27 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.GroupMessageTrigger = void 0;
|
|
4
4
|
class GroupMessageTrigger {
|
|
5
5
|
threshold;
|
|
6
|
-
|
|
6
|
+
pending = new Map();
|
|
7
7
|
constructor(threshold) {
|
|
8
8
|
this.threshold = threshold;
|
|
9
9
|
}
|
|
10
10
|
test(session, route) {
|
|
11
11
|
const threshold = Math.max(1, Math.floor(this.threshold || 1));
|
|
12
|
+
const entry = { session, route };
|
|
12
13
|
if (threshold <= 1 || session.isDirect || !route.channelId) {
|
|
13
|
-
return { shouldForward: true, count: 1, threshold };
|
|
14
|
+
return { shouldForward: true, count: 1, threshold, entries: [entry] };
|
|
14
15
|
}
|
|
15
16
|
const key = this.createKey(route);
|
|
16
|
-
const
|
|
17
|
-
if (
|
|
18
|
-
this.
|
|
19
|
-
return { shouldForward: true, count, threshold, key };
|
|
17
|
+
const entries = [...this.pending.get(key) || [], entry];
|
|
18
|
+
if (entries.length >= threshold) {
|
|
19
|
+
this.pending.delete(key);
|
|
20
|
+
return { shouldForward: true, count: entries.length, threshold, key, entries, forceMention: true };
|
|
20
21
|
}
|
|
21
|
-
this.
|
|
22
|
-
return { shouldForward: false, count, threshold, key };
|
|
22
|
+
this.pending.set(key, entries);
|
|
23
|
+
return { shouldForward: false, count: entries.length, threshold, key, entries: [] };
|
|
23
24
|
}
|
|
24
25
|
clear() {
|
|
25
|
-
this.
|
|
26
|
+
this.pending.clear();
|
|
26
27
|
}
|
|
27
28
|
createKey(route) {
|
|
28
29
|
return [
|
package/out/config.js
CHANGED
|
@@ -43,7 +43,7 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
43
43
|
koishi_1.Schema.const('command').description('命令:仅命中指令前缀时转发。'),
|
|
44
44
|
]).default('coexist').description('消息转发模式。'),
|
|
45
45
|
commandPrefix: koishi_1.Schema.string().default('mai.ko').description('command 模式下触发 mai.ko 的文本前缀。'),
|
|
46
|
-
groupMessageTriggerCount: koishi_1.Schema.number().min(1).default(1).description('
|
|
46
|
+
groupMessageTriggerCount: koishi_1.Schema.number().min(1).default(1).description('同一群聊累计达到多少条消息后批量转发并强制触发 mai.ko 思考;1 表示每条群消息都转发。私聊不受影响。'),
|
|
47
47
|
imageDownloadEnabled: koishi_1.Schema.boolean().default(true).description('转发图片消息前由 Koishi 下载图片并转为 mai.ko 可识别的 base64 图片段。'),
|
|
48
48
|
imageDownloadTimeout: koishi_1.Schema.number().min(1000).default(10000).description('下载单张入站图片的超时时间,单位毫秒。'),
|
|
49
49
|
imageDownloadMaxBytes: koishi_1.Schema.number().min(1024).default(10 * 1024 * 1024).description('允许转发给 mai.ko 的单张图片最大字节数。'),
|
package/out/service.d.ts
CHANGED
|
@@ -38,6 +38,7 @@ export declare class MaibotService extends Service {
|
|
|
38
38
|
dockerRestart(): Promise<RuntimeStatus>;
|
|
39
39
|
getStatus(): RuntimeStatus;
|
|
40
40
|
handleSession(session: Session, next: () => Awaitable<void | Fragment>): Promise<void | Fragment>;
|
|
41
|
+
private forwardSessionToMaim;
|
|
41
42
|
dispose(): Promise<void>;
|
|
42
43
|
private logMessageDetail;
|
|
43
44
|
private logMessageSummary;
|
package/out/service.js
CHANGED
|
@@ -249,19 +249,15 @@ class MaibotService extends koishi_1.Service {
|
|
|
249
249
|
return '';
|
|
250
250
|
return next();
|
|
251
251
|
}
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
this.bridge.lastRouteId = route.routeId;
|
|
262
|
-
this.bridge.lastMessageId = message.message_info.message_id;
|
|
263
|
-
const replyLog = replyContext ? ` reply=${replyContext.targetMessageId} context=${replyContext.contextCount || 0}` : '';
|
|
264
|
-
this.logMessageDetail(`koishi -> maimai forwarded: route=${route.routeId}${replyLog} ${(0, logging_1.describeSegment)(message.message_segment)}`);
|
|
252
|
+
const entries = trigger.entries.length ? trigger.entries : [{ session, route }];
|
|
253
|
+
if (trigger.forceMention) {
|
|
254
|
+
this.logMessageSummary(`group trigger reached: route=${route.routeId} count=${trigger.count}/${trigger.threshold} flushing=${entries.length}`);
|
|
255
|
+
}
|
|
256
|
+
for (let index = 0; index < entries.length; index++) {
|
|
257
|
+
const entry = entries[index];
|
|
258
|
+
const forceMention = !!trigger.forceMention && index === entries.length - 1;
|
|
259
|
+
await this.forwardSessionToMaim(entry.session, entry.route, forceMention);
|
|
260
|
+
}
|
|
265
261
|
if (this.pluginConfig.messageMode === 'exclusive')
|
|
266
262
|
return '';
|
|
267
263
|
}
|
|
@@ -273,6 +269,23 @@ class MaibotService extends koishi_1.Service {
|
|
|
273
269
|
}
|
|
274
270
|
return next();
|
|
275
271
|
}
|
|
272
|
+
async forwardSessionToMaim(session, route, forceMention = false) {
|
|
273
|
+
const replyContext = this.history.resolveReplyContext(session, route);
|
|
274
|
+
const message = await (0, convert_1.sessionToMaimMessage)(session, route, this.apiKey, {
|
|
275
|
+
resolveImage: (source) => this.resolveImageToBase64(source),
|
|
276
|
+
replyContext,
|
|
277
|
+
forceMention,
|
|
278
|
+
});
|
|
279
|
+
this.transport.sendMessage(message);
|
|
280
|
+
this.history.rememberSession(session, route);
|
|
281
|
+
this.bridge.maimSent += 1;
|
|
282
|
+
this.bridge.lastMaimSendAt = Date.now();
|
|
283
|
+
this.bridge.lastRouteId = route.routeId;
|
|
284
|
+
this.bridge.lastMessageId = message.message_info.message_id;
|
|
285
|
+
const replyLog = replyContext ? ` reply=${replyContext.targetMessageId} context=${replyContext.contextCount || 0}` : '';
|
|
286
|
+
const forceLog = forceMention ? ' forced=group-trigger' : '';
|
|
287
|
+
this.logMessageDetail(`koishi -> maimai forwarded: route=${route.routeId}${replyLog}${forceLog} ${(0, logging_1.describeSegment)(message.message_segment)}`);
|
|
288
|
+
}
|
|
276
289
|
async dispose() {
|
|
277
290
|
await this.shutdown();
|
|
278
291
|
this.routes.clear();
|
package/package.json
CHANGED
package/src/bridge/convert.ts
CHANGED
|
@@ -7,6 +7,7 @@ const PLATFORM = 'koishi'
|
|
|
7
7
|
export interface SessionToMaimOptions {
|
|
8
8
|
resolveImage?: (source: string) => Awaitable<string | undefined>
|
|
9
9
|
replyContext?: ReplyContext
|
|
10
|
+
forceMention?: boolean
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export interface ReplyContext {
|
|
@@ -204,6 +205,8 @@ export async function sessionToMaimMessage(
|
|
|
204
205
|
const messageId = String(session.messageId || session.id || `koishi-${Date.now()}`)
|
|
205
206
|
const senderInfo = buildSenderInfo(session)
|
|
206
207
|
const atBot = isAtBot(session)
|
|
208
|
+
const forceMention = !!options.forceMention
|
|
209
|
+
const mentioned = atBot || forceMention
|
|
207
210
|
const replyContext = options.replyContext
|
|
208
211
|
return {
|
|
209
212
|
message_info: {
|
|
@@ -225,8 +228,9 @@ export async function sessionToMaimMessage(
|
|
|
225
228
|
koishi_reply_to_message_id: replyContext?.targetMessageId,
|
|
226
229
|
koishi_reply_context_count: replyContext?.contextCount,
|
|
227
230
|
koishi_is_direct: !!session.isDirect,
|
|
228
|
-
|
|
229
|
-
|
|
231
|
+
koishi_group_trigger_forced: forceMention,
|
|
232
|
+
at_bot: mentioned,
|
|
233
|
+
is_mentioned: mentioned,
|
|
230
234
|
},
|
|
231
235
|
format_info: {
|
|
232
236
|
content_format: ['text', 'image'],
|
|
@@ -6,32 +6,40 @@ export interface GroupTriggerResult {
|
|
|
6
6
|
count: number
|
|
7
7
|
threshold: number
|
|
8
8
|
key?: string
|
|
9
|
+
entries: GroupTriggerEntry[]
|
|
10
|
+
forceMention?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface GroupTriggerEntry {
|
|
14
|
+
session: Session
|
|
15
|
+
route: KoishiRoute
|
|
9
16
|
}
|
|
10
17
|
|
|
11
18
|
export class GroupMessageTrigger {
|
|
12
|
-
private
|
|
19
|
+
private pending = new Map<string, GroupTriggerEntry[]>()
|
|
13
20
|
|
|
14
21
|
constructor(private threshold: number) {}
|
|
15
22
|
|
|
16
23
|
test(session: Session, route: KoishiRoute): GroupTriggerResult {
|
|
17
24
|
const threshold = Math.max(1, Math.floor(this.threshold || 1))
|
|
25
|
+
const entry = { session, route }
|
|
18
26
|
if (threshold <= 1 || session.isDirect || !route.channelId) {
|
|
19
|
-
return { shouldForward: true, count: 1, threshold }
|
|
27
|
+
return { shouldForward: true, count: 1, threshold, entries: [entry] }
|
|
20
28
|
}
|
|
21
29
|
|
|
22
30
|
const key = this.createKey(route)
|
|
23
|
-
const
|
|
24
|
-
if (
|
|
25
|
-
this.
|
|
26
|
-
return { shouldForward: true, count, threshold, key }
|
|
31
|
+
const entries = [...this.pending.get(key) || [], entry]
|
|
32
|
+
if (entries.length >= threshold) {
|
|
33
|
+
this.pending.delete(key)
|
|
34
|
+
return { shouldForward: true, count: entries.length, threshold, key, entries, forceMention: true }
|
|
27
35
|
}
|
|
28
36
|
|
|
29
|
-
this.
|
|
30
|
-
return { shouldForward: false, count, threshold, key }
|
|
37
|
+
this.pending.set(key, entries)
|
|
38
|
+
return { shouldForward: false, count: entries.length, threshold, key, entries: [] }
|
|
31
39
|
}
|
|
32
40
|
|
|
33
41
|
clear() {
|
|
34
|
-
this.
|
|
42
|
+
this.pending.clear()
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
private createKey(route: KoishiRoute) {
|
package/src/config.ts
CHANGED
|
@@ -93,7 +93,7 @@ export const Config: Schema<Config> = Schema.intersect([
|
|
|
93
93
|
Schema.const('command').description('命令:仅命中指令前缀时转发。'),
|
|
94
94
|
]).default('coexist').description('消息转发模式。'),
|
|
95
95
|
commandPrefix: Schema.string().default('mai.ko').description('command 模式下触发 mai.ko 的文本前缀。'),
|
|
96
|
-
groupMessageTriggerCount: Schema.number().min(1).default(1).description('
|
|
96
|
+
groupMessageTriggerCount: Schema.number().min(1).default(1).description('同一群聊累计达到多少条消息后批量转发并强制触发 mai.ko 思考;1 表示每条群消息都转发。私聊不受影响。'),
|
|
97
97
|
imageDownloadEnabled: Schema.boolean().default(true).description('转发图片消息前由 Koishi 下载图片并转为 mai.ko 可识别的 base64 图片段。'),
|
|
98
98
|
imageDownloadTimeout: Schema.number().min(1000).default(10000).description('下载单张入站图片的超时时间,单位毫秒。'),
|
|
99
99
|
imageDownloadMaxBytes: Schema.number().min(1024).default(10 * 1024 * 1024).description('允许转发给 mai.ko 的单张图片最大字节数。'),
|
package/src/service.ts
CHANGED
|
@@ -254,19 +254,17 @@ export class MaibotService extends Service {
|
|
|
254
254
|
return next()
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
const replyLog = replyContext ? ` reply=${replyContext.targetMessageId} context=${replyContext.contextCount || 0}` : ''
|
|
269
|
-
this.logMessageDetail(`koishi -> maimai forwarded: route=${route.routeId}${replyLog} ${describeSegment(message.message_segment)}`)
|
|
257
|
+
const entries = trigger.entries.length ? trigger.entries : [{ session, route }]
|
|
258
|
+
if (trigger.forceMention) {
|
|
259
|
+
this.logMessageSummary(`group trigger reached: route=${route.routeId} count=${trigger.count}/${trigger.threshold} flushing=${entries.length}`)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
for (let index = 0; index < entries.length; index++) {
|
|
263
|
+
const entry = entries[index]
|
|
264
|
+
const forceMention = !!trigger.forceMention && index === entries.length - 1
|
|
265
|
+
await this.forwardSessionToMaim(entry.session, entry.route, forceMention)
|
|
266
|
+
}
|
|
267
|
+
|
|
270
268
|
if (this.pluginConfig.messageMode === 'exclusive') return ''
|
|
271
269
|
} catch (error) {
|
|
272
270
|
const message = error instanceof Error ? error.message : String(error)
|
|
@@ -278,6 +276,24 @@ export class MaibotService extends Service {
|
|
|
278
276
|
return next()
|
|
279
277
|
}
|
|
280
278
|
|
|
279
|
+
private async forwardSessionToMaim(session: Session, route: ReturnType<RouteRegistry['remember']>, forceMention = false) {
|
|
280
|
+
const replyContext = this.history.resolveReplyContext(session, route)
|
|
281
|
+
const message = await sessionToMaimMessage(session, route, this.apiKey, {
|
|
282
|
+
resolveImage: (source) => this.resolveImageToBase64(source),
|
|
283
|
+
replyContext,
|
|
284
|
+
forceMention,
|
|
285
|
+
})
|
|
286
|
+
this.transport.sendMessage(message)
|
|
287
|
+
this.history.rememberSession(session, route)
|
|
288
|
+
this.bridge.maimSent += 1
|
|
289
|
+
this.bridge.lastMaimSendAt = Date.now()
|
|
290
|
+
this.bridge.lastRouteId = route.routeId
|
|
291
|
+
this.bridge.lastMessageId = message.message_info.message_id
|
|
292
|
+
const replyLog = replyContext ? ` reply=${replyContext.targetMessageId} context=${replyContext.contextCount || 0}` : ''
|
|
293
|
+
const forceLog = forceMention ? ' forced=group-trigger' : ''
|
|
294
|
+
this.logMessageDetail(`koishi -> maimai forwarded: route=${route.routeId}${replyLog}${forceLog} ${describeSegment(message.message_segment)}`)
|
|
295
|
+
}
|
|
296
|
+
|
|
281
297
|
async dispose() {
|
|
282
298
|
await this.shutdown()
|
|
283
299
|
this.routes.clear()
|