@xcanwin/manyoyo 7.0.16 → 7.0.18
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/docker/res/claude/settings.json +5 -0
- package/lib/core/event-store.js +36 -5
- package/lib/core/events.js +48 -24
- package/lib/web/frontend/shadcn.html +75 -75
- package/lib/web/server.js +90 -37
- package/package.json +1 -1
package/lib/web/server.js
CHANGED
|
@@ -462,10 +462,9 @@ function removeContainerIdempotent(ctx, containerName) {
|
|
|
462
462
|
}
|
|
463
463
|
}
|
|
464
464
|
|
|
465
|
-
function removeAllAgentHistoryArtifacts(
|
|
466
|
-
const eventStore = new FileEventStore(webHistoryDir);
|
|
465
|
+
function removeAllAgentHistoryArtifacts(state, containerName, agentIds) {
|
|
467
466
|
(Array.isArray(agentIds) ? agentIds : []).forEach(agentId => {
|
|
468
|
-
eventStore.remove(buildWebSessionKey(containerName, agentId));
|
|
467
|
+
state.eventStore.remove(buildWebSessionKey(containerName, agentId));
|
|
469
468
|
});
|
|
470
469
|
}
|
|
471
470
|
|
|
@@ -623,29 +622,57 @@ function appendWebSessionMessage(webHistoryDir, sessionRefOrContainerName, role,
|
|
|
623
622
|
return message;
|
|
624
623
|
}
|
|
625
624
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
625
|
+
// 流式过程中高频出现的"透传"控制事件类型(见 resolveWebStreamControlEventType):
|
|
626
|
+
// 每个 content_delta/trace 事件都会各自触发一次,量随回合长度线性增长。会话
|
|
627
|
+
// 生命周期事件(session.ready/stopping/process.exited 等)不在其中,必须每次
|
|
628
|
+
// 都立即落盘。
|
|
629
|
+
const NOISY_CONTROL_EVENT_TYPES = new Set(['process.stdout', 'agent.turn.delta', 'agent.tool.observed']);
|
|
630
|
+
const LIVE_HISTORY_FLUSH_INTERVAL_MS = 400;
|
|
631
|
+
|
|
632
|
+
// 中间态历史落盘节流:同一 aggregateId 在节流窗口内只允许成功落盘一次,供
|
|
633
|
+
// appendWebSessionControlEvent(events 镜像)与 /agent/stream 路由里的
|
|
634
|
+
// patchWebSessionMessage(trace/content_delta 消息内容)共用同一个"落盘配额",
|
|
635
|
+
// 避免同一个事件被两条独立写路径各自整份重写一次 demo.json。节流只影响"实时
|
|
636
|
+
// 进度对轮询标签页可见的时机",不影响最终数据——两条写路径在回合结束/出错时
|
|
637
|
+
// 都还有一次不受节流影响的完整落盘。
|
|
638
|
+
function shouldFlushLiveHistory(state, aggregateId) {
|
|
639
|
+
const now = Date.now();
|
|
640
|
+
const lastFlushAt = state._liveHistoryFlushThrottle.get(aggregateId) || 0;
|
|
641
|
+
if (now - lastFlushAt < LIVE_HISTORY_FLUSH_INTERVAL_MS) {
|
|
642
|
+
return false;
|
|
643
|
+
}
|
|
644
|
+
state._liveHistoryFlushThrottle.set(aggregateId, now);
|
|
645
|
+
return true;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function appendWebSessionControlEvent(state, sessionRef, type, data = {}) {
|
|
649
|
+
const aggregateId = buildWebSessionKey(sessionRef.containerName, sessionRef.agentId);
|
|
650
|
+
// seq 必须以 state.eventStore 的内存缓存为准(而不是下面 agentSession.events
|
|
651
|
+
// 这份可能因落盘节流而滞后的历史 JSON 副本),否则节流生效时算出来的 seq
|
|
652
|
+
// 会与 eventStore 实际的 lastSeq 对不上,append() 会因"seq 必须连续递增"报错。
|
|
631
653
|
const event = createControlEvent({
|
|
632
654
|
type,
|
|
633
|
-
aggregateId
|
|
634
|
-
seq:
|
|
655
|
+
aggregateId,
|
|
656
|
+
seq: state.eventStore.getNextSeq(aggregateId),
|
|
635
657
|
data
|
|
636
658
|
});
|
|
637
|
-
|
|
638
|
-
|
|
659
|
+
state.eventStore.append(event);
|
|
660
|
+
|
|
661
|
+
const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
|
|
662
|
+
const agentSession = getWebAgentSession(history, sessionRef.agentId, { create: true, includeArchived: true });
|
|
663
|
+
const events = Array.isArray(agentSession.events) ? agentSession.events : [];
|
|
639
664
|
agentSession.events = [...events, event].slice(-WEB_HISTORY_MAX_MESSAGES);
|
|
640
665
|
agentSession.updatedAt = event.timestamp;
|
|
641
666
|
history.updatedAt = event.timestamp;
|
|
642
|
-
|
|
667
|
+
if (!NOISY_CONTROL_EVENT_TYPES.has(type) || shouldFlushLiveHistory(state, aggregateId)) {
|
|
668
|
+
saveWebSessionHistory(state.webHistoryDir, sessionRef.containerName, history);
|
|
669
|
+
}
|
|
643
670
|
return event;
|
|
644
671
|
}
|
|
645
672
|
|
|
646
|
-
function loadWebSessionControlEvents(
|
|
673
|
+
function loadWebSessionControlEvents(state, sessionRef, fallbackEvents = []) {
|
|
647
674
|
try {
|
|
648
|
-
const events =
|
|
675
|
+
const events = state.eventStore.read(
|
|
649
676
|
buildWebSessionKey(sessionRef.containerName, sessionRef.agentId)
|
|
650
677
|
);
|
|
651
678
|
if (events.length) {
|
|
@@ -2069,7 +2096,7 @@ function prepareCodexTraceEvent(payload) {
|
|
|
2069
2096
|
async function prepareWebAgentExecution(ctx, state, sessionRef, prompt) {
|
|
2070
2097
|
const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
|
|
2071
2098
|
const agentSession = getWebAgentSession(history, sessionRef.agentId, { create: true, includeArchived: true });
|
|
2072
|
-
const containerMap = listWebManyoyoContainers(ctx);
|
|
2099
|
+
const containerMap = listWebManyoyoContainers(ctx, state);
|
|
2073
2100
|
const containerInfo = containerMap[sessionRef.containerName] || {};
|
|
2074
2101
|
const normalizedContainerTemplate = normalizeAgentPromptCommandTemplate(history.agentPromptCommand, 'agentPromptCommand');
|
|
2075
2102
|
if (normalizedContainerTemplate !== history.agentPromptCommand) {
|
|
@@ -3009,7 +3036,7 @@ function buildCreateRuntime(ctx, state, payload) {
|
|
|
3009
3036
|
}
|
|
3010
3037
|
|
|
3011
3038
|
function resolveUniqueContainerName(ctx, state, baseName) {
|
|
3012
|
-
const containerMap = listWebManyoyoContainers(ctx);
|
|
3039
|
+
const containerMap = listWebManyoyoContainers(ctx, state);
|
|
3013
3040
|
const historyNames = listWebHistorySessionNames(state.webHistoryDir, ctx.isValidContainerName);
|
|
3014
3041
|
const taken = new Set([...Object.keys(containerMap), ...historyNames]);
|
|
3015
3042
|
// 基于历史最大编号 +1 命名,即使较小编号的副本被删除也不回收其编号,
|
|
@@ -3090,7 +3117,7 @@ async function createClonedContainer(ctx, state, sourceContainerName, requestedN
|
|
|
3090
3117
|
let finalName = '';
|
|
3091
3118
|
if (requestedName) {
|
|
3092
3119
|
validateContainerNameStrict(requestedName);
|
|
3093
|
-
const containerMap = listWebManyoyoContainers(ctx);
|
|
3120
|
+
const containerMap = listWebManyoyoContainers(ctx, state);
|
|
3094
3121
|
const historyNames = listWebHistorySessionNames(state.webHistoryDir, ctx.isValidContainerName);
|
|
3095
3122
|
const taken = new Set([...Object.keys(containerMap), ...historyNames]);
|
|
3096
3123
|
if (taken.has(requestedName)) {
|
|
@@ -3154,7 +3181,20 @@ function estimateStartTimeFromStatus(status) {
|
|
|
3154
3181
|
return null;
|
|
3155
3182
|
}
|
|
3156
3183
|
|
|
3157
|
-
|
|
3184
|
+
// 同一进程内短 TTL 缓存:GET /api/sessions、/detail 等接口各自独立调用这个函数,
|
|
3185
|
+
// 容器一多时 docker ps + 批量 inspect 本身就不便宜,多标签页轮询会让它们在
|
|
3186
|
+
// 短时间内反复触发。2 秒内的重复调用直接复用上一次结果,不再次触碰
|
|
3187
|
+
// ctx.dockerExecArgs(同步 spawnSync,会独占事件循环)。
|
|
3188
|
+
const CONTAINER_LIST_CACHE_TTL_MS = 2000;
|
|
3189
|
+
|
|
3190
|
+
function listWebManyoyoContainers(ctx, state) {
|
|
3191
|
+
if (state && state._containerListCache) {
|
|
3192
|
+
const { timestamp, data } = state._containerListCache;
|
|
3193
|
+
if (Date.now() - timestamp < CONTAINER_LIST_CACHE_TTL_MS) {
|
|
3194
|
+
return data;
|
|
3195
|
+
}
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3158
3198
|
const output = ctx.dockerExecArgs(
|
|
3159
3199
|
['ps', '-a', '--format', '{{.Names}}\t{{.Status}}\t{{.Image}}'],
|
|
3160
3200
|
{ ignoreError: true }
|
|
@@ -3162,6 +3202,9 @@ function listWebManyoyoContainers(ctx) {
|
|
|
3162
3202
|
|
|
3163
3203
|
const map = {};
|
|
3164
3204
|
if (!output.trim()) {
|
|
3205
|
+
if (state) {
|
|
3206
|
+
state._containerListCache = { timestamp: Date.now(), data: map };
|
|
3207
|
+
}
|
|
3165
3208
|
return map;
|
|
3166
3209
|
}
|
|
3167
3210
|
|
|
@@ -3218,6 +3261,9 @@ function listWebManyoyoContainers(ctx) {
|
|
|
3218
3261
|
};
|
|
3219
3262
|
});
|
|
3220
3263
|
|
|
3264
|
+
if (state) {
|
|
3265
|
+
state._containerListCache = { timestamp: Date.now(), data: map };
|
|
3266
|
+
}
|
|
3221
3267
|
return map;
|
|
3222
3268
|
}
|
|
3223
3269
|
|
|
@@ -3874,9 +3920,9 @@ function sendNdjson(res, payload) {
|
|
|
3874
3920
|
res.write(`${JSON.stringify(payload)}\n`);
|
|
3875
3921
|
}
|
|
3876
3922
|
|
|
3877
|
-
function createWebStreamEmitter(res,
|
|
3923
|
+
function createWebStreamEmitter(res, state, sessionRef) {
|
|
3878
3924
|
return (payload, type, data = {}) => {
|
|
3879
|
-
const controlEvent = appendWebSessionControlEvent(
|
|
3925
|
+
const controlEvent = appendWebSessionControlEvent(state, sessionRef, type, data);
|
|
3880
3926
|
sendNdjson(res, { ...payload, controlEvent });
|
|
3881
3927
|
};
|
|
3882
3928
|
}
|
|
@@ -4113,7 +4159,7 @@ function buildSessionAudit(ctx, state, sessionRef) {
|
|
|
4113
4159
|
const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
|
|
4114
4160
|
const agentSession = getWebAgentSession(history, sessionRef.agentId, { includeArchived: true })
|
|
4115
4161
|
|| createEmptyWebAgentSession(sessionRef.agentId);
|
|
4116
|
-
const events = loadWebSessionControlEvents(state
|
|
4162
|
+
const events = loadWebSessionControlEvents(state, sessionRef, agentSession.events);
|
|
4117
4163
|
const applied = history.applied && typeof history.applied === 'object' && !Array.isArray(history.applied)
|
|
4118
4164
|
? history.applied
|
|
4119
4165
|
: {};
|
|
@@ -4781,7 +4827,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
4781
4827
|
method: 'GET',
|
|
4782
4828
|
match: currentPath => currentPath === '/api/system/capacity' ? [] : null,
|
|
4783
4829
|
handler: async () => {
|
|
4784
|
-
const containerMap = listWebManyoyoContainers(ctx);
|
|
4830
|
+
const containerMap = listWebManyoyoContainers(ctx, state);
|
|
4785
4831
|
const diskPath = path.dirname(path.resolve(state.webConfigPath));
|
|
4786
4832
|
const report = estimateContainerCapacity({
|
|
4787
4833
|
runtimeCommand: ctx.dockerCmd || 'docker',
|
|
@@ -4858,7 +4904,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
4858
4904
|
method: 'GET',
|
|
4859
4905
|
match: currentPath => currentPath === '/api/sessions' ? [] : null,
|
|
4860
4906
|
handler: async () => {
|
|
4861
|
-
const containerMap = listWebManyoyoContainers(ctx);
|
|
4907
|
+
const containerMap = listWebManyoyoContainers(ctx, state);
|
|
4862
4908
|
const names = new Set([
|
|
4863
4909
|
...Object.keys(containerMap),
|
|
4864
4910
|
...listWebHistorySessionNames(state.webHistoryDir, ctx.isValidContainerName)
|
|
@@ -5256,7 +5302,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
5256
5302
|
return;
|
|
5257
5303
|
}
|
|
5258
5304
|
|
|
5259
|
-
const containerMap = listWebManyoyoContainers(ctx);
|
|
5305
|
+
const containerMap = listWebManyoyoContainers(ctx, state);
|
|
5260
5306
|
const detail = buildSessionDetail(ctx, state, containerMap, sessionRef);
|
|
5261
5307
|
sendJson(res, 200, { name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId), detail });
|
|
5262
5308
|
}
|
|
@@ -5269,7 +5315,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
5269
5315
|
if (!sessionRef) {
|
|
5270
5316
|
return;
|
|
5271
5317
|
}
|
|
5272
|
-
const containerMap = listWebManyoyoContainers(ctx);
|
|
5318
|
+
const containerMap = listWebManyoyoContainers(ctx, state);
|
|
5273
5319
|
const containerInfo = containerMap[sessionRef.containerName] || {};
|
|
5274
5320
|
const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
|
|
5275
5321
|
const effectiveTemplate = resolveEffectiveAgentPromptCommandForSession(
|
|
@@ -5367,7 +5413,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
5367
5413
|
return;
|
|
5368
5414
|
}
|
|
5369
5415
|
|
|
5370
|
-
const containerMap = listWebManyoyoContainers(ctx);
|
|
5416
|
+
const containerMap = listWebManyoyoContainers(ctx, state);
|
|
5371
5417
|
const detail = buildSessionDetail(ctx, state, containerMap, sessionRef);
|
|
5372
5418
|
sendJson(res, 200, {
|
|
5373
5419
|
saved: true,
|
|
@@ -5516,7 +5562,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
5516
5562
|
'Cache-Control': 'no-store',
|
|
5517
5563
|
'X-Accel-Buffering': 'no'
|
|
5518
5564
|
});
|
|
5519
|
-
const emitStreamEvent = createWebStreamEmitter(res, state
|
|
5565
|
+
const emitStreamEvent = createWebStreamEmitter(res, state, sessionRef);
|
|
5520
5566
|
emitStreamEvent({
|
|
5521
5567
|
type: 'meta',
|
|
5522
5568
|
containerName: sessionRef.containerName,
|
|
@@ -5570,11 +5616,13 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
5570
5616
|
if (event.traceEvent && typeof event.traceEvent === 'object') {
|
|
5571
5617
|
traceEvents.push(event.traceEvent);
|
|
5572
5618
|
}
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5619
|
+
if (shouldFlushLiveHistory(state, buildWebSessionKey(sessionRef.containerName, sessionRef.agentId))) {
|
|
5620
|
+
patchWebSessionMessage(state.webHistoryDir, sessionRef, traceMessage && traceMessage.id, {
|
|
5621
|
+
content: traceLines.join('\n'),
|
|
5622
|
+
traceEvents: traceEvents.slice(),
|
|
5623
|
+
pending: true
|
|
5624
|
+
});
|
|
5625
|
+
}
|
|
5578
5626
|
}
|
|
5579
5627
|
if (event && event.type === 'content_delta' && typeof event.content === 'string') {
|
|
5580
5628
|
if (!streamingReplyMessageId) {
|
|
@@ -5592,7 +5640,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
5592
5640
|
streamingReplyMessageId = streamingReplyMessage && streamingReplyMessage.id
|
|
5593
5641
|
? streamingReplyMessage.id
|
|
5594
5642
|
: '';
|
|
5595
|
-
} else {
|
|
5643
|
+
} else if (shouldFlushLiveHistory(state, buildWebSessionKey(sessionRef.containerName, sessionRef.agentId))) {
|
|
5596
5644
|
patchWebSessionMessage(state.webHistoryDir, sessionRef, streamingReplyMessageId, {
|
|
5597
5645
|
content: event.content,
|
|
5598
5646
|
pending: true
|
|
@@ -5695,7 +5743,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
5695
5743
|
sendJson(res, 404, { error: '当前会话没有运行中的 agent 任务' });
|
|
5696
5744
|
return;
|
|
5697
5745
|
}
|
|
5698
|
-
appendWebSessionControlEvent(state
|
|
5746
|
+
appendWebSessionControlEvent(state, sessionRef, 'session.stopping');
|
|
5699
5747
|
sendJson(res, 200, { ok: true, stopping: true });
|
|
5700
5748
|
}
|
|
5701
5749
|
},
|
|
@@ -5714,7 +5762,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
5714
5762
|
|
|
5715
5763
|
if (removeHistory) {
|
|
5716
5764
|
const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
|
|
5717
|
-
removeAllAgentHistoryArtifacts(state
|
|
5765
|
+
removeAllAgentHistoryArtifacts(state, sessionRef.containerName, Object.keys(history.agents || {}));
|
|
5718
5766
|
removeWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
|
|
5719
5767
|
} else if (removedContainer) {
|
|
5720
5768
|
appendWebSessionMessage(state.webHistoryDir, sessionRef, 'system', `容器 ${sessionRef.containerName} 已删除。`);
|
|
@@ -5743,7 +5791,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
5743
5791
|
if (history.agents && typeof history.agents === 'object') {
|
|
5744
5792
|
if (removeHistory) {
|
|
5745
5793
|
delete history.agents[sessionRef.agentId];
|
|
5746
|
-
removeAllAgentHistoryArtifacts(state
|
|
5794
|
+
removeAllAgentHistoryArtifacts(state, sessionRef.containerName, [sessionRef.agentId]);
|
|
5747
5795
|
} else if (history.agents[sessionRef.agentId]) {
|
|
5748
5796
|
history.agents[sessionRef.agentId].archived = true;
|
|
5749
5797
|
history.agents[sessionRef.agentId].updatedAt = new Date().toISOString();
|
|
@@ -5855,6 +5903,11 @@ async function startWebServer(options) {
|
|
|
5855
5903
|
terminalSessions: new Map(),
|
|
5856
5904
|
agentRuns: new Map()
|
|
5857
5905
|
};
|
|
5906
|
+
// 跨请求复用同一个 FileEventStore 实例,append() 的增量缓存才能生效
|
|
5907
|
+
// (否则每次都 new 一个新实例,缓存永远命中不到)
|
|
5908
|
+
state.eventStore = new FileEventStore(state.webHistoryDir);
|
|
5909
|
+
// shouldFlushLiveHistory() 的节流状态:aggregateId -> 上次成功落盘的时间戳
|
|
5910
|
+
state._liveHistoryFlushThrottle = new Map();
|
|
5858
5911
|
|
|
5859
5912
|
ensureWebHistoryDir(state.webHistoryDir);
|
|
5860
5913
|
|