@xcanwin/manyoyo 5.8.2 → 5.8.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/lib/web/frontend/app.css +13 -2
- package/lib/web/frontend/app.html +2 -1
- package/lib/web/frontend/app.js +73 -19
- package/lib/web/server.js +353 -9
- package/package.json +2 -2
package/lib/web/frontend/app.css
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
--line-strong: #b59263;
|
|
16
16
|
--line-soft: rgba(181, 146, 99, 0.26);
|
|
17
17
|
--tree-guide: rgba(181, 146, 99, 0.44);
|
|
18
|
-
--tree-guide-soft: rgba(181, 146, 99, 0.
|
|
18
|
+
--tree-guide-soft: rgba(181, 146, 99, 0.44);
|
|
19
19
|
|
|
20
20
|
--text: #1f1a14;
|
|
21
21
|
--muted: #6a5f52;
|
|
@@ -341,6 +341,17 @@ textarea:focus-visible {
|
|
|
341
341
|
white-space: pre-wrap;
|
|
342
342
|
}
|
|
343
343
|
|
|
344
|
+
.modal-success {
|
|
345
|
+
margin-top: 10px;
|
|
346
|
+
color: #196c43;
|
|
347
|
+
font-size: 13px;
|
|
348
|
+
padding: 8px 10px;
|
|
349
|
+
border: 1px solid #9fd1b4;
|
|
350
|
+
border-radius: 8px;
|
|
351
|
+
background: #eef9f1;
|
|
352
|
+
white-space: pre-wrap;
|
|
353
|
+
}
|
|
354
|
+
|
|
344
355
|
.config-editor {
|
|
345
356
|
width: 100%;
|
|
346
357
|
min-height: 340px;
|
|
@@ -719,7 +730,7 @@ button.tree-toggle:active {
|
|
|
719
730
|
width: 10px;
|
|
720
731
|
height: 1px;
|
|
721
732
|
background: var(--tree-guide-soft);
|
|
722
|
-
transform: translateY(
|
|
733
|
+
transform: translateY(0);
|
|
723
734
|
}
|
|
724
735
|
|
|
725
736
|
.disclosure-toggle {
|
|
@@ -136,11 +136,12 @@
|
|
|
136
136
|
<div class="modal-body">
|
|
137
137
|
<div id="configPath" class="modal-tip"></div>
|
|
138
138
|
<textarea id="configEditor" class="config-editor" spellcheck="false"></textarea>
|
|
139
|
+
<div id="configStatus" class="modal-success" hidden></div>
|
|
139
140
|
<div id="configError" class="modal-error" hidden></div>
|
|
140
141
|
</div>
|
|
141
142
|
<footer class="modal-footer">
|
|
142
143
|
<button type="button" id="configReloadBtn" class="secondary">重新加载</button>
|
|
143
|
-
<button type="button" id="configSaveBtn"
|
|
144
|
+
<button type="button" id="configSaveBtn">保存</button>
|
|
144
145
|
</footer>
|
|
145
146
|
</section>
|
|
146
147
|
</div>
|
package/lib/web/frontend/app.js
CHANGED
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
agentTemplateModalOpen: false,
|
|
54
54
|
configLoading: false,
|
|
55
55
|
configSaving: false,
|
|
56
|
+
configSaveMessage: '',
|
|
56
57
|
createLoading: false,
|
|
57
58
|
createSubmitting: false,
|
|
58
59
|
agentTemplateSaving: false,
|
|
@@ -127,6 +128,7 @@
|
|
|
127
128
|
const configModalTitle = document.getElementById('configModalTitle');
|
|
128
129
|
const configPath = document.getElementById('configPath');
|
|
129
130
|
const configEditor = document.getElementById('configEditor');
|
|
131
|
+
const configStatus = document.getElementById('configStatus');
|
|
130
132
|
const configError = document.getElementById('configError');
|
|
131
133
|
const configReloadBtn = document.getElementById('configReloadBtn');
|
|
132
134
|
const configSaveBtn = document.getElementById('configSaveBtn');
|
|
@@ -726,6 +728,18 @@
|
|
|
726
728
|
configError.textContent = text;
|
|
727
729
|
}
|
|
728
730
|
|
|
731
|
+
function showConfigStatus(message) {
|
|
732
|
+
if (!configStatus) return;
|
|
733
|
+
const text = String(message || '').trim();
|
|
734
|
+
if (!text) {
|
|
735
|
+
configStatus.hidden = true;
|
|
736
|
+
configStatus.textContent = '';
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
configStatus.hidden = false;
|
|
740
|
+
configStatus.textContent = text;
|
|
741
|
+
}
|
|
742
|
+
|
|
729
743
|
function showDirectoryPickerError(message) {
|
|
730
744
|
if (!directoryPickerError) return;
|
|
731
745
|
const text = String(message || '').trim();
|
|
@@ -2032,7 +2046,12 @@
|
|
|
2032
2046
|
openConfigBtn.disabled = state.configLoading || state.configSaving;
|
|
2033
2047
|
}
|
|
2034
2048
|
if (configSaveBtn) {
|
|
2035
|
-
configSaveBtn.disabled =
|
|
2049
|
+
configSaveBtn.disabled = state.configLoading
|
|
2050
|
+
|| state.configSaving
|
|
2051
|
+
|| !state.configModalOpen
|
|
2052
|
+
|| !state.configSnapshot
|
|
2053
|
+
|| state.configSnapshot.editable === false;
|
|
2054
|
+
configSaveBtn.textContent = state.configSaving ? '保存中...' : '保存';
|
|
2036
2055
|
}
|
|
2037
2056
|
if (configReloadBtn) {
|
|
2038
2057
|
configReloadBtn.disabled = state.configLoading || state.configSaving;
|
|
@@ -2224,30 +2243,33 @@
|
|
|
2224
2243
|
return snapshot;
|
|
2225
2244
|
}
|
|
2226
2245
|
|
|
2246
|
+
function renderConfigModalSnapshot(config) {
|
|
2247
|
+
if (configModalTitle) {
|
|
2248
|
+
configModalTitle.textContent = '编辑配置 (~/.manyoyo/manyoyo.json)';
|
|
2249
|
+
}
|
|
2250
|
+
if (configPath) {
|
|
2251
|
+
const lines = [config.path || ''];
|
|
2252
|
+
if (config.notice) {
|
|
2253
|
+
lines.push(config.notice);
|
|
2254
|
+
}
|
|
2255
|
+
configPath.textContent = lines.filter(Boolean).join('\n');
|
|
2256
|
+
}
|
|
2257
|
+
if (configEditor) {
|
|
2258
|
+
configEditor.readOnly = config.editable === false;
|
|
2259
|
+
configEditor.value = typeof config.raw === 'string' ? config.raw : '';
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2227
2263
|
async function openConfigModal() {
|
|
2228
2264
|
closeCreateModal();
|
|
2229
2265
|
state.configLoading = true;
|
|
2266
|
+
state.configSaveMessage = '';
|
|
2230
2267
|
showConfigError('');
|
|
2268
|
+
showConfigStatus('');
|
|
2231
2269
|
syncUi();
|
|
2232
2270
|
try {
|
|
2233
2271
|
const config = await fetchConfigSnapshot();
|
|
2234
|
-
|
|
2235
|
-
configModalTitle.textContent = '查看配置摘要 (~/.manyoyo/manyoyo.json)';
|
|
2236
|
-
}
|
|
2237
|
-
if (configPath) {
|
|
2238
|
-
const lines = [config.path || ''];
|
|
2239
|
-
if (config.notice) {
|
|
2240
|
-
lines.push(config.notice);
|
|
2241
|
-
}
|
|
2242
|
-
configPath.textContent = lines.filter(Boolean).join('\n');
|
|
2243
|
-
}
|
|
2244
|
-
if (configEditor) {
|
|
2245
|
-
configEditor.readOnly = true;
|
|
2246
|
-
configEditor.value = stringifyPrettyJson({
|
|
2247
|
-
defaults: config.defaults || {},
|
|
2248
|
-
runs: config.parsed && config.parsed.runs ? config.parsed.runs : {}
|
|
2249
|
-
});
|
|
2250
|
-
}
|
|
2272
|
+
renderConfigModalSnapshot(config);
|
|
2251
2273
|
if (config.parseError) {
|
|
2252
2274
|
showConfigError('当前文件存在解析错误:' + config.parseError);
|
|
2253
2275
|
}
|
|
@@ -2267,7 +2289,29 @@
|
|
|
2267
2289
|
}
|
|
2268
2290
|
|
|
2269
2291
|
async function saveConfig() {
|
|
2270
|
-
|
|
2292
|
+
if (!configEditor || !state.configSnapshot || state.configSnapshot.editable === false) {
|
|
2293
|
+
return;
|
|
2294
|
+
}
|
|
2295
|
+
state.configSaving = true;
|
|
2296
|
+
state.configSaveMessage = '';
|
|
2297
|
+
showConfigError('');
|
|
2298
|
+
showConfigStatus('');
|
|
2299
|
+
syncUi();
|
|
2300
|
+
try {
|
|
2301
|
+
await api('/api/config', {
|
|
2302
|
+
method: 'PUT',
|
|
2303
|
+
body: JSON.stringify({ raw: configEditor.value || '' })
|
|
2304
|
+
});
|
|
2305
|
+
const config = await fetchConfigSnapshot();
|
|
2306
|
+
renderConfigModalSnapshot(config);
|
|
2307
|
+
state.configSaveMessage = '已保存到 ~/.manyoyo/manyoyo.json';
|
|
2308
|
+
showConfigStatus(state.configSaveMessage);
|
|
2309
|
+
} catch (e) {
|
|
2310
|
+
showConfigError(e.message);
|
|
2311
|
+
} finally {
|
|
2312
|
+
state.configSaving = false;
|
|
2313
|
+
syncUi();
|
|
2314
|
+
}
|
|
2271
2315
|
}
|
|
2272
2316
|
|
|
2273
2317
|
async function openCreateModal() {
|
|
@@ -3562,6 +3606,16 @@
|
|
|
3562
3606
|
});
|
|
3563
3607
|
}
|
|
3564
3608
|
|
|
3609
|
+
if (configEditor) {
|
|
3610
|
+
configEditor.addEventListener('input', function () {
|
|
3611
|
+
if (!state.configSaveMessage) {
|
|
3612
|
+
return;
|
|
3613
|
+
}
|
|
3614
|
+
state.configSaveMessage = '';
|
|
3615
|
+
showConfigStatus('');
|
|
3616
|
+
});
|
|
3617
|
+
}
|
|
3618
|
+
|
|
3565
3619
|
if (createCancelBtn) {
|
|
3566
3620
|
createCancelBtn.addEventListener('click', function () {
|
|
3567
3621
|
closeCreateModal();
|
package/lib/web/server.js
CHANGED
|
@@ -32,6 +32,7 @@ const WEB_AUTH_TTL_SECONDS = 12 * 60 * 60;
|
|
|
32
32
|
const WEB_SESSION_KEY_SEPARATOR = '~';
|
|
33
33
|
const WEB_DEFAULT_AGENT_ID = 'default';
|
|
34
34
|
const WEB_DEFAULT_AGENT_NAME = 'AGENT 1';
|
|
35
|
+
const WEB_CONFIG_KEEP_SECRET_PLACEHOLDER = '***HIDDEN_SECRET***';
|
|
35
36
|
const FRONTEND_DIR = path.join(__dirname, 'frontend');
|
|
36
37
|
const SAFE_CONTAINER_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
|
|
37
38
|
const IMAGE_VERSION_TAG_PATTERN = /^(\d+\.\d+\.\d+)-([A-Za-z0-9][A-Za-z0-9_.-]*)$/;
|
|
@@ -1635,6 +1636,334 @@ function isSensitiveConfigKey(key) {
|
|
|
1635
1636
|
return Boolean(normalized) && SENSITIVE_CONFIG_KEY_PATTERN.test(normalized);
|
|
1636
1637
|
}
|
|
1637
1638
|
|
|
1639
|
+
function readConfigQuotedString(text, startIndex) {
|
|
1640
|
+
const quote = text[startIndex];
|
|
1641
|
+
let value = '';
|
|
1642
|
+
|
|
1643
|
+
for (let i = startIndex + 1; i < text.length; i += 1) {
|
|
1644
|
+
const ch = text[i];
|
|
1645
|
+
if (ch === '\\') {
|
|
1646
|
+
value += ch;
|
|
1647
|
+
if (i + 1 < text.length) {
|
|
1648
|
+
value += text[i + 1];
|
|
1649
|
+
i += 1;
|
|
1650
|
+
}
|
|
1651
|
+
continue;
|
|
1652
|
+
}
|
|
1653
|
+
if (ch === quote) {
|
|
1654
|
+
return {
|
|
1655
|
+
value,
|
|
1656
|
+
end: i + 1
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
value += ch;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
return null;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
function isConfigIdentifierStart(ch) {
|
|
1666
|
+
return /[A-Za-z_$]/.test(ch);
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
function isConfigIdentifierPart(ch) {
|
|
1670
|
+
return /[A-Za-z0-9_$]/.test(ch);
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
function skipConfigTrivia(text, index) {
|
|
1674
|
+
let cursor = index;
|
|
1675
|
+
while (cursor < text.length) {
|
|
1676
|
+
const ch = text[cursor];
|
|
1677
|
+
const next = text[cursor + 1];
|
|
1678
|
+
if (/\s/.test(ch)) {
|
|
1679
|
+
cursor += 1;
|
|
1680
|
+
continue;
|
|
1681
|
+
}
|
|
1682
|
+
if (ch === '/' && next === '/') {
|
|
1683
|
+
cursor += 2;
|
|
1684
|
+
while (cursor < text.length && text[cursor] !== '\n') {
|
|
1685
|
+
cursor += 1;
|
|
1686
|
+
}
|
|
1687
|
+
continue;
|
|
1688
|
+
}
|
|
1689
|
+
if (ch === '/' && next === '*') {
|
|
1690
|
+
cursor += 2;
|
|
1691
|
+
while (cursor + 1 < text.length && !(text[cursor] === '*' && text[cursor + 1] === '/')) {
|
|
1692
|
+
cursor += 1;
|
|
1693
|
+
}
|
|
1694
|
+
cursor = cursor + 1 < text.length ? cursor + 2 : text.length;
|
|
1695
|
+
continue;
|
|
1696
|
+
}
|
|
1697
|
+
break;
|
|
1698
|
+
}
|
|
1699
|
+
return cursor;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
function scanConfigValueEnd(text, startIndex) {
|
|
1703
|
+
let cursor = startIndex;
|
|
1704
|
+
let stringQuote = '';
|
|
1705
|
+
let lineComment = false;
|
|
1706
|
+
let blockComment = false;
|
|
1707
|
+
let depth = 0;
|
|
1708
|
+
|
|
1709
|
+
for (; cursor < text.length; cursor += 1) {
|
|
1710
|
+
const ch = text[cursor];
|
|
1711
|
+
const next = text[cursor + 1];
|
|
1712
|
+
|
|
1713
|
+
if (lineComment) {
|
|
1714
|
+
if (ch === '\n') {
|
|
1715
|
+
lineComment = false;
|
|
1716
|
+
}
|
|
1717
|
+
continue;
|
|
1718
|
+
}
|
|
1719
|
+
if (blockComment) {
|
|
1720
|
+
if (ch === '*' && next === '/') {
|
|
1721
|
+
blockComment = false;
|
|
1722
|
+
cursor += 1;
|
|
1723
|
+
}
|
|
1724
|
+
continue;
|
|
1725
|
+
}
|
|
1726
|
+
if (stringQuote) {
|
|
1727
|
+
if (ch === '\\') {
|
|
1728
|
+
cursor += 1;
|
|
1729
|
+
continue;
|
|
1730
|
+
}
|
|
1731
|
+
if (ch === stringQuote) {
|
|
1732
|
+
stringQuote = '';
|
|
1733
|
+
}
|
|
1734
|
+
continue;
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
if (ch === '/' && next === '/') {
|
|
1738
|
+
lineComment = true;
|
|
1739
|
+
cursor += 1;
|
|
1740
|
+
continue;
|
|
1741
|
+
}
|
|
1742
|
+
if (ch === '/' && next === '*') {
|
|
1743
|
+
blockComment = true;
|
|
1744
|
+
cursor += 1;
|
|
1745
|
+
continue;
|
|
1746
|
+
}
|
|
1747
|
+
if (ch === '"' || ch === '\'') {
|
|
1748
|
+
stringQuote = ch;
|
|
1749
|
+
continue;
|
|
1750
|
+
}
|
|
1751
|
+
if (ch === '{' || ch === '[' || ch === '(') {
|
|
1752
|
+
depth += 1;
|
|
1753
|
+
continue;
|
|
1754
|
+
}
|
|
1755
|
+
if (ch === '}' || ch === ']' || ch === ')') {
|
|
1756
|
+
if (depth === 0) {
|
|
1757
|
+
break;
|
|
1758
|
+
}
|
|
1759
|
+
depth -= 1;
|
|
1760
|
+
continue;
|
|
1761
|
+
}
|
|
1762
|
+
if (depth === 0 && ch === ',') {
|
|
1763
|
+
break;
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
let end = cursor;
|
|
1768
|
+
while (end > startIndex && /\s/.test(text[end - 1])) {
|
|
1769
|
+
end -= 1;
|
|
1770
|
+
}
|
|
1771
|
+
return end;
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
function findConfigRootObjectStart(text) {
|
|
1775
|
+
const start = skipConfigTrivia(String(text || ''), 0);
|
|
1776
|
+
return text[start] === '{' ? start : -1;
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
function readConfigPropertyToken(text, startIndex) {
|
|
1780
|
+
const ch = text[startIndex];
|
|
1781
|
+
if (ch === '"' || ch === '\'') {
|
|
1782
|
+
const token = readConfigQuotedString(text, startIndex);
|
|
1783
|
+
if (!token) {
|
|
1784
|
+
return null;
|
|
1785
|
+
}
|
|
1786
|
+
return token;
|
|
1787
|
+
}
|
|
1788
|
+
if (!isConfigIdentifierStart(ch)) {
|
|
1789
|
+
return null;
|
|
1790
|
+
}
|
|
1791
|
+
let end = startIndex + 1;
|
|
1792
|
+
while (end < text.length && isConfigIdentifierPart(text[end])) {
|
|
1793
|
+
end += 1;
|
|
1794
|
+
}
|
|
1795
|
+
return {
|
|
1796
|
+
value: text.slice(startIndex, end),
|
|
1797
|
+
end
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
function findConfigObjectPropertyValueRange(text, objectStartIndex, propertyName) {
|
|
1802
|
+
let cursor = skipConfigTrivia(text, objectStartIndex + 1);
|
|
1803
|
+
while (cursor < text.length) {
|
|
1804
|
+
cursor = skipConfigTrivia(text, cursor);
|
|
1805
|
+
if (text[cursor] === '}') {
|
|
1806
|
+
return null;
|
|
1807
|
+
}
|
|
1808
|
+
const token = readConfigPropertyToken(text, cursor);
|
|
1809
|
+
if (!token) {
|
|
1810
|
+
return null;
|
|
1811
|
+
}
|
|
1812
|
+
cursor = skipConfigTrivia(text, token.end);
|
|
1813
|
+
if (text[cursor] !== ':') {
|
|
1814
|
+
return null;
|
|
1815
|
+
}
|
|
1816
|
+
const valueStart = skipConfigTrivia(text, cursor + 1);
|
|
1817
|
+
const valueEnd = scanConfigValueEnd(text, valueStart);
|
|
1818
|
+
if (token.value === propertyName) {
|
|
1819
|
+
return { start: valueStart, end: valueEnd };
|
|
1820
|
+
}
|
|
1821
|
+
cursor = skipConfigTrivia(text, valueEnd);
|
|
1822
|
+
if (text[cursor] === ',') {
|
|
1823
|
+
cursor += 1;
|
|
1824
|
+
continue;
|
|
1825
|
+
}
|
|
1826
|
+
if (text[cursor] === '}') {
|
|
1827
|
+
return null;
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
return null;
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
function findConfigValueRangeByPath(text, pathParts) {
|
|
1834
|
+
if (!Array.isArray(pathParts) || pathParts.length === 0) {
|
|
1835
|
+
return null;
|
|
1836
|
+
}
|
|
1837
|
+
let objectStart = findConfigRootObjectStart(text);
|
|
1838
|
+
if (objectStart === -1) {
|
|
1839
|
+
return null;
|
|
1840
|
+
}
|
|
1841
|
+
let range = null;
|
|
1842
|
+
for (let i = 0; i < pathParts.length; i += 1) {
|
|
1843
|
+
range = findConfigObjectPropertyValueRange(text, objectStart, pathParts[i]);
|
|
1844
|
+
if (!range) {
|
|
1845
|
+
return null;
|
|
1846
|
+
}
|
|
1847
|
+
if (i === pathParts.length - 1) {
|
|
1848
|
+
return range;
|
|
1849
|
+
}
|
|
1850
|
+
const nextObjectStart = skipConfigTrivia(text, range.start);
|
|
1851
|
+
if (text[nextObjectStart] !== '{') {
|
|
1852
|
+
return null;
|
|
1853
|
+
}
|
|
1854
|
+
objectStart = nextObjectStart;
|
|
1855
|
+
}
|
|
1856
|
+
return range;
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
function collectSensitiveConfigPaths(value, pathParts = []) {
|
|
1860
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1861
|
+
return [];
|
|
1862
|
+
}
|
|
1863
|
+
const result = [];
|
|
1864
|
+
Object.entries(toPlainObject(value)).forEach(([key, item]) => {
|
|
1865
|
+
const nextPath = pathParts.concat(key);
|
|
1866
|
+
if (isSensitiveConfigKey(key)) {
|
|
1867
|
+
result.push(nextPath);
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1870
|
+
if (item && typeof item === 'object' && !Array.isArray(item)) {
|
|
1871
|
+
result.push(...collectSensitiveConfigPaths(item, nextPath));
|
|
1872
|
+
}
|
|
1873
|
+
});
|
|
1874
|
+
return result;
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
function collectSensitivePlaceholderPaths(value, pathParts = []) {
|
|
1878
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1879
|
+
return [];
|
|
1880
|
+
}
|
|
1881
|
+
const result = [];
|
|
1882
|
+
Object.entries(toPlainObject(value)).forEach(([key, item]) => {
|
|
1883
|
+
const nextPath = pathParts.concat(key);
|
|
1884
|
+
if (isSensitiveConfigKey(key)) {
|
|
1885
|
+
if (item === WEB_CONFIG_KEEP_SECRET_PLACEHOLDER) {
|
|
1886
|
+
result.push(nextPath);
|
|
1887
|
+
}
|
|
1888
|
+
return;
|
|
1889
|
+
}
|
|
1890
|
+
if (item && typeof item === 'object' && !Array.isArray(item)) {
|
|
1891
|
+
result.push(...collectSensitivePlaceholderPaths(item, nextPath));
|
|
1892
|
+
}
|
|
1893
|
+
});
|
|
1894
|
+
return result;
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
function applyConfigTextReplacements(text, replacements) {
|
|
1898
|
+
return replacements
|
|
1899
|
+
.slice()
|
|
1900
|
+
.sort((a, b) => b.start - a.start)
|
|
1901
|
+
.reduce((result, item) => `${result.slice(0, item.start)}${item.text}${result.slice(item.end)}`, text);
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
function buildConfigPathLabel(pathParts) {
|
|
1905
|
+
return (Array.isArray(pathParts) ? pathParts : []).join('.');
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
function maskWebConfigRaw(raw, parsed) {
|
|
1909
|
+
const text = String(raw || '');
|
|
1910
|
+
const replacements = collectSensitiveConfigPaths(parsed).map(pathParts => {
|
|
1911
|
+
const range = findConfigValueRangeByPath(text, pathParts);
|
|
1912
|
+
if (!range) {
|
|
1913
|
+
throw new Error(`敏感字段定位失败: ${buildConfigPathLabel(pathParts)}`);
|
|
1914
|
+
}
|
|
1915
|
+
return {
|
|
1916
|
+
start: range.start,
|
|
1917
|
+
end: range.end,
|
|
1918
|
+
text: JSON.stringify(WEB_CONFIG_KEEP_SECRET_PLACEHOLDER)
|
|
1919
|
+
};
|
|
1920
|
+
});
|
|
1921
|
+
return applyConfigTextReplacements(text, replacements);
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
function parseConfigRawObject(raw) {
|
|
1925
|
+
const parsed = JSON5.parse(String(raw || ''));
|
|
1926
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
1927
|
+
throw new Error('配置根节点必须是对象(map)');
|
|
1928
|
+
}
|
|
1929
|
+
return toPlainObject(parsed);
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
function restoreWebConfigSecrets(raw, snapshot) {
|
|
1933
|
+
const text = String(raw || '');
|
|
1934
|
+
if (!text.includes(WEB_CONFIG_KEEP_SECRET_PLACEHOLDER)) {
|
|
1935
|
+
return text;
|
|
1936
|
+
}
|
|
1937
|
+
if (!snapshot || snapshot.parseError) {
|
|
1938
|
+
throw new Error('当前配置存在解析错误,无法回填敏感值');
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
const editedConfig = parseConfigRawObject(text);
|
|
1942
|
+
const placeholderPaths = collectSensitivePlaceholderPaths(editedConfig);
|
|
1943
|
+
if (!placeholderPaths.length) {
|
|
1944
|
+
return text;
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
const currentRaw = String(snapshot.raw || '');
|
|
1948
|
+
const replacements = placeholderPaths.map(pathParts => {
|
|
1949
|
+
const editedRange = findConfigValueRangeByPath(text, pathParts);
|
|
1950
|
+
const currentRange = findConfigValueRangeByPath(currentRaw, pathParts);
|
|
1951
|
+
if (!editedRange) {
|
|
1952
|
+
throw new Error(`敏感字段定位失败: ${buildConfigPathLabel(pathParts)}`);
|
|
1953
|
+
}
|
|
1954
|
+
if (!currentRange) {
|
|
1955
|
+
throw new Error(`敏感字段缺少可保留的旧值: ${buildConfigPathLabel(pathParts)}`);
|
|
1956
|
+
}
|
|
1957
|
+
return {
|
|
1958
|
+
start: editedRange.start,
|
|
1959
|
+
end: editedRange.end,
|
|
1960
|
+
text: currentRaw.slice(currentRange.start, currentRange.end)
|
|
1961
|
+
};
|
|
1962
|
+
});
|
|
1963
|
+
|
|
1964
|
+
return applyConfigTextReplacements(text, replacements);
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1638
1967
|
function redactConfigValue(value) {
|
|
1639
1968
|
if (Array.isArray(value)) {
|
|
1640
1969
|
return value.map(item => redactConfigValue(item));
|
|
@@ -1673,13 +2002,29 @@ function redactConfigObject(value) {
|
|
|
1673
2002
|
|
|
1674
2003
|
function buildSafeWebConfigSnapshot(snapshot, ctx) {
|
|
1675
2004
|
const parsed = snapshot && snapshot.parseError ? {} : toPlainObject(snapshot && snapshot.parsed);
|
|
2005
|
+
let raw = '';
|
|
2006
|
+
let editable = false;
|
|
2007
|
+
let notice = 'Web 端显示原文 JSON5;敏感值以 ***HIDDEN_SECRET*** 占位,保存时会保留原值。';
|
|
2008
|
+
if (snapshot && !snapshot.parseError) {
|
|
2009
|
+
try {
|
|
2010
|
+
raw = maskWebConfigRaw(snapshot.raw || '', parsed);
|
|
2011
|
+
editable = true;
|
|
2012
|
+
} catch (e) {
|
|
2013
|
+
raw = '';
|
|
2014
|
+
editable = false;
|
|
2015
|
+
notice = '当前配置无法安全脱敏显示,请在本地 manyoyo.json 中维护。';
|
|
2016
|
+
}
|
|
2017
|
+
} else if (snapshot && snapshot.parseError) {
|
|
2018
|
+
notice = '当前配置解析失败,Web 端暂不提供安全编辑;请先在本地修复 manyoyo.json。';
|
|
2019
|
+
}
|
|
1676
2020
|
return {
|
|
1677
2021
|
path: snapshot && snapshot.path ? snapshot.path : path.resolve(getDefaultWebConfigPath()),
|
|
2022
|
+
raw,
|
|
1678
2023
|
parsed: redactConfigObject(parsed),
|
|
1679
2024
|
defaults: redactConfigObject(buildConfigDefaults(ctx, parsed)),
|
|
1680
2025
|
parseError: snapshot && snapshot.parseError ? snapshot.parseError : null,
|
|
1681
|
-
editable
|
|
1682
|
-
notice
|
|
2026
|
+
editable,
|
|
2027
|
+
notice
|
|
1683
2028
|
};
|
|
1684
2029
|
}
|
|
1685
2030
|
|
|
@@ -1995,11 +2340,7 @@ function readWebConfigSnapshot(configPath) {
|
|
|
1995
2340
|
}
|
|
1996
2341
|
|
|
1997
2342
|
function parseAndValidateConfigRaw(raw) {
|
|
1998
|
-
const
|
|
1999
|
-
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
2000
|
-
throw new Error('配置根节点必须是对象(map)');
|
|
2001
|
-
}
|
|
2002
|
-
const config = toPlainObject(parsed);
|
|
2343
|
+
const config = parseConfigRawObject(raw);
|
|
2003
2344
|
validateWebConfigShape(config);
|
|
2004
2345
|
return config;
|
|
2005
2346
|
}
|
|
@@ -3223,9 +3564,12 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
3223
3564
|
return;
|
|
3224
3565
|
}
|
|
3225
3566
|
|
|
3567
|
+
const currentSnapshot = readWebConfigSnapshot(state.webConfigPath);
|
|
3568
|
+
let finalRaw = raw;
|
|
3226
3569
|
let parsed = null;
|
|
3227
3570
|
try {
|
|
3228
|
-
|
|
3571
|
+
finalRaw = restoreWebConfigSecrets(raw, currentSnapshot);
|
|
3572
|
+
parsed = parseAndValidateConfigRaw(finalRaw);
|
|
3229
3573
|
} catch (e) {
|
|
3230
3574
|
sendJson(res, 400, { error: '配置格式错误', detail: e.message || '解析失败' });
|
|
3231
3575
|
return;
|
|
@@ -3233,7 +3577,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
3233
3577
|
|
|
3234
3578
|
const savePath = path.resolve(state.webConfigPath);
|
|
3235
3579
|
fs.mkdirSync(path.dirname(savePath), { recursive: true });
|
|
3236
|
-
fs.writeFileSync(savePath,
|
|
3580
|
+
fs.writeFileSync(savePath, finalRaw, 'utf-8');
|
|
3237
3581
|
|
|
3238
3582
|
sendJson(res, 200, {
|
|
3239
3583
|
saved: true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xcanwin/manyoyo",
|
|
3
|
-
"version": "5.8.
|
|
3
|
+
"version": "5.8.5",
|
|
4
4
|
"imageVersion": "1.9.0-common",
|
|
5
5
|
"playwrightCliVersion": "0.1.1",
|
|
6
6
|
"description": "AI Agent CLI Security Sandbox for Docker and Podman",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"glob": "^13.0.6",
|
|
75
75
|
"minimatch": "^10.2.2",
|
|
76
76
|
"test-exclude": "^8.0.0",
|
|
77
|
-
"vite": "^6.4.
|
|
77
|
+
"vite": "^6.4.2"
|
|
78
78
|
},
|
|
79
79
|
"jest": {
|
|
80
80
|
"testMatch": [
|