koishi-plugin-mai-bridge 0.1.1 → 0.1.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/README.md +2 -0
- package/out/bridge/group-trigger.d.ts +16 -0
- package/out/bridge/group-trigger.js +36 -0
- package/out/bridge/prepare.d.ts +5 -0
- package/out/bridge/prepare.js +75 -0
- package/out/config.d.ts +1 -0
- package/out/config.js +1 -0
- package/out/service.d.ts +1 -0
- package/out/service.js +18 -1
- package/out/types.d.ts +3 -0
- package/package.json +1 -1
- package/src/bridge/group-trigger.ts +45 -0
- package/src/bridge/prepare.ts +72 -0
- package/src/config.ts +2 -0
- package/src/service.ts +18 -1
- package/src/types.ts +3 -0
package/README.md
CHANGED
|
@@ -58,6 +58,7 @@ webuiEnabled: true
|
|
|
58
58
|
webuiHost: 0.0.0.0
|
|
59
59
|
webuiPort: 8002
|
|
60
60
|
messageMode: coexist
|
|
61
|
+
groupMessageTriggerCount: 1
|
|
61
62
|
```
|
|
62
63
|
|
|
63
64
|
说明:
|
|
@@ -67,6 +68,7 @@ messageMode: coexist
|
|
|
67
68
|
- `apiKey` 可以留空。插件会生成并复用运行期密钥。
|
|
68
69
|
- `dockerNetwork` 按你的 Koishi Docker 网络填写。Koishi 需要能通过 `apiHost` 访问 `maimai-ko`。
|
|
69
70
|
- `webuiPublicUrl` 可填写反代或宿主机映射后的 WebUI 地址,用于 Koishi 控制台显示入口。
|
|
71
|
+
- `groupMessageTriggerCount` 控制群聊累计多少条消息后才转发给 maibot。设为 `3` 时,同一群前两条只记录上下文,第 3 条才拉起思考;私聊不受影响。
|
|
70
72
|
|
|
71
73
|
## 消息模式
|
|
72
74
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Session } from 'koishi';
|
|
2
|
+
import type { KoishiRoute } from '../types';
|
|
3
|
+
export interface GroupTriggerResult {
|
|
4
|
+
shouldForward: boolean;
|
|
5
|
+
count: number;
|
|
6
|
+
threshold: number;
|
|
7
|
+
key?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare class GroupMessageTrigger {
|
|
10
|
+
private threshold;
|
|
11
|
+
private counters;
|
|
12
|
+
constructor(threshold: number);
|
|
13
|
+
test(session: Session, route: KoishiRoute): GroupTriggerResult;
|
|
14
|
+
clear(): void;
|
|
15
|
+
private createKey;
|
|
16
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GroupMessageTrigger = void 0;
|
|
4
|
+
class GroupMessageTrigger {
|
|
5
|
+
threshold;
|
|
6
|
+
counters = new Map();
|
|
7
|
+
constructor(threshold) {
|
|
8
|
+
this.threshold = threshold;
|
|
9
|
+
}
|
|
10
|
+
test(session, route) {
|
|
11
|
+
const threshold = Math.max(1, Math.floor(this.threshold || 1));
|
|
12
|
+
if (threshold <= 1 || session.isDirect || !route.channelId) {
|
|
13
|
+
return { shouldForward: true, count: 1, threshold };
|
|
14
|
+
}
|
|
15
|
+
const key = this.createKey(route);
|
|
16
|
+
const count = (this.counters.get(key) || 0) + 1;
|
|
17
|
+
if (count >= threshold) {
|
|
18
|
+
this.counters.delete(key);
|
|
19
|
+
return { shouldForward: true, count, threshold, key };
|
|
20
|
+
}
|
|
21
|
+
this.counters.set(key, count);
|
|
22
|
+
return { shouldForward: false, count, threshold, key };
|
|
23
|
+
}
|
|
24
|
+
clear() {
|
|
25
|
+
this.counters.clear();
|
|
26
|
+
}
|
|
27
|
+
createKey(route) {
|
|
28
|
+
return [
|
|
29
|
+
route.platform,
|
|
30
|
+
route.botSelfId,
|
|
31
|
+
route.guildId || '',
|
|
32
|
+
route.channelId,
|
|
33
|
+
].join(':');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
exports.GroupMessageTrigger = GroupMessageTrigger;
|
package/out/bridge/prepare.d.ts
CHANGED
|
@@ -32,7 +32,12 @@ export declare class MaibotPrepareManager {
|
|
|
32
32
|
private isValidGitRepository;
|
|
33
33
|
private isRecoverablePartialDirectory;
|
|
34
34
|
private ensurePatch;
|
|
35
|
+
private applyPatch;
|
|
35
36
|
private checkPatchState;
|
|
37
|
+
private refreshManagedPatch;
|
|
38
|
+
private readMarker;
|
|
39
|
+
private listPatchPaths;
|
|
40
|
+
private listDirtyTrackedFiles;
|
|
36
41
|
private readCommit;
|
|
37
42
|
private writeMarker;
|
|
38
43
|
private resolvePatchPath;
|
package/out/bridge/prepare.js
CHANGED
|
@@ -133,6 +133,13 @@ class MaibotPrepareManager {
|
|
|
133
133
|
throw new Error(`blocked: bundled patch not found: ${patchPath}`);
|
|
134
134
|
}
|
|
135
135
|
if (patchState === 'conflict') {
|
|
136
|
+
const refreshed = await this.refreshManagedPatch(patchPath);
|
|
137
|
+
if (refreshed === 'applied')
|
|
138
|
+
return;
|
|
139
|
+
if (refreshed === 'pending') {
|
|
140
|
+
await this.applyPatch(patchPath);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
136
143
|
throw new Error('blocked: bundled patch cannot be applied cleanly; check MaiBot ref or update maimai-koishi.patch');
|
|
137
144
|
}
|
|
138
145
|
if (patchState === 'applied') {
|
|
@@ -140,6 +147,9 @@ class MaibotPrepareManager {
|
|
|
140
147
|
this.pushLog('bundled patch already applied');
|
|
141
148
|
return;
|
|
142
149
|
}
|
|
150
|
+
await this.applyPatch(patchPath);
|
|
151
|
+
}
|
|
152
|
+
async applyPatch(patchPath) {
|
|
143
153
|
this.pushLog(`applying bundled patch: ${patchPath}`);
|
|
144
154
|
await this.git(['apply', patchPath], this.root);
|
|
145
155
|
this.patchApplied = true;
|
|
@@ -155,6 +165,71 @@ class MaibotPrepareManager {
|
|
|
155
165
|
return 'applied';
|
|
156
166
|
return 'conflict';
|
|
157
167
|
}
|
|
168
|
+
async refreshManagedPatch(patchPath) {
|
|
169
|
+
const marker = this.readMarker();
|
|
170
|
+
if (!marker?.patchApplied)
|
|
171
|
+
return;
|
|
172
|
+
if (marker.gitUrl !== this.config.maibotGitUrl || marker.gitRef !== this.config.maibotGitRef)
|
|
173
|
+
return;
|
|
174
|
+
if (!marker.patchChecksum || marker.patchChecksum === this.patchChecksum)
|
|
175
|
+
return;
|
|
176
|
+
const patchPaths = new Set(this.listPatchPaths(patchPath));
|
|
177
|
+
const dirtyFiles = await this.listDirtyTrackedFiles();
|
|
178
|
+
if (!dirtyFiles.length)
|
|
179
|
+
return;
|
|
180
|
+
const foreignFiles = dirtyFiles.filter((file) => !patchPaths.has(file));
|
|
181
|
+
if (foreignFiles.length) {
|
|
182
|
+
throw new Error(`blocked: MaiBot has local changes outside bundled patch: ${foreignFiles.slice(0, 5).join(', ')}`);
|
|
183
|
+
}
|
|
184
|
+
this.pushLog('refreshing managed bundled patch');
|
|
185
|
+
await this.git(['restore', '--source', 'HEAD', '--', ...dirtyFiles], this.root);
|
|
186
|
+
const patchState = await this.checkPatchState(patchPath);
|
|
187
|
+
if (patchState === 'pending')
|
|
188
|
+
return 'pending';
|
|
189
|
+
if (patchState === 'applied') {
|
|
190
|
+
this.patchApplied = true;
|
|
191
|
+
this.pushLog('bundled patch already applied');
|
|
192
|
+
return 'applied';
|
|
193
|
+
}
|
|
194
|
+
throw new Error('blocked: bundled patch cannot be refreshed cleanly; check MaiBot ref or update maimai-koishi.patch');
|
|
195
|
+
}
|
|
196
|
+
readMarker() {
|
|
197
|
+
try {
|
|
198
|
+
const marker = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(this.root, '.mai-ko-prepare.json'), 'utf8'));
|
|
199
|
+
if (!marker || typeof marker !== 'object')
|
|
200
|
+
return;
|
|
201
|
+
return marker;
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
listPatchPaths(patchPath) {
|
|
208
|
+
const paths = new Set();
|
|
209
|
+
const patch = (0, fs_1.readFileSync)(patchPath, 'utf8');
|
|
210
|
+
for (const line of patch.split(/\r?\n/)) {
|
|
211
|
+
const match = /^diff --git a\/(.+) b\/(.+)$/.exec(line);
|
|
212
|
+
if (!match)
|
|
213
|
+
continue;
|
|
214
|
+
if (match[2] !== '/dev/null')
|
|
215
|
+
paths.add(match[2]);
|
|
216
|
+
}
|
|
217
|
+
return [...paths];
|
|
218
|
+
}
|
|
219
|
+
async listDirtyTrackedFiles() {
|
|
220
|
+
const result = await this.git(['status', '--porcelain', '--untracked-files=no'], this.root, true, false);
|
|
221
|
+
if (result.code !== 0)
|
|
222
|
+
return [];
|
|
223
|
+
return result.stdout
|
|
224
|
+
.split(/\r?\n/)
|
|
225
|
+
.map((line) => {
|
|
226
|
+
if (!line.trim())
|
|
227
|
+
return '';
|
|
228
|
+
const path = line.slice(3);
|
|
229
|
+
return path.includes(' -> ') ? path.split(' -> ').pop() : path;
|
|
230
|
+
})
|
|
231
|
+
.filter(Boolean);
|
|
232
|
+
}
|
|
158
233
|
async readCommit() {
|
|
159
234
|
const result = await this.git(['rev-parse', 'HEAD'], this.root, true, false);
|
|
160
235
|
return result.code === 0 ? result.stdout.trim() : undefined;
|
package/out/config.d.ts
CHANGED
package/out/config.js
CHANGED
|
@@ -43,6 +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('同一群聊累计达到多少条消息后才转发给 mai.ko 触发思考;1 表示每条群消息都转发。私聊不受影响。'),
|
|
46
47
|
imageDownloadEnabled: koishi_1.Schema.boolean().default(true).description('转发图片消息前由 Koishi 下载图片并转为 mai.ko 可识别的 base64 图片段。'),
|
|
47
48
|
imageDownloadTimeout: koishi_1.Schema.number().min(1000).default(10000).description('下载单张入站图片的超时时间,单位毫秒。'),
|
|
48
49
|
imageDownloadMaxBytes: koishi_1.Schema.number().min(1024).default(10 * 1024 * 1024).description('允许转发给 mai.ko 的单张图片最大字节数。'),
|
package/out/service.d.ts
CHANGED
package/out/service.js
CHANGED
|
@@ -5,6 +5,7 @@ const koishi_1 = require("koishi");
|
|
|
5
5
|
const convert_1 = require("./bridge/convert");
|
|
6
6
|
const docker_1 = require("./bridge/docker");
|
|
7
7
|
const external_logs_1 = require("./bridge/external-logs");
|
|
8
|
+
const group_trigger_1 = require("./bridge/group-trigger");
|
|
8
9
|
const history_1 = require("./bridge/history");
|
|
9
10
|
const prepare_1 = require("./bridge/prepare");
|
|
10
11
|
const process_1 = require("./bridge/process");
|
|
@@ -21,6 +22,7 @@ class MaibotService extends koishi_1.Service {
|
|
|
21
22
|
docker;
|
|
22
23
|
process;
|
|
23
24
|
externalLogs;
|
|
25
|
+
groupTrigger;
|
|
24
26
|
history;
|
|
25
27
|
routes;
|
|
26
28
|
transport;
|
|
@@ -43,6 +45,7 @@ class MaibotService extends koishi_1.Service {
|
|
|
43
45
|
koishiSent: 0,
|
|
44
46
|
routeMissed: 0,
|
|
45
47
|
sendFailed: 0,
|
|
48
|
+
groupTriggerSkipped: 0,
|
|
46
49
|
};
|
|
47
50
|
constructor(ctx, pluginConfig) {
|
|
48
51
|
super(ctx, 'maimai');
|
|
@@ -53,6 +56,7 @@ class MaibotService extends koishi_1.Service {
|
|
|
53
56
|
this.docker = new docker_1.MaibotDockerManager(pluginConfig, this.apiKey);
|
|
54
57
|
this.process = new process_1.MaibotProcessManager(ctx, pluginConfig, this.apiKey);
|
|
55
58
|
this.externalLogs = new external_logs_1.ExternalLogForwarder(ctx, pluginConfig, [this.apiKey, pluginConfig.apiKey]);
|
|
59
|
+
this.groupTrigger = new group_trigger_1.GroupMessageTrigger(pluginConfig.groupMessageTriggerCount);
|
|
56
60
|
this.history = new history_1.MessageHistory(pluginConfig.routeTtl);
|
|
57
61
|
this.routes = new routes_1.RouteRegistry(pluginConfig.routeTtl);
|
|
58
62
|
this.transport = new transport_1.MaimTransport(ctx, pluginConfig, this.apiKey, {
|
|
@@ -221,6 +225,19 @@ class MaibotService extends koishi_1.Service {
|
|
|
221
225
|
this.bridge.lastMessageId = String(session.messageId || session.id || '');
|
|
222
226
|
this.logMessageDetail(`koishi message received: ${(0, logging_1.describeSession)(session)}`);
|
|
223
227
|
try {
|
|
228
|
+
const route = this.routes.remember(session);
|
|
229
|
+
const trigger = this.groupTrigger.test(session, route);
|
|
230
|
+
if (!trigger.shouldForward) {
|
|
231
|
+
this.history.rememberSession(session, route);
|
|
232
|
+
this.bridge.groupTriggerSkipped += 1;
|
|
233
|
+
this.bridge.lastGroupTriggerCount = trigger.count;
|
|
234
|
+
this.bridge.lastGroupTriggerThreshold = trigger.threshold;
|
|
235
|
+
this.bridge.lastRouteId = route.routeId;
|
|
236
|
+
this.logMessageDetail(`koishi -> maimai gated: route=${route.routeId} count=${trigger.count}/${trigger.threshold} ${(0, logging_1.describeSession)(session)}`);
|
|
237
|
+
if (this.pluginConfig.messageMode === 'exclusive')
|
|
238
|
+
return '';
|
|
239
|
+
return next();
|
|
240
|
+
}
|
|
224
241
|
if (this.transportState !== 'connected') {
|
|
225
242
|
const error = `bridge is not connected: state=${this.transportState}`;
|
|
226
243
|
this.bridge.sendFailed += 1;
|
|
@@ -232,7 +249,6 @@ class MaibotService extends koishi_1.Service {
|
|
|
232
249
|
return '';
|
|
233
250
|
return next();
|
|
234
251
|
}
|
|
235
|
-
const route = this.routes.remember(session);
|
|
236
252
|
const replyContext = this.history.resolveReplyContext(session, route);
|
|
237
253
|
const message = await (0, convert_1.sessionToMaimMessage)(session, route, this.apiKey, {
|
|
238
254
|
resolveImage: (source) => this.resolveImageToBase64(source),
|
|
@@ -260,6 +276,7 @@ class MaibotService extends koishi_1.Service {
|
|
|
260
276
|
async dispose() {
|
|
261
277
|
await this.shutdown();
|
|
262
278
|
this.routes.clear();
|
|
279
|
+
this.groupTrigger.clear();
|
|
263
280
|
this.history.clear();
|
|
264
281
|
}
|
|
265
282
|
logMessageDetail(message) {
|
package/out/types.d.ts
CHANGED
|
@@ -51,12 +51,15 @@ export interface RuntimeStatus {
|
|
|
51
51
|
koishiSent: number;
|
|
52
52
|
routeMissed: number;
|
|
53
53
|
sendFailed: number;
|
|
54
|
+
groupTriggerSkipped: number;
|
|
54
55
|
lastKoishiMessageAt?: number;
|
|
55
56
|
lastMaimSendAt?: number;
|
|
56
57
|
lastMaimMessageAt?: number;
|
|
57
58
|
lastKoishiSendAt?: number;
|
|
58
59
|
lastRouteId?: string;
|
|
59
60
|
lastMessageId?: string;
|
|
61
|
+
lastGroupTriggerCount?: number;
|
|
62
|
+
lastGroupTriggerThreshold?: number;
|
|
60
63
|
lastError?: string;
|
|
61
64
|
};
|
|
62
65
|
webui: {
|
package/package.json
CHANGED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Session } from 'koishi'
|
|
2
|
+
import type { KoishiRoute } from '../types'
|
|
3
|
+
|
|
4
|
+
export interface GroupTriggerResult {
|
|
5
|
+
shouldForward: boolean
|
|
6
|
+
count: number
|
|
7
|
+
threshold: number
|
|
8
|
+
key?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class GroupMessageTrigger {
|
|
12
|
+
private counters = new Map<string, number>()
|
|
13
|
+
|
|
14
|
+
constructor(private threshold: number) {}
|
|
15
|
+
|
|
16
|
+
test(session: Session, route: KoishiRoute): GroupTriggerResult {
|
|
17
|
+
const threshold = Math.max(1, Math.floor(this.threshold || 1))
|
|
18
|
+
if (threshold <= 1 || session.isDirect || !route.channelId) {
|
|
19
|
+
return { shouldForward: true, count: 1, threshold }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const key = this.createKey(route)
|
|
23
|
+
const count = (this.counters.get(key) || 0) + 1
|
|
24
|
+
if (count >= threshold) {
|
|
25
|
+
this.counters.delete(key)
|
|
26
|
+
return { shouldForward: true, count, threshold, key }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
this.counters.set(key, count)
|
|
30
|
+
return { shouldForward: false, count, threshold, key }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
clear() {
|
|
34
|
+
this.counters.clear()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private createKey(route: KoishiRoute) {
|
|
38
|
+
return [
|
|
39
|
+
route.platform,
|
|
40
|
+
route.botSelfId,
|
|
41
|
+
route.guildId || '',
|
|
42
|
+
route.channelId,
|
|
43
|
+
].join(':')
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/bridge/prepare.ts
CHANGED
|
@@ -158,6 +158,12 @@ export class MaibotPrepareManager {
|
|
|
158
158
|
throw new Error(`blocked: bundled patch not found: ${patchPath}`)
|
|
159
159
|
}
|
|
160
160
|
if (patchState === 'conflict') {
|
|
161
|
+
const refreshed = await this.refreshManagedPatch(patchPath)
|
|
162
|
+
if (refreshed === 'applied') return
|
|
163
|
+
if (refreshed === 'pending') {
|
|
164
|
+
await this.applyPatch(patchPath)
|
|
165
|
+
return
|
|
166
|
+
}
|
|
161
167
|
throw new Error('blocked: bundled patch cannot be applied cleanly; check MaiBot ref or update maimai-koishi.patch')
|
|
162
168
|
}
|
|
163
169
|
if (patchState === 'applied') {
|
|
@@ -166,6 +172,10 @@ export class MaibotPrepareManager {
|
|
|
166
172
|
return
|
|
167
173
|
}
|
|
168
174
|
|
|
175
|
+
await this.applyPatch(patchPath)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private async applyPatch(patchPath: string) {
|
|
169
179
|
this.pushLog(`applying bundled patch: ${patchPath}`)
|
|
170
180
|
await this.git(['apply', patchPath], this.root)
|
|
171
181
|
this.patchApplied = true
|
|
@@ -180,6 +190,68 @@ export class MaibotPrepareManager {
|
|
|
180
190
|
return 'conflict'
|
|
181
191
|
}
|
|
182
192
|
|
|
193
|
+
private async refreshManagedPatch(patchPath: string): Promise<'pending' | 'applied' | undefined> {
|
|
194
|
+
const marker = this.readMarker()
|
|
195
|
+
if (!marker?.patchApplied) return
|
|
196
|
+
if (marker.gitUrl !== this.config.maibotGitUrl || marker.gitRef !== this.config.maibotGitRef) return
|
|
197
|
+
if (!marker.patchChecksum || marker.patchChecksum === this.patchChecksum) return
|
|
198
|
+
|
|
199
|
+
const patchPaths = new Set(this.listPatchPaths(patchPath))
|
|
200
|
+
const dirtyFiles = await this.listDirtyTrackedFiles()
|
|
201
|
+
if (!dirtyFiles.length) return
|
|
202
|
+
|
|
203
|
+
const foreignFiles = dirtyFiles.filter((file) => !patchPaths.has(file))
|
|
204
|
+
if (foreignFiles.length) {
|
|
205
|
+
throw new Error(`blocked: MaiBot has local changes outside bundled patch: ${foreignFiles.slice(0, 5).join(', ')}`)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
this.pushLog('refreshing managed bundled patch')
|
|
209
|
+
await this.git(['restore', '--source', 'HEAD', '--', ...dirtyFiles], this.root)
|
|
210
|
+
|
|
211
|
+
const patchState = await this.checkPatchState(patchPath)
|
|
212
|
+
if (patchState === 'pending') return 'pending'
|
|
213
|
+
if (patchState === 'applied') {
|
|
214
|
+
this.patchApplied = true
|
|
215
|
+
this.pushLog('bundled patch already applied')
|
|
216
|
+
return 'applied'
|
|
217
|
+
}
|
|
218
|
+
throw new Error('blocked: bundled patch cannot be refreshed cleanly; check MaiBot ref or update maimai-koishi.patch')
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private readMarker(): PrepareMarker | undefined {
|
|
222
|
+
try {
|
|
223
|
+
const marker = JSON.parse(readFileSync(join(this.root, '.mai-ko-prepare.json'), 'utf8')) as PrepareMarker
|
|
224
|
+
if (!marker || typeof marker !== 'object') return
|
|
225
|
+
return marker
|
|
226
|
+
} catch {
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private listPatchPaths(patchPath: string) {
|
|
232
|
+
const paths = new Set<string>()
|
|
233
|
+
const patch = readFileSync(patchPath, 'utf8')
|
|
234
|
+
for (const line of patch.split(/\r?\n/)) {
|
|
235
|
+
const match = /^diff --git a\/(.+) b\/(.+)$/.exec(line)
|
|
236
|
+
if (!match) continue
|
|
237
|
+
if (match[2] !== '/dev/null') paths.add(match[2])
|
|
238
|
+
}
|
|
239
|
+
return [...paths]
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private async listDirtyTrackedFiles() {
|
|
243
|
+
const result = await this.git(['status', '--porcelain', '--untracked-files=no'], this.root, true, false)
|
|
244
|
+
if (result.code !== 0) return []
|
|
245
|
+
return result.stdout
|
|
246
|
+
.split(/\r?\n/)
|
|
247
|
+
.map((line) => {
|
|
248
|
+
if (!line.trim()) return ''
|
|
249
|
+
const path = line.slice(3)
|
|
250
|
+
return path.includes(' -> ') ? path.split(' -> ').pop()! : path
|
|
251
|
+
})
|
|
252
|
+
.filter(Boolean)
|
|
253
|
+
}
|
|
254
|
+
|
|
183
255
|
private async readCommit() {
|
|
184
256
|
const result = await this.git(['rev-parse', 'HEAD'], this.root, true, false)
|
|
185
257
|
return result.code === 0 ? result.stdout.trim() : undefined
|
package/src/config.ts
CHANGED
|
@@ -24,6 +24,7 @@ export interface Config {
|
|
|
24
24
|
showWebuiToken: boolean
|
|
25
25
|
messageMode: MessageMode
|
|
26
26
|
commandPrefix: string
|
|
27
|
+
groupMessageTriggerCount: number
|
|
27
28
|
imageDownloadEnabled: boolean
|
|
28
29
|
imageDownloadTimeout: number
|
|
29
30
|
imageDownloadMaxBytes: number
|
|
@@ -92,6 +93,7 @@ export const Config: Schema<Config> = Schema.intersect([
|
|
|
92
93
|
Schema.const('command').description('命令:仅命中指令前缀时转发。'),
|
|
93
94
|
]).default('coexist').description('消息转发模式。'),
|
|
94
95
|
commandPrefix: Schema.string().default('mai.ko').description('command 模式下触发 mai.ko 的文本前缀。'),
|
|
96
|
+
groupMessageTriggerCount: Schema.number().min(1).default(1).description('同一群聊累计达到多少条消息后才转发给 mai.ko 触发思考;1 表示每条群消息都转发。私聊不受影响。'),
|
|
95
97
|
imageDownloadEnabled: Schema.boolean().default(true).description('转发图片消息前由 Koishi 下载图片并转为 mai.ko 可识别的 base64 图片段。'),
|
|
96
98
|
imageDownloadTimeout: Schema.number().min(1000).default(10000).description('下载单张入站图片的超时时间,单位毫秒。'),
|
|
97
99
|
imageDownloadMaxBytes: Schema.number().min(1024).default(10 * 1024 * 1024).description('允许转发给 mai.ko 的单张图片最大字节数。'),
|
package/src/service.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { Config } from './config'
|
|
|
3
3
|
import { getFallbackRouteHints, getRouteIdFromMaim, maimMessageToFragment, sessionToMaimMessage, shouldForwardSession } from './bridge/convert'
|
|
4
4
|
import { MaibotDockerManager } from './bridge/docker'
|
|
5
5
|
import { ExternalLogForwarder } from './bridge/external-logs'
|
|
6
|
+
import { GroupMessageTrigger } from './bridge/group-trigger'
|
|
6
7
|
import { MessageHistory } from './bridge/history'
|
|
7
8
|
import { MaibotPrepareManager } from './bridge/prepare'
|
|
8
9
|
import { MaibotProcessManager, createApiKey } from './bridge/process'
|
|
@@ -19,6 +20,7 @@ export class MaibotService extends Service {
|
|
|
19
20
|
private docker: MaibotDockerManager
|
|
20
21
|
private process: MaibotProcessManager
|
|
21
22
|
private externalLogs: ExternalLogForwarder
|
|
23
|
+
private groupTrigger: GroupMessageTrigger
|
|
22
24
|
private history: MessageHistory
|
|
23
25
|
private routes: RouteRegistry
|
|
24
26
|
private transport: MaimTransport
|
|
@@ -41,6 +43,7 @@ export class MaibotService extends Service {
|
|
|
41
43
|
koishiSent: 0,
|
|
42
44
|
routeMissed: 0,
|
|
43
45
|
sendFailed: 0,
|
|
46
|
+
groupTriggerSkipped: 0,
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
constructor(public ctx: Context, private pluginConfig: Config) {
|
|
@@ -50,6 +53,7 @@ export class MaibotService extends Service {
|
|
|
50
53
|
this.docker = new MaibotDockerManager(pluginConfig, this.apiKey)
|
|
51
54
|
this.process = new MaibotProcessManager(ctx, pluginConfig, this.apiKey)
|
|
52
55
|
this.externalLogs = new ExternalLogForwarder(ctx, pluginConfig, [this.apiKey, pluginConfig.apiKey])
|
|
56
|
+
this.groupTrigger = new GroupMessageTrigger(pluginConfig.groupMessageTriggerCount)
|
|
53
57
|
this.history = new MessageHistory(pluginConfig.routeTtl)
|
|
54
58
|
this.routes = new RouteRegistry(pluginConfig.routeTtl)
|
|
55
59
|
this.transport = new MaimTransport(ctx, pluginConfig, this.apiKey, {
|
|
@@ -227,6 +231,19 @@ export class MaibotService extends Service {
|
|
|
227
231
|
this.logMessageDetail(`koishi message received: ${describeSession(session)}`)
|
|
228
232
|
|
|
229
233
|
try {
|
|
234
|
+
const route = this.routes.remember(session)
|
|
235
|
+
const trigger = this.groupTrigger.test(session, route)
|
|
236
|
+
if (!trigger.shouldForward) {
|
|
237
|
+
this.history.rememberSession(session, route)
|
|
238
|
+
this.bridge.groupTriggerSkipped += 1
|
|
239
|
+
this.bridge.lastGroupTriggerCount = trigger.count
|
|
240
|
+
this.bridge.lastGroupTriggerThreshold = trigger.threshold
|
|
241
|
+
this.bridge.lastRouteId = route.routeId
|
|
242
|
+
this.logMessageDetail(`koishi -> maimai gated: route=${route.routeId} count=${trigger.count}/${trigger.threshold} ${describeSession(session)}`)
|
|
243
|
+
if (this.pluginConfig.messageMode === 'exclusive') return ''
|
|
244
|
+
return next()
|
|
245
|
+
}
|
|
246
|
+
|
|
230
247
|
if (this.transportState !== 'connected') {
|
|
231
248
|
const error = `bridge is not connected: state=${this.transportState}`
|
|
232
249
|
this.bridge.sendFailed += 1
|
|
@@ -237,7 +254,6 @@ export class MaibotService extends Service {
|
|
|
237
254
|
return next()
|
|
238
255
|
}
|
|
239
256
|
|
|
240
|
-
const route = this.routes.remember(session)
|
|
241
257
|
const replyContext = this.history.resolveReplyContext(session, route)
|
|
242
258
|
const message = await sessionToMaimMessage(session, route, this.apiKey, {
|
|
243
259
|
resolveImage: (source) => this.resolveImageToBase64(source),
|
|
@@ -265,6 +281,7 @@ export class MaibotService extends Service {
|
|
|
265
281
|
async dispose() {
|
|
266
282
|
await this.shutdown()
|
|
267
283
|
this.routes.clear()
|
|
284
|
+
this.groupTrigger.clear()
|
|
268
285
|
this.history.clear()
|
|
269
286
|
}
|
|
270
287
|
|
package/src/types.ts
CHANGED
|
@@ -53,12 +53,15 @@ export interface RuntimeStatus {
|
|
|
53
53
|
koishiSent: number
|
|
54
54
|
routeMissed: number
|
|
55
55
|
sendFailed: number
|
|
56
|
+
groupTriggerSkipped: number
|
|
56
57
|
lastKoishiMessageAt?: number
|
|
57
58
|
lastMaimSendAt?: number
|
|
58
59
|
lastMaimMessageAt?: number
|
|
59
60
|
lastKoishiSendAt?: number
|
|
60
61
|
lastRouteId?: string
|
|
61
62
|
lastMessageId?: string
|
|
63
|
+
lastGroupTriggerCount?: number
|
|
64
|
+
lastGroupTriggerThreshold?: number
|
|
62
65
|
lastError?: string
|
|
63
66
|
}
|
|
64
67
|
webui: {
|