@xcanwin/manyoyo 5.4.4 → 5.4.12
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 +29 -10
- package/docker/manyoyo.Dockerfile +10 -12
- package/docker/res/playwright/playwright-cli-wrapper.sh +82 -0
- package/lib/global-config.js +315 -0
- package/lib/plugin/playwright.js +59 -19
- package/manyoyo.example.json +5 -5
- 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.12-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.12-common
|
|
141
141
|
|
|
142
142
|
# full 版本
|
|
143
|
-
manyoyo build --iv 1.8.
|
|
143
|
+
manyoyo build --iv 1.8.12-full
|
|
144
144
|
|
|
145
145
|
# 自定义工具集
|
|
146
146
|
manyoyo build --iba TOOL=go,codex,java,gemini
|
package/bin/manyoyo.js
CHANGED
|
@@ -8,9 +8,9 @@ const crypto = require('crypto');
|
|
|
8
8
|
const net = require('net');
|
|
9
9
|
const readline = require('readline');
|
|
10
10
|
const { Command } = require('commander');
|
|
11
|
-
const JSON5 = require('json5');
|
|
12
11
|
const { startWebServer } = require('../lib/web/server');
|
|
13
12
|
const { buildContainerRunArgs, buildContainerRunCommand } = require('../lib/container-run');
|
|
13
|
+
const { getManyoyoConfigPath, readManyoyoConfig, syncGlobalImageVersion } = require('../lib/global-config');
|
|
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');
|
|
@@ -309,19 +309,29 @@ function installServeProcessDiagnostics(logger) {
|
|
|
309
309
|
* @returns {Config} 配置对象
|
|
310
310
|
*/
|
|
311
311
|
function loadConfig() {
|
|
312
|
-
const
|
|
313
|
-
if (
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
return config;
|
|
317
|
-
} catch (e) {
|
|
318
|
-
console.error(`${YELLOW}⚠️ 配置文件格式错误: ${configPath}${NC}`);
|
|
312
|
+
const result = readManyoyoConfig();
|
|
313
|
+
if (result.exists) {
|
|
314
|
+
if (result.parseError) {
|
|
315
|
+
console.error(`${YELLOW}⚠️ 配置文件格式错误: ${result.path}${NC}`);
|
|
319
316
|
return {};
|
|
320
317
|
}
|
|
318
|
+
return result.config;
|
|
321
319
|
}
|
|
322
320
|
return {};
|
|
323
321
|
}
|
|
324
322
|
|
|
323
|
+
function syncBuiltImageVersionToGlobalConfig(imageVersion) {
|
|
324
|
+
const syncResult = syncGlobalImageVersion(imageVersion);
|
|
325
|
+
if (syncResult.updated) {
|
|
326
|
+
console.log(`${GREEN}✅ 已同步 ${path.basename(getManyoyoConfigPath())} 的 imageVersion: ${imageVersion}${NC}`);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (syncResult.reason === 'unchanged') {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
console.log(`${YELLOW}⚠️ 镜像构建成功,但未更新 imageVersion: ${syncResult.path}${NC}`);
|
|
333
|
+
}
|
|
334
|
+
|
|
325
335
|
function loadRunConfig(name, config) {
|
|
326
336
|
const runName = String(name || '').trim();
|
|
327
337
|
if (!runName) {
|
|
@@ -1061,6 +1071,14 @@ async function setupCommander() {
|
|
|
1061
1071
|
host: options.host || ''
|
|
1062
1072
|
}, options));
|
|
1063
1073
|
|
|
1074
|
+
command.command('cli-add')
|
|
1075
|
+
.description('输出 playwright-cli skill 安装命令')
|
|
1076
|
+
.action(() => selectPluginAction({
|
|
1077
|
+
action: 'cli-add',
|
|
1078
|
+
pluginName: 'playwright',
|
|
1079
|
+
scene: 'all'
|
|
1080
|
+
}));
|
|
1081
|
+
|
|
1064
1082
|
command.command('ext-download')
|
|
1065
1083
|
.description('下载并解压 Playwright 扩展到 ~/.manyoyo/plugin/playwright/extensions/')
|
|
1066
1084
|
.option('--prodversion <ver>', 'CRX 下载使用的 Chrome 版本号 (默认 132.0.0.0)')
|
|
@@ -1099,8 +1117,8 @@ async function setupCommander() {
|
|
|
1099
1117
|
${MANYOYO_NAME} serve 127.0.0.1:3000 启动本机网页服务
|
|
1100
1118
|
${MANYOYO_NAME} serve 127.0.0.1:3000 -d 后台启动;未设密码时会打印本次随机密码
|
|
1101
1119
|
${MANYOYO_NAME} serve 0.0.0.0:3000 -U admin -P 123 -d 后台启动并监听全部网卡
|
|
1102
|
-
${MANYOYO_NAME} playwright up mcp-host-headless
|
|
1103
|
-
${MANYOYO_NAME}
|
|
1120
|
+
${MANYOYO_NAME} playwright up mcp-host-headless 启动 playwright MCP 宿主场景(默认/推荐)
|
|
1121
|
+
${MANYOYO_NAME} playwright up cli-host-headless 启动 playwright CLI 宿主场景(供容器内 playwright-cli 附着)
|
|
1104
1122
|
${MANYOYO_NAME} run -n test -q tip -q cmd 多次使用静默选项
|
|
1105
1123
|
`);
|
|
1106
1124
|
|
|
@@ -1971,6 +1989,7 @@ async function main() {
|
|
|
1971
1989
|
pruneDanglingImages,
|
|
1972
1990
|
colors: { RED, GREEN, YELLOW, BLUE, CYAN, NC }
|
|
1973
1991
|
});
|
|
1992
|
+
syncBuiltImageVersionToGlobalConfig(runtime.imageVersion);
|
|
1974
1993
|
process.exit(0);
|
|
1975
1994
|
}
|
|
1976
1995
|
|
|
@@ -127,6 +127,7 @@ EOX
|
|
|
127
127
|
|
|
128
128
|
# 从 cache-stage 复制 Node.js(缓存或下载)
|
|
129
129
|
COPY --from=cache-stage /opt/node /usr/local
|
|
130
|
+
COPY ./package.json /tmp/manyoyo-package.json
|
|
130
131
|
COPY ./docker/res/playwright/cli-cont-headless.init.js /app/config/cli-cont-headless.init.js
|
|
131
132
|
COPY ./docker/res/playwright/cli-cont-headless.json /app/config/cli-cont-headless.json
|
|
132
133
|
COPY ./docker/res/ /tmp/docker-res/
|
|
@@ -202,22 +203,15 @@ RUN <<EOX
|
|
|
202
203
|
;; esac
|
|
203
204
|
|
|
204
205
|
# 安装 Playwright CLI skills(不在镜像构建阶段下载浏览器)
|
|
205
|
-
|
|
206
|
+
PLAYWRIGHT_CLI_VERSION=$(node -p "const pkg = require('/tmp/manyoyo-package.json'); const value = String(pkg.playwrightCliVersion || '').trim(); if (!value) { throw new Error('package.json.playwrightCliVersion is required'); } value")
|
|
207
|
+
npm install -g "@playwright/cli@${PLAYWRIGHT_CLI_VERSION}"
|
|
206
208
|
PLAYWRIGHT_CLI_INSTALL_DIR=/tmp/playwright-cli-install
|
|
207
209
|
mkdir -p "${PLAYWRIGHT_CLI_INSTALL_DIR}/.playwright"
|
|
208
|
-
|
|
209
|
-
{
|
|
210
|
-
"browser": {
|
|
211
|
-
"browserName": "chromium",
|
|
212
|
-
"launchOptions": {
|
|
213
|
-
"channel": "chrome"
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
EOF
|
|
210
|
+
echo '{"browser":{"browserName":"chromium","launchOptions":{"channel":"chromium"}}}' > "${PLAYWRIGHT_CLI_INSTALL_DIR}/.playwright/cli.config.json"
|
|
218
211
|
cd "${PLAYWRIGHT_CLI_INSTALL_DIR}"
|
|
219
|
-
playwright-cli install --skills
|
|
212
|
+
playwright-cli --config="${PLAYWRIGHT_CLI_INSTALL_DIR}/.playwright/cli.config.json" install --skills
|
|
220
213
|
mkdir -p "$HOME/.codex/skills/playwright-cli" ~/.gemini/skills/playwright-cli
|
|
214
|
+
cp -R "${PLAYWRIGHT_CLI_INSTALL_DIR}/.claude/skills/playwright-cli/." "$HOME/.claude/skills/playwright-cli/"
|
|
221
215
|
cp -R "${PLAYWRIGHT_CLI_INSTALL_DIR}/.claude/skills/playwright-cli/." "$HOME/.codex/skills/playwright-cli/"
|
|
222
216
|
cp -R "${PLAYWRIGHT_CLI_INSTALL_DIR}/.claude/skills/playwright-cli/." "$HOME/.gemini/skills/playwright-cli/"
|
|
223
217
|
cd $OLDPWD
|
|
@@ -225,9 +219,13 @@ EOF
|
|
|
225
219
|
|
|
226
220
|
# 清理
|
|
227
221
|
npm cache clean --force
|
|
222
|
+
rm -f /tmp/manyoyo-package.json
|
|
228
223
|
rm -rf /tmp/* /var/tmp/* /var/log/apt /var/log/*.log /var/lib/apt/lists/* ~/.npm ~/go/pkg/mod/cache
|
|
229
224
|
EOX
|
|
230
225
|
|
|
226
|
+
COPY ./docker/res/playwright/playwright-cli-wrapper.sh /usr/local/bin/playwright-cli
|
|
227
|
+
RUN chmod +x /usr/local/bin/playwright-cli
|
|
228
|
+
|
|
231
229
|
# 从 cache-stage 复制 JDT LSP(缓存或下载)
|
|
232
230
|
COPY --from=cache-stage /opt/jdtls /tmp/jdtls-cache
|
|
233
231
|
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
set -eu
|
|
3
|
+
|
|
4
|
+
PLAYWRIGHT_NODE_BIN="${PLAYWRIGHT_NODE_BIN:-node}"
|
|
5
|
+
PLAYWRIGHT_CLI_ROOT="/usr/local/lib/node_modules/@playwright/cli/node_modules/playwright"
|
|
6
|
+
PLAYWRIGHT_CLI_PROGRAM="${PLAYWRIGHT_CLI_ROOT}/lib/cli/client/program.js"
|
|
7
|
+
PLAYWRIGHT_REAL_CLI="${PLAYWRIGHT_CLI_ROOT}/cli.js"
|
|
8
|
+
|
|
9
|
+
resolve_browser() {
|
|
10
|
+
browser=""
|
|
11
|
+
|
|
12
|
+
while [ "$#" -gt 0 ]; do
|
|
13
|
+
case "$1" in
|
|
14
|
+
--browser=*)
|
|
15
|
+
browser="${1#--browser=}"
|
|
16
|
+
;;
|
|
17
|
+
--browser)
|
|
18
|
+
shift
|
|
19
|
+
if [ "$#" -gt 0 ]; then
|
|
20
|
+
browser="$1"
|
|
21
|
+
shift
|
|
22
|
+
fi
|
|
23
|
+
continue
|
|
24
|
+
;;
|
|
25
|
+
--)
|
|
26
|
+
break
|
|
27
|
+
;;
|
|
28
|
+
-*)
|
|
29
|
+
;;
|
|
30
|
+
*)
|
|
31
|
+
if [ -z "$browser" ]; then
|
|
32
|
+
browser="$1"
|
|
33
|
+
fi
|
|
34
|
+
;;
|
|
35
|
+
esac
|
|
36
|
+
shift
|
|
37
|
+
done
|
|
38
|
+
|
|
39
|
+
if [ -n "$browser" ]; then
|
|
40
|
+
printf '%s\n' "$browser"
|
|
41
|
+
return 0
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
"$PLAYWRIGHT_NODE_BIN" <<'NODE'
|
|
45
|
+
const fs = require('fs');
|
|
46
|
+
const path = require('path');
|
|
47
|
+
|
|
48
|
+
const candidates = [
|
|
49
|
+
process.env.PLAYWRIGHT_MCP_CONFIG,
|
|
50
|
+
path.resolve('.playwright/cli.config.json')
|
|
51
|
+
].filter(Boolean);
|
|
52
|
+
|
|
53
|
+
for (const filePath of candidates) {
|
|
54
|
+
try {
|
|
55
|
+
const config = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
56
|
+
const browser = config && config.browser ? config.browser : {};
|
|
57
|
+
const launchOptions = browser.launchOptions || {};
|
|
58
|
+
const channel = String(launchOptions.channel || browser.browserName || '').trim();
|
|
59
|
+
if (channel) {
|
|
60
|
+
process.stdout.write(channel);
|
|
61
|
+
process.exit(0);
|
|
62
|
+
}
|
|
63
|
+
} catch (_) {}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
process.stdout.write('chromium');
|
|
67
|
+
NODE
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if [ "${1-}" = "install-browser" ]; then
|
|
71
|
+
shift
|
|
72
|
+
for arg in "$@"; do
|
|
73
|
+
if [ "$arg" = "--help" ] || [ "$arg" = "-h" ]; then
|
|
74
|
+
exec "$PLAYWRIGHT_NODE_BIN" "$PLAYWRIGHT_CLI_PROGRAM" install-browser "$@"
|
|
75
|
+
fi
|
|
76
|
+
done
|
|
77
|
+
|
|
78
|
+
BROWSER="$(resolve_browser "$@")"
|
|
79
|
+
exec "$PLAYWRIGHT_NODE_BIN" "$PLAYWRIGHT_REAL_CLI" install "$BROWSER"
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
exec "$PLAYWRIGHT_NODE_BIN" "$PLAYWRIGHT_CLI_PROGRAM" "$@"
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const JSON5 = require('json5');
|
|
7
|
+
|
|
8
|
+
function getManyoyoConfigPath(homeDir = os.homedir()) {
|
|
9
|
+
return path.join(homeDir, '.manyoyo', 'manyoyo.json');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function readManyoyoConfig(homeDir = os.homedir()) {
|
|
13
|
+
const configPath = getManyoyoConfigPath(homeDir);
|
|
14
|
+
if (!fs.existsSync(configPath)) {
|
|
15
|
+
return {
|
|
16
|
+
path: configPath,
|
|
17
|
+
exists: false,
|
|
18
|
+
config: {}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const config = JSON5.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
24
|
+
return {
|
|
25
|
+
path: configPath,
|
|
26
|
+
exists: true,
|
|
27
|
+
config
|
|
28
|
+
};
|
|
29
|
+
} catch (error) {
|
|
30
|
+
return {
|
|
31
|
+
path: configPath,
|
|
32
|
+
exists: true,
|
|
33
|
+
config: {},
|
|
34
|
+
parseError: error
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readQuotedString(text, startIndex) {
|
|
40
|
+
const quote = text[startIndex];
|
|
41
|
+
let value = '';
|
|
42
|
+
|
|
43
|
+
for (let i = startIndex + 1; i < text.length; i += 1) {
|
|
44
|
+
const ch = text[i];
|
|
45
|
+
if (ch === '\\') {
|
|
46
|
+
value += ch;
|
|
47
|
+
if (i + 1 < text.length) {
|
|
48
|
+
value += text[i + 1];
|
|
49
|
+
i += 1;
|
|
50
|
+
}
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (ch === quote) {
|
|
54
|
+
return {
|
|
55
|
+
value,
|
|
56
|
+
end: i + 1
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
value += ch;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isIdentifierStart(ch) {
|
|
66
|
+
return /[A-Za-z_$]/.test(ch);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isIdentifierPart(ch) {
|
|
70
|
+
return /[A-Za-z0-9_$]/.test(ch);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function skipWhitespace(text, index) {
|
|
74
|
+
let i = index;
|
|
75
|
+
while (i < text.length && /\s/.test(text[i])) {
|
|
76
|
+
i += 1;
|
|
77
|
+
}
|
|
78
|
+
return i;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function findTopLevelPropertyValueRange(text, propertyName) {
|
|
82
|
+
let depth = 0;
|
|
83
|
+
let inString = '';
|
|
84
|
+
let inLineComment = false;
|
|
85
|
+
let inBlockComment = false;
|
|
86
|
+
|
|
87
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
88
|
+
const ch = text[i];
|
|
89
|
+
const next = text[i + 1];
|
|
90
|
+
|
|
91
|
+
if (inLineComment) {
|
|
92
|
+
if (ch === '\n') inLineComment = false;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (inBlockComment) {
|
|
96
|
+
if (ch === '*' && next === '/') {
|
|
97
|
+
inBlockComment = false;
|
|
98
|
+
i += 1;
|
|
99
|
+
}
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (inString) {
|
|
103
|
+
if (ch === '\\') {
|
|
104
|
+
i += 1;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (ch === inString) inString = '';
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (ch === '/' && next === '/') {
|
|
112
|
+
inLineComment = true;
|
|
113
|
+
i += 1;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (ch === '/' && next === '*') {
|
|
117
|
+
inBlockComment = true;
|
|
118
|
+
i += 1;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (depth === 1 && !/\s|,/.test(ch)) {
|
|
122
|
+
let property = '';
|
|
123
|
+
let cursor = i;
|
|
124
|
+
if (ch === '"' || ch === '\'') {
|
|
125
|
+
const token = readQuotedString(text, i);
|
|
126
|
+
if (!token) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
property = token.value;
|
|
130
|
+
cursor = token.end;
|
|
131
|
+
} else if (isIdentifierStart(ch)) {
|
|
132
|
+
cursor = i + 1;
|
|
133
|
+
while (cursor < text.length && isIdentifierPart(text[cursor])) {
|
|
134
|
+
cursor += 1;
|
|
135
|
+
}
|
|
136
|
+
property = text.slice(i, cursor);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (property) {
|
|
140
|
+
const colonIndex = skipWhitespace(text, cursor);
|
|
141
|
+
if (text[colonIndex] === ':') {
|
|
142
|
+
if (property !== propertyName) {
|
|
143
|
+
i = colonIndex;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let valueStart = skipWhitespace(text, colonIndex + 1);
|
|
148
|
+
let valueEnd = valueStart;
|
|
149
|
+
let valueString = '';
|
|
150
|
+
let valueLineComment = false;
|
|
151
|
+
let valueBlockComment = false;
|
|
152
|
+
let valueDepth = 0;
|
|
153
|
+
|
|
154
|
+
for (; valueEnd < text.length; valueEnd += 1) {
|
|
155
|
+
const valueCh = text[valueEnd];
|
|
156
|
+
const valueNext = text[valueEnd + 1];
|
|
157
|
+
|
|
158
|
+
if (valueLineComment) {
|
|
159
|
+
if (valueCh === '\n') valueLineComment = false;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (valueBlockComment) {
|
|
163
|
+
if (valueCh === '*' && valueNext === '/') {
|
|
164
|
+
valueBlockComment = false;
|
|
165
|
+
valueEnd += 1;
|
|
166
|
+
}
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (valueString) {
|
|
170
|
+
if (valueCh === '\\') {
|
|
171
|
+
valueEnd += 1;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (valueCh === valueString) valueString = '';
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (valueCh === '/' && valueNext === '/') {
|
|
179
|
+
valueLineComment = true;
|
|
180
|
+
valueEnd += 1;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (valueCh === '/' && valueNext === '*') {
|
|
184
|
+
valueBlockComment = true;
|
|
185
|
+
valueEnd += 1;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (valueCh === '"' || valueCh === '\'') {
|
|
189
|
+
valueString = valueCh;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (valueCh === '{' || valueCh === '[' || valueCh === '(') {
|
|
193
|
+
valueDepth += 1;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (valueCh === '}' || valueCh === ']' || valueCh === ')') {
|
|
197
|
+
if (valueDepth === 0) {
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
valueDepth -= 1;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (valueDepth === 0 && valueCh === ',') {
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
while (valueEnd > valueStart && /\s/.test(text[valueEnd - 1])) {
|
|
209
|
+
valueEnd -= 1;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
start: valueStart,
|
|
214
|
+
end: valueEnd
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (ch === '"' || ch === '\'') {
|
|
221
|
+
inString = ch;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (ch === '{' || ch === '[') {
|
|
225
|
+
depth += 1;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (ch === '}' || ch === ']') {
|
|
229
|
+
depth -= 1;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function insertTopLevelImageVersion(text, imageVersion) {
|
|
238
|
+
const openBraceIndex = text.indexOf('{');
|
|
239
|
+
if (openBraceIndex === -1) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const newlineIndex = text.indexOf('\n', openBraceIndex);
|
|
244
|
+
const insertIndex = newlineIndex === -1 ? openBraceIndex + 1 : newlineIndex + 1;
|
|
245
|
+
return `${text.slice(0, insertIndex)} imageVersion: ${JSON.stringify(imageVersion)},\n${text.slice(insertIndex)}`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function updateImageVersionText(text, imageVersion) {
|
|
249
|
+
const range = findTopLevelPropertyValueRange(text, 'imageVersion');
|
|
250
|
+
if (range) {
|
|
251
|
+
return `${text.slice(0, range.start)}${JSON.stringify(imageVersion)}${text.slice(range.end)}`;
|
|
252
|
+
}
|
|
253
|
+
return insertTopLevelImageVersion(text, imageVersion);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function syncGlobalImageVersion(imageVersion, options = {}) {
|
|
257
|
+
const homeDir = options.homeDir || os.homedir();
|
|
258
|
+
const result = readManyoyoConfig(homeDir);
|
|
259
|
+
const configPath = result.path;
|
|
260
|
+
|
|
261
|
+
if (result.parseError) {
|
|
262
|
+
return {
|
|
263
|
+
updated: false,
|
|
264
|
+
path: configPath,
|
|
265
|
+
reason: 'parse-error'
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const currentConfig = result.config;
|
|
270
|
+
if (typeof currentConfig !== 'object' || currentConfig === null || Array.isArray(currentConfig)) {
|
|
271
|
+
return {
|
|
272
|
+
updated: false,
|
|
273
|
+
path: configPath,
|
|
274
|
+
reason: 'invalid-root'
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (currentConfig.imageVersion === imageVersion) {
|
|
279
|
+
return {
|
|
280
|
+
updated: false,
|
|
281
|
+
path: configPath,
|
|
282
|
+
reason: 'unchanged'
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const nextConfig = {
|
|
287
|
+
...currentConfig,
|
|
288
|
+
imageVersion
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
292
|
+
if (result.exists) {
|
|
293
|
+
const currentText = fs.readFileSync(configPath, 'utf-8');
|
|
294
|
+
const updatedText = updateImageVersionText(currentText, imageVersion);
|
|
295
|
+
if (updatedText) {
|
|
296
|
+
fs.writeFileSync(configPath, updatedText.endsWith('\n') ? updatedText : `${updatedText}\n`);
|
|
297
|
+
} else {
|
|
298
|
+
fs.writeFileSync(configPath, `${JSON.stringify(nextConfig, null, 4)}\n`);
|
|
299
|
+
}
|
|
300
|
+
} else {
|
|
301
|
+
fs.writeFileSync(configPath, `${JSON.stringify(nextConfig, null, 4)}\n`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
updated: true,
|
|
306
|
+
path: configPath,
|
|
307
|
+
reason: result.exists ? 'updated' : 'created'
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
module.exports = {
|
|
312
|
+
getManyoyoConfigPath,
|
|
313
|
+
readManyoyoConfig,
|
|
314
|
+
syncGlobalImageVersion
|
|
315
|
+
};
|
package/lib/plugin/playwright.js
CHANGED
|
@@ -6,6 +6,7 @@ const os = require('os');
|
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const crypto = require('crypto');
|
|
8
8
|
const { spawn, spawnSync } = require('child_process');
|
|
9
|
+
const { playwrightCliVersion: PLAYWRIGHT_CLI_VERSION } = require('../../package.json');
|
|
9
10
|
|
|
10
11
|
const EXTENSIONS = [
|
|
11
12
|
['ublock-origin-lite', 'ddkjiahejlhfcafbddmgiahcphecmpfh'],
|
|
@@ -25,7 +26,7 @@ const SCENE_DEFS = {
|
|
|
25
26
|
composeFile: 'compose-headless.yaml',
|
|
26
27
|
projectName: 'my-playwright-mcp-cont-headless',
|
|
27
28
|
containerName: 'my-playwright-mcp-cont-headless',
|
|
28
|
-
portKey: '
|
|
29
|
+
portKey: 'mcpContHeadless',
|
|
29
30
|
headless: true,
|
|
30
31
|
listenHost: '0.0.0.0'
|
|
31
32
|
},
|
|
@@ -36,7 +37,7 @@ const SCENE_DEFS = {
|
|
|
36
37
|
composeFile: 'compose-headed.yaml',
|
|
37
38
|
projectName: 'my-playwright-mcp-cont-headed',
|
|
38
39
|
containerName: 'my-playwright-mcp-cont-headed',
|
|
39
|
-
portKey: '
|
|
40
|
+
portKey: 'mcpContHeaded',
|
|
40
41
|
headless: false,
|
|
41
42
|
listenHost: '0.0.0.0'
|
|
42
43
|
},
|
|
@@ -44,7 +45,7 @@ const SCENE_DEFS = {
|
|
|
44
45
|
type: 'host',
|
|
45
46
|
engine: 'mcp',
|
|
46
47
|
configFile: 'mcp-host-headless.json',
|
|
47
|
-
portKey: '
|
|
48
|
+
portKey: 'mcpHostHeadless',
|
|
48
49
|
headless: true,
|
|
49
50
|
listenHost: '127.0.0.1'
|
|
50
51
|
},
|
|
@@ -52,7 +53,7 @@ const SCENE_DEFS = {
|
|
|
52
53
|
type: 'host',
|
|
53
54
|
engine: 'mcp',
|
|
54
55
|
configFile: 'mcp-host-headed.json',
|
|
55
|
-
portKey: '
|
|
56
|
+
portKey: 'mcpHostHeaded',
|
|
56
57
|
headless: false,
|
|
57
58
|
listenHost: '127.0.0.1'
|
|
58
59
|
},
|
|
@@ -293,7 +294,6 @@ class PlaywrightPlugin {
|
|
|
293
294
|
runtime: 'mixed',
|
|
294
295
|
enabledScenes: [...SCENE_ORDER],
|
|
295
296
|
cliSessionScene: 'cli-host-headless',
|
|
296
|
-
hostListen: '127.0.0.1',
|
|
297
297
|
mcpDefaultHost: 'host.docker.internal',
|
|
298
298
|
dockerTag: process.env.PLAYWRIGHT_MCP_DOCKER_TAG || 'latest',
|
|
299
299
|
containerRuntime: '',
|
|
@@ -306,13 +306,13 @@ class PlaywrightPlugin {
|
|
|
306
306
|
disableWebRTC: false,
|
|
307
307
|
composeDir: path.join(__dirname, 'playwright-assets'),
|
|
308
308
|
ports: {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
309
|
+
mcpContHeadless: 8931,
|
|
310
|
+
mcpContHeaded: 8932,
|
|
311
|
+
mcpHostHeadless: 8933,
|
|
312
|
+
mcpHostHeaded: 8934,
|
|
313
313
|
cliHostHeadless: 8935,
|
|
314
314
|
cliHostHeaded: 8936,
|
|
315
|
-
|
|
315
|
+
mcpContHeadedNoVnc: 6080
|
|
316
316
|
}
|
|
317
317
|
};
|
|
318
318
|
|
|
@@ -380,6 +380,24 @@ class PlaywrightPlugin {
|
|
|
380
380
|
this.stderr.write(`${line}\n`);
|
|
381
381
|
}
|
|
382
382
|
|
|
383
|
+
remindCliSessionScene(sceneName) {
|
|
384
|
+
if (!isCliScene(sceneName)) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (sceneName !== 'cli-host-headed') {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (this.config.cliSessionScene === sceneName) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
this.writeStdout('[tip] 如果希望容器内 manyoyo run 自动附着到当前 CLI 宿主场景,请在 ~/.manyoyo/manyoyo.json 中设置:');
|
|
394
|
+
this.writeStdout(' "plugins": {');
|
|
395
|
+
this.writeStdout(' "playwright": {');
|
|
396
|
+
this.writeStdout(' "cliSessionScene": "cli-host-headed"');
|
|
397
|
+
this.writeStdout(' }');
|
|
398
|
+
this.writeStdout(' }');
|
|
399
|
+
}
|
|
400
|
+
|
|
383
401
|
randomAlnum(length = 16) {
|
|
384
402
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
385
403
|
let out = '';
|
|
@@ -481,15 +499,15 @@ class PlaywrightPlugin {
|
|
|
481
499
|
fs.writeFileSync(this.sceneCliAttachConfigPath(sceneName), `${JSON.stringify(payload, null, 4)}\n`, 'utf8');
|
|
482
500
|
}
|
|
483
501
|
|
|
502
|
+
removeSceneCliAttachConfig(sceneName) {
|
|
503
|
+
fs.rmSync(this.sceneCliAttachConfigPath(sceneName), { force: true });
|
|
504
|
+
}
|
|
505
|
+
|
|
484
506
|
sceneInitScriptPath(sceneName) {
|
|
485
507
|
const configFile = path.basename(this.sceneConfigPath(sceneName), '.json');
|
|
486
508
|
return path.join(this.config.configDir, `${configFile}.init.js`);
|
|
487
509
|
}
|
|
488
510
|
|
|
489
|
-
legacySceneInitScriptPath(sceneName) {
|
|
490
|
-
return path.join(this.config.configDir, `${sceneName}.init.js`);
|
|
491
|
-
}
|
|
492
|
-
|
|
493
511
|
buildInitScriptContent() {
|
|
494
512
|
const lines = [
|
|
495
513
|
"'use strict';",
|
|
@@ -540,10 +558,6 @@ class PlaywrightPlugin {
|
|
|
540
558
|
const filePath = this.sceneInitScriptPath(sceneName);
|
|
541
559
|
const content = this.buildInitScriptContent();
|
|
542
560
|
fs.writeFileSync(filePath, content, 'utf8');
|
|
543
|
-
const legacyFilePath = this.legacySceneInitScriptPath(sceneName);
|
|
544
|
-
if (legacyFilePath !== filePath) {
|
|
545
|
-
fs.rmSync(legacyFilePath, { force: true });
|
|
546
|
-
}
|
|
547
561
|
return filePath;
|
|
548
562
|
}
|
|
549
563
|
|
|
@@ -900,7 +914,7 @@ class PlaywrightPlugin {
|
|
|
900
914
|
PLAYWRIGHT_MCP_CONFIG_PATH: cfgPath,
|
|
901
915
|
PLAYWRIGHT_MCP_CONTAINER_NAME: def.containerName,
|
|
902
916
|
PLAYWRIGHT_MCP_IMAGE: this.config.headedImage,
|
|
903
|
-
PLAYWRIGHT_MCP_NOVNC_PORT: String(this.config.ports.
|
|
917
|
+
PLAYWRIGHT_MCP_NOVNC_PORT: String(this.config.ports.mcpContHeadedNoVnc)
|
|
904
918
|
};
|
|
905
919
|
|
|
906
920
|
if (sceneName === 'mcp-cont-headed') {
|
|
@@ -1221,6 +1235,7 @@ class PlaywrightPlugin {
|
|
|
1221
1235
|
const logFile = this.sceneLogFile(sceneName);
|
|
1222
1236
|
const port = this.scenePort(sceneName);
|
|
1223
1237
|
this.removeSceneEndpoint(sceneName);
|
|
1238
|
+
this.removeSceneCliAttachConfig(sceneName);
|
|
1224
1239
|
|
|
1225
1240
|
let managedPids = this.hostScenePids(sceneName);
|
|
1226
1241
|
if (managedPids.length > 0 && (await this.portReady(port))) {
|
|
@@ -1263,6 +1278,7 @@ class PlaywrightPlugin {
|
|
|
1263
1278
|
if (managedPids.length > 0) {
|
|
1264
1279
|
fs.writeFileSync(pidFile, `${managedPids[0]}`, 'utf8');
|
|
1265
1280
|
this.writeStdout(`[up] ${sceneName} ready on 127.0.0.1:${port} (pid(s) ${managedPids.join(' ')})`);
|
|
1281
|
+
this.remindCliSessionScene(sceneName);
|
|
1266
1282
|
return 0;
|
|
1267
1283
|
}
|
|
1268
1284
|
}
|
|
@@ -1285,6 +1301,7 @@ class PlaywrightPlugin {
|
|
|
1285
1301
|
const port = this.scenePort(sceneName);
|
|
1286
1302
|
const managedPids = this.hostScenePids(sceneName);
|
|
1287
1303
|
this.removeSceneEndpoint(sceneName);
|
|
1304
|
+
this.removeSceneCliAttachConfig(sceneName);
|
|
1288
1305
|
|
|
1289
1306
|
for (const pid of managedPids) {
|
|
1290
1307
|
try {
|
|
@@ -1542,6 +1559,25 @@ class PlaywrightPlugin {
|
|
|
1542
1559
|
return 0;
|
|
1543
1560
|
}
|
|
1544
1561
|
|
|
1562
|
+
printCliAdd() {
|
|
1563
|
+
const lines = [
|
|
1564
|
+
'PLAYWRIGHT_CLI_INSTALL_DIR="${TMPDIR:-/tmp}/manyoyo-playwright-cli-install-$$"',
|
|
1565
|
+
'mkdir -p "$PLAYWRIGHT_CLI_INSTALL_DIR/.playwright"',
|
|
1566
|
+
'echo \'{"browser":{"browserName":"chromium","launchOptions":{"channel":"chromium"}}}\' > "$PLAYWRIGHT_CLI_INSTALL_DIR/.playwright/cli.config.json"',
|
|
1567
|
+
'cd "$PLAYWRIGHT_CLI_INSTALL_DIR"',
|
|
1568
|
+
`npm install -g @playwright/cli@${PLAYWRIGHT_CLI_VERSION}`,
|
|
1569
|
+
'playwright-cli install --skills',
|
|
1570
|
+
'mkdir -p ~/.codex/skills/playwright-cli ~/.gemini/skills/playwright-cli',
|
|
1571
|
+
'cp -R "$PLAYWRIGHT_CLI_INSTALL_DIR/.claude/skills/playwright-cli/." ~/.claude/skills/playwright-cli/',
|
|
1572
|
+
'cp -R "$PLAYWRIGHT_CLI_INSTALL_DIR/.claude/skills/playwright-cli/." ~/.codex/skills/playwright-cli/',
|
|
1573
|
+
'cp -R "$PLAYWRIGHT_CLI_INSTALL_DIR/.claude/skills/playwright-cli/." ~/.gemini/skills/playwright-cli/',
|
|
1574
|
+
'cd $OLDPWD',
|
|
1575
|
+
'rm -rf "$PLAYWRIGHT_CLI_INSTALL_DIR"'
|
|
1576
|
+
];
|
|
1577
|
+
this.writeStdout(lines.join('\n'));
|
|
1578
|
+
return 0;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1545
1581
|
printSummary() {
|
|
1546
1582
|
const scenes = this.resolveTargets('all');
|
|
1547
1583
|
this.writeStdout(`playwright\truntime=${this.config.runtime}\tscenes=${scenes.join(',')}`);
|
|
@@ -1586,6 +1622,10 @@ class PlaywrightPlugin {
|
|
|
1586
1622
|
return this.printMcpAdd(host);
|
|
1587
1623
|
}
|
|
1588
1624
|
|
|
1625
|
+
if (action === 'cli-add') {
|
|
1626
|
+
return this.printCliAdd();
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1589
1629
|
if (action === 'ext-download') {
|
|
1590
1630
|
return await this.downloadExtensions({ prodversion });
|
|
1591
1631
|
}
|
package/manyoyo.example.json
CHANGED
|
@@ -64,13 +64,13 @@
|
|
|
64
64
|
// 是否禁用 WebRTC(默认 false)
|
|
65
65
|
"disableWebRTC": false,
|
|
66
66
|
"ports": {
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
67
|
+
"mcpContHeadless": 8931,
|
|
68
|
+
"mcpContHeaded": 8932,
|
|
69
|
+
"mcpHostHeadless": 8933,
|
|
70
|
+
"mcpHostHeaded": 8934,
|
|
71
71
|
"cliHostHeadless": 8935,
|
|
72
72
|
"cliHostHeaded": 8936,
|
|
73
|
-
"
|
|
73
|
+
"mcpContHeadedNoVnc": 6080
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
},
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xcanwin/manyoyo",
|
|
3
|
-
"version": "5.4.
|
|
4
|
-
"imageVersion": "1.8.
|
|
3
|
+
"version": "5.4.12",
|
|
4
|
+
"imageVersion": "1.8.12-common",
|
|
5
|
+
"playwrightCliVersion": "0.1.1",
|
|
5
6
|
"description": "AI Agent CLI Security Sandbox for Docker and Podman",
|
|
6
7
|
"keywords": [
|
|
7
8
|
"manyoyo",
|