newmark-agent 0.4.8 → 0.5.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/dist/conversation-utility-host.bundle.cjs +238 -29
- package/dist/core/agent.d.ts +1 -0
- package/dist/core/agent.js +102 -10
- package/dist/core/agentKernelRunner.js +1 -1
- package/dist/core/autoRouter.d.ts +1 -1
- package/dist/core/autoRouter.js +16 -7
- package/dist/core/conversationKernel.d.ts +13 -8
- package/dist/core/conversationKernel.js +111 -10
- package/dist/core/electronUtilityAgentClient.d.ts +2 -8
- package/dist/core/electronUtilityRuntimePool.d.ts +3 -15
- package/dist/core/utilityAgentProtocol.d.ts +2 -8
- package/dist/core/workEventCoalescer.d.ts +16 -0
- package/dist/core/workEventCoalescer.js +52 -0
- package/dist/core/wslAgentClient.d.ts +2 -8
- package/dist/core/wslAgentProtocol.d.ts +2 -8
- package/dist/core/wslAgentRuntimePool.d.ts +3 -15
- package/dist/llm/provider.d.ts +5 -0
- package/dist/llm/provider.js +63 -1
- package/dist/main.js +221 -175
- package/dist/preload.js +2 -0
- package/dist/server.d.ts +17 -1
- package/dist/server.js +331 -9
- package/dist/ui/index.html +295 -51
- package/dist/wsl-agent-host.bundle.cjs +238 -29
- package/package.json +10 -5
package/dist/server.js
CHANGED
|
@@ -33,12 +33,18 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.MOBILE_WORKSPACE_UPLOAD_MAX_BYTES = void 0;
|
|
37
|
+
exports.configureHostedServer = configureHostedServer;
|
|
38
|
+
exports.stopHostedServer = stopHostedServer;
|
|
39
|
+
exports.hostedServerStatus = hostedServerStatus;
|
|
40
|
+
exports.setHostedServerEnabled = setHostedServerEnabled;
|
|
36
41
|
exports.runServer = runServer;
|
|
37
42
|
exports.setHostedServerAutomation = setHostedServerAutomation;
|
|
38
43
|
const http = __importStar(require("http"));
|
|
39
44
|
const fs = __importStar(require("fs"));
|
|
40
45
|
const path = __importStar(require("path"));
|
|
41
46
|
const os = __importStar(require("os"));
|
|
47
|
+
const crypto_1 = require("crypto");
|
|
42
48
|
const uiPreferences_1 = require("./core/uiPreferences");
|
|
43
49
|
const child_process_1 = require("child_process");
|
|
44
50
|
const agent_1 = require("./core/agent");
|
|
@@ -49,6 +55,7 @@ const workspaceFileRouter_1 = require("./core/workspaceFileRouter");
|
|
|
49
55
|
const nativeBash_1 = require("./core/nativeBash");
|
|
50
56
|
const installUpdate_1 = require("./core/installUpdate");
|
|
51
57
|
const mobilePairing_1 = require("./core/mobilePairing");
|
|
58
|
+
const workEventCoalescer_1 = require("./core/workEventCoalescer");
|
|
52
59
|
const PORT = 47890;
|
|
53
60
|
let agent = null;
|
|
54
61
|
let automation = null;
|
|
@@ -59,6 +66,14 @@ const mobileWorkEventSubscribers = new Set();
|
|
|
59
66
|
const mobileScopedRuntimes = new Map();
|
|
60
67
|
let hostedWorkEventSink = null;
|
|
61
68
|
let unsubscribeHostedWorkEvents = null;
|
|
69
|
+
let unsubscribeServerAgentWorkEvents = null;
|
|
70
|
+
let hostedServer = null;
|
|
71
|
+
let hostedServerRoot = '';
|
|
72
|
+
let hostedServerOptions = {};
|
|
73
|
+
let hostedServerError = '';
|
|
74
|
+
let hostedServerStartedAt = 0;
|
|
75
|
+
let hostedAccessHost = '';
|
|
76
|
+
let hostedAccessHostCheckedAt = 0;
|
|
62
77
|
let hostedConversationUiState;
|
|
63
78
|
let hostedConversationUiAction;
|
|
64
79
|
let hostedConversationPrompt;
|
|
@@ -74,9 +89,12 @@ function publishMobileWorkEvent(event) {
|
|
|
74
89
|
}
|
|
75
90
|
}
|
|
76
91
|
function publishServerWorkEvent(event) {
|
|
92
|
+
serverWorkEventCoalescer.push(event);
|
|
93
|
+
}
|
|
94
|
+
const serverWorkEventCoalescer = new workEventCoalescer_1.WorkEventCoalescer(event => {
|
|
77
95
|
publishMobileWorkEvent(event);
|
|
78
96
|
hostedWorkEventSink?.(event);
|
|
79
|
-
}
|
|
97
|
+
});
|
|
80
98
|
function mobileAuthorized(req) {
|
|
81
99
|
if (!mobileToken)
|
|
82
100
|
return false;
|
|
@@ -112,6 +130,8 @@ const MOBILE_EDITABLE_EXTENSIONS = new Set([
|
|
|
112
130
|
'.bash', '.zsh', '.ps1', '.bat', '.cmd', '.sql', '.tex', '.typ', '.properties', '.gradle', '.gitignore',
|
|
113
131
|
]);
|
|
114
132
|
const MOBILE_EDITOR_MAX_BYTES = 1024 * 1024;
|
|
133
|
+
const MAX_API_BODY_BYTES = 30 * 1024 * 1024;
|
|
134
|
+
exports.MOBILE_WORKSPACE_UPLOAD_MAX_BYTES = 64 * 1024 * 1024;
|
|
115
135
|
function resolveMobileWorkspacePath(ws, relativePath) {
|
|
116
136
|
const root = path.resolve(ws.path);
|
|
117
137
|
const clean = String(relativePath || '').replace(/\\/g, '/').replace(/^\/+/, '');
|
|
@@ -130,6 +150,156 @@ function mobileEditableFile(target) {
|
|
|
130
150
|
const base = path.basename(target).toLowerCase();
|
|
131
151
|
return MOBILE_EDITABLE_EXTENSIONS.has(path.extname(base)) || MOBILE_EDITABLE_EXTENSIONS.has(base);
|
|
132
152
|
}
|
|
153
|
+
function validMobileUploadDirectory(value) {
|
|
154
|
+
const clean = String(value || '').replace(/\\/g, '/');
|
|
155
|
+
return !path.isAbsolute(clean)
|
|
156
|
+
&& !/^[A-Za-z]:\//.test(clean)
|
|
157
|
+
&& !clean.split('/').some(segment => segment === '..' || segment.includes('\0'));
|
|
158
|
+
}
|
|
159
|
+
function validMobileUploadFileName(value) {
|
|
160
|
+
const clean = String(value || '');
|
|
161
|
+
return clean.length > 0
|
|
162
|
+
&& clean.length <= 255
|
|
163
|
+
&& clean !== '.'
|
|
164
|
+
&& clean !== '..'
|
|
165
|
+
&& !clean.includes('/')
|
|
166
|
+
&& !clean.includes('\\')
|
|
167
|
+
&& !clean.includes('\0')
|
|
168
|
+
&& path.basename(clean) === clean;
|
|
169
|
+
}
|
|
170
|
+
function numberedUploadName(fileName, index) {
|
|
171
|
+
if (index <= 0)
|
|
172
|
+
return fileName;
|
|
173
|
+
const extension = path.extname(fileName);
|
|
174
|
+
const stem = fileName.slice(0, fileName.length - extension.length);
|
|
175
|
+
return `${stem} (${index})${extension}`;
|
|
176
|
+
}
|
|
177
|
+
async function handleMobileWorkspaceUpload(req, res) {
|
|
178
|
+
if (agent && !agent.config.getBool('remote', 'touch_enabled')) {
|
|
179
|
+
mobileJson(res, { error: 'Remote touch disabled' }, 403);
|
|
180
|
+
req.resume();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (!mobileAuthorized(req)) {
|
|
184
|
+
mobileJson(res, { error: 'Unauthorized' }, 401);
|
|
185
|
+
req.resume();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (!agent) {
|
|
189
|
+
mobileJson(res, { error: 'Agent not initialized' }, 500);
|
|
190
|
+
req.resume();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const url = new URL(req.url || '/', `http://localhost:${PORT}`);
|
|
194
|
+
const workspaceId = url.searchParams.get('workspaceId') || '';
|
|
195
|
+
const directory = url.searchParams.get('directory') || '';
|
|
196
|
+
const fileName = url.searchParams.get('fileName') || '';
|
|
197
|
+
const ws = resolveMobileWorkspace(agent, workspaceId);
|
|
198
|
+
if (!ws) {
|
|
199
|
+
mobileJson(res, { error: 'Unknown workspace' }, 404);
|
|
200
|
+
req.resume();
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (!validMobileUploadDirectory(directory) || !validMobileUploadFileName(fileName)) {
|
|
204
|
+
mobileJson(res, { error: 'Invalid upload path or file name' }, 400);
|
|
205
|
+
req.resume();
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const declaredLength = Number(req.headers['content-length'] || -1);
|
|
209
|
+
if (Number.isFinite(declaredLength) && declaredLength > exports.MOBILE_WORKSPACE_UPLOAD_MAX_BYTES) {
|
|
210
|
+
mobileJson(res, { error: 'Upload exceeds the 64 MiB limit' }, 413);
|
|
211
|
+
req.resume();
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
let temporaryPath = '';
|
|
215
|
+
try {
|
|
216
|
+
const resolved = resolveMobileWorkspacePath(ws, directory);
|
|
217
|
+
const realRoot = await fs.promises.realpath(resolved.root);
|
|
218
|
+
if (directory === 'Uploaded') {
|
|
219
|
+
await fs.promises.mkdir(resolved.target, { recursive: true });
|
|
220
|
+
}
|
|
221
|
+
const realDirectory = await fs.promises.realpath(resolved.target);
|
|
222
|
+
const canonicalRelative = path.relative(realRoot, realDirectory);
|
|
223
|
+
if (canonicalRelative === '..' || canonicalRelative.startsWith(`..${path.sep}`) || path.isAbsolute(canonicalRelative)) {
|
|
224
|
+
throw new Error('Path escapes workspace through a symbolic link');
|
|
225
|
+
}
|
|
226
|
+
const directoryStat = await fs.promises.stat(realDirectory);
|
|
227
|
+
if (!directoryStat.isDirectory()) {
|
|
228
|
+
mobileJson(res, { error: 'Upload destination is not a directory' }, 400);
|
|
229
|
+
req.resume();
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
temporaryPath = path.join(realDirectory, `.newmark-upload-${(0, crypto_1.randomUUID)()}.tmp`);
|
|
233
|
+
const handle = await fs.promises.open(temporaryPath, 'wx', 0o600);
|
|
234
|
+
const hash = (0, crypto_1.createHash)('sha256');
|
|
235
|
+
let size = 0;
|
|
236
|
+
try {
|
|
237
|
+
for await (const part of req) {
|
|
238
|
+
const chunk = Buffer.isBuffer(part) ? part : Buffer.from(part);
|
|
239
|
+
size += chunk.length;
|
|
240
|
+
if (size > exports.MOBILE_WORKSPACE_UPLOAD_MAX_BYTES)
|
|
241
|
+
throw new RangeError('Upload exceeds the 64 MiB limit');
|
|
242
|
+
hash.update(chunk);
|
|
243
|
+
let offset = 0;
|
|
244
|
+
while (offset < chunk.length) {
|
|
245
|
+
const { bytesWritten } = await handle.write(chunk, offset, chunk.length - offset);
|
|
246
|
+
if (bytesWritten <= 0)
|
|
247
|
+
throw new Error('Unable to write the complete upload');
|
|
248
|
+
offset += bytesWritten;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
await handle.sync();
|
|
252
|
+
}
|
|
253
|
+
finally {
|
|
254
|
+
await handle.close();
|
|
255
|
+
}
|
|
256
|
+
let finalPath = '';
|
|
257
|
+
let finalName = '';
|
|
258
|
+
let published = false;
|
|
259
|
+
for (let index = 0; index < 10_000; index++) {
|
|
260
|
+
finalName = numberedUploadName(fileName, index);
|
|
261
|
+
finalPath = path.join(realDirectory, finalName);
|
|
262
|
+
try {
|
|
263
|
+
// A hard link publishes the already-complete inode atomically and fails
|
|
264
|
+
// with EEXIST, so parallel uploads can never overwrite one another.
|
|
265
|
+
await fs.promises.link(temporaryPath, finalPath);
|
|
266
|
+
published = true;
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
if (error?.code === 'EEXIST')
|
|
271
|
+
continue;
|
|
272
|
+
throw error;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (!published)
|
|
276
|
+
throw new Error('Unable to allocate a unique upload name');
|
|
277
|
+
await fs.promises.unlink(temporaryPath);
|
|
278
|
+
temporaryPath = '';
|
|
279
|
+
const relativePath = path.posix.join(resolved.relative, finalName).replace(/^\.\//, '');
|
|
280
|
+
const suppliedMime = String(req.headers['content-type'] || 'application/octet-stream').split(';')[0].trim();
|
|
281
|
+
const mimeType = /^[\w.+-]+\/[\w.+-]+$/.test(suppliedMime) ? suppliedMime : 'application/octet-stream';
|
|
282
|
+
mobileJson(res, {
|
|
283
|
+
ok: true,
|
|
284
|
+
file: { name: finalName, path: relativePath, size, mimeType, sha256: hash.digest('hex'), created: true },
|
|
285
|
+
}, 201);
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
if (temporaryPath)
|
|
289
|
+
await fs.promises.unlink(temporaryPath).catch(() => { });
|
|
290
|
+
if (error instanceof RangeError)
|
|
291
|
+
mobileJson(res, { error: error.message }, 413);
|
|
292
|
+
else if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') {
|
|
293
|
+
mobileJson(res, { error: 'Upload destination does not exist' }, 400);
|
|
294
|
+
}
|
|
295
|
+
else if (String(error?.message || '').toLowerCase().includes('workspace')) {
|
|
296
|
+
mobileJson(res, { error: 'Invalid upload destination' }, 400);
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
mobileJson(res, { error: 'Upload could not be stored' }, 500);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
133
303
|
function mobileRightSidebarState(current, ws, conversationId) {
|
|
134
304
|
const scoped = mobileScopedAgent(ws, conversationId);
|
|
135
305
|
return {
|
|
@@ -1107,7 +1277,7 @@ async function handleApi(req, res, body) {
|
|
|
1107
1277
|
const workspaceId = String(params.workspaceId || '');
|
|
1108
1278
|
const conversationId = String(params.conversationId || '');
|
|
1109
1279
|
const action = String(params.action || '');
|
|
1110
|
-
const allowed = new Set(['goal_update', 'goal_guide', 'conversation_guide', 'goal_toggle_pause', 'goal_clear', 'flow_pause', 'flow_resume', 'flow_guide', 'conversation_stop', 'input_mode', 'queue_enqueue', 'queue_update', 'queue_delete', 'queue_toggle_pause', 'queue_guide']);
|
|
1280
|
+
const allowed = new Set(['goal_update', 'goal_guide', 'conversation_guide', 'goal_toggle_pause', 'goal_clear', 'flow_pause', 'flow_resume', 'flow_guide', 'conversation_stop', 'input_mode', 'queue_enqueue', 'queue_update', 'queue_delete', 'queue_reorder', 'queue_toggle_pause', 'queue_guide']);
|
|
1111
1281
|
if (!workspaceId || !conversationId || !allowed.has(action)) {
|
|
1112
1282
|
mobileJson(res, { error: 'workspaceId, conversationId, and a valid action are required' }, 400);
|
|
1113
1283
|
return;
|
|
@@ -1448,7 +1618,8 @@ function startServer(root, options = {}) {
|
|
|
1448
1618
|
agent.setConversationFromStorage(previousConversation);
|
|
1449
1619
|
}
|
|
1450
1620
|
});
|
|
1451
|
-
|
|
1621
|
+
unsubscribeServerAgentWorkEvents?.();
|
|
1622
|
+
unsubscribeServerAgentWorkEvents = agent.subscribeWorkEvents(publishServerWorkEvent);
|
|
1452
1623
|
if (automation) {
|
|
1453
1624
|
agent.setAutomationManager(automation);
|
|
1454
1625
|
if (!options.automation)
|
|
@@ -1470,10 +1641,30 @@ function startServer(root, options = {}) {
|
|
|
1470
1641
|
handleMobileEvents(req, res);
|
|
1471
1642
|
return;
|
|
1472
1643
|
}
|
|
1644
|
+
if (req.method === 'POST' && url.pathname === '/api/mobile/workspace-file-upload') {
|
|
1645
|
+
await handleMobileWorkspaceUpload(req, res);
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1473
1648
|
if (req.method === 'POST' && url.pathname.startsWith('/api/')) {
|
|
1474
1649
|
let body = '';
|
|
1475
|
-
|
|
1476
|
-
|
|
1650
|
+
let bodyBytes = 0;
|
|
1651
|
+
let rejected = false;
|
|
1652
|
+
req.on('data', chunk => {
|
|
1653
|
+
if (rejected)
|
|
1654
|
+
return;
|
|
1655
|
+
bodyBytes += Buffer.byteLength(chunk);
|
|
1656
|
+
if (bodyBytes > MAX_API_BODY_BYTES) {
|
|
1657
|
+
rejected = true;
|
|
1658
|
+
if (url.pathname.startsWith('/api/mobile/'))
|
|
1659
|
+
mobileJson(res, { error: 'Request body too large' }, 413);
|
|
1660
|
+
else
|
|
1661
|
+
jsonResponse(res, { error: 'Request body too large' }, 413);
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1664
|
+
body += chunk;
|
|
1665
|
+
});
|
|
1666
|
+
req.on('end', () => { if (!rejected)
|
|
1667
|
+
handleApi(req, res, body); });
|
|
1477
1668
|
return;
|
|
1478
1669
|
}
|
|
1479
1670
|
if (url.pathname.startsWith('/api/')) {
|
|
@@ -1492,6 +1683,14 @@ function startServer(root, options = {}) {
|
|
|
1492
1683
|
const lan = (0, mobilePairing_1.lanIpv4)();
|
|
1493
1684
|
const accessHost = tailscale || lan || '<lan-or-tailscale-ip>';
|
|
1494
1685
|
const tokenPath = path.join(root, '.newmark-mobile-token');
|
|
1686
|
+
hostedServer = server;
|
|
1687
|
+
hostedServerError = '';
|
|
1688
|
+
hostedServerStartedAt = Date.now();
|
|
1689
|
+
server.once('error', error => {
|
|
1690
|
+
hostedServerError = error instanceof Error ? error.message : String(error);
|
|
1691
|
+
if (hostedServer === server)
|
|
1692
|
+
hostedServer = null;
|
|
1693
|
+
});
|
|
1495
1694
|
server.listen(PORT, bindHost, () => {
|
|
1496
1695
|
console.log(`\n Newmark Agent v1.0 - Server Mode`);
|
|
1497
1696
|
console.log(` Bind: ${bindHost}:${PORT}`);
|
|
@@ -1503,14 +1702,137 @@ function startServer(root, options = {}) {
|
|
|
1503
1702
|
console.log(` Mobile events (SSE): http://${accessHost}:${PORT}/api/mobile/events?token=<token>`);
|
|
1504
1703
|
console.log(` Press Ctrl+C to stop\n`);
|
|
1505
1704
|
});
|
|
1705
|
+
return server;
|
|
1706
|
+
}
|
|
1707
|
+
function configuredAccessHost() {
|
|
1708
|
+
const now = Date.now();
|
|
1709
|
+
if (now - hostedAccessHostCheckedAt < 30_000)
|
|
1710
|
+
return hostedAccessHost;
|
|
1711
|
+
hostedAccessHost = (0, mobilePairing_1.tailscaleIpv4)() || (0, mobilePairing_1.lanIpv4)() || '';
|
|
1712
|
+
hostedAccessHostCheckedAt = now;
|
|
1713
|
+
return hostedAccessHost;
|
|
1714
|
+
}
|
|
1715
|
+
function waitForHostedServerListening(timeoutMs = 5000) {
|
|
1716
|
+
if (hostedServer?.listening)
|
|
1717
|
+
return Promise.resolve();
|
|
1718
|
+
return new Promise((resolve, reject) => {
|
|
1719
|
+
const deadline = Date.now() + timeoutMs;
|
|
1720
|
+
const tick = () => {
|
|
1721
|
+
if (hostedServer?.listening)
|
|
1722
|
+
return resolve();
|
|
1723
|
+
if (hostedServerError)
|
|
1724
|
+
return reject(new Error(hostedServerError));
|
|
1725
|
+
if (Date.now() >= deadline)
|
|
1726
|
+
return reject(new Error('Mobile server listen timeout'));
|
|
1727
|
+
setTimeout(tick, 50);
|
|
1728
|
+
};
|
|
1729
|
+
tick();
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1732
|
+
async function probeHostedServer(host) {
|
|
1733
|
+
if (!hostedServer?.listening || !mobileToken)
|
|
1734
|
+
return { ok: false, error: hostedServerError || 'Mobile server is not listening' };
|
|
1735
|
+
return await new Promise(resolve => {
|
|
1736
|
+
const request = http.get({
|
|
1737
|
+
hostname: host,
|
|
1738
|
+
port: PORT,
|
|
1739
|
+
path: `/api/mobile/hello?token=${encodeURIComponent(mobileToken)}`,
|
|
1740
|
+
timeout: 1800,
|
|
1741
|
+
}, response => {
|
|
1742
|
+
let body = '';
|
|
1743
|
+
response.setEncoding('utf8');
|
|
1744
|
+
response.on('data', chunk => { body += chunk; });
|
|
1745
|
+
response.on('end', () => {
|
|
1746
|
+
try {
|
|
1747
|
+
const parsed = JSON.parse(body || '{}');
|
|
1748
|
+
const ok = response.statusCode === 200 && parsed.ok === true;
|
|
1749
|
+
resolve({ ok, error: ok ? '' : String(parsed.error || `HTTP ${response.statusCode || 0}`) });
|
|
1750
|
+
}
|
|
1751
|
+
catch (error) {
|
|
1752
|
+
resolve({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
1753
|
+
}
|
|
1754
|
+
});
|
|
1755
|
+
});
|
|
1756
|
+
request.on('timeout', () => request.destroy(new Error('Mobile server probe timeout')));
|
|
1757
|
+
request.on('error', error => resolve({ ok: false, error: error.message }));
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
function configureHostedServer(root, options = {}) {
|
|
1761
|
+
hostedServerRoot = root;
|
|
1762
|
+
hostedServerOptions = options;
|
|
1763
|
+
}
|
|
1764
|
+
async function stopHostedServer() {
|
|
1765
|
+
const server = hostedServer;
|
|
1766
|
+
hostedServer = null;
|
|
1767
|
+
if (server) {
|
|
1768
|
+
await new Promise(resolve => {
|
|
1769
|
+
const timer = setTimeout(resolve, 2500);
|
|
1770
|
+
server.close(() => {
|
|
1771
|
+
clearTimeout(timer);
|
|
1772
|
+
resolve();
|
|
1773
|
+
});
|
|
1774
|
+
server.closeAllConnections?.();
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1777
|
+
unsubscribeHostedWorkEvents?.();
|
|
1778
|
+
unsubscribeHostedWorkEvents = null;
|
|
1779
|
+
unsubscribeServerAgentWorkEvents?.();
|
|
1780
|
+
unsubscribeServerAgentWorkEvents = null;
|
|
1781
|
+
hostedServerStartedAt = 0;
|
|
1782
|
+
}
|
|
1783
|
+
async function hostedServerStatus(enabled = true, probeHost = '') {
|
|
1784
|
+
const listening = !!hostedServer?.listening;
|
|
1785
|
+
const host = enabled ? (String(probeHost || '').trim() || configuredAccessHost()) : '';
|
|
1786
|
+
let reachable = false;
|
|
1787
|
+
let error = hostedServerError;
|
|
1788
|
+
if (enabled && listening) {
|
|
1789
|
+
if (!host)
|
|
1790
|
+
error = 'No LAN or Tailscale IPv4 address is available';
|
|
1791
|
+
else {
|
|
1792
|
+
const probe = await probeHostedServer(host);
|
|
1793
|
+
reachable = probe.ok;
|
|
1794
|
+
if (!probe.ok)
|
|
1795
|
+
error = probe.error;
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
const state = !enabled ? 'off' : listening && reachable ? 'listening' : 'error';
|
|
1799
|
+
return {
|
|
1800
|
+
enabled,
|
|
1801
|
+
listening,
|
|
1802
|
+
reachable,
|
|
1803
|
+
state,
|
|
1804
|
+
host,
|
|
1805
|
+
port: PORT,
|
|
1806
|
+
error: state === 'error' ? (error || 'Mobile server is not reachable') : '',
|
|
1807
|
+
checkedAt: new Date().toISOString(),
|
|
1808
|
+
startedAt: hostedServerStartedAt,
|
|
1809
|
+
};
|
|
1810
|
+
}
|
|
1811
|
+
async function setHostedServerEnabled(enabled, probeHost = '') {
|
|
1812
|
+
await stopHostedServer();
|
|
1813
|
+
hostedServerError = '';
|
|
1814
|
+
if (enabled) {
|
|
1815
|
+
if (!hostedServerRoot)
|
|
1816
|
+
throw new Error('Hosted mobile server is not configured');
|
|
1817
|
+
startServer(hostedServerRoot, hostedServerOptions);
|
|
1818
|
+
try {
|
|
1819
|
+
await waitForHostedServerListening();
|
|
1820
|
+
}
|
|
1821
|
+
catch (error) {
|
|
1822
|
+
hostedServerError = error instanceof Error ? error.message : String(error);
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
return await hostedServerStatus(enabled, probeHost);
|
|
1506
1826
|
}
|
|
1507
|
-
let hostedServerStarted = false;
|
|
1508
1827
|
/** 托管启动 server(GUI/TUI 内嵌调用;幂等防重入,进程常驻即服务常驻) */
|
|
1509
1828
|
function runServer(root, options = {}) {
|
|
1510
|
-
if (
|
|
1829
|
+
if (!hostedServerRoot || Object.keys(options).length > 0)
|
|
1830
|
+
configureHostedServer(root, options);
|
|
1831
|
+
else
|
|
1832
|
+
hostedServerRoot = root;
|
|
1833
|
+
if (hostedServer?.listening)
|
|
1511
1834
|
return;
|
|
1512
|
-
|
|
1513
|
-
startServer(root, options);
|
|
1835
|
+
startServer(hostedServerRoot, hostedServerOptions);
|
|
1514
1836
|
}
|
|
1515
1837
|
/** Attach the GUI-owned automation manager after deferred startup. */
|
|
1516
1838
|
function setHostedServerAutomation(manager) {
|