@xcanwin/manyoyo 6.2.4 → 6.2.6
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 +94 -137
- package/lib/web/frontend/app.html +28 -1
- package/lib/web/frontend/app.js +498 -401
- package/lib/web/server.js +290 -1
- package/package.json +1 -1
package/lib/web/server.js
CHANGED
|
@@ -157,11 +157,21 @@ function normalizeWebAgentName(agentId, agentName) {
|
|
|
157
157
|
return String(agentId || '').trim() || WEB_DEFAULT_AGENT_NAME;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
const WEB_REMARK_MAX_LENGTH = 200;
|
|
161
|
+
|
|
162
|
+
function normalizeRemarkText(value) {
|
|
163
|
+
if (typeof value !== 'string') {
|
|
164
|
+
return '';
|
|
165
|
+
}
|
|
166
|
+
return value.trim().slice(0, WEB_REMARK_MAX_LENGTH);
|
|
167
|
+
}
|
|
168
|
+
|
|
160
169
|
function createEmptyWebAgentSession(agentId, agentName) {
|
|
161
170
|
return {
|
|
162
171
|
agentId,
|
|
163
172
|
agentName: normalizeWebAgentName(agentId, agentName),
|
|
164
173
|
agentPromptCommand: '',
|
|
174
|
+
remark: '',
|
|
165
175
|
createdAt: null,
|
|
166
176
|
updatedAt: null,
|
|
167
177
|
messages: [],
|
|
@@ -181,6 +191,7 @@ function normalizeWebAgentSessionRecord(agentId, rawAgent) {
|
|
|
181
191
|
agentPromptCommand: typeof source.agentPromptCommand === 'string'
|
|
182
192
|
? normalizeAgentPromptCommandTemplate(source.agentPromptCommand, `agents.${agentId}.agentPromptCommand`)
|
|
183
193
|
: '',
|
|
194
|
+
remark: normalizeRemarkText(source.remark),
|
|
184
195
|
createdAt: typeof source.createdAt === 'string' ? source.createdAt : null,
|
|
185
196
|
updatedAt: typeof source.updatedAt === 'string' ? source.updatedAt : null,
|
|
186
197
|
messages: Array.isArray(source.messages) ? source.messages : [],
|
|
@@ -244,6 +255,9 @@ function normalizeWebHistoryRecord(containerName, rawData) {
|
|
|
244
255
|
const applied = data.applied && typeof data.applied === 'object' && !Array.isArray(data.applied)
|
|
245
256
|
? data.applied
|
|
246
257
|
: null;
|
|
258
|
+
const runtimeSnapshot = data.runtimeSnapshot && typeof data.runtimeSnapshot === 'object' && !Array.isArray(data.runtimeSnapshot)
|
|
259
|
+
? data.runtimeSnapshot
|
|
260
|
+
: null;
|
|
247
261
|
const history = {
|
|
248
262
|
containerName,
|
|
249
263
|
updatedAt: typeof data.updatedAt === 'string' ? data.updatedAt : null,
|
|
@@ -251,7 +265,10 @@ function normalizeWebHistoryRecord(containerName, rawData) {
|
|
|
251
265
|
typeof data.agentPromptCommand === 'string' ? data.agentPromptCommand : '',
|
|
252
266
|
applied
|
|
253
267
|
),
|
|
268
|
+
remark: normalizeRemarkText(data.remark),
|
|
254
269
|
applied,
|
|
270
|
+
// 服务端内部字段,绝不能被 buildSessionSummary/buildSessionDetail/buildSessionAudit 引用或透传给前端。
|
|
271
|
+
runtimeSnapshot,
|
|
255
272
|
agents: {}
|
|
256
273
|
};
|
|
257
274
|
|
|
@@ -2547,6 +2564,24 @@ function buildCreateRuntime(ctx, state, payload) {
|
|
|
2547
2564
|
containerPorts,
|
|
2548
2565
|
agentPromptCommand,
|
|
2549
2566
|
defaultCommand: buildDefaultCommand(shellPrefix, shell, shellSuffix) || '/bin/bash',
|
|
2567
|
+
// 仅供服务端内部复用(如"创建相同配置容器"/"复制容器"),绝不能透传给任何对外响应字段,
|
|
2568
|
+
// 因为 env/volumes/ports 可能包含密钥原始值(对外响应向来只暴露 envCount 等计数)。
|
|
2569
|
+
runtimeSnapshot: {
|
|
2570
|
+
hostPath,
|
|
2571
|
+
containerPath,
|
|
2572
|
+
imageName,
|
|
2573
|
+
imageVersion,
|
|
2574
|
+
containerMode,
|
|
2575
|
+
shellPrefix: shellPrefix || '',
|
|
2576
|
+
shell: shell || '',
|
|
2577
|
+
shellSuffix: shellSuffix || '',
|
|
2578
|
+
agentPromptCommand: configuredAgentPromptCommand,
|
|
2579
|
+
yolo: yolo || '',
|
|
2580
|
+
env: resolvedBase.env || {},
|
|
2581
|
+
envFile: resolvedBase.envFile || [],
|
|
2582
|
+
volumes: resolvedBase.volumes || [],
|
|
2583
|
+
ports: resolvedBase.ports || []
|
|
2584
|
+
},
|
|
2550
2585
|
applied: {
|
|
2551
2586
|
containerName,
|
|
2552
2587
|
hostPath,
|
|
@@ -2569,6 +2604,138 @@ function buildCreateRuntime(ctx, state, payload) {
|
|
|
2569
2604
|
};
|
|
2570
2605
|
}
|
|
2571
2606
|
|
|
2607
|
+
function resolveUniqueContainerName(ctx, state, baseName) {
|
|
2608
|
+
const containerMap = listWebManyoyoContainers(ctx);
|
|
2609
|
+
const historyNames = listWebHistorySessionNames(state.webHistoryDir, ctx.isValidContainerName);
|
|
2610
|
+
const taken = new Set([...Object.keys(containerMap), ...historyNames]);
|
|
2611
|
+
// 基于历史最大编号 +1 命名,即使较小编号的副本被删除也不回收其编号,
|
|
2612
|
+
// 避免"删除 -copy1 后新副本又叫 -copy1"造成的名称语义混乱。
|
|
2613
|
+
const escapedBase = baseName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
2614
|
+
const pattern = new RegExp(`^${escapedBase}-copy(\\d+)$`);
|
|
2615
|
+
let maxN = 0;
|
|
2616
|
+
taken.forEach(name => {
|
|
2617
|
+
const matched = name.match(pattern);
|
|
2618
|
+
if (matched) {
|
|
2619
|
+
const n = Number(matched[1]);
|
|
2620
|
+
if (Number.isFinite(n) && n > maxN) {
|
|
2621
|
+
maxN = n;
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
});
|
|
2625
|
+
return `${baseName}-copy${maxN + 1}`;
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
function buildCloneCreateOptionsFromSnapshot(runtimeSnapshot, fallbackHistory) {
|
|
2629
|
+
if (runtimeSnapshot && typeof runtimeSnapshot === 'object') {
|
|
2630
|
+
return {
|
|
2631
|
+
hostPath: runtimeSnapshot.hostPath || '',
|
|
2632
|
+
containerPath: runtimeSnapshot.containerPath || '',
|
|
2633
|
+
imageName: runtimeSnapshot.imageName || '',
|
|
2634
|
+
imageVersion: runtimeSnapshot.imageVersion || '',
|
|
2635
|
+
containerMode: runtimeSnapshot.containerMode || '',
|
|
2636
|
+
shellPrefix: runtimeSnapshot.shellPrefix || '',
|
|
2637
|
+
shell: runtimeSnapshot.shell || '',
|
|
2638
|
+
shellSuffix: runtimeSnapshot.shellSuffix || '',
|
|
2639
|
+
agentPromptCommand: runtimeSnapshot.agentPromptCommand || '',
|
|
2640
|
+
yolo: runtimeSnapshot.yolo || '',
|
|
2641
|
+
env: runtimeSnapshot.env || {},
|
|
2642
|
+
envFile: runtimeSnapshot.envFile || [],
|
|
2643
|
+
volumes: runtimeSnapshot.volumes || [],
|
|
2644
|
+
ports: runtimeSnapshot.ports || []
|
|
2645
|
+
};
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
const applied = fallbackHistory && fallbackHistory.applied && typeof fallbackHistory.applied === 'object'
|
|
2649
|
+
? fallbackHistory.applied
|
|
2650
|
+
: {};
|
|
2651
|
+
return {
|
|
2652
|
+
hostPath: applied.hostPath || '',
|
|
2653
|
+
containerPath: applied.containerPath || '',
|
|
2654
|
+
imageName: applied.imageName || '',
|
|
2655
|
+
imageVersion: applied.imageVersion || '',
|
|
2656
|
+
containerMode: applied.containerMode || '',
|
|
2657
|
+
shellPrefix: applied.shellPrefix || '',
|
|
2658
|
+
shell: applied.shell || '',
|
|
2659
|
+
shellSuffix: applied.shellSuffix || '',
|
|
2660
|
+
agentPromptCommand: typeof fallbackHistory.agentPromptCommand === 'string' ? fallbackHistory.agentPromptCommand : '',
|
|
2661
|
+
yolo: applied.yolo || '',
|
|
2662
|
+
env: {},
|
|
2663
|
+
envFile: [],
|
|
2664
|
+
volumes: [],
|
|
2665
|
+
ports: []
|
|
2666
|
+
};
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
function cloneWebAgentSessionsDeep(sourceHistory) {
|
|
2670
|
+
const cloned = {};
|
|
2671
|
+
const agents = sourceHistory && sourceHistory.agents && typeof sourceHistory.agents === 'object'
|
|
2672
|
+
? sourceHistory.agents
|
|
2673
|
+
: {};
|
|
2674
|
+
Object.keys(agents).forEach(agentId => {
|
|
2675
|
+
cloned[agentId] = JSON.parse(JSON.stringify(agents[agentId]));
|
|
2676
|
+
});
|
|
2677
|
+
return cloned;
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2680
|
+
async function createClonedContainer(ctx, state, sourceContainerName, requestedName, options = {}) {
|
|
2681
|
+
const copyHistory = options.copyHistory === true;
|
|
2682
|
+
const history = loadWebSessionHistory(state.webHistoryDir, sourceContainerName);
|
|
2683
|
+
const cloneFidelity = history.runtimeSnapshot ? 'full' : 'partial';
|
|
2684
|
+
const createOptions = buildCloneCreateOptionsFromSnapshot(history.runtimeSnapshot, history);
|
|
2685
|
+
|
|
2686
|
+
let finalName = '';
|
|
2687
|
+
if (requestedName) {
|
|
2688
|
+
validateContainerNameStrict(requestedName);
|
|
2689
|
+
const containerMap = listWebManyoyoContainers(ctx);
|
|
2690
|
+
const historyNames = listWebHistorySessionNames(state.webHistoryDir, ctx.isValidContainerName);
|
|
2691
|
+
const taken = new Set([...Object.keys(containerMap), ...historyNames]);
|
|
2692
|
+
if (taken.has(requestedName)) {
|
|
2693
|
+
const err = new Error(`容器名已存在: ${requestedName}`);
|
|
2694
|
+
err.statusCode = 409;
|
|
2695
|
+
throw err;
|
|
2696
|
+
}
|
|
2697
|
+
finalName = requestedName;
|
|
2698
|
+
} else {
|
|
2699
|
+
finalName = resolveUniqueContainerName(ctx, state, sourceContainerName);
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
createOptions.containerName = finalName;
|
|
2703
|
+
const runtime = buildCreateRuntime(ctx, state, { createOptions });
|
|
2704
|
+
await ensureWebContainer(ctx, state, runtime);
|
|
2705
|
+
setWebSessionAgentPromptCommand(state.webHistoryDir, runtime.containerName, runtime.agentPromptCommand);
|
|
2706
|
+
|
|
2707
|
+
const patch = {
|
|
2708
|
+
applied: runtime.applied,
|
|
2709
|
+
runtimeSnapshot: runtime.runtimeSnapshot
|
|
2710
|
+
};
|
|
2711
|
+
if (copyHistory) {
|
|
2712
|
+
patch.agents = cloneWebAgentSessionsDeep(history);
|
|
2713
|
+
patch.agentPromptCommand = history.agentPromptCommand;
|
|
2714
|
+
}
|
|
2715
|
+
patchWebSessionHistory(state.webHistoryDir, finalName, patch);
|
|
2716
|
+
|
|
2717
|
+
return {
|
|
2718
|
+
name: finalName,
|
|
2719
|
+
applied: runtime.applied,
|
|
2720
|
+
sourceContainerName,
|
|
2721
|
+
cloneFidelity,
|
|
2722
|
+
resumeMayFail: copyHistory
|
|
2723
|
+
};
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
function resolveContainerNamesByHostPath(ctx, state, hostPath) {
|
|
2727
|
+
const target = String(hostPath || '').trim();
|
|
2728
|
+
const containerMap = listWebManyoyoContainers(ctx);
|
|
2729
|
+
const names = new Set([
|
|
2730
|
+
...Object.keys(containerMap),
|
|
2731
|
+
...listWebHistorySessionNames(state.webHistoryDir, ctx.isValidContainerName)
|
|
2732
|
+
]);
|
|
2733
|
+
return Array.from(names).filter(name => {
|
|
2734
|
+
const summary = buildSessionSummary(ctx, state, containerMap, { containerName: name, agentId: WEB_DEFAULT_AGENT_ID });
|
|
2735
|
+
return summary && String(summary.hostPath || '').trim() === target;
|
|
2736
|
+
});
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2572
2739
|
// Estimate container start time from "Up X hours/minutes/seconds" status string.
|
|
2573
2740
|
// Uses relative time to avoid Podman Machine VM clock drift issues.
|
|
2574
2741
|
function estimateStartTimeFromStatus(status) {
|
|
@@ -3283,6 +3450,8 @@ function buildSessionSummary(ctx, state, containerMap, sessionRef) {
|
|
|
3283
3450
|
containerName,
|
|
3284
3451
|
agentId,
|
|
3285
3452
|
agentName: agentSession.agentName,
|
|
3453
|
+
agentRemark: agentSession.remark || '',
|
|
3454
|
+
containerRemark: history.remark || '',
|
|
3286
3455
|
status: containerInfo.status || 'history',
|
|
3287
3456
|
image: containerInfo.image || '',
|
|
3288
3457
|
createdAt,
|
|
@@ -3924,7 +4093,8 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
3924
4093
|
await ensureWebContainer(ctx, state, runtime);
|
|
3925
4094
|
setWebSessionAgentPromptCommand(state.webHistoryDir, runtime.containerName, runtime.agentPromptCommand);
|
|
3926
4095
|
patchWebSessionHistory(state.webHistoryDir, runtime.containerName, {
|
|
3927
|
-
applied: runtime.applied
|
|
4096
|
+
applied: runtime.applied,
|
|
4097
|
+
runtimeSnapshot: runtime.runtimeSnapshot
|
|
3928
4098
|
});
|
|
3929
4099
|
sendJson(res, 200, { name: runtime.containerName, applied: runtime.applied });
|
|
3930
4100
|
}
|
|
@@ -3948,6 +4118,125 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
3948
4118
|
});
|
|
3949
4119
|
}
|
|
3950
4120
|
},
|
|
4121
|
+
{
|
|
4122
|
+
method: 'POST',
|
|
4123
|
+
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/clone-config$/),
|
|
4124
|
+
handler: async match => {
|
|
4125
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
4126
|
+
if (!sessionRef) {
|
|
4127
|
+
return;
|
|
4128
|
+
}
|
|
4129
|
+
if (sessionRef.agentId !== WEB_DEFAULT_AGENT_ID) {
|
|
4130
|
+
sendJson(res, 400, { error: '该操作需在容器层级执行' });
|
|
4131
|
+
return;
|
|
4132
|
+
}
|
|
4133
|
+
const sourceContainerName = sessionRef.containerName;
|
|
4134
|
+
const historyExists = fs.existsSync(getWebHistoryFile(state.webHistoryDir, sourceContainerName));
|
|
4135
|
+
if (!historyExists && !ctx.containerExists(sourceContainerName)) {
|
|
4136
|
+
sendJson(res, 404, { error: `容器不存在: ${sourceContainerName}` });
|
|
4137
|
+
return;
|
|
4138
|
+
}
|
|
4139
|
+
const payload = await readJsonBody(req);
|
|
4140
|
+
const requestedName = pickFirstString(payload.containerName);
|
|
4141
|
+
try {
|
|
4142
|
+
const result = await createClonedContainer(ctx, state, sourceContainerName, requestedName, { copyHistory: false });
|
|
4143
|
+
sendJson(res, 200, result);
|
|
4144
|
+
} catch (e) {
|
|
4145
|
+
sendJson(res, e.statusCode || 400, { error: e.message || '创建相同配置容器失败' });
|
|
4146
|
+
}
|
|
4147
|
+
}
|
|
4148
|
+
},
|
|
4149
|
+
{
|
|
4150
|
+
method: 'POST',
|
|
4151
|
+
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/duplicate$/),
|
|
4152
|
+
handler: async match => {
|
|
4153
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
4154
|
+
if (!sessionRef) {
|
|
4155
|
+
return;
|
|
4156
|
+
}
|
|
4157
|
+
if (sessionRef.agentId !== WEB_DEFAULT_AGENT_ID) {
|
|
4158
|
+
sendJson(res, 400, { error: '该操作需在容器层级执行' });
|
|
4159
|
+
return;
|
|
4160
|
+
}
|
|
4161
|
+
const sourceContainerName = sessionRef.containerName;
|
|
4162
|
+
const historyExists = fs.existsSync(getWebHistoryFile(state.webHistoryDir, sourceContainerName));
|
|
4163
|
+
if (!historyExists && !ctx.containerExists(sourceContainerName)) {
|
|
4164
|
+
sendJson(res, 404, { error: `容器不存在: ${sourceContainerName}` });
|
|
4165
|
+
return;
|
|
4166
|
+
}
|
|
4167
|
+
const payload = await readJsonBody(req);
|
|
4168
|
+
const requestedName = pickFirstString(payload.containerName);
|
|
4169
|
+
try {
|
|
4170
|
+
const result = await createClonedContainer(ctx, state, sourceContainerName, requestedName, { copyHistory: true });
|
|
4171
|
+
sendJson(res, 200, result);
|
|
4172
|
+
} catch (e) {
|
|
4173
|
+
sendJson(res, e.statusCode || 400, { error: e.message || '复制容器失败' });
|
|
4174
|
+
}
|
|
4175
|
+
}
|
|
4176
|
+
},
|
|
4177
|
+
{
|
|
4178
|
+
method: 'POST',
|
|
4179
|
+
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/container-remark$/),
|
|
4180
|
+
handler: async match => {
|
|
4181
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
4182
|
+
if (!sessionRef) {
|
|
4183
|
+
return;
|
|
4184
|
+
}
|
|
4185
|
+
if (sessionRef.agentId !== WEB_DEFAULT_AGENT_ID) {
|
|
4186
|
+
sendJson(res, 400, { error: '该操作需在容器层级执行' });
|
|
4187
|
+
return;
|
|
4188
|
+
}
|
|
4189
|
+
const payload = await readJsonBody(req);
|
|
4190
|
+
const remark = normalizeRemarkText(payload.remark);
|
|
4191
|
+
patchWebSessionHistory(state.webHistoryDir, sessionRef.containerName, { remark });
|
|
4192
|
+
sendJson(res, 200, { containerName: sessionRef.containerName, remark });
|
|
4193
|
+
}
|
|
4194
|
+
},
|
|
4195
|
+
{
|
|
4196
|
+
method: 'POST',
|
|
4197
|
+
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/agent-remark$/),
|
|
4198
|
+
handler: async match => {
|
|
4199
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
4200
|
+
if (!sessionRef) {
|
|
4201
|
+
return;
|
|
4202
|
+
}
|
|
4203
|
+
const payload = await readJsonBody(req);
|
|
4204
|
+
const remark = normalizeRemarkText(payload.remark);
|
|
4205
|
+
patchWebAgentSessionState(state.webHistoryDir, sessionRef, { remark });
|
|
4206
|
+
sendJson(res, 200, { name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId), remark });
|
|
4207
|
+
}
|
|
4208
|
+
},
|
|
4209
|
+
{
|
|
4210
|
+
method: 'POST',
|
|
4211
|
+
match: currentPath => currentPath === '/api/sessions/by-directory/remove' ? [] : null,
|
|
4212
|
+
handler: async () => {
|
|
4213
|
+
const payload = await readJsonBody(req);
|
|
4214
|
+
const hostPath = typeof payload.hostPath === 'string' ? payload.hostPath : '';
|
|
4215
|
+
if (!hostPath.trim()) {
|
|
4216
|
+
sendJson(res, 400, { error: 'hostPath 不能为空' });
|
|
4217
|
+
return;
|
|
4218
|
+
}
|
|
4219
|
+
const targets = resolveContainerNamesByHostPath(ctx, state, hostPath);
|
|
4220
|
+
if (!targets.length) {
|
|
4221
|
+
sendJson(res, 404, { error: '未找到该工作目录下的容器' });
|
|
4222
|
+
return;
|
|
4223
|
+
}
|
|
4224
|
+
const removed = [];
|
|
4225
|
+
const errors = [];
|
|
4226
|
+
targets.forEach(name => {
|
|
4227
|
+
try {
|
|
4228
|
+
if (ctx.containerExists(name)) {
|
|
4229
|
+
ctx.removeContainer(name);
|
|
4230
|
+
}
|
|
4231
|
+
removeWebSessionHistory(state.webHistoryDir, name);
|
|
4232
|
+
removed.push(name);
|
|
4233
|
+
} catch (e) {
|
|
4234
|
+
errors.push({ name, error: e.message || String(e) });
|
|
4235
|
+
}
|
|
4236
|
+
});
|
|
4237
|
+
sendJson(res, 200, { hostPath, removed, errors });
|
|
4238
|
+
}
|
|
4239
|
+
},
|
|
3951
4240
|
{
|
|
3952
4241
|
method: 'GET',
|
|
3953
4242
|
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/messages$/),
|