@xcanwin/manyoyo 5.1.9 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/manyoyo.js +109 -4
- package/config.example.json +17 -0
- package/lib/agent-resume.js +15 -1
- package/lib/plugin/playwright.js +132 -10
- package/lib/web/frontend/app.css +13 -0
- package/lib/web/frontend/app.html +3 -1
- package/lib/web/frontend/app.js +228 -24
- package/lib/web/server.js +128 -5
- package/package.json +1 -1
package/bin/manyoyo.js
CHANGED
|
@@ -56,8 +56,12 @@ let IMAGE_VERSION = IMAGE_VERSION_DEFAULT || `${IMAGE_VERSION_BASE}-common`;
|
|
|
56
56
|
let EXEC_COMMAND = "";
|
|
57
57
|
let EXEC_COMMAND_PREFIX = "";
|
|
58
58
|
let EXEC_COMMAND_SUFFIX = "";
|
|
59
|
+
let FIRST_EXEC_COMMAND = "";
|
|
60
|
+
let FIRST_EXEC_COMMAND_PREFIX = "";
|
|
61
|
+
let FIRST_EXEC_COMMAND_SUFFIX = "";
|
|
59
62
|
let IMAGE_BUILD_ARGS = [];
|
|
60
63
|
let CONTAINER_ENVS = [];
|
|
64
|
+
let FIRST_CONTAINER_ENVS = [];
|
|
61
65
|
let CONTAINER_VOLUMES = [];
|
|
62
66
|
let CONTAINER_PORTS = [];
|
|
63
67
|
const MANYOYO_NAME = detectCommandName();
|
|
@@ -236,6 +240,7 @@ function sanitizeSensitiveData(obj) {
|
|
|
236
240
|
* @property {string} [imageVersion] - 镜像版本
|
|
237
241
|
* @property {Object.<string, string|number|boolean>} [env] - 环境变量映射
|
|
238
242
|
* @property {string[]} [envFile] - 环境文件数组
|
|
243
|
+
* @property {{shellPrefix?:string,shell?:string,shellSuffix?:string,env?:Object.<string,string|number|boolean>,envFile?:string[]}} [first] - 仅首次创建容器执行的一次性命令配置
|
|
239
244
|
* @property {string[]} [volumes] - 挂载卷数组
|
|
240
245
|
* @property {Object.<string, Object>} [plugins] - 可选插件配置映射(如 plugins.playwright)
|
|
241
246
|
* @property {Object.<string, Object>} [runs] - 运行配置映射(-r <name>)
|
|
@@ -436,12 +441,27 @@ function normalizeCliEnvMap(envList) {
|
|
|
436
441
|
return envMap;
|
|
437
442
|
}
|
|
438
443
|
|
|
439
|
-
function
|
|
444
|
+
function normalizeFirstConfig(firstConfig, sourceLabel) {
|
|
445
|
+
if (firstConfig === undefined || firstConfig === null) {
|
|
446
|
+
return {};
|
|
447
|
+
}
|
|
448
|
+
if (typeof firstConfig !== 'object' || Array.isArray(firstConfig)) {
|
|
449
|
+
console.error(`${RED}⚠️ 错误: ${sourceLabel} 的 first 必须是对象(map),例如 {"shell":"init.sh"}${NC}`);
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
452
|
+
return firstConfig;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function addEnvTo(targetEnvs, env) {
|
|
440
456
|
const parsed = parseEnvEntry(env);
|
|
441
|
-
|
|
457
|
+
targetEnvs.push("--env", `${parsed.key}=${parsed.value}`);
|
|
442
458
|
}
|
|
443
459
|
|
|
444
|
-
function
|
|
460
|
+
function addEnv(env) {
|
|
461
|
+
addEnvTo(CONTAINER_ENVS, env);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function addEnvFileTo(targetEnvs, envFile) {
|
|
445
465
|
const filePath = String(envFile || '').trim();
|
|
446
466
|
if (!path.isAbsolute(filePath)) {
|
|
447
467
|
console.error(`${RED}⚠️ 错误: --env-file 仅支持绝对路径: ${envFile}${NC}`);
|
|
@@ -472,7 +492,7 @@ function addEnvFile(envFile) {
|
|
|
472
492
|
}
|
|
473
493
|
|
|
474
494
|
if (key) {
|
|
475
|
-
|
|
495
|
+
targetEnvs.push("--env", `${key}=${value}`);
|
|
476
496
|
}
|
|
477
497
|
}
|
|
478
498
|
}
|
|
@@ -482,6 +502,10 @@ function addEnvFile(envFile) {
|
|
|
482
502
|
return {};
|
|
483
503
|
}
|
|
484
504
|
|
|
505
|
+
function addEnvFile(envFile) {
|
|
506
|
+
return addEnvFileTo(CONTAINER_ENVS, envFile);
|
|
507
|
+
}
|
|
508
|
+
|
|
485
509
|
function addVolume(volume) {
|
|
486
510
|
CONTAINER_VOLUMES.push("--volume", volume);
|
|
487
511
|
}
|
|
@@ -1128,6 +1152,8 @@ async function setupCommander() {
|
|
|
1128
1152
|
|
|
1129
1153
|
// Load run config if specified
|
|
1130
1154
|
const runConfig = options.run ? loadRunConfig(options.run, config) : {};
|
|
1155
|
+
const globalFirstConfig = normalizeFirstConfig(config.first, '全局配置');
|
|
1156
|
+
const runFirstConfig = normalizeFirstConfig(runConfig.first, '运行配置');
|
|
1131
1157
|
|
|
1132
1158
|
// Merge configs: command line > run config > global config > defaults
|
|
1133
1159
|
// Override mode (scalar values): use first defined value
|
|
@@ -1158,6 +1184,18 @@ async function setupCommander() {
|
|
|
1158
1184
|
if (mergedShellSuffix) {
|
|
1159
1185
|
EXEC_COMMAND_SUFFIX = normalizeCommandSuffix(mergedShellSuffix);
|
|
1160
1186
|
}
|
|
1187
|
+
const mergedFirstShellPrefix = pickConfigValue(runFirstConfig.shellPrefix, globalFirstConfig.shellPrefix);
|
|
1188
|
+
if (mergedFirstShellPrefix) {
|
|
1189
|
+
FIRST_EXEC_COMMAND_PREFIX = `${mergedFirstShellPrefix} `;
|
|
1190
|
+
}
|
|
1191
|
+
const mergedFirstShell = pickConfigValue(runFirstConfig.shell, globalFirstConfig.shell);
|
|
1192
|
+
if (mergedFirstShell) {
|
|
1193
|
+
FIRST_EXEC_COMMAND = mergedFirstShell;
|
|
1194
|
+
}
|
|
1195
|
+
const mergedFirstShellSuffix = pickConfigValue(runFirstConfig.shellSuffix, globalFirstConfig.shellSuffix);
|
|
1196
|
+
if (mergedFirstShellSuffix) {
|
|
1197
|
+
FIRST_EXEC_COMMAND_SUFFIX = normalizeCommandSuffix(mergedFirstShellSuffix);
|
|
1198
|
+
}
|
|
1161
1199
|
|
|
1162
1200
|
// Basic name validation to reduce injection risk
|
|
1163
1201
|
validateName('containerName', CONTAINER_NAME, SAFE_CONTAINER_NAME_PATTERN);
|
|
@@ -1181,6 +1219,18 @@ async function setupCommander() {
|
|
|
1181
1219
|
};
|
|
1182
1220
|
Object.entries(envMap).forEach(([key, value]) => addEnv(`${key}=${value}`));
|
|
1183
1221
|
|
|
1222
|
+
const firstEnvFileList = [
|
|
1223
|
+
...toArray(globalFirstConfig.envFile),
|
|
1224
|
+
...toArray(runFirstConfig.envFile)
|
|
1225
|
+
].filter(Boolean);
|
|
1226
|
+
firstEnvFileList.forEach(ef => addEnvFileTo(FIRST_CONTAINER_ENVS, ef));
|
|
1227
|
+
|
|
1228
|
+
const firstEnvMap = {
|
|
1229
|
+
...normalizeJsonEnvMap(globalFirstConfig.env, '全局配置 first'),
|
|
1230
|
+
...normalizeJsonEnvMap(runFirstConfig.env, '运行配置 first')
|
|
1231
|
+
};
|
|
1232
|
+
Object.entries(firstEnvMap).forEach(([key, value]) => addEnvTo(FIRST_CONTAINER_ENVS, `${key}=${value}`));
|
|
1233
|
+
|
|
1184
1234
|
const volumeList = mergeArrayConfig(config.volumes, runConfig.volumes, options.volume);
|
|
1185
1235
|
volumeList.forEach(v => addVolume(v));
|
|
1186
1236
|
|
|
@@ -1267,6 +1317,18 @@ async function setupCommander() {
|
|
|
1267
1317
|
prefix: EXEC_COMMAND_PREFIX,
|
|
1268
1318
|
shell: EXEC_COMMAND,
|
|
1269
1319
|
suffix: EXEC_COMMAND_SUFFIX
|
|
1320
|
+
},
|
|
1321
|
+
first: {
|
|
1322
|
+
envFile: firstEnvFileList,
|
|
1323
|
+
env: firstEnvMap,
|
|
1324
|
+
shellPrefix: FIRST_EXEC_COMMAND_PREFIX.trim(),
|
|
1325
|
+
shell: FIRST_EXEC_COMMAND || "",
|
|
1326
|
+
shellSuffix: FIRST_EXEC_COMMAND_SUFFIX || "",
|
|
1327
|
+
exec: {
|
|
1328
|
+
prefix: FIRST_EXEC_COMMAND_PREFIX,
|
|
1329
|
+
shell: FIRST_EXEC_COMMAND,
|
|
1330
|
+
suffix: FIRST_EXEC_COMMAND_SUFFIX
|
|
1331
|
+
}
|
|
1270
1332
|
}
|
|
1271
1333
|
};
|
|
1272
1334
|
// 敏感信息脱敏
|
|
@@ -1300,8 +1362,12 @@ function createRuntimeContext(modeState = {}) {
|
|
|
1300
1362
|
execCommand: EXEC_COMMAND,
|
|
1301
1363
|
execCommandPrefix: EXEC_COMMAND_PREFIX,
|
|
1302
1364
|
execCommandSuffix: EXEC_COMMAND_SUFFIX,
|
|
1365
|
+
firstExecCommand: FIRST_EXEC_COMMAND,
|
|
1366
|
+
firstExecCommandPrefix: FIRST_EXEC_COMMAND_PREFIX,
|
|
1367
|
+
firstExecCommandSuffix: FIRST_EXEC_COMMAND_SUFFIX,
|
|
1303
1368
|
contModeArgs: CONT_MODE_ARGS,
|
|
1304
1369
|
containerEnvs: CONTAINER_ENVS,
|
|
1370
|
+
firstContainerEnvs: FIRST_CONTAINER_ENVS,
|
|
1305
1371
|
containerVolumes: CONTAINER_VOLUMES,
|
|
1306
1372
|
containerPorts: CONTAINER_PORTS,
|
|
1307
1373
|
quiet: QUIET,
|
|
@@ -1379,6 +1445,42 @@ function joinExecCommand(prefix, command, suffix) {
|
|
|
1379
1445
|
return `${prefix || ''}${command || ''}${suffix || ''}`;
|
|
1380
1446
|
}
|
|
1381
1447
|
|
|
1448
|
+
function executeFirstCommand(runtime) {
|
|
1449
|
+
if (!runtime.firstExecCommand || !String(runtime.firstExecCommand).trim()) {
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
const firstCommand = joinExecCommand(
|
|
1454
|
+
runtime.firstExecCommandPrefix,
|
|
1455
|
+
runtime.firstExecCommand,
|
|
1456
|
+
runtime.firstExecCommandSuffix
|
|
1457
|
+
);
|
|
1458
|
+
|
|
1459
|
+
if (!(runtime.quiet.cmd || runtime.quiet.full)) {
|
|
1460
|
+
console.log(`${BLUE}----------------------------------------${NC}`);
|
|
1461
|
+
console.log(`⚙️ 首次预执行命令: ${YELLOW}${firstCommand}${NC}`);
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
const firstExecArgs = [
|
|
1465
|
+
'exec',
|
|
1466
|
+
...(runtime.firstContainerEnvs || []),
|
|
1467
|
+
runtime.containerName,
|
|
1468
|
+
'/bin/bash',
|
|
1469
|
+
'-c',
|
|
1470
|
+
firstCommand
|
|
1471
|
+
];
|
|
1472
|
+
const firstExecResult = spawnSync(`${DOCKER_CMD}`, firstExecArgs, { stdio: 'inherit' });
|
|
1473
|
+
if (firstExecResult.error) {
|
|
1474
|
+
throw firstExecResult.error;
|
|
1475
|
+
}
|
|
1476
|
+
if (typeof firstExecResult.status === 'number' && firstExecResult.status !== 0) {
|
|
1477
|
+
throw new Error(`首次预执行命令失败,退出码: ${firstExecResult.status}`);
|
|
1478
|
+
}
|
|
1479
|
+
if (firstExecResult.signal) {
|
|
1480
|
+
throw new Error(`首次预执行命令被信号终止: ${firstExecResult.signal}`);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1382
1484
|
/**
|
|
1383
1485
|
* 创建新容器
|
|
1384
1486
|
* @returns {Promise<string>} 默认命令
|
|
@@ -1412,6 +1514,9 @@ async function createNewContainer(runtime) {
|
|
|
1412
1514
|
// Wait for container to be ready
|
|
1413
1515
|
await waitForContainerReady(runtime.containerName);
|
|
1414
1516
|
|
|
1517
|
+
// Run one-time bootstrap command for newly created containers only.
|
|
1518
|
+
executeFirstCommand(runtime);
|
|
1519
|
+
|
|
1415
1520
|
return defaultCommand;
|
|
1416
1521
|
}
|
|
1417
1522
|
|
package/config.example.json
CHANGED
|
@@ -11,6 +11,15 @@
|
|
|
11
11
|
"shellPrefix": "",
|
|
12
12
|
"shell": "",
|
|
13
13
|
"shellSuffix": "",
|
|
14
|
+
"agentPromptCommand": "",
|
|
15
|
+
// 仅首次创建容器时执行一次(创建后、常规 shell 前)
|
|
16
|
+
"first": {
|
|
17
|
+
"shellPrefix": "",
|
|
18
|
+
"shell": "",
|
|
19
|
+
"shellSuffix": "",
|
|
20
|
+
"env": {},
|
|
21
|
+
"envFile": []
|
|
22
|
+
},
|
|
14
23
|
"yolo": "",
|
|
15
24
|
"serverUser": "admin",
|
|
16
25
|
"serverPass": "change-this-password",
|
|
@@ -38,6 +47,10 @@
|
|
|
38
47
|
"vncPasswordEnvKey": "VNC_PASSWORD",
|
|
39
48
|
// playwright ext-download 的 CRX prodversion 参数
|
|
40
49
|
"extensionProdversion": "132.0.0.0",
|
|
50
|
+
// 注入 navigator.platform(默认与内置 UA 对齐为 MacIntel)
|
|
51
|
+
"navigatorPlatform": "MacIntel",
|
|
52
|
+
// 是否禁用 WebRTC(默认 false)
|
|
53
|
+
"disableWebRTC": false,
|
|
41
54
|
"ports": {
|
|
42
55
|
"contHeadless": 8931,
|
|
43
56
|
"contHeaded": 8932,
|
|
@@ -54,6 +67,10 @@
|
|
|
54
67
|
"containerName": "my-claude-{now}",
|
|
55
68
|
"yolo": "c",
|
|
56
69
|
"shell": "claude",
|
|
70
|
+
"agentPromptCommand": "claude -p {prompt}",
|
|
71
|
+
"first": {
|
|
72
|
+
"shell": "echo first-init"
|
|
73
|
+
},
|
|
57
74
|
"env": {
|
|
58
75
|
"ANTHROPIC_MODEL": "claude-sonnet-4-5"
|
|
59
76
|
},
|
package/lib/agent-resume.js
CHANGED
|
@@ -9,6 +9,13 @@ const AGENT_RESUME_ARG_MAP = {
|
|
|
9
9
|
opencode: '-c'
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
+
const AGENT_PROMPT_TEMPLATE_MAP = {
|
|
13
|
+
claude: 'claude -p {prompt}',
|
|
14
|
+
gemini: 'gemini -p {prompt}',
|
|
15
|
+
codex: 'codex exec {prompt}',
|
|
16
|
+
opencode: 'opencode run {prompt}'
|
|
17
|
+
};
|
|
18
|
+
|
|
12
19
|
function stripLeadingAssignments(commandText) {
|
|
13
20
|
let rest = String(commandText || '').trim();
|
|
14
21
|
const assignmentPattern = /^(?:[A-Za-z_][A-Za-z0-9_]*=)(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s]+)(?:\s+|$)/;
|
|
@@ -67,6 +74,11 @@ function resolveAgentResumeArg(commandText) {
|
|
|
67
74
|
return AGENT_RESUME_ARG_MAP[program] || '';
|
|
68
75
|
}
|
|
69
76
|
|
|
77
|
+
function resolveAgentPromptCommandTemplate(commandText) {
|
|
78
|
+
const program = resolveAgentProgram(commandText);
|
|
79
|
+
return AGENT_PROMPT_TEMPLATE_MAP[program] || '';
|
|
80
|
+
}
|
|
81
|
+
|
|
70
82
|
function buildAgentResumeCommand(commandText) {
|
|
71
83
|
const baseCommand = String(commandText || '').trim();
|
|
72
84
|
if (!baseCommand) {
|
|
@@ -80,6 +92,8 @@ function buildAgentResumeCommand(commandText) {
|
|
|
80
92
|
}
|
|
81
93
|
|
|
82
94
|
module.exports = {
|
|
95
|
+
resolveAgentProgram,
|
|
83
96
|
resolveAgentResumeArg,
|
|
84
|
-
buildAgentResumeCommand
|
|
97
|
+
buildAgentResumeCommand,
|
|
98
|
+
resolveAgentPromptCommandTemplate
|
|
85
99
|
};
|
package/lib/plugin/playwright.js
CHANGED
|
@@ -65,6 +65,21 @@ const DEFAULT_FINGERPRINT_PROFILE = {
|
|
|
65
65
|
width: 1366,
|
|
66
66
|
height: 768
|
|
67
67
|
};
|
|
68
|
+
const DISABLE_WEBRTC_LAUNCH_ARGS = ['--disable-webrtc'];
|
|
69
|
+
|
|
70
|
+
function platformFromUserAgent(userAgent) {
|
|
71
|
+
const ua = String(userAgent || '').toLowerCase();
|
|
72
|
+
if (ua.includes('macintosh') || ua.includes('mac os x')) {
|
|
73
|
+
return 'MacIntel';
|
|
74
|
+
}
|
|
75
|
+
if (ua.includes('windows')) {
|
|
76
|
+
return 'Win32';
|
|
77
|
+
}
|
|
78
|
+
if (ua.includes('android') || ua.includes('linux')) {
|
|
79
|
+
return 'Linux x86_64';
|
|
80
|
+
}
|
|
81
|
+
return 'MacIntel';
|
|
82
|
+
}
|
|
68
83
|
|
|
69
84
|
function sleep(ms) {
|
|
70
85
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
@@ -94,6 +109,22 @@ function asStringArray(value, fallback) {
|
|
|
94
109
|
.filter(Boolean);
|
|
95
110
|
}
|
|
96
111
|
|
|
112
|
+
function asBoolean(value, fallback = false) {
|
|
113
|
+
if (typeof value === 'boolean') {
|
|
114
|
+
return value;
|
|
115
|
+
}
|
|
116
|
+
if (typeof value === 'string') {
|
|
117
|
+
const normalized = value.trim().toLowerCase();
|
|
118
|
+
if (normalized === 'true') {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
if (normalized === 'false') {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return fallback;
|
|
126
|
+
}
|
|
127
|
+
|
|
97
128
|
function isHostPermission(value) {
|
|
98
129
|
if (value === '<all_urls>') {
|
|
99
130
|
return true;
|
|
@@ -242,6 +273,8 @@ class PlaywrightPlugin {
|
|
|
242
273
|
configDir: path.join(pluginRootDir, 'config'),
|
|
243
274
|
runDir: path.join(pluginRootDir, 'run'),
|
|
244
275
|
extensionProdversion: '132.0.0.0',
|
|
276
|
+
navigatorPlatform: platformFromUserAgent(DEFAULT_FINGERPRINT_PROFILE.userAgent),
|
|
277
|
+
disableWebRTC: false,
|
|
245
278
|
composeDir: path.join(__dirname, 'playwright-assets'),
|
|
246
279
|
ports: {
|
|
247
280
|
contHeadless: 8931,
|
|
@@ -273,6 +306,8 @@ class PlaywrightPlugin {
|
|
|
273
306
|
asStringArray(this.globalConfig.enabledScenes, [...defaultConfig.enabledScenes])
|
|
274
307
|
);
|
|
275
308
|
merged.containerRuntime = this.resolveContainerRuntime(merged.containerRuntime);
|
|
309
|
+
merged.navigatorPlatform = String(merged.navigatorPlatform || defaultConfig.navigatorPlatform).trim() || defaultConfig.navigatorPlatform;
|
|
310
|
+
merged.disableWebRTC = asBoolean(merged.disableWebRTC, defaultConfig.disableWebRTC);
|
|
276
311
|
|
|
277
312
|
if (merged.enabledScenes.length === 0) {
|
|
278
313
|
throw new Error('playwright.enabledScenes 不能为空');
|
|
@@ -377,6 +412,72 @@ class PlaywrightPlugin {
|
|
|
377
412
|
return !fs.existsSync(this.sceneConfigPath(sceneName));
|
|
378
413
|
}
|
|
379
414
|
|
|
415
|
+
sceneInitScriptPath(sceneName) {
|
|
416
|
+
const configFile = path.basename(this.sceneConfigPath(sceneName), '.json');
|
|
417
|
+
return path.join(this.config.configDir, `${configFile}.init.js`);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
legacySceneInitScriptPath(sceneName) {
|
|
421
|
+
return path.join(this.config.configDir, `${sceneName}.init.js`);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
buildInitScriptContent() {
|
|
425
|
+
const lines = [
|
|
426
|
+
"'use strict';",
|
|
427
|
+
'(function () {',
|
|
428
|
+
` const platformValue = ${JSON.stringify(this.config.navigatorPlatform)};`,
|
|
429
|
+
' try {',
|
|
430
|
+
' const navProto = Object.getPrototypeOf(navigator);',
|
|
431
|
+
" Object.defineProperty(navProto, 'platform', {",
|
|
432
|
+
' configurable: true,',
|
|
433
|
+
' get: () => platformValue',
|
|
434
|
+
' });',
|
|
435
|
+
' } catch (_) {}'
|
|
436
|
+
];
|
|
437
|
+
|
|
438
|
+
if (this.config.disableWebRTC) {
|
|
439
|
+
lines.push(
|
|
440
|
+
' try {',
|
|
441
|
+
' const scope = globalThis;',
|
|
442
|
+
" const blocked = ['RTCPeerConnection', 'webkitRTCPeerConnection', 'RTCIceCandidate', 'RTCRtpSender', 'RTCRtpReceiver', 'RTCRtpTransceiver', 'RTCDataChannel'];",
|
|
443
|
+
' for (const name of blocked) {',
|
|
444
|
+
" Object.defineProperty(scope, name, { configurable: true, writable: true, value: undefined });",
|
|
445
|
+
' }',
|
|
446
|
+
' if (navigator.mediaDevices) {',
|
|
447
|
+
' const errorFactory = () => {',
|
|
448
|
+
' try {',
|
|
449
|
+
" return new DOMException('WebRTC is disabled', 'NotAllowedError');",
|
|
450
|
+
' } catch (_) {',
|
|
451
|
+
" const error = new Error('WebRTC is disabled');",
|
|
452
|
+
" error.name = 'NotAllowedError';",
|
|
453
|
+
' return error;',
|
|
454
|
+
' }',
|
|
455
|
+
' };',
|
|
456
|
+
" Object.defineProperty(navigator.mediaDevices, 'getUserMedia', {",
|
|
457
|
+
' configurable: true,',
|
|
458
|
+
' writable: true,',
|
|
459
|
+
' value: async () => { throw errorFactory(); }',
|
|
460
|
+
' });',
|
|
461
|
+
' }',
|
|
462
|
+
' } catch (_) {}'
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
lines.push('})();', '');
|
|
467
|
+
return lines.join('\n');
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
ensureSceneInitScript(sceneName) {
|
|
471
|
+
const filePath = this.sceneInitScriptPath(sceneName);
|
|
472
|
+
const content = this.buildInitScriptContent();
|
|
473
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
474
|
+
const legacyFilePath = this.legacySceneInitScriptPath(sceneName);
|
|
475
|
+
if (legacyFilePath !== filePath) {
|
|
476
|
+
fs.rmSync(legacyFilePath, { force: true });
|
|
477
|
+
}
|
|
478
|
+
return filePath;
|
|
479
|
+
}
|
|
480
|
+
|
|
380
481
|
defaultBrowserName(sceneName) {
|
|
381
482
|
const cfg = this.buildSceneConfig(sceneName);
|
|
382
483
|
const browserName = cfg && cfg.browser && cfg.browser.browserName;
|
|
@@ -553,6 +654,7 @@ class PlaywrightPlugin {
|
|
|
553
654
|
const def = SCENE_DEFS[sceneName];
|
|
554
655
|
const port = this.scenePort(sceneName);
|
|
555
656
|
const extensionPaths = asStringArray(options.extensionPaths, []);
|
|
657
|
+
const initScript = asStringArray(options.initScript, []);
|
|
556
658
|
const baseLaunchArgs = [
|
|
557
659
|
`--user-agent=${DEFAULT_FINGERPRINT_PROFILE.userAgent}`,
|
|
558
660
|
`--lang=${DEFAULT_FINGERPRINT_PROFILE.locale}`,
|
|
@@ -569,6 +671,9 @@ class PlaywrightPlugin {
|
|
|
569
671
|
if (extensionPaths.length > 0) {
|
|
570
672
|
launchOptions.args.push(...this.buildExtensionLaunchArgs(extensionPaths));
|
|
571
673
|
}
|
|
674
|
+
if (this.config.disableWebRTC) {
|
|
675
|
+
launchOptions.args.push(...DISABLE_WEBRTC_LAUNCH_ARGS);
|
|
676
|
+
}
|
|
572
677
|
|
|
573
678
|
return {
|
|
574
679
|
server: {
|
|
@@ -584,6 +689,7 @@ class PlaywrightPlugin {
|
|
|
584
689
|
browser: {
|
|
585
690
|
chromiumSandbox: true,
|
|
586
691
|
browserName: 'chromium',
|
|
692
|
+
initScript,
|
|
587
693
|
launchOptions,
|
|
588
694
|
contextOptions: {
|
|
589
695
|
userAgent: DEFAULT_FINGERPRINT_PROFILE.userAgent,
|
|
@@ -607,7 +713,13 @@ class PlaywrightPlugin {
|
|
|
607
713
|
|
|
608
714
|
ensureSceneConfig(sceneName, options = {}) {
|
|
609
715
|
fs.mkdirSync(this.config.configDir, { recursive: true });
|
|
610
|
-
const
|
|
716
|
+
const initScriptPath = this.ensureSceneInitScript(sceneName);
|
|
717
|
+
const configuredInitScript = asStringArray(options.initScript, []);
|
|
718
|
+
const initScript = configuredInitScript.length > 0 ? configuredInitScript : [initScriptPath];
|
|
719
|
+
const payload = this.buildSceneConfig(sceneName, {
|
|
720
|
+
...options,
|
|
721
|
+
initScript
|
|
722
|
+
});
|
|
611
723
|
const filePath = this.sceneConfigPath(sceneName);
|
|
612
724
|
fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 4)}\n`, 'utf8');
|
|
613
725
|
return filePath;
|
|
@@ -717,21 +829,31 @@ class PlaywrightPlugin {
|
|
|
717
829
|
}
|
|
718
830
|
|
|
719
831
|
const incomingExtensionPaths = asStringArray(options.extensionPaths, []);
|
|
720
|
-
|
|
832
|
+
const hostInitScriptPath = this.sceneInitScriptPath(sceneName);
|
|
833
|
+
const containerInitScriptPath = path.posix.join('/app/config', path.basename(hostInitScriptPath));
|
|
834
|
+
let configOptions = {
|
|
835
|
+
...options,
|
|
836
|
+
extensionPaths: incomingExtensionPaths,
|
|
837
|
+
initScript: [containerInitScriptPath]
|
|
838
|
+
};
|
|
721
839
|
const composeFiles = [this.containerComposePath(sceneName)];
|
|
840
|
+
const volumeMounts = [`${hostInitScriptPath}:${containerInitScriptPath}:ro`];
|
|
722
841
|
|
|
723
842
|
if (incomingExtensionPaths.length > 0) {
|
|
724
843
|
const mapped = this.buildContainerExtensionMounts(incomingExtensionPaths);
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
this.ensureContainerComposeOverride(sceneName, []);
|
|
844
|
+
volumeMounts.push(...mapped.volumeMounts);
|
|
845
|
+
configOptions = {
|
|
846
|
+
...options,
|
|
847
|
+
extensionPaths: mapped.containerPaths,
|
|
848
|
+
initScript: [containerInitScriptPath]
|
|
849
|
+
};
|
|
732
850
|
}
|
|
733
|
-
|
|
734
851
|
const cfgPath = this.ensureSceneConfig(sceneName, configOptions);
|
|
852
|
+
const overridePath = this.ensureContainerComposeOverride(sceneName, volumeMounts);
|
|
853
|
+
if (overridePath) {
|
|
854
|
+
composeFiles.push(overridePath);
|
|
855
|
+
}
|
|
856
|
+
|
|
735
857
|
const env = this.containerEnv(sceneName, cfgPath, { requireVncPassword: true });
|
|
736
858
|
const def = SCENE_DEFS[sceneName];
|
|
737
859
|
|
package/lib/web/frontend/app.css
CHANGED
|
@@ -641,6 +641,7 @@ textarea:focus-visible {
|
|
|
641
641
|
}
|
|
642
642
|
|
|
643
643
|
body.command-mode #modeCommandBtn,
|
|
644
|
+
body.agent-mode #modeAgentBtn,
|
|
644
645
|
body.terminal-mode #modeTerminalBtn {
|
|
645
646
|
color: #ffffff;
|
|
646
647
|
background: var(--accent);
|
|
@@ -722,14 +723,26 @@ body.command-mode #messages {
|
|
|
722
723
|
display: flex;
|
|
723
724
|
}
|
|
724
725
|
|
|
726
|
+
body.agent-mode #messages {
|
|
727
|
+
display: flex;
|
|
728
|
+
}
|
|
729
|
+
|
|
725
730
|
body.command-mode #terminalPanel {
|
|
726
731
|
display: none;
|
|
727
732
|
}
|
|
728
733
|
|
|
734
|
+
body.agent-mode #terminalPanel {
|
|
735
|
+
display: none;
|
|
736
|
+
}
|
|
737
|
+
|
|
729
738
|
body.command-mode .composer {
|
|
730
739
|
display: block;
|
|
731
740
|
}
|
|
732
741
|
|
|
742
|
+
body.agent-mode .composer {
|
|
743
|
+
display: block;
|
|
744
|
+
}
|
|
745
|
+
|
|
733
746
|
body.terminal-mode #messages {
|
|
734
747
|
display: none;
|
|
735
748
|
}
|
|
@@ -66,6 +66,7 @@
|
|
|
66
66
|
<section class="mode-switch" id="modeSwitch">
|
|
67
67
|
<div class="mode-switch-left">
|
|
68
68
|
<button type="button" id="modeCommandBtn" class="secondary is-active">命令模式</button>
|
|
69
|
+
<button type="button" id="modeAgentBtn" class="secondary">AGENT 模式</button>
|
|
69
70
|
<button type="button" id="modeTerminalBtn" class="secondary">交互终端</button>
|
|
70
71
|
</div>
|
|
71
72
|
<div class="mode-terminal-controls">
|
|
@@ -85,7 +86,7 @@
|
|
|
85
86
|
<button type="submit" id="sendBtn">发送</button>
|
|
86
87
|
</div>
|
|
87
88
|
<div class="composer-foot">
|
|
88
|
-
<span>Enter 发送 · Shift/Alt + Enter 换行</span>
|
|
89
|
+
<span id="composerHint">Enter 发送 · Shift/Alt + Enter 换行</span>
|
|
89
90
|
<span id="sendState" class="send-state">未选择会话</span>
|
|
90
91
|
</div>
|
|
91
92
|
</form>
|
|
@@ -140,6 +141,7 @@
|
|
|
140
141
|
<label>shell<input id="createShell" placeholder="例如 claude / codex" /></label>
|
|
141
142
|
<label>shellSuffix<input id="createShellSuffix" placeholder="例如 --dangerously-skip-permissions" /></label>
|
|
142
143
|
<label>yolo<input id="createYolo" placeholder="例如 c / cx / gm / oc" /></label>
|
|
144
|
+
<label>agentPromptCommand<input id="createAgentPromptCommand" placeholder="例如 codex exec --plain-text {prompt}" /></label>
|
|
143
145
|
</div>
|
|
144
146
|
<label class="text-block">env (KEY=VALUE,每行一项)
|
|
145
147
|
<textarea id="createEnv" placeholder="KEY=value"></textarea>
|
package/lib/web/frontend/app.js
CHANGED
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
configSaving: false,
|
|
53
53
|
createLoading: false,
|
|
54
54
|
createSubmitting: false,
|
|
55
|
+
createAgentPromptAuto: false,
|
|
55
56
|
createDefaults: null,
|
|
56
57
|
createRuns: {},
|
|
57
58
|
sessionNodeMap: new Map(),
|
|
@@ -105,6 +106,7 @@
|
|
|
105
106
|
const createShellPrefix = document.getElementById('createShellPrefix');
|
|
106
107
|
const createShell = document.getElementById('createShell');
|
|
107
108
|
const createShellSuffix = document.getElementById('createShellSuffix');
|
|
109
|
+
const createAgentPromptCommand = document.getElementById('createAgentPromptCommand');
|
|
108
110
|
const createYolo = document.getElementById('createYolo');
|
|
109
111
|
const createEnv = document.getElementById('createEnv');
|
|
110
112
|
const createEnvFile = document.getElementById('createEnvFile');
|
|
@@ -112,6 +114,7 @@
|
|
|
112
114
|
const activeTitle = document.getElementById('activeTitle');
|
|
113
115
|
const activeMeta = document.getElementById('activeMeta');
|
|
114
116
|
const modeCommandBtn = document.getElementById('modeCommandBtn');
|
|
117
|
+
const modeAgentBtn = document.getElementById('modeAgentBtn');
|
|
115
118
|
const modeTerminalBtn = document.getElementById('modeTerminalBtn');
|
|
116
119
|
const messagesNode = document.getElementById('messages');
|
|
117
120
|
const terminalPanel = document.getElementById('terminalPanel');
|
|
@@ -121,6 +124,7 @@
|
|
|
121
124
|
const terminalScreen = document.getElementById('terminalScreen');
|
|
122
125
|
const composer = document.getElementById('composer');
|
|
123
126
|
const commandInput = document.getElementById('commandInput');
|
|
127
|
+
const composerHint = document.getElementById('composerHint');
|
|
124
128
|
const sendState = document.getElementById('sendState');
|
|
125
129
|
const sendBtn = document.getElementById('sendBtn');
|
|
126
130
|
const refreshBtn = document.getElementById('refreshBtn');
|
|
@@ -133,10 +137,33 @@
|
|
|
133
137
|
const TERMINAL_MIN_ROWS = 12;
|
|
134
138
|
const TERMINAL_DEFAULT_COLS = 120;
|
|
135
139
|
const TERMINAL_DEFAULT_ROWS = 36;
|
|
140
|
+
const YOLO_COMMAND_MAP = {
|
|
141
|
+
claude: 'IS_SANDBOX=1 claude --dangerously-skip-permissions',
|
|
142
|
+
cc: 'IS_SANDBOX=1 claude --dangerously-skip-permissions',
|
|
143
|
+
c: 'IS_SANDBOX=1 claude --dangerously-skip-permissions',
|
|
144
|
+
gemini: 'gemini --yolo',
|
|
145
|
+
gm: 'gemini --yolo',
|
|
146
|
+
g: 'gemini --yolo',
|
|
147
|
+
codex: 'codex --dangerously-bypass-approvals-and-sandbox',
|
|
148
|
+
cx: 'codex --dangerously-bypass-approvals-and-sandbox',
|
|
149
|
+
opencode: 'OPENCODE_PERMISSION=\'{"*":"allow"}\' opencode',
|
|
150
|
+
oc: 'OPENCODE_PERMISSION=\'{"*":"allow"}\' opencode'
|
|
151
|
+
};
|
|
152
|
+
const AGENT_PROMPT_TEMPLATE_MAP = {
|
|
153
|
+
claude: 'claude -p {prompt}',
|
|
154
|
+
gemini: 'gemini -p {prompt}',
|
|
155
|
+
codex: 'codex exec {prompt}',
|
|
156
|
+
opencode: 'opencode run {prompt}'
|
|
157
|
+
};
|
|
136
158
|
|
|
137
|
-
function roleName(role) {
|
|
159
|
+
function roleName(role, message) {
|
|
138
160
|
if (role === 'user') return '你';
|
|
139
|
-
if (role === 'assistant')
|
|
161
|
+
if (role === 'assistant') {
|
|
162
|
+
if (message && message.mode === 'agent') {
|
|
163
|
+
return 'AGENT 回复';
|
|
164
|
+
}
|
|
165
|
+
return '容器输出';
|
|
166
|
+
}
|
|
140
167
|
return '系统';
|
|
141
168
|
}
|
|
142
169
|
|
|
@@ -242,6 +269,97 @@
|
|
|
242
269
|
return envMap;
|
|
243
270
|
}
|
|
244
271
|
|
|
272
|
+
function buildDefaultCommand(shellPrefix, shell, shellSuffix) {
|
|
273
|
+
const parts = [];
|
|
274
|
+
if (shellPrefix && String(shellPrefix).trim()) {
|
|
275
|
+
parts.push(String(shellPrefix).trim());
|
|
276
|
+
}
|
|
277
|
+
if (shell && String(shell).trim()) {
|
|
278
|
+
parts.push(String(shell).trim());
|
|
279
|
+
}
|
|
280
|
+
if (shellSuffix && String(shellSuffix).trim()) {
|
|
281
|
+
parts.push(String(shellSuffix).trim());
|
|
282
|
+
}
|
|
283
|
+
return parts.join(' ').trim();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function resolveYoloCommand(yolo) {
|
|
287
|
+
const key = String(yolo || '').trim().toLowerCase();
|
|
288
|
+
if (!key) return '';
|
|
289
|
+
return YOLO_COMMAND_MAP[key] || '';
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function stripLeadingAssignments(commandText) {
|
|
293
|
+
let rest = String(commandText || '').trim();
|
|
294
|
+
const assignmentPattern = /^(?:[A-Za-z_][A-Za-z0-9_]*=)(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s]+)(?:\s+|$)/;
|
|
295
|
+
while (rest) {
|
|
296
|
+
const matched = rest.match(assignmentPattern);
|
|
297
|
+
if (!matched) break;
|
|
298
|
+
rest = rest.slice(matched[0].length).trim();
|
|
299
|
+
}
|
|
300
|
+
return rest;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function readLeadingToken(commandText) {
|
|
304
|
+
const text = String(commandText || '').trim();
|
|
305
|
+
if (!text) return { token: '', rest: '' };
|
|
306
|
+
const tokenMatch = text.match(/^(?:"((?:\\.|[^"])*)"|'((?:\\.|[^'])*)'|([^\s]+))(?:\s+|$)/);
|
|
307
|
+
if (!tokenMatch) return { token: '', rest: '' };
|
|
308
|
+
return {
|
|
309
|
+
token: tokenMatch[1] || tokenMatch[2] || tokenMatch[3] || '',
|
|
310
|
+
rest: text.slice(tokenMatch[0].length).trim()
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function normalizeProgramName(token) {
|
|
315
|
+
const text = String(token || '').trim();
|
|
316
|
+
if (!text) return '';
|
|
317
|
+
return text.replace(/\\/g, '/').split('/').pop().toLowerCase();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function resolveAgentProgram(commandText) {
|
|
321
|
+
let rest = stripLeadingAssignments(commandText);
|
|
322
|
+
let leading = readLeadingToken(rest);
|
|
323
|
+
let program = normalizeProgramName(leading.token);
|
|
324
|
+
if (program === 'env') {
|
|
325
|
+
rest = stripLeadingAssignments(leading.rest);
|
|
326
|
+
leading = readLeadingToken(rest);
|
|
327
|
+
program = normalizeProgramName(leading.token);
|
|
328
|
+
}
|
|
329
|
+
return program;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function resolveAgentPromptTemplate(commandText) {
|
|
333
|
+
const program = resolveAgentProgram(commandText);
|
|
334
|
+
return AGENT_PROMPT_TEMPLATE_MAP[program] || '';
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function inferCreateAgentPromptCommand() {
|
|
338
|
+
let shell = (createShell.value || '').trim();
|
|
339
|
+
const yoloCommand = resolveYoloCommand(createYolo.value || '');
|
|
340
|
+
if (yoloCommand) {
|
|
341
|
+
shell = yoloCommand;
|
|
342
|
+
}
|
|
343
|
+
const fullCommand = buildDefaultCommand(
|
|
344
|
+
(createShellPrefix.value || '').trim(),
|
|
345
|
+
shell,
|
|
346
|
+
(createShellSuffix.value || '').trim()
|
|
347
|
+
);
|
|
348
|
+
return resolveAgentPromptTemplate(fullCommand);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function updateCreateAgentPromptCommandFromCommand() {
|
|
352
|
+
if (!createAgentPromptCommand) return;
|
|
353
|
+
const current = String(createAgentPromptCommand.value || '').trim();
|
|
354
|
+
const inferred = inferCreateAgentPromptCommand();
|
|
355
|
+
const canAutoReplace = state.createAgentPromptAuto || !current;
|
|
356
|
+
if (!canAutoReplace) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
createAgentPromptCommand.value = inferred;
|
|
360
|
+
state.createAgentPromptAuto = Boolean(inferred);
|
|
361
|
+
}
|
|
362
|
+
|
|
245
363
|
function fillCreateForm(defaults) {
|
|
246
364
|
const value = defaults && typeof defaults === 'object' ? defaults : {};
|
|
247
365
|
createContainerName.value = value.containerName || '';
|
|
@@ -253,10 +371,13 @@
|
|
|
253
371
|
createShellPrefix.value = value.shellPrefix || '';
|
|
254
372
|
createShell.value = value.shell || '';
|
|
255
373
|
createShellSuffix.value = value.shellSuffix || '';
|
|
374
|
+
createAgentPromptCommand.value = value.agentPromptCommand || '';
|
|
375
|
+
state.createAgentPromptAuto = false;
|
|
256
376
|
createYolo.value = value.yolo || '';
|
|
257
377
|
createEnv.value = envMapToText(value.env);
|
|
258
378
|
createEnvFile.value = Array.isArray(value.envFile) ? value.envFile.join('\n') : '';
|
|
259
379
|
createVolumes.value = Array.isArray(value.volumes) ? value.volumes.join('\n') : '';
|
|
380
|
+
updateCreateAgentPromptCommandFromCommand();
|
|
260
381
|
}
|
|
261
382
|
|
|
262
383
|
function mergeCreateDefaults(baseDefaults, runConfig) {
|
|
@@ -272,6 +393,7 @@
|
|
|
272
393
|
shellPrefix: run.shellPrefix != null ? String(run.shellPrefix) : (base.shellPrefix || ''),
|
|
273
394
|
shell: run.shell != null ? String(run.shell) : (base.shell || ''),
|
|
274
395
|
shellSuffix: run.shellSuffix != null ? String(run.shellSuffix) : (base.shellSuffix || ''),
|
|
396
|
+
agentPromptCommand: run.agentPromptCommand != null ? String(run.agentPromptCommand) : (base.agentPromptCommand || ''),
|
|
275
397
|
yolo: run.yolo != null ? String(run.yolo) : (base.yolo || ''),
|
|
276
398
|
env: {},
|
|
277
399
|
envFile: [],
|
|
@@ -358,6 +480,7 @@
|
|
|
358
480
|
shellPrefix: (createShellPrefix.value || '').trim(),
|
|
359
481
|
shell: (createShell.value || '').trim(),
|
|
360
482
|
shellSuffix: (createShellSuffix.value || '').trim(),
|
|
483
|
+
agentPromptCommand: (createAgentPromptCommand.value || '').trim(),
|
|
361
484
|
yolo: (createYolo.value || '').trim(),
|
|
362
485
|
env: textToEnvMap(createEnv.value),
|
|
363
486
|
envFile: textToLineArray(createEnvFile.value),
|
|
@@ -392,6 +515,15 @@
|
|
|
392
515
|
}) || null;
|
|
393
516
|
}
|
|
394
517
|
|
|
518
|
+
function isComposerMode() {
|
|
519
|
+
return state.mode === 'command' || state.mode === 'agent';
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function isActiveAgentEnabled() {
|
|
523
|
+
const active = getActiveSession();
|
|
524
|
+
return Boolean(active && active.agentEnabled);
|
|
525
|
+
}
|
|
526
|
+
|
|
395
527
|
function buildActiveMeta(session) {
|
|
396
528
|
if (!session) {
|
|
397
529
|
return '会话不可用';
|
|
@@ -411,7 +543,7 @@
|
|
|
411
543
|
|
|
412
544
|
lines.push({
|
|
413
545
|
className: 'msg-meta-role',
|
|
414
|
-
text: roleName(message && message.role)
|
|
546
|
+
text: roleName(message && message.role, message)
|
|
415
547
|
});
|
|
416
548
|
|
|
417
549
|
return lines;
|
|
@@ -761,7 +893,7 @@
|
|
|
761
893
|
if (!state.active) {
|
|
762
894
|
activeTitle.textContent = '未选择会话';
|
|
763
895
|
activeMeta.textContent = '请选择左侧会话';
|
|
764
|
-
if (
|
|
896
|
+
if (isComposerMode()) {
|
|
765
897
|
commandInput.value = '';
|
|
766
898
|
}
|
|
767
899
|
} else {
|
|
@@ -769,21 +901,31 @@
|
|
|
769
901
|
activeMeta.textContent = buildActiveMeta(getActiveSession());
|
|
770
902
|
}
|
|
771
903
|
|
|
772
|
-
const commandMode = state.mode
|
|
904
|
+
const commandMode = state.mode === 'command';
|
|
905
|
+
const agentMode = state.mode === 'agent';
|
|
906
|
+
const terminalMode = state.mode === 'terminal';
|
|
907
|
+
const composerMode = commandMode || agentMode;
|
|
908
|
+
const agentEnabled = isActiveAgentEnabled();
|
|
909
|
+
|
|
773
910
|
document.body.classList.toggle('command-mode', commandMode);
|
|
774
|
-
document.body.classList.toggle('
|
|
911
|
+
document.body.classList.toggle('agent-mode', agentMode);
|
|
912
|
+
document.body.classList.toggle('terminal-mode', terminalMode);
|
|
775
913
|
if (modeCommandBtn) {
|
|
776
914
|
modeCommandBtn.classList.toggle('is-active', commandMode);
|
|
777
915
|
modeCommandBtn.setAttribute('aria-pressed', commandMode ? 'true' : 'false');
|
|
778
916
|
}
|
|
917
|
+
if (modeAgentBtn) {
|
|
918
|
+
modeAgentBtn.classList.toggle('is-active', agentMode);
|
|
919
|
+
modeAgentBtn.setAttribute('aria-pressed', agentMode ? 'true' : 'false');
|
|
920
|
+
}
|
|
779
921
|
if (modeTerminalBtn) {
|
|
780
|
-
modeTerminalBtn.classList.toggle('is-active',
|
|
781
|
-
modeTerminalBtn.setAttribute('aria-pressed',
|
|
922
|
+
modeTerminalBtn.classList.toggle('is-active', terminalMode);
|
|
923
|
+
modeTerminalBtn.setAttribute('aria-pressed', terminalMode ? 'true' : 'false');
|
|
782
924
|
}
|
|
783
925
|
if (terminalPanel) {
|
|
784
|
-
terminalPanel.hidden =
|
|
926
|
+
terminalPanel.hidden = !terminalMode;
|
|
785
927
|
}
|
|
786
|
-
if (
|
|
928
|
+
if (terminalMode && state.terminal.terminalReady) {
|
|
787
929
|
scheduleTerminalFit(false);
|
|
788
930
|
}
|
|
789
931
|
|
|
@@ -791,8 +933,18 @@
|
|
|
791
933
|
refreshBtn.disabled = busy;
|
|
792
934
|
removeBtn.disabled = !state.active || busy;
|
|
793
935
|
removeAllBtn.disabled = !state.active || busy;
|
|
794
|
-
sendBtn.disabled = !
|
|
795
|
-
commandInput.disabled = !
|
|
936
|
+
sendBtn.disabled = !composerMode || !state.active || busy || (agentMode && !agentEnabled);
|
|
937
|
+
commandInput.disabled = !composerMode || !state.active || state.sending || (agentMode && !agentEnabled);
|
|
938
|
+
if (commandInput) {
|
|
939
|
+
commandInput.placeholder = agentMode
|
|
940
|
+
? '输入提示词,例如:请帮我分析当前项目结构并给出重构建议'
|
|
941
|
+
: '输入容器命令,例如: ls -la';
|
|
942
|
+
}
|
|
943
|
+
if (composerHint) {
|
|
944
|
+
composerHint.textContent = agentMode
|
|
945
|
+
? 'Enter 发送提示词 · Shift/Alt + Enter 换行'
|
|
946
|
+
: 'Enter 发送 · Shift/Alt + Enter 换行';
|
|
947
|
+
}
|
|
796
948
|
if (openCreateBtn) {
|
|
797
949
|
openCreateBtn.disabled = state.createLoading || state.createSubmitting;
|
|
798
950
|
}
|
|
@@ -835,6 +987,8 @@
|
|
|
835
987
|
|
|
836
988
|
if (!state.active) {
|
|
837
989
|
sendState.textContent = '未选择会话';
|
|
990
|
+
} else if (agentMode && !agentEnabled) {
|
|
991
|
+
sendState.textContent = '当前会话未配置 AGENT 模板';
|
|
838
992
|
} else if (state.sending) {
|
|
839
993
|
sendState.textContent = '发送中...';
|
|
840
994
|
} else if (state.loadingSessions || state.loadingMessages) {
|
|
@@ -1228,7 +1382,9 @@
|
|
|
1228
1382
|
messagesNode.innerHTML = '';
|
|
1229
1383
|
const empty = document.createElement('div');
|
|
1230
1384
|
empty.className = 'empty';
|
|
1231
|
-
empty.textContent = '
|
|
1385
|
+
empty.textContent = state.mode === 'agent'
|
|
1386
|
+
? '输入提示词后,AGENT 回复会显示在这里。'
|
|
1387
|
+
: '输入命令后,容器输出会显示在这里。';
|
|
1232
1388
|
messagesNode.appendChild(empty);
|
|
1233
1389
|
state.messageRenderKeys = [];
|
|
1234
1390
|
return;
|
|
@@ -1442,7 +1598,7 @@
|
|
|
1442
1598
|
return -1;
|
|
1443
1599
|
}
|
|
1444
1600
|
|
|
1445
|
-
function appendAssistantMessageLocal(sessionName, result) {
|
|
1601
|
+
function appendAssistantMessageLocal(sessionName, result, mode) {
|
|
1446
1602
|
if (state.active !== sessionName) {
|
|
1447
1603
|
return;
|
|
1448
1604
|
}
|
|
@@ -1453,7 +1609,8 @@
|
|
|
1453
1609
|
role: 'assistant',
|
|
1454
1610
|
content: outputText,
|
|
1455
1611
|
timestamp: new Date().toISOString(),
|
|
1456
|
-
exitCode: exitCode
|
|
1612
|
+
exitCode: exitCode,
|
|
1613
|
+
mode: mode || 'command'
|
|
1457
1614
|
});
|
|
1458
1615
|
}
|
|
1459
1616
|
|
|
@@ -1508,6 +1665,26 @@
|
|
|
1508
1665
|
});
|
|
1509
1666
|
}
|
|
1510
1667
|
|
|
1668
|
+
[createShellPrefix, createShell, createShellSuffix, createYolo].forEach(function (inputNode) {
|
|
1669
|
+
if (!inputNode) return;
|
|
1670
|
+
inputNode.addEventListener('input', function () {
|
|
1671
|
+
updateCreateAgentPromptCommandFromCommand();
|
|
1672
|
+
});
|
|
1673
|
+
});
|
|
1674
|
+
|
|
1675
|
+
if (createAgentPromptCommand) {
|
|
1676
|
+
createAgentPromptCommand.addEventListener('input', function () {
|
|
1677
|
+
const current = String(createAgentPromptCommand.value || '').trim();
|
|
1678
|
+
if (!current) {
|
|
1679
|
+
state.createAgentPromptAuto = true;
|
|
1680
|
+
updateCreateAgentPromptCommandFromCommand();
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
const inferred = inferCreateAgentPromptCommand();
|
|
1684
|
+
state.createAgentPromptAuto = Boolean(inferred) && inferred === current;
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1511
1688
|
if (createForm) {
|
|
1512
1689
|
createForm.addEventListener('submit', async function (event) {
|
|
1513
1690
|
event.preventDefault();
|
|
@@ -1517,9 +1694,13 @@
|
|
|
1517
1694
|
syncUi();
|
|
1518
1695
|
try {
|
|
1519
1696
|
const createOptions = collectCreateOptions();
|
|
1697
|
+
const runName = createRun ? String(createRun.value || '').trim() : '';
|
|
1520
1698
|
const data = await api('/api/sessions', {
|
|
1521
1699
|
method: 'POST',
|
|
1522
|
-
body: JSON.stringify({
|
|
1700
|
+
body: JSON.stringify({
|
|
1701
|
+
run: runName || undefined,
|
|
1702
|
+
createOptions: createOptions
|
|
1703
|
+
})
|
|
1523
1704
|
});
|
|
1524
1705
|
closeCreateModal();
|
|
1525
1706
|
await loadSessions(data.name);
|
|
@@ -1540,16 +1721,23 @@
|
|
|
1540
1721
|
if (!state.active) return;
|
|
1541
1722
|
if (state.sending) return;
|
|
1542
1723
|
if (state.loadingSessions || state.loadingMessages) return;
|
|
1543
|
-
|
|
1544
|
-
|
|
1724
|
+
if (!isComposerMode()) return;
|
|
1725
|
+
const mode = state.mode === 'agent' ? 'agent' : 'command';
|
|
1726
|
+
if (mode === 'agent' && !isActiveAgentEnabled()) {
|
|
1727
|
+
syncUi();
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
const inputText = (commandInput.value || '').trim();
|
|
1731
|
+
if (!inputText) return;
|
|
1545
1732
|
|
|
1546
1733
|
const submitSession = state.active;
|
|
1547
1734
|
const pendingMessage = {
|
|
1548
1735
|
id: createLocalMessageId('local-user'),
|
|
1549
1736
|
role: 'user',
|
|
1550
|
-
content:
|
|
1737
|
+
content: inputText,
|
|
1551
1738
|
timestamp: new Date().toISOString(),
|
|
1552
|
-
pending: true
|
|
1739
|
+
pending: true,
|
|
1740
|
+
mode: mode
|
|
1553
1741
|
};
|
|
1554
1742
|
state.messages.push(pendingMessage);
|
|
1555
1743
|
renderMessages(state.messages, { stickToBottom: true });
|
|
@@ -1559,9 +1747,14 @@
|
|
|
1559
1747
|
try {
|
|
1560
1748
|
commandInput.value = '';
|
|
1561
1749
|
commandInput.focus();
|
|
1562
|
-
const
|
|
1750
|
+
const endpoint = mode === 'agent' ? '/agent' : '/run';
|
|
1751
|
+
const runResult = await api('/api/sessions/' + encodeURIComponent(submitSession) + endpoint, {
|
|
1563
1752
|
method: 'POST',
|
|
1564
|
-
body: JSON.stringify(
|
|
1753
|
+
body: JSON.stringify(
|
|
1754
|
+
mode === 'agent'
|
|
1755
|
+
? { prompt: inputText }
|
|
1756
|
+
: { command: inputText }
|
|
1757
|
+
)
|
|
1565
1758
|
});
|
|
1566
1759
|
const pendingIndex = confirmPendingUserMessage(submitSession, pendingMessage.id);
|
|
1567
1760
|
if (pendingIndex >= 0 && pendingIndex < state.messageRenderKeys.length) {
|
|
@@ -1572,7 +1765,7 @@
|
|
|
1572
1765
|
}
|
|
1573
1766
|
}
|
|
1574
1767
|
}
|
|
1575
|
-
appendAssistantMessageLocal(submitSession, runResult);
|
|
1768
|
+
appendAssistantMessageLocal(submitSession, runResult, mode);
|
|
1576
1769
|
if (state.active === submitSession) {
|
|
1577
1770
|
renderMessages(state.messages, { stickToBottom: true });
|
|
1578
1771
|
}
|
|
@@ -1607,7 +1800,7 @@
|
|
|
1607
1800
|
|
|
1608
1801
|
// Enter / Ctrl+Enter: 发送
|
|
1609
1802
|
event.preventDefault();
|
|
1610
|
-
if (!state.active || state.sending) {
|
|
1803
|
+
if (!state.active || state.sending || !isComposerMode()) {
|
|
1611
1804
|
return;
|
|
1612
1805
|
}
|
|
1613
1806
|
composer.requestSubmit();
|
|
@@ -1616,6 +1809,16 @@
|
|
|
1616
1809
|
if (modeCommandBtn) {
|
|
1617
1810
|
modeCommandBtn.addEventListener('click', function () {
|
|
1618
1811
|
state.mode = 'command';
|
|
1812
|
+
renderMessages(state.messages, { forceFullRender: true });
|
|
1813
|
+
syncUi();
|
|
1814
|
+
commandInput.focus();
|
|
1815
|
+
});
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
if (modeAgentBtn) {
|
|
1819
|
+
modeAgentBtn.addEventListener('click', function () {
|
|
1820
|
+
state.mode = 'agent';
|
|
1821
|
+
renderMessages(state.messages, { forceFullRender: true });
|
|
1619
1822
|
syncUi();
|
|
1620
1823
|
commandInput.focus();
|
|
1621
1824
|
});
|
|
@@ -1624,6 +1827,7 @@
|
|
|
1624
1827
|
if (modeTerminalBtn) {
|
|
1625
1828
|
modeTerminalBtn.addEventListener('click', function () {
|
|
1626
1829
|
state.mode = 'terminal';
|
|
1830
|
+
renderMessages(state.messages, { forceFullRender: true });
|
|
1627
1831
|
syncUi();
|
|
1628
1832
|
if (ensureTerminalReady()) {
|
|
1629
1833
|
if (!state.terminal.connected && !state.terminal.connecting) {
|
package/lib/web/server.js
CHANGED
|
@@ -9,6 +9,7 @@ const http = require('http');
|
|
|
9
9
|
const WebSocket = require('ws');
|
|
10
10
|
const JSON5 = require('json5');
|
|
11
11
|
const { buildContainerRunArgs } = require('../container-run');
|
|
12
|
+
const { resolveAgentPromptCommandTemplate } = require('../agent-resume');
|
|
12
13
|
|
|
13
14
|
const WEB_HISTORY_MAX_MESSAGES = 500;
|
|
14
15
|
const WEB_OUTPUT_MAX_CHARS = 16000;
|
|
@@ -48,6 +49,7 @@ const DEFAULT_WEB_CONFIG_TEMPLATE = `{
|
|
|
48
49
|
"shellPrefix": "",
|
|
49
50
|
"shell": "",
|
|
50
51
|
"shellSuffix": "",
|
|
52
|
+
"agentPromptCommand": "",
|
|
51
53
|
"yolo": "",
|
|
52
54
|
"env": {},
|
|
53
55
|
"envFile": [],
|
|
@@ -100,7 +102,12 @@ function loadWebSessionHistory(webHistoryDir, containerName) {
|
|
|
100
102
|
ensureWebHistoryDir(webHistoryDir);
|
|
101
103
|
const filePath = getWebHistoryFile(webHistoryDir, containerName);
|
|
102
104
|
if (!fs.existsSync(filePath)) {
|
|
103
|
-
return {
|
|
105
|
+
return {
|
|
106
|
+
containerName,
|
|
107
|
+
updatedAt: null,
|
|
108
|
+
messages: [],
|
|
109
|
+
agentPromptCommand: ''
|
|
110
|
+
};
|
|
104
111
|
}
|
|
105
112
|
|
|
106
113
|
try {
|
|
@@ -108,10 +115,18 @@ function loadWebSessionHistory(webHistoryDir, containerName) {
|
|
|
108
115
|
return {
|
|
109
116
|
containerName,
|
|
110
117
|
updatedAt: data.updatedAt || null,
|
|
111
|
-
messages: Array.isArray(data.messages) ? data.messages : []
|
|
118
|
+
messages: Array.isArray(data.messages) ? data.messages : [],
|
|
119
|
+
agentPromptCommand: typeof data.agentPromptCommand === 'string'
|
|
120
|
+
? data.agentPromptCommand
|
|
121
|
+
: ''
|
|
112
122
|
};
|
|
113
123
|
} catch (e) {
|
|
114
|
-
return {
|
|
124
|
+
return {
|
|
125
|
+
containerName,
|
|
126
|
+
updatedAt: null,
|
|
127
|
+
messages: [],
|
|
128
|
+
agentPromptCommand: ''
|
|
129
|
+
};
|
|
115
130
|
}
|
|
116
131
|
}
|
|
117
132
|
|
|
@@ -156,6 +171,12 @@ function appendWebSessionMessage(webHistoryDir, containerName, role, content, ex
|
|
|
156
171
|
saveWebSessionHistory(webHistoryDir, containerName, history);
|
|
157
172
|
}
|
|
158
173
|
|
|
174
|
+
function setWebSessionAgentPromptCommand(webHistoryDir, containerName, agentPromptCommand) {
|
|
175
|
+
const history = loadWebSessionHistory(webHistoryDir, containerName);
|
|
176
|
+
history.agentPromptCommand = normalizeAgentPromptCommandTemplate(agentPromptCommand, 'agentPromptCommand');
|
|
177
|
+
saveWebSessionHistory(webHistoryDir, containerName, history);
|
|
178
|
+
}
|
|
179
|
+
|
|
159
180
|
function stripAnsi(text) {
|
|
160
181
|
if (typeof text !== 'string') return '';
|
|
161
182
|
return text.replace(/\x1b\[[0-9;]*m/g, '');
|
|
@@ -167,6 +188,38 @@ function clipText(text, maxChars = WEB_OUTPUT_MAX_CHARS) {
|
|
|
167
188
|
return `${text.slice(0, maxChars)}\n...[truncated]`;
|
|
168
189
|
}
|
|
169
190
|
|
|
191
|
+
function normalizeAgentPromptCommandTemplate(value, sourceLabel = 'agentPromptCommand') {
|
|
192
|
+
if (value === undefined || value === null) {
|
|
193
|
+
return '';
|
|
194
|
+
}
|
|
195
|
+
if (typeof value !== 'string') {
|
|
196
|
+
throw new Error(`${sourceLabel} 必须是字符串`);
|
|
197
|
+
}
|
|
198
|
+
const text = value.trim();
|
|
199
|
+
if (!text) {
|
|
200
|
+
return '';
|
|
201
|
+
}
|
|
202
|
+
if (!text.includes('{prompt}')) {
|
|
203
|
+
throw new Error(`${sourceLabel} 必须包含 {prompt} 占位符`);
|
|
204
|
+
}
|
|
205
|
+
return text;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function isAgentPromptCommandEnabled(value) {
|
|
209
|
+
return typeof value === 'string' && value.includes('{prompt}') && Boolean(value.trim());
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function quoteBashSingleValue(value) {
|
|
213
|
+
const text = String(value || '');
|
|
214
|
+
return `'${text.replace(/'/g, `'\"'\"'`)}'`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function renderAgentPromptCommand(template, prompt) {
|
|
218
|
+
const templateText = normalizeAgentPromptCommandTemplate(template, 'agentPromptCommand');
|
|
219
|
+
const safePrompt = quoteBashSingleValue(prompt);
|
|
220
|
+
return templateText.replace(/\{prompt\}/g, safePrompt);
|
|
221
|
+
}
|
|
222
|
+
|
|
170
223
|
function secureStringEqual(a, b) {
|
|
171
224
|
const aStr = String(a || '');
|
|
172
225
|
const bStr = String(b || '');
|
|
@@ -478,11 +531,20 @@ function validateWebConfigShape(configObject) {
|
|
|
478
531
|
if (hasOwn(config, 'ports')) {
|
|
479
532
|
normalizeStringArray(config.ports, 'ports');
|
|
480
533
|
}
|
|
534
|
+
if (hasOwn(config, 'agentPromptCommand')) {
|
|
535
|
+
normalizeAgentPromptCommandTemplate(config.agentPromptCommand, 'agentPromptCommand');
|
|
536
|
+
}
|
|
481
537
|
if (hasOwn(config, 'runs')) {
|
|
482
538
|
const runs = config.runs;
|
|
483
539
|
if (runs !== undefined && (typeof runs !== 'object' || runs === null || Array.isArray(runs))) {
|
|
484
540
|
throw new Error('runs 必须是对象(map)');
|
|
485
541
|
}
|
|
542
|
+
Object.entries(toPlainObject(runs)).forEach(([runName, runConfig]) => {
|
|
543
|
+
const normalizedRun = toPlainObject(runConfig);
|
|
544
|
+
if (hasOwn(normalizedRun, 'agentPromptCommand')) {
|
|
545
|
+
normalizeAgentPromptCommandTemplate(normalizedRun.agentPromptCommand, `runs.${runName}.agentPromptCommand`);
|
|
546
|
+
}
|
|
547
|
+
});
|
|
486
548
|
}
|
|
487
549
|
}
|
|
488
550
|
|
|
@@ -556,6 +618,7 @@ function buildConfigDefaults(ctx, config) {
|
|
|
556
618
|
shellPrefix: hasOwn(parsed, 'shellPrefix') ? String(parsed.shellPrefix || '') : '',
|
|
557
619
|
shell: hasOwn(parsed, 'shell') ? String(parsed.shell || '') : '',
|
|
558
620
|
shellSuffix: hasOwn(parsed, 'shellSuffix') ? String(parsed.shellSuffix || '') : '',
|
|
621
|
+
agentPromptCommand: '',
|
|
559
622
|
yolo: hasOwn(parsed, 'yolo') ? String(parsed.yolo || '') : '',
|
|
560
623
|
env: {},
|
|
561
624
|
envFile: [],
|
|
@@ -583,6 +646,11 @@ function buildConfigDefaults(ctx, config) {
|
|
|
583
646
|
} catch (e) {
|
|
584
647
|
defaults.ports = [];
|
|
585
648
|
}
|
|
649
|
+
try {
|
|
650
|
+
defaults.agentPromptCommand = normalizeAgentPromptCommandTemplate(parsed.agentPromptCommand, 'agentPromptCommand');
|
|
651
|
+
} catch (e) {
|
|
652
|
+
defaults.agentPromptCommand = '';
|
|
653
|
+
}
|
|
586
654
|
|
|
587
655
|
return defaults;
|
|
588
656
|
}
|
|
@@ -607,6 +675,9 @@ function buildCreateRuntime(ctx, state, payload) {
|
|
|
607
675
|
const requestOptions = toPlainObject(body.createOptions);
|
|
608
676
|
const snapshot = readWebConfigSnapshot(state.webConfigPath);
|
|
609
677
|
const config = snapshot.parseError ? {} : snapshot.parsed;
|
|
678
|
+
const runs = toPlainObject(config.runs);
|
|
679
|
+
const runName = pickFirstString(body.run);
|
|
680
|
+
const runConfig = runName && hasOwn(runs, runName) ? toPlainObject(runs[runName]) : {};
|
|
610
681
|
|
|
611
682
|
const hasRequestEnv = hasOwn(requestOptions, 'env');
|
|
612
683
|
const hasRequestEnvFile = hasOwn(requestOptions, 'envFile');
|
|
@@ -663,6 +734,18 @@ function buildCreateRuntime(ctx, state, payload) {
|
|
|
663
734
|
shell = yoloCommand;
|
|
664
735
|
}
|
|
665
736
|
|
|
737
|
+
const configuredAgentPromptCommand = normalizeAgentPromptCommandTemplate(
|
|
738
|
+
hasOwn(requestOptions, 'agentPromptCommand')
|
|
739
|
+
? requestOptions.agentPromptCommand
|
|
740
|
+
: (hasOwn(runConfig, 'agentPromptCommand') ? runConfig.agentPromptCommand : config.agentPromptCommand),
|
|
741
|
+
'agentPromptCommand'
|
|
742
|
+
);
|
|
743
|
+
const inferredAgentPromptCommand = normalizeAgentPromptCommandTemplate(
|
|
744
|
+
resolveAgentPromptCommandTemplate(buildDefaultCommand(shellPrefix, shell, shellSuffix)),
|
|
745
|
+
'agentPromptCommand'
|
|
746
|
+
);
|
|
747
|
+
const agentPromptCommand = configuredAgentPromptCommand || inferredAgentPromptCommand;
|
|
748
|
+
|
|
666
749
|
let containerEnvs = Array.isArray(ctx.containerEnvs) ? ctx.containerEnvs.slice() : [];
|
|
667
750
|
if (hasRequestEnv || hasRequestEnvFile || hasConfigEnv || hasConfigEnvFile) {
|
|
668
751
|
const configEnv = normalizeEnvMap(config.env, 'config.env');
|
|
@@ -717,6 +800,7 @@ function buildCreateRuntime(ctx, state, payload) {
|
|
|
717
800
|
containerEnvs,
|
|
718
801
|
containerVolumes,
|
|
719
802
|
containerPorts,
|
|
803
|
+
agentPromptCommand,
|
|
720
804
|
defaultCommand: buildDefaultCommand(shellPrefix, shell, shellSuffix) || '/bin/bash',
|
|
721
805
|
applied: {
|
|
722
806
|
containerName,
|
|
@@ -728,6 +812,7 @@ function buildCreateRuntime(ctx, state, payload) {
|
|
|
728
812
|
shellPrefix: shellPrefix || '',
|
|
729
813
|
shell: shell || '',
|
|
730
814
|
shellSuffix: shellSuffix || '',
|
|
815
|
+
agentEnabled: isAgentPromptCommandEnabled(agentPromptCommand),
|
|
731
816
|
yolo: yolo || '',
|
|
732
817
|
envCount: Math.floor(containerEnvs.length / 2),
|
|
733
818
|
volumeCount: Math.floor(containerVolumes.length / 2),
|
|
@@ -918,7 +1003,8 @@ function buildSessionSummary(ctx, state, containerMap, name) {
|
|
|
918
1003
|
status: containerInfo.status || 'history',
|
|
919
1004
|
image: containerInfo.image || '',
|
|
920
1005
|
updatedAt,
|
|
921
|
-
messageCount: history.messages.length
|
|
1006
|
+
messageCount: history.messages.length,
|
|
1007
|
+
agentEnabled: isAgentPromptCommandEnabled(history.agentPromptCommand)
|
|
922
1008
|
};
|
|
923
1009
|
}
|
|
924
1010
|
|
|
@@ -1325,6 +1411,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
1325
1411
|
}
|
|
1326
1412
|
|
|
1327
1413
|
await ensureWebContainer(ctx, state, runtime);
|
|
1414
|
+
setWebSessionAgentPromptCommand(state.webHistoryDir, runtime.containerName, runtime.agentPromptCommand);
|
|
1328
1415
|
sendJson(res, 200, { name: runtime.containerName, applied: runtime.applied });
|
|
1329
1416
|
}
|
|
1330
1417
|
},
|
|
@@ -1369,6 +1456,42 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
1369
1456
|
sendJson(res, 200, { exitCode: result.exitCode, output: result.output });
|
|
1370
1457
|
}
|
|
1371
1458
|
},
|
|
1459
|
+
{
|
|
1460
|
+
method: 'POST',
|
|
1461
|
+
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/agent$/),
|
|
1462
|
+
handler: async match => {
|
|
1463
|
+
const containerName = getValidSessionName(ctx, res, match[1]);
|
|
1464
|
+
if (!containerName) {
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
const payload = await readJsonBody(req);
|
|
1469
|
+
const prompt = (payload.prompt || '').trim();
|
|
1470
|
+
if (!prompt) {
|
|
1471
|
+
sendJson(res, 400, { error: 'prompt 不能为空' });
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
const history = loadWebSessionHistory(state.webHistoryDir, containerName);
|
|
1476
|
+
if (!isAgentPromptCommandEnabled(history.agentPromptCommand)) {
|
|
1477
|
+
sendJson(res, 400, { error: '当前会话未配置 agentPromptCommand' });
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
const command = renderAgentPromptCommand(history.agentPromptCommand, prompt);
|
|
1482
|
+
await ensureWebContainer(ctx, state, containerName);
|
|
1483
|
+
appendWebSessionMessage(state.webHistoryDir, containerName, 'user', prompt, { mode: 'agent' });
|
|
1484
|
+
const result = await execCommandInWebContainer(ctx, containerName, command);
|
|
1485
|
+
appendWebSessionMessage(
|
|
1486
|
+
state.webHistoryDir,
|
|
1487
|
+
containerName,
|
|
1488
|
+
'assistant',
|
|
1489
|
+
result.output,
|
|
1490
|
+
{ exitCode: result.exitCode, mode: 'agent' }
|
|
1491
|
+
);
|
|
1492
|
+
sendJson(res, 200, { exitCode: result.exitCode, output: result.output });
|
|
1493
|
+
}
|
|
1494
|
+
},
|
|
1372
1495
|
{
|
|
1373
1496
|
method: 'POST',
|
|
1374
1497
|
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/remove$/),
|
|
@@ -1613,7 +1736,7 @@ async function startWebServer(options) {
|
|
|
1613
1736
|
const { GREEN, CYAN, YELLOW, NC } = ctx.colors;
|
|
1614
1737
|
const listenHost = formatUrlHost(ctx.serverHost);
|
|
1615
1738
|
console.log(`${GREEN}✅ MANYOYO Web 服务已启动: http://${listenHost}:${listenPort}${NC}`);
|
|
1616
|
-
console.log(`${CYAN}提示: 左侧是 manyoyo
|
|
1739
|
+
console.log(`${CYAN}提示: 左侧是 manyoyo 容器会话列表,右侧支持命令模式、AGENT 模式与交互式终端模式。${NC}`);
|
|
1617
1740
|
if (ctx.serverHost === '0.0.0.0') {
|
|
1618
1741
|
console.log(`${CYAN}提示: 当前监听全部网卡,请用本机局域网 IP 访问。${NC}`);
|
|
1619
1742
|
}
|