@xcanwin/manyoyo 7.0.18 → 7.0.20
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 +0 -5
- package/lib/core/event-store.js +5 -36
- package/lib/core/events.js +24 -48
- package/lib/web/frontend/app.js +32 -0
- package/lib/web/frontend/shadcn.html +26 -26
- package/lib/web/server.js +150 -107
- package/package.json +1 -1
package/lib/core/event-store.js
CHANGED
|
@@ -5,7 +5,6 @@ const path = require('path');
|
|
|
5
5
|
const {
|
|
6
6
|
validateControlEvent,
|
|
7
7
|
selectEventsAfterCursor,
|
|
8
|
-
applyEventToProjection,
|
|
9
8
|
projectSessionEvents
|
|
10
9
|
} = require('./events');
|
|
11
10
|
|
|
@@ -18,11 +17,6 @@ class FileEventStore {
|
|
|
18
17
|
this.rootDir = path.resolve(rootDir);
|
|
19
18
|
this.eventsDir = path.join(this.rootDir, 'events');
|
|
20
19
|
this.projectionsDir = path.join(this.rootDir, 'projections');
|
|
21
|
-
// append() 的增量缓存:aggregateId -> { lastSeq, projection }。命中时
|
|
22
|
-
// 避免每次追加都要整份重读 + 重新校验 + 重新投影该聚合的全部历史事件
|
|
23
|
-
// (O(该聚合累计事件数)),只有本实例第一次碰到这个 aggregateId 才退回
|
|
24
|
-
// read() 全量重建。仅作用于同一个长生命周期实例内,不跨进程/跨实例。
|
|
25
|
-
this._appendCache = new Map();
|
|
26
20
|
}
|
|
27
21
|
|
|
28
22
|
getEventFilePath(aggregateId) {
|
|
@@ -64,42 +58,18 @@ class FileEventStore {
|
|
|
64
58
|
return events;
|
|
65
59
|
}
|
|
66
60
|
|
|
67
|
-
_ensureCacheEntry(aggregateId) {
|
|
68
|
-
let cached = this._appendCache.get(aggregateId);
|
|
69
|
-
if (!cached) {
|
|
70
|
-
const events = this.read(aggregateId);
|
|
71
|
-
cached = {
|
|
72
|
-
lastSeq: events.length ? events[events.length - 1].seq : 0,
|
|
73
|
-
projection: projectSessionEvents(events)
|
|
74
|
-
};
|
|
75
|
-
this._appendCache.set(aggregateId, cached);
|
|
76
|
-
}
|
|
77
|
-
return cached;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// 调用方(例如 lib/web/server.js 的 appendWebSessionControlEvent)在构造下
|
|
81
|
-
// 一个事件前,用这个方法拿"权威的下一个 seq",不要再依赖自己单独维护的
|
|
82
|
-
// 一份事件副本(如历史 JSON 里的 agentSession.events)——那份副本如果因为
|
|
83
|
-
// 落盘节流而滞后,算出来的 seq 会与这里的内存缓存不一致,append() 时报错。
|
|
84
|
-
getNextSeq(aggregateId) {
|
|
85
|
-
const normalizedAggregateId = String(aggregateId || '').trim();
|
|
86
|
-
return this._ensureCacheEntry(normalizedAggregateId).lastSeq + 1;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
61
|
append(event) {
|
|
90
62
|
validateControlEvent(event);
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
const expectedSeq = cached.lastSeq ? cached.lastSeq + 1 : event.seq;
|
|
63
|
+
const events = this.read(event.aggregateId);
|
|
64
|
+
const expectedSeq = events.length ? events[events.length - 1].seq + 1 : event.seq;
|
|
94
65
|
if (event.seq !== expectedSeq) {
|
|
95
66
|
throw new Error(`seq 必须连续递增,期望 ${expectedSeq},实际 ${event.seq}`);
|
|
96
67
|
}
|
|
97
68
|
|
|
98
69
|
fs.mkdirSync(this.eventsDir, { recursive: true });
|
|
99
|
-
fs.appendFileSync(this.getEventFilePath(aggregateId), `${JSON.stringify(event)}\n`);
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
this.saveProjection(aggregateId, cached.projection);
|
|
70
|
+
fs.appendFileSync(this.getEventFilePath(event.aggregateId), `${JSON.stringify(event)}\n`);
|
|
71
|
+
const projection = projectSessionEvents([...events, event]);
|
|
72
|
+
this.saveProjection(event.aggregateId, projection);
|
|
103
73
|
return event;
|
|
104
74
|
}
|
|
105
75
|
|
|
@@ -139,7 +109,6 @@ class FileEventStore {
|
|
|
139
109
|
if (fs.existsSync(projectionFilePath)) {
|
|
140
110
|
fs.unlinkSync(projectionFilePath);
|
|
141
111
|
}
|
|
142
|
-
this._appendCache.delete(String(aggregateId || '').trim());
|
|
143
112
|
}
|
|
144
113
|
}
|
|
145
114
|
|
package/lib/core/events.js
CHANGED
|
@@ -101,64 +101,42 @@ function selectEventsAfterCursor(events, cursor = 0) {
|
|
|
101
101
|
return result;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
function
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
function projectSessionEvents(events) {
|
|
105
|
+
const orderedEvents = selectEventsAfterCursor(events, 0);
|
|
106
|
+
const projection = {
|
|
107
|
+
aggregateId: orderedEvents.length ? orderedEvents[0].aggregateId : '',
|
|
107
108
|
status: 'idle',
|
|
108
|
-
lastSeq: 0,
|
|
109
|
+
lastSeq: orderedEvents.length ? orderedEvents[orderedEvents.length - 1].seq : 0,
|
|
109
110
|
childSessions: []
|
|
110
111
|
};
|
|
111
|
-
|
|
112
|
+
const childSessions = new Map();
|
|
112
113
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (event.type === 'session.stopped' || event.type === 'process.interrupted') next.status = 'interrupted';
|
|
129
|
-
if (event.type === 'session.failed' || event.type === 'agent.turn.failed') next.status = 'failed';
|
|
130
|
-
if (event.type === 'process.exited') {
|
|
131
|
-
next.status = Number(event.data.exitCode) === 0 ? 'completed' : 'failed';
|
|
132
|
-
}
|
|
133
|
-
if (event.type.startsWith('agent.child.')) {
|
|
134
|
-
const childSessionId = String(event.data.childSessionId || '').trim();
|
|
135
|
-
if (childSessionId) {
|
|
136
|
-
const existingIndex = next.childSessions.findIndex(child => child.id === childSessionId);
|
|
137
|
-
const current = existingIndex >= 0
|
|
138
|
-
? next.childSessions[existingIndex]
|
|
139
|
-
: { id: childSessionId, agentProgram: '', status: 'observed' };
|
|
114
|
+
for (const event of orderedEvents) {
|
|
115
|
+
if (event.type === 'session.created') projection.status = 'starting';
|
|
116
|
+
if (event.type === 'session.ready' || event.type === 'process.started' || event.type === 'agent.turn.started') projection.status = 'running';
|
|
117
|
+
if (event.type === 'session.stopping') projection.status = 'stopping';
|
|
118
|
+
if (event.type === 'session.stopped' || event.type === 'process.interrupted') projection.status = 'interrupted';
|
|
119
|
+
if (event.type === 'session.failed' || event.type === 'agent.turn.failed') projection.status = 'failed';
|
|
120
|
+
if (event.type === 'process.exited') {
|
|
121
|
+
projection.status = Number(event.data.exitCode) === 0 ? 'completed' : 'failed';
|
|
122
|
+
}
|
|
123
|
+
if (event.type.startsWith('agent.child.')) {
|
|
124
|
+
const childSessionId = String(event.data.childSessionId || '').trim();
|
|
125
|
+
if (!childSessionId) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const current = childSessions.get(childSessionId) || { id: childSessionId, agentProgram: '', status: 'observed' };
|
|
140
129
|
if (event.data.agentProgram) {
|
|
141
130
|
current.agentProgram = String(event.data.agentProgram);
|
|
142
131
|
}
|
|
143
132
|
if (event.type === 'agent.child.completed') current.status = 'completed';
|
|
144
133
|
if (event.type === 'agent.child.failed') current.status = 'failed';
|
|
145
134
|
if (event.type === 'agent.child.interrupted') current.status = 'interrupted';
|
|
146
|
-
|
|
147
|
-
next.childSessions[existingIndex] = current;
|
|
148
|
-
} else {
|
|
149
|
-
next.childSessions.push(current);
|
|
150
|
-
}
|
|
135
|
+
childSessions.set(childSessionId, current);
|
|
151
136
|
}
|
|
152
137
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
function projectSessionEvents(events) {
|
|
157
|
-
const orderedEvents = selectEventsAfterCursor(events, 0);
|
|
158
|
-
return orderedEvents.reduce(
|
|
159
|
-
applyEventToProjection,
|
|
160
|
-
emptyProjection(orderedEvents.length ? orderedEvents[0].aggregateId : '')
|
|
161
|
-
);
|
|
138
|
+
projection.childSessions = Array.from(childSessions.values());
|
|
139
|
+
return projection;
|
|
162
140
|
}
|
|
163
141
|
|
|
164
142
|
module.exports = {
|
|
@@ -167,7 +145,5 @@ module.exports = {
|
|
|
167
145
|
createControlEvent,
|
|
168
146
|
validateControlEvent,
|
|
169
147
|
selectEventsAfterCursor,
|
|
170
|
-
emptyProjection,
|
|
171
|
-
applyEventToProjection,
|
|
172
148
|
projectSessionEvents
|
|
173
149
|
};
|
package/lib/web/frontend/app.js
CHANGED
|
@@ -4439,6 +4439,21 @@
|
|
|
4439
4439
|
}
|
|
4440
4440
|
}
|
|
4441
4441
|
|
|
4442
|
+
function appendStreamingReplyChunkLocal(sessionName, replyMessageId, chunk, reset) {
|
|
4443
|
+
if (state.active !== sessionName) {
|
|
4444
|
+
return;
|
|
4445
|
+
}
|
|
4446
|
+
for (var i = state.messages.length - 1; i >= 0; i -= 1) {
|
|
4447
|
+
var message = state.messages[i];
|
|
4448
|
+
if (!message || String(message.id || '') !== String(replyMessageId || '')) {
|
|
4449
|
+
continue;
|
|
4450
|
+
}
|
|
4451
|
+
message.content = reset ? '' : String(message.content || '') + String(chunk || '');
|
|
4452
|
+
message.timestamp = new Date().toISOString();
|
|
4453
|
+
return;
|
|
4454
|
+
}
|
|
4455
|
+
}
|
|
4456
|
+
|
|
4442
4457
|
function removeStreamingReplyLocal(sessionName, replyMessageId) {
|
|
4443
4458
|
if (state.active !== sessionName) {
|
|
4444
4459
|
return;
|
|
@@ -4522,6 +4537,23 @@
|
|
|
4522
4537
|
}
|
|
4523
4538
|
return;
|
|
4524
4539
|
}
|
|
4540
|
+
if (event.type === 'content_chunk') {
|
|
4541
|
+
// token 级增量:服务端只发新增片段,这里追加。
|
|
4542
|
+
// reset 表示换了一条 assistant 消息,从空白重新开始
|
|
4543
|
+
if (!streamingReplyId) {
|
|
4544
|
+
streamingReplyId = appendStreamingReplyLocal(sessionName);
|
|
4545
|
+
}
|
|
4546
|
+
appendStreamingReplyChunkLocal(
|
|
4547
|
+
sessionName,
|
|
4548
|
+
streamingReplyId,
|
|
4549
|
+
event.reset === true ? '' : String(event.text || ''),
|
|
4550
|
+
event.reset === true
|
|
4551
|
+
);
|
|
4552
|
+
if (state.active === sessionName) {
|
|
4553
|
+
renderMessages(state.messages);
|
|
4554
|
+
}
|
|
4555
|
+
return;
|
|
4556
|
+
}
|
|
4525
4557
|
if (event.type === 'result') {
|
|
4526
4558
|
finalResult = event;
|
|
4527
4559
|
if (event.interrupted) {
|