koishi-plugin-mai-bridge 0.1.0
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 +110 -0
- package/client/index.ts +11 -0
- package/client/page.vue +148 -0
- package/client/tsconfig.json +11 -0
- package/deploy/Dockerfile.maimai-koishi +35 -0
- package/dist/index.js +2 -0
- package/out/bridge/agreements.d.ts +12 -0
- package/out/bridge/agreements.js +47 -0
- package/out/bridge/convert.d.ts +25 -0
- package/out/bridge/convert.js +274 -0
- package/out/bridge/docker.d.ts +30 -0
- package/out/bridge/docker.js +229 -0
- package/out/bridge/external-logs.d.ts +29 -0
- package/out/bridge/external-logs.js +166 -0
- package/out/bridge/history.d.ts +27 -0
- package/out/bridge/history.js +195 -0
- package/out/bridge/logging.d.ts +7 -0
- package/out/bridge/logging.js +77 -0
- package/out/bridge/prepare.d.ts +42 -0
- package/out/bridge/prepare.js +199 -0
- package/out/bridge/process.d.ts +31 -0
- package/out/bridge/process.js +208 -0
- package/out/bridge/routes.d.ts +17 -0
- package/out/bridge/routes.js +85 -0
- package/out/bridge/runtime-key.d.ts +3 -0
- package/out/bridge/runtime-key.js +34 -0
- package/out/bridge/transport.d.ts +39 -0
- package/out/bridge/transport.js +197 -0
- package/out/bridge/webui-token.d.ts +9 -0
- package/out/bridge/webui-token.js +36 -0
- package/out/commands/index.d.ts +5 -0
- package/out/commands/index.js +64 -0
- package/out/config.d.ts +52 -0
- package/out/config.js +77 -0
- package/out/console.d.ts +16 -0
- package/out/console.js +24 -0
- package/out/index.d.ts +11 -0
- package/out/index.js +32 -0
- package/out/locales.d.ts +18 -0
- package/out/locales.js +21 -0
- package/out/service.d.ts +61 -0
- package/out/service.js +558 -0
- package/out/setup/postinstall.d.ts +1 -0
- package/out/setup/postinstall.js +98 -0
- package/out/types.d.ts +165 -0
- package/out/types.js +2 -0
- package/out/utils/command.d.ts +2 -0
- package/out/utils/command.js +20 -0
- package/out/utils/paths.d.ts +4 -0
- package/out/utils/paths.js +17 -0
- package/out/utils/spawn.d.ts +13 -0
- package/out/utils/spawn.js +53 -0
- package/package.json +74 -0
- package/patches/maimai-koishi.patch +553 -0
- package/scripts/postinstall.js +10 -0
- package/src/bridge/agreements.ts +60 -0
- package/src/bridge/convert.ts +308 -0
- package/src/bridge/docker.ts +244 -0
- package/src/bridge/external-logs.ts +171 -0
- package/src/bridge/history.ts +199 -0
- package/src/bridge/logging.ts +71 -0
- package/src/bridge/prepare.ts +226 -0
- package/src/bridge/process.ts +214 -0
- package/src/bridge/routes.ts +82 -0
- package/src/bridge/runtime-key.ts +36 -0
- package/src/bridge/transport.ts +211 -0
- package/src/bridge/webui-token.ts +41 -0
- package/src/commands/index.ts +69 -0
- package/src/config.ts +126 -0
- package/src/console.ts +39 -0
- package/src/index.ts +36 -0
- package/src/locales/en-US.yml +6 -0
- package/src/locales/zh-CN.yml +6 -0
- package/src/locales.ts +19 -0
- package/src/service.ts +567 -0
- package/src/setup/postinstall.ts +99 -0
- package/src/types.ts +178 -0
- package/src/utils/command.ts +15 -0
- package/src/utils/paths.ts +15 -0
- package/src/utils/spawn.ts +59 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sessionToMaimMessage = sessionToMaimMessage;
|
|
4
|
+
exports.maimMessageToFragment = maimMessageToFragment;
|
|
5
|
+
exports.getRouteIdFromMaim = getRouteIdFromMaim;
|
|
6
|
+
exports.getFallbackRouteHints = getFallbackRouteHints;
|
|
7
|
+
exports.shouldForwardSession = shouldForwardSession;
|
|
8
|
+
const koishi_1 = require("koishi");
|
|
9
|
+
const PLATFORM = 'koishi';
|
|
10
|
+
function textSeg(content) {
|
|
11
|
+
return { type: 'text', data: content };
|
|
12
|
+
}
|
|
13
|
+
function atSeg(element) {
|
|
14
|
+
const id = String(element.attrs.id || element.attrs.userId || element.attrs.qq || '').trim();
|
|
15
|
+
const name = String(element.attrs.name || element.attrs.nick || '').trim();
|
|
16
|
+
return {
|
|
17
|
+
type: 'at',
|
|
18
|
+
data: {
|
|
19
|
+
target_user_id: id,
|
|
20
|
+
target_user_nickname: name,
|
|
21
|
+
target_user_cardname: name,
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function stringifyUnknown(element) {
|
|
26
|
+
const attrs = Object.entries(element.attrs || {})
|
|
27
|
+
.map(([key, value]) => `${key}=${String(value)}`)
|
|
28
|
+
.join(' ');
|
|
29
|
+
return attrs ? `[${element.type} ${attrs}]` : `[${element.type}]`;
|
|
30
|
+
}
|
|
31
|
+
async function imageSegFromSource(source, options) {
|
|
32
|
+
const src = typeof source === 'string' ? source.trim() : '';
|
|
33
|
+
if (!src)
|
|
34
|
+
return;
|
|
35
|
+
const dataUrl = /^data:image\/[^;]+;base64,([A-Za-z0-9+/=\s]+)$/i.exec(src);
|
|
36
|
+
if (dataUrl) {
|
|
37
|
+
return { type: 'image', data: dataUrl[1].replace(/\s/g, '') };
|
|
38
|
+
}
|
|
39
|
+
const base64Url = /^base64:\/\/([A-Za-z0-9+/=\s]+)$/i.exec(src);
|
|
40
|
+
if (base64Url) {
|
|
41
|
+
return { type: 'image', data: base64Url[1].replace(/\s/g, '') };
|
|
42
|
+
}
|
|
43
|
+
if (/^https?:\/\//i.test(src) && options.resolveImage) {
|
|
44
|
+
const base64 = await options.resolveImage(src);
|
|
45
|
+
if (base64)
|
|
46
|
+
return { type: 'image', data: base64 };
|
|
47
|
+
}
|
|
48
|
+
return textSeg(`[图片: ${src}]`);
|
|
49
|
+
}
|
|
50
|
+
async function elementToSeg(element, options) {
|
|
51
|
+
if (element.type === 'text') {
|
|
52
|
+
return textSeg(element.attrs.content ?? '');
|
|
53
|
+
}
|
|
54
|
+
if (element.type === 'quote' || element.type === 'reply') {
|
|
55
|
+
return replySegFromElement(element, options.replyContext);
|
|
56
|
+
}
|
|
57
|
+
if (element.type === 'at') {
|
|
58
|
+
return atSeg(element);
|
|
59
|
+
}
|
|
60
|
+
if (element.type === 'img' || element.type === 'image') {
|
|
61
|
+
const src = element.attrs.src || element.attrs.url || element.attrs.file;
|
|
62
|
+
return await imageSegFromSource(src, options) || textSeg(stringifyUnknown(element));
|
|
63
|
+
}
|
|
64
|
+
if (element.type === 'br') {
|
|
65
|
+
return textSeg('\n');
|
|
66
|
+
}
|
|
67
|
+
return textSeg(stringifyUnknown(element));
|
|
68
|
+
}
|
|
69
|
+
async function normalizeSegments(elements, options) {
|
|
70
|
+
const hasReplyElement = elements.some((element) => element.type === 'quote' || element.type === 'reply');
|
|
71
|
+
const segments = (await Promise.all(elements.map((element) => elementToSeg(element, options))))
|
|
72
|
+
.filter((segment) => !!segment);
|
|
73
|
+
if (options.replyContext && !hasReplyElement) {
|
|
74
|
+
segments.unshift(replySeg(options.replyContext));
|
|
75
|
+
}
|
|
76
|
+
if (!segments.length)
|
|
77
|
+
return textSeg('');
|
|
78
|
+
if (segments.length === 1)
|
|
79
|
+
return segments[0];
|
|
80
|
+
return {
|
|
81
|
+
type: 'seglist',
|
|
82
|
+
data: segments,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function getElements(session) {
|
|
86
|
+
const existing = session.elements;
|
|
87
|
+
if (Array.isArray(existing))
|
|
88
|
+
return existing;
|
|
89
|
+
return koishi_1.h.parse(session.content || '');
|
|
90
|
+
}
|
|
91
|
+
function textFromChildren(element) {
|
|
92
|
+
const children = element.children;
|
|
93
|
+
if (!Array.isArray(children))
|
|
94
|
+
return '';
|
|
95
|
+
return children.map((child) => {
|
|
96
|
+
if (typeof child === 'string')
|
|
97
|
+
return child;
|
|
98
|
+
if (child?.type === 'text')
|
|
99
|
+
return child.attrs?.content ?? '';
|
|
100
|
+
if (child?.type === 'at')
|
|
101
|
+
return `@${child.attrs?.name || child.attrs?.id || ''}`;
|
|
102
|
+
if (child?.type === 'img' || child?.type === 'image')
|
|
103
|
+
return '[图片]';
|
|
104
|
+
return child?.type ? stringifyUnknown(child) : '';
|
|
105
|
+
}).join('').trim();
|
|
106
|
+
}
|
|
107
|
+
function replySeg(context) {
|
|
108
|
+
const data = {
|
|
109
|
+
target_message_id: context.targetMessageId,
|
|
110
|
+
};
|
|
111
|
+
if (context.targetMessageContent)
|
|
112
|
+
data.target_message_content = context.targetMessageContent;
|
|
113
|
+
if (context.targetMessageSenderId)
|
|
114
|
+
data.target_message_sender_id = context.targetMessageSenderId;
|
|
115
|
+
if (context.targetMessageSenderNickname)
|
|
116
|
+
data.target_message_sender_nickname = context.targetMessageSenderNickname;
|
|
117
|
+
if (context.targetMessageSenderCardname)
|
|
118
|
+
data.target_message_sender_cardname = context.targetMessageSenderCardname;
|
|
119
|
+
if (context.contextCount !== undefined)
|
|
120
|
+
data.koishi_context_count = context.contextCount;
|
|
121
|
+
if (Object.keys(data).length === 1)
|
|
122
|
+
return { type: 'reply', data: context.targetMessageId };
|
|
123
|
+
return { type: 'reply', data };
|
|
124
|
+
}
|
|
125
|
+
function replySegFromElement(element, context) {
|
|
126
|
+
const targetMessageId = firstString(element.attrs.id, element.attrs.messageId, element.attrs.target, context?.targetMessageId);
|
|
127
|
+
if (!targetMessageId)
|
|
128
|
+
return textSeg(stringifyUnknown(element));
|
|
129
|
+
return replySeg({
|
|
130
|
+
targetMessageId,
|
|
131
|
+
targetMessageContent: context?.targetMessageId === targetMessageId
|
|
132
|
+
? context.targetMessageContent
|
|
133
|
+
: textFromChildren(element) || firstString(element.attrs.content, element.attrs.text),
|
|
134
|
+
targetMessageSenderId: context?.targetMessageId === targetMessageId ? context.targetMessageSenderId : undefined,
|
|
135
|
+
targetMessageSenderNickname: context?.targetMessageId === targetMessageId ? context.targetMessageSenderNickname : undefined,
|
|
136
|
+
targetMessageSenderCardname: context?.targetMessageId === targetMessageId ? context.targetMessageSenderCardname : undefined,
|
|
137
|
+
contextCount: context?.targetMessageId === targetMessageId ? context.contextCount : undefined,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
function isAtBot(session) {
|
|
141
|
+
return getElements(session).some((element) => {
|
|
142
|
+
if (element.type !== 'at')
|
|
143
|
+
return false;
|
|
144
|
+
const id = String(element.attrs.id || element.attrs.userId || element.attrs.qq || '').trim();
|
|
145
|
+
return !!id && id === String(session.selfId || '').trim();
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
function nonEmptyString(value, fallback) {
|
|
149
|
+
const normalized = typeof value === 'string' ? value.trim() : '';
|
|
150
|
+
return normalized || fallback;
|
|
151
|
+
}
|
|
152
|
+
function firstString(...values) {
|
|
153
|
+
for (const value of values) {
|
|
154
|
+
const normalized = typeof value === 'string' ? value.trim() : '';
|
|
155
|
+
if (normalized)
|
|
156
|
+
return normalized;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function buildSenderInfo(session) {
|
|
160
|
+
const userId = nonEmptyString(session.userId || session.author?.id, 'unknown');
|
|
161
|
+
const channelId = nonEmptyString(session.channelId, 'unknown');
|
|
162
|
+
const userInfo = {
|
|
163
|
+
platform: PLATFORM,
|
|
164
|
+
user_id: userId,
|
|
165
|
+
user_nickname: nonEmptyString(session.username || session.author?.name, userId),
|
|
166
|
+
user_cardname: nonEmptyString(session.author?.nick, userId),
|
|
167
|
+
};
|
|
168
|
+
if (session.isDirect) {
|
|
169
|
+
return { user_info: userInfo };
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
group_info: {
|
|
173
|
+
platform: PLATFORM,
|
|
174
|
+
group_id: channelId,
|
|
175
|
+
group_name: nonEmptyString(session.event?.channel?.name || session.event?.guild?.name, channelId),
|
|
176
|
+
},
|
|
177
|
+
user_info: userInfo,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
async function sessionToMaimMessage(session, route, apiKey, options = {}) {
|
|
181
|
+
const messageId = String(session.messageId || session.id || `koishi-${Date.now()}`);
|
|
182
|
+
const senderInfo = buildSenderInfo(session);
|
|
183
|
+
const atBot = isAtBot(session);
|
|
184
|
+
const replyContext = options.replyContext;
|
|
185
|
+
return {
|
|
186
|
+
message_info: {
|
|
187
|
+
platform: PLATFORM,
|
|
188
|
+
message_id: messageId,
|
|
189
|
+
time: session.timestamp ? session.timestamp / 1000 : Date.now() / 1000,
|
|
190
|
+
group_info: senderInfo.group_info,
|
|
191
|
+
user_info: senderInfo.user_info,
|
|
192
|
+
sender_info: senderInfo,
|
|
193
|
+
additional_config: {
|
|
194
|
+
koishi_route_id: route.routeId,
|
|
195
|
+
koishi_self_id: session.selfId,
|
|
196
|
+
koishi_platform: session.platform,
|
|
197
|
+
koishi_channel_id: session.channelId || '',
|
|
198
|
+
koishi_guild_id: session.guildId,
|
|
199
|
+
koishi_user_id: session.userId,
|
|
200
|
+
koishi_message_id: messageId,
|
|
201
|
+
koishi_reply_to_message_id: replyContext?.targetMessageId,
|
|
202
|
+
koishi_reply_context_count: replyContext?.contextCount,
|
|
203
|
+
koishi_is_direct: !!session.isDirect,
|
|
204
|
+
at_bot: atBot,
|
|
205
|
+
is_mentioned: atBot,
|
|
206
|
+
},
|
|
207
|
+
format_info: {
|
|
208
|
+
content_format: ['text', 'image'],
|
|
209
|
+
accept_format: ['text', 'image'],
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
message_segment: await normalizeSegments(getElements(session), options),
|
|
213
|
+
message_dim: {
|
|
214
|
+
api_key: apiKey,
|
|
215
|
+
platform: PLATFORM,
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function segToFragment(segment) {
|
|
220
|
+
if (segment.type === 'seglist' && Array.isArray(segment.data)) {
|
|
221
|
+
return segment.data
|
|
222
|
+
.map(segToFragment)
|
|
223
|
+
.filter((fragment) => fragment !== '' && (!Array.isArray(fragment) || fragment.length > 0));
|
|
224
|
+
}
|
|
225
|
+
if (segment.type === 'text') {
|
|
226
|
+
return String(segment.data ?? '');
|
|
227
|
+
}
|
|
228
|
+
if (segment.type === 'image' || segment.type === 'img') {
|
|
229
|
+
const src = String(segment.data ?? '');
|
|
230
|
+
if (!src)
|
|
231
|
+
return '';
|
|
232
|
+
const normalized = /^(https?:|file:|data:)/.test(src) ? src : `data:image/png;base64,${src}`;
|
|
233
|
+
return (0, koishi_1.h)('img', { src: normalized });
|
|
234
|
+
}
|
|
235
|
+
if (segment.type === 'emoji') {
|
|
236
|
+
return String(segment.data ?? '');
|
|
237
|
+
}
|
|
238
|
+
if (segment.type === 'reply') {
|
|
239
|
+
return '';
|
|
240
|
+
}
|
|
241
|
+
return `[${segment.type}]${typeof segment.data === 'string' ? segment.data : JSON.stringify(segment.data)}`;
|
|
242
|
+
}
|
|
243
|
+
function maimMessageToFragment(message) {
|
|
244
|
+
return segToFragment(message.message_segment);
|
|
245
|
+
}
|
|
246
|
+
function getRouteIdFromMaim(message) {
|
|
247
|
+
return message.message_info.additional_config?.koishi_route_id;
|
|
248
|
+
}
|
|
249
|
+
function getFallbackRouteHints(message) {
|
|
250
|
+
const receiver = message.message_info.receiver_info;
|
|
251
|
+
const messageInfo = message.message_info;
|
|
252
|
+
const additional = message.message_info.additional_config || {};
|
|
253
|
+
const channelId = firstString(additional.koishi_channel_id, receiver?.group_info?.group_id, messageInfo.group_info?.group_id);
|
|
254
|
+
const userId = firstString(additional.koishi_user_id, additional.platform_io_target_user_id, receiver?.user_info?.user_id, messageInfo.user_info?.user_id);
|
|
255
|
+
const isDirect = typeof additional.koishi_is_direct === 'boolean'
|
|
256
|
+
? additional.koishi_is_direct
|
|
257
|
+
: !channelId;
|
|
258
|
+
return {
|
|
259
|
+
channelId,
|
|
260
|
+
userId,
|
|
261
|
+
selfId: firstString(additional.koishi_self_id),
|
|
262
|
+
isDirect,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
function shouldForwardSession(session, config) {
|
|
266
|
+
if (!session.content && !getElements(session).length)
|
|
267
|
+
return false;
|
|
268
|
+
if (config.messageMode !== 'command')
|
|
269
|
+
return true;
|
|
270
|
+
const prefix = config.commandPrefix.trim();
|
|
271
|
+
if (!prefix)
|
|
272
|
+
return true;
|
|
273
|
+
return (session.content || '').trimStart().startsWith(prefix);
|
|
274
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Config } from '../config';
|
|
2
|
+
import type { RuntimeStatus } from '../types';
|
|
3
|
+
export declare class MaibotDockerManager {
|
|
4
|
+
private config;
|
|
5
|
+
private apiKey;
|
|
6
|
+
private logger;
|
|
7
|
+
private state;
|
|
8
|
+
private lastError?;
|
|
9
|
+
private updatedAt?;
|
|
10
|
+
private logs;
|
|
11
|
+
constructor(config: Config, apiKey: string);
|
|
12
|
+
getStatus(): RuntimeStatus['docker'];
|
|
13
|
+
getLogs(): string[];
|
|
14
|
+
build(): Promise<void>;
|
|
15
|
+
start(): Promise<void>;
|
|
16
|
+
stop(): Promise<void>;
|
|
17
|
+
restart(): Promise<void>;
|
|
18
|
+
private createContainer;
|
|
19
|
+
private createRunArgs;
|
|
20
|
+
private buildEnv;
|
|
21
|
+
private volumeMap;
|
|
22
|
+
private ensureVolumeDirs;
|
|
23
|
+
private containerExists;
|
|
24
|
+
private removeContainer;
|
|
25
|
+
private docker;
|
|
26
|
+
private run;
|
|
27
|
+
private fail;
|
|
28
|
+
private pushLog;
|
|
29
|
+
private sanitize;
|
|
30
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MaibotDockerManager = void 0;
|
|
4
|
+
const fs_1 = require("fs");
|
|
5
|
+
const path_1 = require("path");
|
|
6
|
+
const koishi_1 = require("koishi");
|
|
7
|
+
const paths_1 = require("../utils/paths");
|
|
8
|
+
const spawn_1 = require("../utils/spawn");
|
|
9
|
+
const command_1 = require("../utils/command");
|
|
10
|
+
const agreements_1 = require("./agreements");
|
|
11
|
+
class MaibotDockerManager {
|
|
12
|
+
config;
|
|
13
|
+
apiKey;
|
|
14
|
+
logger = new koishi_1.Logger('mai.ko/docker');
|
|
15
|
+
state = 'idle';
|
|
16
|
+
lastError;
|
|
17
|
+
updatedAt;
|
|
18
|
+
logs = [];
|
|
19
|
+
constructor(config, apiKey) {
|
|
20
|
+
this.config = config;
|
|
21
|
+
this.apiKey = apiKey;
|
|
22
|
+
}
|
|
23
|
+
getStatus() {
|
|
24
|
+
return {
|
|
25
|
+
state: this.state,
|
|
26
|
+
containerName: this.config.dockerContainerName,
|
|
27
|
+
imageName: this.config.dockerImageName,
|
|
28
|
+
lastError: this.lastError,
|
|
29
|
+
updatedAt: this.updatedAt,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
getLogs() {
|
|
33
|
+
return [...this.logs];
|
|
34
|
+
}
|
|
35
|
+
async build() {
|
|
36
|
+
this.state = 'building';
|
|
37
|
+
this.lastError = undefined;
|
|
38
|
+
this.updatedAt = Date.now();
|
|
39
|
+
try {
|
|
40
|
+
const root = (0, paths_1.resolveMaibotRoot)(this.config);
|
|
41
|
+
if (!(0, fs_1.existsSync)(root)) {
|
|
42
|
+
throw new Error(`maibotRoot does not exist: ${root}`);
|
|
43
|
+
}
|
|
44
|
+
this.pushLog(`building Docker image: ${this.config.dockerImageName}`);
|
|
45
|
+
const result = await this.docker(['build', '-t', this.config.dockerImageName, root]);
|
|
46
|
+
(0, spawn_1.assertSuccessful)(result, 'docker build');
|
|
47
|
+
this.state = 'stopped';
|
|
48
|
+
this.updatedAt = Date.now();
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
this.fail(error);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async start() {
|
|
55
|
+
this.lastError = undefined;
|
|
56
|
+
this.updatedAt = Date.now();
|
|
57
|
+
try {
|
|
58
|
+
if (this.config.dockerRecreateOnStart) {
|
|
59
|
+
await this.removeContainer(true);
|
|
60
|
+
}
|
|
61
|
+
const exists = await this.containerExists();
|
|
62
|
+
if (!exists) {
|
|
63
|
+
await this.createContainer();
|
|
64
|
+
}
|
|
65
|
+
this.state = 'starting';
|
|
66
|
+
this.pushLog(`starting Docker container: ${this.config.dockerContainerName}`);
|
|
67
|
+
const result = await this.docker(['start', this.config.dockerContainerName]);
|
|
68
|
+
(0, spawn_1.assertSuccessful)(result, 'docker start');
|
|
69
|
+
this.state = 'running';
|
|
70
|
+
this.updatedAt = Date.now();
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
this.fail(error);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async stop() {
|
|
77
|
+
this.state = 'stopping';
|
|
78
|
+
this.updatedAt = Date.now();
|
|
79
|
+
try {
|
|
80
|
+
const exists = await this.containerExists();
|
|
81
|
+
if (exists) {
|
|
82
|
+
const result = await this.docker(['stop', this.config.dockerContainerName], true);
|
|
83
|
+
if (result.code !== 0)
|
|
84
|
+
this.pushLog(`docker stop warning: ${result.stderr.trim() || result.stdout.trim()}`);
|
|
85
|
+
}
|
|
86
|
+
this.state = 'stopped';
|
|
87
|
+
this.updatedAt = Date.now();
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
this.fail(error);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async restart() {
|
|
94
|
+
await this.stop();
|
|
95
|
+
await this.removeContainer(true);
|
|
96
|
+
await this.build();
|
|
97
|
+
if (this.state === 'error' || this.state === 'blocked')
|
|
98
|
+
return;
|
|
99
|
+
await this.start();
|
|
100
|
+
}
|
|
101
|
+
async createContainer() {
|
|
102
|
+
const root = (0, paths_1.resolveMaibotRoot)(this.config);
|
|
103
|
+
const agreements = (0, agreements_1.prepareAgreementEnv)(root, { accept: this.config.acceptMaibotAgreements });
|
|
104
|
+
if (!agreements.ok) {
|
|
105
|
+
this.state = 'blocked';
|
|
106
|
+
throw new Error(`blocked: ${agreements.reason}`);
|
|
107
|
+
}
|
|
108
|
+
this.ensureVolumeDirs();
|
|
109
|
+
const args = this.createRunArgs(agreements.env);
|
|
110
|
+
this.pushLog(`creating Docker container: ${this.config.dockerContainerName}`);
|
|
111
|
+
const result = await this.docker(args);
|
|
112
|
+
(0, spawn_1.assertSuccessful)(result, 'docker create');
|
|
113
|
+
}
|
|
114
|
+
createRunArgs(agreementEnv) {
|
|
115
|
+
const args = [
|
|
116
|
+
'create',
|
|
117
|
+
'--name',
|
|
118
|
+
this.config.dockerContainerName,
|
|
119
|
+
'--restart',
|
|
120
|
+
'unless-stopped',
|
|
121
|
+
];
|
|
122
|
+
if (this.config.dockerNetwork) {
|
|
123
|
+
args.push('--network', this.config.dockerNetwork);
|
|
124
|
+
}
|
|
125
|
+
if (this.config.dockerPublishedWebuiPort > 0) {
|
|
126
|
+
args.push('-p', `${this.config.dockerPublishedWebuiPort}:${this.config.webuiPort}`);
|
|
127
|
+
}
|
|
128
|
+
const env = this.buildEnv(agreementEnv);
|
|
129
|
+
for (const [key, value] of Object.entries(env)) {
|
|
130
|
+
args.push('-e', `${key}=${value}`);
|
|
131
|
+
}
|
|
132
|
+
for (const [source, target] of this.volumeMap()) {
|
|
133
|
+
args.push('-v', `${source}:${target}`);
|
|
134
|
+
}
|
|
135
|
+
args.push(this.config.dockerImageName);
|
|
136
|
+
return args;
|
|
137
|
+
}
|
|
138
|
+
buildEnv(agreementEnv) {
|
|
139
|
+
return {
|
|
140
|
+
...agreementEnv,
|
|
141
|
+
TZ: process.env.TZ || 'Asia/Shanghai',
|
|
142
|
+
MAIBOT_WORKER_PROCESS: '1',
|
|
143
|
+
MAIBOT_KOISHI_MODE: '1',
|
|
144
|
+
MAIBOT_KOISHI_API_HOST: '0.0.0.0',
|
|
145
|
+
MAIBOT_KOISHI_API_PORT: String(this.config.apiPort),
|
|
146
|
+
MAIBOT_KOISHI_API_KEY: this.apiKey,
|
|
147
|
+
MAIBOT_KOISHI_LEGACY_HOST: '0.0.0.0',
|
|
148
|
+
MAIBOT_KOISHI_LEGACY_PORT: String(this.config.legacyPort),
|
|
149
|
+
MAIBOT_KOISHI_WEBUI_ENABLED: this.config.webuiEnabled ? '1' : '0',
|
|
150
|
+
MAIBOT_KOISHI_WEBUI_HOST: this.config.webuiHost,
|
|
151
|
+
MAIBOT_KOISHI_WEBUI_PORT: String(this.config.webuiPort),
|
|
152
|
+
WEBUI_HOST: this.config.webuiHost,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
volumeMap() {
|
|
156
|
+
const dataDir = (0, paths_1.resolveMaiKoDataDir)();
|
|
157
|
+
return [
|
|
158
|
+
[(0, path_1.join)(dataDir, 'config'), '/MaiMBot/config'],
|
|
159
|
+
[(0, path_1.join)(dataDir, 'data'), '/MaiMBot/data'],
|
|
160
|
+
[(0, path_1.join)(dataDir, 'plugins'), '/MaiMBot/plugins'],
|
|
161
|
+
[(0, path_1.join)(dataDir, 'logs'), '/MaiMBot/logs'],
|
|
162
|
+
[(0, path_1.join)(dataDir, 'depends-data'), '/MaiMBot/depends-data'],
|
|
163
|
+
];
|
|
164
|
+
}
|
|
165
|
+
ensureVolumeDirs() {
|
|
166
|
+
for (const [source] of this.volumeMap()) {
|
|
167
|
+
(0, fs_1.mkdirSync)(source, { recursive: true });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async containerExists() {
|
|
171
|
+
const result = await this.run(this.config.dockerCommand, ['inspect', this.config.dockerContainerName], false);
|
|
172
|
+
return result.code === 0;
|
|
173
|
+
}
|
|
174
|
+
async removeContainer(force = false) {
|
|
175
|
+
const exists = await this.containerExists();
|
|
176
|
+
if (!exists)
|
|
177
|
+
return;
|
|
178
|
+
const args = ['rm'];
|
|
179
|
+
if (force)
|
|
180
|
+
args.push('-f');
|
|
181
|
+
args.push(this.config.dockerContainerName);
|
|
182
|
+
const result = await this.docker(args, true);
|
|
183
|
+
if (result.code !== 0) {
|
|
184
|
+
this.pushLog(`docker rm warning: ${result.stderr.trim() || result.stdout.trim()}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async docker(args, allowFailure = false) {
|
|
188
|
+
const result = await this.run(this.config.dockerCommand, args);
|
|
189
|
+
if (!allowFailure && result.code !== 0) {
|
|
190
|
+
(0, spawn_1.assertSuccessful)(result, `${this.config.dockerCommand} ${args[0]}`);
|
|
191
|
+
}
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
run(command, args, logOutput = true) {
|
|
195
|
+
return (0, spawn_1.runCommand)(command, args, {
|
|
196
|
+
onLine: logOutput ? (line, stream) => this.pushLog(`${stream}: ${this.sanitize(line)}`) : undefined,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
fail(error) {
|
|
200
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
201
|
+
this.lastError = message;
|
|
202
|
+
this.state = message.startsWith('blocked:') ? 'blocked' : 'error';
|
|
203
|
+
this.updatedAt = Date.now();
|
|
204
|
+
this.logger.warn(this.sanitize(message));
|
|
205
|
+
}
|
|
206
|
+
pushLog(line) {
|
|
207
|
+
const normalized = this.sanitize(line);
|
|
208
|
+
if (!normalized)
|
|
209
|
+
return;
|
|
210
|
+
this.logs.push(normalized);
|
|
211
|
+
if (this.logs.length > this.config.logLines) {
|
|
212
|
+
this.logs.splice(0, this.logs.length - this.config.logLines);
|
|
213
|
+
}
|
|
214
|
+
this.logger.info(normalized);
|
|
215
|
+
}
|
|
216
|
+
sanitize(line) {
|
|
217
|
+
let result = line
|
|
218
|
+
.replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, '')
|
|
219
|
+
.replace(/\bsk-[A-Za-z0-9_-]{12,}\b/g, 'sk-***REDACTED***')
|
|
220
|
+
.trimEnd();
|
|
221
|
+
for (const secret of [this.apiKey, encodeURIComponent(this.apiKey), this.config.apiKey]) {
|
|
222
|
+
if (!secret)
|
|
223
|
+
continue;
|
|
224
|
+
result = result.split(secret).join((0, command_1.redactSecret)(secret));
|
|
225
|
+
}
|
|
226
|
+
return result;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
exports.MaibotDockerManager = MaibotDockerManager;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type Context } from 'koishi';
|
|
2
|
+
import type { Config } from '../config';
|
|
3
|
+
export declare class ExternalLogForwarder {
|
|
4
|
+
private ctx;
|
|
5
|
+
private config;
|
|
6
|
+
private secrets;
|
|
7
|
+
private logger;
|
|
8
|
+
private child?;
|
|
9
|
+
private logs;
|
|
10
|
+
private partial;
|
|
11
|
+
private stopping;
|
|
12
|
+
private forceEnabled;
|
|
13
|
+
private restartDispose?;
|
|
14
|
+
constructor(ctx: Context, config: Config, secrets?: string[]);
|
|
15
|
+
get isRunning(): boolean;
|
|
16
|
+
getLogs(): string[];
|
|
17
|
+
start(force?: boolean): void;
|
|
18
|
+
stop(): void;
|
|
19
|
+
private scheduleRestart;
|
|
20
|
+
private clearRestart;
|
|
21
|
+
private setTimer;
|
|
22
|
+
private buildCommandLine;
|
|
23
|
+
private pushLog;
|
|
24
|
+
private flushPartial;
|
|
25
|
+
private writeLine;
|
|
26
|
+
private isDebugLine;
|
|
27
|
+
private isWarningLine;
|
|
28
|
+
private sanitizeLine;
|
|
29
|
+
}
|