@xcanwin/manyoyo 5.3.9 → 5.4.4
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 +3 -3
- package/bin/manyoyo.js +70 -7
- package/docker/manyoyo.Dockerfile +39 -19
- package/docker/res/claude/claude.json +9 -0
- package/docker/res/claude/settings.json +6 -0
- package/docker/res/claude/statusline.sh +58 -0
- package/docker/res/codex/config.toml +7 -0
- package/docker/res/gemini/settings.json +21 -0
- package/docker/res/opencode/opencode.json +22 -0
- package/docker/res/playwright/cli-cont-headless.init.js +12 -0
- package/docker/res/playwright/cli-cont-headless.json +37 -0
- package/docker/res/supervisor/s.conf +3 -0
- package/lib/container-run.js +1 -0
- package/lib/init-config.js +1 -12
- package/lib/plugin/playwright-assets/compose-headed.yaml +1 -1
- package/lib/plugin/playwright-assets/compose-headless.yaml +1 -1
- package/lib/plugin/playwright.js +219 -33
- package/lib/web/server.js +4 -0
- package/manyoyo.example.json +7 -3
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ AI Agent CLI 往往需要:
|
|
|
53
53
|
```bash
|
|
54
54
|
npm install -g @xcanwin/manyoyo
|
|
55
55
|
podman pull ubuntu:24.04 # 仅 Podman 需要
|
|
56
|
-
manyoyo build --iv 1.8.
|
|
56
|
+
manyoyo build --iv 1.8.8-common
|
|
57
57
|
manyoyo init all
|
|
58
58
|
manyoyo run -r claude
|
|
59
59
|
```
|
|
@@ -137,10 +137,10 @@ manyoyo config command
|
|
|
137
137
|
|
|
138
138
|
```bash
|
|
139
139
|
# common 版本
|
|
140
|
-
manyoyo build --iv 1.8.
|
|
140
|
+
manyoyo build --iv 1.8.8-common
|
|
141
141
|
|
|
142
142
|
# full 版本
|
|
143
|
-
manyoyo build --iv 1.8.
|
|
143
|
+
manyoyo build --iv 1.8.8-full
|
|
144
144
|
|
|
145
145
|
# 自定义工具集
|
|
146
146
|
manyoyo build --iba TOOL=go,codex,java,gemini
|
package/bin/manyoyo.js
CHANGED
|
@@ -14,7 +14,7 @@ const { buildContainerRunArgs, buildContainerRunCommand } = require('../lib/cont
|
|
|
14
14
|
const { initAgentConfigs } = require('../lib/init-config');
|
|
15
15
|
const { buildImage } = require('../lib/image-build');
|
|
16
16
|
const { resolveAgentResumeArg, buildAgentResumeCommand } = require('../lib/agent-resume');
|
|
17
|
-
const { runPluginCommand } = require('../lib/plugin');
|
|
17
|
+
const { runPluginCommand, createPlugin } = require('../lib/plugin');
|
|
18
18
|
const { buildManyoyoLogPath } = require('../lib/log-path');
|
|
19
19
|
const {
|
|
20
20
|
sanitizeSensitiveData,
|
|
@@ -71,6 +71,7 @@ let CONTAINER_ENVS = [];
|
|
|
71
71
|
let FIRST_CONTAINER_ENVS = [];
|
|
72
72
|
let CONTAINER_VOLUMES = [];
|
|
73
73
|
let CONTAINER_PORTS = [];
|
|
74
|
+
let CONTAINER_EXTRA_ARGS = [];
|
|
74
75
|
const MANYOYO_NAME = detectCommandName();
|
|
75
76
|
let CONT_MODE_ARGS = [];
|
|
76
77
|
let QUIET = {};
|
|
@@ -559,6 +560,63 @@ function addEnvFile(envFile) {
|
|
|
559
560
|
return addEnvFileTo(CONTAINER_ENVS, envFile);
|
|
560
561
|
}
|
|
561
562
|
|
|
563
|
+
function hasEnvKey(targetEnvs, key) {
|
|
564
|
+
for (let i = 0; i < targetEnvs.length; i += 2) {
|
|
565
|
+
if (targetEnvs[i] !== '--env') {
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
const text = String(targetEnvs[i + 1] || '');
|
|
569
|
+
const idx = text.indexOf('=');
|
|
570
|
+
if (idx > 0 && text.slice(0, idx) === key) {
|
|
571
|
+
return true;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function appendUniqueArgs(targetArgs, extraArgs) {
|
|
578
|
+
const joinedExisting = new Set();
|
|
579
|
+
for (let i = 0; i < targetArgs.length; i += 2) {
|
|
580
|
+
const head = String(targetArgs[i] || '');
|
|
581
|
+
const value = String(targetArgs[i + 1] || '');
|
|
582
|
+
if (head.startsWith('--')) {
|
|
583
|
+
joinedExisting.add(`${head}\u0000${value}`);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
for (let i = 0; i < extraArgs.length; i += 2) {
|
|
588
|
+
const head = String(extraArgs[i] || '');
|
|
589
|
+
const value = String(extraArgs[i + 1] || '');
|
|
590
|
+
const signature = `${head}\u0000${value}`;
|
|
591
|
+
if (!joinedExisting.has(signature)) {
|
|
592
|
+
joinedExisting.add(signature);
|
|
593
|
+
targetArgs.push(head, value);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function applyPlaywrightCliSessionIntegration(config, runConfig) {
|
|
599
|
+
try {
|
|
600
|
+
const plugin = createPlugin('playwright', {
|
|
601
|
+
globalConfig: config,
|
|
602
|
+
runConfig,
|
|
603
|
+
projectRoot: path.join(__dirname, '..')
|
|
604
|
+
});
|
|
605
|
+
const integration = plugin.buildCliSessionIntegration(DOCKER_CMD);
|
|
606
|
+
for (const entry of integration.envEntries) {
|
|
607
|
+
const parsed = parseEnvEntry(entry);
|
|
608
|
+
if (!hasEnvKey(CONTAINER_ENVS, parsed.key)) {
|
|
609
|
+
addEnv(`${parsed.key}=${parsed.value}`);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
appendUniqueArgs(CONTAINER_EXTRA_ARGS, integration.extraArgs);
|
|
613
|
+
appendUniqueArgs(CONTAINER_VOLUMES, integration.volumeEntries || []);
|
|
614
|
+
} catch (error) {
|
|
615
|
+
console.error(`${RED}⚠️ 错误: Playwright CLI 会话注入失败: ${error.message || String(error)}${NC}`);
|
|
616
|
+
process.exit(1);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
562
620
|
function addVolume(volume) {
|
|
563
621
|
CONTAINER_VOLUMES.push("--volume", volume);
|
|
564
622
|
}
|
|
@@ -954,7 +1012,7 @@ async function setupCommander() {
|
|
|
954
1012
|
...options,
|
|
955
1013
|
pluginAction: params.action || 'ls',
|
|
956
1014
|
pluginName: params.pluginName || 'playwright',
|
|
957
|
-
pluginScene: params.scene || 'host-headless',
|
|
1015
|
+
pluginScene: params.scene || 'mcp-host-headless',
|
|
958
1016
|
pluginHost: params.host || '',
|
|
959
1017
|
pluginExtensionPaths: Array.isArray(params.extensionPaths) ? params.extensionPaths : [],
|
|
960
1018
|
pluginExtensionNames: Array.isArray(params.extensionNames) ? params.extensionNames : [],
|
|
@@ -975,7 +1033,7 @@ async function setupCommander() {
|
|
|
975
1033
|
const actions = ['up', 'down', 'status', 'health', 'logs'];
|
|
976
1034
|
actions.forEach(action => {
|
|
977
1035
|
const sceneCommand = command.command(`${action} [scene]`)
|
|
978
|
-
.description(`执行 playwright ${action} 场景(scene 默认 host-headless)`)
|
|
1036
|
+
.description(`执行 playwright ${action} 场景(scene 默认 mcp-host-headless)`)
|
|
979
1037
|
.option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)');
|
|
980
1038
|
|
|
981
1039
|
if (action === 'up') {
|
|
@@ -986,7 +1044,7 @@ async function setupCommander() {
|
|
|
986
1044
|
sceneCommand.action((scene, options) => selectPluginAction({
|
|
987
1045
|
action,
|
|
988
1046
|
pluginName: 'playwright',
|
|
989
|
-
scene: scene || 'host-headless',
|
|
1047
|
+
scene: scene || 'mcp-host-headless',
|
|
990
1048
|
extensionPaths: action === 'up' ? (options.extPath || []) : [],
|
|
991
1049
|
extensionNames: action === 'up' ? (options.extName || []) : []
|
|
992
1050
|
}, options));
|
|
@@ -1041,8 +1099,8 @@ async function setupCommander() {
|
|
|
1041
1099
|
${MANYOYO_NAME} serve 127.0.0.1:3000 启动本机网页服务
|
|
1042
1100
|
${MANYOYO_NAME} serve 127.0.0.1:3000 -d 后台启动;未设密码时会打印本次随机密码
|
|
1043
1101
|
${MANYOYO_NAME} serve 0.0.0.0:3000 -U admin -P 123 -d 后台启动并监听全部网卡
|
|
1044
|
-
${MANYOYO_NAME} playwright up host-headless 启动 playwright 默认场景(推荐)
|
|
1045
|
-
${MANYOYO_NAME} plugin playwright up host-headless 通过 plugin 命名空间启动
|
|
1102
|
+
${MANYOYO_NAME} playwright up mcp-host-headless 启动 playwright 默认场景(推荐)
|
|
1103
|
+
${MANYOYO_NAME} plugin playwright up mcp-host-headless 通过 plugin 命名空间启动
|
|
1046
1104
|
${MANYOYO_NAME} run -n test -q tip -q cmd 多次使用静默选项
|
|
1047
1105
|
`);
|
|
1048
1106
|
|
|
@@ -1208,7 +1266,7 @@ Notes:
|
|
|
1208
1266
|
pluginRequest: {
|
|
1209
1267
|
action: options.pluginAction,
|
|
1210
1268
|
pluginName: options.pluginName,
|
|
1211
|
-
scene: options.pluginScene || 'host-headless',
|
|
1269
|
+
scene: options.pluginScene || 'mcp-host-headless',
|
|
1212
1270
|
host: options.pluginHost || '',
|
|
1213
1271
|
extensionPaths: Array.isArray(options.pluginExtensionPaths) ? options.pluginExtensionPaths : [],
|
|
1214
1272
|
extensionNames: Array.isArray(options.pluginExtensionNames) ? options.pluginExtensionNames : [],
|
|
@@ -1302,6 +1360,8 @@ Notes:
|
|
|
1302
1360
|
};
|
|
1303
1361
|
Object.entries(firstEnvMap).forEach(([key, value]) => addEnvTo(FIRST_CONTAINER_ENVS, `${key}=${value}`));
|
|
1304
1362
|
|
|
1363
|
+
applyPlaywrightCliSessionIntegration(config, runConfig);
|
|
1364
|
+
|
|
1305
1365
|
const volumeList = mergeArrayConfig(config.volumes, runConfig.volumes, options.volume);
|
|
1306
1366
|
volumeList.forEach(v => addVolume(v));
|
|
1307
1367
|
|
|
@@ -1438,6 +1498,7 @@ function createRuntimeContext(modeState = {}) {
|
|
|
1438
1498
|
firstExecCommandPrefix: FIRST_EXEC_COMMAND_PREFIX,
|
|
1439
1499
|
firstExecCommandSuffix: FIRST_EXEC_COMMAND_SUFFIX,
|
|
1440
1500
|
contModeArgs: CONT_MODE_ARGS,
|
|
1501
|
+
containerExtraArgs: CONTAINER_EXTRA_ARGS,
|
|
1441
1502
|
containerEnvs: CONTAINER_ENVS,
|
|
1442
1503
|
firstContainerEnvs: FIRST_CONTAINER_ENVS,
|
|
1443
1504
|
containerVolumes: CONTAINER_VOLUMES,
|
|
@@ -1662,6 +1723,7 @@ function buildDockerRunArgs(runtime) {
|
|
|
1662
1723
|
imageName: runtime.imageName,
|
|
1663
1724
|
imageVersion: runtime.imageVersion,
|
|
1664
1725
|
contModeArgs: runtime.contModeArgs,
|
|
1726
|
+
containerExtraArgs: runtime.containerExtraArgs,
|
|
1665
1727
|
containerEnvs: runtime.containerEnvs,
|
|
1666
1728
|
containerVolumes: runtime.containerVolumes,
|
|
1667
1729
|
containerPorts: runtime.containerPorts,
|
|
@@ -1826,6 +1888,7 @@ async function runWebServerMode(runtime) {
|
|
|
1826
1888
|
execCommand: runtime.execCommand,
|
|
1827
1889
|
execCommandSuffix: runtime.execCommandSuffix,
|
|
1828
1890
|
contModeArgs: runtime.contModeArgs,
|
|
1891
|
+
containerExtraArgs: runtime.containerExtraArgs,
|
|
1829
1892
|
containerEnvs: runtime.containerEnvs,
|
|
1830
1893
|
containerVolumes: runtime.containerVolumes,
|
|
1831
1894
|
containerPorts: runtime.containerPorts,
|
|
@@ -64,7 +64,7 @@ FROM ubuntu:24.04
|
|
|
64
64
|
|
|
65
65
|
ARG TARGETARCH
|
|
66
66
|
ARG NODE_VERSION=24
|
|
67
|
-
ARG TOOL="
|
|
67
|
+
ARG TOOL="common"
|
|
68
68
|
|
|
69
69
|
# 镜像源参数化(默认使用阿里云,可按需覆盖)
|
|
70
70
|
ARG APT_MIRROR=https://mirrors.aliyun.com
|
|
@@ -74,9 +74,11 @@ ARG PIP_INDEX_URL=https://mirrors.tencent.com/pypi/simple
|
|
|
74
74
|
ARG PY_TEXT_PIP_PACKAGES="PyYAML python-dotenv tomlkit pyjson5 jsonschema"
|
|
75
75
|
ARG PY_TEXT_EXTRA_PIP_PACKAGES=""
|
|
76
76
|
ENV LANG=C.UTF-8 \
|
|
77
|
-
LC_ALL=C.UTF-8
|
|
77
|
+
LC_ALL=C.UTF-8 \
|
|
78
|
+
PIP_ROOT_USER_ACTION=ignore \
|
|
79
|
+
PLAYWRIGHT_MCP_CONFIG=/app/config/cli-cont-headless.json
|
|
78
80
|
|
|
79
|
-
#
|
|
81
|
+
# 合并系统依赖与 Python 安装为单层,减少镜像体积
|
|
80
82
|
RUN <<EOX
|
|
81
83
|
# 配置 APT 镜像源
|
|
82
84
|
sed -i "s|http://[^/]*\.ubuntu\.com|${APT_MIRROR}|g" /etc/apt/sources.list.d/ubuntu.sources
|
|
@@ -87,12 +89,14 @@ RUN <<EOX
|
|
|
87
89
|
# 开发与构建
|
|
88
90
|
# 系统管理
|
|
89
91
|
# 通用工具
|
|
92
|
+
# Python
|
|
90
93
|
apt-get -o Acquire::https::Verify-Peer=false update -y
|
|
91
94
|
apt-get -o Acquire::https::Verify-Peer=false install -y --no-install-recommends \
|
|
92
95
|
ca-certificates openssl curl wget net-tools iputils-ping dnsutils socat ncat ssh \
|
|
93
96
|
git gh g++ make sqlite3 \
|
|
94
|
-
procps psmisc lsof supervisor
|
|
95
|
-
nano jq file tree ripgrep less bc xxd tar zip unzip gzip
|
|
97
|
+
procps psmisc lsof supervisor \
|
|
98
|
+
nano jq file tree ripgrep less bc xxd tar zip unzip gzip \
|
|
99
|
+
python3.12 python3.12-dev python3.12-venv python3-pip
|
|
96
100
|
|
|
97
101
|
# 更新 CA 证书
|
|
98
102
|
update-ca-certificates
|
|
@@ -107,15 +111,7 @@ RUN <<EOX
|
|
|
107
111
|
apt-get install -y --no-install-recommends docker.io
|
|
108
112
|
;; esac
|
|
109
113
|
|
|
110
|
-
#
|
|
111
|
-
apt-get clean
|
|
112
|
-
rm -rf /tmp/* /var/tmp/* /var/log/apt /var/log/*.log /var/lib/apt/lists/* ~/.cache ~/.npm ~/go/pkg/mod/cache
|
|
113
|
-
EOX
|
|
114
|
-
|
|
115
|
-
RUN <<EOX
|
|
116
|
-
# 安装 python
|
|
117
|
-
apt-get update -y
|
|
118
|
-
apt-get install -y --no-install-recommends python3.12 python3.12-dev python3.12-venv python3-pip
|
|
114
|
+
# 配置 python
|
|
119
115
|
ln -sf /usr/bin/python3 /usr/bin/python
|
|
120
116
|
ln -sf /usr/bin/pip3 /usr/bin/pip
|
|
121
117
|
pip config set global.index-url "${PIP_INDEX_URL}"
|
|
@@ -131,6 +127,8 @@ EOX
|
|
|
131
127
|
|
|
132
128
|
# 从 cache-stage 复制 Node.js(缓存或下载)
|
|
133
129
|
COPY --from=cache-stage /opt/node /usr/local
|
|
130
|
+
COPY ./docker/res/playwright/cli-cont-headless.init.js /app/config/cli-cont-headless.init.js
|
|
131
|
+
COPY ./docker/res/playwright/cli-cont-headless.json /app/config/cli-cont-headless.json
|
|
134
132
|
COPY ./docker/res/ /tmp/docker-res/
|
|
135
133
|
ARG GIT_SSL_NO_VERIFY=false
|
|
136
134
|
|
|
@@ -203,9 +201,31 @@ RUN <<EOX
|
|
|
203
201
|
cp /tmp/docker-res/opencode/opencode.json ~/.config/opencode/opencode.json
|
|
204
202
|
;; esac
|
|
205
203
|
|
|
204
|
+
# 安装 Playwright CLI skills(不在镜像构建阶段下载浏览器)
|
|
205
|
+
npm install -g @playwright/cli@latest
|
|
206
|
+
PLAYWRIGHT_CLI_INSTALL_DIR=/tmp/playwright-cli-install
|
|
207
|
+
mkdir -p "${PLAYWRIGHT_CLI_INSTALL_DIR}/.playwright"
|
|
208
|
+
cat > "${PLAYWRIGHT_CLI_INSTALL_DIR}/.playwright/cli.config.json" <<'EOF'
|
|
209
|
+
{
|
|
210
|
+
"browser": {
|
|
211
|
+
"browserName": "chromium",
|
|
212
|
+
"launchOptions": {
|
|
213
|
+
"channel": "chrome"
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
EOF
|
|
218
|
+
cd "${PLAYWRIGHT_CLI_INSTALL_DIR}"
|
|
219
|
+
playwright-cli install --skills
|
|
220
|
+
mkdir -p "$HOME/.codex/skills/playwright-cli" ~/.gemini/skills/playwright-cli
|
|
221
|
+
cp -R "${PLAYWRIGHT_CLI_INSTALL_DIR}/.claude/skills/playwright-cli/." "$HOME/.codex/skills/playwright-cli/"
|
|
222
|
+
cp -R "${PLAYWRIGHT_CLI_INSTALL_DIR}/.claude/skills/playwright-cli/." "$HOME/.gemini/skills/playwright-cli/"
|
|
223
|
+
cd $OLDPWD
|
|
224
|
+
rm -rf "${PLAYWRIGHT_CLI_INSTALL_DIR}"
|
|
225
|
+
|
|
206
226
|
# 清理
|
|
207
227
|
npm cache clean --force
|
|
208
|
-
rm -rf /tmp/* /var/tmp/* /var/log/apt /var/log/*.log /var/lib/apt/lists/* ~/.
|
|
228
|
+
rm -rf /tmp/* /var/tmp/* /var/log/apt /var/log/*.log /var/lib/apt/lists/* ~/.npm ~/go/pkg/mod/cache
|
|
209
229
|
EOX
|
|
210
230
|
|
|
211
231
|
# 从 cache-stage 复制 JDT LSP(缓存或下载)
|
|
@@ -224,7 +244,7 @@ RUN <<EOX
|
|
|
224
244
|
|
|
225
245
|
# 清理
|
|
226
246
|
apt-get clean
|
|
227
|
-
rm -rf /tmp/* /var/tmp/* /var/log/apt /var/log/*.log /var/lib/apt/lists/* ~/.
|
|
247
|
+
rm -rf /tmp/* /var/tmp/* /var/log/apt /var/log/*.log /var/lib/apt/lists/* ~/.npm ~/go/pkg/mod/cache
|
|
228
248
|
;; esac
|
|
229
249
|
rm -rf /tmp/jdtls-cache
|
|
230
250
|
EOX
|
|
@@ -236,7 +256,7 @@ RUN <<EOX
|
|
|
236
256
|
# 安装 go
|
|
237
257
|
case ",$TOOL," in *,full,*|*,go,*)
|
|
238
258
|
apt-get update -y
|
|
239
|
-
apt-get install -y --no-install-recommends golang
|
|
259
|
+
apt-get install -y --no-install-recommends golang gcc
|
|
240
260
|
go env -w GOPROXY=https://mirrors.tencent.com/go
|
|
241
261
|
|
|
242
262
|
# 安装 LSP服务(go)
|
|
@@ -252,7 +272,7 @@ RUN <<EOX
|
|
|
252
272
|
# 清理
|
|
253
273
|
apt-get clean
|
|
254
274
|
go clean -modcache -cache
|
|
255
|
-
rm -rf /tmp/* /var/tmp/* /var/log/apt /var/log/*.log /var/lib/apt/lists/* ~/.
|
|
275
|
+
rm -rf /tmp/* /var/tmp/* /var/log/apt /var/log/*.log /var/lib/apt/lists/* ~/.npm ~/go/pkg/mod/cache
|
|
256
276
|
;; esac
|
|
257
277
|
rm -rf /tmp/gopls-cache
|
|
258
278
|
EOX
|
|
@@ -262,7 +282,7 @@ COPY ./docker/res/supervisor/s.conf /etc/supervisor/conf.d/s.conf
|
|
|
262
282
|
|
|
263
283
|
RUN <<EOX
|
|
264
284
|
# 清理
|
|
265
|
-
rm -rf /tmp/* /var/tmp/* /var/log/apt /var/log/*.log /var/lib/apt/lists/* ~/.
|
|
285
|
+
rm -rf /tmp/* /var/tmp/* /var/log/apt /var/log/*.log /var/lib/apt/lists/* ~/.npm ~/go/pkg/mod/cache
|
|
266
286
|
EOX
|
|
267
287
|
|
|
268
288
|
WORKDIR /tmp
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Claude Code status line script
|
|
3
|
+
# Items: used-tokens, context-used, model-with-reasoning, current-dir
|
|
4
|
+
|
|
5
|
+
input=$(cat)
|
|
6
|
+
|
|
7
|
+
total_input=$(echo "$input" | jq -r '.context_window.total_input_tokens // 0')
|
|
8
|
+
total_output=$(echo "$input" | jq -r '.context_window.total_output_tokens // 0')
|
|
9
|
+
used_pct=$(echo "$input" | jq -r '.context_window.used_percentage // empty')
|
|
10
|
+
model_id=$(echo "$input" | jq -r '.model.id // ""')
|
|
11
|
+
model_name=$(echo "$input" | jq -r '.model.display_name // ""')
|
|
12
|
+
cwd=$(echo "$input" | jq -r '.workspace.current_dir // .cwd // ""')
|
|
13
|
+
|
|
14
|
+
# used-tokens (omit when zero)
|
|
15
|
+
used_tokens=$((total_input + total_output))
|
|
16
|
+
if [ "$used_tokens" -ge 1000000 ]; then
|
|
17
|
+
used_tokens_str="$(awk "BEGIN {printf \"%.1fM\", $used_tokens/1000000}") used"
|
|
18
|
+
elif [ "$used_tokens" -ge 1000 ]; then
|
|
19
|
+
used_tokens_str="$(awk "BEGIN {printf \"%.1fk\", $used_tokens/1000}") used"
|
|
20
|
+
elif [ "$used_tokens" -gt 0 ]; then
|
|
21
|
+
used_tokens_str="${used_tokens} used"
|
|
22
|
+
else
|
|
23
|
+
used_tokens_str=""
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
# context-used (omit when unknown)
|
|
27
|
+
if [ -n "$used_pct" ]; then
|
|
28
|
+
context_used_str="$(printf "%.0f" "$used_pct")% used"
|
|
29
|
+
else
|
|
30
|
+
context_used_str=""
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
# model-with-reasoning
|
|
34
|
+
if echo "$model_id" | grep -qi "thinking\|extended"; then
|
|
35
|
+
model_str="${model_name}[T]"
|
|
36
|
+
else
|
|
37
|
+
model_str="${model_name}"
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
# current-dir (absolute path)
|
|
41
|
+
if [ -n "$cwd" ]; then
|
|
42
|
+
current_dir="$cwd"
|
|
43
|
+
else
|
|
44
|
+
current_dir="$PWD"
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
# Assemble (skip empty parts)
|
|
48
|
+
parts=()
|
|
49
|
+
[ -n "$used_tokens_str" ] && parts+=("$used_tokens_str")
|
|
50
|
+
[ -n "$context_used_str" ] && parts+=("$context_used_str")
|
|
51
|
+
[ -n "$model_str" ] && parts+=("$model_str")
|
|
52
|
+
[ -n "$current_dir" ] && parts+=("$current_dir")
|
|
53
|
+
|
|
54
|
+
result=""
|
|
55
|
+
for part in "${parts[@]}"; do
|
|
56
|
+
[ -z "$result" ] && result="$part" || result="$result · $part"
|
|
57
|
+
done
|
|
58
|
+
printf "%s" "$result"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"privacy": {
|
|
3
|
+
"usageStatisticsEnabled": false
|
|
4
|
+
},
|
|
5
|
+
"general": {
|
|
6
|
+
"previewFeatures": true,
|
|
7
|
+
"enableAutoUpdate": false,
|
|
8
|
+
"enableAutoUpdateNotification": false
|
|
9
|
+
},
|
|
10
|
+
"ui": {
|
|
11
|
+
"showLineNumbers": false
|
|
12
|
+
},
|
|
13
|
+
"security": {
|
|
14
|
+
"auth": {
|
|
15
|
+
"selectedType": "oauth-personal"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"model": {
|
|
19
|
+
"name": "gemini-3-pro-preview"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://opencode.ai/config.json",
|
|
3
|
+
"autoupdate": false,
|
|
4
|
+
"model": "Custom_Provider/{env:OPENAI_MODEL}",
|
|
5
|
+
"provider": {
|
|
6
|
+
"Custom_Provider": {
|
|
7
|
+
"npm": "@ai-sdk/openai-compatible",
|
|
8
|
+
"options": {
|
|
9
|
+
"baseURL": "{env:OPENAI_BASE_URL}",
|
|
10
|
+
"apiKey": "{env:OPENAI_API_KEY}",
|
|
11
|
+
"headers": {
|
|
12
|
+
"User-Agent": "opencode"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"models": {
|
|
16
|
+
"{env:OPENAI_MODEL}": {},
|
|
17
|
+
"claude-sonnet-4-5-20250929": {},
|
|
18
|
+
"gpt-5.2": {}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
(function () {
|
|
4
|
+
const platformValue = 'MacIntel';
|
|
5
|
+
try {
|
|
6
|
+
const navProto = Object.getPrototypeOf(navigator);
|
|
7
|
+
Object.defineProperty(navProto, 'platform', {
|
|
8
|
+
configurable: true,
|
|
9
|
+
get: () => platformValue
|
|
10
|
+
});
|
|
11
|
+
} catch (_) {}
|
|
12
|
+
})();
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"outputDir": "/tmp/.playwright-cli",
|
|
3
|
+
"browser": {
|
|
4
|
+
"chromiumSandbox": true,
|
|
5
|
+
"browserName": "chromium",
|
|
6
|
+
"initScript": [
|
|
7
|
+
"/app/config/cli-cont-headless.init.js"
|
|
8
|
+
],
|
|
9
|
+
"launchOptions": {
|
|
10
|
+
"channel": "chromium",
|
|
11
|
+
"headless": true,
|
|
12
|
+
"args": [
|
|
13
|
+
"--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
|
|
14
|
+
"--lang=zh-CN",
|
|
15
|
+
"--window-size=1366,768",
|
|
16
|
+
"--disable-blink-features=AutomationControlled",
|
|
17
|
+
"--force-webrtc-ip-handling-policy=disable_non_proxied_udp"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"contextOptions": {
|
|
21
|
+
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
|
|
22
|
+
"locale": "zh-CN",
|
|
23
|
+
"timezoneId": "Asia/Shanghai",
|
|
24
|
+
"viewport": {
|
|
25
|
+
"width": 1366,
|
|
26
|
+
"height": 768
|
|
27
|
+
},
|
|
28
|
+
"screen": {
|
|
29
|
+
"width": 1366,
|
|
30
|
+
"height": 768
|
|
31
|
+
},
|
|
32
|
+
"extraHTTPHeaders": {
|
|
33
|
+
"Accept-Language": "zh-CN,zh;q=0.9"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
package/lib/container-run.js
CHANGED
|
@@ -10,6 +10,7 @@ function buildContainerRunArgs(options) {
|
|
|
10
10
|
'--name', options.containerName,
|
|
11
11
|
'--entrypoint', '',
|
|
12
12
|
...(options.contModeArgs || []),
|
|
13
|
+
...(options.containerExtraArgs || []),
|
|
13
14
|
...(options.containerEnvs || []),
|
|
14
15
|
...(options.containerVolumes || []),
|
|
15
16
|
...(options.containerPorts || []),
|
package/lib/init-config.js
CHANGED
|
@@ -193,18 +193,7 @@ function collectCodexInitData(homeDir, ctx) {
|
|
|
193
193
|
|
|
194
194
|
if (configToml && typeof configToml === 'object') {
|
|
195
195
|
setInitValue(values, 'OPENAI_MODEL', configToml.model);
|
|
196
|
-
|
|
197
|
-
let providerConfig = null;
|
|
198
|
-
if (providers && typeof providers === 'object') {
|
|
199
|
-
const selectedProviderName =
|
|
200
|
-
(typeof configToml.model_provider === 'string' && providers[configToml.model_provider])
|
|
201
|
-
? configToml.model_provider
|
|
202
|
-
: Object.keys(providers)[0];
|
|
203
|
-
providerConfig = selectedProviderName ? providers[selectedProviderName] : null;
|
|
204
|
-
}
|
|
205
|
-
if (providerConfig && typeof providerConfig === 'object') {
|
|
206
|
-
setInitValue(values, 'OPENAI_BASE_URL', providerConfig.base_url);
|
|
207
|
-
}
|
|
196
|
+
setInitValue(values, 'OPENAI_BASE_URL', configToml.openai_base_url);
|
|
208
197
|
}
|
|
209
198
|
|
|
210
199
|
if (fs.existsSync(codexDir)) {
|
|
@@ -6,7 +6,7 @@ services:
|
|
|
6
6
|
args:
|
|
7
7
|
PLAYWRIGHT_MCP_BASE_IMAGE: mcr.microsoft.com/playwright/mcp:${PLAYWRIGHT_MCP_DOCKER_TAG:-latest}
|
|
8
8
|
image: ${PLAYWRIGHT_MCP_IMAGE:-localhost/xcanwin/manyoyo-playwright-headed}
|
|
9
|
-
container_name: ${PLAYWRIGHT_MCP_CONTAINER_NAME:-my-playwright-cont-headed}
|
|
9
|
+
container_name: ${PLAYWRIGHT_MCP_CONTAINER_NAME:-my-playwright-mcp-cont-headed}
|
|
10
10
|
init: true
|
|
11
11
|
stdin_open: true
|
|
12
12
|
ports:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
services:
|
|
2
2
|
playwright:
|
|
3
3
|
image: mcr.microsoft.com/playwright/mcp:${PLAYWRIGHT_MCP_DOCKER_TAG:-latest}
|
|
4
|
-
container_name: ${PLAYWRIGHT_MCP_CONTAINER_NAME:-my-playwright-cont-headless}
|
|
4
|
+
container_name: ${PLAYWRIGHT_MCP_CONTAINER_NAME:-my-playwright-mcp-cont-headless}
|
|
5
5
|
init: true
|
|
6
6
|
stdin_open: true
|
|
7
7
|
ports:
|
package/lib/plugin/playwright.js
CHANGED
|
@@ -15,42 +15,62 @@ const EXTENSIONS = [
|
|
|
15
15
|
['webgl-fingerprint-defender', 'olnbjpaejebpnokblkepbphhembdicik']
|
|
16
16
|
];
|
|
17
17
|
|
|
18
|
-
const SCENE_ORDER = ['cont-headless', 'cont-headed', 'host-headless', 'host-headed'];
|
|
18
|
+
const SCENE_ORDER = ['mcp-cont-headless', 'mcp-cont-headed', 'mcp-host-headless', 'mcp-host-headed', 'cli-host-headless', 'cli-host-headed'];
|
|
19
19
|
|
|
20
20
|
const SCENE_DEFS = {
|
|
21
|
-
'cont-headless': {
|
|
21
|
+
'mcp-cont-headless': {
|
|
22
22
|
type: 'container',
|
|
23
|
-
|
|
23
|
+
engine: 'mcp',
|
|
24
|
+
configFile: 'mcp-cont-headless.json',
|
|
24
25
|
composeFile: 'compose-headless.yaml',
|
|
25
|
-
projectName: 'my-playwright-cont-headless',
|
|
26
|
-
containerName: 'my-playwright-cont-headless',
|
|
26
|
+
projectName: 'my-playwright-mcp-cont-headless',
|
|
27
|
+
containerName: 'my-playwright-mcp-cont-headless',
|
|
27
28
|
portKey: 'contHeadless',
|
|
28
29
|
headless: true,
|
|
29
30
|
listenHost: '0.0.0.0'
|
|
30
31
|
},
|
|
31
|
-
'cont-headed': {
|
|
32
|
+
'mcp-cont-headed': {
|
|
32
33
|
type: 'container',
|
|
33
|
-
|
|
34
|
+
engine: 'mcp',
|
|
35
|
+
configFile: 'mcp-cont-headed.json',
|
|
34
36
|
composeFile: 'compose-headed.yaml',
|
|
35
|
-
projectName: 'my-playwright-cont-headed',
|
|
36
|
-
containerName: 'my-playwright-cont-headed',
|
|
37
|
+
projectName: 'my-playwright-mcp-cont-headed',
|
|
38
|
+
containerName: 'my-playwright-mcp-cont-headed',
|
|
37
39
|
portKey: 'contHeaded',
|
|
38
40
|
headless: false,
|
|
39
41
|
listenHost: '0.0.0.0'
|
|
40
42
|
},
|
|
41
|
-
'host-headless': {
|
|
43
|
+
'mcp-host-headless': {
|
|
42
44
|
type: 'host',
|
|
43
|
-
|
|
45
|
+
engine: 'mcp',
|
|
46
|
+
configFile: 'mcp-host-headless.json',
|
|
44
47
|
portKey: 'hostHeadless',
|
|
45
48
|
headless: true,
|
|
46
49
|
listenHost: '127.0.0.1'
|
|
47
50
|
},
|
|
48
|
-
'host-headed': {
|
|
51
|
+
'mcp-host-headed': {
|
|
49
52
|
type: 'host',
|
|
50
|
-
|
|
53
|
+
engine: 'mcp',
|
|
54
|
+
configFile: 'mcp-host-headed.json',
|
|
51
55
|
portKey: 'hostHeaded',
|
|
52
56
|
headless: false,
|
|
53
57
|
listenHost: '127.0.0.1'
|
|
58
|
+
},
|
|
59
|
+
'cli-host-headless': {
|
|
60
|
+
type: 'host',
|
|
61
|
+
engine: 'cli',
|
|
62
|
+
configFile: 'cli-host-headless.json',
|
|
63
|
+
portKey: 'cliHostHeadless',
|
|
64
|
+
headless: true,
|
|
65
|
+
listenHost: '0.0.0.0'
|
|
66
|
+
},
|
|
67
|
+
'cli-host-headed': {
|
|
68
|
+
type: 'host',
|
|
69
|
+
engine: 'cli',
|
|
70
|
+
configFile: 'cli-host-headed.json',
|
|
71
|
+
portKey: 'cliHostHeaded',
|
|
72
|
+
headless: false,
|
|
73
|
+
listenHost: '0.0.0.0'
|
|
54
74
|
}
|
|
55
75
|
};
|
|
56
76
|
|
|
@@ -67,6 +87,14 @@ const DEFAULT_FINGERPRINT_PROFILE = {
|
|
|
67
87
|
};
|
|
68
88
|
const DISABLE_WEBRTC_LAUNCH_ARGS = ['--disable-webrtc'];
|
|
69
89
|
|
|
90
|
+
function isMcpScene(sceneName) {
|
|
91
|
+
return Boolean(SCENE_DEFS[sceneName] && SCENE_DEFS[sceneName].engine === 'mcp');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isCliScene(sceneName) {
|
|
95
|
+
return Boolean(SCENE_DEFS[sceneName] && SCENE_DEFS[sceneName].engine === 'cli');
|
|
96
|
+
}
|
|
97
|
+
|
|
70
98
|
function platformFromUserAgent(userAgent) {
|
|
71
99
|
const ua = String(userAgent || '').toLowerCase();
|
|
72
100
|
if (ua.includes('macintosh') || ua.includes('mac os x')) {
|
|
@@ -264,6 +292,7 @@ class PlaywrightPlugin {
|
|
|
264
292
|
const defaultConfig = {
|
|
265
293
|
runtime: 'mixed',
|
|
266
294
|
enabledScenes: [...SCENE_ORDER],
|
|
295
|
+
cliSessionScene: 'cli-host-headless',
|
|
267
296
|
hostListen: '127.0.0.1',
|
|
268
297
|
mcpDefaultHost: 'host.docker.internal',
|
|
269
298
|
dockerTag: process.env.PLAYWRIGHT_MCP_DOCKER_TAG || 'latest',
|
|
@@ -281,6 +310,8 @@ class PlaywrightPlugin {
|
|
|
281
310
|
contHeaded: 8932,
|
|
282
311
|
hostHeadless: 8933,
|
|
283
312
|
hostHeaded: 8934,
|
|
313
|
+
cliHostHeadless: 8935,
|
|
314
|
+
cliHostHeaded: 8936,
|
|
284
315
|
contHeadedNoVnc: 6080
|
|
285
316
|
}
|
|
286
317
|
};
|
|
@@ -306,6 +337,7 @@ class PlaywrightPlugin {
|
|
|
306
337
|
asStringArray(this.globalConfig.enabledScenes, [...defaultConfig.enabledScenes])
|
|
307
338
|
);
|
|
308
339
|
merged.containerRuntime = this.resolveContainerRuntime(merged.containerRuntime);
|
|
340
|
+
merged.cliSessionScene = String(merged.cliSessionScene || defaultConfig.cliSessionScene).trim();
|
|
309
341
|
merged.navigatorPlatform = String(merged.navigatorPlatform || defaultConfig.navigatorPlatform).trim() || defaultConfig.navigatorPlatform;
|
|
310
342
|
merged.disableWebRTC = asBoolean(merged.disableWebRTC, defaultConfig.disableWebRTC);
|
|
311
343
|
|
|
@@ -317,6 +349,9 @@ class PlaywrightPlugin {
|
|
|
317
349
|
if (invalidScene) {
|
|
318
350
|
throw new Error(`playwright.enabledScenes 包含未知场景: ${invalidScene}`);
|
|
319
351
|
}
|
|
352
|
+
if (merged.cliSessionScene && !isCliScene(merged.cliSessionScene)) {
|
|
353
|
+
throw new Error(`playwright.cliSessionScene 无效: ${merged.cliSessionScene}`);
|
|
354
|
+
}
|
|
320
355
|
|
|
321
356
|
return merged;
|
|
322
357
|
}
|
|
@@ -412,6 +447,40 @@ class PlaywrightPlugin {
|
|
|
412
447
|
return !fs.existsSync(this.sceneConfigPath(sceneName));
|
|
413
448
|
}
|
|
414
449
|
|
|
450
|
+
sceneEndpointPath(sceneName) {
|
|
451
|
+
return path.join(this.config.runDir, `${sceneName}.endpoint.json`);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
readSceneEndpoint(sceneName) {
|
|
455
|
+
const filePath = this.sceneEndpointPath(sceneName);
|
|
456
|
+
if (!fs.existsSync(filePath)) {
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
try {
|
|
460
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
461
|
+
} catch {
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
writeSceneEndpoint(sceneName, payload) {
|
|
467
|
+
fs.mkdirSync(this.config.runDir, { recursive: true });
|
|
468
|
+
fs.writeFileSync(this.sceneEndpointPath(sceneName), `${JSON.stringify(payload, null, 4)}\n`, 'utf8');
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
removeSceneEndpoint(sceneName) {
|
|
472
|
+
fs.rmSync(this.sceneEndpointPath(sceneName), { force: true });
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
sceneCliAttachConfigPath(sceneName) {
|
|
476
|
+
return path.join(this.config.runDir, `${sceneName}.cli-attach.json`);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
writeSceneCliAttachConfig(sceneName, payload) {
|
|
480
|
+
fs.mkdirSync(this.config.runDir, { recursive: true });
|
|
481
|
+
fs.writeFileSync(this.sceneCliAttachConfigPath(sceneName), `${JSON.stringify(payload, null, 4)}\n`, 'utf8');
|
|
482
|
+
}
|
|
483
|
+
|
|
415
484
|
sceneInitScriptPath(sceneName) {
|
|
416
485
|
const configFile = path.basename(this.sceneConfigPath(sceneName), '.json');
|
|
417
486
|
return path.join(this.config.configDir, `${configFile}.init.js`);
|
|
@@ -479,6 +548,9 @@ class PlaywrightPlugin {
|
|
|
479
548
|
}
|
|
480
549
|
|
|
481
550
|
defaultBrowserName(sceneName) {
|
|
551
|
+
if (isCliScene(sceneName)) {
|
|
552
|
+
return 'chromium';
|
|
553
|
+
}
|
|
482
554
|
const cfg = this.buildSceneConfig(sceneName);
|
|
483
555
|
const browserName = cfg && cfg.browser && cfg.browser.browserName;
|
|
484
556
|
return String(browserName || 'chromium');
|
|
@@ -494,10 +566,10 @@ class PlaywrightPlugin {
|
|
|
494
566
|
}
|
|
495
567
|
|
|
496
568
|
ensureHostScenePrerequisites(sceneName) {
|
|
497
|
-
if (!this.sceneConfigMissing(sceneName)) {
|
|
569
|
+
if (!isCliScene(sceneName) && !this.sceneConfigMissing(sceneName)) {
|
|
498
570
|
return;
|
|
499
571
|
}
|
|
500
|
-
this.runCmd([this.
|
|
572
|
+
this.runCmd([this.playwrightBinPath(sceneName), 'install', '--with-deps', this.defaultBrowserName(sceneName)], { check: true });
|
|
501
573
|
}
|
|
502
574
|
|
|
503
575
|
scenePidFile(sceneName) {
|
|
@@ -517,6 +589,26 @@ class PlaywrightPlugin {
|
|
|
517
589
|
return binPath;
|
|
518
590
|
}
|
|
519
591
|
|
|
592
|
+
playwrightBinPath(sceneName) {
|
|
593
|
+
if (!isCliScene(sceneName)) {
|
|
594
|
+
return this.localBinPath('playwright');
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const filename = process.platform === 'win32' ? 'playwright.cmd' : 'playwright';
|
|
598
|
+
const candidates = [
|
|
599
|
+
path.join(this.projectRoot, 'node_modules', '@playwright', 'mcp', 'node_modules', '.bin', filename),
|
|
600
|
+
path.join(this.projectRoot, 'node_modules', '.bin', filename)
|
|
601
|
+
];
|
|
602
|
+
|
|
603
|
+
for (const candidate of candidates) {
|
|
604
|
+
if (fs.existsSync(candidate)) {
|
|
605
|
+
return candidate;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
throw new Error(`local binary not found for ${sceneName}. Run npm install first.`);
|
|
610
|
+
}
|
|
611
|
+
|
|
520
612
|
extensionDirPath() {
|
|
521
613
|
return path.join(os.homedir(), '.manyoyo', 'plugin', 'playwright', 'extensions');
|
|
522
614
|
}
|
|
@@ -650,22 +742,25 @@ class PlaywrightPlugin {
|
|
|
650
742
|
return { containerPaths, volumeMounts };
|
|
651
743
|
}
|
|
652
744
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
const port = this.scenePort(sceneName);
|
|
656
|
-
const extensionPaths = asStringArray(options.extensionPaths, []);
|
|
657
|
-
const initScript = asStringArray(options.initScript, []);
|
|
658
|
-
const baseLaunchArgs = [
|
|
745
|
+
baseLaunchArgs() {
|
|
746
|
+
return [
|
|
659
747
|
`--user-agent=${DEFAULT_FINGERPRINT_PROFILE.userAgent}`,
|
|
660
748
|
`--lang=${DEFAULT_FINGERPRINT_PROFILE.locale}`,
|
|
661
749
|
`--window-size=${DEFAULT_FINGERPRINT_PROFILE.width},${DEFAULT_FINGERPRINT_PROFILE.height}`,
|
|
662
750
|
'--disable-blink-features=AutomationControlled',
|
|
663
751
|
'--force-webrtc-ip-handling-policy=disable_non_proxied_udp'
|
|
664
752
|
];
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
buildMcpSceneConfig(sceneName, options = {}) {
|
|
756
|
+
const def = SCENE_DEFS[sceneName];
|
|
757
|
+
const port = this.scenePort(sceneName);
|
|
758
|
+
const extensionPaths = asStringArray(options.extensionPaths, []);
|
|
759
|
+
const initScript = asStringArray(options.initScript, []);
|
|
665
760
|
const launchOptions = {
|
|
666
761
|
channel: 'chromium',
|
|
667
762
|
headless: def.headless,
|
|
668
|
-
args: [...baseLaunchArgs]
|
|
763
|
+
args: [...this.baseLaunchArgs()]
|
|
669
764
|
};
|
|
670
765
|
|
|
671
766
|
if (extensionPaths.length > 0) {
|
|
@@ -676,7 +771,7 @@ class PlaywrightPlugin {
|
|
|
676
771
|
}
|
|
677
772
|
|
|
678
773
|
return {
|
|
679
|
-
outputDir: '/tmp
|
|
774
|
+
outputDir: '/tmp/.playwright-mcp',
|
|
680
775
|
server: {
|
|
681
776
|
host: def.listenHost,
|
|
682
777
|
port,
|
|
@@ -712,8 +807,44 @@ class PlaywrightPlugin {
|
|
|
712
807
|
};
|
|
713
808
|
}
|
|
714
809
|
|
|
810
|
+
buildCliSceneConfig(sceneName, options = {}) {
|
|
811
|
+
const def = SCENE_DEFS[sceneName];
|
|
812
|
+
const extensionPaths = asStringArray(options.extensionPaths, []);
|
|
813
|
+
const payload = {
|
|
814
|
+
host: def.listenHost,
|
|
815
|
+
port: this.scenePort(sceneName),
|
|
816
|
+
wsPath: `/${sceneName}-${crypto.randomBytes(8).toString('hex')}`,
|
|
817
|
+
headless: def.headless,
|
|
818
|
+
channel: 'chromium',
|
|
819
|
+
chromiumSandbox: true,
|
|
820
|
+
args: [...this.baseLaunchArgs()]
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
if (extensionPaths.length > 0) {
|
|
824
|
+
payload.args.push(...this.buildExtensionLaunchArgs(extensionPaths));
|
|
825
|
+
}
|
|
826
|
+
if (this.config.disableWebRTC) {
|
|
827
|
+
payload.args.push(...DISABLE_WEBRTC_LAUNCH_ARGS);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
return payload;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
buildSceneConfig(sceneName, options = {}) {
|
|
834
|
+
if (isCliScene(sceneName)) {
|
|
835
|
+
return this.buildCliSceneConfig(sceneName, options);
|
|
836
|
+
}
|
|
837
|
+
return this.buildMcpSceneConfig(sceneName, options);
|
|
838
|
+
}
|
|
839
|
+
|
|
715
840
|
ensureSceneConfig(sceneName, options = {}) {
|
|
716
841
|
fs.mkdirSync(this.config.configDir, { recursive: true });
|
|
842
|
+
if (isCliScene(sceneName)) {
|
|
843
|
+
const payload = this.buildCliSceneConfig(sceneName, options);
|
|
844
|
+
const filePath = this.sceneConfigPath(sceneName);
|
|
845
|
+
fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 4)}\n`, 'utf8');
|
|
846
|
+
return filePath;
|
|
847
|
+
}
|
|
717
848
|
const initScriptPath = this.ensureSceneInitScript(sceneName);
|
|
718
849
|
const configuredInitScript = asStringArray(options.initScript, []);
|
|
719
850
|
const initScript = configuredInitScript.length > 0 ? configuredInitScript : [initScriptPath];
|
|
@@ -772,13 +903,13 @@ class PlaywrightPlugin {
|
|
|
772
903
|
PLAYWRIGHT_MCP_NOVNC_PORT: String(this.config.ports.contHeadedNoVnc)
|
|
773
904
|
};
|
|
774
905
|
|
|
775
|
-
if (sceneName === 'cont-headed') {
|
|
906
|
+
if (sceneName === 'mcp-cont-headed') {
|
|
776
907
|
const envKey = this.config.vncPasswordEnvKey;
|
|
777
908
|
let password = process.env[envKey];
|
|
778
909
|
if (!password) {
|
|
779
910
|
password = this.randomAlnum(16);
|
|
780
911
|
if (requireVncPassword) {
|
|
781
|
-
this.writeStdout(`[up] cont-headed ${envKey} not set; generated random 16-char password: ${password}`);
|
|
912
|
+
this.writeStdout(`[up] mcp-cont-headed ${envKey} not set; generated random 16-char password: ${password}`);
|
|
782
913
|
}
|
|
783
914
|
}
|
|
784
915
|
env.VNC_PASSWORD = password;
|
|
@@ -816,6 +947,37 @@ class PlaywrightPlugin {
|
|
|
816
947
|
return overridePath;
|
|
817
948
|
}
|
|
818
949
|
|
|
950
|
+
buildCliSessionIntegration(dockerCmd) {
|
|
951
|
+
const sceneName = this.config.cliSessionScene;
|
|
952
|
+
if (!sceneName) {
|
|
953
|
+
return { envEntries: [], extraArgs: [], volumeEntries: [] };
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
const endpoint = this.readSceneEndpoint(sceneName);
|
|
957
|
+
if (!endpoint || !Number.isInteger(endpoint.port) || endpoint.port <= 0 || typeof endpoint.wsPath !== 'string' || !endpoint.wsPath) {
|
|
958
|
+
return { envEntries: [], extraArgs: [], volumeEntries: [] };
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
const normalizedDockerCmd = String(dockerCmd || '').trim().toLowerCase();
|
|
962
|
+
const connectHost = normalizedDockerCmd === 'podman' ? 'host.containers.internal' : 'host.docker.internal';
|
|
963
|
+
const remoteEndpoint = `ws://${connectHost}:${endpoint.port}${endpoint.wsPath}`;
|
|
964
|
+
const hostConfigPath = this.sceneCliAttachConfigPath(sceneName);
|
|
965
|
+
const containerConfigPath = `/tmp/manyoyo-playwright/${sceneName}.cli-attach.json`;
|
|
966
|
+
this.writeSceneCliAttachConfig(sceneName, {
|
|
967
|
+
browser: {
|
|
968
|
+
remoteEndpoint
|
|
969
|
+
}
|
|
970
|
+
});
|
|
971
|
+
const envEntries = [
|
|
972
|
+
`PLAYWRIGHT_MCP_CONFIG=${containerConfigPath}`
|
|
973
|
+
];
|
|
974
|
+
const extraArgs = normalizedDockerCmd === 'docker'
|
|
975
|
+
? ['--add-host', 'host.docker.internal:host-gateway']
|
|
976
|
+
: [];
|
|
977
|
+
const volumeEntries = ['--volume', `${hostConfigPath}:${containerConfigPath}:ro`];
|
|
978
|
+
return { envEntries, extraArgs, volumeEntries };
|
|
979
|
+
}
|
|
980
|
+
|
|
819
981
|
async startContainer(sceneName, options = {}) {
|
|
820
982
|
const runtime = this.config.containerRuntime;
|
|
821
983
|
if (!this.ensureCommandAvailable(runtime)) {
|
|
@@ -972,8 +1134,21 @@ class PlaywrightPlugin {
|
|
|
972
1134
|
return cp.returncode === 0 ? 0 : 1;
|
|
973
1135
|
}
|
|
974
1136
|
|
|
975
|
-
|
|
976
|
-
|
|
1137
|
+
hostLaunchCommand(sceneName, cfgPath) {
|
|
1138
|
+
if (isCliScene(sceneName)) {
|
|
1139
|
+
return {
|
|
1140
|
+
command: this.playwrightBinPath(sceneName),
|
|
1141
|
+
args: ['launch-server', '--browser', this.defaultBrowserName(sceneName), '--config', String(cfgPath)]
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
return {
|
|
1145
|
+
command: this.localBinPath('playwright-mcp'),
|
|
1146
|
+
args: ['--config', String(cfgPath)]
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
spawnHostProcess(command, args, logFd) {
|
|
1151
|
+
return spawn(command, args, {
|
|
977
1152
|
detached: true,
|
|
978
1153
|
stdio: ['ignore', logFd, logFd]
|
|
979
1154
|
});
|
|
@@ -998,7 +1173,9 @@ class PlaywrightPlugin {
|
|
|
998
1173
|
|
|
999
1174
|
hostScenePids(sceneName) {
|
|
1000
1175
|
const cfgPath = this.sceneConfigPath(sceneName);
|
|
1001
|
-
const pattern =
|
|
1176
|
+
const pattern = isCliScene(sceneName)
|
|
1177
|
+
? `playwright.*launch-server.*--config ${cfgPath}`
|
|
1178
|
+
: `playwright-mcp.*--config ${cfgPath}`;
|
|
1002
1179
|
const cp = this.runCmd(['pgrep', '-f', pattern], { captureOutput: true, check: false });
|
|
1003
1180
|
|
|
1004
1181
|
if (cp.returncode !== 0 || !cp.stdout.trim()) {
|
|
@@ -1043,6 +1220,7 @@ class PlaywrightPlugin {
|
|
|
1043
1220
|
const pidFile = this.scenePidFile(sceneName);
|
|
1044
1221
|
const logFile = this.sceneLogFile(sceneName);
|
|
1045
1222
|
const port = this.scenePort(sceneName);
|
|
1223
|
+
this.removeSceneEndpoint(sceneName);
|
|
1046
1224
|
|
|
1047
1225
|
let managedPids = this.hostScenePids(sceneName);
|
|
1048
1226
|
if (managedPids.length > 0 && (await this.portReady(port))) {
|
|
@@ -1058,22 +1236,29 @@ class PlaywrightPlugin {
|
|
|
1058
1236
|
|
|
1059
1237
|
fs.rmSync(pidFile, { force: true });
|
|
1060
1238
|
const logFd = fs.openSync(logFile, 'a');
|
|
1061
|
-
let
|
|
1239
|
+
let launchCommand = null;
|
|
1062
1240
|
try {
|
|
1063
|
-
|
|
1241
|
+
launchCommand = this.hostLaunchCommand(sceneName, cfgPath);
|
|
1064
1242
|
} catch (error) {
|
|
1065
1243
|
fs.closeSync(logFd);
|
|
1066
1244
|
this.writeStderr(`[up] ${sceneName} failed: ${error.message || String(error)}`);
|
|
1067
1245
|
return 1;
|
|
1068
1246
|
}
|
|
1069
1247
|
|
|
1070
|
-
const starter = this.spawnHostProcess(
|
|
1248
|
+
const starter = this.spawnHostProcess(launchCommand.command, launchCommand.args, logFd);
|
|
1071
1249
|
fs.closeSync(logFd);
|
|
1072
1250
|
if (typeof starter.unref === 'function') {
|
|
1073
1251
|
starter.unref();
|
|
1074
1252
|
}
|
|
1075
1253
|
|
|
1076
1254
|
if (await this.waitForPort(port)) {
|
|
1255
|
+
if (isCliScene(sceneName)) {
|
|
1256
|
+
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
|
|
1257
|
+
this.writeSceneEndpoint(sceneName, {
|
|
1258
|
+
port,
|
|
1259
|
+
wsPath: String(cfg.wsPath || '')
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1077
1262
|
managedPids = await this.waitForHostPids(sceneName, starter.pid);
|
|
1078
1263
|
if (managedPids.length > 0) {
|
|
1079
1264
|
fs.writeFileSync(pidFile, `${managedPids[0]}`, 'utf8');
|
|
@@ -1099,6 +1284,7 @@ class PlaywrightPlugin {
|
|
|
1099
1284
|
const pidFile = this.scenePidFile(sceneName);
|
|
1100
1285
|
const port = this.scenePort(sceneName);
|
|
1101
1286
|
const managedPids = this.hostScenePids(sceneName);
|
|
1287
|
+
this.removeSceneEndpoint(sceneName);
|
|
1102
1288
|
|
|
1103
1289
|
for (const pid of managedPids) {
|
|
1104
1290
|
try {
|
|
@@ -1337,7 +1523,7 @@ class PlaywrightPlugin {
|
|
|
1337
1523
|
return 1;
|
|
1338
1524
|
}
|
|
1339
1525
|
|
|
1340
|
-
const scenes = this.resolveTargets('all');
|
|
1526
|
+
const scenes = this.resolveTargets('all').filter(sceneName => isMcpScene(sceneName));
|
|
1341
1527
|
for (const sceneName of scenes) {
|
|
1342
1528
|
const url = `http://${host}:${this.scenePort(sceneName)}/mcp`;
|
|
1343
1529
|
this.writeStdout(`claude mcp add -t http -s user playwright-${sceneName} ${url}`);
|
|
@@ -1391,7 +1577,7 @@ class PlaywrightPlugin {
|
|
|
1391
1577
|
return 1;
|
|
1392
1578
|
}
|
|
1393
1579
|
|
|
1394
|
-
async run({ action, scene = 'host-headless', host = '', extensionPaths = [], extensionNames = [], prodversion = '' }) {
|
|
1580
|
+
async run({ action, scene = 'mcp-host-headless', host = '', extensionPaths = [], extensionNames = [], prodversion = '' }) {
|
|
1395
1581
|
if (action === 'ls') {
|
|
1396
1582
|
return this.printSummary();
|
|
1397
1583
|
}
|
package/lib/web/server.js
CHANGED
|
@@ -778,6 +778,7 @@ function buildStaticContainerRuntime(ctx, containerName) {
|
|
|
778
778
|
imageName: ctx.imageName,
|
|
779
779
|
imageVersion: ctx.imageVersion,
|
|
780
780
|
contModeArgs: Array.isArray(ctx.contModeArgs) ? ctx.contModeArgs.slice() : [],
|
|
781
|
+
containerExtraArgs: Array.isArray(ctx.containerExtraArgs) ? ctx.containerExtraArgs.slice() : [],
|
|
781
782
|
containerEnvs: Array.isArray(ctx.containerEnvs) ? ctx.containerEnvs.slice() : [],
|
|
782
783
|
containerVolumes: Array.isArray(ctx.containerVolumes) ? ctx.containerVolumes.slice() : [],
|
|
783
784
|
containerPorts: Array.isArray(ctx.containerPorts) ? ctx.containerPorts.slice() : [],
|
|
@@ -918,6 +919,7 @@ function buildCreateRuntime(ctx, state, payload) {
|
|
|
918
919
|
imageName,
|
|
919
920
|
imageVersion,
|
|
920
921
|
contModeArgs,
|
|
922
|
+
containerExtraArgs: Array.isArray(ctx.containerExtraArgs) ? ctx.containerExtraArgs.slice() : [],
|
|
921
923
|
containerEnvs,
|
|
922
924
|
containerVolumes,
|
|
923
925
|
containerPorts,
|
|
@@ -1019,6 +1021,7 @@ async function ensureWebContainer(ctx, state, containerInput) {
|
|
|
1019
1021
|
imageName: runtime.imageName,
|
|
1020
1022
|
imageVersion: runtime.imageVersion,
|
|
1021
1023
|
contModeArgs: runtime.contModeArgs,
|
|
1024
|
+
containerExtraArgs: runtime.containerExtraArgs,
|
|
1022
1025
|
containerEnvs: runtime.containerEnvs,
|
|
1023
1026
|
containerVolumes: runtime.containerVolumes,
|
|
1024
1027
|
containerPorts: runtime.containerPorts,
|
|
@@ -1770,6 +1773,7 @@ async function startWebServer(options) {
|
|
|
1770
1773
|
execCommand: options.execCommand,
|
|
1771
1774
|
execCommandSuffix: options.execCommandSuffix,
|
|
1772
1775
|
contModeArgs: options.contModeArgs,
|
|
1776
|
+
containerExtraArgs: options.containerExtraArgs,
|
|
1773
1777
|
containerEnvs: options.containerEnvs,
|
|
1774
1778
|
containerVolumes: options.containerVolumes,
|
|
1775
1779
|
containerPorts: options.containerPorts,
|
package/manyoyo.example.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"hostPath": "/path/to/your/project",
|
|
7
7
|
"containerPath": "/path/to/your/project",
|
|
8
8
|
"imageName": "localhost/xcanwin/manyoyo",
|
|
9
|
-
"imageVersion": "1.8.
|
|
9
|
+
"imageVersion": "1.8.8-common",
|
|
10
10
|
"containerMode": "common",
|
|
11
11
|
|
|
12
12
|
// 容器启动环境(env 按 key 合并覆盖;其余数组参数会累加)
|
|
@@ -50,10 +50,12 @@
|
|
|
50
50
|
// mixed: 支持容器+宿主机;container: 仅容器;host: 仅宿主机
|
|
51
51
|
"runtime": "mixed",
|
|
52
52
|
// 启用场景(可按需裁剪)
|
|
53
|
-
"enabledScenes": ["cont-headless", "cont-headed", "host-headless", "host-headed"],
|
|
53
|
+
"enabledScenes": ["mcp-cont-headless", "mcp-cont-headed", "mcp-host-headless", "mcp-host-headed", "cli-host-headless", "cli-host-headed"],
|
|
54
|
+
// my run 默认注入的 playwright-cli 宿主场景
|
|
55
|
+
"cliSessionScene": "cli-host-headless",
|
|
54
56
|
// mcp-add 默认 host(可改为 localhost / 127.0.0.1)
|
|
55
57
|
"mcpDefaultHost": "host.docker.internal",
|
|
56
|
-
// cont-headed 场景读取的密码环境变量名(默认 VNC_PASSWORD)
|
|
58
|
+
// mcp-cont-headed 场景读取的密码环境变量名(默认 VNC_PASSWORD)
|
|
57
59
|
"vncPasswordEnvKey": "VNC_PASSWORD",
|
|
58
60
|
// playwright ext-download 的 CRX prodversion 参数
|
|
59
61
|
"extensionProdversion": "132.0.0.0",
|
|
@@ -66,6 +68,8 @@
|
|
|
66
68
|
"contHeaded": 8932,
|
|
67
69
|
"hostHeadless": 8933,
|
|
68
70
|
"hostHeaded": 8934,
|
|
71
|
+
"cliHostHeadless": 8935,
|
|
72
|
+
"cliHostHeaded": 8936,
|
|
69
73
|
"contHeadedNoVnc": 6080
|
|
70
74
|
}
|
|
71
75
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xcanwin/manyoyo",
|
|
3
|
-
"version": "5.
|
|
4
|
-
"imageVersion": "1.8.
|
|
3
|
+
"version": "5.4.4",
|
|
4
|
+
"imageVersion": "1.8.8-common",
|
|
5
5
|
"description": "AI Agent CLI Security Sandbox for Docker and Podman",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"manyoyo",
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"README.md",
|
|
51
51
|
"LICENSE",
|
|
52
52
|
"docker/manyoyo.Dockerfile",
|
|
53
|
+
"docker/res/**",
|
|
53
54
|
"manyoyo.example.json"
|
|
54
55
|
],
|
|
55
56
|
"dependencies": {
|