wendkeep 0.71.1 → 0.72.1
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 +64 -3
- package/README.en.md +6 -2
- package/README.md +6 -2
- package/docs/en/commands/observer.md +68 -43
- package/docs/en/commands/sessions-and-import.md +4 -4
- package/docs/pt-BR/commands/observer.md +70 -45
- package/docs/pt-BR/commands/sessions-and-import.md +4 -4
- package/hooks/observer-publish.mjs +1 -0
- package/hooks/understand-inject.mjs +1 -1
- package/package.json +5 -4
- package/packages/integrations/src/host-hooks.mjs +3 -1
- package/packages/vault/src/memory-store.mjs +17 -4
- 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/init.mjs +2 -2
- package/src/observer-auth.mjs +10 -0
- package/src/observer-memory-publish.mjs +15 -9
- package/src/observer-privacy.mjs +23 -0
- package/src/observer-publish.mjs +25 -20
- package/src/observer-server.mjs +274 -40
- package/src/observer-sql-migrate.mjs +335 -0
- package/src/observer-sql-publish.mjs +439 -0
- package/src/observer-sql-store.mjs +573 -0
- package/src/observer-transcript-store.mjs +49 -0
- package/src/observer.mjs +33 -8
- package/src/release-changelog.mjs +1 -1
- package/src/release-provenance.mjs +68 -0
- package/src/taxonomy.mjs +1 -1
- package/src/vault-readme.mjs +2 -2
- package/web/observer/app.mjs +248 -1
- package/web/observer/index.html +1 -0
- package/web/observer/styles.css +34 -1
package/src/observer-server.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
|
+
import { createHash, timingSafeEqual } from 'node:crypto';
|
|
2
3
|
import { readFileSync } from 'node:fs';
|
|
3
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { gunzipSync } from 'node:zlib';
|
|
4
6
|
import {
|
|
5
7
|
appendObserverEvent,
|
|
6
8
|
getObserverProject,
|
|
@@ -10,19 +12,35 @@ import {
|
|
|
10
12
|
} from './observer-store.mjs';
|
|
11
13
|
import { MAX_SNAPSHOT_BYTES, validateObserverSnapshot } from './observer-snapshot.mjs';
|
|
12
14
|
import {
|
|
13
|
-
applyMemoryEvent,
|
|
14
|
-
exportMemoryBundle,
|
|
15
|
-
readMemoryDocument,
|
|
16
|
-
readMemorySync,
|
|
17
|
-
readMemoryTree,
|
|
18
15
|
setMemoryMode,
|
|
19
|
-
searchMemory,
|
|
20
16
|
validateMemoryEvent,
|
|
21
17
|
} from './observer-memory.mjs';
|
|
18
|
+
import {
|
|
19
|
+
OBSERVER_SQL_FILE,
|
|
20
|
+
OBSERVER_SQL_SCHEMA_VERSION,
|
|
21
|
+
ensureObserverDatabase,
|
|
22
|
+
ingestObserverEvents,
|
|
23
|
+
migrateObserverDatabase,
|
|
24
|
+
readSqlProject,
|
|
25
|
+
listSqlProjects,
|
|
26
|
+
readSqlDocument,
|
|
27
|
+
readSqlSync,
|
|
28
|
+
readSqlTree,
|
|
29
|
+
searchSqlDocuments,
|
|
30
|
+
exportSqlMemoryBundle,
|
|
31
|
+
readTranscript,
|
|
32
|
+
readUsageBreakdown,
|
|
33
|
+
readUsageCalls,
|
|
34
|
+
readUsageSummary,
|
|
35
|
+
registerSqlProject,
|
|
36
|
+
} from './observer-sql-store.mjs';
|
|
37
|
+
import { migrateObserverContainerData } from './observer-sql-migrate.mjs';
|
|
22
38
|
|
|
23
39
|
const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1']);
|
|
24
40
|
const MAX_BODY_BYTES = MAX_SNAPSHOT_BYTES + 4096;
|
|
25
41
|
const MAX_MEMORY_BODY_BYTES = 8 * 1024 * 1024;
|
|
42
|
+
const MAX_SQL_BODY_BYTES = 64 * 1024 * 1024;
|
|
43
|
+
const MAX_SQL_EXPANDED_BODY_BYTES = 256 * 1024 * 1024;
|
|
26
44
|
const STATIC_ROOT = fileURLToPath(new URL('../web/observer/', import.meta.url));
|
|
27
45
|
const STATIC_ASSETS = new Map([
|
|
28
46
|
['/index.html', { file: 'index.html', type: 'text/html; charset=utf-8' }],
|
|
@@ -35,6 +53,39 @@ function loopbackOnly(host) {
|
|
|
35
53
|
return LOOPBACK_HOSTS.has(String(host || '').toLowerCase());
|
|
36
54
|
}
|
|
37
55
|
|
|
56
|
+
function safeTokenEqual(actual, expected) {
|
|
57
|
+
if (!actual || !expected) return false;
|
|
58
|
+
const left = createHash('sha256').update(String(actual)).digest();
|
|
59
|
+
const right = createHash('sha256').update(String(expected)).digest();
|
|
60
|
+
return timingSafeEqual(left, right);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function bearerToken(req) {
|
|
64
|
+
const match = String(req.headers.authorization || '').match(/^Bearer\s+(.+)$/i);
|
|
65
|
+
return match?.[1] || '';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function requestHostname(value) {
|
|
69
|
+
try { return new URL(`http://${String(value || '')}`).hostname.toLowerCase(); }
|
|
70
|
+
catch { return ''; }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function validateAuthority(req, { loopback }) {
|
|
74
|
+
const hostname = requestHostname(req.headers.host);
|
|
75
|
+
if (!hostname || (loopback && !LOOPBACK_HOSTS.has(hostname))) {
|
|
76
|
+
return { ok: false, status: 421, code: 'invalid_host', message: 'Host não corresponde ao binding do Observer.' };
|
|
77
|
+
}
|
|
78
|
+
const origin = String(req.headers.origin || '');
|
|
79
|
+
if (origin) {
|
|
80
|
+
let originHostname = '';
|
|
81
|
+
try { originHostname = new URL(origin).hostname.toLowerCase(); } catch { /* invalid below */ }
|
|
82
|
+
if (!originHostname || originHostname !== hostname) {
|
|
83
|
+
return { ok: false, status: 403, code: 'invalid_origin', message: 'Origin não corresponde ao Host do Observer.' };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return { ok: true };
|
|
87
|
+
}
|
|
88
|
+
|
|
38
89
|
function json(res, status, body) {
|
|
39
90
|
const content = JSON.stringify(body);
|
|
40
91
|
res.writeHead(status, {
|
|
@@ -67,7 +118,7 @@ function serveStatic(res, pathname) {
|
|
|
67
118
|
return true;
|
|
68
119
|
}
|
|
69
120
|
|
|
70
|
-
function readBody(req, maxBytes = MAX_BODY_BYTES) {
|
|
121
|
+
function readBody(req, maxBytes = MAX_BODY_BYTES, { gunzip = false, expandedMaxBytes = maxBytes } = {}) {
|
|
71
122
|
return new Promise((resolve, reject) => {
|
|
72
123
|
let size = 0;
|
|
73
124
|
let tooLarge = false;
|
|
@@ -85,7 +136,24 @@ function readBody(req, maxBytes = MAX_BODY_BYTES) {
|
|
|
85
136
|
chunks.push(chunk);
|
|
86
137
|
});
|
|
87
138
|
req.on('end', () => {
|
|
88
|
-
if (
|
|
139
|
+
if (tooLarge) return;
|
|
140
|
+
let body = Buffer.concat(chunks);
|
|
141
|
+
if (gunzip && String(req.headers['content-encoding'] || '').toLowerCase() === 'gzip') {
|
|
142
|
+
try { body = gunzipSync(body); }
|
|
143
|
+
catch {
|
|
144
|
+
const error = new Error('corpo gzip inválido.');
|
|
145
|
+
error.code = 'invalid_content_encoding';
|
|
146
|
+
reject(error);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (body.length > expandedMaxBytes) {
|
|
150
|
+
const error = new Error('corpo expandido acima do limite.');
|
|
151
|
+
error.code = 'payload_too_large';
|
|
152
|
+
reject(error);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
resolve(body.toString('utf8'));
|
|
89
157
|
});
|
|
90
158
|
req.on('error', reject);
|
|
91
159
|
});
|
|
@@ -108,11 +176,41 @@ function projectIdFrom(parts) {
|
|
|
108
176
|
return parts[0] === 'v1' && parts[1] === 'projects' && parts[2] ? parts[2] : '';
|
|
109
177
|
}
|
|
110
178
|
|
|
111
|
-
function
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
179
|
+
function ensureSqlProjectRegistration(dataDir, sqlDb, projectId) {
|
|
180
|
+
try {
|
|
181
|
+
if (readSqlProject(sqlDb, projectId)) return true;
|
|
182
|
+
} catch (error) {
|
|
183
|
+
if (error?.code !== 'project_not_registered') throw error;
|
|
184
|
+
}
|
|
185
|
+
const legacy = getObserverProject(dataDir, projectId)
|
|
186
|
+
|| listRegisteredObserverProjects(dataDir).find((item) => item.projectId === projectId);
|
|
187
|
+
if (!legacy) return false;
|
|
188
|
+
registerSqlProject(sqlDb, {
|
|
189
|
+
projectId: legacy.projectId,
|
|
190
|
+
projectName: legacy.projectName,
|
|
191
|
+
wendkeepVersion: legacy.snapshot?.wendkeep_version || legacy.wendkeepVersion || '',
|
|
192
|
+
});
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function sqlMemoryEvent(projectId, event) {
|
|
197
|
+
return {
|
|
198
|
+
schema_version: 1,
|
|
199
|
+
event_id: event.event_id,
|
|
200
|
+
kind: event.operation === 'delete' ? 'document.delete' : 'document.upsert',
|
|
201
|
+
project_id: projectId,
|
|
202
|
+
occurred_at: event.captured_at || new Date().toISOString(),
|
|
203
|
+
payload: {
|
|
204
|
+
logical_path: event.logical_path,
|
|
205
|
+
entity_type: event.entity_type,
|
|
206
|
+
content: event.content || '',
|
|
207
|
+
content_hash: event.content_hash || '',
|
|
208
|
+
revision: event.revision || 1,
|
|
209
|
+
source_session_id: event.source_session_id || '',
|
|
210
|
+
source_turn_id: event.source_turn_id || '',
|
|
211
|
+
metadata: event.metadata || {},
|
|
212
|
+
},
|
|
213
|
+
};
|
|
116
214
|
}
|
|
117
215
|
|
|
118
216
|
export async function startObserverServer({
|
|
@@ -120,16 +218,57 @@ export async function startObserverServer({
|
|
|
120
218
|
port = 8787,
|
|
121
219
|
dataDir,
|
|
122
220
|
allowNonLoopback = false,
|
|
221
|
+
token = process.env.WENDKEEP_OBSERVER_TOKEN || '',
|
|
123
222
|
} = {}) {
|
|
124
223
|
if (!loopbackOnly(host) && !allowNonLoopback) {
|
|
125
224
|
throw new Error(`Observer HTTP aceita somente host loopback; recebido: ${host}`);
|
|
126
225
|
}
|
|
226
|
+
if (!loopbackOnly(host) && !token) {
|
|
227
|
+
const error = new Error('Observer non-loopback exige --token ou WENDKEEP_OBSERVER_TOKEN.');
|
|
228
|
+
error.code = 'WENDKEEP_OBSERVER_TOKEN_REQUIRED';
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
127
231
|
if (!dataDir) throw new Error('dataDir é obrigatório.');
|
|
232
|
+
const sqlDb = ensureObserverDatabase(dataDir);
|
|
233
|
+
const databaseMigration = migrateObserverDatabase(sqlDb);
|
|
234
|
+
const legacyMigration = migrateObserverContainerData(dataDir, { database: sqlDb });
|
|
235
|
+
const registered = [
|
|
236
|
+
...listRegisteredObserverProjects(dataDir),
|
|
237
|
+
...readObserverIndex(dataDir).projects.map((item) => ({
|
|
238
|
+
projectId: item.projectId,
|
|
239
|
+
projectName: item.projectName,
|
|
240
|
+
wendkeepVersion: item.snapshot?.wendkeep_version || '',
|
|
241
|
+
})),
|
|
242
|
+
];
|
|
243
|
+
for (const project of registered) registerSqlProject(sqlDb, project);
|
|
128
244
|
const server = createServer(async (req, res) => {
|
|
129
245
|
try {
|
|
130
246
|
const pathname = new URL(req.url || '/', 'http://127.0.0.1').pathname;
|
|
247
|
+
const authority = validateAuthority(req, { loopback: loopbackOnly(host) });
|
|
248
|
+
if (!authority.ok) {
|
|
249
|
+
errorResponse(res, authority.status, authority.code, authority.message);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const authenticated = safeTokenEqual(bearerToken(req), token);
|
|
253
|
+
const mutating = !['GET', 'HEAD', 'OPTIONS'].includes(String(req.method || '').toUpperCase());
|
|
254
|
+
if ((mutating || !loopbackOnly(host)) && !authenticated) {
|
|
255
|
+
errorResponse(res, 401, 'observer_auth_required', 'Bearer token válido é obrigatório para esta operação.');
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
131
258
|
if (req.method === 'GET' && pathname === '/healthz') {
|
|
132
|
-
json(res, 200, {
|
|
259
|
+
json(res, 200, {
|
|
260
|
+
ok: true,
|
|
261
|
+
service: 'wendkeep-observer',
|
|
262
|
+
schema_version: 1,
|
|
263
|
+
database: {
|
|
264
|
+
engine: 'sqlite',
|
|
265
|
+
file: OBSERVER_SQL_FILE,
|
|
266
|
+
schema_version: OBSERVER_SQL_SCHEMA_VERSION,
|
|
267
|
+
migrations: databaseMigration.applied.length,
|
|
268
|
+
legacy_migration: legacyMigration,
|
|
269
|
+
ready: true,
|
|
270
|
+
},
|
|
271
|
+
});
|
|
133
272
|
return;
|
|
134
273
|
}
|
|
135
274
|
if (req.method === 'GET' && (pathname === '/' || STATIC_ASSETS.has(pathname))) {
|
|
@@ -148,9 +287,20 @@ export async function startObserverServer({
|
|
|
148
287
|
|
|
149
288
|
if (parts.length === 2 && req.method === 'GET') {
|
|
150
289
|
const index = readObserverIndex(dataDir);
|
|
290
|
+
const legacy = new Map(index.projects.map(({ snapshot, ...summary }) => [summary.projectId, summary]));
|
|
291
|
+
for (const project of listSqlProjects(sqlDb)) {
|
|
292
|
+
const current = legacy.get(project.project_id) || {};
|
|
293
|
+
legacy.set(project.project_id, {
|
|
294
|
+
...current,
|
|
295
|
+
projectId: project.project_id,
|
|
296
|
+
projectName: current.projectName || project.project_name,
|
|
297
|
+
wendkeepVersion: current.wendkeepVersion || project.wendkeep_version,
|
|
298
|
+
registeredAt: current.registeredAt || project.registered_at,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
151
301
|
json(res, 200, {
|
|
152
302
|
schema_version: index.schema_version,
|
|
153
|
-
projects:
|
|
303
|
+
projects: [...legacy.values()].sort((a, b) => a.projectId.localeCompare(b.projectId)),
|
|
154
304
|
});
|
|
155
305
|
return;
|
|
156
306
|
}
|
|
@@ -160,18 +310,78 @@ export async function startObserverServer({
|
|
|
160
310
|
errorResponse(res, 404, 'not_found', 'projeto não informado.');
|
|
161
311
|
return;
|
|
162
312
|
}
|
|
313
|
+
ensureSqlProjectRegistration(dataDir, sqlDb, projectId);
|
|
314
|
+
|
|
315
|
+
if (parts.length === 4 && parts[3] === 'ingest' && req.method === 'POST') {
|
|
316
|
+
if (!ensureSqlProjectRegistration(dataDir, sqlDb, projectId)) {
|
|
317
|
+
errorResponse(res, 404, 'project_not_found', 'projeto não encontrado: ' + projectId);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const body = parseJson(await readBody(req, MAX_SQL_BODY_BYTES, {
|
|
321
|
+
gunzip: true,
|
|
322
|
+
expandedMaxBytes: MAX_SQL_EXPANDED_BODY_BYTES,
|
|
323
|
+
}));
|
|
324
|
+
const events = Array.isArray(body.events) ? body.events : [];
|
|
325
|
+
if (events.length === 0) {
|
|
326
|
+
errorResponse(res, 400, 'invalid_ingest_batch', 'events deve conter pelo menos um evento.');
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const result = ingestObserverEvents(sqlDb, { projectId, events });
|
|
330
|
+
const status = result.conflicts > 0 ? 409 : result.rejected > 0 ? 400 : result.accepted > 0 ? 201 : 200;
|
|
331
|
+
json(res, status, result);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (parts.length >= 4 && parts[3] === 'usage' && req.method === 'GET') {
|
|
336
|
+
if (!ensureSqlProjectRegistration(dataDir, sqlDb, projectId)) {
|
|
337
|
+
errorResponse(res, 404, 'project_not_found', 'projeto não encontrado: ' + projectId);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const query = new URL(req.url || '/', 'http://127.0.0.1').searchParams;
|
|
341
|
+
const filters = {
|
|
342
|
+
from: query.get('from') || '', to: query.get('to') || '', agentId: query.get('agent_id') || '', subagentId: query.get('subagent_id') || '',
|
|
343
|
+
sessionId: query.get('session_id') || '', changeSlug: query.get('change') || query.get('change_slug') || '', role: query.get('role') || '',
|
|
344
|
+
model: query.get('model') || '', provider: query.get('provider') || '', modelProvider: query.get('model_provider') || '',
|
|
345
|
+
limit: query.get('limit') || 100, offset: query.get('offset') || 0,
|
|
346
|
+
};
|
|
347
|
+
if (parts[4] === 'summary') {
|
|
348
|
+
json(res, 200, readUsageSummary(sqlDb, projectId, filters));
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (parts[4] === 'breakdown') {
|
|
352
|
+
json(res, 200, readUsageBreakdown(sqlDb, projectId, filters));
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (parts[4] === 'calls') {
|
|
356
|
+
json(res, 200, readUsageCalls(sqlDb, projectId, filters));
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (parts.length === 5 && parts[3] === 'transcripts' && req.method === 'GET') {
|
|
362
|
+
if (!ensureSqlProjectRegistration(dataDir, sqlDb, projectId)) {
|
|
363
|
+
errorResponse(res, 404, 'project_not_found', 'projeto não encontrado: ' + projectId);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
try {
|
|
367
|
+
json(res, 200, readTranscript(sqlDb, projectId, parts[4]));
|
|
368
|
+
} catch (error) {
|
|
369
|
+
errorResponse(res, error?.code === 'transcript_not_found' ? 404 : 400, error?.code || 'transcript_error', error?.message || 'transcript indisponível.');
|
|
370
|
+
}
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
163
373
|
|
|
164
374
|
if (parts.length === 4 && parts[3] === 'sync' && req.method === 'GET') {
|
|
165
|
-
if (!
|
|
375
|
+
if (!ensureSqlProjectRegistration(dataDir, sqlDb, projectId)) {
|
|
166
376
|
errorResponse(res, 404, 'project_not_found', 'projeto não encontrado: ' + projectId);
|
|
167
377
|
return;
|
|
168
378
|
}
|
|
169
|
-
json(res, 200,
|
|
379
|
+
json(res, 200, { ...readSqlSync(sqlDb, projectId), mode: 'container-authority' });
|
|
170
380
|
return;
|
|
171
381
|
}
|
|
172
382
|
|
|
173
383
|
if (parts.length === 4 && parts[3] === 'sync' && req.method === 'PUT') {
|
|
174
|
-
if (!
|
|
384
|
+
if (!ensureSqlProjectRegistration(dataDir, sqlDb, projectId)) {
|
|
175
385
|
errorResponse(res, 404, 'project_not_found', 'projeto não encontrado: ' + projectId);
|
|
176
386
|
return;
|
|
177
387
|
}
|
|
@@ -185,20 +395,21 @@ export async function startObserverServer({
|
|
|
185
395
|
}
|
|
186
396
|
|
|
187
397
|
if (parts.length >= 4 && parts[3] === 'memory') {
|
|
188
|
-
if (!
|
|
398
|
+
if (!ensureSqlProjectRegistration(dataDir, sqlDb, projectId)) {
|
|
189
399
|
errorResponse(res, 404, 'project_not_found', 'projeto não encontrado: ' + projectId);
|
|
190
400
|
return;
|
|
191
401
|
}
|
|
192
402
|
const memoryAction = parts[4] || '';
|
|
193
403
|
if (memoryAction === 'tree' && req.method === 'GET') {
|
|
194
404
|
const query = new URL(req.url || '/', 'http://127.0.0.1').searchParams;
|
|
195
|
-
|
|
405
|
+
const tree = readSqlTree(sqlDb, projectId, query.get('prefix') || '');
|
|
406
|
+
json(res, 200, { ...tree, document_count: tree.documents.length });
|
|
196
407
|
return;
|
|
197
408
|
}
|
|
198
409
|
if (memoryAction === 'document' && req.method === 'GET') {
|
|
199
410
|
const query = new URL(req.url || '/', 'http://127.0.0.1').searchParams;
|
|
200
411
|
try {
|
|
201
|
-
json(res, 200,
|
|
412
|
+
json(res, 200, readSqlDocument(sqlDb, projectId, query.get('path') || ''));
|
|
202
413
|
} catch (error) {
|
|
203
414
|
const status = error?.code === 'memory_not_found' ? 404 : 400;
|
|
204
415
|
errorResponse(res, status, error?.code || 'invalid_memory_path', error?.message || 'documento inválido.');
|
|
@@ -207,11 +418,11 @@ export async function startObserverServer({
|
|
|
207
418
|
}
|
|
208
419
|
if (memoryAction === 'search' && req.method === 'GET') {
|
|
209
420
|
const query = new URL(req.url || '/', 'http://127.0.0.1').searchParams;
|
|
210
|
-
json(res, 200, { project_id: projectId, query: query.get('q') || '', results:
|
|
421
|
+
json(res, 200, { project_id: projectId, query: query.get('q') || '', results: searchSqlDocuments(sqlDb, projectId, query.get('q') || '') });
|
|
211
422
|
return;
|
|
212
423
|
}
|
|
213
424
|
if (memoryAction === 'export' && req.method === 'GET') {
|
|
214
|
-
json(res, 200,
|
|
425
|
+
json(res, 200, { ...exportSqlMemoryBundle(sqlDb, projectId), mode: 'container-authority' });
|
|
215
426
|
return;
|
|
216
427
|
}
|
|
217
428
|
if (memoryAction === 'events' && req.method === 'POST') {
|
|
@@ -221,30 +432,36 @@ export async function startObserverServer({
|
|
|
221
432
|
errorResponse(res, 400, 'invalid_memory_batch', 'events deve conter pelo menos um evento.');
|
|
222
433
|
return;
|
|
223
434
|
}
|
|
224
|
-
const
|
|
435
|
+
const validationErrors = [];
|
|
225
436
|
for (const event of events) {
|
|
226
|
-
if (event?.project_id !== projectId) {
|
|
227
|
-
results.push({ accepted: false, errors: ['project_id do evento não corresponde à rota.'] });
|
|
228
|
-
continue;
|
|
229
|
-
}
|
|
230
437
|
const validation = validateMemoryEvent(event);
|
|
231
|
-
|
|
438
|
+
if (!validation.ok) validationErrors.push(...validation.errors);
|
|
232
439
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const conflicts = results.filter((item) => item.conflict).length;
|
|
236
|
-
const rejected = results.length - accepted - duplicates - conflicts;
|
|
237
|
-
if (conflicts > 0 || rejected > 0) {
|
|
238
|
-
json(res, conflicts > 0 ? 409 : 400, { accepted, duplicates, conflicts, rejected, results });
|
|
440
|
+
if (validationErrors.length) {
|
|
441
|
+
errorResponse(res, 400, 'invalid_memory_batch', validationErrors.join(' '));
|
|
239
442
|
return;
|
|
240
443
|
}
|
|
241
|
-
|
|
444
|
+
const result = ingestObserverEvents(sqlDb, { projectId, events: events.map((event) => sqlMemoryEvent(projectId, event)) });
|
|
445
|
+
const status = result.conflicts > 0 ? 409 : result.rejected > 0 ? 400 : result.accepted > 0 ? 201 : 200;
|
|
446
|
+
json(res, status, result);
|
|
242
447
|
return;
|
|
243
448
|
}
|
|
244
449
|
}
|
|
245
450
|
|
|
246
451
|
if (parts.length === 3 && req.method === 'GET') {
|
|
247
|
-
|
|
452
|
+
let project = getObserverProject(dataDir, projectId);
|
|
453
|
+
if (!project) {
|
|
454
|
+
try {
|
|
455
|
+
const sqlProject = readSqlProject(sqlDb, projectId);
|
|
456
|
+
project = {
|
|
457
|
+
projectId: sqlProject.project_id,
|
|
458
|
+
projectName: sqlProject.project_name,
|
|
459
|
+
wendkeepVersion: sqlProject.wendkeep_version,
|
|
460
|
+
registeredAt: sqlProject.registered_at,
|
|
461
|
+
eventCount: 0,
|
|
462
|
+
};
|
|
463
|
+
} catch { /* handled as not found below */ }
|
|
464
|
+
}
|
|
248
465
|
if (!project) {
|
|
249
466
|
errorResponse(res, 404, 'project_not_found', `projeto não encontrado: ${projectId}`);
|
|
250
467
|
return;
|
|
@@ -268,6 +485,11 @@ export async function startObserverServer({
|
|
|
268
485
|
errorResponse(res, 400, 'invalid_project', result.errors.join(' '));
|
|
269
486
|
return;
|
|
270
487
|
}
|
|
488
|
+
registerSqlProject(sqlDb, {
|
|
489
|
+
projectId,
|
|
490
|
+
projectName: body.project_name,
|
|
491
|
+
wendkeepVersion: body.wendkeep_version,
|
|
492
|
+
});
|
|
271
493
|
json(res, 201, result.project);
|
|
272
494
|
return;
|
|
273
495
|
}
|
|
@@ -275,7 +497,15 @@ export async function startObserverServer({
|
|
|
275
497
|
if (parts.length === 4 && parts[3] === 'changes' && req.method === 'GET') {
|
|
276
498
|
const project = getObserverProject(dataDir, projectId);
|
|
277
499
|
if (!project) {
|
|
278
|
-
|
|
500
|
+
let sqlProject;
|
|
501
|
+
try { sqlProject = readSqlProject(sqlDb, projectId); } catch (error) {
|
|
502
|
+
if (error?.code !== 'project_not_registered') throw error;
|
|
503
|
+
}
|
|
504
|
+
if (!sqlProject) {
|
|
505
|
+
errorResponse(res, 404, 'project_not_found', `projeto não encontrado: ${projectId}`);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
json(res, 200, { project_id: projectId, changes: [] });
|
|
279
509
|
return;
|
|
280
510
|
}
|
|
281
511
|
json(res, 200, { project_id: projectId, changes: project.snapshot?.changes || [] });
|
|
@@ -306,7 +536,7 @@ export async function startObserverServer({
|
|
|
306
536
|
errorResponse(res, 404, 'not_found', 'rota não encontrada.');
|
|
307
537
|
} catch (error) {
|
|
308
538
|
if (res.headersSent) return;
|
|
309
|
-
const status = error?.code === 'payload_too_large' ? 413 : error?.code
|
|
539
|
+
const status = error?.code === 'payload_too_large' ? 413 : ['invalid_json', 'invalid_content_encoding'].includes(error?.code) ? 400 : 500;
|
|
310
540
|
errorResponse(res, status, error?.code || 'observer_error', error?.message || 'erro interno do Observer.');
|
|
311
541
|
}
|
|
312
542
|
});
|
|
@@ -328,6 +558,10 @@ export async function startObserverServer({
|
|
|
328
558
|
return {
|
|
329
559
|
server,
|
|
330
560
|
address: () => server.address(),
|
|
331
|
-
close: () => new Promise((resolve, reject) => server.close((error) =>
|
|
561
|
+
close: () => new Promise((resolve, reject) => server.close((error) => {
|
|
562
|
+
try { sqlDb.close(); } catch { /* already closed */ }
|
|
563
|
+
if (error) reject(error);
|
|
564
|
+
else resolve();
|
|
565
|
+
})),
|
|
332
566
|
};
|
|
333
567
|
}
|