@xcanwin/manyoyo 5.8.5 → 5.8.9

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.
@@ -0,0 +1,215 @@
1
+ 'use strict';
2
+
3
+ const { spawn } = require('child_process');
4
+
5
+ function createTextBuffer(maxChars) {
6
+ let value = '';
7
+ let truncated = false;
8
+ return {
9
+ append(chunk) {
10
+ if (!chunk) {
11
+ return;
12
+ }
13
+ const text = chunk.toString('utf-8');
14
+ if (!text) {
15
+ return;
16
+ }
17
+ if (value.length >= maxChars) {
18
+ truncated = true;
19
+ return;
20
+ }
21
+ const remain = maxChars - value.length;
22
+ if (text.length > remain) {
23
+ value += text.slice(0, remain);
24
+ truncated = true;
25
+ return;
26
+ }
27
+ value += text;
28
+ },
29
+ buildOutput(suffix) {
30
+ return truncated ? `${value}\n...${suffix}` : value;
31
+ }
32
+ };
33
+ }
34
+
35
+ function drainLines(text, carry, handleLine) {
36
+ let pending = carry + String(text || '');
37
+ let newlineIndex = pending.indexOf('\n');
38
+ while (newlineIndex !== -1) {
39
+ const line = pending.slice(0, newlineIndex).replace(/\r$/, '');
40
+ handleLine(line);
41
+ pending = pending.slice(newlineIndex + 1);
42
+ newlineIndex = pending.indexOf('\n');
43
+ }
44
+ return pending;
45
+ }
46
+
47
+ function createWebContainerExecHelpers(options = {}) {
48
+ const buildWebSessionKey = options.buildWebSessionKey || (() => '');
49
+ const defaultAgentId = options.defaultAgentId || 'default';
50
+ const extractAgentMessageFromStructuredOutput = options.extractAgentMessageFromStructuredOutput || (() => '');
51
+ const parseJsonObjectLine = options.parseJsonObjectLine || (() => null);
52
+ const prepareStructuredTraceEvents = options.prepareStructuredTraceEvents || (() => []);
53
+ const extractContentDeltaFromPayload = options.extractContentDeltaFromPayload || (() => null);
54
+ const structuredTraceDeps = options.structuredTraceDeps || {};
55
+ const clipText = options.clipText || (text => String(text || ''));
56
+ const stripAnsi = options.stripAnsi || (text => String(text || ''));
57
+ const maxRawOutputChars = Number.isInteger(options.maxRawOutputChars) ? options.maxRawOutputChars : 32 * 1024 * 1024;
58
+
59
+ function buildFinalOutput(agentProgram, stdoutBuffer, stderrBuffer) {
60
+ const clippedStdout = stdoutBuffer.buildOutput('[stdout-truncated]');
61
+ const clippedStderr = stderrBuffer.buildOutput('[stderr-truncated]');
62
+ const clippedRaw = `${clippedStdout}${clippedStdout && clippedStderr ? '\n' : ''}${clippedStderr}`;
63
+ const extractedAgentMessage = extractAgentMessageFromStructuredOutput(agentProgram, clippedStdout);
64
+ const cleanOutputSource = extractedAgentMessage || clippedRaw;
65
+ return clipText(stripAnsi(cleanOutputSource).trim() || '(无输出)');
66
+ }
67
+
68
+ return {
69
+ async execCommandInWebContainer(ctx, containerName, command, options = {}) {
70
+ const opts = options && typeof options === 'object' ? options : {};
71
+ const agentProgram = typeof opts.agentProgram === 'string' ? opts.agentProgram : '';
72
+ return await new Promise((resolve, reject) => {
73
+ const process = spawn(
74
+ ctx.dockerCmd,
75
+ ['exec', containerName, '/bin/bash', '-lc', command],
76
+ { stdio: ['ignore', 'pipe', 'pipe'] }
77
+ );
78
+
79
+ const stdoutBuffer = createTextBuffer(maxRawOutputChars);
80
+ const stderrBuffer = createTextBuffer(maxRawOutputChars);
81
+
82
+ process.stdout.on('data', chunk => stdoutBuffer.append(chunk));
83
+ process.stderr.on('data', chunk => stderrBuffer.append(chunk));
84
+
85
+ process.on('error', reject);
86
+ process.on('close', code => {
87
+ const exitCode = typeof code === 'number' ? code : 1;
88
+ resolve({
89
+ exitCode,
90
+ output: buildFinalOutput(agentProgram, stdoutBuffer, stderrBuffer)
91
+ });
92
+ });
93
+ });
94
+ },
95
+ async execAgentInWebContainerStream(ctx, state, sessionRefOrContainerName, command, options = {}) {
96
+ const opts = options && typeof options === 'object' ? options : {};
97
+ const sessionRef = typeof sessionRefOrContainerName === 'string'
98
+ ? { containerName: sessionRefOrContainerName, agentId: defaultAgentId }
99
+ : sessionRefOrContainerName;
100
+ const sessionKey = buildWebSessionKey(sessionRef.containerName, sessionRef.agentId);
101
+ const agentProgram = typeof opts.agentProgram === 'string' ? opts.agentProgram : '';
102
+ const onEvent = typeof opts.onEvent === 'function' ? opts.onEvent : () => {};
103
+ const process = spawn(
104
+ ctx.dockerCmd,
105
+ ['exec', sessionRef.containerName, '/bin/bash', '-lc', command],
106
+ { stdio: ['ignore', 'pipe', 'pipe'] }
107
+ );
108
+
109
+ const runState = {
110
+ containerName: sessionRef.containerName,
111
+ sessionKey,
112
+ process,
113
+ command,
114
+ startedAt: new Date().toISOString(),
115
+ stopping: false
116
+ };
117
+ state.agentRuns.set(sessionRef.containerName, runState);
118
+
119
+ return await new Promise((resolve, reject) => {
120
+ const stdoutBuffer = createTextBuffer(maxRawOutputChars);
121
+ const stderrBuffer = createTextBuffer(maxRawOutputChars);
122
+ let stdoutPending = '';
123
+ let stderrPending = '';
124
+ const structuredTraceState = {
125
+ toolNamesById: new Map()
126
+ };
127
+ let contentDeltaAccumulator = '';
128
+
129
+ function emitStdoutTraceLine(line) {
130
+ const rawLine = String(line || '').trim();
131
+ if (!rawLine) {
132
+ return;
133
+ }
134
+ if (agentProgram === 'claude' || agentProgram === 'gemini' || agentProgram === 'codex' || agentProgram === 'opencode') {
135
+ const payload = parseJsonObjectLine(rawLine);
136
+ if (payload) {
137
+ const traceEvents = prepareStructuredTraceEvents(agentProgram, payload, structuredTraceState, structuredTraceDeps);
138
+ traceEvents.forEach(traceEvent => {
139
+ if (!traceEvent || !traceEvent.text) {
140
+ return;
141
+ }
142
+ onEvent({
143
+ type: 'trace',
144
+ stream: 'stdout',
145
+ text: traceEvent.text,
146
+ traceEvent
147
+ });
148
+ });
149
+ const deltaContent = extractContentDeltaFromPayload(agentProgram, payload, structuredTraceDeps);
150
+ if (deltaContent !== null) {
151
+ if (deltaContent.reset) {
152
+ contentDeltaAccumulator = deltaContent.text;
153
+ } else {
154
+ contentDeltaAccumulator += deltaContent.text;
155
+ }
156
+ onEvent({
157
+ type: 'content_delta',
158
+ content: contentDeltaAccumulator
159
+ });
160
+ }
161
+ return;
162
+ }
163
+ if (agentProgram === 'codex' && (/^OpenAI Codex\b/.test(rawLine) || /^tokens used\b/i.test(rawLine))) {
164
+ return;
165
+ }
166
+ }
167
+ onEvent({ type: 'trace', stream: 'stdout', text: rawLine });
168
+ }
169
+
170
+ function emitStderrTraceLine(line) {
171
+ const rawLine = String(line || '').trim();
172
+ if (!rawLine) {
173
+ return;
174
+ }
175
+ onEvent({ type: 'trace', stream: 'stderr', text: `[stderr] ${rawLine}` });
176
+ }
177
+
178
+ process.stdout.on('data', chunk => {
179
+ stdoutBuffer.append(chunk);
180
+ stdoutPending = drainLines(chunk.toString('utf-8'), stdoutPending, emitStdoutTraceLine);
181
+ });
182
+ process.stderr.on('data', chunk => {
183
+ stderrBuffer.append(chunk);
184
+ stderrPending = drainLines(chunk.toString('utf-8'), stderrPending, emitStderrTraceLine);
185
+ });
186
+
187
+ process.on('error', error => {
188
+ state.agentRuns.delete(sessionRef.containerName);
189
+ reject(error);
190
+ });
191
+ process.on('close', code => {
192
+ state.agentRuns.delete(sessionRef.containerName);
193
+ if (stdoutPending) {
194
+ emitStdoutTraceLine(stdoutPending);
195
+ stdoutPending = '';
196
+ }
197
+ if (stderrPending) {
198
+ emitStderrTraceLine(stderrPending);
199
+ stderrPending = '';
200
+ }
201
+ const exitCode = typeof code === 'number' ? code : 1;
202
+ resolve({
203
+ exitCode,
204
+ output: buildFinalOutput(agentProgram, stdoutBuffer, stderrBuffer),
205
+ interrupted: exitCode !== 0 && runState.stopping === true
206
+ });
207
+ });
208
+ });
209
+ }
210
+ };
211
+ }
212
+
213
+ module.exports = {
214
+ createWebContainerExecHelpers
215
+ };
@@ -0,0 +1,163 @@
1
+ 'use strict';
2
+
3
+ function createWebHttpHandlers(deps) {
4
+ const {
5
+ loadTemplate,
6
+ sendHtml,
7
+ sendJson,
8
+ sendRedirect,
9
+ sendStaticAsset,
10
+ sendVendorAsset,
11
+ readJsonBody,
12
+ secureStringEqual,
13
+ createWebAuthSession,
14
+ clearWebAuthSession,
15
+ getWebAuthCookie,
16
+ getWebAuthClearCookie,
17
+ getWebAuthSession,
18
+ AUTH_FRONTEND_ASSETS,
19
+ APP_FRONTEND_ASSETS,
20
+ APP_VENDOR_ASSETS,
21
+ handleWebApi
22
+ } = deps;
23
+
24
+ function serveAllowedStaticAsset(req, res, pathname, pattern, allowedAssets, sendAsset) {
25
+ const matched = req.method === 'GET' ? pathname.match(pattern) : null;
26
+ if (!matched) {
27
+ return false;
28
+ }
29
+ const assetName = matched[1];
30
+ if (!allowedAssets.has(assetName)) {
31
+ sendHtml(res, 404, '<h1>404 Not Found</h1>');
32
+ return true;
33
+ }
34
+ sendAsset(res, assetName);
35
+ return true;
36
+ }
37
+
38
+ async function handleWebAuthRoutes(req, res, pathname, ctx, state) {
39
+ if (req.method === 'GET' && pathname === '/favicon.ico') {
40
+ res.writeHead(204, { 'Cache-Control': 'no-store' });
41
+ res.end();
42
+ return true;
43
+ }
44
+
45
+ if (req.method === 'GET' && pathname === '/auth/login') {
46
+ sendHtml(res, 200, loadTemplate('login.html'));
47
+ return true;
48
+ }
49
+
50
+ if (serveAllowedStaticAsset(req, res, pathname, /^\/auth\/frontend\/([A-Za-z0-9._-]+)$/, AUTH_FRONTEND_ASSETS, sendStaticAsset)) {
51
+ return true;
52
+ }
53
+
54
+ if (req.method === 'POST' && pathname === '/auth/login') {
55
+ const payload = await readJsonBody(req);
56
+ const username = String(payload.username || '').trim();
57
+ const password = String(payload.password || '');
58
+
59
+ if (!username || !password) {
60
+ sendJson(res, 400, { error: '用户名和密码不能为空' });
61
+ return true;
62
+ }
63
+
64
+ const userOk = secureStringEqual(username, ctx.authUser);
65
+ const passOk = secureStringEqual(password, ctx.authPass);
66
+ if (!(userOk && passOk)) {
67
+ sendJson(res, 401, { error: '用户名或密码错误' });
68
+ return true;
69
+ }
70
+
71
+ const sessionId = createWebAuthSession(state, username);
72
+ sendJson(
73
+ res,
74
+ 200,
75
+ { ok: true, username },
76
+ { 'Set-Cookie': getWebAuthCookie(sessionId) }
77
+ );
78
+ return true;
79
+ }
80
+
81
+ if (req.method === 'POST' && pathname === '/auth/logout') {
82
+ clearWebAuthSession(state, req);
83
+ sendJson(
84
+ res,
85
+ 200,
86
+ { ok: true },
87
+ { 'Set-Cookie': getWebAuthClearCookie() }
88
+ );
89
+ return true;
90
+ }
91
+
92
+ return false;
93
+ }
94
+
95
+ function sendWebUnauthorized(res, pathname) {
96
+ if (pathname.startsWith('/api/') || pathname.startsWith('/auth/')) {
97
+ sendJson(res, 401, { error: 'UNAUTHORIZED' });
98
+ return;
99
+ }
100
+ if (pathname === '/' || pathname === '') {
101
+ sendRedirect(res, 302, '/auth/login', { 'Set-Cookie': getWebAuthClearCookie() });
102
+ return;
103
+ }
104
+ sendHtml(
105
+ res,
106
+ 401,
107
+ loadTemplate('login.html'),
108
+ { 'Set-Cookie': getWebAuthClearCookie() }
109
+ );
110
+ }
111
+
112
+ async function handleWebHttpRequest(req, res, pathname, ctx, state) {
113
+ if (await handleWebAuthRoutes(req, res, pathname, ctx, state)) {
114
+ return true;
115
+ }
116
+
117
+ const authSession = getWebAuthSession(state, req);
118
+ if (!authSession) {
119
+ sendWebUnauthorized(res, pathname);
120
+ return true;
121
+ }
122
+
123
+ if (req.method === 'GET' && pathname === '/') {
124
+ sendHtml(res, 200, loadTemplate('app.html'));
125
+ return true;
126
+ }
127
+
128
+ if (serveAllowedStaticAsset(req, res, pathname, /^\/app\/frontend\/([A-Za-z0-9._-]+)$/, APP_FRONTEND_ASSETS, sendStaticAsset)) {
129
+ return true;
130
+ }
131
+
132
+ if (serveAllowedStaticAsset(req, res, pathname, /^\/app\/vendor\/([A-Za-z0-9._-]+)$/, APP_VENDOR_ASSETS, sendVendorAsset)) {
133
+ return true;
134
+ }
135
+
136
+ if (pathname === '/healthz') {
137
+ sendJson(res, 200, { ok: true });
138
+ return true;
139
+ }
140
+
141
+ if (pathname.startsWith('/api/')) {
142
+ const handled = await handleWebApi(req, res, pathname, ctx, state);
143
+ if (!handled) {
144
+ sendJson(res, 404, { error: 'Not Found' });
145
+ }
146
+ return true;
147
+ }
148
+
149
+ sendHtml(res, 404, '<h1>404 Not Found</h1>');
150
+ return true;
151
+ }
152
+
153
+ return {
154
+ serveAllowedStaticAsset,
155
+ handleWebAuthRoutes,
156
+ sendWebUnauthorized,
157
+ handleWebHttpRequest
158
+ };
159
+ }
160
+
161
+ module.exports = {
162
+ createWebHttpHandlers
163
+ };
@@ -0,0 +1,50 @@
1
+ 'use strict';
2
+
3
+ function createWebRuntimeStateHelpers(options = {}) {
4
+ const createMap = () => new Map();
5
+
6
+ return {
7
+ createInitialWebRuntimeState(baseState = {}) {
8
+ return {
9
+ ...baseState,
10
+ authSessions: createMap(),
11
+ terminalSessions: createMap(),
12
+ agentRuns: createMap()
13
+ };
14
+ },
15
+ stopWebAgentRun(state, containerName) {
16
+ const runState = state.agentRuns.get(containerName);
17
+ if (!runState || !runState.process || runState.process.killed) {
18
+ return false;
19
+ }
20
+ runState.stopping = true;
21
+ try {
22
+ runState.process.kill('SIGTERM');
23
+ } catch {
24
+ return false;
25
+ }
26
+ return true;
27
+ },
28
+ cleanupWebRuntimeState(state) {
29
+ for (const session of state.terminalSessions.values()) {
30
+ const ptyProcess = session && session.ptyProcess;
31
+ if (ptyProcess && !ptyProcess.killed) {
32
+ try { ptyProcess.kill('SIGTERM'); } catch {}
33
+ }
34
+ }
35
+ state.terminalSessions.clear();
36
+
37
+ for (const runState of state.agentRuns.values()) {
38
+ const child = runState && runState.process;
39
+ if (child && !child.killed) {
40
+ try { child.kill('SIGTERM'); } catch {}
41
+ }
42
+ }
43
+ state.agentRuns.clear();
44
+ }
45
+ };
46
+ }
47
+
48
+ module.exports = {
49
+ createWebRuntimeStateHelpers
50
+ };
@@ -0,0 +1,71 @@
1
+ 'use strict';
2
+
3
+ const os = require('os');
4
+ const path = require('path');
5
+
6
+ function createWebServerContextHelpers(options = {}) {
7
+ const createInitialWebRuntimeState = options.createInitialWebRuntimeState || (base => base);
8
+ const getDefaultWebConfigPath = options.getDefaultWebConfigPath || (() => '');
9
+
10
+ return {
11
+ createWebServerContext(rawOptions = {}) {
12
+ const fallbackLogger = {
13
+ info: () => {},
14
+ warn: () => {},
15
+ error: () => {}
16
+ };
17
+ const ctx = {
18
+ serverHost: rawOptions.serverHost || '127.0.0.1',
19
+ serverPort: rawOptions.serverPort,
20
+ authUser: rawOptions.authUser,
21
+ authPass: rawOptions.authPass,
22
+ authPassAuto: rawOptions.authPassAuto,
23
+ dockerCmd: rawOptions.dockerCmd,
24
+ hostPath: rawOptions.hostPath,
25
+ containerPath: rawOptions.containerPath,
26
+ imageName: rawOptions.imageName,
27
+ imageVersion: rawOptions.imageVersion,
28
+ execCommandPrefix: rawOptions.execCommandPrefix,
29
+ execCommand: rawOptions.execCommand,
30
+ execCommandSuffix: rawOptions.execCommandSuffix,
31
+ contModeArgs: rawOptions.contModeArgs,
32
+ containerExtraArgs: rawOptions.containerExtraArgs,
33
+ containerEnvs: rawOptions.containerEnvs,
34
+ containerVolumes: rawOptions.containerVolumes,
35
+ containerPorts: rawOptions.containerPorts,
36
+ validateHostPath: rawOptions.validateHostPath,
37
+ formatDate: rawOptions.formatDate,
38
+ isValidContainerName: rawOptions.isValidContainerName,
39
+ containerExists: rawOptions.containerExists,
40
+ getContainerStatus: rawOptions.getContainerStatus,
41
+ waitForContainerReady: rawOptions.waitForContainerReady,
42
+ dockerExecArgs: rawOptions.dockerExecArgs,
43
+ showImagePullHint: rawOptions.showImagePullHint,
44
+ removeContainer: rawOptions.removeContainer,
45
+ logger: rawOptions.logger && typeof rawOptions.logger.info === 'function' ? rawOptions.logger : fallbackLogger,
46
+ colors: rawOptions.colors || {
47
+ GREEN: '',
48
+ CYAN: '',
49
+ YELLOW: '',
50
+ NC: ''
51
+ }
52
+ };
53
+
54
+ if (!ctx.authUser || !ctx.authPass) {
55
+ throw new Error('Web 认证配置缺失,请设置 serve -U / serve -P');
56
+ }
57
+
58
+ return ctx;
59
+ },
60
+ createWebServerState(rawOptions = {}) {
61
+ return createInitialWebRuntimeState({
62
+ webHistoryDir: rawOptions.webHistoryDir || path.join(os.homedir(), '.manyoyo', 'web-history'),
63
+ webConfigPath: rawOptions.webConfigPath || getDefaultWebConfigPath()
64
+ });
65
+ }
66
+ };
67
+ }
68
+
69
+ module.exports = {
70
+ createWebServerContextHelpers
71
+ };
@@ -0,0 +1,129 @@
1
+ 'use strict';
2
+
3
+ function createWebServerLifecycleHelpers(options = {}) {
4
+ const http = options.http;
5
+ const WebSocket = options.WebSocket;
6
+ const formatUrlHost = options.formatUrlHost || (host => host);
7
+ const normalizeTerminalSize = options.normalizeTerminalSize || ((cols, rows) => ({ cols, rows }));
8
+ const bindTerminalWebSocket = options.bindTerminalWebSocket || (() => {});
9
+ const handleWebUpgradeRequest = options.handleWebUpgradeRequest || (() => {});
10
+ const sendJson = options.sendJson || (() => {});
11
+ const sendHtml = options.sendHtml || (() => {});
12
+ const cleanupWebRuntimeState = options.cleanupWebRuntimeState || (() => {});
13
+
14
+ return {
15
+ createWsServer(ctx, state) {
16
+ const wsServer = new WebSocket.Server({
17
+ noServer: true,
18
+ maxPayload: 1024 * 1024
19
+ });
20
+ wsServer.on('error', err => {
21
+ ctx.logger.error('ws server error', err);
22
+ });
23
+
24
+ wsServer.on('connection', (ws, req, meta = {}) => {
25
+ const containerName = meta.containerName;
26
+ if (!containerName || !ctx.isValidContainerName(containerName)) {
27
+ ws.close();
28
+ return;
29
+ }
30
+ const { cols, rows } = normalizeTerminalSize(meta.cols, meta.rows);
31
+ bindTerminalWebSocket(ctx, state, ws, containerName, cols, rows);
32
+ });
33
+
34
+ return wsServer;
35
+ },
36
+ createHttpServer(ctx, state, wsServer, handleWebHttpRequest) {
37
+ const server = http.createServer(async (req, res) => {
38
+ try {
39
+ const fallbackHost = `${formatUrlHost(ctx.serverHost)}:${ctx.serverPort}`;
40
+ const url = new URL(req.url, `http://${req.headers.host || fallbackHost}`);
41
+ const pathname = url.pathname;
42
+ await handleWebHttpRequest(req, res, pathname, ctx, state);
43
+ } catch (error) {
44
+ ctx.logger.error('http request error', {
45
+ method: req && req.method ? req.method : '',
46
+ url: req && req.url ? req.url : '',
47
+ message: error && error.message ? error.message : 'Server Error'
48
+ });
49
+ if ((req.url || '').startsWith('/api/')) {
50
+ sendJson(res, 500, { error: error.message || 'Server Error' });
51
+ } else {
52
+ sendHtml(res, 500, '<h1>500 Server Error</h1>');
53
+ }
54
+ }
55
+ });
56
+ server.on('error', err => {
57
+ ctx.logger.error('http server error', err);
58
+ });
59
+ server.on('close', () => {
60
+ ctx.logger.warn('http server closed');
61
+ });
62
+ server.on('upgrade', (req, socket, head) => {
63
+ handleWebUpgradeRequest(req, socket, head, wsServer, ctx, state, server.__manyoyoListenPort || ctx.serverPort);
64
+ });
65
+ return server;
66
+ },
67
+ async listenWebServer(server, ctx) {
68
+ let listenPort = ctx.serverPort;
69
+ await new Promise((resolve, reject) => {
70
+ server.once('error', err => {
71
+ ctx.logger.error('http server listen failed', err);
72
+ reject(err);
73
+ });
74
+ server.listen(ctx.serverPort, ctx.serverHost, () => {
75
+ const address = server.address();
76
+ if (address && typeof address === 'object' && typeof address.port === 'number') {
77
+ listenPort = address.port;
78
+ }
79
+ server.__manyoyoListenPort = listenPort;
80
+ const { GREEN, CYAN, YELLOW, NC } = ctx.colors;
81
+ const listenHost = formatUrlHost(ctx.serverHost);
82
+ console.log(`${GREEN}✅ MANYOYO Web 服务已启动: http://${listenHost}:${listenPort}${NC}`);
83
+ console.log(`${CYAN}提示: 左侧是 manyoyo 容器会话列表,中间是活动/终端/配置/检查工作台,右侧显示当前会话上下文。${NC}`);
84
+ if (ctx.serverHost === '0.0.0.0') {
85
+ console.log(`${CYAN}提示: 当前监听全部网卡,请用本机局域网 IP 访问。${NC}`);
86
+ }
87
+ console.log(`${CYAN}🔐 登录用户名: ${YELLOW}${ctx.authUser}${NC}`);
88
+ if (ctx.authPassAuto) {
89
+ console.log(`${CYAN}🔐 登录密码(本次随机): ${YELLOW}${ctx.authPass}${NC}`);
90
+ } else {
91
+ console.log(`${CYAN}🔐 登录密码: 使用你配置的 serve -P / serverPass / MANYOYO_SERVER_PASS${NC}`);
92
+ }
93
+ ctx.logger.info('web server started', {
94
+ host: ctx.serverHost,
95
+ port: listenPort,
96
+ authUser: ctx.authUser,
97
+ authPassAuto: Boolean(ctx.authPassAuto)
98
+ });
99
+ resolve();
100
+ });
101
+ });
102
+ return listenPort;
103
+ },
104
+ closeWebServer(server, wsServer, ctx, state) {
105
+ return new Promise(resolve => {
106
+ ctx.logger.info('web server closing');
107
+ cleanupWebRuntimeState(state);
108
+
109
+ const closeHttp = () => {
110
+ if (!server.listening) {
111
+ resolve();
112
+ return;
113
+ }
114
+ server.close(() => resolve());
115
+ };
116
+
117
+ try {
118
+ wsServer.close(() => closeHttp());
119
+ } catch {
120
+ closeHttp();
121
+ }
122
+ });
123
+ }
124
+ };
125
+ }
126
+
127
+ module.exports = {
128
+ createWebServerLifecycleHelpers
129
+ };