@xcanwin/manyoyo 6.2.5 → 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/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$/),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcanwin/manyoyo",
3
- "version": "6.2.5",
3
+ "version": "6.2.6",
4
4
  "imageVersion": "1.9.1-common",
5
5
  "playwrightCliVersion": "0.1.18",
6
6
  "description": "AI Agent CLI Security Sandbox for Docker and Podman",