@xcanwin/manyoyo 8.3.3 → 8.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/README.md +1 -1
- package/bin/manyoyo.js +111 -48
- package/docker/manyoyo.Dockerfile +41 -8
- package/docker/res/agent/update-agents.sh +1 -1
- package/docker/res/playwright/browser.json +9 -12
- package/docker/res/playwright/chrome-policy.json +3 -0
- package/docker/res/playwright/env +3 -0
- package/docker/res/playwright/playwright-cli.sh +5 -0
- package/docker/res/playwright/stealth.init.js +11 -9
- package/lib/browser-background.js +23 -0
- package/lib/container-env.sh +34 -0
- package/lib/container-exec.js +132 -0
- package/lib/container-id.js +11 -0
- package/lib/container-init.sh +40 -0
- package/lib/container-manage-options.js +35 -0
- package/lib/container-network.js +615 -0
- package/lib/container-run.js +27 -3
- package/lib/container-state.js +467 -0
- package/lib/egress-denied.js +128 -0
- package/lib/egress-proxy.js +315 -0
- package/lib/egress-sidecar-main.js +112 -0
- package/lib/egress-sidecar.js +276 -0
- package/lib/env-text.js +62 -0
- package/lib/json5-text-edit.js +5 -5
- package/lib/network-endpoints.js +99 -0
- package/lib/network-policy.js +269 -0
- package/lib/plugin/fingerprint.js +65 -50
- package/lib/plugin/playwright-extensions.js +0 -9
- package/lib/plugin/playwright-server.js +22 -2
- package/lib/plugin/playwright.js +55 -16
- package/lib/port-forward.js +137 -0
- package/lib/web/AGENTS.md +4 -0
- package/lib/web/container-manage.js +506 -0
- package/lib/web/denied-notify.js +52 -0
- package/lib/web/index.html +81 -77
- package/lib/web/server.js +352 -135
- package/manyoyo.example.json +20 -2
- package/package.json +13 -8
package/lib/web/server.js
CHANGED
|
@@ -9,9 +9,23 @@ const { consumeLoginToken, isLoopbackHost } = require('../login-token');
|
|
|
9
9
|
const http = require('http');
|
|
10
10
|
const WebSocket = require('ws');
|
|
11
11
|
const JSON5 = require('json5');
|
|
12
|
-
const { buildContainerRunArgs } = require('../container-run');
|
|
12
|
+
const { buildContainerRunArgs, hasNetworkArg } = require('../container-run');
|
|
13
13
|
const { buildContainerIntegration, mergeIntegration } = require('../plugin');
|
|
14
14
|
const { applyAptMirror } = require('../mirrors');
|
|
15
|
+
const containerState = require('../container-state');
|
|
16
|
+
const { buildExecArgs, resolveContainerId } = require('../container-exec');
|
|
17
|
+
const { createNetworkManager, NETWORK_NAME } = require('../container-network');
|
|
18
|
+
const { normalizePolicy } = require('../network-policy');
|
|
19
|
+
const { withEnvEndpoints } = require('../network-endpoints');
|
|
20
|
+
const { egressPaths, isSidecarName } = require('../egress-sidecar');
|
|
21
|
+
const { read: readDenied } = require('../egress-denied');
|
|
22
|
+
const { isBrowserBackground } = require('../browser-background');
|
|
23
|
+
const { createDeniedNotifier } = require('./denied-notify');
|
|
24
|
+
const { domainAllowed } = require('../network-policy');
|
|
25
|
+
const { createPortForwarder } = require('../port-forward');
|
|
26
|
+
const { createContainerManageRoutes, pendingRisks } = require('./container-manage');
|
|
27
|
+
const { resolveManageOptions } = require('../container-manage-options');
|
|
28
|
+
const { defaultPolicy } = require('../network-policy');
|
|
15
29
|
const { normalizeMirrors } = require('../runtime-normalizers');
|
|
16
30
|
const { estimateContainerCapacity } = require('../capacity');
|
|
17
31
|
const { extractAgentMessageFromCodexJsonl } = require('../codex-output');
|
|
@@ -1188,6 +1202,21 @@ function accumulateUsageTotal(baseline, turnUsage) {
|
|
|
1188
1202
|
};
|
|
1189
1203
|
}
|
|
1190
1204
|
|
|
1205
|
+
// 所有进用户容器的 exec 都经这里:env 从状态目录现读(见 lib/container-exec.js),子进程结束后删临时 env 文件
|
|
1206
|
+
function spawnContainerExec(ctx, containerName, execOptions, spawnOptions) {
|
|
1207
|
+
const exec = buildExecArgs({ homeDir: ctx.homeDir || os.homedir(), dockerExecArgs: ctx.dockerExecArgs }, containerName, execOptions);
|
|
1208
|
+
let child;
|
|
1209
|
+
try {
|
|
1210
|
+
child = spawn(ctx.dockerCmd, exec.args, { ...spawnOptions, env: ctx.dockerEnv });
|
|
1211
|
+
} catch (e) {
|
|
1212
|
+
exec.cleanup();
|
|
1213
|
+
throw e;
|
|
1214
|
+
}
|
|
1215
|
+
child.once('close', exec.cleanup);
|
|
1216
|
+
child.once('error', exec.cleanup);
|
|
1217
|
+
return child;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1191
1220
|
// 起一个短命的容器内子进程,通过对应协议问一次元信息(初始化握手 / model 列表),拿到结果就杀掉进程——
|
|
1192
1221
|
// 不是长驻会话,不会产生真实对话轮次或费用。
|
|
1193
1222
|
function runAgentProtocolProcess(ctx, containerName, command, options = {}) {
|
|
@@ -1200,7 +1229,7 @@ function runAgentProtocolProcess(ctx, containerName, command, options = {}) {
|
|
|
1200
1229
|
let stdoutAll = '';
|
|
1201
1230
|
let child;
|
|
1202
1231
|
try {
|
|
1203
|
-
child =
|
|
1232
|
+
child = spawnContainerExec(ctx, containerName, { interactive: true, command: ['/bin/bash', '-lc', command] }, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
1204
1233
|
} catch (e) {
|
|
1205
1234
|
resolve({ result: null, stdout: '' });
|
|
1206
1235
|
return;
|
|
@@ -2493,40 +2522,16 @@ function normalizeStringArray(value, sourceLabel) {
|
|
|
2493
2522
|
.filter(Boolean);
|
|
2494
2523
|
}
|
|
2495
2524
|
|
|
2496
|
-
|
|
2525
|
+
// 环境变量文件:只解析路径并检查存在,内容每次 exec 现读(见 lib/container-state.js 的 readEnvFiles)
|
|
2526
|
+
function resolveEnvFilePath(filePath) {
|
|
2497
2527
|
const resolvedPath = expandHomeAliasPath(filePath);
|
|
2498
2528
|
if (!path.isAbsolute(resolvedPath)) {
|
|
2499
|
-
throw new Error(
|
|
2529
|
+
throw new Error(`环境变量文件仅支持绝对路径: ${filePath}`);
|
|
2500
2530
|
}
|
|
2501
2531
|
if (!fs.existsSync(resolvedPath)) {
|
|
2502
|
-
throw new Error(
|
|
2532
|
+
throw new Error(`未找到环境变量文件: ${resolvedPath}`);
|
|
2503
2533
|
}
|
|
2504
|
-
|
|
2505
|
-
const content = fs.readFileSync(resolvedPath, 'utf-8');
|
|
2506
|
-
const args = [];
|
|
2507
|
-
const lines = content.split('\n');
|
|
2508
|
-
|
|
2509
|
-
for (let line of lines) {
|
|
2510
|
-
const match = line.match(/^(?:export\s+)?([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.*)$/);
|
|
2511
|
-
if (!match) {
|
|
2512
|
-
continue;
|
|
2513
|
-
}
|
|
2514
|
-
|
|
2515
|
-
const key = match[1];
|
|
2516
|
-
let value = match[2].trim();
|
|
2517
|
-
if (/[\r\n\0]/.test(value)) continue;
|
|
2518
|
-
if (/[\$\(\)\`\|\&\*\{\};<>]/.test(value)) continue;
|
|
2519
|
-
if (/^\(/.test(value)) continue;
|
|
2520
|
-
|
|
2521
|
-
if (/^"(.*)"$/.test(value)) {
|
|
2522
|
-
value = value.slice(1, -1);
|
|
2523
|
-
} else if (/^'(.*)'$/.test(value)) {
|
|
2524
|
-
value = value.slice(1, -1);
|
|
2525
|
-
}
|
|
2526
|
-
args.push('--env', `${key}=${value}`);
|
|
2527
|
-
}
|
|
2528
|
-
|
|
2529
|
-
return args;
|
|
2534
|
+
return resolvedPath;
|
|
2530
2535
|
}
|
|
2531
2536
|
|
|
2532
2537
|
function resolveContainerModeArgs(mode) {
|
|
@@ -2693,7 +2698,7 @@ function buildConfigDefaults(ctx, config) {
|
|
|
2693
2698
|
const defaults = {
|
|
2694
2699
|
containerName: hasOwn(parsed, 'containerName') ? String(parsed.containerName || '') : '',
|
|
2695
2700
|
hostPath: sanitizeDefaultHostPath(pickFirstString(parsed.hostPath, ctx.hostPath)),
|
|
2696
|
-
containerPath: pickFirstString(parsed.containerPath
|
|
2701
|
+
containerPath: pickFirstString(parsed.containerPath),
|
|
2697
2702
|
imageName: pickFirstString(parsed.imageName, ctx.imageName),
|
|
2698
2703
|
imageVersion: pickFirstString(parsed.imageVersion, ctx.imageVersion),
|
|
2699
2704
|
containerMode: hasOwn(parsed, 'containerMode') ? String(parsed.containerMode || '') : '',
|
|
@@ -2747,6 +2752,7 @@ function buildStaticContainerRuntime(ctx, containerName) {
|
|
|
2747
2752
|
contModeArgs: Array.isArray(ctx.contModeArgs) ? ctx.contModeArgs.slice() : [],
|
|
2748
2753
|
containerExtraArgs: Array.isArray(ctx.containerExtraArgs) ? ctx.containerExtraArgs.slice() : [],
|
|
2749
2754
|
containerEnvs: Array.isArray(ctx.containerEnvs) ? ctx.containerEnvs.slice() : [],
|
|
2755
|
+
envFiles: Array.isArray(ctx.envFiles) ? ctx.envFiles.slice() : [],
|
|
2750
2756
|
containerVolumes: Array.isArray(ctx.containerVolumes) ? ctx.containerVolumes.slice() : [],
|
|
2751
2757
|
containerPorts: Array.isArray(ctx.containerPorts) ? ctx.containerPorts.slice() : [],
|
|
2752
2758
|
defaultCommand: buildDefaultCommand(ctx.execCommandPrefix, ctx.execCommand, ctx.execCommandSuffix) || '/bin/bash'
|
|
@@ -2797,7 +2803,9 @@ function buildCreateRuntime(ctx, state, payload) {
|
|
|
2797
2803
|
defaults: {
|
|
2798
2804
|
hostPath: ctx.hostPath,
|
|
2799
2805
|
containerName: `my-${ctx.formatDate()}`,
|
|
2800
|
-
|
|
2806
|
+
// 不用 ctx.containerPath:那是 serve 进程的启动目录(CLI 的 cwd 语义),与用户要挂载的目录无关;
|
|
2807
|
+
// 没配置也没传时落到下面的 `|| hostPath`,容器内路径与宿主机路径一致
|
|
2808
|
+
containerPath: '',
|
|
2801
2809
|
imageName: ctx.imageName,
|
|
2802
2810
|
imageVersion: ctx.imageVersion
|
|
2803
2811
|
},
|
|
@@ -2818,6 +2826,12 @@ function buildCreateRuntime(ctx, state, payload) {
|
|
|
2818
2826
|
parseServerListen: () => ({ host: '', port: 0 })
|
|
2819
2827
|
});
|
|
2820
2828
|
|
|
2829
|
+
// 自启动与网络:请求 > runs.<name> > 全局配置;请求里放开限制(open / 非本机绑定)要带 confirmRisk
|
|
2830
|
+
const manage = resolveManageOptions({ requestOptions, runConfig, globalConfig: config });
|
|
2831
|
+
if (manage.fromRequest && requestOptions.confirmRisk !== true && pendingRisks(defaultPolicy(), manage.network).length) {
|
|
2832
|
+
throw new Error('该网络设置会放开限制(open 预设或绑定非本机地址),需要确认(confirmRisk)');
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2821
2835
|
const containerName = resolvedBase.containerName;
|
|
2822
2836
|
validateContainerNameStrict(containerName);
|
|
2823
2837
|
|
|
@@ -2886,6 +2900,7 @@ function buildCreateRuntime(ctx, state, payload) {
|
|
|
2886
2900
|
const resumeSupported = Boolean(buildAgentResumeCommand(agentProgram));
|
|
2887
2901
|
|
|
2888
2902
|
let containerEnvs = Array.isArray(ctx.containerEnvs) ? ctx.containerEnvs.slice() : [];
|
|
2903
|
+
let envFiles = Array.isArray(ctx.envFiles) ? ctx.envFiles.slice() : [];
|
|
2889
2904
|
const hasRunEnv = hasOwn(runConfig, 'env');
|
|
2890
2905
|
const hasRunEnvFile = hasOwn(runConfig, 'envFile');
|
|
2891
2906
|
if (hasRequestEnv || hasRequestEnvFile || hasRunEnv || hasRunEnvFile || hasConfigEnv || hasConfigEnvFile) {
|
|
@@ -2896,13 +2911,8 @@ function buildCreateRuntime(ctx, state, payload) {
|
|
|
2896
2911
|
envArgs.push('--env', `${parsed.key}=${parsed.value}`);
|
|
2897
2912
|
});
|
|
2898
2913
|
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
envFileList.forEach(filePath => {
|
|
2902
|
-
envFileArgs.push(...parseEnvFileToArgs(filePath));
|
|
2903
|
-
});
|
|
2904
|
-
|
|
2905
|
-
containerEnvs = [...envArgs, ...envFileArgs];
|
|
2914
|
+
containerEnvs = envArgs;
|
|
2915
|
+
envFiles = resolvedBase.envFile.map(resolveEnvFilePath);
|
|
2906
2916
|
}
|
|
2907
2917
|
|
|
2908
2918
|
let containerVolumes = Array.isArray(ctx.containerVolumes) ? ctx.containerVolumes.slice() : [];
|
|
@@ -2934,8 +2944,11 @@ function buildCreateRuntime(ctx, state, payload) {
|
|
|
2934
2944
|
contModeArgs,
|
|
2935
2945
|
containerExtraArgs: Array.isArray(ctx.containerExtraArgs) ? ctx.containerExtraArgs.slice() : [],
|
|
2936
2946
|
containerEnvs,
|
|
2947
|
+
envFiles,
|
|
2937
2948
|
containerVolumes,
|
|
2938
2949
|
containerPorts,
|
|
2950
|
+
autostart: manage.autostart,
|
|
2951
|
+
network: manage.network,
|
|
2939
2952
|
agentPromptCommand,
|
|
2940
2953
|
defaultCommand: buildDefaultCommand(shellPrefix, shell, shellSuffix) || '/bin/bash',
|
|
2941
2954
|
// 仅供服务端内部复用(如"创建相同配置容器"/"复制容器"),绝不能透传给任何对外响应字段,
|
|
@@ -2954,7 +2967,10 @@ function buildCreateRuntime(ctx, state, payload) {
|
|
|
2954
2967
|
env: resolvedBase.env || {},
|
|
2955
2968
|
envFile: resolvedBase.envFile || [],
|
|
2956
2969
|
volumes: resolvedBase.volumes || [],
|
|
2957
|
-
ports: resolvedBase.ports || []
|
|
2970
|
+
ports: resolvedBase.ports || [],
|
|
2971
|
+
autostart: manage.autostart,
|
|
2972
|
+
// 复制容器时端口暴露(宿主机端口会冲突)与 peers(按 id 引用别的容器)不带过去
|
|
2973
|
+
network: { ...manage.network, expose: [], peers: { inbound: [] } }
|
|
2958
2974
|
},
|
|
2959
2975
|
applied: {
|
|
2960
2976
|
containerName,
|
|
@@ -2972,6 +2988,8 @@ function buildCreateRuntime(ctx, state, payload) {
|
|
|
2972
2988
|
resumeSupported,
|
|
2973
2989
|
yolo: yolo || '',
|
|
2974
2990
|
envCount: Math.floor(containerEnvs.length / 2),
|
|
2991
|
+
autostartEnabled: Boolean(manage.autostart.trim()),
|
|
2992
|
+
networkPreset: manage.network.preset,
|
|
2975
2993
|
volumeCount: Math.floor(containerVolumes.length / 2),
|
|
2976
2994
|
portCount: Math.floor(containerPorts.length / 2)
|
|
2977
2995
|
}
|
|
@@ -3015,7 +3033,11 @@ function buildCloneCreateOptionsFromSnapshot(runtimeSnapshot, fallbackHistory) {
|
|
|
3015
3033
|
env: runtimeSnapshot.env || {},
|
|
3016
3034
|
envFile: runtimeSnapshot.envFile || [],
|
|
3017
3035
|
volumes: runtimeSnapshot.volumes || [],
|
|
3018
|
-
ports: runtimeSnapshot.ports || []
|
|
3036
|
+
ports: runtimeSnapshot.ports || [],
|
|
3037
|
+
autostart: runtimeSnapshot.autostart || '',
|
|
3038
|
+
network: runtimeSnapshot.network || undefined,
|
|
3039
|
+
// 快照来自服务端、原容器创建时已确认过,复制不必再确认
|
|
3040
|
+
confirmRisk: runtimeSnapshot.network ? true : undefined
|
|
3019
3041
|
};
|
|
3020
3042
|
}
|
|
3021
3043
|
|
|
@@ -3138,7 +3160,7 @@ function listWebManyoyoContainers(ctx) {
|
|
|
3138
3160
|
const candidates = [];
|
|
3139
3161
|
output.trim().split('\n').forEach(line => {
|
|
3140
3162
|
const [name, status, image] = line.split('\t');
|
|
3141
|
-
if (!ctx.isValidContainerName(name)) {
|
|
3163
|
+
if (!ctx.isValidContainerName(name) || isSidecarName(name)) {
|
|
3142
3164
|
return;
|
|
3143
3165
|
}
|
|
3144
3166
|
const imageName = image || '';
|
|
@@ -3295,13 +3317,114 @@ async function cleanupStaleSetupTestContainers(ctx) {
|
|
|
3295
3317
|
const names = String(listed || '').split('\n').map(line => line.trim())
|
|
3296
3318
|
.filter(name => name.startsWith(SETUP_TEST_CONTAINER_PREFIX) && /^[A-Za-z0-9_.-]+$/.test(name));
|
|
3297
3319
|
for (const name of names) {
|
|
3320
|
+
const stateId = String(await ctx.dockerExecAsync(
|
|
3321
|
+
['inspect', '-f', '{{index .Config.Labels "manyoyo.id"}}', name], { timeout: 10000 }
|
|
3322
|
+
).catch(() => '') || '').trim();
|
|
3298
3323
|
await ctx.dockerExecAsync(['rm', '-f', name], { timeout: 20000 }).catch(() => {});
|
|
3324
|
+
if (stateId) containerState.removeState(ctx.homeDir || os.homedir(), stateId);
|
|
3299
3325
|
}
|
|
3300
3326
|
} catch (e) {
|
|
3301
3327
|
ctx.logger.warn('cleanup stale setup test containers failed', { message: e && e.message });
|
|
3302
3328
|
}
|
|
3303
3329
|
}
|
|
3304
3330
|
|
|
3331
|
+
// Agent 一轮对话里,容器被过滤代理拦下的新域名(浏览器后台请求不算):只在 allowlist 容器上轮询
|
|
3332
|
+
function createWebDeniedNotifier(ctx, sessionRef, emit) {
|
|
3333
|
+
try {
|
|
3334
|
+
const homeDir = ctx.homeDir || os.homedir();
|
|
3335
|
+
const id = resolveContainerId(ctx.dockerExecArgs, sessionRef.containerName);
|
|
3336
|
+
if (!id || !containerState.stateExists(homeDir, id)) return null;
|
|
3337
|
+
const deniedDir = egressPaths(homeDir).denied;
|
|
3338
|
+
const policyOf = () => normalizePolicy(containerState.readNetworkRaw(homeDir, id) || undefined);
|
|
3339
|
+
if (policyOf().preset !== 'allowlist') return null;
|
|
3340
|
+
return createDeniedNotifier({
|
|
3341
|
+
read: () => readDenied(deniedDir, id),
|
|
3342
|
+
isIgnored: host => isBrowserBackground(host) || domainAllowed(policyOf().egress.domains, host),
|
|
3343
|
+
emit
|
|
3344
|
+
});
|
|
3345
|
+
} catch (e) {
|
|
3346
|
+
return null;
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
|
|
3350
|
+
function getWebPortForwarder(ctx, state) {
|
|
3351
|
+
if (!state.portForwarder) {
|
|
3352
|
+
const homeDir = ctx.homeDir || os.homedir();
|
|
3353
|
+
state.portForwarder = createPortForwarder({
|
|
3354
|
+
command: ctx.dockerCmd,
|
|
3355
|
+
env: ctx.dockerEnv,
|
|
3356
|
+
homeDir,
|
|
3357
|
+
// id → 当前容器名:只信运行时里真实存在的(serve 启动时与接口调用时登记),meta.json 里的旧名字可能已被同名新容器占用
|
|
3358
|
+
resolveName: id => (state.containerNames && state.containerNames.get(id)) || '',
|
|
3359
|
+
warn: message => ctx.logger.warn(message)
|
|
3360
|
+
});
|
|
3361
|
+
}
|
|
3362
|
+
return state.portForwarder;
|
|
3363
|
+
}
|
|
3364
|
+
|
|
3365
|
+
// 运行时就绪后的容器服务:规则对账 + start 事件订阅(外部重启后补下发)、serve 启动时拉起标记了“自动拉起”的容器、
|
|
3366
|
+
// 恢复端口暴露。没有 watchStarts 的替身管理器(单测)不启动任何后台服务。
|
|
3367
|
+
async function startContainerServices(ctx, state) {
|
|
3368
|
+
const manager = getWebNetworkManager(ctx, state);
|
|
3369
|
+
const homeDir = ctx.homeDir || os.homedir();
|
|
3370
|
+
sweepStaleExecEnvFiles(homeDir);
|
|
3371
|
+
const reconcile = () => reconcileContainerServices(ctx, state, manager);
|
|
3372
|
+
state.networkWatch = manager.watchStarts({ onReconnect: reconcile });
|
|
3373
|
+
await reconcile();
|
|
3374
|
+
}
|
|
3375
|
+
|
|
3376
|
+
// 拉起 autostartOnServe 的容器并恢复端口暴露:serve 启动时做一次,运行时重启后(事件流重连)再做一次,幂等
|
|
3377
|
+
async function reconcileContainerServices(ctx, state, manager) {
|
|
3378
|
+
const homeDir = ctx.homeDir || os.homedir();
|
|
3379
|
+
const forwarder = getWebPortForwarder(ctx, state);
|
|
3380
|
+
// 以运行时里真实存在的容器为准(按 id 对应名字);状态目录里有、容器却没有的是孤儿,既不拉起也不转发
|
|
3381
|
+
state.containerNames = new Map((await manager.listManaged()).map(item => [item.id, item.name]));
|
|
3382
|
+
for (const id of containerState.listStateIds(homeDir)) {
|
|
3383
|
+
const name = state.containerNames.get(id);
|
|
3384
|
+
if (!name) continue;
|
|
3385
|
+
let policy;
|
|
3386
|
+
try { policy = normalizePolicy(containerState.readNetworkRaw(homeDir, id) || undefined); } catch (e) { continue; }
|
|
3387
|
+
if (policy.autostartOnServe) {
|
|
3388
|
+
try {
|
|
3389
|
+
if (ctx.getContainerStatus(name) !== 'running') await ctx.dockerExecAsync(['start', name], { timeout: 30000 });
|
|
3390
|
+
} catch (e) {
|
|
3391
|
+
ctx.logger.warn('autostart container failed', { name, message: e && e.message });
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
3394
|
+
if (policy.expose.length) await forwarder.sync(id, policy.expose);
|
|
3395
|
+
}
|
|
3396
|
+
}
|
|
3397
|
+
|
|
3398
|
+
// serve 异常退出时 exec 的临时 env 文件(明文)可能残留:启动时清掉超过 6 小时的(正在跑的长任务不会这么久还没读完它)
|
|
3399
|
+
function sweepStaleExecEnvFiles(homeDir) {
|
|
3400
|
+
const dir = path.join(homeDir, '.manyoyo', 'tmp');
|
|
3401
|
+
try {
|
|
3402
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
3403
|
+
if (!/^(exec|env)-/.test(entry)) continue;
|
|
3404
|
+
const file = path.join(dir, entry);
|
|
3405
|
+
if (Date.now() - fs.statSync(file).mtimeMs > 6 * 3600 * 1000) fs.rmSync(file, { force: true });
|
|
3406
|
+
}
|
|
3407
|
+
} catch (e) { /* 没有目录 */ }
|
|
3408
|
+
}
|
|
3409
|
+
|
|
3410
|
+
// 运行时就绪后启动 startContainerServices
|
|
3411
|
+
function startContainerNetworkWatch(ctx, state) {
|
|
3412
|
+
const manager = getWebNetworkManager(ctx, state);
|
|
3413
|
+
if (typeof manager.watchStarts !== 'function' || state.networkWatch || state.containerServicesStarting) return;
|
|
3414
|
+
state.containerServicesStarting = true;
|
|
3415
|
+
const deadline = Date.now() + 10 * 60 * 1000;
|
|
3416
|
+
const tick = () => {
|
|
3417
|
+
const status = ctx.runtimeState.status;
|
|
3418
|
+
if (status === 'ready') {
|
|
3419
|
+
startContainerServices(ctx, state).catch(e => ctx.logger.warn('container services failed', { message: e && e.message }));
|
|
3420
|
+
return;
|
|
3421
|
+
}
|
|
3422
|
+
if (status === 'failed' || Date.now() > deadline) return;
|
|
3423
|
+
setTimeout(tick, 1000).unref();
|
|
3424
|
+
};
|
|
3425
|
+
tick();
|
|
3426
|
+
}
|
|
3427
|
+
|
|
3305
3428
|
function startStaleSetupCleanup(ctx) {
|
|
3306
3429
|
const deadline = Date.now() + 10 * 60 * 1000;
|
|
3307
3430
|
const tick = () => {
|
|
@@ -3347,6 +3470,24 @@ async function buildWebPlaywrightIntegration(ctx, state, runtime) {
|
|
|
3347
3470
|
return mergeIntegration(runtime, integration);
|
|
3348
3471
|
}
|
|
3349
3472
|
|
|
3473
|
+
// serve 进程的网络规则下发器(一份,懒创建)
|
|
3474
|
+
function getWebNetworkManager(ctx, state) {
|
|
3475
|
+
if (ctx.networkManager) return ctx.networkManager;
|
|
3476
|
+
if (!state.networkManager) {
|
|
3477
|
+
state.networkManager = createNetworkManager({
|
|
3478
|
+
command: ctx.dockerCmd,
|
|
3479
|
+
env: ctx.dockerEnv,
|
|
3480
|
+
homeDir: ctx.homeDir || os.homedir(),
|
|
3481
|
+
imageRef: () => `${ctx.imageName}:${ctx.imageVersion}`,
|
|
3482
|
+
getMirrorUrls: () => Object.values(readWebMirrors(state)).filter(Boolean),
|
|
3483
|
+
warn: message => ctx.logger.warn(message)
|
|
3484
|
+
});
|
|
3485
|
+
}
|
|
3486
|
+
return state.networkManager;
|
|
3487
|
+
}
|
|
3488
|
+
|
|
3489
|
+
// 进入容器前一律检查网络规则(宿主机侧的“本次启动已下发”状态):文件浏览也会 exec /bin/bash -lc,
|
|
3490
|
+
// 里面的 .bashrc 等是 Agent 能改的代码,不能在规则缺失的窗口里跑
|
|
3350
3491
|
async function ensureWebContainer(ctx, state, containerInput, messageSessionRef = null) {
|
|
3351
3492
|
const runtime = typeof containerInput === 'string'
|
|
3352
3493
|
? buildStaticContainerRuntime(ctx, containerInput)
|
|
@@ -3358,8 +3499,27 @@ async function ensureWebContainer(ctx, state, containerInput, messageSessionRef
|
|
|
3358
3499
|
|
|
3359
3500
|
if (!ctx.containerExists(runtime.containerName)) {
|
|
3360
3501
|
const mirrors = readWebMirrors(state);
|
|
3502
|
+
// 用户 env 不进容器配置,写进状态目录的 box/env,每次 exec 现读(见 lib/container-exec.js)
|
|
3503
|
+
const userEnvLines = containerState.userEnvLines(runtime.containerEnvs);
|
|
3361
3504
|
const playwright = await buildWebPlaywrightIntegration(ctx, state, runtime);
|
|
3505
|
+
const containerEnvs = containerState.stripEnvKeys(playwright.containerEnvs, new Set(userEnvLines.map(containerState.envLineKey)));
|
|
3506
|
+
const networkPolicy = await withEnvEndpoints(normalizePolicy(runtime.network), [...userEnvLines, ...containerState.readEnvFilePaths(runtime.envFiles || []).flatMap(file => file.entries.map(entry => `${entry.key}=${entry.value}`))]);
|
|
3507
|
+
const networkManager = getWebNetworkManager(ctx, state);
|
|
3508
|
+
if (!hasNetworkArg([...(runtime.contModeArgs || []), ...(playwright.containerExtraArgs || [])])) {
|
|
3509
|
+
await networkManager.ensureBridgeNetwork();
|
|
3510
|
+
}
|
|
3511
|
+
const containerStateInfo = containerState.createState({
|
|
3512
|
+
homeDir: ctx.homeDir || os.homedir(),
|
|
3513
|
+
envLines: userEnvLines,
|
|
3514
|
+
envFiles: runtime.envFiles,
|
|
3515
|
+
autostart: runtime.autostart,
|
|
3516
|
+
network: networkPolicy,
|
|
3517
|
+
netRequired: networkPolicy.preset !== 'open',
|
|
3518
|
+
meta: { name: runtime.containerName }
|
|
3519
|
+
});
|
|
3362
3520
|
const args = buildContainerRunArgs({
|
|
3521
|
+
state: containerStateInfo,
|
|
3522
|
+
defaultNetwork: NETWORK_NAME,
|
|
3363
3523
|
mirrors,
|
|
3364
3524
|
containerName: runtime.containerName,
|
|
3365
3525
|
hostPath: runtime.hostPath,
|
|
@@ -3368,22 +3528,31 @@ async function ensureWebContainer(ctx, state, containerInput, messageSessionRef
|
|
|
3368
3528
|
imageVersion: runtime.imageVersion,
|
|
3369
3529
|
contModeArgs: runtime.contModeArgs,
|
|
3370
3530
|
containerExtraArgs: playwright.containerExtraArgs,
|
|
3371
|
-
containerEnvs
|
|
3531
|
+
containerEnvs,
|
|
3372
3532
|
containerVolumes: playwright.containerVolumes,
|
|
3373
3533
|
containerPorts: runtime.containerPorts,
|
|
3374
3534
|
defaultCommand: runtime.defaultCommand
|
|
3375
3535
|
});
|
|
3376
3536
|
|
|
3377
|
-
await ensureWebImage(ctx, state, webImageRef(ctx, runtime), { failFast: true });
|
|
3378
|
-
|
|
3379
3537
|
try {
|
|
3538
|
+
await ensureWebImage(ctx, state, webImageRef(ctx, runtime), { failFast: true });
|
|
3380
3539
|
ctx.dockerExecArgs(args, { stdio: 'pipe' });
|
|
3381
3540
|
} catch (e) {
|
|
3541
|
+
containerState.removeState(ctx.homeDir || os.homedir(), containerStateInfo.id);
|
|
3382
3542
|
ctx.showImagePullHint(e);
|
|
3383
3543
|
throw e;
|
|
3384
3544
|
}
|
|
3385
3545
|
|
|
3386
3546
|
await ctx.waitForContainerReady(runtime.containerName, { throwOnFailure: true });
|
|
3547
|
+
// 先下发网络规则再做任何 exec;失败即关闭(抛错,容器留着,init 在等门闩)
|
|
3548
|
+
await networkManager.apply(runtime.containerName, { expectId: containerStateInfo.id });
|
|
3549
|
+
// 创建时填的端口暴露:容器已就绪、规则已下发后立即开始转发;端口被占等失败只警告,网络页里能看到没有生效
|
|
3550
|
+
if (networkPolicy.expose.length) {
|
|
3551
|
+
if (!state.containerNames) state.containerNames = new Map();
|
|
3552
|
+
state.containerNames.set(containerStateInfo.id, runtime.containerName);
|
|
3553
|
+
const failures = await getWebPortForwarder(ctx, state).sync(containerStateInfo.id, networkPolicy.expose);
|
|
3554
|
+
failures.forEach(message => console.warn(message));
|
|
3555
|
+
}
|
|
3387
3556
|
applyAptMirror({
|
|
3388
3557
|
dockerExecArgs: ctx.dockerExecArgs,
|
|
3389
3558
|
containerName: runtime.containerName,
|
|
@@ -3409,16 +3578,18 @@ async function ensureWebContainer(ctx, state, containerInput, messageSessionRef
|
|
|
3409
3578
|
`容器 ${runtime.containerName} 已启动。`
|
|
3410
3579
|
);
|
|
3411
3580
|
}
|
|
3581
|
+
await getWebNetworkManager(ctx, state).ensureReady(runtime.containerName);
|
|
3412
3582
|
}
|
|
3413
3583
|
|
|
3414
3584
|
async function execCommandInWebContainer(ctx, containerName, command, options = {}) {
|
|
3415
3585
|
const opts = options && typeof options === 'object' ? options : {};
|
|
3416
3586
|
const agentProgram = typeof opts.agentProgram === 'string' ? opts.agentProgram : '';
|
|
3417
3587
|
return await new Promise((resolve, reject) => {
|
|
3418
|
-
const process =
|
|
3419
|
-
ctx
|
|
3420
|
-
|
|
3421
|
-
{
|
|
3588
|
+
const process = spawnContainerExec(
|
|
3589
|
+
ctx,
|
|
3590
|
+
containerName,
|
|
3591
|
+
{ withEnv: opts.withEnv !== false, command: ['/bin/bash', '-lc', command] },
|
|
3592
|
+
{ stdio: ['ignore', 'pipe', 'pipe'] }
|
|
3422
3593
|
);
|
|
3423
3594
|
|
|
3424
3595
|
const MAX_RAW_OUTPUT_CHARS = 32 * 1024 * 1024;
|
|
@@ -3483,10 +3654,11 @@ __MANYOYO_NODE__`;
|
|
|
3483
3654
|
}
|
|
3484
3655
|
|
|
3485
3656
|
function spawnContainerRawFileStream(ctx, containerName, filePath) {
|
|
3486
|
-
return
|
|
3487
|
-
ctx
|
|
3488
|
-
|
|
3489
|
-
{
|
|
3657
|
+
return spawnContainerExec(
|
|
3658
|
+
ctx,
|
|
3659
|
+
containerName,
|
|
3660
|
+
{ withEnv: false, command: ['cat', '--', filePath] },
|
|
3661
|
+
{ stdio: ['ignore', 'pipe', 'pipe'] }
|
|
3490
3662
|
);
|
|
3491
3663
|
}
|
|
3492
3664
|
|
|
@@ -3516,7 +3688,7 @@ function inferFileLanguage(filePath) {
|
|
|
3516
3688
|
}
|
|
3517
3689
|
|
|
3518
3690
|
async function execJsonCommandInWebContainer(ctx, containerName, command) {
|
|
3519
|
-
const result = await execCommandInWebContainer(ctx, containerName, command);
|
|
3691
|
+
const result = await execCommandInWebContainer(ctx, containerName, command, { withEnv: false });
|
|
3520
3692
|
if (result.exitCode !== 0) {
|
|
3521
3693
|
throw new Error(result.output || '容器命令执行失败');
|
|
3522
3694
|
}
|
|
@@ -3821,10 +3993,11 @@ async function execAgentInWebContainerStream(ctx, state, sessionRefOrContainerNa
|
|
|
3821
3993
|
: reserveWebAgentRun(state, sessionRef);
|
|
3822
3994
|
let process;
|
|
3823
3995
|
try {
|
|
3824
|
-
process =
|
|
3825
|
-
ctx
|
|
3826
|
-
|
|
3827
|
-
{
|
|
3996
|
+
process = spawnContainerExec(
|
|
3997
|
+
ctx,
|
|
3998
|
+
sessionRef.containerName,
|
|
3999
|
+
{ command: ['/bin/bash', '-lc', command] },
|
|
4000
|
+
{ stdio: ['ignore', 'pipe', 'pipe'] }
|
|
3828
4001
|
);
|
|
3829
4002
|
} catch (e) {
|
|
3830
4003
|
// spawn 同步抛错(如 dockerCmd 不存在)时必须释放锁,否则这个容器永远发不出消息
|
|
@@ -4184,9 +4357,14 @@ async function runSetupConnectionTest(ctx, state, agent, envMap) {
|
|
|
4184
4357
|
const workpath = getSetupWorkpath(state);
|
|
4185
4358
|
fs.mkdirSync(workpath, { recursive: true });
|
|
4186
4359
|
const containerName = `manyoyo-setup-test-${crypto.randomBytes(4).toString('hex')}`;
|
|
4187
|
-
const
|
|
4188
|
-
|
|
4360
|
+
const testHome = ctx.homeDir || os.homedir();
|
|
4361
|
+
const containerStateInfo = containerState.createState({
|
|
4362
|
+
homeDir: testHome,
|
|
4363
|
+
envLines: Object.entries(envMap).map(([key, value]) => `${key}=${value}`),
|
|
4364
|
+
meta: { name: containerName }
|
|
4365
|
+
});
|
|
4189
4366
|
const args = buildContainerRunArgs({
|
|
4367
|
+
state: containerStateInfo,
|
|
4190
4368
|
containerName,
|
|
4191
4369
|
hostPath: workpath,
|
|
4192
4370
|
containerPath: '/workspace',
|
|
@@ -4194,7 +4372,7 @@ async function runSetupConnectionTest(ctx, state, agent, envMap) {
|
|
|
4194
4372
|
imageVersion: ctx.imageVersion,
|
|
4195
4373
|
contModeArgs: [],
|
|
4196
4374
|
containerExtraArgs: [],
|
|
4197
|
-
containerEnvs,
|
|
4375
|
+
containerEnvs: [],
|
|
4198
4376
|
containerVolumes: [],
|
|
4199
4377
|
containerPorts: [],
|
|
4200
4378
|
defaultCommand: '/bin/bash'
|
|
@@ -4220,6 +4398,7 @@ async function runSetupConnectionTest(ctx, state, agent, envMap) {
|
|
|
4220
4398
|
} catch (e) {
|
|
4221
4399
|
// 容器可能根本没创建成功
|
|
4222
4400
|
}
|
|
4401
|
+
containerState.removeState(testHome, containerStateInfo.id);
|
|
4223
4402
|
}
|
|
4224
4403
|
}
|
|
4225
4404
|
|
|
@@ -4605,6 +4784,9 @@ function spawnWebTerminalProcess(ctx, containerName, cols, rows) {
|
|
|
4605
4784
|
'if [ -f ~/.bashrc ]; then',
|
|
4606
4785
|
' . ~/.bashrc',
|
|
4607
4786
|
'fi',
|
|
4787
|
+
'if [ -f /run/manyoyo-sys/env.sh ]; then',
|
|
4788
|
+
' . /run/manyoyo-sys/env.sh',
|
|
4789
|
+
'fi',
|
|
4608
4790
|
'if [ -n "${MANYOYO_TERM_COLS:-}" ] && [ -n "${MANYOYO_TERM_ROWS:-}" ]; then',
|
|
4609
4791
|
' COLUMNS="$MANYOYO_TERM_COLS"',
|
|
4610
4792
|
' LINES="$MANYOYO_TERM_ROWS"',
|
|
@@ -4628,20 +4810,16 @@ function spawnWebTerminalProcess(ctx, containerName, cols, rows) {
|
|
|
4628
4810
|
|
|
4629
4811
|
const termValue = process.env.TERM && process.env.TERM !== 'dumb' ? process.env.TERM : 'xterm-256color';
|
|
4630
4812
|
const colorTermValue = process.env.COLORTERM || 'truecolor';
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
'/bin/bash',
|
|
4640
|
-
|
|
4641
|
-
terminalBootstrap
|
|
4642
|
-
];
|
|
4643
|
-
|
|
4644
|
-
return spawn(ctx.dockerCmd, dockerExecArgs, { stdio: ['pipe', 'pipe', 'pipe'], env: ctx.dockerEnv });
|
|
4813
|
+
return spawnContainerExec(ctx, containerName, {
|
|
4814
|
+
interactive: true,
|
|
4815
|
+
envs: [
|
|
4816
|
+
`TERM=${termValue}`,
|
|
4817
|
+
`COLORTERM=${colorTermValue}`,
|
|
4818
|
+
`MANYOYO_TERM_COLS=${String(cols)}`,
|
|
4819
|
+
`MANYOYO_TERM_ROWS=${String(rows)}`
|
|
4820
|
+
],
|
|
4821
|
+
command: ['/bin/bash', '-lc', terminalBootstrap]
|
|
4822
|
+
}, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
4645
4823
|
}
|
|
4646
4824
|
|
|
4647
4825
|
function bindTerminalWebSocket(ctx, state, ws, containerName, cols, rows) {
|
|
@@ -5209,6 +5387,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
5209
5387
|
ctx.retryRuntimeHeal();
|
|
5210
5388
|
if (ctx.autoPullImage) startWebImagePrepare(ctx, state);
|
|
5211
5389
|
startStaleSetupCleanup(ctx);
|
|
5390
|
+
startContainerNetworkWatch(ctx, state);
|
|
5212
5391
|
}
|
|
5213
5392
|
// 镜像拉取失败的条目会一直挂着:清掉后重新拉
|
|
5214
5393
|
if (ctx.runtimeState.status === 'ready' && ctx.autoPullImage) {
|
|
@@ -6096,69 +6275,79 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
6096
6275
|
});
|
|
6097
6276
|
|
|
6098
6277
|
let lastPartialPersistAt = 0;
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
|
|
6104
|
-
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6278
|
+
const handleStreamEvent = event => {
|
|
6279
|
+
// token 级增量:只走网线,不进事件日志,历史落盘按
|
|
6280
|
+
// AGENT_STREAM_PARTIAL_PERSIST_INTERVAL_MS 节流
|
|
6281
|
+
const isPartialDelta = event && event.type === 'content_delta' && event.partial === true;
|
|
6282
|
+
if (event && event.type === 'trace' && event.text) {
|
|
6283
|
+
traceLines.push(String(event.text));
|
|
6284
|
+
if (event.traceEvent && typeof event.traceEvent === 'object') {
|
|
6285
|
+
traceEvents.push(event.traceEvent);
|
|
6286
|
+
}
|
|
6287
|
+
patchWebSessionMessage(state.webHistoryDir, sessionRef, traceMessage && traceMessage.id, {
|
|
6288
|
+
content: traceLines.join('\n'),
|
|
6289
|
+
traceEvents: traceEvents.slice(),
|
|
6290
|
+
pending: true
|
|
6291
|
+
});
|
|
6292
|
+
}
|
|
6293
|
+
if (event && event.type === 'content_delta' && typeof event.content === 'string') {
|
|
6294
|
+
const now = Date.now();
|
|
6295
|
+
const shouldPersist = !isPartialDelta
|
|
6296
|
+
|| now - lastPartialPersistAt >= AGENT_STREAM_PARTIAL_PERSIST_INTERVAL_MS;
|
|
6297
|
+
if (!streamingReplyMessageId) {
|
|
6298
|
+
const streamingReplyMessage = appendWebSessionMessage(
|
|
6299
|
+
state.webHistoryDir,
|
|
6300
|
+
sessionRef,
|
|
6301
|
+
'assistant',
|
|
6302
|
+
event.content,
|
|
6303
|
+
{
|
|
6304
|
+
mode: 'agent',
|
|
6305
|
+
streamingReply: true,
|
|
6116
6306
|
pending: true
|
|
6117
|
-
});
|
|
6118
|
-
}
|
|
6119
|
-
if (event && event.type === 'content_delta' && typeof event.content === 'string') {
|
|
6120
|
-
const now = Date.now();
|
|
6121
|
-
const shouldPersist = !isPartialDelta
|
|
6122
|
-
|| now - lastPartialPersistAt >= AGENT_STREAM_PARTIAL_PERSIST_INTERVAL_MS;
|
|
6123
|
-
if (!streamingReplyMessageId) {
|
|
6124
|
-
const streamingReplyMessage = appendWebSessionMessage(
|
|
6125
|
-
state.webHistoryDir,
|
|
6126
|
-
sessionRef,
|
|
6127
|
-
'assistant',
|
|
6128
|
-
event.content,
|
|
6129
|
-
{
|
|
6130
|
-
mode: 'agent',
|
|
6131
|
-
streamingReply: true,
|
|
6132
|
-
pending: true
|
|
6133
|
-
}
|
|
6134
|
-
);
|
|
6135
|
-
streamingReplyMessageId = streamingReplyMessage && streamingReplyMessage.id
|
|
6136
|
-
? streamingReplyMessage.id
|
|
6137
|
-
: '';
|
|
6138
|
-
lastPartialPersistAt = now;
|
|
6139
|
-
} else if (shouldPersist) {
|
|
6140
|
-
patchWebSessionMessage(state.webHistoryDir, sessionRef, streamingReplyMessageId, {
|
|
6141
|
-
content: event.content,
|
|
6142
|
-
pending: true
|
|
6143
|
-
});
|
|
6144
|
-
lastPartialPersistAt = now;
|
|
6145
6307
|
}
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
|
|
6149
|
-
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
}
|
|
6156
|
-
emitStreamEvent(event, resolveWebStreamControlEventType(event), {
|
|
6157
|
-
transportType: event && event.type ? event.type : '',
|
|
6158
|
-
text: event && (event.text || event.content) ? String(event.text || event.content) : ''
|
|
6308
|
+
);
|
|
6309
|
+
streamingReplyMessageId = streamingReplyMessage && streamingReplyMessage.id
|
|
6310
|
+
? streamingReplyMessage.id
|
|
6311
|
+
: '';
|
|
6312
|
+
lastPartialPersistAt = now;
|
|
6313
|
+
} else if (shouldPersist) {
|
|
6314
|
+
patchWebSessionMessage(state.webHistoryDir, sessionRef, streamingReplyMessageId, {
|
|
6315
|
+
content: event.content,
|
|
6316
|
+
pending: true
|
|
6159
6317
|
});
|
|
6318
|
+
lastPartialPersistAt = now;
|
|
6160
6319
|
}
|
|
6320
|
+
}
|
|
6321
|
+
if (isPartialDelta) {
|
|
6322
|
+
lastStreamWriteAt = Date.now();
|
|
6323
|
+
// 只发增量片段:前端在 content_chunk 上做追加,
|
|
6324
|
+
// reset 表示换了一条 assistant 消息、从空白重新开始
|
|
6325
|
+
sendNdjson(res, event.chunkReset === true
|
|
6326
|
+
? { type: 'content_chunk', text: '', reset: true }
|
|
6327
|
+
: { type: 'content_chunk', text: event.chunk });
|
|
6328
|
+
return;
|
|
6329
|
+
}
|
|
6330
|
+
emitStreamEvent(event, resolveWebStreamControlEventType(event), {
|
|
6331
|
+
transportType: event && event.type ? event.type : '',
|
|
6332
|
+
text: event && (event.text || event.content) ? String(event.text || event.content) : ''
|
|
6161
6333
|
});
|
|
6334
|
+
|
|
6335
|
+
};
|
|
6336
|
+
// 域名白名单下被过滤代理拦下的新域名:对话进行中立即提示一次
|
|
6337
|
+
const deniedNotifier = createWebDeniedNotifier(ctx, sessionRef, text => handleStreamEvent({ type: 'trace', stream: 'stdout', text }));
|
|
6338
|
+
try {
|
|
6339
|
+
if (deniedNotifier) deniedNotifier.start();
|
|
6340
|
+
let result;
|
|
6341
|
+
try {
|
|
6342
|
+
result = await execAgentInWebContainerStream(ctx, state, sessionRef, command, {
|
|
6343
|
+
agentProgram: agentMeta.agentProgram,
|
|
6344
|
+
// 复用路由入口占好的那把锁,而不是另起一份运行登记
|
|
6345
|
+
runState,
|
|
6346
|
+
onEvent: handleStreamEvent
|
|
6347
|
+
});
|
|
6348
|
+
} finally {
|
|
6349
|
+
if (deniedNotifier) deniedNotifier.stop();
|
|
6350
|
+
}
|
|
6162
6351
|
ctx.logger.info('agent turn finished', {
|
|
6163
6352
|
session: turnSessionKey,
|
|
6164
6353
|
durationMs: Date.now() - turnStartedAt,
|
|
@@ -6321,6 +6510,26 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
6321
6510
|
}
|
|
6322
6511
|
];
|
|
6323
6512
|
|
|
6513
|
+
routes.push(...createContainerManageRoutes({
|
|
6514
|
+
ctx,
|
|
6515
|
+
state,
|
|
6516
|
+
req,
|
|
6517
|
+
res,
|
|
6518
|
+
deps: {
|
|
6519
|
+
sendJson,
|
|
6520
|
+
readJsonBody,
|
|
6521
|
+
decodeSessionName,
|
|
6522
|
+
getNetworkManager: () => getWebNetworkManager(ctx, state),
|
|
6523
|
+
getPortForwarder: () => getWebPortForwarder(ctx, state),
|
|
6524
|
+
rememberContainerName: (id, name) => {
|
|
6525
|
+
if (!state.containerNames) state.containerNames = new Map();
|
|
6526
|
+
state.containerNames.set(id, name);
|
|
6527
|
+
},
|
|
6528
|
+
ensureWebContainer,
|
|
6529
|
+
execCommandInWebContainer
|
|
6530
|
+
}
|
|
6531
|
+
}));
|
|
6532
|
+
|
|
6324
6533
|
for (const route of routes) {
|
|
6325
6534
|
if (route.method !== req.method) {
|
|
6326
6535
|
continue;
|
|
@@ -6370,6 +6579,8 @@ async function startWebServer(options) {
|
|
|
6370
6579
|
contModeArgs: options.contModeArgs,
|
|
6371
6580
|
containerExtraArgs: options.containerExtraArgs,
|
|
6372
6581
|
containerEnvs: options.containerEnvs,
|
|
6582
|
+
networkManager: options.networkManager,
|
|
6583
|
+
envFiles: options.envFiles,
|
|
6373
6584
|
containerVolumes: options.containerVolumes,
|
|
6374
6585
|
containerPorts: options.containerPorts,
|
|
6375
6586
|
validateHostPath: options.validateHostPath,
|
|
@@ -6598,7 +6809,7 @@ async function startWebServer(options) {
|
|
|
6598
6809
|
console.log(`${GREEN}✅ MANYOYO Web 服务已启动: http://${listenHost}:${listenPort}${NC}`);
|
|
6599
6810
|
console.log(`${CYAN}提示: 左侧是 manyoyo 容器与 AGENT 列表,右侧是聊天/终端/文件/详情/配置/检查工作台。${NC}`);
|
|
6600
6811
|
if (ctx.serverHost === '0.0.0.0') {
|
|
6601
|
-
console.log(`${CYAN}提示: 当前监听全部网卡,请用本机局域网 IP 访问。${NC}`);
|
|
6812
|
+
console.log(`${CYAN}提示: 当前监听全部网卡,请用本机局域网 / 公网 IP 访问。${NC}`);
|
|
6602
6813
|
}
|
|
6603
6814
|
console.log(`${CYAN}🔐 登录用户名: ${YELLOW}${ctx.authUser}${NC}`);
|
|
6604
6815
|
if (ctx.authPassAuto) {
|
|
@@ -6617,6 +6828,7 @@ async function startWebServer(options) {
|
|
|
6617
6828
|
});
|
|
6618
6829
|
|
|
6619
6830
|
startStaleSetupCleanup(ctx);
|
|
6831
|
+
startContainerNetworkWatch(ctx, state);
|
|
6620
6832
|
if (ctx.autoPullImage) {
|
|
6621
6833
|
startWebImagePrepare(ctx, state);
|
|
6622
6834
|
}
|
|
@@ -6628,6 +6840,11 @@ async function startWebServer(options) {
|
|
|
6628
6840
|
port: listenPort,
|
|
6629
6841
|
close: () => new Promise(resolve => {
|
|
6630
6842
|
ctx.logger.info('web server closing');
|
|
6843
|
+
if (state.networkWatch) {
|
|
6844
|
+
state.networkWatch.stop();
|
|
6845
|
+
state.networkWatch = null;
|
|
6846
|
+
}
|
|
6847
|
+
if (state.portForwarder) state.portForwarder.closeAll();
|
|
6631
6848
|
for (const session of state.terminalSessions.values()) {
|
|
6632
6849
|
const ptyProcess = session && session.ptyProcess;
|
|
6633
6850
|
if (ptyProcess && !ptyProcess.killed) {
|