@xcanwin/manyoyo 6.2.1 → 6.2.5
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/bin/manyoyo.js +8 -0
- package/lib/runtime-resolver.js +10 -0
- package/lib/web/frontend/app.css +17 -9
- package/lib/web/frontend/app.html +6 -4
- package/lib/web/frontend/app.js +26 -7
- package/lib/web/frontend/login.html +1 -2
- package/lib/web/server.js +44 -5
- package/manyoyo.example.json +7 -0
- package/package.json +1 -1
package/bin/manyoyo.js
CHANGED
|
@@ -90,6 +90,7 @@ let SERVER_PORT = 3000;
|
|
|
90
90
|
let SERVER_AUTH_USER = "";
|
|
91
91
|
let SERVER_AUTH_PASS = "";
|
|
92
92
|
let SERVER_AUTH_PASS_AUTO = false;
|
|
93
|
+
let SERVER_TITLE = null;
|
|
93
94
|
const SAFE_CONTAINER_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
|
|
94
95
|
|
|
95
96
|
// Color definitions using ANSI codes
|
|
@@ -306,6 +307,7 @@ function installServeProcessDiagnostics(logger) {
|
|
|
306
307
|
* @property {{shellPrefix?:string,shell?:string,shellSuffix?:string,env?:Object.<string,string|number|boolean>,envFile?:string[]}} [first] - 仅首次创建容器执行的一次性命令配置
|
|
307
308
|
* @property {string[]} [volumes] - 挂载卷数组
|
|
308
309
|
* @property {Object.<string, Object>} [plugins] - 可选插件配置映射(如 plugins.playwright)
|
|
310
|
+
* @property {{title?:string}} [serve] - serve 命令专属配置(如自定义网页标题,留空字符串则显示为空)
|
|
309
311
|
* @property {Object.<string, Object>} [runs] - 运行配置映射(-r <name>)
|
|
310
312
|
* @property {string} [yolo] - YOLO 模式
|
|
311
313
|
* @property {string} [containerMode] - 容器模式
|
|
@@ -1461,6 +1463,7 @@ Notes:
|
|
|
1461
1463
|
SERVER_AUTH_USER = resolvedRuntime.serverUser || '';
|
|
1462
1464
|
SERVER_AUTH_PASS = resolvedRuntime.serverPass || '';
|
|
1463
1465
|
SERVER_AUTH_PASS_AUTO = Boolean(resolvedRuntime.serverPassAuto);
|
|
1466
|
+
SERVER_TITLE = resolvedRuntime.serveTitle;
|
|
1464
1467
|
|
|
1465
1468
|
if (isShowConfigMode) {
|
|
1466
1469
|
const finalConfig = {
|
|
@@ -1489,6 +1492,9 @@ Notes:
|
|
|
1489
1492
|
serverPort: isServerMode ? SERVER_PORT : null,
|
|
1490
1493
|
serverUser: SERVER_AUTH_USER || "",
|
|
1491
1494
|
serverPass: SERVER_AUTH_PASS || "",
|
|
1495
|
+
serve: {
|
|
1496
|
+
title: SERVER_TITLE
|
|
1497
|
+
},
|
|
1492
1498
|
exec: {
|
|
1493
1499
|
prefix: EXEC_COMMAND_PREFIX,
|
|
1494
1500
|
shell: EXEC_COMMAND,
|
|
@@ -1590,6 +1596,7 @@ function createRuntimeContext(modeState = {}) {
|
|
|
1590
1596
|
serverAuthUser: SERVER_AUTH_USER,
|
|
1591
1597
|
serverAuthPass: SERVER_AUTH_PASS,
|
|
1592
1598
|
serverAuthPassAuto: SERVER_AUTH_PASS_AUTO,
|
|
1599
|
+
serveTitle: SERVER_TITLE,
|
|
1593
1600
|
logger: null
|
|
1594
1601
|
};
|
|
1595
1602
|
}
|
|
@@ -2086,6 +2093,7 @@ async function runWebServerMode(runtime) {
|
|
|
2086
2093
|
authUser: runtime.serverAuthUser,
|
|
2087
2094
|
authPass: runtime.serverAuthPass,
|
|
2088
2095
|
authPassAuto: runtime.serverAuthPassAuto,
|
|
2096
|
+
serveTitle: runtime.serveTitle,
|
|
2089
2097
|
dockerCmd: DOCKER_CMD,
|
|
2090
2098
|
hostPath: runtime.hostPath,
|
|
2091
2099
|
containerPath: runtime.containerPath,
|
package/lib/runtime-resolver.js
CHANGED
|
@@ -148,6 +148,15 @@ function resolveRuntimeConfig(options = {}) {
|
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
let serveTitle = null;
|
|
152
|
+
const globalServeConfig = globalConfig.serve && typeof globalConfig.serve === 'object' ? globalConfig.serve : null;
|
|
153
|
+
const runServeConfig = runConfig.serve && typeof runConfig.serve === 'object' ? runConfig.serve : null;
|
|
154
|
+
if (runServeConfig && Object.prototype.hasOwnProperty.call(runServeConfig, 'title')) {
|
|
155
|
+
serveTitle = String(runServeConfig.title);
|
|
156
|
+
} else if (globalServeConfig && Object.prototype.hasOwnProperty.call(globalServeConfig, 'title')) {
|
|
157
|
+
serveTitle = String(globalServeConfig.title);
|
|
158
|
+
}
|
|
159
|
+
|
|
151
160
|
if (!hostPath) {
|
|
152
161
|
hostPath = defaults.hostPath;
|
|
153
162
|
}
|
|
@@ -197,6 +206,7 @@ function resolveRuntimeConfig(options = {}) {
|
|
|
197
206
|
serverUser,
|
|
198
207
|
serverPass,
|
|
199
208
|
serverPassAuto,
|
|
209
|
+
serveTitle,
|
|
200
210
|
exec: {
|
|
201
211
|
prefix: execPrefix,
|
|
202
212
|
shell: execShell,
|
package/lib/web/frontend/app.css
CHANGED
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
|
|
14
14
|
--line: #d9c7ad;
|
|
15
15
|
--line-strong: #b59263;
|
|
16
|
-
--line-medium: rgba(181, 146, 99, 0.4);
|
|
17
16
|
--tree-guide: rgba(181, 146, 99, 0.44);
|
|
18
17
|
--tree-guide-soft: rgba(181, 146, 99, 0.44);
|
|
19
18
|
--tree-active: #ecdfc8;
|
|
@@ -118,7 +117,7 @@ body::after {
|
|
|
118
117
|
|
|
119
118
|
.sidebar {
|
|
120
119
|
min-height: 0;
|
|
121
|
-
padding: 16px;
|
|
120
|
+
padding: 16px 12px 8px;
|
|
122
121
|
display: flex;
|
|
123
122
|
flex-direction: column;
|
|
124
123
|
gap: 12px;
|
|
@@ -151,6 +150,7 @@ body::after {
|
|
|
151
150
|
font-size: 22px;
|
|
152
151
|
line-height: 1;
|
|
153
152
|
text-transform: uppercase;
|
|
153
|
+
white-space: nowrap;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
.sidebar-actions {
|
|
@@ -166,6 +166,16 @@ body::after {
|
|
|
166
166
|
min-width: 62px;
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
.sidebar-create button,
|
|
170
|
+
.sidebar-bottom button {
|
|
171
|
+
width: 100%;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.sidebar-bottom {
|
|
175
|
+
display: flex;
|
|
176
|
+
margin-top: -4px;
|
|
177
|
+
}
|
|
178
|
+
|
|
169
179
|
input:focus,
|
|
170
180
|
textarea:focus {
|
|
171
181
|
outline: none;
|
|
@@ -898,7 +908,7 @@ textarea:focus-visible {
|
|
|
898
908
|
|
|
899
909
|
.main {
|
|
900
910
|
min-height: 0;
|
|
901
|
-
padding: 14px;
|
|
911
|
+
padding: 8px 14px;
|
|
902
912
|
display: grid;
|
|
903
913
|
grid-template-rows: auto minmax(0, 1fr) auto;
|
|
904
914
|
gap: 0;
|
|
@@ -914,8 +924,7 @@ textarea:focus-visible {
|
|
|
914
924
|
justify-content: space-between;
|
|
915
925
|
align-items: flex-start;
|
|
916
926
|
gap: 12px;
|
|
917
|
-
padding:
|
|
918
|
-
border-bottom: 1px solid var(--line-medium);
|
|
927
|
+
padding: 0 8px 4px;
|
|
919
928
|
}
|
|
920
929
|
|
|
921
930
|
.header-main {
|
|
@@ -995,7 +1004,7 @@ body.workspace-switcher-open .workspace-switcher-panel {
|
|
|
995
1004
|
.workspace-shell {
|
|
996
1005
|
min-height: 0;
|
|
997
1006
|
height: 100%;
|
|
998
|
-
padding-top:
|
|
1007
|
+
padding-top: 6px;
|
|
999
1008
|
display: block;
|
|
1000
1009
|
overflow: hidden;
|
|
1001
1010
|
}
|
|
@@ -1871,8 +1880,8 @@ details.trace-card > .trace-card-summary {
|
|
|
1871
1880
|
}
|
|
1872
1881
|
|
|
1873
1882
|
.composer {
|
|
1874
|
-
margin-top:
|
|
1875
|
-
padding:
|
|
1883
|
+
margin-top: 4px;
|
|
1884
|
+
padding: 4px 8px 0;
|
|
1876
1885
|
border-radius: 14px;
|
|
1877
1886
|
background: linear-gradient(180deg, rgba(255, 255, 255, 0.32) 0%, rgba(255, 249, 240, 0.78) 100%);
|
|
1878
1887
|
}
|
|
@@ -2116,7 +2125,6 @@ body.composer-options-open .composer-options-panel {
|
|
|
2116
2125
|
|
|
2117
2126
|
.header {
|
|
2118
2127
|
padding: 4px 12px;
|
|
2119
|
-
border-bottom: none;
|
|
2120
2128
|
}
|
|
2121
2129
|
|
|
2122
2130
|
.workbench-tabs {
|
|
@@ -18,10 +18,6 @@
|
|
|
18
18
|
<div class="brand-wrap">
|
|
19
19
|
<div class="brand">MANYOYO Web</div>
|
|
20
20
|
</div>
|
|
21
|
-
<div class="sidebar-actions">
|
|
22
|
-
<button type="button" id="openConfigBtn" class="secondary">配置</button>
|
|
23
|
-
<button type="button" id="openCreateBtn">新建</button>
|
|
24
|
-
</div>
|
|
25
21
|
<button
|
|
26
22
|
type="button"
|
|
27
23
|
id="mobileSidebarClose"
|
|
@@ -29,11 +25,17 @@
|
|
|
29
25
|
aria-label="关闭会话列表"
|
|
30
26
|
>关闭</button>
|
|
31
27
|
</div>
|
|
28
|
+
<div class="sidebar-create">
|
|
29
|
+
<button type="button" id="openCreateBtn">新建容器</button>
|
|
30
|
+
</div>
|
|
32
31
|
<div class="session-head">
|
|
33
32
|
<span>工作台</span>
|
|
34
33
|
<span id="sessionCount">0 个</span>
|
|
35
34
|
</div>
|
|
36
35
|
<div id="sessionList" role="tree" aria-label="会话树"></div>
|
|
36
|
+
<div class="sidebar-bottom">
|
|
37
|
+
<button type="button" id="openConfigBtn" class="secondary">系统设置</button>
|
|
38
|
+
</div>
|
|
37
39
|
</aside>
|
|
38
40
|
<main class="main">
|
|
39
41
|
<header class="header">
|
package/lib/web/frontend/app.js
CHANGED
|
@@ -1414,6 +1414,9 @@
|
|
|
1414
1414
|
state.agentTemplateModalOpen = true;
|
|
1415
1415
|
fillAgentTemplateForm(detail);
|
|
1416
1416
|
syncUi();
|
|
1417
|
+
if (isMobileLayout()) {
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1417
1420
|
if (isActiveAgentOverrideEditable() && agentCliSelect) {
|
|
1418
1421
|
agentCliSelect.focus();
|
|
1419
1422
|
} else if (containerCliSelect) {
|
|
@@ -1861,7 +1864,9 @@
|
|
|
1861
1864
|
state.terminal.connected = true;
|
|
1862
1865
|
state.terminal.status = '已连接';
|
|
1863
1866
|
if (state.terminal.term) {
|
|
1864
|
-
|
|
1867
|
+
if (!isMobileLayout()) {
|
|
1868
|
+
state.terminal.term.focus();
|
|
1869
|
+
}
|
|
1865
1870
|
scheduleTerminalFit(true);
|
|
1866
1871
|
}
|
|
1867
1872
|
syncUi();
|
|
@@ -1999,7 +2004,7 @@
|
|
|
1999
2004
|
if (!state.terminal.connected && !state.terminal.connecting && !isActiveSessionHistoryOnly()) {
|
|
2000
2005
|
connectTerminal();
|
|
2001
2006
|
}
|
|
2002
|
-
if (state.terminal.term) {
|
|
2007
|
+
if (state.terminal.term && !isMobileLayout()) {
|
|
2003
2008
|
state.terminal.term.focus();
|
|
2004
2009
|
}
|
|
2005
2010
|
}
|
|
@@ -2246,12 +2251,15 @@
|
|
|
2246
2251
|
}
|
|
2247
2252
|
|
|
2248
2253
|
function syncUi() {
|
|
2254
|
+
const useStaticTitle = typeof window.__MANYOYO_SERVE_TITLE__ === 'string';
|
|
2249
2255
|
if (!state.active) {
|
|
2250
|
-
|
|
2256
|
+
if (!useStaticTitle) {
|
|
2257
|
+
document.title = window.ManyoyoChatBehavior.buildDocumentTitle('');
|
|
2258
|
+
}
|
|
2251
2259
|
if (isComposerMode()) {
|
|
2252
2260
|
commandInput.value = '';
|
|
2253
2261
|
}
|
|
2254
|
-
} else {
|
|
2262
|
+
} else if (!useStaticTitle) {
|
|
2255
2263
|
const activeSession = getActiveSession();
|
|
2256
2264
|
document.title = window.ManyoyoChatBehavior.buildDocumentTitle(
|
|
2257
2265
|
activeSession && activeSession.agentName ? activeSession.agentName : state.active
|
|
@@ -4479,6 +4487,11 @@
|
|
|
4479
4487
|
return;
|
|
4480
4488
|
}
|
|
4481
4489
|
|
|
4490
|
+
// 窄屏(手机端):Enter 始终换行,发送需点击发送按钮
|
|
4491
|
+
if (isMobileLayout()) {
|
|
4492
|
+
return;
|
|
4493
|
+
}
|
|
4494
|
+
|
|
4482
4495
|
// Shift+Enter / Option(Alt)+Enter: 换行
|
|
4483
4496
|
if (event.shiftKey || event.altKey) {
|
|
4484
4497
|
return;
|
|
@@ -4498,7 +4511,9 @@
|
|
|
4498
4511
|
renderMessages(state.messages, { forceFullRender: true });
|
|
4499
4512
|
closeComposerOptionsMenu();
|
|
4500
4513
|
syncUi();
|
|
4501
|
-
|
|
4514
|
+
if (!isMobileLayout()) {
|
|
4515
|
+
commandInput.focus();
|
|
4516
|
+
}
|
|
4502
4517
|
});
|
|
4503
4518
|
}
|
|
4504
4519
|
|
|
@@ -4508,7 +4523,9 @@
|
|
|
4508
4523
|
renderMessages(state.messages, { forceFullRender: true });
|
|
4509
4524
|
closeComposerOptionsMenu();
|
|
4510
4525
|
syncUi();
|
|
4511
|
-
|
|
4526
|
+
if (!isMobileLayout()) {
|
|
4527
|
+
commandInput.focus();
|
|
4528
|
+
}
|
|
4512
4529
|
});
|
|
4513
4530
|
}
|
|
4514
4531
|
|
|
@@ -4522,7 +4539,9 @@
|
|
|
4522
4539
|
viewActivityBtn.addEventListener('click', function () {
|
|
4523
4540
|
closeWorkspaceSwitcherMenu();
|
|
4524
4541
|
setActiveTab('activity');
|
|
4525
|
-
|
|
4542
|
+
if (!isMobileLayout()) {
|
|
4543
|
+
commandInput.focus();
|
|
4544
|
+
}
|
|
4526
4545
|
});
|
|
4527
4546
|
}
|
|
4528
4547
|
|
|
@@ -6,14 +6,13 @@
|
|
|
6
6
|
name="viewport"
|
|
7
7
|
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"
|
|
8
8
|
/>
|
|
9
|
-
<title>MANYOYO Web
|
|
9
|
+
<title>MANYOYO Web</title>
|
|
10
10
|
<link rel="stylesheet" href="/auth/frontend/login.css" />
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<form class="card" id="loginForm">
|
|
14
14
|
<div class="badge">MANYOYO</div>
|
|
15
15
|
<h1>Web 登录</h1>
|
|
16
|
-
<p>登录后可访问容器会话与对话管理。</p>
|
|
17
16
|
<label for="username">用户名</label>
|
|
18
17
|
<input id="username" name="username" autocomplete="username" required />
|
|
19
18
|
<label for="password">密码</label>
|
package/lib/web/server.js
CHANGED
|
@@ -3458,6 +3458,44 @@ function loadTemplate(name) {
|
|
|
3458
3458
|
return fs.readFileSync(filePath, 'utf-8');
|
|
3459
3459
|
}
|
|
3460
3460
|
|
|
3461
|
+
function escapeHtmlText(value) {
|
|
3462
|
+
return String(value == null ? '' : value)
|
|
3463
|
+
.replace(/&/g, '&')
|
|
3464
|
+
.replace(/</g, '<')
|
|
3465
|
+
.replace(/>/g, '>');
|
|
3466
|
+
}
|
|
3467
|
+
|
|
3468
|
+
// JSON.stringify 不转义 "</script>",且作为 String.replace 的第二个参数时 $&/$'/$` 等会被
|
|
3469
|
+
// 当成特殊替换模式重新解释;用替换函数 + 转义 "<" 双重防护,避免 serveTitle 里的内容
|
|
3470
|
+
// 提前闭合 <script> 标签或拼接进原始 HTML。
|
|
3471
|
+
function jsonForInlineScript(value) {
|
|
3472
|
+
return JSON.stringify(value).replace(/</g, '\\u003c');
|
|
3473
|
+
}
|
|
3474
|
+
|
|
3475
|
+
function applyServeTitle(html, ctx) {
|
|
3476
|
+
if (typeof ctx.serveTitle !== 'string') {
|
|
3477
|
+
return html;
|
|
3478
|
+
}
|
|
3479
|
+
// serveTitle 已配置(含空字符串):固定 <title>,覆盖模板默认的 MANYOYO Web
|
|
3480
|
+
return html.replace('<title>MANYOYO Web</title>', () => `<title>${escapeHtmlText(ctx.serveTitle)}</title>`);
|
|
3481
|
+
}
|
|
3482
|
+
|
|
3483
|
+
function renderIndexHtml(ctx) {
|
|
3484
|
+
let html = applyServeTitle(loadTemplate('app.html'), ctx);
|
|
3485
|
+
if (typeof ctx.serveTitle === 'string') {
|
|
3486
|
+
// 告知前端跳过按会话动态改写 document.title
|
|
3487
|
+
html = html.replace(
|
|
3488
|
+
'</head>',
|
|
3489
|
+
() => ` <script>window.__MANYOYO_SERVE_TITLE__ = ${jsonForInlineScript(ctx.serveTitle)};</script>\n</head>`
|
|
3490
|
+
);
|
|
3491
|
+
}
|
|
3492
|
+
return html;
|
|
3493
|
+
}
|
|
3494
|
+
|
|
3495
|
+
function renderLoginHtml(ctx) {
|
|
3496
|
+
return applyServeTitle(loadTemplate('login.html'), ctx);
|
|
3497
|
+
}
|
|
3498
|
+
|
|
3461
3499
|
function toPositiveInt(value, fallback) {
|
|
3462
3500
|
const parsed = Number.parseInt(value, 10);
|
|
3463
3501
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
@@ -3656,7 +3694,7 @@ async function handleWebAuthRoutes(req, res, pathname, ctx, state) {
|
|
|
3656
3694
|
}
|
|
3657
3695
|
|
|
3658
3696
|
if (req.method === 'GET' && pathname === '/auth/login') {
|
|
3659
|
-
sendHtml(res, 200,
|
|
3697
|
+
sendHtml(res, 200, renderLoginHtml(ctx));
|
|
3660
3698
|
return true;
|
|
3661
3699
|
}
|
|
3662
3700
|
|
|
@@ -3712,7 +3750,7 @@ async function handleWebAuthRoutes(req, res, pathname, ctx, state) {
|
|
|
3712
3750
|
return false;
|
|
3713
3751
|
}
|
|
3714
3752
|
|
|
3715
|
-
function sendWebUnauthorized(res, pathname) {
|
|
3753
|
+
function sendWebUnauthorized(res, pathname, ctx) {
|
|
3716
3754
|
if (pathname.startsWith('/api/') || pathname.startsWith('/auth/')) {
|
|
3717
3755
|
sendJson(res, 401, { error: 'UNAUTHORIZED' });
|
|
3718
3756
|
return;
|
|
@@ -3724,7 +3762,7 @@ function sendWebUnauthorized(res, pathname) {
|
|
|
3724
3762
|
sendHtml(
|
|
3725
3763
|
res,
|
|
3726
3764
|
401,
|
|
3727
|
-
|
|
3765
|
+
renderLoginHtml(ctx),
|
|
3728
3766
|
{ 'Set-Cookie': getWebAuthClearCookie() }
|
|
3729
3767
|
);
|
|
3730
3768
|
}
|
|
@@ -4498,6 +4536,7 @@ async function startWebServer(options) {
|
|
|
4498
4536
|
authUser: options.authUser,
|
|
4499
4537
|
authPass: options.authPass,
|
|
4500
4538
|
authPassAuto: options.authPassAuto,
|
|
4539
|
+
serveTitle: typeof options.serveTitle === 'string' ? options.serveTitle : null,
|
|
4501
4540
|
dockerCmd: options.dockerCmd,
|
|
4502
4541
|
hostPath: options.hostPath,
|
|
4503
4542
|
containerPath: options.containerPath,
|
|
@@ -4574,12 +4613,12 @@ async function startWebServer(options) {
|
|
|
4574
4613
|
|
|
4575
4614
|
const authSession = getWebAuthSession(state, req);
|
|
4576
4615
|
if (!authSession) {
|
|
4577
|
-
sendWebUnauthorized(res, pathname);
|
|
4616
|
+
sendWebUnauthorized(res, pathname, ctx);
|
|
4578
4617
|
return;
|
|
4579
4618
|
}
|
|
4580
4619
|
|
|
4581
4620
|
if (req.method === 'GET' && pathname === '/') {
|
|
4582
|
-
sendHtml(res, 200,
|
|
4621
|
+
sendHtml(res, 200, renderIndexHtml(ctx));
|
|
4583
4622
|
return;
|
|
4584
4623
|
}
|
|
4585
4624
|
|
package/manyoyo.example.json
CHANGED
|
@@ -44,6 +44,13 @@
|
|
|
44
44
|
"serverUser": "admin",
|
|
45
45
|
"serverPass": "change-this-password",
|
|
46
46
|
|
|
47
|
+
// serve 命令专属配置(覆盖型:runs.<name>.serve > 全局配置)
|
|
48
|
+
// 不设置 serve 字段(或不设置 title 字段)时,网页 <title> 按当前会话 Agent 名动态显示
|
|
49
|
+
// 一旦设置 title,则固定显示该值(不再随会话变化),留空字符串 "" 即固定显示为空
|
|
50
|
+
// "serve": {
|
|
51
|
+
// "title": "My MANYOYO"
|
|
52
|
+
// },
|
|
53
|
+
|
|
47
54
|
// 可选插件(manyoyo playwright / manyoyo plugin playwright)
|
|
48
55
|
"plugins": {
|
|
49
56
|
"playwright": {
|