@xcanwin/manyoyo 7.0.17 → 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/core/event-store.js
CHANGED
|
@@ -5,6 +5,7 @@ const path = require('path');
|
|
|
5
5
|
const {
|
|
6
6
|
validateControlEvent,
|
|
7
7
|
selectEventsAfterCursor,
|
|
8
|
+
applyEventToProjection,
|
|
8
9
|
projectSessionEvents
|
|
9
10
|
} = require('./events');
|
|
10
11
|
|
|
@@ -17,6 +18,11 @@ class FileEventStore {
|
|
|
17
18
|
this.rootDir = path.resolve(rootDir);
|
|
18
19
|
this.eventsDir = path.join(this.rootDir, 'events');
|
|
19
20
|
this.projectionsDir = path.join(this.rootDir, 'projections');
|
|
21
|
+
// append() 的增量缓存:aggregateId -> { lastSeq, projection }。命中时
|
|
22
|
+
// 避免每次追加都要整份重读 + 重新校验 + 重新投影该聚合的全部历史事件
|
|
23
|
+
// (O(该聚合累计事件数)),只有本实例第一次碰到这个 aggregateId 才退回
|
|
24
|
+
// read() 全量重建。仅作用于同一个长生命周期实例内,不跨进程/跨实例。
|
|
25
|
+
this._appendCache = new Map();
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
getEventFilePath(aggregateId) {
|
|
@@ -58,18 +64,42 @@ class FileEventStore {
|
|
|
58
64
|
return events;
|
|
59
65
|
}
|
|
60
66
|
|
|
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
|
+
|
|
61
89
|
append(event) {
|
|
62
90
|
validateControlEvent(event);
|
|
63
|
-
const
|
|
64
|
-
const
|
|
91
|
+
const aggregateId = String(event.aggregateId || '').trim();
|
|
92
|
+
const cached = this._ensureCacheEntry(aggregateId);
|
|
93
|
+
const expectedSeq = cached.lastSeq ? cached.lastSeq + 1 : event.seq;
|
|
65
94
|
if (event.seq !== expectedSeq) {
|
|
66
95
|
throw new Error(`seq 必须连续递增,期望 ${expectedSeq},实际 ${event.seq}`);
|
|
67
96
|
}
|
|
68
97
|
|
|
69
98
|
fs.mkdirSync(this.eventsDir, { recursive: true });
|
|
70
|
-
fs.appendFileSync(this.getEventFilePath(
|
|
71
|
-
|
|
72
|
-
|
|
99
|
+
fs.appendFileSync(this.getEventFilePath(aggregateId), `${JSON.stringify(event)}\n`);
|
|
100
|
+
cached.projection = applyEventToProjection(cached.projection, event);
|
|
101
|
+
cached.lastSeq = event.seq;
|
|
102
|
+
this.saveProjection(aggregateId, cached.projection);
|
|
73
103
|
return event;
|
|
74
104
|
}
|
|
75
105
|
|
|
@@ -109,6 +139,7 @@ class FileEventStore {
|
|
|
109
139
|
if (fs.existsSync(projectionFilePath)) {
|
|
110
140
|
fs.unlinkSync(projectionFilePath);
|
|
111
141
|
}
|
|
142
|
+
this._appendCache.delete(String(aggregateId || '').trim());
|
|
112
143
|
}
|
|
113
144
|
}
|
|
114
145
|
|
package/lib/core/events.js
CHANGED
|
@@ -101,42 +101,64 @@ function selectEventsAfterCursor(events, cursor = 0) {
|
|
|
101
101
|
return result;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
function
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
aggregateId: orderedEvents.length ? orderedEvents[0].aggregateId : '',
|
|
104
|
+
function emptyProjection(aggregateId = '') {
|
|
105
|
+
return {
|
|
106
|
+
aggregateId,
|
|
108
107
|
status: 'idle',
|
|
109
|
-
lastSeq:
|
|
108
|
+
lastSeq: 0,
|
|
110
109
|
childSessions: []
|
|
111
110
|
};
|
|
112
|
-
|
|
111
|
+
}
|
|
113
112
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
113
|
+
// 单条事件对投影的影响只取决于"这条事件本身 + 上一次的投影结果",不依赖更早
|
|
114
|
+
// 的历史(status 各分支互相覆盖,取的始终是最后一条命中事件的值;childSessions
|
|
115
|
+
// 按 id 增量合并)。这个性质让 FileEventStore.append() 可以在内存里增量维护
|
|
116
|
+
// 投影,不必每次追加事件都重新扫一遍该聚合的全部历史事件。
|
|
117
|
+
function applyEventToProjection(projection, event) {
|
|
118
|
+
const next = {
|
|
119
|
+
aggregateId: event.aggregateId,
|
|
120
|
+
status: projection.status,
|
|
121
|
+
lastSeq: event.seq,
|
|
122
|
+
childSessions: projection.childSessions.map(child => ({ ...child }))
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
if (event.type === 'session.created') next.status = 'starting';
|
|
126
|
+
if (event.type === 'session.ready' || event.type === 'process.started' || event.type === 'agent.turn.started') next.status = 'running';
|
|
127
|
+
if (event.type === 'session.stopping') next.status = 'stopping';
|
|
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' };
|
|
129
140
|
if (event.data.agentProgram) {
|
|
130
141
|
current.agentProgram = String(event.data.agentProgram);
|
|
131
142
|
}
|
|
132
143
|
if (event.type === 'agent.child.completed') current.status = 'completed';
|
|
133
144
|
if (event.type === 'agent.child.failed') current.status = 'failed';
|
|
134
145
|
if (event.type === 'agent.child.interrupted') current.status = 'interrupted';
|
|
135
|
-
|
|
146
|
+
if (existingIndex >= 0) {
|
|
147
|
+
next.childSessions[existingIndex] = current;
|
|
148
|
+
} else {
|
|
149
|
+
next.childSessions.push(current);
|
|
150
|
+
}
|
|
136
151
|
}
|
|
137
152
|
}
|
|
138
|
-
|
|
139
|
-
|
|
153
|
+
return next;
|
|
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
|
+
);
|
|
140
162
|
}
|
|
141
163
|
|
|
142
164
|
module.exports = {
|
|
@@ -145,5 +167,7 @@ module.exports = {
|
|
|
145
167
|
createControlEvent,
|
|
146
168
|
validateControlEvent,
|
|
147
169
|
selectEventsAfterCursor,
|
|
170
|
+
emptyProjection,
|
|
171
|
+
applyEventToProjection,
|
|
148
172
|
projectSessionEvents
|
|
149
173
|
};
|