@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/README.md
CHANGED
|
@@ -53,7 +53,7 @@ manyoyo init all # 把本机已有的 Agent 配置搬进来
|
|
|
53
53
|
manyoyo run -y c # 在沙箱里以 YOLO 模式启动 Claude Code
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
默认镜像 `ghcr.io/xcanwin/manyoyo` 首次使用时自动拉取;需要自定义镜像时再执行 `manyoyo build --iv 2.
|
|
56
|
+
默认镜像 `ghcr.io/xcanwin/manyoyo` 首次使用时自动拉取;需要自定义镜像时再执行 `manyoyo build --iv 2.2.0-common`。更多命令见[命令速查](https://xcanwin.github.io/manyoyo/reference/cli-options)。
|
|
57
57
|
|
|
58
58
|
## 它能做什么
|
|
59
59
|
|
package/bin/manyoyo.js
CHANGED
|
@@ -9,7 +9,8 @@ const net = require('net');
|
|
|
9
9
|
const readline = require('readline');
|
|
10
10
|
const { Command, Help } = require('commander');
|
|
11
11
|
const { startWebServer } = require('../lib/web/server');
|
|
12
|
-
const {
|
|
12
|
+
const { isSidecarName } = require('../lib/egress-sidecar');
|
|
13
|
+
const { buildContainerRunArgs, buildContainerRunCommand, runWithEnvFile, hasNetworkArg } = require('../lib/container-run');
|
|
13
14
|
const { getManyoyoConfigPath, readManyoyoConfig, syncGlobalImageVersion } = require('../lib/global-config');
|
|
14
15
|
const { resolveUpdateImageVersion } = require('../lib/image-version-policy');
|
|
15
16
|
const { selectContainerRuntime, mergeRuntimeEnv } = require('../lib/container-runtime');
|
|
@@ -44,6 +45,12 @@ const {
|
|
|
44
45
|
normalizeMirrors
|
|
45
46
|
} = require('../lib/runtime-normalizers');
|
|
46
47
|
const { applyAptMirror } = require('../lib/mirrors');
|
|
48
|
+
const containerState = require('../lib/container-state');
|
|
49
|
+
const { buildExecArgs, resolveContainerId } = require('../lib/container-exec');
|
|
50
|
+
const { createNetworkManager, NETWORK_NAME } = require('../lib/container-network');
|
|
51
|
+
const { normalizePolicy } = require('../lib/network-policy');
|
|
52
|
+
const { withEnvEndpoints } = require('../lib/network-endpoints');
|
|
53
|
+
const { resolveManageOptions } = require('../lib/container-manage-options');
|
|
47
54
|
const {
|
|
48
55
|
sanitizeSensitiveData,
|
|
49
56
|
sanitizeServeLogText,
|
|
@@ -131,6 +138,8 @@ let CONTAINER_RUNTIME = null;
|
|
|
131
138
|
let UPDATE_CHECK_ENABLED = true;
|
|
132
139
|
// 全局配置 mirrors(apt/npm/pip 软件源,空 = 官方默认):容器创建时在容器层生效,镜像不变
|
|
133
140
|
let MIRRORS = { apt: '', npm: '', pip: '' };
|
|
141
|
+
let MANAGE_OPTIONS = { autostart: '', network: null };
|
|
142
|
+
let ENV_FILES = [];
|
|
134
143
|
// serve 的容器环境状态,供 GET /api/system/runtime 读取
|
|
135
144
|
const RUNTIME_STATE = { status: 'ready', message: '' };
|
|
136
145
|
const DOCKER_DAEMON_ERROR_CODES = new Set(['PODMAN_MACHINE_UNAVAILABLE', 'DOCKER_DAEMON_UNAVAILABLE', 'PORT_IN_USE']);
|
|
@@ -342,6 +351,9 @@ function installServeProcessDiagnostics(logger) {
|
|
|
342
351
|
* @property {string} [yolo] - YOLO 模式
|
|
343
352
|
* @property {string} [containerMode] - 容器模式
|
|
344
353
|
* @property {string} [containerRuntime] - 容器运行时(auto/docker/podman,默认 auto;仅全局配置生效)
|
|
354
|
+
* @property {string} [autostart] - 容器每次启动时由容器内 init 执行的 bash 脚本(新建容器时写入状态目录)
|
|
355
|
+
* @property {boolean} [autostartOnServe] - serve 启动时自动拉起此容器
|
|
356
|
+
* @property {object} [network] - 网络策略(preset / host / egress / peers,见 lib/network-policy.js),不写为默认收紧
|
|
345
357
|
* @property {{apt?: string, npm?: string, pip?: string}} [mirrors] - 容器内 apt/npm/pip 软件源(http/https URL,空/缺省为官方默认;仅全局配置生效)
|
|
346
358
|
* @property {boolean} [updateCheck] - serve 是否每天检查一次新版本(默认 true;仅全局配置生效,请求不附带任何本机信息)
|
|
347
359
|
* @property {number} [cacheTTL] - 缓存过期天数
|
|
@@ -567,43 +579,16 @@ function addEnvFileTo(targetEnvs, envFile) {
|
|
|
567
579
|
}
|
|
568
580
|
|
|
569
581
|
if (fs.existsSync(filePath)) {
|
|
570
|
-
|
|
571
|
-
const
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
// Match pattern: (export )?(KEY)=(VALUE)
|
|
575
|
-
const match = line.match(/^(?:export\s+)?([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.*)$/);
|
|
576
|
-
if (match) {
|
|
577
|
-
let key = match[1];
|
|
578
|
-
let value = match[2].trim();
|
|
579
|
-
|
|
580
|
-
// Filter malicious characters
|
|
581
|
-
if (/[\r\n\0]/.test(value)) continue;
|
|
582
|
-
if (/[\$\(\)\`\|\&\*\{\};<>]/.test(value)) continue;
|
|
583
|
-
if (/^\(/.test(value)) continue;
|
|
584
|
-
|
|
585
|
-
// Remove quotes
|
|
586
|
-
if (/^"(.*)"$/.test(value)) {
|
|
587
|
-
value = value.slice(1, -1);
|
|
588
|
-
} else if (/^'(.*)'$/.test(value)) {
|
|
589
|
-
value = value.slice(1, -1);
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
if (key) {
|
|
593
|
-
targetEnvs.push("--env", `${key}=${value}`);
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
}
|
|
582
|
+
// 与「环境变量」文本同一套语法(见 lib/env-text.js);值里有被禁止的字符的行会被跳过并提示
|
|
583
|
+
const { entries, invalid } = containerState.parseEnvFileContent(fs.readFileSync(filePath, 'utf-8'));
|
|
584
|
+
entries.forEach(entry => targetEnvs.push('--env', `${entry.key}=${entry.value}`));
|
|
585
|
+
invalid.forEach(item => console.error(`${YELLOW}⚠️ ${filePath} 第 ${item.line} 行已跳过: ${item.reason}${NC}`));
|
|
597
586
|
return {};
|
|
598
587
|
}
|
|
599
588
|
console.error(`${RED}⚠️ 未找到环境文件: ${envFile}${NC}`);
|
|
600
589
|
return {};
|
|
601
590
|
}
|
|
602
591
|
|
|
603
|
-
function addEnvFile(envFile) {
|
|
604
|
-
return addEnvFileTo(CONTAINER_ENVS, envFile);
|
|
605
|
-
}
|
|
606
|
-
|
|
607
592
|
// Playwright 浏览器模式的容器参数;在容器创建时(运行时已确定)才计算,失败只警告,不影响 run
|
|
608
593
|
async function applyPlaywrightIntegration(runtime) {
|
|
609
594
|
const integration = await buildContainerIntegration({
|
|
@@ -768,7 +753,10 @@ function getContainerStatus(name) {
|
|
|
768
753
|
|
|
769
754
|
function removeContainer(name) {
|
|
770
755
|
if ( !(QUIET.crm || QUIET.full) ) console.log(`${YELLOW}🗑️ 正在删除容器: ${name}...${NC}`);
|
|
756
|
+
// 先读 id:容器删掉后就查不到了;同时删状态目录,同名重建不会继承旧 env / 规则
|
|
757
|
+
const stateId = resolveContainerId(dockerExecArgs, name);
|
|
771
758
|
dockerExecArgs(['rm', '-f', name], { stdio: 'pipe' });
|
|
759
|
+
if (stateId) containerState.removeState(os.homedir(), stateId);
|
|
772
760
|
if ( !(QUIET.crm || QUIET.full) ) console.log(`${GREEN}✅ 已彻底删除。${NC}`);
|
|
773
761
|
}
|
|
774
762
|
|
|
@@ -1007,6 +995,7 @@ function getContList() {
|
|
|
1007
995
|
const image = cols[4] || '';
|
|
1008
996
|
// include manyoyo runtime containers (image match)
|
|
1009
997
|
// and plugin containers (both legacy manyoyo-* and new my-* prefixes)
|
|
998
|
+
if (isSidecarName(name)) return false; // 域名白名单的过滤代理容器不是用户的会话容器
|
|
1010
999
|
return image.includes('manyoyo') || name.startsWith('manyoyo-') || name.startsWith('my-');
|
|
1011
1000
|
});
|
|
1012
1001
|
|
|
@@ -1503,6 +1492,9 @@ https://github.com/xcanwin/manyoyo
|
|
|
1503
1492
|
const runConfig = runNameToLoad ? loadRunConfig(runNameToLoad, config) : {};
|
|
1504
1493
|
const globalFirstConfig = normalizeFirstConfig(config.first, '全局配置');
|
|
1505
1494
|
const runFirstConfig = normalizeFirstConfig(runConfig.first, '运行配置');
|
|
1495
|
+
// 自启动脚本与网络策略:runs.<name> > 全局配置(新建容器时写进状态目录,Web 上可随时改)
|
|
1496
|
+
const manageOptions = resolveManageOptions({ runConfig, globalConfig: config });
|
|
1497
|
+
MANAGE_OPTIONS = manageOptions;
|
|
1506
1498
|
|
|
1507
1499
|
const resolvedRuntime = resolveRuntimeConfig({
|
|
1508
1500
|
cliOptions: options,
|
|
@@ -1551,7 +1543,17 @@ https://github.com/xcanwin/manyoyo
|
|
|
1551
1543
|
|
|
1552
1544
|
// Merge mode (array values): concatenate all sources
|
|
1553
1545
|
const envFileList = resolvedRuntime.envFile;
|
|
1554
|
-
|
|
1546
|
+
// 环境变量文件不再在创建时展开成 --env:登记路径,每次 exec 现读(改了文件下一条命令就生效)
|
|
1547
|
+
ENV_FILES = [];
|
|
1548
|
+
envFileList.forEach(ef => {
|
|
1549
|
+
const filePath = String(ef || '').trim();
|
|
1550
|
+
if (!path.isAbsolute(filePath)) {
|
|
1551
|
+
console.error(`${RED}⚠️ 错误: --env-file 仅支持绝对路径: ${ef}${NC}`);
|
|
1552
|
+
process.exit(1);
|
|
1553
|
+
}
|
|
1554
|
+
if (!fs.existsSync(filePath)) console.error(`${YELLOW}⚠️ 未找到环境文件(创建后放进去也会生效): ${ef}${NC}`);
|
|
1555
|
+
ENV_FILES.push(filePath);
|
|
1556
|
+
});
|
|
1555
1557
|
|
|
1556
1558
|
const envMap = resolvedRuntime.env;
|
|
1557
1559
|
Object.entries(envMap).forEach(([key, value]) => addEnv(`${key}=${value}`));
|
|
@@ -1612,6 +1614,8 @@ https://github.com/xcanwin/manyoyo
|
|
|
1612
1614
|
containerMode: contModeValue || "",
|
|
1613
1615
|
containerRuntime: config.containerRuntime || "auto",
|
|
1614
1616
|
mirrors: MIRRORS,
|
|
1617
|
+
autostart: manageOptions.autostart,
|
|
1618
|
+
network: manageOptions.network,
|
|
1615
1619
|
shellPrefix: EXEC_COMMAND_PREFIX.trim(),
|
|
1616
1620
|
shell: EXEC_COMMAND || "",
|
|
1617
1621
|
shellSuffix: EXEC_COMMAND_SUFFIX || "",
|
|
@@ -1753,6 +1757,9 @@ function createRuntimeContext(modeState = {}) {
|
|
|
1753
1757
|
containerExtraArgs: CONTAINER_EXTRA_ARGS,
|
|
1754
1758
|
containerEnvs: CONTAINER_ENVS,
|
|
1755
1759
|
mirrors: MIRRORS,
|
|
1760
|
+
autostart: MANAGE_OPTIONS.autostart,
|
|
1761
|
+
network: MANAGE_OPTIONS.network,
|
|
1762
|
+
envFiles: ENV_FILES,
|
|
1756
1763
|
firstContainerEnvs: FIRST_CONTAINER_ENVS,
|
|
1757
1764
|
containerVolumes: CONTAINER_VOLUMES,
|
|
1758
1765
|
containerPorts: CONTAINER_PORTS,
|
|
@@ -2047,15 +2054,16 @@ function executeFirstCommand(runtime) {
|
|
|
2047
2054
|
console.log(`⚙️ 首次预执行命令: ${YELLOW}${firstCommand}${NC}`);
|
|
2048
2055
|
}
|
|
2049
2056
|
|
|
2050
|
-
const
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2057
|
+
const firstExec = buildExecArgs({ homeDir: os.homedir(), dockerExecArgs }, runtime.containerName, {
|
|
2058
|
+
extraEnv: containerState.envArgsToLines(runtime.firstContainerEnvs),
|
|
2059
|
+
command: ['/bin/bash', '-c', firstCommand]
|
|
2060
|
+
});
|
|
2061
|
+
let firstExecResult;
|
|
2062
|
+
try {
|
|
2063
|
+
firstExecResult = spawnSync(`${DOCKER_CMD}`, firstExec.args, { stdio: 'inherit', env: DOCKER_ENV });
|
|
2064
|
+
} finally {
|
|
2065
|
+
firstExec.cleanup();
|
|
2066
|
+
}
|
|
2059
2067
|
if (firstExecResult.error) {
|
|
2060
2068
|
throw firstExecResult.error;
|
|
2061
2069
|
}
|
|
@@ -2083,15 +2091,38 @@ async function createNewContainer(runtime) {
|
|
|
2083
2091
|
);
|
|
2084
2092
|
const defaultCommand = runtime.execCommand;
|
|
2085
2093
|
|
|
2094
|
+
// 用户 env 不进容器配置(podman inspect 看不到明文),改写进状态目录的 box/env,每次 exec 现读
|
|
2095
|
+
const userEnvLines = containerState.userEnvLines(runtime.containerEnvs);
|
|
2086
2096
|
await applyPlaywrightIntegration(runtime);
|
|
2097
|
+
runtime.containerEnvs = containerState.stripEnvKeys(runtime.containerEnvs, new Set(userEnvLines.map(containerState.envLineKey)));
|
|
2087
2098
|
|
|
2088
2099
|
if (runtime.showCommand) {
|
|
2100
|
+
runtime.state = previewContainerState();
|
|
2089
2101
|
console.log(buildDockerRunCmd(runtime));
|
|
2090
2102
|
process.exit(0);
|
|
2091
2103
|
}
|
|
2092
2104
|
|
|
2093
2105
|
await ensureRunImage(runtime);
|
|
2094
2106
|
|
|
2107
|
+
const networkPolicy = await withEnvEndpoints(normalizePolicy(runtime.network), [...userEnvLines, ...containerState.readEnvFilePaths(runtime.envFiles || []).flatMap(file => file.entries.map(entry => `${entry.key}=${entry.value}`))]);
|
|
2108
|
+
const networkManager = createCliNetworkManager(runtime);
|
|
2109
|
+
if (!hasNetworkArg([...(runtime.contModeArgs || []), ...(runtime.containerExtraArgs || [])])) {
|
|
2110
|
+
try {
|
|
2111
|
+
await networkManager.ensureBridgeNetwork();
|
|
2112
|
+
} catch (e) {
|
|
2113
|
+
throw new Error(`无法创建 ${NETWORK_NAME} 网络: ${e.message}`);
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
runtime.state = containerState.createState({
|
|
2117
|
+
homeDir: os.homedir(),
|
|
2118
|
+
envLines: userEnvLines,
|
|
2119
|
+
envFiles: runtime.envFiles,
|
|
2120
|
+
autostart: runtime.autostart,
|
|
2121
|
+
network: networkPolicy,
|
|
2122
|
+
netRequired: networkPolicy.preset !== 'open',
|
|
2123
|
+
meta: { name: runtime.containerName }
|
|
2124
|
+
});
|
|
2125
|
+
|
|
2095
2126
|
// 使用数组参数执行命令(安全方式)
|
|
2096
2127
|
try {
|
|
2097
2128
|
const args = buildDockerRunArgs(runtime);
|
|
@@ -2100,12 +2131,16 @@ async function createNewContainer(runtime) {
|
|
|
2100
2131
|
showImagePullHint(e);
|
|
2101
2132
|
// 失败的 run 可能留下 Created 状态的容器;这个名字创建前不存在,所以只清理本次留下的这一个
|
|
2102
2133
|
try { dockerExecArgs(['rm', '-f', runtime.containerName], { stdio: 'pipe' }); } catch (cleanupError) { /* 尽力清理 */ }
|
|
2134
|
+
containerState.removeState(os.homedir(), runtime.state.id);
|
|
2103
2135
|
throw explainCreateFailure(e);
|
|
2104
2136
|
}
|
|
2105
2137
|
|
|
2106
2138
|
// Wait for container to be ready
|
|
2107
2139
|
await waitForContainerReady(runtime.containerName);
|
|
2108
2140
|
|
|
2141
|
+
// 先下发网络规则再做任何 exec;失败即关闭(容器留着,init 在等门闩,不会跑自启动)
|
|
2142
|
+
await networkManager.apply(runtime.containerName, { expectId: runtime.state.id });
|
|
2143
|
+
|
|
2109
2144
|
applyAptMirror({
|
|
2110
2145
|
dockerExecArgs,
|
|
2111
2146
|
containerName: runtime.containerName,
|
|
@@ -2123,8 +2158,26 @@ async function createNewContainer(runtime) {
|
|
|
2123
2158
|
* 构建 Docker run 命令参数数组(安全方式,避免命令注入)
|
|
2124
2159
|
* @returns {string[]} 命令参数数组
|
|
2125
2160
|
*/
|
|
2161
|
+
// 容器网络规则的下发器:CLI 与 serve 各建一份,helper 容器用当前 imageVersion 的官方镜像
|
|
2162
|
+
function createCliNetworkManager(runtime) {
|
|
2163
|
+
return createNetworkManager({
|
|
2164
|
+
command: DOCKER_CMD,
|
|
2165
|
+
env: DOCKER_ENV,
|
|
2166
|
+
homeDir: os.homedir(),
|
|
2167
|
+
imageRef: () => `${runtime.imageName}:${runtime.imageVersion}`,
|
|
2168
|
+
getMirrorUrls: () => Object.values(runtime.mirrors || {}).filter(Boolean)
|
|
2169
|
+
});
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
function previewContainerState() {
|
|
2173
|
+
const dir = path.join(os.homedir(), '.manyoyo', 'containers', '<id>');
|
|
2174
|
+
return { id: '<id>', box: path.join(dir, 'box'), sys: path.join(dir, 'sys') };
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2126
2177
|
function buildDockerRunArgs(runtime) {
|
|
2127
2178
|
return buildContainerRunArgs({
|
|
2179
|
+
state: runtime.state,
|
|
2180
|
+
defaultNetwork: NETWORK_NAME,
|
|
2128
2181
|
containerName: runtime.containerName,
|
|
2129
2182
|
hostPath: runtime.hostPath,
|
|
2130
2183
|
containerPath: runtime.containerPath,
|
|
@@ -2159,6 +2212,7 @@ async function connectExistingContainer(runtime) {
|
|
|
2159
2212
|
if (status !== 'running') {
|
|
2160
2213
|
dockerExecArgs(['start', runtime.containerName], { stdio: 'pipe' });
|
|
2161
2214
|
}
|
|
2215
|
+
await createCliNetworkManager(runtime).ensureReady(runtime.containerName);
|
|
2162
2216
|
|
|
2163
2217
|
// Get default command from label
|
|
2164
2218
|
const defaultCommand = dockerExecArgs(['inspect', '-f', '{{index .Config.Labels "manyoyo.default_cmd"}}', runtime.containerName]).trim();
|
|
@@ -2183,6 +2237,8 @@ async function setupContainer(runtime) {
|
|
|
2183
2237
|
process.exit(0);
|
|
2184
2238
|
}
|
|
2185
2239
|
runtime.execCommand = joinExecCommand(runtime.execCommandPrefix, runtime.execCommand, runtime.execCommandSuffix);
|
|
2240
|
+
runtime.containerEnvs = containerState.stripEnvKeys(runtime.containerEnvs, new Set(containerState.userEnvLines(runtime.containerEnvs).map(containerState.envLineKey)));
|
|
2241
|
+
runtime.state = previewContainerState();
|
|
2186
2242
|
console.log(buildDockerRunCmd(runtime));
|
|
2187
2243
|
process.exit(0);
|
|
2188
2244
|
}
|
|
@@ -2193,7 +2249,7 @@ async function setupContainer(runtime) {
|
|
|
2193
2249
|
}
|
|
2194
2250
|
}
|
|
2195
2251
|
|
|
2196
|
-
function executeInContainer(runtime, defaultCommand) {
|
|
2252
|
+
async function executeInContainer(runtime, defaultCommand) {
|
|
2197
2253
|
if (!containerExists(runtime.containerName)) {
|
|
2198
2254
|
throw new Error(`未找到容器: ${runtime.containerName}`);
|
|
2199
2255
|
}
|
|
@@ -2202,6 +2258,7 @@ function executeInContainer(runtime, defaultCommand) {
|
|
|
2202
2258
|
if (status !== 'running') {
|
|
2203
2259
|
dockerExecArgs(['start', runtime.containerName], { stdio: 'pipe' });
|
|
2204
2260
|
}
|
|
2261
|
+
await createCliNetworkManager(runtime).ensureReady(runtime.containerName);
|
|
2205
2262
|
|
|
2206
2263
|
getHelloTip(runtime.containerName, defaultCommand, runtime.execCommand);
|
|
2207
2264
|
if (!(runtime.quiet.cmd || runtime.quiet.full)) {
|
|
@@ -2210,10 +2267,15 @@ function executeInContainer(runtime, defaultCommand) {
|
|
|
2210
2267
|
}
|
|
2211
2268
|
|
|
2212
2269
|
// Execute command in container
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2270
|
+
const exec = buildExecArgs({ homeDir: os.homedir(), dockerExecArgs }, runtime.containerName, {
|
|
2271
|
+
interactive: true,
|
|
2272
|
+
tty: true,
|
|
2273
|
+
command: runtime.execCommand ? ['/bin/bash', '-c', runtime.execCommand] : ['/bin/bash']
|
|
2274
|
+
});
|
|
2275
|
+
try {
|
|
2276
|
+
spawnSync(`${DOCKER_CMD}`, exec.args, { stdio: 'inherit', env: DOCKER_ENV });
|
|
2277
|
+
} finally {
|
|
2278
|
+
exec.cleanup();
|
|
2217
2279
|
}
|
|
2218
2280
|
}
|
|
2219
2281
|
|
|
@@ -2458,6 +2520,7 @@ async function runWebServerMode(runtime) {
|
|
|
2458
2520
|
contModeArgs: runtime.contModeArgs,
|
|
2459
2521
|
containerExtraArgs: runtime.containerExtraArgs,
|
|
2460
2522
|
containerEnvs: runtime.containerEnvs,
|
|
2523
|
+
envFiles: runtime.envFiles,
|
|
2461
2524
|
containerVolumes: runtime.containerVolumes,
|
|
2462
2525
|
containerPorts: runtime.containerPorts,
|
|
2463
2526
|
validateHostPath: value => validateHostPathOrThrow(value),
|
|
@@ -2574,7 +2637,7 @@ async function main() {
|
|
|
2574
2637
|
// 7-8. Execute command and handle post-exit interactions
|
|
2575
2638
|
let shouldContinue = true;
|
|
2576
2639
|
while (shouldContinue) {
|
|
2577
|
-
executeInContainer(runtime, defaultCommand);
|
|
2640
|
+
await executeInContainer(runtime, defaultCommand);
|
|
2578
2641
|
shouldContinue = await handlePostExit(runtime, defaultCommand);
|
|
2579
2642
|
}
|
|
2580
2643
|
|
|
@@ -114,7 +114,7 @@ RUN <<EOX
|
|
|
114
114
|
# Python
|
|
115
115
|
apt-get -o Acquire::https::Verify-Peer=false update -y
|
|
116
116
|
apt-get -o Acquire::https::Verify-Peer=false install -y --no-install-recommends \
|
|
117
|
-
ca-certificates openssl curl wget net-tools iputils-ping dnsutils socat ncat ssh \
|
|
117
|
+
ca-certificates openssl curl wget net-tools iputils-ping dnsutils socat ncat ssh nftables \
|
|
118
118
|
git gh g++ make sqlite3 \
|
|
119
119
|
procps psmisc lsof supervisor \
|
|
120
120
|
nano jq file tree ripgrep less bc xxd tar zip unzip gzip \
|
|
@@ -230,18 +230,47 @@ RUN <<EOX
|
|
|
230
230
|
cp /tmp/docker-res/opencode/opencode.json ~/.config/opencode/opencode.json
|
|
231
231
|
;; esac
|
|
232
232
|
|
|
233
|
-
# 安装 Playwright CLI skills 与
|
|
233
|
+
# 安装 Playwright CLI skills、patchright-core 与 Google Chrome
|
|
234
234
|
PLAYWRIGHT_CLI_INSTALL_DIR=/tmp/playwright-cli-install
|
|
235
235
|
mkdir -p "$PLAYWRIGHT_CLI_INSTALL_DIR/.playwright"
|
|
236
236
|
cd "$PLAYWRIGHT_CLI_INSTALL_DIR"
|
|
237
|
-
|
|
237
|
+
pkg_field() { node -p "const pkg = require('/tmp/manyoyo-package.json'); const value = String(pkg.$1 || '').trim(); if (!value) { throw new Error('package.json.$1 is required'); } value"; }
|
|
238
|
+
PLAYWRIGHT_CLI_VERSION=$(pkg_field playwrightCliVersion)
|
|
239
|
+
PATCHRIGHT_CORE_VERSION=$(pkg_field patchrightCoreVersion)
|
|
240
|
+
PATCHRIGHT_CORE_INTEGRITY=$(pkg_field patchrightCoreIntegrity)
|
|
238
241
|
npm install -g "@playwright/cli@${PLAYWRIGHT_CLI_VERSION}"
|
|
239
|
-
#
|
|
242
|
+
# 把 @playwright/cli 自带的 playwright-core 换成 patchright-core(开源补丁版,可从源码逐字节复现):
|
|
243
|
+
# 官方版会在页面与 Worker 里启用 CDP Runtime 域,被检测站点识别;patchright 去掉了这一痕迹。
|
|
244
|
+
# 两者必须同 minor(容器内 client 与宿主机 launchServer 要求 minor 完全一致),并以 package.json 里的 integrity 校验,不一致就中止构建
|
|
245
|
+
CLI_DIR="$(npm root -g)/@playwright/cli"
|
|
246
|
+
CLI_CORE_VERSION=$(node -p "require('$CLI_DIR/node_modules/playwright-core/package.json').version")
|
|
247
|
+
[ "${CLI_CORE_VERSION%.*}" = "${PATCHRIGHT_CORE_VERSION%.*}" ] || { echo "playwright-core ${CLI_CORE_VERSION} 与 patchright-core ${PATCHRIGHT_CORE_VERSION} minor 不一致" >&2; exit 1; }
|
|
248
|
+
npm pack "patchright-core@${PATCHRIGHT_CORE_VERSION}" --silent >/dev/null
|
|
249
|
+
PATCHRIGHT_TARBALL="patchright-core-${PATCHRIGHT_CORE_VERSION}.tgz"
|
|
250
|
+
[ "sha512-$(openssl dgst -sha512 -binary "$PATCHRIGHT_TARBALL" | openssl base64 -A)" = "$PATCHRIGHT_CORE_INTEGRITY" ] || { echo "patchright-core 完整性校验失败" >&2; exit 1; }
|
|
251
|
+
for core in $(find "$CLI_DIR" -type d -name playwright-core -path '*/node_modules/playwright-core'); do
|
|
252
|
+
rm -rf "$core"
|
|
253
|
+
mkdir -p "$core"
|
|
254
|
+
tar -xzf "$PATCHRIGHT_TARBALL" -C "$core" --strip-components=1
|
|
255
|
+
# patchright 的客户端默认在隔离世界执行 evaluate(页面读不到 Agent 的变量,Agent 也读不到页面自己定义的全局变量),
|
|
256
|
+
# 对 Agent 毫无用处;把客户端 evaluate / $eval 等 12 处默认值改回主世界(与官方 Playwright 行为一致)。
|
|
257
|
+
# 在 integrity 校验之后做,次数对不上就中止(升级 patchright 时必须人工复核);实测不影响 dabi / rebrowser 等检测
|
|
258
|
+
ISOLATED_DEFAULTS=12
|
|
259
|
+
[ "$(grep -o 'isolatedContext = true' "$core/lib/coreBundle.js" | wc -l)" = "$ISOLATED_DEFAULTS" ] || { echo "patchright-core 里 isolatedContext 默认值数量不是 ${ISOLATED_DEFAULTS},请人工复核" >&2; exit 1; }
|
|
260
|
+
sed -i 's/isolatedContext = true/isolatedContext = false/g' "$core/lib/coreBundle.js"
|
|
261
|
+
done
|
|
240
262
|
# 默认在 Xvfb 虚拟屏(配轻量窗口管理器)里跑有头浏览器,所以装 xvfb、fluxbox 与常见字体;x11vnc / noVNC 供 playwright vnc 模式使用
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
263
|
+
apt-get update
|
|
264
|
+
apt-get install -y --no-install-recommends xvfb fluxbox x11vnc novnc websockify fonts-noto-cjk fonts-noto-color-emoji fonts-liberation gpg
|
|
265
|
+
# Google Chrome stable:官方 apt 源(amd64 / arm64 都有),固定签名密钥指纹,apt 再校验 Release 签名
|
|
266
|
+
curl -fsSL https://dl.google.com/linux/linux_signing_key.pub -o /tmp/google-linux-signing-key.pub
|
|
267
|
+
[ "$(gpg --show-keys --with-colons /tmp/google-linux-signing-key.pub | awk -F: '/^fpr/{print $10; exit}')" = "EB4C1BFD4F042F6DDDCCEC917721F63BD38B4796" ] || { echo "Google 签名密钥指纹不符" >&2; exit 1; }
|
|
268
|
+
gpg --dearmor < /tmp/google-linux-signing-key.pub > /usr/share/keyrings/google-chrome.gpg
|
|
269
|
+
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/google-chrome.gpg] https://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list
|
|
270
|
+
apt-get update
|
|
271
|
+
apt-get install -y --no-install-recommends google-chrome-stable
|
|
272
|
+
rm -f /etc/apt/sources.list.d/google-chrome.list /etc/cron.daily/google-chrome
|
|
273
|
+
google-chrome --version
|
|
245
274
|
playwright-cli install --skills
|
|
246
275
|
PLAYWRIGHT_CLI_SKILL_SOURCE="$PLAYWRIGHT_CLI_INSTALL_DIR/.claude/skills/playwright-cli"
|
|
247
276
|
for target in ~/.claude/skills/playwright-cli ~/.codex/skills/playwright-cli ~/.gemini/skills/playwright-cli; do
|
|
@@ -261,8 +290,12 @@ EOX
|
|
|
261
290
|
# Playwright 默认配置(挂载宿主机目录时会被覆盖,路径固定)与按需启动 Xvfb 的 playwright-cli 包装
|
|
262
291
|
COPY ./docker/res/playwright/browser.json /run/manyoyo-playwright/config.json
|
|
263
292
|
COPY ./docker/res/playwright/stealth.init.js /run/manyoyo-playwright/stealth.init.js
|
|
293
|
+
COPY ./docker/res/playwright/env /run/manyoyo-playwright/env
|
|
264
294
|
COPY --chmod=755 ./docker/res/playwright/playwright-cli.sh /usr/local/sbin/playwright-cli
|
|
265
295
|
RUN ln -s playwright-cli /usr/local/sbin/playwright-mcp
|
|
296
|
+
# Chrome 154 起,不安全页面(http://host.containers.internal 这类宿主机别名)不能向 local 地址空间发请求(含同源 fetch),
|
|
297
|
+
# 会让 Agent 测试宿主机上的本地网页时 fetch 失败(旧的 Chromium 没有这个限制);这条企业策略关闭该检查,只在容器内的 Chrome 生效
|
|
298
|
+
COPY ./docker/res/playwright/chrome-policy.json /etc/opt/chrome/policies/managed/local-network.json
|
|
266
299
|
|
|
267
300
|
# 从 cache-stage 复制 JDT LSP 到最终位置,避免中转层残留
|
|
268
301
|
COPY --from=cache-stage /opt/jdtls /root/.local/share/jdtls
|
|
@@ -32,7 +32,7 @@ echo "[manyoyo] Agent CLI versions before update:"
|
|
|
32
32
|
print_agent_versions
|
|
33
33
|
|
|
34
34
|
if [ "${#update_packages[@]}" -gt 0 ]; then
|
|
35
|
-
npm_config_update_notifier=false npm install -g npm@latest "${update_packages[@]}"
|
|
35
|
+
npm_config_update_notifier=false npm install -g npm@latest "${update_packages[@]}"
|
|
36
36
|
npm config set allow-scripts=@anthropic-ai/claude-code,@openai/codex,@google/gemini-cli,opencode-ai,@playwright/cli,pyright,typescript-language-server,typescript --location=user
|
|
37
37
|
else
|
|
38
38
|
echo "[manyoyo] No existing Agent CLI found; skip npm install."
|
|
@@ -1,27 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"outputDir": "/tmp/.playwright-cli",
|
|
3
3
|
"browser": {
|
|
4
|
-
"chromiumSandbox": false,
|
|
5
4
|
"browserName": "chromium",
|
|
5
|
+
"contextOptions": {
|
|
6
|
+
"viewport": null
|
|
7
|
+
},
|
|
6
8
|
"initScript": [
|
|
7
9
|
"/run/manyoyo-playwright/stealth.init.js"
|
|
8
10
|
],
|
|
9
|
-
"contextOptions": {
|
|
10
|
-
"locale": "en-US",
|
|
11
|
-
"timezoneId": "UTC",
|
|
12
|
-
"viewport": null,
|
|
13
|
-
"extraHTTPHeaders": {
|
|
14
|
-
"Accept-Language": "en-US,en;q=0.9"
|
|
15
|
-
}
|
|
16
|
-
},
|
|
17
11
|
"launchOptions": {
|
|
18
|
-
"channel": "
|
|
12
|
+
"channel": "chrome",
|
|
13
|
+
"chromiumSandbox": false,
|
|
19
14
|
"headless": false,
|
|
20
15
|
"args": [
|
|
21
16
|
"--lang=en-US",
|
|
22
|
-
"--window-size=1920,
|
|
17
|
+
"--window-size=1920,1058",
|
|
18
|
+
"--window-position=0,0",
|
|
23
19
|
"--disable-blink-features=AutomationControlled",
|
|
24
|
-
"--force-webrtc-ip-handling-policy=disable_non_proxied_udp"
|
|
20
|
+
"--force-webrtc-ip-handling-policy=disable_non_proxied_udp",
|
|
21
|
+
"--disable-webrtc"
|
|
25
22
|
]
|
|
26
23
|
}
|
|
27
24
|
}
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
# 以 playwright-cli 名字调用时交给 @playwright/cli;以 playwright-mcp 名字调用时启动 stdio MCP(读同一份 $PLAYWRIGHT_MCP_CONFIG)。
|
|
4
4
|
export DISPLAY=:99
|
|
5
5
|
CONFIG="${PLAYWRIGHT_MCP_CONFIG:-/run/manyoyo-playwright/config.json}"
|
|
6
|
+
# 语言与时区用进程环境变量(TZ / LANG / LANGUAGE)原生提供,页面与 Worker 才一致;由宿主机生成,与 config.json 同目录
|
|
7
|
+
ENV_FILE="$(dirname "$CONFIG")/env"
|
|
8
|
+
if [ -r "$ENV_FILE" ]; then
|
|
9
|
+
. "$ENV_FILE"
|
|
10
|
+
fi
|
|
6
11
|
# 浏览器在容器外(headed / chrome / vnc)时不需要虚拟屏
|
|
7
12
|
if ! grep -qE '"(remoteEndpoint|cdpEndpoint)"' "$CONFIG" 2>/dev/null; then
|
|
8
13
|
# 以锁文件里的进程是否活着为准;容器被 kill 后遗留的 socket 与锁文件要清掉
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
(function () {
|
|
3
3
|
try {
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
Object.defineProperty(
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
4
|
+
const blocked = ['RTCPeerConnection', 'webkitRTCPeerConnection', 'RTCIceCandidate', 'RTCRtpSender', 'RTCRtpReceiver', 'RTCRtpTransceiver', 'RTCDataChannel'];
|
|
5
|
+
for (const name of blocked) {
|
|
6
|
+
Object.defineProperty(globalThis, name, { configurable: true, writable: true, value: undefined });
|
|
7
|
+
}
|
|
8
|
+
if (navigator.mediaDevices) {
|
|
9
|
+
Object.defineProperty(navigator.mediaDevices, 'getUserMedia', {
|
|
10
|
+
configurable: true,
|
|
11
|
+
writable: true,
|
|
12
|
+
value: async () => { throw new DOMException('WebRTC is disabled', 'NotAllowedError'); }
|
|
13
|
+
});
|
|
14
|
+
}
|
|
13
15
|
} catch (_) {}
|
|
14
16
|
})();
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { domainAllowed } = require('./network-policy');
|
|
4
|
+
|
|
5
|
+
// 容器里的 Chrome 启动后会自己连一些谷歌的后台服务(更新、账号同步、字体 / 资源)。白名单下它们必然被拦,
|
|
6
|
+
// 对用户没有意义:网络分区的“最近被拦截”把它们折叠成“浏览器后台请求”,对话里也不为它们提示。
|
|
7
|
+
// 数据只写在这一处。
|
|
8
|
+
const BROWSER_BACKGROUND_DOMAINS = [
|
|
9
|
+
'accounts.google.com',
|
|
10
|
+
'update.googleapis.com',
|
|
11
|
+
'www.google.com',
|
|
12
|
+
'www.gstatic.com',
|
|
13
|
+
'dl.google.com',
|
|
14
|
+
'clients2.google.com',
|
|
15
|
+
'android.clients.google.com',
|
|
16
|
+
'content-autofill.googleapis.com'
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
function isBrowserBackground(host) {
|
|
20
|
+
return domainAllowed(BROWSER_BACKGROUND_DOMAINS, host);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = { BROWSER_BACKGROUND_DOMAINS, isBrowserBackground };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# manyoyo 容器内的 env 加载:init.sh 启动自启动前加载,终端里的 `reload-env` 也用它。
|
|
2
|
+
# 由宿主机以只读目录挂到 /run/manyoyo-sys/env.sh,容器内不可改。
|
|
3
|
+
# 与宿主机侧(lib/env-text.js)同一语法:一行 KEY=VALUE,# 注释与空行忽略,可选 `export ` 前缀与 `=` 两侧空白,
|
|
4
|
+
# 值两端成对的引号去掉;不展开变量、不执行命令;key 必须合法
|
|
5
|
+
load_env() {
|
|
6
|
+
local file=$1 line key val
|
|
7
|
+
[ -f "$file" ] || return 0
|
|
8
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
9
|
+
line=${line%$'\r'}
|
|
10
|
+
line="${line#"${line%%[![:space:]]*}"}"
|
|
11
|
+
line="${line%"${line##*[![:space:]]}"}"
|
|
12
|
+
case $line in ''|'#'*) continue ;; esac
|
|
13
|
+
if [[ $line =~ ^export[[:space:]]+ ]]; then line=${line#"${BASH_REMATCH[0]}"}; fi
|
|
14
|
+
[[ $line =~ ^([^=[:space:]]+)[[:space:]]*=[[:space:]]*(.*)$ ]] || continue
|
|
15
|
+
key=${BASH_REMATCH[1]}
|
|
16
|
+
val=${BASH_REMATCH[2]}
|
|
17
|
+
[[ $key =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue
|
|
18
|
+
if [ "${#val}" -ge 2 ]; then
|
|
19
|
+
case $val in
|
|
20
|
+
\"*\") val=${val:1:${#val}-2} ;;
|
|
21
|
+
\'*\') val=${val:1:${#val}-2} ;;
|
|
22
|
+
esac
|
|
23
|
+
fi
|
|
24
|
+
export "$key=$val"
|
|
25
|
+
done < "$file"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# 已打开的终端里让最新的环境变量生效(优先级与新开终端一致:managed < 环境变量文件 < 用户 env)
|
|
29
|
+
reload-env() {
|
|
30
|
+
local sys=${MANYOYO_SYS_DIR:-/run/manyoyo-sys} box=${MANYOYO_BOX_DIR:-/run/manyoyo}
|
|
31
|
+
load_env "$sys/managed.env"
|
|
32
|
+
load_env "$sys/files.env"
|
|
33
|
+
load_env "$box/env"
|
|
34
|
+
}
|