koishi-plugin-mai-bridge 0.1.3 → 0.1.5
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 +6 -1
- package/client/page.vue +3 -0
- package/dist/index.js +2 -2
- package/out/bridge/convert.js +2 -1
- package/out/bridge/group-trigger.d.ts +8 -0
- package/out/bridge/group-trigger.js +37 -1
- package/out/commands/index.d.ts +2 -1
- package/out/commands/index.js +40 -10
- package/out/config.d.ts +6 -1
- package/out/config.js +10 -1
- package/out/service.d.ts +1 -0
- package/out/service.js +21 -7
- package/out/types.d.ts +4 -0
- package/package.json +1 -1
- package/src/bridge/convert.ts +2 -1
- package/src/bridge/group-trigger.ts +39 -0
- package/src/commands/index.ts +38 -11
- package/src/config.ts +16 -2
- package/src/service.ts +21 -8
- package/src/types.ts +4 -0
package/README.md
CHANGED
|
@@ -59,6 +59,8 @@ webuiHost: 0.0.0.0
|
|
|
59
59
|
webuiPort: 8002
|
|
60
60
|
messageMode: coexist
|
|
61
61
|
groupMessageTriggerCount: 1
|
|
62
|
+
directMessageTriggerCount: 1
|
|
63
|
+
commandResultMode: source
|
|
62
64
|
```
|
|
63
65
|
|
|
64
66
|
说明:
|
|
@@ -68,7 +70,10 @@ groupMessageTriggerCount: 1
|
|
|
68
70
|
- `apiKey` 可以留空。插件会生成并复用运行期密钥。
|
|
69
71
|
- `dockerNetwork` 按你的 Koishi Docker 网络填写。Koishi 需要能通过 `apiHost` 访问 `maimai-ko`。
|
|
70
72
|
- `webuiPublicUrl` 可填写反代或宿主机映射后的 WebUI 地址,用于 Koishi 控制台显示入口。
|
|
71
|
-
- `groupMessageTriggerCount` 控制群聊累计多少条消息后批量转发并强制触发 maibot 思考。设为 `3` 时,同一群前两条先缓存,第 3
|
|
73
|
+
- `groupMessageTriggerCount` 控制群聊累计多少条消息后批量转发并强制触发 maibot 思考。设为 `3` 时,同一群前两条先缓存,第 3 条会连同前两条一起转发。
|
|
74
|
+
- `directMessageTriggerCount` 控制私聊累计多少条消息后批量转发并强制触发 maibot 思考。默认 `1`,表示每条私信都会触发;设为 `3` 时,同一私聊前两条只缓存,第 3 条统一转发并触发回复。
|
|
75
|
+
- `commandResultMode` 控制聊天中执行 `mai.ko.*` 管理指令后的结果发送方式。`source` 发回原群聊/私聊,`admin` 发到管理员私聊,`silent` 不发送结果。
|
|
76
|
+
- `commandResultAdminUserId` 仅在 `commandResultMode: admin` 时使用。填写管理员 QQ 号;留空时发给指令调用者。
|
|
72
77
|
|
|
73
78
|
## 消息模式
|
|
74
79
|
|
package/client/page.vue
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
<p>Bridge:{{ status?.transport.state || 'unknown' }}</p>
|
|
11
11
|
<p v-if="status?.bridge">中转:Koishi 收 {{ status.bridge.koishiReceived }} / 发往 mai.ko {{ status.bridge.maimSent }} / 收到 mai.ko {{ status.bridge.maimReceived }} / 回发 Koishi {{ status.bridge.koishiSent }}</p>
|
|
12
12
|
<p v-if="status?.bridge">中转异常:路由失败 {{ status.bridge.routeMissed }} / 发送失败 {{ status.bridge.sendFailed }}</p>
|
|
13
|
+
<p v-if="status?.bridge">触发限制:群聊跳过 {{ status.bridge.groupTriggerSkipped }} / 私信跳过 {{ status.bridge.directTriggerSkipped }}</p>
|
|
13
14
|
<p>WebUI:{{ webuiText }}</p>
|
|
14
15
|
<p v-if="status?.webui.token?.value">WebUI Token:<code>{{ status.webui.token.value }}</code></p>
|
|
15
16
|
<p v-else-if="status?.webui.token?.lastError">WebUI Token:{{ status.webui.token.lastError }}</p>
|
|
@@ -76,6 +77,8 @@ interface RuntimeStatus {
|
|
|
76
77
|
koishiSent: number
|
|
77
78
|
routeMissed: number
|
|
78
79
|
sendFailed: number
|
|
80
|
+
groupTriggerSkipped: number
|
|
81
|
+
directTriggerSkipped: number
|
|
79
82
|
lastError?: string
|
|
80
83
|
}
|
|
81
84
|
webui: {
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{defineComponent as
|
|
2
|
-
`)),1)):u("v-if",true)]}),_:1})]),_:1})}}}),
|
|
1
|
+
import{defineComponent as ee,ref as te,computed as oe,onMounted as re,resolveComponent as v,openBlock as r,createBlock as F,withCtx as a,createVNode as s,createElementVNode as k,toDisplayString as t,createElementBlock as n,createCommentVNode as u,createTextVNode as l}from"vue";import{send as d}from"@koishijs/client";const ae={key:0},ne={key:1},ue={key:2},le={key:3},ie={key:4},se={key:5},de={key:6},ke={key:7},pe={key:8},ve={key:9},ce={key:10},fe={key:12},be={key:13},me=ee({__name:"page",setup(c){const e=te(),P=oe(()=>e.value?e.value.webui.enabled?e.value.webui.url||`${e.value.webui.host}:${e.value.webui.port}`:"disabled":"unknown");async function f(){e.value=await d("mai-ko/status")}async function q(){e.value=await d("mai-ko/start")}async function z(){e.value=await d("mai-ko/stop")}async function A(){e.value=await d("mai-ko/restart")}async function G(){e.value=await d("mai-ko/reconnect")}async function J(){e.value=await d("mai-ko/prepare")}async function L(){e.value=await d("mai-ko/docker-start")}async function O(){e.value=await d("mai-ko/docker-stop")}async function Q(){e.value=await d("mai-ko/docker-restart")}function X(){var p;(p=e.value)!=null&&p.webui.url&&window.open(e.value.webui.url,"_blank","noopener,noreferrer")}return re(f),(p,o)=>{const i=v("k-button"),Y=v("k-card"),Z=v("k-layout");return r(),F(Z,null,{default:a(()=>[s(Y,null,{header:a(()=>[...o[0]||(o[0]=[k("span",null,"mai.ko 托管状态",-1)])]),default:a(()=>{var b,m,_,w,g,y,C,E,D,R,S,x,T,B,I,N,W,U,V,H,K,M,$,h,j;return[k("p",null,"进程:"+t(((b=e.value)==null?void 0:b.process.state)||"unknown"),1),k("p",null,"准备:"+t(((m=e.value)==null?void 0:m.prepare.state)||"unknown")+" / "+t(((_=e.value)==null?void 0:_.prepare.root)||"-"),1),k("p",null,"Docker:"+t(((w=e.value)==null?void 0:w.docker.state)||"unknown")+" / "+t(((g=e.value)==null?void 0:g.docker.containerName)||"-"),1),k("p",null,"Bridge:"+t(((y=e.value)==null?void 0:y.transport.state)||"unknown"),1),(C=e.value)!=null&&C.bridge?(r(),n("p",ae,"中转:Koishi 收 "+t(e.value.bridge.koishiReceived)+" / 发往 mai.ko "+t(e.value.bridge.maimSent)+" / 收到 mai.ko "+t(e.value.bridge.maimReceived)+" / 回发 Koishi "+t(e.value.bridge.koishiSent),1)):u("v-if",true),(E=e.value)!=null&&E.bridge?(r(),n("p",ne,"中转异常:路由失败 "+t(e.value.bridge.routeMissed)+" / 发送失败 "+t(e.value.bridge.sendFailed),1)):u("v-if",true),(D=e.value)!=null&&D.bridge?(r(),n("p",ue,"触发限制:群聊跳过 "+t(e.value.bridge.groupTriggerSkipped)+" / 私信跳过 "+t(e.value.bridge.directTriggerSkipped),1)):u("v-if",true),k("p",null,"WebUI:"+t(P.value),1),(S=(R=e.value)==null?void 0:R.webui.token)!=null&&S.value?(r(),n("p",le,[o[1]||(o[1]=l("WebUI Token:",-1)),k("code",null,t(e.value.webui.token.value),1)])):(T=(x=e.value)==null?void 0:x.webui.token)!=null&&T.lastError?(r(),n("p",ie,"WebUI Token:"+t(e.value.webui.token.lastError),1)):u("v-if",true),k("p",null,"PID:"+t(((B=e.value)==null?void 0:B.process.pid)||"-"),1),(I=e.value)!=null&&I.process.blockedReason?(r(),n("p",se,"阻塞原因:"+t(e.value.process.blockedReason),1)):u("v-if",true),(N=e.value)!=null&&N.prepare.blockedReason?(r(),n("p",de,"准备阻塞:"+t(e.value.prepare.blockedReason),1)):u("v-if",true),(W=e.value)!=null&&W.prepare.lastError?(r(),n("p",ke,"准备错误:"+t(e.value.prepare.lastError),1)):u("v-if",true),(U=e.value)!=null&&U.docker.lastError?(r(),n("p",pe,"Docker 错误:"+t(e.value.docker.lastError),1)):u("v-if",true),(V=e.value)!=null&&V.transport.lastError?(r(),n("p",ve,"Bridge 错误:"+t(e.value.transport.lastError),1)):u("v-if",true),(K=(H=e.value)==null?void 0:H.bridge)!=null&&K.lastError?(r(),n("p",ce,"中转错误:"+t(e.value.bridge.lastError),1)):u("v-if",true),s(i,{onClick:f},{default:a(()=>[...o[2]||(o[2]=[l("刷新",-1)])]),_:1}),s(i,{onClick:q},{default:a(()=>[...o[3]||(o[3]=[l("启动",-1)])]),_:1}),s(i,{onClick:z},{default:a(()=>[...o[4]||(o[4]=[l("停止",-1)])]),_:1}),s(i,{onClick:A},{default:a(()=>[...o[5]||(o[5]=[l("重启",-1)])]),_:1}),s(i,{onClick:G},{default:a(()=>[...o[6]||(o[6]=[l("重连",-1)])]),_:1}),s(i,{onClick:J},{default:a(()=>[...o[7]||(o[7]=[l("准备",-1)])]),_:1}),s(i,{onClick:L},{default:a(()=>[...o[8]||(o[8]=[l("Docker 启动",-1)])]),_:1}),s(i,{onClick:O},{default:a(()=>[...o[9]||(o[9]=[l("Docker 停止",-1)])]),_:1}),s(i,{onClick:Q},{default:a(()=>[...o[10]||(o[10]=[l("Docker 重启",-1)])]),_:1}),(M=e.value)!=null&&M.webui.enabled&&(($=e.value)!=null&&$.webui.url)?(r(),F(i,{key:11,onClick:X},{default:a(()=>[...o[11]||(o[11]=[l("打开 WebUI",-1)])]),_:1})):u("v-if",true),(h=e.value)!=null&&h.logsHint?(r(),n("p",fe,t(e.value.logsHint),1)):u("v-if",true),(j=e.value)!=null&&j.logs.length?(r(),n("pre",be,t(e.value.logs.slice(-20).join(`
|
|
2
|
+
`)),1)):u("v-if",true)]}),_:1})]),_:1})}}}),ge=c=>{c.page({name:"mai.ko",path:"/mai-ko",authority:3,component:me})};export{ge as default};
|
package/out/bridge/convert.js
CHANGED
|
@@ -204,7 +204,8 @@ async function sessionToMaimMessage(session, route, apiKey, options = {}) {
|
|
|
204
204
|
koishi_reply_to_message_id: replyContext?.targetMessageId,
|
|
205
205
|
koishi_reply_context_count: replyContext?.contextCount,
|
|
206
206
|
koishi_is_direct: !!session.isDirect,
|
|
207
|
-
koishi_group_trigger_forced: forceMention,
|
|
207
|
+
koishi_group_trigger_forced: !session.isDirect && forceMention,
|
|
208
|
+
koishi_direct_trigger_forced: !!session.isDirect && forceMention,
|
|
208
209
|
at_bot: mentioned,
|
|
209
210
|
is_mentioned: mentioned,
|
|
210
211
|
},
|
|
@@ -20,3 +20,11 @@ export declare class GroupMessageTrigger {
|
|
|
20
20
|
clear(): void;
|
|
21
21
|
private createKey;
|
|
22
22
|
}
|
|
23
|
+
export declare class DirectMessageTrigger {
|
|
24
|
+
private threshold;
|
|
25
|
+
private pending;
|
|
26
|
+
constructor(threshold: number);
|
|
27
|
+
test(session: Session, route: KoishiRoute): GroupTriggerResult;
|
|
28
|
+
clear(): void;
|
|
29
|
+
private createKey;
|
|
30
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.GroupMessageTrigger = void 0;
|
|
3
|
+
exports.DirectMessageTrigger = exports.GroupMessageTrigger = void 0;
|
|
4
4
|
class GroupMessageTrigger {
|
|
5
5
|
threshold;
|
|
6
6
|
pending = new Map();
|
|
@@ -35,3 +35,39 @@ class GroupMessageTrigger {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
exports.GroupMessageTrigger = GroupMessageTrigger;
|
|
38
|
+
class DirectMessageTrigger {
|
|
39
|
+
threshold;
|
|
40
|
+
pending = new Map();
|
|
41
|
+
constructor(threshold) {
|
|
42
|
+
this.threshold = threshold;
|
|
43
|
+
}
|
|
44
|
+
test(session, route) {
|
|
45
|
+
const threshold = Math.max(1, Math.floor(this.threshold || 1));
|
|
46
|
+
const entry = { session, route };
|
|
47
|
+
if (!session.isDirect || !route.userId) {
|
|
48
|
+
return { shouldForward: true, count: 1, threshold, entries: [entry] };
|
|
49
|
+
}
|
|
50
|
+
if (threshold <= 1) {
|
|
51
|
+
return { shouldForward: true, count: 1, threshold, entries: [entry], forceMention: true };
|
|
52
|
+
}
|
|
53
|
+
const key = this.createKey(route);
|
|
54
|
+
const entries = [...this.pending.get(key) || [], entry];
|
|
55
|
+
if (entries.length >= threshold) {
|
|
56
|
+
this.pending.delete(key);
|
|
57
|
+
return { shouldForward: true, count: entries.length, threshold, key, entries, forceMention: true };
|
|
58
|
+
}
|
|
59
|
+
this.pending.set(key, entries);
|
|
60
|
+
return { shouldForward: false, count: entries.length, threshold, key, entries: [] };
|
|
61
|
+
}
|
|
62
|
+
clear() {
|
|
63
|
+
this.pending.clear();
|
|
64
|
+
}
|
|
65
|
+
createKey(route) {
|
|
66
|
+
return [
|
|
67
|
+
route.platform,
|
|
68
|
+
route.botSelfId,
|
|
69
|
+
route.userId,
|
|
70
|
+
].join(':');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
exports.DirectMessageTrigger = DirectMessageTrigger;
|
package/out/commands/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Context, type Fragment, type Session } from 'koishi';
|
|
2
2
|
import type { Config } from '../config';
|
|
3
3
|
import type { RuntimeStatus } from '../types';
|
|
4
4
|
export declare function renderStatus(status: RuntimeStatus): string;
|
|
5
|
+
export declare function sendCommandResult(session: Session, config: Config, content: string): Promise<Fragment | void>;
|
|
5
6
|
export declare function registerCommands(ctx: Context, config: Config): void;
|
package/out/commands/index.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.renderStatus = renderStatus;
|
|
4
|
+
exports.sendCommandResult = sendCommandResult;
|
|
4
5
|
exports.registerCommands = registerCommands;
|
|
6
|
+
const koishi_1 = require("koishi");
|
|
7
|
+
const logger = new koishi_1.Logger('mai.ko/commands');
|
|
5
8
|
function formatTime(value) {
|
|
6
9
|
return value ? new Date(value).toLocaleString() : '-';
|
|
7
10
|
}
|
|
@@ -16,6 +19,7 @@ function renderStatus(status) {
|
|
|
16
19
|
`重连次数: ${status.transport.reconnectAttempts}`,
|
|
17
20
|
`中转: Koishi收=${status.bridge.koishiReceived} / 发往mai.ko=${status.bridge.maimSent} / 收到mai.ko=${status.bridge.maimReceived} / 回发Koishi=${status.bridge.koishiSent}`,
|
|
18
21
|
`中转异常: 路由失败=${status.bridge.routeMissed} / 发送失败=${status.bridge.sendFailed}`,
|
|
22
|
+
`触发限制: 群聊跳过=${status.bridge.groupTriggerSkipped} / 私信跳过=${status.bridge.directTriggerSkipped}`,
|
|
19
23
|
`启动时间: ${formatTime(status.process.startedAt)}`,
|
|
20
24
|
];
|
|
21
25
|
if (status.process.blockedReason)
|
|
@@ -38,27 +42,53 @@ function renderStatus(status) {
|
|
|
38
42
|
}
|
|
39
43
|
return lines.join('\n');
|
|
40
44
|
}
|
|
45
|
+
async function sendCommandResult(session, config, content) {
|
|
46
|
+
if (config.commandResultMode === 'silent')
|
|
47
|
+
return;
|
|
48
|
+
if (config.commandResultMode !== 'admin')
|
|
49
|
+
return content;
|
|
50
|
+
const userId = config.commandResultAdminUserId.trim() || session.userId;
|
|
51
|
+
if (!userId)
|
|
52
|
+
return 'mai.ko 指令结果发送失败:没有可用的管理员用户 ID。';
|
|
53
|
+
if (!session.bot?.sendPrivateMessage)
|
|
54
|
+
return 'mai.ko 指令结果发送失败:当前平台不支持私聊发送。';
|
|
55
|
+
try {
|
|
56
|
+
const guildId = config.commandResultAdminGuildId.trim() || undefined;
|
|
57
|
+
await session.bot.sendPrivateMessage(userId, content, guildId, { session });
|
|
58
|
+
if (config.commandResultNotifySource)
|
|
59
|
+
return 'mai.ko 指令结果已发送给管理员。';
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
63
|
+
logger.warn(`send command result failed: user=${userId} error=${message}`);
|
|
64
|
+
return `mai.ko 指令结果发送失败:${message}`;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function handleCommandResult(session, config, status) {
|
|
68
|
+
const content = renderStatus(await status);
|
|
69
|
+
return session ? sendCommandResult(session, config, content) : content;
|
|
70
|
+
}
|
|
41
71
|
function registerCommands(ctx, config) {
|
|
42
72
|
const options = { authority: config.commandAuthority };
|
|
43
73
|
ctx.command('mai.ko', '查看 mai.ko 托管状态', options)
|
|
44
74
|
.alias('maibot')
|
|
45
|
-
.action(() =>
|
|
75
|
+
.action((argv) => handleCommandResult(argv.session, config, ctx.maimai.getStatus()));
|
|
46
76
|
ctx.command('mai.ko.status', '查看 mai.ko 托管状态', options)
|
|
47
|
-
.action(() =>
|
|
77
|
+
.action((argv) => handleCommandResult(argv.session, config, ctx.maimai.getStatus()));
|
|
48
78
|
ctx.command('mai.ko.start', '启动 mai.ko', options)
|
|
49
|
-
.action(
|
|
79
|
+
.action((argv) => handleCommandResult(argv.session, config, ctx.maimai.launch()));
|
|
50
80
|
ctx.command('mai.ko.stop', '停止 mai.ko', options)
|
|
51
|
-
.action(
|
|
81
|
+
.action((argv) => handleCommandResult(argv.session, config, ctx.maimai.shutdown()));
|
|
52
82
|
ctx.command('mai.ko.restart', '重启 mai.ko', options)
|
|
53
|
-
.action(
|
|
83
|
+
.action((argv) => handleCommandResult(argv.session, config, ctx.maimai.restart()));
|
|
54
84
|
ctx.command('mai.ko.reconnect', '重连 mai.ko Bridge', options)
|
|
55
|
-
.action(
|
|
85
|
+
.action((argv) => handleCommandResult(argv.session, config, ctx.maimai.reconnect()));
|
|
56
86
|
ctx.command('mai.ko.prepare', '准备 MaiBot 源码、补丁与 Docker 镜像', options)
|
|
57
|
-
.action(
|
|
87
|
+
.action((argv) => handleCommandResult(argv.session, config, ctx.maimai.prepare()));
|
|
58
88
|
ctx.command('mai.ko.docker.start', '启动 maimai-ko Docker 容器', options)
|
|
59
|
-
.action(
|
|
89
|
+
.action((argv) => handleCommandResult(argv.session, config, ctx.maimai.dockerStart()));
|
|
60
90
|
ctx.command('mai.ko.docker.stop', '停止 maimai-ko Docker 容器', options)
|
|
61
|
-
.action(
|
|
91
|
+
.action((argv) => handleCommandResult(argv.session, config, ctx.maimai.dockerStop()));
|
|
62
92
|
ctx.command('mai.ko.docker.restart', '重建并重启 maimai-ko Docker 容器', options)
|
|
63
|
-
.action(
|
|
93
|
+
.action((argv) => handleCommandResult(argv.session, config, ctx.maimai.dockerRestart()));
|
|
64
94
|
}
|
package/out/config.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Schema } from 'koishi';
|
|
2
|
-
import type { MessageLogLevel, MessageMode, ProcessMode } from './types';
|
|
2
|
+
import type { CommandResultMode, MessageLogLevel, MessageMode, ProcessMode } from './types';
|
|
3
3
|
export interface Config {
|
|
4
4
|
processMode: ProcessMode;
|
|
5
5
|
maibotRoot: string;
|
|
@@ -24,11 +24,16 @@ export interface Config {
|
|
|
24
24
|
messageMode: MessageMode;
|
|
25
25
|
commandPrefix: string;
|
|
26
26
|
groupMessageTriggerCount: number;
|
|
27
|
+
directMessageTriggerCount: number;
|
|
27
28
|
imageDownloadEnabled: boolean;
|
|
28
29
|
imageDownloadTimeout: number;
|
|
29
30
|
imageDownloadMaxBytes: number;
|
|
30
31
|
messageLogLevel: MessageLogLevel;
|
|
31
32
|
commandAuthority: number;
|
|
33
|
+
commandResultMode: CommandResultMode;
|
|
34
|
+
commandResultAdminUserId: string;
|
|
35
|
+
commandResultAdminGuildId: string;
|
|
36
|
+
commandResultNotifySource: boolean;
|
|
32
37
|
acceptMaibotAgreements: boolean;
|
|
33
38
|
startupTimeout: number;
|
|
34
39
|
shutdownTimeout: number;
|
package/out/config.js
CHANGED
|
@@ -43,7 +43,8 @@ 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
|
+
groupMessageTriggerCount: koishi_1.Schema.number().min(1).default(1).description('同一群聊累计达到多少条消息后批量转发并强制触发 mai.ko 思考;1 表示每条群消息都转发。'),
|
|
47
|
+
directMessageTriggerCount: koishi_1.Schema.number().min(1).default(1).description('同一私聊累计达到多少条消息后批量转发并强制触发 mai.ko 思考;1 表示每条私信都转发。'),
|
|
47
48
|
imageDownloadEnabled: koishi_1.Schema.boolean().default(true).description('转发图片消息前由 Koishi 下载图片并转为 mai.ko 可识别的 base64 图片段。'),
|
|
48
49
|
imageDownloadTimeout: koishi_1.Schema.number().min(1000).default(10000).description('下载单张入站图片的超时时间,单位毫秒。'),
|
|
49
50
|
imageDownloadMaxBytes: koishi_1.Schema.number().min(1024).default(10 * 1024 * 1024).description('允许转发给 mai.ko 的单张图片最大字节数。'),
|
|
@@ -53,6 +54,14 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
53
54
|
koishi_1.Schema.const('detail').description('详细:输出每一步消息中转细节。'),
|
|
54
55
|
]).default('summary').description('mai.ko 消息中转日志详细程度。'),
|
|
55
56
|
commandAuthority: koishi_1.Schema.number().min(0).max(5).default(3).description('管理指令需要的权限等级。'),
|
|
57
|
+
commandResultMode: koishi_1.Schema.union([
|
|
58
|
+
koishi_1.Schema.const('source').description('原会话:指令结果发送到触发指令的群聊或私聊。'),
|
|
59
|
+
koishi_1.Schema.const('admin').description('管理员私聊:指令结果发送到指定管理员;未填写管理员时发送给指令调用者。'),
|
|
60
|
+
koishi_1.Schema.const('silent').description('静默:执行指令但不发送结果。'),
|
|
61
|
+
]).default('source').description('聊天中执行 mai.ko 管理指令后的结果发送方式。'),
|
|
62
|
+
commandResultAdminUserId: koishi_1.Schema.string().default('').description('管理员用户 ID。仅 commandResultMode=admin 时使用;留空时发送给指令调用者。'),
|
|
63
|
+
commandResultAdminGuildId: koishi_1.Schema.string().default('').description('发送管理员私聊时附带的群组/频道 ID。通常可留空。'),
|
|
64
|
+
commandResultNotifySource: koishi_1.Schema.boolean().default(false).description('结果发送给管理员后,是否在原会话发送一条简短提示。'),
|
|
56
65
|
}).description('消息路由'),
|
|
57
66
|
koishi_1.Schema.object({
|
|
58
67
|
startupTimeout: koishi_1.Schema.number().min(1000).default(60000).description('等待 mai.ko API 可连接的超时时间,单位毫秒。'),
|
package/out/service.d.ts
CHANGED
package/out/service.js
CHANGED
|
@@ -23,6 +23,7 @@ class MaibotService extends koishi_1.Service {
|
|
|
23
23
|
process;
|
|
24
24
|
externalLogs;
|
|
25
25
|
groupTrigger;
|
|
26
|
+
directTrigger;
|
|
26
27
|
history;
|
|
27
28
|
routes;
|
|
28
29
|
transport;
|
|
@@ -46,6 +47,7 @@ class MaibotService extends koishi_1.Service {
|
|
|
46
47
|
routeMissed: 0,
|
|
47
48
|
sendFailed: 0,
|
|
48
49
|
groupTriggerSkipped: 0,
|
|
50
|
+
directTriggerSkipped: 0,
|
|
49
51
|
};
|
|
50
52
|
constructor(ctx, pluginConfig) {
|
|
51
53
|
super(ctx, 'maimai');
|
|
@@ -57,6 +59,7 @@ class MaibotService extends koishi_1.Service {
|
|
|
57
59
|
this.process = new process_1.MaibotProcessManager(ctx, pluginConfig, this.apiKey);
|
|
58
60
|
this.externalLogs = new external_logs_1.ExternalLogForwarder(ctx, pluginConfig, [this.apiKey, pluginConfig.apiKey]);
|
|
59
61
|
this.groupTrigger = new group_trigger_1.GroupMessageTrigger(pluginConfig.groupMessageTriggerCount);
|
|
62
|
+
this.directTrigger = new group_trigger_1.DirectMessageTrigger(pluginConfig.directMessageTriggerCount);
|
|
60
63
|
this.history = new history_1.MessageHistory(pluginConfig.routeTtl);
|
|
61
64
|
this.routes = new routes_1.RouteRegistry(pluginConfig.routeTtl);
|
|
62
65
|
this.transport = new transport_1.MaimTransport(ctx, pluginConfig, this.apiKey, {
|
|
@@ -226,14 +229,24 @@ class MaibotService extends koishi_1.Service {
|
|
|
226
229
|
this.logMessageDetail(`koishi message received: ${(0, logging_1.describeSession)(session)}`);
|
|
227
230
|
try {
|
|
228
231
|
const route = this.routes.remember(session);
|
|
229
|
-
const
|
|
232
|
+
const triggerKind = session.isDirect ? 'direct' : 'group';
|
|
233
|
+
const trigger = session.isDirect
|
|
234
|
+
? this.directTrigger.test(session, route)
|
|
235
|
+
: this.groupTrigger.test(session, route);
|
|
230
236
|
if (!trigger.shouldForward) {
|
|
231
237
|
this.history.rememberSession(session, route);
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
238
|
+
if (triggerKind === 'direct') {
|
|
239
|
+
this.bridge.directTriggerSkipped += 1;
|
|
240
|
+
this.bridge.lastDirectTriggerCount = trigger.count;
|
|
241
|
+
this.bridge.lastDirectTriggerThreshold = trigger.threshold;
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
this.bridge.groupTriggerSkipped += 1;
|
|
245
|
+
this.bridge.lastGroupTriggerCount = trigger.count;
|
|
246
|
+
this.bridge.lastGroupTriggerThreshold = trigger.threshold;
|
|
247
|
+
}
|
|
235
248
|
this.bridge.lastRouteId = route.routeId;
|
|
236
|
-
this.logMessageDetail(`koishi -> maimai gated: route=${route.routeId} count=${trigger.count}/${trigger.threshold} ${(0, logging_1.describeSession)(session)}`);
|
|
249
|
+
this.logMessageDetail(`koishi -> maimai gated: kind=${triggerKind} route=${route.routeId} count=${trigger.count}/${trigger.threshold} ${(0, logging_1.describeSession)(session)}`);
|
|
237
250
|
if (this.pluginConfig.messageMode === 'exclusive')
|
|
238
251
|
return '';
|
|
239
252
|
return next();
|
|
@@ -251,7 +264,7 @@ class MaibotService extends koishi_1.Service {
|
|
|
251
264
|
}
|
|
252
265
|
const entries = trigger.entries.length ? trigger.entries : [{ session, route }];
|
|
253
266
|
if (trigger.forceMention) {
|
|
254
|
-
this.logMessageSummary(
|
|
267
|
+
this.logMessageSummary(`${triggerKind} trigger reached: route=${route.routeId} count=${trigger.count}/${trigger.threshold} flushing=${entries.length}`);
|
|
255
268
|
}
|
|
256
269
|
for (let index = 0; index < entries.length; index++) {
|
|
257
270
|
const entry = entries[index];
|
|
@@ -283,13 +296,14 @@ class MaibotService extends koishi_1.Service {
|
|
|
283
296
|
this.bridge.lastRouteId = route.routeId;
|
|
284
297
|
this.bridge.lastMessageId = message.message_info.message_id;
|
|
285
298
|
const replyLog = replyContext ? ` reply=${replyContext.targetMessageId} context=${replyContext.contextCount || 0}` : '';
|
|
286
|
-
const forceLog = forceMention ? ' forced=
|
|
299
|
+
const forceLog = forceMention ? ' forced=message-trigger' : '';
|
|
287
300
|
this.logMessageDetail(`koishi -> maimai forwarded: route=${route.routeId}${replyLog}${forceLog} ${(0, logging_1.describeSegment)(message.message_segment)}`);
|
|
288
301
|
}
|
|
289
302
|
async dispose() {
|
|
290
303
|
await this.shutdown();
|
|
291
304
|
this.routes.clear();
|
|
292
305
|
this.groupTrigger.clear();
|
|
306
|
+
this.directTrigger.clear();
|
|
293
307
|
this.history.clear();
|
|
294
308
|
}
|
|
295
309
|
logMessageDetail(message) {
|
package/out/types.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Awaitable, Context, Fragment, Session } from 'koishi';
|
|
2
2
|
export type MessageMode = 'coexist' | 'exclusive' | 'command';
|
|
3
|
+
export type CommandResultMode = 'source' | 'admin' | 'silent';
|
|
3
4
|
export type ProcessMode = 'docker' | 'managed' | 'external';
|
|
4
5
|
export type ProcessState = 'idle' | 'starting' | 'running' | 'stopping' | 'stopped' | 'blocked' | 'error';
|
|
5
6
|
export type TransportState = 'idle' | 'connecting' | 'connected' | 'disconnected' | 'error';
|
|
@@ -52,6 +53,7 @@ export interface RuntimeStatus {
|
|
|
52
53
|
routeMissed: number;
|
|
53
54
|
sendFailed: number;
|
|
54
55
|
groupTriggerSkipped: number;
|
|
56
|
+
directTriggerSkipped: number;
|
|
55
57
|
lastKoishiMessageAt?: number;
|
|
56
58
|
lastMaimSendAt?: number;
|
|
57
59
|
lastMaimMessageAt?: number;
|
|
@@ -60,6 +62,8 @@ export interface RuntimeStatus {
|
|
|
60
62
|
lastMessageId?: string;
|
|
61
63
|
lastGroupTriggerCount?: number;
|
|
62
64
|
lastGroupTriggerThreshold?: number;
|
|
65
|
+
lastDirectTriggerCount?: number;
|
|
66
|
+
lastDirectTriggerThreshold?: number;
|
|
63
67
|
lastError?: string;
|
|
64
68
|
};
|
|
65
69
|
webui: {
|
package/package.json
CHANGED
package/src/bridge/convert.ts
CHANGED
|
@@ -228,7 +228,8 @@ export async function sessionToMaimMessage(
|
|
|
228
228
|
koishi_reply_to_message_id: replyContext?.targetMessageId,
|
|
229
229
|
koishi_reply_context_count: replyContext?.contextCount,
|
|
230
230
|
koishi_is_direct: !!session.isDirect,
|
|
231
|
-
koishi_group_trigger_forced: forceMention,
|
|
231
|
+
koishi_group_trigger_forced: !session.isDirect && forceMention,
|
|
232
|
+
koishi_direct_trigger_forced: !!session.isDirect && forceMention,
|
|
232
233
|
at_bot: mentioned,
|
|
233
234
|
is_mentioned: mentioned,
|
|
234
235
|
},
|
|
@@ -51,3 +51,42 @@ export class GroupMessageTrigger {
|
|
|
51
51
|
].join(':')
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
|
+
|
|
55
|
+
export class DirectMessageTrigger {
|
|
56
|
+
private pending = new Map<string, GroupTriggerEntry[]>()
|
|
57
|
+
|
|
58
|
+
constructor(private threshold: number) {}
|
|
59
|
+
|
|
60
|
+
test(session: Session, route: KoishiRoute): GroupTriggerResult {
|
|
61
|
+
const threshold = Math.max(1, Math.floor(this.threshold || 1))
|
|
62
|
+
const entry = { session, route }
|
|
63
|
+
if (!session.isDirect || !route.userId) {
|
|
64
|
+
return { shouldForward: true, count: 1, threshold, entries: [entry] }
|
|
65
|
+
}
|
|
66
|
+
if (threshold <= 1) {
|
|
67
|
+
return { shouldForward: true, count: 1, threshold, entries: [entry], forceMention: true }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const key = this.createKey(route)
|
|
71
|
+
const entries = [...this.pending.get(key) || [], entry]
|
|
72
|
+
if (entries.length >= threshold) {
|
|
73
|
+
this.pending.delete(key)
|
|
74
|
+
return { shouldForward: true, count: entries.length, threshold, key, entries, forceMention: true }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this.pending.set(key, entries)
|
|
78
|
+
return { shouldForward: false, count: entries.length, threshold, key, entries: [] }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
clear() {
|
|
82
|
+
this.pending.clear()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private createKey(route: KoishiRoute) {
|
|
86
|
+
return [
|
|
87
|
+
route.platform,
|
|
88
|
+
route.botSelfId,
|
|
89
|
+
route.userId,
|
|
90
|
+
].join(':')
|
|
91
|
+
}
|
|
92
|
+
}
|
package/src/commands/index.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { Logger, type Awaitable, type Context, type Fragment, type Session } from 'koishi'
|
|
2
2
|
import type { Config } from '../config'
|
|
3
3
|
import type { RuntimeStatus } from '../types'
|
|
4
4
|
|
|
5
|
+
const logger = new Logger('mai.ko/commands')
|
|
6
|
+
|
|
5
7
|
function formatTime(value?: number) {
|
|
6
8
|
return value ? new Date(value).toLocaleString() : '-'
|
|
7
9
|
}
|
|
@@ -17,6 +19,7 @@ export function renderStatus(status: RuntimeStatus) {
|
|
|
17
19
|
`重连次数: ${status.transport.reconnectAttempts}`,
|
|
18
20
|
`中转: Koishi收=${status.bridge.koishiReceived} / 发往mai.ko=${status.bridge.maimSent} / 收到mai.ko=${status.bridge.maimReceived} / 回发Koishi=${status.bridge.koishiSent}`,
|
|
19
21
|
`中转异常: 路由失败=${status.bridge.routeMissed} / 发送失败=${status.bridge.sendFailed}`,
|
|
22
|
+
`触发限制: 群聊跳过=${status.bridge.groupTriggerSkipped} / 私信跳过=${status.bridge.directTriggerSkipped}`,
|
|
20
23
|
`启动时间: ${formatTime(status.process.startedAt)}`,
|
|
21
24
|
]
|
|
22
25
|
if (status.process.blockedReason) lines.push(`阻塞原因: ${status.process.blockedReason}`)
|
|
@@ -33,37 +36,61 @@ export function renderStatus(status: RuntimeStatus) {
|
|
|
33
36
|
return lines.join('\n')
|
|
34
37
|
}
|
|
35
38
|
|
|
39
|
+
export async function sendCommandResult(session: Session, config: Config, content: string): Promise<Fragment | void> {
|
|
40
|
+
if (config.commandResultMode === 'silent') return
|
|
41
|
+
if (config.commandResultMode !== 'admin') return content
|
|
42
|
+
|
|
43
|
+
const userId = config.commandResultAdminUserId.trim() || session.userId
|
|
44
|
+
if (!userId) return 'mai.ko 指令结果发送失败:没有可用的管理员用户 ID。'
|
|
45
|
+
if (!session.bot?.sendPrivateMessage) return 'mai.ko 指令结果发送失败:当前平台不支持私聊发送。'
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const guildId = config.commandResultAdminGuildId.trim() || undefined
|
|
49
|
+
await session.bot.sendPrivateMessage(userId, content, guildId, { session })
|
|
50
|
+
if (config.commandResultNotifySource) return 'mai.ko 指令结果已发送给管理员。'
|
|
51
|
+
} catch (error) {
|
|
52
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
53
|
+
logger.warn(`send command result failed: user=${userId} error=${message}`)
|
|
54
|
+
return `mai.ko 指令结果发送失败:${message}`
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function handleCommandResult(session: Session | undefined, config: Config, status: Awaitable<RuntimeStatus>) {
|
|
59
|
+
const content = renderStatus(await status)
|
|
60
|
+
return session ? sendCommandResult(session, config, content) : content
|
|
61
|
+
}
|
|
62
|
+
|
|
36
63
|
export function registerCommands(ctx: Context, config: Config) {
|
|
37
64
|
const options = { authority: config.commandAuthority }
|
|
38
65
|
|
|
39
66
|
ctx.command('mai.ko', '查看 mai.ko 托管状态', options)
|
|
40
67
|
.alias('maibot')
|
|
41
|
-
.action(() =>
|
|
68
|
+
.action((argv) => handleCommandResult(argv.session, config, ctx.maimai.getStatus()))
|
|
42
69
|
|
|
43
70
|
ctx.command('mai.ko.status', '查看 mai.ko 托管状态', options)
|
|
44
|
-
.action(() =>
|
|
71
|
+
.action((argv) => handleCommandResult(argv.session, config, ctx.maimai.getStatus()))
|
|
45
72
|
|
|
46
73
|
ctx.command('mai.ko.start', '启动 mai.ko', options)
|
|
47
|
-
.action(
|
|
74
|
+
.action((argv) => handleCommandResult(argv.session, config, ctx.maimai.launch()))
|
|
48
75
|
|
|
49
76
|
ctx.command('mai.ko.stop', '停止 mai.ko', options)
|
|
50
|
-
.action(
|
|
77
|
+
.action((argv) => handleCommandResult(argv.session, config, ctx.maimai.shutdown()))
|
|
51
78
|
|
|
52
79
|
ctx.command('mai.ko.restart', '重启 mai.ko', options)
|
|
53
|
-
.action(
|
|
80
|
+
.action((argv) => handleCommandResult(argv.session, config, ctx.maimai.restart()))
|
|
54
81
|
|
|
55
82
|
ctx.command('mai.ko.reconnect', '重连 mai.ko Bridge', options)
|
|
56
|
-
.action(
|
|
83
|
+
.action((argv) => handleCommandResult(argv.session, config, ctx.maimai.reconnect()))
|
|
57
84
|
|
|
58
85
|
ctx.command('mai.ko.prepare', '准备 MaiBot 源码、补丁与 Docker 镜像', options)
|
|
59
|
-
.action(
|
|
86
|
+
.action((argv) => handleCommandResult(argv.session, config, ctx.maimai.prepare()))
|
|
60
87
|
|
|
61
88
|
ctx.command('mai.ko.docker.start', '启动 maimai-ko Docker 容器', options)
|
|
62
|
-
.action(
|
|
89
|
+
.action((argv) => handleCommandResult(argv.session, config, ctx.maimai.dockerStart()))
|
|
63
90
|
|
|
64
91
|
ctx.command('mai.ko.docker.stop', '停止 maimai-ko Docker 容器', options)
|
|
65
|
-
.action(
|
|
92
|
+
.action((argv) => handleCommandResult(argv.session, config, ctx.maimai.dockerStop()))
|
|
66
93
|
|
|
67
94
|
ctx.command('mai.ko.docker.restart', '重建并重启 maimai-ko Docker 容器', options)
|
|
68
|
-
.action(
|
|
95
|
+
.action((argv) => handleCommandResult(argv.session, config, ctx.maimai.dockerRestart()))
|
|
69
96
|
}
|
package/src/config.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Schema } from 'koishi'
|
|
2
|
-
import type { MessageLogLevel, MessageMode, ProcessMode } from './types'
|
|
2
|
+
import type { CommandResultMode, MessageLogLevel, MessageMode, ProcessMode } from './types'
|
|
3
3
|
|
|
4
4
|
export interface Config {
|
|
5
5
|
processMode: ProcessMode
|
|
@@ -25,11 +25,16 @@ export interface Config {
|
|
|
25
25
|
messageMode: MessageMode
|
|
26
26
|
commandPrefix: string
|
|
27
27
|
groupMessageTriggerCount: number
|
|
28
|
+
directMessageTriggerCount: number
|
|
28
29
|
imageDownloadEnabled: boolean
|
|
29
30
|
imageDownloadTimeout: number
|
|
30
31
|
imageDownloadMaxBytes: number
|
|
31
32
|
messageLogLevel: MessageLogLevel
|
|
32
33
|
commandAuthority: number
|
|
34
|
+
commandResultMode: CommandResultMode
|
|
35
|
+
commandResultAdminUserId: string
|
|
36
|
+
commandResultAdminGuildId: string
|
|
37
|
+
commandResultNotifySource: boolean
|
|
33
38
|
acceptMaibotAgreements: boolean
|
|
34
39
|
startupTimeout: number
|
|
35
40
|
shutdownTimeout: number
|
|
@@ -93,7 +98,8 @@ export const Config: Schema<Config> = Schema.intersect([
|
|
|
93
98
|
Schema.const('command').description('命令:仅命中指令前缀时转发。'),
|
|
94
99
|
]).default('coexist').description('消息转发模式。'),
|
|
95
100
|
commandPrefix: Schema.string().default('mai.ko').description('command 模式下触发 mai.ko 的文本前缀。'),
|
|
96
|
-
groupMessageTriggerCount: Schema.number().min(1).default(1).description('同一群聊累计达到多少条消息后批量转发并强制触发 mai.ko 思考;1
|
|
101
|
+
groupMessageTriggerCount: Schema.number().min(1).default(1).description('同一群聊累计达到多少条消息后批量转发并强制触发 mai.ko 思考;1 表示每条群消息都转发。'),
|
|
102
|
+
directMessageTriggerCount: Schema.number().min(1).default(1).description('同一私聊累计达到多少条消息后批量转发并强制触发 mai.ko 思考;1 表示每条私信都转发。'),
|
|
97
103
|
imageDownloadEnabled: Schema.boolean().default(true).description('转发图片消息前由 Koishi 下载图片并转为 mai.ko 可识别的 base64 图片段。'),
|
|
98
104
|
imageDownloadTimeout: Schema.number().min(1000).default(10000).description('下载单张入站图片的超时时间,单位毫秒。'),
|
|
99
105
|
imageDownloadMaxBytes: Schema.number().min(1024).default(10 * 1024 * 1024).description('允许转发给 mai.ko 的单张图片最大字节数。'),
|
|
@@ -103,6 +109,14 @@ export const Config: Schema<Config> = Schema.intersect([
|
|
|
103
109
|
Schema.const('detail').description('详细:输出每一步消息中转细节。'),
|
|
104
110
|
]).default('summary').description('mai.ko 消息中转日志详细程度。'),
|
|
105
111
|
commandAuthority: Schema.number().min(0).max(5).default(3).description('管理指令需要的权限等级。'),
|
|
112
|
+
commandResultMode: Schema.union([
|
|
113
|
+
Schema.const('source').description('原会话:指令结果发送到触发指令的群聊或私聊。'),
|
|
114
|
+
Schema.const('admin').description('管理员私聊:指令结果发送到指定管理员;未填写管理员时发送给指令调用者。'),
|
|
115
|
+
Schema.const('silent').description('静默:执行指令但不发送结果。'),
|
|
116
|
+
]).default('source').description('聊天中执行 mai.ko 管理指令后的结果发送方式。'),
|
|
117
|
+
commandResultAdminUserId: Schema.string().default('').description('管理员用户 ID。仅 commandResultMode=admin 时使用;留空时发送给指令调用者。'),
|
|
118
|
+
commandResultAdminGuildId: Schema.string().default('').description('发送管理员私聊时附带的群组/频道 ID。通常可留空。'),
|
|
119
|
+
commandResultNotifySource: Schema.boolean().default(false).description('结果发送给管理员后,是否在原会话发送一条简短提示。'),
|
|
106
120
|
}).description('消息路由'),
|
|
107
121
|
Schema.object({
|
|
108
122
|
startupTimeout: Schema.number().min(1000).default(60000).description('等待 mai.ko API 可连接的超时时间,单位毫秒。'),
|
package/src/service.ts
CHANGED
|
@@ -3,7 +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
|
+
import { DirectMessageTrigger, GroupMessageTrigger } from './bridge/group-trigger'
|
|
7
7
|
import { MessageHistory } from './bridge/history'
|
|
8
8
|
import { MaibotPrepareManager } from './bridge/prepare'
|
|
9
9
|
import { MaibotProcessManager, createApiKey } from './bridge/process'
|
|
@@ -21,6 +21,7 @@ export class MaibotService extends Service {
|
|
|
21
21
|
private process: MaibotProcessManager
|
|
22
22
|
private externalLogs: ExternalLogForwarder
|
|
23
23
|
private groupTrigger: GroupMessageTrigger
|
|
24
|
+
private directTrigger: DirectMessageTrigger
|
|
24
25
|
private history: MessageHistory
|
|
25
26
|
private routes: RouteRegistry
|
|
26
27
|
private transport: MaimTransport
|
|
@@ -44,6 +45,7 @@ export class MaibotService extends Service {
|
|
|
44
45
|
routeMissed: 0,
|
|
45
46
|
sendFailed: 0,
|
|
46
47
|
groupTriggerSkipped: 0,
|
|
48
|
+
directTriggerSkipped: 0,
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
constructor(public ctx: Context, private pluginConfig: Config) {
|
|
@@ -54,6 +56,7 @@ export class MaibotService extends Service {
|
|
|
54
56
|
this.process = new MaibotProcessManager(ctx, pluginConfig, this.apiKey)
|
|
55
57
|
this.externalLogs = new ExternalLogForwarder(ctx, pluginConfig, [this.apiKey, pluginConfig.apiKey])
|
|
56
58
|
this.groupTrigger = new GroupMessageTrigger(pluginConfig.groupMessageTriggerCount)
|
|
59
|
+
this.directTrigger = new DirectMessageTrigger(pluginConfig.directMessageTriggerCount)
|
|
57
60
|
this.history = new MessageHistory(pluginConfig.routeTtl)
|
|
58
61
|
this.routes = new RouteRegistry(pluginConfig.routeTtl)
|
|
59
62
|
this.transport = new MaimTransport(ctx, pluginConfig, this.apiKey, {
|
|
@@ -232,14 +235,23 @@ export class MaibotService extends Service {
|
|
|
232
235
|
|
|
233
236
|
try {
|
|
234
237
|
const route = this.routes.remember(session)
|
|
235
|
-
const
|
|
238
|
+
const triggerKind = session.isDirect ? 'direct' : 'group'
|
|
239
|
+
const trigger = session.isDirect
|
|
240
|
+
? this.directTrigger.test(session, route)
|
|
241
|
+
: this.groupTrigger.test(session, route)
|
|
236
242
|
if (!trigger.shouldForward) {
|
|
237
243
|
this.history.rememberSession(session, route)
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
244
|
+
if (triggerKind === 'direct') {
|
|
245
|
+
this.bridge.directTriggerSkipped += 1
|
|
246
|
+
this.bridge.lastDirectTriggerCount = trigger.count
|
|
247
|
+
this.bridge.lastDirectTriggerThreshold = trigger.threshold
|
|
248
|
+
} else {
|
|
249
|
+
this.bridge.groupTriggerSkipped += 1
|
|
250
|
+
this.bridge.lastGroupTriggerCount = trigger.count
|
|
251
|
+
this.bridge.lastGroupTriggerThreshold = trigger.threshold
|
|
252
|
+
}
|
|
241
253
|
this.bridge.lastRouteId = route.routeId
|
|
242
|
-
this.logMessageDetail(`koishi -> maimai gated: route=${route.routeId} count=${trigger.count}/${trigger.threshold} ${describeSession(session)}`)
|
|
254
|
+
this.logMessageDetail(`koishi -> maimai gated: kind=${triggerKind} route=${route.routeId} count=${trigger.count}/${trigger.threshold} ${describeSession(session)}`)
|
|
243
255
|
if (this.pluginConfig.messageMode === 'exclusive') return ''
|
|
244
256
|
return next()
|
|
245
257
|
}
|
|
@@ -256,7 +268,7 @@ export class MaibotService extends Service {
|
|
|
256
268
|
|
|
257
269
|
const entries = trigger.entries.length ? trigger.entries : [{ session, route }]
|
|
258
270
|
if (trigger.forceMention) {
|
|
259
|
-
this.logMessageSummary(
|
|
271
|
+
this.logMessageSummary(`${triggerKind} trigger reached: route=${route.routeId} count=${trigger.count}/${trigger.threshold} flushing=${entries.length}`)
|
|
260
272
|
}
|
|
261
273
|
|
|
262
274
|
for (let index = 0; index < entries.length; index++) {
|
|
@@ -290,7 +302,7 @@ export class MaibotService extends Service {
|
|
|
290
302
|
this.bridge.lastRouteId = route.routeId
|
|
291
303
|
this.bridge.lastMessageId = message.message_info.message_id
|
|
292
304
|
const replyLog = replyContext ? ` reply=${replyContext.targetMessageId} context=${replyContext.contextCount || 0}` : ''
|
|
293
|
-
const forceLog = forceMention ? ' forced=
|
|
305
|
+
const forceLog = forceMention ? ' forced=message-trigger' : ''
|
|
294
306
|
this.logMessageDetail(`koishi -> maimai forwarded: route=${route.routeId}${replyLog}${forceLog} ${describeSegment(message.message_segment)}`)
|
|
295
307
|
}
|
|
296
308
|
|
|
@@ -298,6 +310,7 @@ export class MaibotService extends Service {
|
|
|
298
310
|
await this.shutdown()
|
|
299
311
|
this.routes.clear()
|
|
300
312
|
this.groupTrigger.clear()
|
|
313
|
+
this.directTrigger.clear()
|
|
301
314
|
this.history.clear()
|
|
302
315
|
}
|
|
303
316
|
|
package/src/types.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Awaitable, Context, Fragment, Session } from 'koishi'
|
|
2
2
|
|
|
3
3
|
export type MessageMode = 'coexist' | 'exclusive' | 'command'
|
|
4
|
+
export type CommandResultMode = 'source' | 'admin' | 'silent'
|
|
4
5
|
export type ProcessMode = 'docker' | 'managed' | 'external'
|
|
5
6
|
export type ProcessState = 'idle' | 'starting' | 'running' | 'stopping' | 'stopped' | 'blocked' | 'error'
|
|
6
7
|
export type TransportState = 'idle' | 'connecting' | 'connected' | 'disconnected' | 'error'
|
|
@@ -54,6 +55,7 @@ export interface RuntimeStatus {
|
|
|
54
55
|
routeMissed: number
|
|
55
56
|
sendFailed: number
|
|
56
57
|
groupTriggerSkipped: number
|
|
58
|
+
directTriggerSkipped: number
|
|
57
59
|
lastKoishiMessageAt?: number
|
|
58
60
|
lastMaimSendAt?: number
|
|
59
61
|
lastMaimMessageAt?: number
|
|
@@ -62,6 +64,8 @@ export interface RuntimeStatus {
|
|
|
62
64
|
lastMessageId?: string
|
|
63
65
|
lastGroupTriggerCount?: number
|
|
64
66
|
lastGroupTriggerThreshold?: number
|
|
67
|
+
lastDirectTriggerCount?: number
|
|
68
|
+
lastDirectTriggerThreshold?: number
|
|
65
69
|
lastError?: string
|
|
66
70
|
}
|
|
67
71
|
webui: {
|