@xcanwin/manyoyo 5.7.2 → 5.7.3
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/lib/web/frontend/app.css +2 -2
- package/lib/web/frontend/app.html +2 -2
- package/lib/web/frontend/app.js +100 -65
- package/lib/web/server.js +120 -31
- package/package.json +1 -1
package/lib/web/frontend/app.css
CHANGED
|
@@ -783,8 +783,8 @@ body.mobile-actions-open .header-actions {
|
|
|
783
783
|
color: var(--muted);
|
|
784
784
|
font-size: 12px;
|
|
785
785
|
font-weight: 700;
|
|
786
|
-
text-transform:
|
|
787
|
-
letter-spacing: 0.
|
|
786
|
+
text-transform: none;
|
|
787
|
+
letter-spacing: 0.1px;
|
|
788
788
|
line-height: 1.45;
|
|
789
789
|
overflow-wrap: anywhere;
|
|
790
790
|
}
|
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
<div id="configModal" class="modal-backdrop" hidden>
|
|
127
127
|
<section class="modal" role="dialog" aria-modal="true" aria-labelledby="configModalTitle">
|
|
128
128
|
<header class="modal-header">
|
|
129
|
-
<h2 id="configModalTitle"
|
|
129
|
+
<h2 id="configModalTitle">查看配置摘要 (~/.manyoyo/manyoyo.json)</h2>
|
|
130
130
|
<button type="button" id="configCancelBtn" class="secondary">关闭</button>
|
|
131
131
|
</header>
|
|
132
132
|
<div class="modal-body">
|
|
@@ -136,7 +136,7 @@
|
|
|
136
136
|
</div>
|
|
137
137
|
<footer class="modal-footer">
|
|
138
138
|
<button type="button" id="configReloadBtn" class="secondary">重新加载</button>
|
|
139
|
-
<button type="button" id="configSaveBtn"
|
|
139
|
+
<button type="button" id="configSaveBtn">只读</button>
|
|
140
140
|
</footer>
|
|
141
141
|
</section>
|
|
142
142
|
</div>
|
package/lib/web/frontend/app.js
CHANGED
|
@@ -104,6 +104,7 @@
|
|
|
104
104
|
const openConfigBtn = document.getElementById('openConfigBtn');
|
|
105
105
|
const openCreateBtn = document.getElementById('openCreateBtn');
|
|
106
106
|
const configModal = document.getElementById('configModal');
|
|
107
|
+
const configModalTitle = document.getElementById('configModalTitle');
|
|
107
108
|
const configPath = document.getElementById('configPath');
|
|
108
109
|
const configEditor = document.getElementById('configEditor');
|
|
109
110
|
const configError = document.getElementById('configError');
|
|
@@ -621,9 +622,10 @@
|
|
|
621
622
|
createAgentPromptCommand.value = value.agentPromptCommand || '';
|
|
622
623
|
state.createAgentPromptAuto = false;
|
|
623
624
|
createYolo.value = value.yolo || '';
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
625
|
+
// 敏感 env 与继承数组由服务端在创建时合并,前端表单默认不回显,避免泄露或重复提交。
|
|
626
|
+
createEnv.value = '';
|
|
627
|
+
createEnvFile.value = '';
|
|
628
|
+
createVolumes.value = '';
|
|
627
629
|
updateCreateAgentPromptCommandFromCommand();
|
|
628
630
|
}
|
|
629
631
|
|
|
@@ -1236,9 +1238,9 @@
|
|
|
1236
1238
|
function renderSessionDetailPanels() {
|
|
1237
1239
|
const detail = state.sessionDetail;
|
|
1238
1240
|
if (!state.active) {
|
|
1239
|
-
renderEmptyInspector(detailSummary, '详情视图', '选择左侧会话后,这里会显示会话概览、Agent
|
|
1240
|
-
renderEmptyInspector(configSummary, '配置视图', '
|
|
1241
|
-
renderEmptyInspector(checkSummary, '检查视图', '
|
|
1241
|
+
renderEmptyInspector(detailSummary, '详情视图', '选择左侧会话后,这里会显示会话概览、Agent 运行状态与最近活动。');
|
|
1242
|
+
renderEmptyInspector(configSummary, '配置视图', '选择会话后可查看当前容器会话的生效配置摘要。');
|
|
1243
|
+
renderEmptyInspector(checkSummary, '检查视图', '选择会话后可查看当前会话的诊断结论与最近问题。');
|
|
1242
1244
|
return;
|
|
1243
1245
|
}
|
|
1244
1246
|
if (state.loadingSessionDetail) {
|
|
@@ -1258,6 +1260,51 @@
|
|
|
1258
1260
|
const applied = detail.applied || {};
|
|
1259
1261
|
const status = sessionStatusInfo(detail.status);
|
|
1260
1262
|
const updatedText = formatDateTime(detail.updatedAt) || '暂无更新';
|
|
1263
|
+
const lastResumeText = detail.lastResumeAt ? formatDateTime(detail.lastResumeAt) : '暂无';
|
|
1264
|
+
const latestTimestampText = detail.latestTimestamp ? formatDateTime(detail.latestTimestamp) : '暂无';
|
|
1265
|
+
const latestRoleMap = {
|
|
1266
|
+
user: '我',
|
|
1267
|
+
assistant: 'Agent',
|
|
1268
|
+
system: '系统'
|
|
1269
|
+
};
|
|
1270
|
+
const latestRoleLabel = latestRoleMap[String(detail.latestRole || '').toLowerCase()] || (detail.latestRole || '暂无');
|
|
1271
|
+
const imageVersionValid = /^\d+\.\d+\.\d+-[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(String(applied.imageVersion || ''));
|
|
1272
|
+
let resumeStatusValue = '未执行';
|
|
1273
|
+
let resumeStatusTone = 'warn';
|
|
1274
|
+
let resumeStatusDetail = detail.resumeSupported
|
|
1275
|
+
? '支持 resume,但当前会话还没有最近一次执行记录。'
|
|
1276
|
+
: '当前 Agent 程序或模板不支持 resume。';
|
|
1277
|
+
if (detail.lastResumeOk === true) {
|
|
1278
|
+
resumeStatusValue = '最近成功';
|
|
1279
|
+
resumeStatusTone = 'ok';
|
|
1280
|
+
resumeStatusDetail = `最近一次 resume 成功,时间:${lastResumeText}。`;
|
|
1281
|
+
} else if (detail.lastResumeOk === false) {
|
|
1282
|
+
resumeStatusValue = '最近失败';
|
|
1283
|
+
resumeStatusTone = 'danger';
|
|
1284
|
+
resumeStatusDetail = detail.lastResumeError
|
|
1285
|
+
? `最近一次 resume 失败:${detail.lastResumeError}`
|
|
1286
|
+
: `最近一次 resume 失败,时间:${lastResumeText}。`;
|
|
1287
|
+
} else if (!detail.resumeSupported) {
|
|
1288
|
+
resumeStatusValue = '不支持';
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
const commandEntries = [];
|
|
1292
|
+
if (applied.shellPrefix) {
|
|
1293
|
+
commandEntries.push({ label: 'shellPrefix', value: applied.shellPrefix });
|
|
1294
|
+
}
|
|
1295
|
+
if (applied.shell) {
|
|
1296
|
+
commandEntries.push({ label: 'shell', value: applied.shell });
|
|
1297
|
+
}
|
|
1298
|
+
if (applied.shellSuffix) {
|
|
1299
|
+
commandEntries.push({ label: 'shellSuffix', value: applied.shellSuffix });
|
|
1300
|
+
}
|
|
1301
|
+
if (applied.defaultCommand && applied.defaultCommand !== applied.shell) {
|
|
1302
|
+
commandEntries.push({ label: '启动命令', value: applied.defaultCommand });
|
|
1303
|
+
} else if (!applied.shell) {
|
|
1304
|
+
commandEntries.push({ label: '启动命令', value: applied.defaultCommand || '—' });
|
|
1305
|
+
}
|
|
1306
|
+
commandEntries.push({ label: 'Agent 模板', value: detail.agentPromptCommand || '—' });
|
|
1307
|
+
commandEntries.push({ label: 'yolo', value: applied.yolo || '—' });
|
|
1261
1308
|
|
|
1262
1309
|
if (detailSummary) {
|
|
1263
1310
|
detailSummary.innerHTML = '';
|
|
@@ -1268,45 +1315,41 @@
|
|
|
1268
1315
|
{ label: '最近更新', value: updatedText },
|
|
1269
1316
|
{ label: '消息数', value: String(safeMessageCount(detail.messageCount)) }
|
|
1270
1317
|
]);
|
|
1271
|
-
renderKeyValueCard(detailSummary, 'Agent
|
|
1318
|
+
renderKeyValueCard(detailSummary, 'Agent 运行', [
|
|
1272
1319
|
{ label: '已启用', value: detail.agentEnabled ? '是' : '否', tone: detail.agentEnabled ? 'ok' : 'warn' },
|
|
1273
1320
|
{ label: '程序', value: detail.agentProgram || '—' },
|
|
1274
1321
|
{ label: '支持 resume', value: detail.resumeSupported ? '是' : '否', tone: detail.resumeSupported ? 'ok' : 'warn' },
|
|
1275
|
-
{ label: '最近 resume', value:
|
|
1322
|
+
{ label: '最近 resume', value: lastResumeText },
|
|
1276
1323
|
{ label: '最近结果', value: detail.lastResumeOk == null ? '暂无' : (detail.lastResumeOk ? '成功' : '失败'), tone: detail.lastResumeOk == null ? 'info' : (detail.lastResumeOk ? 'ok' : 'danger') }
|
|
1277
1324
|
]);
|
|
1278
|
-
renderKeyValueCard(detailSummary, '
|
|
1279
|
-
{ label: '
|
|
1280
|
-
{ label: '
|
|
1281
|
-
{ label: '
|
|
1282
|
-
{ label: 'containerMode', value: applied.containerMode || 'default' },
|
|
1283
|
-
{ label: 'env/vol/ports', value: `${applied.envCount || 0} / ${applied.volumeCount || 0} / ${applied.portCount || 0}` }
|
|
1325
|
+
renderKeyValueCard(detailSummary, '最近活动', [
|
|
1326
|
+
{ label: '最近角色', value: latestRoleLabel },
|
|
1327
|
+
{ label: '最近时间', value: latestTimestampText },
|
|
1328
|
+
{ label: 'resume 状态', value: resumeStatusValue, tone: resumeStatusTone }
|
|
1284
1329
|
]);
|
|
1285
1330
|
}
|
|
1286
1331
|
|
|
1287
1332
|
if (configSummary) {
|
|
1288
1333
|
configSummary.innerHTML = '';
|
|
1289
|
-
renderKeyValueCard(configSummary, '
|
|
1334
|
+
renderKeyValueCard(configSummary, '基础配置', [
|
|
1290
1335
|
{ label: 'containerName', value: applied.containerName || detail.name || state.active },
|
|
1291
|
-
{ label: 'hostPath', value: applied.hostPath || '—' },
|
|
1292
|
-
{ label: 'containerPath', value: applied.containerPath || '—' },
|
|
1293
1336
|
{ label: 'imageName', value: applied.imageName || detail.image || '—' },
|
|
1294
1337
|
{ label: 'imageVersion', value: applied.imageVersion || '—' },
|
|
1295
1338
|
{ label: 'containerMode', value: applied.containerMode || 'default' }
|
|
1296
|
-
], { actionLabel: '打开配置', actionId: 'configSummaryOpenBtn' });
|
|
1297
|
-
renderKeyValueCard(configSummary, '命令与 Agent', [
|
|
1298
|
-
{ label: 'shellPrefix', value: applied.shellPrefix || '—' },
|
|
1299
|
-
{ label: 'shell', value: applied.shell || '—' },
|
|
1300
|
-
{ label: 'shellSuffix', value: applied.shellSuffix || '—' },
|
|
1301
|
-
{ label: '默认命令', value: applied.defaultCommand || '—' },
|
|
1302
|
-
{ label: 'Agent 模板', value: detail.agentPromptCommand || '—' },
|
|
1303
|
-
{ label: 'yolo', value: applied.yolo || '—' }
|
|
1304
1339
|
]);
|
|
1340
|
+
renderKeyValueCard(configSummary, '路径与资源', [
|
|
1341
|
+
{ label: 'hostPath', value: applied.hostPath || '—' },
|
|
1342
|
+
{ label: 'containerPath', value: applied.containerPath || '—' },
|
|
1343
|
+
{ label: 'env 数量', value: String(applied.envCount || 0) },
|
|
1344
|
+
{ label: 'volume 数量', value: String(applied.volumeCount || 0) },
|
|
1345
|
+
{ label: 'port 数量', value: String(applied.portCount || 0) }
|
|
1346
|
+
]);
|
|
1347
|
+
renderKeyValueCard(configSummary, '命令与 Agent', commandEntries);
|
|
1305
1348
|
}
|
|
1306
1349
|
|
|
1307
1350
|
if (checkSummary) {
|
|
1308
1351
|
checkSummary.innerHTML = '';
|
|
1309
|
-
renderCheckCard(checkSummary, '
|
|
1352
|
+
renderCheckCard(checkSummary, '运行检查', [
|
|
1310
1353
|
{
|
|
1311
1354
|
label: '容器状态',
|
|
1312
1355
|
value: status.label,
|
|
@@ -1314,28 +1357,32 @@
|
|
|
1314
1357
|
detail: status.tone === 'running' ? '容器处于可交互状态。' : '当前不是活跃运行态,部分功能可能受限。'
|
|
1315
1358
|
},
|
|
1316
1359
|
{
|
|
1317
|
-
label: 'Agent
|
|
1360
|
+
label: 'Agent 输入',
|
|
1318
1361
|
value: detail.agentEnabled ? '已配置' : '未配置',
|
|
1319
1362
|
tone: detail.agentEnabled ? 'ok' : 'warn',
|
|
1320
1363
|
detail: detail.agentEnabled ? '活动页可直接发送 Agent 提示词。' : '当前会话不支持 Agent 模式。'
|
|
1321
1364
|
},
|
|
1322
1365
|
{
|
|
1323
|
-
label: 'Resume
|
|
1324
|
-
value:
|
|
1325
|
-
tone:
|
|
1326
|
-
detail:
|
|
1366
|
+
label: 'Resume 健康',
|
|
1367
|
+
value: resumeStatusValue,
|
|
1368
|
+
tone: resumeStatusTone,
|
|
1369
|
+
detail: resumeStatusDetail
|
|
1327
1370
|
},
|
|
1328
1371
|
{
|
|
1329
|
-
label: '
|
|
1330
|
-
value:
|
|
1331
|
-
tone:
|
|
1332
|
-
detail:
|
|
1372
|
+
label: '镜像版本',
|
|
1373
|
+
value: imageVersionValid ? '格式正常' : '格式异常',
|
|
1374
|
+
tone: imageVersionValid ? 'ok' : 'danger',
|
|
1375
|
+
detail: applied.imageVersion
|
|
1376
|
+
? `当前值:${applied.imageVersion}。建议保持 x.y.z-后缀 格式,便于 manyoyo 的版本校验。`
|
|
1377
|
+
: '缺少 imageVersion,manyoyo 的版本校验会失效。'
|
|
1333
1378
|
},
|
|
1334
1379
|
{
|
|
1335
|
-
label: '
|
|
1336
|
-
value: applied.hostPath && applied.containerPath ? '
|
|
1380
|
+
label: '工作目录映射',
|
|
1381
|
+
value: applied.hostPath && applied.containerPath ? '已配置' : '缺失',
|
|
1337
1382
|
tone: applied.hostPath && applied.containerPath ? 'ok' : 'danger',
|
|
1338
|
-
detail:
|
|
1383
|
+
detail: applied.hostPath && applied.containerPath
|
|
1384
|
+
? '宿主目录与容器目录都已配置。'
|
|
1385
|
+
: 'hostPath / containerPath 是容器会话最关键的上下文。'
|
|
1339
1386
|
}
|
|
1340
1387
|
]);
|
|
1341
1388
|
if (detail.lastResumeError) {
|
|
@@ -1349,13 +1396,6 @@
|
|
|
1349
1396
|
]);
|
|
1350
1397
|
}
|
|
1351
1398
|
}
|
|
1352
|
-
|
|
1353
|
-
const configSummaryOpenBtn = document.getElementById('configSummaryOpenBtn');
|
|
1354
|
-
if (configSummaryOpenBtn) {
|
|
1355
|
-
configSummaryOpenBtn.addEventListener('click', function () {
|
|
1356
|
-
openConfigModal();
|
|
1357
|
-
});
|
|
1358
|
-
}
|
|
1359
1399
|
}
|
|
1360
1400
|
|
|
1361
1401
|
function syncUi() {
|
|
@@ -1444,7 +1484,7 @@
|
|
|
1444
1484
|
openConfigBtn.disabled = state.configLoading || state.configSaving;
|
|
1445
1485
|
}
|
|
1446
1486
|
if (configSaveBtn) {
|
|
1447
|
-
configSaveBtn.disabled =
|
|
1487
|
+
configSaveBtn.disabled = true;
|
|
1448
1488
|
}
|
|
1449
1489
|
if (configReloadBtn) {
|
|
1450
1490
|
configReloadBtn.disabled = state.configLoading || state.configSaving;
|
|
@@ -1612,11 +1652,22 @@
|
|
|
1612
1652
|
syncUi();
|
|
1613
1653
|
try {
|
|
1614
1654
|
const config = await fetchConfigSnapshot();
|
|
1655
|
+
if (configModalTitle) {
|
|
1656
|
+
configModalTitle.textContent = '查看配置摘要 (~/.manyoyo/manyoyo.json)';
|
|
1657
|
+
}
|
|
1615
1658
|
if (configPath) {
|
|
1616
|
-
|
|
1659
|
+
const lines = [config.path || ''];
|
|
1660
|
+
if (config.notice) {
|
|
1661
|
+
lines.push(config.notice);
|
|
1662
|
+
}
|
|
1663
|
+
configPath.textContent = lines.filter(Boolean).join('\n');
|
|
1617
1664
|
}
|
|
1618
1665
|
if (configEditor) {
|
|
1619
|
-
configEditor.
|
|
1666
|
+
configEditor.readOnly = true;
|
|
1667
|
+
configEditor.value = stringifyPrettyJson({
|
|
1668
|
+
defaults: config.defaults || {},
|
|
1669
|
+
runs: config.parsed && config.parsed.runs ? config.parsed.runs : {}
|
|
1670
|
+
});
|
|
1620
1671
|
}
|
|
1621
1672
|
if (config.parseError) {
|
|
1622
1673
|
showConfigError('当前文件存在解析错误:' + config.parseError);
|
|
@@ -1637,23 +1688,7 @@
|
|
|
1637
1688
|
}
|
|
1638
1689
|
|
|
1639
1690
|
async function saveConfig() {
|
|
1640
|
-
|
|
1641
|
-
showConfigError('');
|
|
1642
|
-
syncUi();
|
|
1643
|
-
try {
|
|
1644
|
-
await api('/api/config', {
|
|
1645
|
-
method: 'PUT',
|
|
1646
|
-
body: JSON.stringify({ raw: configEditor ? configEditor.value : '' })
|
|
1647
|
-
});
|
|
1648
|
-
await fetchConfigSnapshot();
|
|
1649
|
-
showConfigError('');
|
|
1650
|
-
alert('配置已保存。后续新建会读取最新配置。');
|
|
1651
|
-
} catch (e) {
|
|
1652
|
-
showConfigError(e.message);
|
|
1653
|
-
} finally {
|
|
1654
|
-
state.configSaving = false;
|
|
1655
|
-
syncUi();
|
|
1656
|
-
}
|
|
1691
|
+
alert('Web 端已禁用明文配置编辑,请在本地 ~/.manyoyo/manyoyo.json 中维护敏感配置。');
|
|
1657
1692
|
}
|
|
1658
1693
|
|
|
1659
1694
|
async function openCreateModal() {
|
package/lib/web/server.js
CHANGED
|
@@ -32,6 +32,8 @@ const WEB_AUTH_TTL_SECONDS = 12 * 60 * 60;
|
|
|
32
32
|
const FRONTEND_DIR = path.join(__dirname, 'frontend');
|
|
33
33
|
const SAFE_CONTAINER_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
|
|
34
34
|
const IMAGE_VERSION_TAG_PATTERN = /^(\d+\.\d+\.\d+)-([A-Za-z0-9][A-Za-z0-9_.-]*)$/;
|
|
35
|
+
const SENSITIVE_CONFIG_KEY_PATTERN = /(pass(word)?|passwd|secret|token|api(?:_|-)?key|auth(?:_|-)?token|oauth(?:_|-)?token)$/i;
|
|
36
|
+
const REDACTED_CONFIG_VALUE = '***';
|
|
35
37
|
|
|
36
38
|
const YOLO_COMMAND_MAP = {
|
|
37
39
|
claude: 'IS_SANDBOX=1 claude --dangerously-skip-permissions',
|
|
@@ -1300,6 +1302,59 @@ function toPlainObject(value) {
|
|
|
1300
1302
|
return value;
|
|
1301
1303
|
}
|
|
1302
1304
|
|
|
1305
|
+
function isSensitiveConfigKey(key) {
|
|
1306
|
+
const normalized = String(key || '').trim();
|
|
1307
|
+
return Boolean(normalized) && SENSITIVE_CONFIG_KEY_PATTERN.test(normalized);
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
function redactConfigValue(value) {
|
|
1311
|
+
if (Array.isArray(value)) {
|
|
1312
|
+
return value.map(item => redactConfigValue(item));
|
|
1313
|
+
}
|
|
1314
|
+
if (!value || typeof value !== 'object') {
|
|
1315
|
+
return REDACTED_CONFIG_VALUE;
|
|
1316
|
+
}
|
|
1317
|
+
return redactConfigObject(value);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
function redactConfigObject(value) {
|
|
1321
|
+
if (Array.isArray(value)) {
|
|
1322
|
+
return value.map(item => redactConfigValue(item));
|
|
1323
|
+
}
|
|
1324
|
+
if (!value || typeof value !== 'object') {
|
|
1325
|
+
return value;
|
|
1326
|
+
}
|
|
1327
|
+
const result = {};
|
|
1328
|
+
Object.entries(toPlainObject(value)).forEach(([key, item]) => {
|
|
1329
|
+
if (isSensitiveConfigKey(key)) {
|
|
1330
|
+
result[key] = redactConfigValue(item);
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
if (Array.isArray(item)) {
|
|
1334
|
+
result[key] = item.map(entry => redactConfigValue(entry));
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
if (item && typeof item === 'object') {
|
|
1338
|
+
result[key] = redactConfigObject(item);
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
result[key] = item;
|
|
1342
|
+
});
|
|
1343
|
+
return result;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
function buildSafeWebConfigSnapshot(snapshot, ctx) {
|
|
1347
|
+
const parsed = snapshot && snapshot.parseError ? {} : toPlainObject(snapshot && snapshot.parsed);
|
|
1348
|
+
return {
|
|
1349
|
+
path: snapshot && snapshot.path ? snapshot.path : path.resolve(getDefaultWebConfigPath()),
|
|
1350
|
+
parsed: redactConfigObject(parsed),
|
|
1351
|
+
defaults: redactConfigObject(buildConfigDefaults(ctx, parsed)),
|
|
1352
|
+
parseError: snapshot && snapshot.parseError ? snapshot.parseError : null,
|
|
1353
|
+
editable: false,
|
|
1354
|
+
notice: 'Web 端仅显示脱敏后的配置摘要;敏感值请在本地 manyoyo.json 中维护。'
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1303
1358
|
function pickFirstString() {
|
|
1304
1359
|
for (let i = 0; i < arguments.length; i += 1) {
|
|
1305
1360
|
const value = arguments[i];
|
|
@@ -1705,23 +1760,29 @@ function buildCreateRuntime(ctx, state, payload) {
|
|
|
1705
1760
|
const hasConfigPorts = hasOwn(config, 'ports');
|
|
1706
1761
|
|
|
1707
1762
|
const requestName = pickFirstString(requestOptions.containerName, body.name);
|
|
1708
|
-
let containerName = pickFirstString(requestName, config.containerName);
|
|
1763
|
+
let containerName = pickFirstString(requestName, runConfig.containerName, config.containerName);
|
|
1709
1764
|
if (!containerName) {
|
|
1710
1765
|
containerName = `my-${ctx.formatDate()}`;
|
|
1711
1766
|
}
|
|
1712
1767
|
containerName = resolveNowTemplate(containerName, ctx.formatDate);
|
|
1713
1768
|
validateContainerNameStrict(containerName);
|
|
1714
1769
|
|
|
1715
|
-
const hostPath = pickFirstString(requestOptions.hostPath, config.hostPath, ctx.hostPath);
|
|
1770
|
+
const hostPath = pickFirstString(requestOptions.hostPath, runConfig.hostPath, config.hostPath, ctx.hostPath);
|
|
1716
1771
|
if (typeof ctx.validateHostPath === 'function') {
|
|
1717
1772
|
ctx.validateHostPath(hostPath);
|
|
1718
1773
|
} else {
|
|
1719
1774
|
validateWebHostPath(hostPath);
|
|
1720
1775
|
}
|
|
1721
1776
|
|
|
1722
|
-
const containerPath = pickFirstString(
|
|
1723
|
-
|
|
1724
|
-
|
|
1777
|
+
const containerPath = pickFirstString(
|
|
1778
|
+
requestOptions.containerPath,
|
|
1779
|
+
runConfig.containerPath,
|
|
1780
|
+
config.containerPath,
|
|
1781
|
+
ctx.containerPath,
|
|
1782
|
+
hostPath
|
|
1783
|
+
) || hostPath;
|
|
1784
|
+
const imageName = pickFirstString(requestOptions.imageName, runConfig.imageName, config.imageName, ctx.imageName);
|
|
1785
|
+
const imageVersion = pickFirstString(requestOptions.imageVersion, runConfig.imageVersion, config.imageVersion, ctx.imageVersion);
|
|
1725
1786
|
|
|
1726
1787
|
if (!/^[A-Za-z0-9][A-Za-z0-9._/:-]*$/.test(imageName)) {
|
|
1727
1788
|
throw new Error(`imageName 非法: ${imageName}`);
|
|
@@ -1730,7 +1791,7 @@ function buildCreateRuntime(ctx, state, payload) {
|
|
|
1730
1791
|
|
|
1731
1792
|
let contModeArgs = Array.isArray(ctx.contModeArgs) ? ctx.contModeArgs.slice() : [];
|
|
1732
1793
|
let containerMode = '';
|
|
1733
|
-
const modeValue = pickFirstString(requestOptions.containerMode, config.containerMode);
|
|
1794
|
+
const modeValue = pickFirstString(requestOptions.containerMode, runConfig.containerMode, config.containerMode);
|
|
1734
1795
|
if (modeValue) {
|
|
1735
1796
|
const mode = resolveContainerModeArgs(modeValue);
|
|
1736
1797
|
containerMode = mode.mode;
|
|
@@ -1739,16 +1800,24 @@ function buildCreateRuntime(ctx, state, payload) {
|
|
|
1739
1800
|
|
|
1740
1801
|
const shellPrefix = hasOwn(requestOptions, 'shellPrefix')
|
|
1741
1802
|
? String(requestOptions.shellPrefix || '')
|
|
1742
|
-
: (hasOwn(
|
|
1803
|
+
: (hasOwn(runConfig, 'shellPrefix')
|
|
1804
|
+
? String(runConfig.shellPrefix || '')
|
|
1805
|
+
: (hasOwn(config, 'shellPrefix') ? String(config.shellPrefix || '') : String(ctx.execCommandPrefix || '')));
|
|
1743
1806
|
let shell = hasOwn(requestOptions, 'shell')
|
|
1744
1807
|
? String(requestOptions.shell || '')
|
|
1745
|
-
: (hasOwn(
|
|
1808
|
+
: (hasOwn(runConfig, 'shell')
|
|
1809
|
+
? String(runConfig.shell || '')
|
|
1810
|
+
: (hasOwn(config, 'shell') ? String(config.shell || '') : String(ctx.execCommand || '')));
|
|
1746
1811
|
const shellSuffix = hasOwn(requestOptions, 'shellSuffix')
|
|
1747
1812
|
? String(requestOptions.shellSuffix || '')
|
|
1748
|
-
: (hasOwn(
|
|
1813
|
+
: (hasOwn(runConfig, 'shellSuffix')
|
|
1814
|
+
? String(runConfig.shellSuffix || '')
|
|
1815
|
+
: (hasOwn(config, 'shellSuffix') ? String(config.shellSuffix || '') : String(ctx.execCommandSuffix || '')));
|
|
1749
1816
|
const yolo = hasOwn(requestOptions, 'yolo')
|
|
1750
1817
|
? String(requestOptions.yolo || '')
|
|
1751
|
-
: (hasOwn(
|
|
1818
|
+
: (hasOwn(runConfig, 'yolo')
|
|
1819
|
+
? String(runConfig.yolo || '')
|
|
1820
|
+
: (hasOwn(config, 'yolo') ? String(config.yolo || '') : ''));
|
|
1752
1821
|
const yoloCommand = resolveYoloCommand(yolo);
|
|
1753
1822
|
if (yoloCommand) {
|
|
1754
1823
|
shell = yoloCommand;
|
|
@@ -1769,19 +1838,23 @@ function buildCreateRuntime(ctx, state, payload) {
|
|
|
1769
1838
|
const resumeSupported = Boolean(buildAgentResumeCommand(agentProgram));
|
|
1770
1839
|
|
|
1771
1840
|
let containerEnvs = Array.isArray(ctx.containerEnvs) ? ctx.containerEnvs.slice() : [];
|
|
1772
|
-
|
|
1841
|
+
const hasRunEnv = hasOwn(runConfig, 'env');
|
|
1842
|
+
const hasRunEnvFile = hasOwn(runConfig, 'envFile');
|
|
1843
|
+
if (hasRequestEnv || hasRequestEnvFile || hasRunEnv || hasRunEnvFile || hasConfigEnv || hasConfigEnvFile) {
|
|
1773
1844
|
const configEnv = normalizeEnvMap(config.env, 'config.env');
|
|
1845
|
+
const runEnv = normalizeEnvMap(runConfig.env, runName ? `runs.${runName}.env` : 'run.env');
|
|
1774
1846
|
const requestEnv = hasRequestEnv ? normalizeEnvMap(requestOptions.env, 'createOptions.env') : {};
|
|
1775
|
-
const mergedEnv = { ...configEnv, ...requestEnv };
|
|
1847
|
+
const mergedEnv = { ...configEnv, ...runEnv, ...requestEnv };
|
|
1776
1848
|
const envArgs = [];
|
|
1777
1849
|
Object.entries(mergedEnv).forEach(([key, value]) => {
|
|
1778
1850
|
const parsed = parseEnvEntry(`${key}=${value}`);
|
|
1779
1851
|
envArgs.push('--env', `${parsed.key}=${parsed.value}`);
|
|
1780
1852
|
});
|
|
1781
1853
|
|
|
1782
|
-
const envFileList =
|
|
1783
|
-
|
|
1784
|
-
|
|
1854
|
+
const envFileList = []
|
|
1855
|
+
.concat(normalizeStringArray(config.envFile, 'config.envFile'))
|
|
1856
|
+
.concat(normalizeStringArray(runConfig.envFile, runName ? `runs.${runName}.envFile` : 'run.envFile'))
|
|
1857
|
+
.concat(hasRequestEnvFile ? normalizeStringArray(requestOptions.envFile, 'createOptions.envFile') : []);
|
|
1785
1858
|
const envFileArgs = [];
|
|
1786
1859
|
envFileList.forEach(filePath => {
|
|
1787
1860
|
envFileArgs.push(...parseEnvFileToArgs(filePath));
|
|
@@ -1791,10 +1864,12 @@ function buildCreateRuntime(ctx, state, payload) {
|
|
|
1791
1864
|
}
|
|
1792
1865
|
|
|
1793
1866
|
let containerVolumes = Array.isArray(ctx.containerVolumes) ? ctx.containerVolumes.slice() : [];
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1867
|
+
const hasRunVolumes = hasOwn(runConfig, 'volumes');
|
|
1868
|
+
if (hasRequestVolumes || hasRunVolumes || hasConfigVolumes) {
|
|
1869
|
+
const volumeList = []
|
|
1870
|
+
.concat(normalizeStringArray(config.volumes, 'config.volumes'))
|
|
1871
|
+
.concat(normalizeStringArray(runConfig.volumes, runName ? `runs.${runName}.volumes` : 'run.volumes'))
|
|
1872
|
+
.concat(hasRequestVolumes ? normalizeStringArray(requestOptions.volumes, 'createOptions.volumes') : []);
|
|
1798
1873
|
containerVolumes = [];
|
|
1799
1874
|
volumeList.forEach(volume => {
|
|
1800
1875
|
containerVolumes.push('--volume', normalizeVolume(volume));
|
|
@@ -1802,10 +1877,12 @@ function buildCreateRuntime(ctx, state, payload) {
|
|
|
1802
1877
|
}
|
|
1803
1878
|
|
|
1804
1879
|
let containerPorts = Array.isArray(ctx.containerPorts) ? ctx.containerPorts.slice() : [];
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1880
|
+
const hasRunPorts = hasOwn(runConfig, 'ports');
|
|
1881
|
+
if (hasRequestPorts || hasRunPorts || hasConfigPorts) {
|
|
1882
|
+
const portList = []
|
|
1883
|
+
.concat(normalizeStringArray(config.ports, 'config.ports'))
|
|
1884
|
+
.concat(normalizeStringArray(runConfig.ports, runName ? `runs.${runName}.ports` : 'run.ports'))
|
|
1885
|
+
.concat(hasRequestPorts ? normalizeStringArray(requestOptions.ports, 'createOptions.ports') : []);
|
|
1809
1886
|
containerPorts = [];
|
|
1810
1887
|
portList.forEach(port => {
|
|
1811
1888
|
containerPorts.push('--publish', port);
|
|
@@ -2216,6 +2293,15 @@ function sendHtml(res, statusCode, html, extraHeaders = {}) {
|
|
|
2216
2293
|
res.end(html);
|
|
2217
2294
|
}
|
|
2218
2295
|
|
|
2296
|
+
function sendRedirect(res, statusCode, location, extraHeaders = {}) {
|
|
2297
|
+
res.writeHead(statusCode, {
|
|
2298
|
+
Location: location,
|
|
2299
|
+
'Cache-Control': 'no-store',
|
|
2300
|
+
...extraHeaders
|
|
2301
|
+
});
|
|
2302
|
+
res.end('');
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2219
2305
|
function decodeSessionName(encoded) {
|
|
2220
2306
|
try {
|
|
2221
2307
|
return decodeURIComponent(encoded);
|
|
@@ -2563,6 +2649,12 @@ function bindTerminalWebSocket(ctx, state, ws, containerName, cols, rows) {
|
|
|
2563
2649
|
}
|
|
2564
2650
|
|
|
2565
2651
|
async function handleWebAuthRoutes(req, res, pathname, ctx, state) {
|
|
2652
|
+
if (req.method === 'GET' && pathname === '/favicon.ico') {
|
|
2653
|
+
res.writeHead(204, { 'Cache-Control': 'no-store' });
|
|
2654
|
+
res.end();
|
|
2655
|
+
return true;
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2566
2658
|
if (req.method === 'GET' && pathname === '/auth/login') {
|
|
2567
2659
|
sendHtml(res, 200, loadTemplate('login.html'));
|
|
2568
2660
|
return true;
|
|
@@ -2625,6 +2717,10 @@ function sendWebUnauthorized(res, pathname) {
|
|
|
2625
2717
|
sendJson(res, 401, { error: 'UNAUTHORIZED' });
|
|
2626
2718
|
return;
|
|
2627
2719
|
}
|
|
2720
|
+
if (pathname === '/' || pathname === '') {
|
|
2721
|
+
sendRedirect(res, 302, '/auth/login', { 'Set-Cookie': getWebAuthClearCookie() });
|
|
2722
|
+
return;
|
|
2723
|
+
}
|
|
2628
2724
|
sendHtml(
|
|
2629
2725
|
res,
|
|
2630
2726
|
401,
|
|
@@ -2648,14 +2744,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
2648
2744
|
match: currentPath => currentPath === '/api/config' ? [] : null,
|
|
2649
2745
|
handler: async () => {
|
|
2650
2746
|
const snapshot = readWebConfigSnapshot(state.webConfigPath);
|
|
2651
|
-
|
|
2652
|
-
sendJson(res, 200, {
|
|
2653
|
-
path: snapshot.path,
|
|
2654
|
-
raw: snapshot.raw,
|
|
2655
|
-
parsed: snapshot.parseError ? null : snapshot.parsed,
|
|
2656
|
-
parseError: snapshot.parseError,
|
|
2657
|
-
defaults
|
|
2658
|
-
});
|
|
2747
|
+
sendJson(res, 200, buildSafeWebConfigSnapshot(snapshot, ctx));
|
|
2659
2748
|
}
|
|
2660
2749
|
},
|
|
2661
2750
|
{
|