wendkeep 0.70.0 → 0.72.0
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/CHANGELOG.md +57 -0
- package/README.en.md +7 -3
- package/README.md +7 -3
- package/docs/en/commands/observer.md +80 -35
- package/docs/pt-BR/commands/observer.md +82 -37
- package/hooks/observer-publish.mjs +1 -0
- package/package.json +3 -2
- package/packages/integrations/src/host-hooks.mjs +2 -0
- package/schema/observer/001-authority.sql +107 -0
- package/schema/observer/002-usage.sql +72 -0
- package/schema/observer/003-transcripts.sql +21 -0
- package/src/observer-memory-publish.mjs +335 -0
- package/src/observer-memory.mjs +308 -0
- package/src/observer-publish.mjs +23 -9
- package/src/observer-server.mjs +335 -22
- package/src/observer-sql-migrate.mjs +335 -0
- package/src/observer-sql-publish.mjs +398 -0
- package/src/observer-sql-store.mjs +542 -0
- package/src/observer-transcript-store.mjs +49 -0
- package/src/observer.mjs +72 -16
- package/web/observer/app.mjs +858 -0
- package/web/observer/favicon.svg +5 -0
- package/web/observer/index.html +126 -0
- package/web/observer/styles.css +262 -0
package/src/observer-server.mjs
CHANGED
|
@@ -1,9 +1,52 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
|
-
import {
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { gunzipSync } from 'node:zlib';
|
|
5
|
+
import {
|
|
6
|
+
appendObserverEvent,
|
|
7
|
+
getObserverProject,
|
|
8
|
+
listRegisteredObserverProjects,
|
|
9
|
+
readObserverIndex,
|
|
10
|
+
registerObserverProject,
|
|
11
|
+
} from './observer-store.mjs';
|
|
3
12
|
import { MAX_SNAPSHOT_BYTES, validateObserverSnapshot } from './observer-snapshot.mjs';
|
|
13
|
+
import {
|
|
14
|
+
setMemoryMode,
|
|
15
|
+
validateMemoryEvent,
|
|
16
|
+
} from './observer-memory.mjs';
|
|
17
|
+
import {
|
|
18
|
+
OBSERVER_SQL_FILE,
|
|
19
|
+
OBSERVER_SQL_SCHEMA_VERSION,
|
|
20
|
+
ensureObserverDatabase,
|
|
21
|
+
ingestObserverEvents,
|
|
22
|
+
migrateObserverDatabase,
|
|
23
|
+
readSqlProject,
|
|
24
|
+
listSqlProjects,
|
|
25
|
+
readSqlDocument,
|
|
26
|
+
readSqlSync,
|
|
27
|
+
readSqlTree,
|
|
28
|
+
searchSqlDocuments,
|
|
29
|
+
exportSqlMemoryBundle,
|
|
30
|
+
readTranscript,
|
|
31
|
+
readUsageBreakdown,
|
|
32
|
+
readUsageCalls,
|
|
33
|
+
readUsageSummary,
|
|
34
|
+
registerSqlProject,
|
|
35
|
+
} from './observer-sql-store.mjs';
|
|
36
|
+
import { migrateObserverContainerData } from './observer-sql-migrate.mjs';
|
|
4
37
|
|
|
5
38
|
const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1']);
|
|
6
39
|
const MAX_BODY_BYTES = MAX_SNAPSHOT_BYTES + 4096;
|
|
40
|
+
const MAX_MEMORY_BODY_BYTES = 8 * 1024 * 1024;
|
|
41
|
+
const MAX_SQL_BODY_BYTES = 64 * 1024 * 1024;
|
|
42
|
+
const MAX_SQL_EXPANDED_BODY_BYTES = 256 * 1024 * 1024;
|
|
43
|
+
const STATIC_ROOT = fileURLToPath(new URL('../web/observer/', import.meta.url));
|
|
44
|
+
const STATIC_ASSETS = new Map([
|
|
45
|
+
['/index.html', { file: 'index.html', type: 'text/html; charset=utf-8' }],
|
|
46
|
+
['/styles.css', { file: 'styles.css', type: 'text/css; charset=utf-8' }],
|
|
47
|
+
['/app.mjs', { file: 'app.mjs', type: 'text/javascript; charset=utf-8' }],
|
|
48
|
+
['/favicon.svg', { file: 'favicon.svg', type: 'image/svg+xml' }],
|
|
49
|
+
]);
|
|
7
50
|
|
|
8
51
|
function loopbackOnly(host) {
|
|
9
52
|
return LOOPBACK_HOSTS.has(String(host || '').toLowerCase());
|
|
@@ -23,7 +66,25 @@ function errorResponse(res, status, code, message) {
|
|
|
23
66
|
json(res, status, { error: { code, message } });
|
|
24
67
|
}
|
|
25
68
|
|
|
26
|
-
function
|
|
69
|
+
function serveStatic(res, pathname) {
|
|
70
|
+
const asset = STATIC_ASSETS.get(pathname === '/' ? '/index.html' : pathname);
|
|
71
|
+
if (!asset) return false;
|
|
72
|
+
try {
|
|
73
|
+
const body = readFileSync(new URL(asset.file, `file://${STATIC_ROOT.replace(/\\/g, '/')}/`));
|
|
74
|
+
res.writeHead(200, {
|
|
75
|
+
'content-type': asset.type,
|
|
76
|
+
'cache-control': 'no-store',
|
|
77
|
+
'x-content-type-options': 'nosniff',
|
|
78
|
+
'content-length': body.byteLength,
|
|
79
|
+
});
|
|
80
|
+
res.end(body);
|
|
81
|
+
} catch {
|
|
82
|
+
errorResponse(res, 500, 'dashboard_asset_error', 'asset do dashboard indisponível.');
|
|
83
|
+
}
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function readBody(req, maxBytes = MAX_BODY_BYTES, { gunzip = false, expandedMaxBytes = maxBytes } = {}) {
|
|
27
88
|
return new Promise((resolve, reject) => {
|
|
28
89
|
let size = 0;
|
|
29
90
|
let tooLarge = false;
|
|
@@ -31,7 +92,7 @@ function readBody(req) {
|
|
|
31
92
|
req.on('data', (chunk) => {
|
|
32
93
|
if (tooLarge) return;
|
|
33
94
|
size += chunk.length;
|
|
34
|
-
if (size >
|
|
95
|
+
if (size > maxBytes) {
|
|
35
96
|
tooLarge = true;
|
|
36
97
|
const error = new Error('corpo acima do limite.');
|
|
37
98
|
error.code = 'payload_too_large';
|
|
@@ -41,7 +102,24 @@ function readBody(req) {
|
|
|
41
102
|
chunks.push(chunk);
|
|
42
103
|
});
|
|
43
104
|
req.on('end', () => {
|
|
44
|
-
if (
|
|
105
|
+
if (tooLarge) return;
|
|
106
|
+
let body = Buffer.concat(chunks);
|
|
107
|
+
if (gunzip && String(req.headers['content-encoding'] || '').toLowerCase() === 'gzip') {
|
|
108
|
+
try { body = gunzipSync(body); }
|
|
109
|
+
catch {
|
|
110
|
+
const error = new Error('corpo gzip inválido.');
|
|
111
|
+
error.code = 'invalid_content_encoding';
|
|
112
|
+
reject(error);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (body.length > expandedMaxBytes) {
|
|
116
|
+
const error = new Error('corpo expandido acima do limite.');
|
|
117
|
+
error.code = 'payload_too_large';
|
|
118
|
+
reject(error);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
resolve(body.toString('utf8'));
|
|
45
123
|
});
|
|
46
124
|
req.on('error', reject);
|
|
47
125
|
});
|
|
@@ -56,13 +134,6 @@ function parseJson(text) {
|
|
|
56
134
|
}
|
|
57
135
|
}
|
|
58
136
|
|
|
59
|
-
function authorized(req, token) {
|
|
60
|
-
if (!token) return false;
|
|
61
|
-
const header = String(req.headers.authorization || '');
|
|
62
|
-
return header === `Bearer ${token}`
|
|
63
|
-
|| req.headers['x-wendkeep-observer-token'] === token;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
137
|
function pathParts(url) {
|
|
67
138
|
return new URL(url, 'http://127.0.0.1').pathname.split('/').filter(Boolean).map((part) => decodeURIComponent(part));
|
|
68
139
|
}
|
|
@@ -71,28 +142,93 @@ function projectIdFrom(parts) {
|
|
|
71
142
|
return parts[0] === 'v1' && parts[1] === 'projects' && parts[2] ? parts[2] : '';
|
|
72
143
|
}
|
|
73
144
|
|
|
145
|
+
function ensureSqlProjectRegistration(dataDir, sqlDb, projectId) {
|
|
146
|
+
try {
|
|
147
|
+
if (readSqlProject(sqlDb, projectId)) return true;
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (error?.code !== 'project_not_registered') throw error;
|
|
150
|
+
}
|
|
151
|
+
const legacy = getObserverProject(dataDir, projectId)
|
|
152
|
+
|| listRegisteredObserverProjects(dataDir).find((item) => item.projectId === projectId);
|
|
153
|
+
if (!legacy) return false;
|
|
154
|
+
registerSqlProject(sqlDb, {
|
|
155
|
+
projectId: legacy.projectId,
|
|
156
|
+
projectName: legacy.projectName,
|
|
157
|
+
wendkeepVersion: legacy.snapshot?.wendkeep_version || legacy.wendkeepVersion || '',
|
|
158
|
+
});
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function sqlMemoryEvent(projectId, event) {
|
|
163
|
+
return {
|
|
164
|
+
schema_version: 1,
|
|
165
|
+
event_id: event.event_id,
|
|
166
|
+
kind: event.operation === 'delete' ? 'document.delete' : 'document.upsert',
|
|
167
|
+
project_id: projectId,
|
|
168
|
+
occurred_at: event.captured_at || new Date().toISOString(),
|
|
169
|
+
payload: {
|
|
170
|
+
logical_path: event.logical_path,
|
|
171
|
+
entity_type: event.entity_type,
|
|
172
|
+
content: event.content || '',
|
|
173
|
+
content_hash: event.content_hash || '',
|
|
174
|
+
revision: event.revision || 1,
|
|
175
|
+
source_session_id: event.source_session_id || '',
|
|
176
|
+
source_turn_id: event.source_turn_id || '',
|
|
177
|
+
metadata: event.metadata || {},
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
74
182
|
export async function startObserverServer({
|
|
75
183
|
host = '127.0.0.1',
|
|
76
184
|
port = 8787,
|
|
77
185
|
dataDir,
|
|
78
|
-
token = process.env.WENDKEEP_OBSERVER_TOKEN || '',
|
|
79
186
|
allowNonLoopback = false,
|
|
80
187
|
} = {}) {
|
|
81
188
|
if (!loopbackOnly(host) && !allowNonLoopback) {
|
|
82
189
|
throw new Error(`Observer HTTP aceita somente host loopback; recebido: ${host}`);
|
|
83
190
|
}
|
|
84
191
|
if (!dataDir) throw new Error('dataDir é obrigatório.');
|
|
192
|
+
const sqlDb = ensureObserverDatabase(dataDir);
|
|
193
|
+
const databaseMigration = migrateObserverDatabase(sqlDb);
|
|
194
|
+
const legacyMigration = migrateObserverContainerData(dataDir, { database: sqlDb });
|
|
195
|
+
const registered = [
|
|
196
|
+
...listRegisteredObserverProjects(dataDir),
|
|
197
|
+
...readObserverIndex(dataDir).projects.map((item) => ({
|
|
198
|
+
projectId: item.projectId,
|
|
199
|
+
projectName: item.projectName,
|
|
200
|
+
wendkeepVersion: item.snapshot?.wendkeep_version || '',
|
|
201
|
+
})),
|
|
202
|
+
];
|
|
203
|
+
for (const project of registered) registerSqlProject(sqlDb, project);
|
|
85
204
|
const server = createServer(async (req, res) => {
|
|
86
205
|
try {
|
|
87
|
-
const
|
|
88
|
-
if (req.method === 'GET' &&
|
|
89
|
-
json(res, 200, {
|
|
206
|
+
const pathname = new URL(req.url || '/', 'http://127.0.0.1').pathname;
|
|
207
|
+
if (req.method === 'GET' && pathname === '/healthz') {
|
|
208
|
+
json(res, 200, {
|
|
209
|
+
ok: true,
|
|
210
|
+
service: 'wendkeep-observer',
|
|
211
|
+
schema_version: 1,
|
|
212
|
+
database: {
|
|
213
|
+
engine: 'sqlite',
|
|
214
|
+
file: OBSERVER_SQL_FILE,
|
|
215
|
+
schema_version: OBSERVER_SQL_SCHEMA_VERSION,
|
|
216
|
+
migrations: databaseMigration.applied.length,
|
|
217
|
+
legacy_migration: legacyMigration,
|
|
218
|
+
ready: true,
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (req.method === 'GET' && (pathname === '/' || STATIC_ASSETS.has(pathname))) {
|
|
224
|
+
serveStatic(res, pathname);
|
|
90
225
|
return;
|
|
91
226
|
}
|
|
92
|
-
if (!
|
|
93
|
-
errorResponse(res,
|
|
227
|
+
if (req.method === 'GET' && !pathname.startsWith('/v1/')) {
|
|
228
|
+
errorResponse(res, 404, 'not_found', 'rota não encontrada.');
|
|
94
229
|
return;
|
|
95
230
|
}
|
|
231
|
+
const parts = pathParts(req.url || '/');
|
|
96
232
|
if (parts[0] !== 'v1' || parts[1] !== 'projects') {
|
|
97
233
|
errorResponse(res, 404, 'not_found', 'rota não encontrada.');
|
|
98
234
|
return;
|
|
@@ -100,9 +236,20 @@ export async function startObserverServer({
|
|
|
100
236
|
|
|
101
237
|
if (parts.length === 2 && req.method === 'GET') {
|
|
102
238
|
const index = readObserverIndex(dataDir);
|
|
239
|
+
const legacy = new Map(index.projects.map(({ snapshot, ...summary }) => [summary.projectId, summary]));
|
|
240
|
+
for (const project of listSqlProjects(sqlDb)) {
|
|
241
|
+
const current = legacy.get(project.project_id) || {};
|
|
242
|
+
legacy.set(project.project_id, {
|
|
243
|
+
...current,
|
|
244
|
+
projectId: project.project_id,
|
|
245
|
+
projectName: current.projectName || project.project_name,
|
|
246
|
+
wendkeepVersion: current.wendkeepVersion || project.wendkeep_version,
|
|
247
|
+
registeredAt: current.registeredAt || project.registered_at,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
103
250
|
json(res, 200, {
|
|
104
251
|
schema_version: index.schema_version,
|
|
105
|
-
projects:
|
|
252
|
+
projects: [...legacy.values()].sort((a, b) => a.projectId.localeCompare(b.projectId)),
|
|
106
253
|
});
|
|
107
254
|
return;
|
|
108
255
|
}
|
|
@@ -112,9 +259,158 @@ export async function startObserverServer({
|
|
|
112
259
|
errorResponse(res, 404, 'not_found', 'projeto não informado.');
|
|
113
260
|
return;
|
|
114
261
|
}
|
|
262
|
+
ensureSqlProjectRegistration(dataDir, sqlDb, projectId);
|
|
263
|
+
|
|
264
|
+
if (parts.length === 4 && parts[3] === 'ingest' && req.method === 'POST') {
|
|
265
|
+
if (!ensureSqlProjectRegistration(dataDir, sqlDb, projectId)) {
|
|
266
|
+
errorResponse(res, 404, 'project_not_found', 'projeto não encontrado: ' + projectId);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const body = parseJson(await readBody(req, MAX_SQL_BODY_BYTES, {
|
|
270
|
+
gunzip: true,
|
|
271
|
+
expandedMaxBytes: MAX_SQL_EXPANDED_BODY_BYTES,
|
|
272
|
+
}));
|
|
273
|
+
const events = Array.isArray(body.events) ? body.events : [];
|
|
274
|
+
if (events.length === 0) {
|
|
275
|
+
errorResponse(res, 400, 'invalid_ingest_batch', 'events deve conter pelo menos um evento.');
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const result = ingestObserverEvents(sqlDb, { projectId, events });
|
|
279
|
+
const status = result.conflicts > 0 ? 409 : result.rejected > 0 ? 400 : result.accepted > 0 ? 201 : 200;
|
|
280
|
+
json(res, status, result);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (parts.length >= 4 && parts[3] === 'usage' && req.method === 'GET') {
|
|
285
|
+
if (!ensureSqlProjectRegistration(dataDir, sqlDb, projectId)) {
|
|
286
|
+
errorResponse(res, 404, 'project_not_found', 'projeto não encontrado: ' + projectId);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const query = new URL(req.url || '/', 'http://127.0.0.1').searchParams;
|
|
290
|
+
const filters = {
|
|
291
|
+
from: query.get('from') || '', to: query.get('to') || '', agentId: query.get('agent_id') || '', subagentId: query.get('subagent_id') || '',
|
|
292
|
+
sessionId: query.get('session_id') || '', changeSlug: query.get('change') || query.get('change_slug') || '', role: query.get('role') || '',
|
|
293
|
+
model: query.get('model') || '', provider: query.get('provider') || '', modelProvider: query.get('model_provider') || '',
|
|
294
|
+
limit: query.get('limit') || 100, offset: query.get('offset') || 0,
|
|
295
|
+
};
|
|
296
|
+
if (parts[4] === 'summary') {
|
|
297
|
+
json(res, 200, readUsageSummary(sqlDb, projectId, filters));
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (parts[4] === 'breakdown') {
|
|
301
|
+
json(res, 200, readUsageBreakdown(sqlDb, projectId, filters));
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (parts[4] === 'calls') {
|
|
305
|
+
json(res, 200, readUsageCalls(sqlDb, projectId, filters));
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (parts.length === 5 && parts[3] === 'transcripts' && req.method === 'GET') {
|
|
311
|
+
if (!ensureSqlProjectRegistration(dataDir, sqlDb, projectId)) {
|
|
312
|
+
errorResponse(res, 404, 'project_not_found', 'projeto não encontrado: ' + projectId);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
json(res, 200, readTranscript(sqlDb, projectId, parts[4]));
|
|
317
|
+
} catch (error) {
|
|
318
|
+
errorResponse(res, error?.code === 'transcript_not_found' ? 404 : 400, error?.code || 'transcript_error', error?.message || 'transcript indisponível.');
|
|
319
|
+
}
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (parts.length === 4 && parts[3] === 'sync' && req.method === 'GET') {
|
|
324
|
+
if (!ensureSqlProjectRegistration(dataDir, sqlDb, projectId)) {
|
|
325
|
+
errorResponse(res, 404, 'project_not_found', 'projeto não encontrado: ' + projectId);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
json(res, 200, { ...readSqlSync(sqlDb, projectId), mode: 'container-authority' });
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (parts.length === 4 && parts[3] === 'sync' && req.method === 'PUT') {
|
|
333
|
+
if (!ensureSqlProjectRegistration(dataDir, sqlDb, projectId)) {
|
|
334
|
+
errorResponse(res, 404, 'project_not_found', 'projeto não encontrado: ' + projectId);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const body = parseJson(await readBody(req));
|
|
338
|
+
try {
|
|
339
|
+
json(res, 200, setMemoryMode(dataDir, projectId, body.mode));
|
|
340
|
+
} catch (error) {
|
|
341
|
+
errorResponse(res, 400, error?.code || 'invalid_memory_mode', error?.message || 'modo inválido.');
|
|
342
|
+
}
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (parts.length >= 4 && parts[3] === 'memory') {
|
|
347
|
+
if (!ensureSqlProjectRegistration(dataDir, sqlDb, projectId)) {
|
|
348
|
+
errorResponse(res, 404, 'project_not_found', 'projeto não encontrado: ' + projectId);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const memoryAction = parts[4] || '';
|
|
352
|
+
if (memoryAction === 'tree' && req.method === 'GET') {
|
|
353
|
+
const query = new URL(req.url || '/', 'http://127.0.0.1').searchParams;
|
|
354
|
+
const tree = readSqlTree(sqlDb, projectId, query.get('prefix') || '');
|
|
355
|
+
json(res, 200, { ...tree, document_count: tree.documents.length });
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (memoryAction === 'document' && req.method === 'GET') {
|
|
359
|
+
const query = new URL(req.url || '/', 'http://127.0.0.1').searchParams;
|
|
360
|
+
try {
|
|
361
|
+
json(res, 200, readSqlDocument(sqlDb, projectId, query.get('path') || ''));
|
|
362
|
+
} catch (error) {
|
|
363
|
+
const status = error?.code === 'memory_not_found' ? 404 : 400;
|
|
364
|
+
errorResponse(res, status, error?.code || 'invalid_memory_path', error?.message || 'documento inválido.');
|
|
365
|
+
}
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (memoryAction === 'search' && req.method === 'GET') {
|
|
369
|
+
const query = new URL(req.url || '/', 'http://127.0.0.1').searchParams;
|
|
370
|
+
json(res, 200, { project_id: projectId, query: query.get('q') || '', results: searchSqlDocuments(sqlDb, projectId, query.get('q') || '') });
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
if (memoryAction === 'export' && req.method === 'GET') {
|
|
374
|
+
json(res, 200, { ...exportSqlMemoryBundle(sqlDb, projectId), mode: 'container-authority' });
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if (memoryAction === 'events' && req.method === 'POST') {
|
|
378
|
+
const body = parseJson(await readBody(req, MAX_MEMORY_BODY_BYTES));
|
|
379
|
+
const events = Array.isArray(body.events) ? body.events : [];
|
|
380
|
+
if (events.length === 0) {
|
|
381
|
+
errorResponse(res, 400, 'invalid_memory_batch', 'events deve conter pelo menos um evento.');
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
const validationErrors = [];
|
|
385
|
+
for (const event of events) {
|
|
386
|
+
const validation = validateMemoryEvent(event);
|
|
387
|
+
if (!validation.ok) validationErrors.push(...validation.errors);
|
|
388
|
+
}
|
|
389
|
+
if (validationErrors.length) {
|
|
390
|
+
errorResponse(res, 400, 'invalid_memory_batch', validationErrors.join(' '));
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
const result = ingestObserverEvents(sqlDb, { projectId, events: events.map((event) => sqlMemoryEvent(projectId, event)) });
|
|
394
|
+
const status = result.conflicts > 0 ? 409 : result.rejected > 0 ? 400 : result.accepted > 0 ? 201 : 200;
|
|
395
|
+
json(res, status, result);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
115
399
|
|
|
116
400
|
if (parts.length === 3 && req.method === 'GET') {
|
|
117
|
-
|
|
401
|
+
let project = getObserverProject(dataDir, projectId);
|
|
402
|
+
if (!project) {
|
|
403
|
+
try {
|
|
404
|
+
const sqlProject = readSqlProject(sqlDb, projectId);
|
|
405
|
+
project = {
|
|
406
|
+
projectId: sqlProject.project_id,
|
|
407
|
+
projectName: sqlProject.project_name,
|
|
408
|
+
wendkeepVersion: sqlProject.wendkeep_version,
|
|
409
|
+
registeredAt: sqlProject.registered_at,
|
|
410
|
+
eventCount: 0,
|
|
411
|
+
};
|
|
412
|
+
} catch { /* handled as not found below */ }
|
|
413
|
+
}
|
|
118
414
|
if (!project) {
|
|
119
415
|
errorResponse(res, 404, 'project_not_found', `projeto não encontrado: ${projectId}`);
|
|
120
416
|
return;
|
|
@@ -138,6 +434,11 @@ export async function startObserverServer({
|
|
|
138
434
|
errorResponse(res, 400, 'invalid_project', result.errors.join(' '));
|
|
139
435
|
return;
|
|
140
436
|
}
|
|
437
|
+
registerSqlProject(sqlDb, {
|
|
438
|
+
projectId,
|
|
439
|
+
projectName: body.project_name,
|
|
440
|
+
wendkeepVersion: body.wendkeep_version,
|
|
441
|
+
});
|
|
141
442
|
json(res, 201, result.project);
|
|
142
443
|
return;
|
|
143
444
|
}
|
|
@@ -145,7 +446,15 @@ export async function startObserverServer({
|
|
|
145
446
|
if (parts.length === 4 && parts[3] === 'changes' && req.method === 'GET') {
|
|
146
447
|
const project = getObserverProject(dataDir, projectId);
|
|
147
448
|
if (!project) {
|
|
148
|
-
|
|
449
|
+
let sqlProject;
|
|
450
|
+
try { sqlProject = readSqlProject(sqlDb, projectId); } catch (error) {
|
|
451
|
+
if (error?.code !== 'project_not_registered') throw error;
|
|
452
|
+
}
|
|
453
|
+
if (!sqlProject) {
|
|
454
|
+
errorResponse(res, 404, 'project_not_found', `projeto não encontrado: ${projectId}`);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
json(res, 200, { project_id: projectId, changes: [] });
|
|
149
458
|
return;
|
|
150
459
|
}
|
|
151
460
|
json(res, 200, { project_id: projectId, changes: project.snapshot?.changes || [] });
|
|
@@ -176,7 +485,7 @@ export async function startObserverServer({
|
|
|
176
485
|
errorResponse(res, 404, 'not_found', 'rota não encontrada.');
|
|
177
486
|
} catch (error) {
|
|
178
487
|
if (res.headersSent) return;
|
|
179
|
-
const status = error?.code === 'payload_too_large' ? 413 : error?.code
|
|
488
|
+
const status = error?.code === 'payload_too_large' ? 413 : ['invalid_json', 'invalid_content_encoding'].includes(error?.code) ? 400 : 500;
|
|
180
489
|
errorResponse(res, status, error?.code || 'observer_error', error?.message || 'erro interno do Observer.');
|
|
181
490
|
}
|
|
182
491
|
});
|
|
@@ -198,6 +507,10 @@ export async function startObserverServer({
|
|
|
198
507
|
return {
|
|
199
508
|
server,
|
|
200
509
|
address: () => server.address(),
|
|
201
|
-
close: () => new Promise((resolve, reject) => server.close((error) =>
|
|
510
|
+
close: () => new Promise((resolve, reject) => server.close((error) => {
|
|
511
|
+
try { sqlDb.close(); } catch { /* already closed */ }
|
|
512
|
+
if (error) reject(error);
|
|
513
|
+
else resolve();
|
|
514
|
+
})),
|
|
202
515
|
};
|
|
203
516
|
}
|