koishi-plugin-mai-bridge 0.1.2 → 0.1.4
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 +3 -1
- package/client/page.vue +3 -0
- package/dist/index.js +2 -2
- package/out/bridge/convert.d.ts +1 -0
- package/out/bridge/convert.js +6 -2
- package/out/bridge/group-trigger.d.ts +15 -1
- package/out/bridge/group-trigger.js +47 -10
- package/out/commands/index.js +1 -0
- package/out/config.d.ts +1 -0
- package/out/config.js +2 -1
- package/out/service.d.ts +2 -0
- package/out/service.js +45 -18
- package/out/types.d.ts +3 -0
- package/package.json +1 -1
- package/src/bridge/convert.ts +7 -2
- package/src/bridge/group-trigger.ts +56 -9
- package/src/commands/index.ts +1 -0
- package/src/config.ts +3 -1
- package/src/service.ts +48 -19
- package/src/types.ts +3 -0
package/README.md
CHANGED
|
@@ -59,6 +59,7 @@ webuiHost: 0.0.0.0
|
|
|
59
59
|
webuiPort: 8002
|
|
60
60
|
messageMode: coexist
|
|
61
61
|
groupMessageTriggerCount: 1
|
|
62
|
+
directMessageTriggerCount: 1
|
|
62
63
|
```
|
|
63
64
|
|
|
64
65
|
说明:
|
|
@@ -68,7 +69,8 @@ groupMessageTriggerCount: 1
|
|
|
68
69
|
- `apiKey` 可以留空。插件会生成并复用运行期密钥。
|
|
69
70
|
- `dockerNetwork` 按你的 Koishi Docker 网络填写。Koishi 需要能通过 `apiHost` 访问 `maimai-ko`。
|
|
70
71
|
- `webuiPublicUrl` 可填写反代或宿主机映射后的 WebUI 地址,用于 Koishi 控制台显示入口。
|
|
71
|
-
- `groupMessageTriggerCount`
|
|
72
|
+
- `groupMessageTriggerCount` 控制群聊累计多少条消息后批量转发并强制触发 maibot 思考。设为 `3` 时,同一群前两条先缓存,第 3 条会连同前两条一起转发。
|
|
73
|
+
- `directMessageTriggerCount` 控制私聊累计多少条消息后批量转发并强制触发 maibot 思考。默认 `1`,表示每条私信都会触发;设为 `3` 时,同一私聊前两条只缓存,第 3 条统一转发并触发回复。
|
|
72
74
|
|
|
73
75
|
## 消息模式
|
|
74
76
|
|
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.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,10 @@ 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: !session.isDirect && forceMention,
|
|
208
|
+
koishi_direct_trigger_forced: !!session.isDirect && forceMention,
|
|
209
|
+
at_bot: mentioned,
|
|
210
|
+
is_mentioned: mentioned,
|
|
207
211
|
},
|
|
208
212
|
format_info: {
|
|
209
213
|
content_format: ['text', 'image'],
|
|
@@ -5,10 +5,24 @@ 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;
|
|
18
|
+
constructor(threshold: number);
|
|
19
|
+
test(session: Session, route: KoishiRoute): GroupTriggerResult;
|
|
20
|
+
clear(): void;
|
|
21
|
+
private createKey;
|
|
22
|
+
}
|
|
23
|
+
export declare class DirectMessageTrigger {
|
|
24
|
+
private threshold;
|
|
25
|
+
private pending;
|
|
12
26
|
constructor(threshold: number);
|
|
13
27
|
test(session: Session, route: KoishiRoute): GroupTriggerResult;
|
|
14
28
|
clear(): void;
|
|
@@ -1,28 +1,29 @@
|
|
|
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();
|
|
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 [
|
|
@@ -34,3 +35,39 @@ class GroupMessageTrigger {
|
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
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.js
CHANGED
|
@@ -16,6 +16,7 @@ function renderStatus(status) {
|
|
|
16
16
|
`重连次数: ${status.transport.reconnectAttempts}`,
|
|
17
17
|
`中转: Koishi收=${status.bridge.koishiReceived} / 发往mai.ko=${status.bridge.maimSent} / 收到mai.ko=${status.bridge.maimReceived} / 回发Koishi=${status.bridge.koishiSent}`,
|
|
18
18
|
`中转异常: 路由失败=${status.bridge.routeMissed} / 发送失败=${status.bridge.sendFailed}`,
|
|
19
|
+
`触发限制: 群聊跳过=${status.bridge.groupTriggerSkipped} / 私信跳过=${status.bridge.directTriggerSkipped}`,
|
|
19
20
|
`启动时间: ${formatTime(status.process.startedAt)}`,
|
|
20
21
|
];
|
|
21
22
|
if (status.process.blockedReason)
|
package/out/config.d.ts
CHANGED
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('
|
|
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 的单张图片最大字节数。'),
|
package/out/service.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export declare class MaibotService extends Service {
|
|
|
11
11
|
private process;
|
|
12
12
|
private externalLogs;
|
|
13
13
|
private groupTrigger;
|
|
14
|
+
private directTrigger;
|
|
14
15
|
private history;
|
|
15
16
|
private routes;
|
|
16
17
|
private transport;
|
|
@@ -38,6 +39,7 @@ export declare class MaibotService extends Service {
|
|
|
38
39
|
dockerRestart(): Promise<RuntimeStatus>;
|
|
39
40
|
getStatus(): RuntimeStatus;
|
|
40
41
|
handleSession(session: Session, next: () => Awaitable<void | Fragment>): Promise<void | Fragment>;
|
|
42
|
+
private forwardSessionToMaim;
|
|
41
43
|
dispose(): Promise<void>;
|
|
42
44
|
private logMessageDetail;
|
|
43
45
|
private logMessageSummary;
|
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();
|
|
@@ -249,19 +262,15 @@ class MaibotService extends koishi_1.Service {
|
|
|
249
262
|
return '';
|
|
250
263
|
return next();
|
|
251
264
|
}
|
|
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)}`);
|
|
265
|
+
const entries = trigger.entries.length ? trigger.entries : [{ session, route }];
|
|
266
|
+
if (trigger.forceMention) {
|
|
267
|
+
this.logMessageSummary(`${triggerKind} trigger reached: route=${route.routeId} count=${trigger.count}/${trigger.threshold} flushing=${entries.length}`);
|
|
268
|
+
}
|
|
269
|
+
for (let index = 0; index < entries.length; index++) {
|
|
270
|
+
const entry = entries[index];
|
|
271
|
+
const forceMention = !!trigger.forceMention && index === entries.length - 1;
|
|
272
|
+
await this.forwardSessionToMaim(entry.session, entry.route, forceMention);
|
|
273
|
+
}
|
|
265
274
|
if (this.pluginConfig.messageMode === 'exclusive')
|
|
266
275
|
return '';
|
|
267
276
|
}
|
|
@@ -273,10 +282,28 @@ class MaibotService extends koishi_1.Service {
|
|
|
273
282
|
}
|
|
274
283
|
return next();
|
|
275
284
|
}
|
|
285
|
+
async forwardSessionToMaim(session, route, forceMention = false) {
|
|
286
|
+
const replyContext = this.history.resolveReplyContext(session, route);
|
|
287
|
+
const message = await (0, convert_1.sessionToMaimMessage)(session, route, this.apiKey, {
|
|
288
|
+
resolveImage: (source) => this.resolveImageToBase64(source),
|
|
289
|
+
replyContext,
|
|
290
|
+
forceMention,
|
|
291
|
+
});
|
|
292
|
+
this.transport.sendMessage(message);
|
|
293
|
+
this.history.rememberSession(session, route);
|
|
294
|
+
this.bridge.maimSent += 1;
|
|
295
|
+
this.bridge.lastMaimSendAt = Date.now();
|
|
296
|
+
this.bridge.lastRouteId = route.routeId;
|
|
297
|
+
this.bridge.lastMessageId = message.message_info.message_id;
|
|
298
|
+
const replyLog = replyContext ? ` reply=${replyContext.targetMessageId} context=${replyContext.contextCount || 0}` : '';
|
|
299
|
+
const forceLog = forceMention ? ' forced=message-trigger' : '';
|
|
300
|
+
this.logMessageDetail(`koishi -> maimai forwarded: route=${route.routeId}${replyLog}${forceLog} ${(0, logging_1.describeSegment)(message.message_segment)}`);
|
|
301
|
+
}
|
|
276
302
|
async dispose() {
|
|
277
303
|
await this.shutdown();
|
|
278
304
|
this.routes.clear();
|
|
279
305
|
this.groupTrigger.clear();
|
|
306
|
+
this.directTrigger.clear();
|
|
280
307
|
this.history.clear();
|
|
281
308
|
}
|
|
282
309
|
logMessageDetail(message) {
|
package/out/types.d.ts
CHANGED
|
@@ -52,6 +52,7 @@ export interface RuntimeStatus {
|
|
|
52
52
|
routeMissed: number;
|
|
53
53
|
sendFailed: number;
|
|
54
54
|
groupTriggerSkipped: number;
|
|
55
|
+
directTriggerSkipped: number;
|
|
55
56
|
lastKoishiMessageAt?: number;
|
|
56
57
|
lastMaimSendAt?: number;
|
|
57
58
|
lastMaimMessageAt?: number;
|
|
@@ -60,6 +61,8 @@ export interface RuntimeStatus {
|
|
|
60
61
|
lastMessageId?: string;
|
|
61
62
|
lastGroupTriggerCount?: number;
|
|
62
63
|
lastGroupTriggerThreshold?: number;
|
|
64
|
+
lastDirectTriggerCount?: number;
|
|
65
|
+
lastDirectTriggerThreshold?: number;
|
|
63
66
|
lastError?: string;
|
|
64
67
|
};
|
|
65
68
|
webui: {
|
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,10 @@ 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: !session.isDirect && forceMention,
|
|
232
|
+
koishi_direct_trigger_forced: !!session.isDirect && forceMention,
|
|
233
|
+
at_bot: mentioned,
|
|
234
|
+
is_mentioned: mentioned,
|
|
230
235
|
},
|
|
231
236
|
format_info: {
|
|
232
237
|
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) {
|
|
@@ -43,3 +51,42 @@ export class GroupMessageTrigger {
|
|
|
43
51
|
].join(':')
|
|
44
52
|
}
|
|
45
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
|
@@ -17,6 +17,7 @@ export function renderStatus(status: RuntimeStatus) {
|
|
|
17
17
|
`重连次数: ${status.transport.reconnectAttempts}`,
|
|
18
18
|
`中转: Koishi收=${status.bridge.koishiReceived} / 发往mai.ko=${status.bridge.maimSent} / 收到mai.ko=${status.bridge.maimReceived} / 回发Koishi=${status.bridge.koishiSent}`,
|
|
19
19
|
`中转异常: 路由失败=${status.bridge.routeMissed} / 发送失败=${status.bridge.sendFailed}`,
|
|
20
|
+
`触发限制: 群聊跳过=${status.bridge.groupTriggerSkipped} / 私信跳过=${status.bridge.directTriggerSkipped}`,
|
|
20
21
|
`启动时间: ${formatTime(status.process.startedAt)}`,
|
|
21
22
|
]
|
|
22
23
|
if (status.process.blockedReason) lines.push(`阻塞原因: ${status.process.blockedReason}`)
|
package/src/config.ts
CHANGED
|
@@ -25,6 +25,7 @@ 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
|
|
@@ -93,7 +94,8 @@ export const Config: Schema<Config> = Schema.intersect([
|
|
|
93
94
|
Schema.const('command').description('命令:仅命中指令前缀时转发。'),
|
|
94
95
|
]).default('coexist').description('消息转发模式。'),
|
|
95
96
|
commandPrefix: Schema.string().default('mai.ko').description('command 模式下触发 mai.ko 的文本前缀。'),
|
|
96
|
-
groupMessageTriggerCount: Schema.number().min(1).default(1).description('
|
|
97
|
+
groupMessageTriggerCount: Schema.number().min(1).default(1).description('同一群聊累计达到多少条消息后批量转发并强制触发 mai.ko 思考;1 表示每条群消息都转发。'),
|
|
98
|
+
directMessageTriggerCount: Schema.number().min(1).default(1).description('同一私聊累计达到多少条消息后批量转发并强制触发 mai.ko 思考;1 表示每条私信都转发。'),
|
|
97
99
|
imageDownloadEnabled: Schema.boolean().default(true).description('转发图片消息前由 Koishi 下载图片并转为 mai.ko 可识别的 base64 图片段。'),
|
|
98
100
|
imageDownloadTimeout: Schema.number().min(1000).default(10000).description('下载单张入站图片的超时时间,单位毫秒。'),
|
|
99
101
|
imageDownloadMaxBytes: Schema.number().min(1024).default(10 * 1024 * 1024).description('允许转发给 mai.ko 的单张图片最大字节数。'),
|
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
|
}
|
|
@@ -254,19 +266,17 @@ export class MaibotService extends Service {
|
|
|
254
266
|
return next()
|
|
255
267
|
}
|
|
256
268
|
|
|
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)}`)
|
|
269
|
+
const entries = trigger.entries.length ? trigger.entries : [{ session, route }]
|
|
270
|
+
if (trigger.forceMention) {
|
|
271
|
+
this.logMessageSummary(`${triggerKind} trigger reached: route=${route.routeId} count=${trigger.count}/${trigger.threshold} flushing=${entries.length}`)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
for (let index = 0; index < entries.length; index++) {
|
|
275
|
+
const entry = entries[index]
|
|
276
|
+
const forceMention = !!trigger.forceMention && index === entries.length - 1
|
|
277
|
+
await this.forwardSessionToMaim(entry.session, entry.route, forceMention)
|
|
278
|
+
}
|
|
279
|
+
|
|
270
280
|
if (this.pluginConfig.messageMode === 'exclusive') return ''
|
|
271
281
|
} catch (error) {
|
|
272
282
|
const message = error instanceof Error ? error.message : String(error)
|
|
@@ -278,10 +288,29 @@ export class MaibotService extends Service {
|
|
|
278
288
|
return next()
|
|
279
289
|
}
|
|
280
290
|
|
|
291
|
+
private async forwardSessionToMaim(session: Session, route: ReturnType<RouteRegistry['remember']>, forceMention = false) {
|
|
292
|
+
const replyContext = this.history.resolveReplyContext(session, route)
|
|
293
|
+
const message = await sessionToMaimMessage(session, route, this.apiKey, {
|
|
294
|
+
resolveImage: (source) => this.resolveImageToBase64(source),
|
|
295
|
+
replyContext,
|
|
296
|
+
forceMention,
|
|
297
|
+
})
|
|
298
|
+
this.transport.sendMessage(message)
|
|
299
|
+
this.history.rememberSession(session, route)
|
|
300
|
+
this.bridge.maimSent += 1
|
|
301
|
+
this.bridge.lastMaimSendAt = Date.now()
|
|
302
|
+
this.bridge.lastRouteId = route.routeId
|
|
303
|
+
this.bridge.lastMessageId = message.message_info.message_id
|
|
304
|
+
const replyLog = replyContext ? ` reply=${replyContext.targetMessageId} context=${replyContext.contextCount || 0}` : ''
|
|
305
|
+
const forceLog = forceMention ? ' forced=message-trigger' : ''
|
|
306
|
+
this.logMessageDetail(`koishi -> maimai forwarded: route=${route.routeId}${replyLog}${forceLog} ${describeSegment(message.message_segment)}`)
|
|
307
|
+
}
|
|
308
|
+
|
|
281
309
|
async dispose() {
|
|
282
310
|
await this.shutdown()
|
|
283
311
|
this.routes.clear()
|
|
284
312
|
this.groupTrigger.clear()
|
|
313
|
+
this.directTrigger.clear()
|
|
285
314
|
this.history.clear()
|
|
286
315
|
}
|
|
287
316
|
|
package/src/types.ts
CHANGED
|
@@ -54,6 +54,7 @@ export interface RuntimeStatus {
|
|
|
54
54
|
routeMissed: number
|
|
55
55
|
sendFailed: number
|
|
56
56
|
groupTriggerSkipped: number
|
|
57
|
+
directTriggerSkipped: number
|
|
57
58
|
lastKoishiMessageAt?: number
|
|
58
59
|
lastMaimSendAt?: number
|
|
59
60
|
lastMaimMessageAt?: number
|
|
@@ -62,6 +63,8 @@ export interface RuntimeStatus {
|
|
|
62
63
|
lastMessageId?: string
|
|
63
64
|
lastGroupTriggerCount?: number
|
|
64
65
|
lastGroupTriggerThreshold?: number
|
|
66
|
+
lastDirectTriggerCount?: number
|
|
67
|
+
lastDirectTriggerThreshold?: number
|
|
65
68
|
lastError?: string
|
|
66
69
|
}
|
|
67
70
|
webui: {
|