wendkeep 0.71.1 → 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 +28 -0
- package/README.en.md +6 -2
- package/README.md +6 -2
- package/docs/en/commands/observer.md +54 -35
- package/docs/pt-BR/commands/observer.md +56 -37
- package/hooks/observer-publish.mjs +1 -0
- package/package.json +2 -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 +2 -1
- package/src/observer-publish.mjs +15 -13
- package/src/observer-server.mjs +223 -40
- 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 +23 -5
- package/web/observer/app.mjs +248 -1
- package/web/observer/index.html +1 -0
- package/web/observer/styles.css +34 -1
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS transcripts (
|
|
2
|
+
transcript_id TEXT PRIMARY KEY,
|
|
3
|
+
project_id TEXT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE,
|
|
4
|
+
session_id TEXT NOT NULL REFERENCES sessions(session_id) ON DELETE CASCADE,
|
|
5
|
+
agent_id TEXT NOT NULL REFERENCES agent_runs(agent_id) ON DELETE CASCADE,
|
|
6
|
+
coverage TEXT NOT NULL DEFAULT 'summary_only',
|
|
7
|
+
codec TEXT NOT NULL DEFAULT 'gzip',
|
|
8
|
+
content_gzip BLOB NOT NULL,
|
|
9
|
+
content_sha256 TEXT NOT NULL,
|
|
10
|
+
original_bytes INTEGER NOT NULL,
|
|
11
|
+
compressed_bytes INTEGER NOT NULL,
|
|
12
|
+
source TEXT NOT NULL DEFAULT '',
|
|
13
|
+
occurred_at TEXT NOT NULL,
|
|
14
|
+
metadata_json TEXT NOT NULL DEFAULT '{}'
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
CREATE INDEX IF NOT EXISTS idx_transcripts_project_session
|
|
18
|
+
ON transcripts(project_id, session_id, occurred_at);
|
|
19
|
+
|
|
20
|
+
CREATE INDEX IF NOT EXISTS idx_transcripts_project_coverage
|
|
21
|
+
ON transcripts(project_id, coverage);
|
|
@@ -68,7 +68,8 @@ function entityType(logicalPath) {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
function shouldSkip(name, relativePath) {
|
|
71
|
-
if (TRANSIENT_NAMES.has(name) || name
|
|
71
|
+
if (TRANSIENT_NAMES.has(name) || name === 'observer-sql-state.json' || name === 'observer-sql-outbox'
|
|
72
|
+
|| name.endsWith('.tmp') || name.endsWith('.lock')) return true;
|
|
72
73
|
if (relativePath === MEMORY_STATE_FILE) return true;
|
|
73
74
|
return false;
|
|
74
75
|
}
|
package/src/observer-publish.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { buildProjectSnapshot } from './observer-snapshot.mjs';
|
|
4
|
-
import {
|
|
4
|
+
import { publishObserverSql } from './observer-sql-publish.mjs';
|
|
5
5
|
|
|
6
6
|
const OUTBOX_REL = join('.brain', 'observer-outbox');
|
|
7
7
|
const REQUEST_TIMEOUT_MS = 500;
|
|
@@ -90,23 +90,23 @@ export async function publishObserverSnapshot({
|
|
|
90
90
|
projectRoot,
|
|
91
91
|
url = process.env.WENDKEEP_OBSERVER_URL || '',
|
|
92
92
|
now = new Date(),
|
|
93
|
+
input = {},
|
|
93
94
|
} = {}) {
|
|
94
95
|
try {
|
|
95
96
|
const event = buildProjectSnapshot({ vaultBase, projectRoot, now });
|
|
96
|
-
|
|
97
|
+
const sql = await publishObserverSql({
|
|
98
|
+
vaultBase,
|
|
99
|
+
projectId: event.project_id,
|
|
100
|
+
url,
|
|
101
|
+
input,
|
|
102
|
+
now,
|
|
103
|
+
});
|
|
104
|
+
if (!url) return { ok: sql.ok, skipped: true, queued: sql.queued, hookExitCode: 0, event_id: event.event_id, sql };
|
|
97
105
|
|
|
98
106
|
await retryObserverOutbox({ vaultBase, url });
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
vaultBase,
|
|
103
|
-
projectId: event.project_id,
|
|
104
|
-
url,
|
|
105
|
-
now,
|
|
106
|
-
});
|
|
107
|
-
} catch (error) {
|
|
108
|
-
memory = { ok: false, queued: false, error: error.message };
|
|
109
|
-
}
|
|
107
|
+
// SQL is the live authority. Keep the legacy-shaped `memory` field for
|
|
108
|
+
// older integrations while reporting the real SQL publication separately.
|
|
109
|
+
const memory = { ok: sql.ok, queued: sql.queued, changed: sql.changed, pending: sql.pending, authority: 'sqlite' };
|
|
110
110
|
try {
|
|
111
111
|
const response = await postSnapshot(url, event);
|
|
112
112
|
return {
|
|
@@ -116,6 +116,7 @@ export async function publishObserverSnapshot({
|
|
|
116
116
|
event_id: event.event_id,
|
|
117
117
|
duplicate: response.duplicate === true,
|
|
118
118
|
memory,
|
|
119
|
+
sql,
|
|
119
120
|
};
|
|
120
121
|
} catch (error) {
|
|
121
122
|
queueOutbox(vaultBase, event);
|
|
@@ -126,6 +127,7 @@ export async function publishObserverSnapshot({
|
|
|
126
127
|
event_id: event.event_id,
|
|
127
128
|
error: error.message,
|
|
128
129
|
memory,
|
|
130
|
+
sql,
|
|
129
131
|
};
|
|
130
132
|
}
|
|
131
133
|
} catch (error) {
|
package/src/observer-server.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { gunzipSync } from 'node:zlib';
|
|
4
5
|
import {
|
|
5
6
|
appendObserverEvent,
|
|
6
7
|
getObserverProject,
|
|
@@ -10,19 +11,35 @@ import {
|
|
|
10
11
|
} from './observer-store.mjs';
|
|
11
12
|
import { MAX_SNAPSHOT_BYTES, validateObserverSnapshot } from './observer-snapshot.mjs';
|
|
12
13
|
import {
|
|
13
|
-
applyMemoryEvent,
|
|
14
|
-
exportMemoryBundle,
|
|
15
|
-
readMemoryDocument,
|
|
16
|
-
readMemorySync,
|
|
17
|
-
readMemoryTree,
|
|
18
14
|
setMemoryMode,
|
|
19
|
-
searchMemory,
|
|
20
15
|
validateMemoryEvent,
|
|
21
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';
|
|
22
37
|
|
|
23
38
|
const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1']);
|
|
24
39
|
const MAX_BODY_BYTES = MAX_SNAPSHOT_BYTES + 4096;
|
|
25
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;
|
|
26
43
|
const STATIC_ROOT = fileURLToPath(new URL('../web/observer/', import.meta.url));
|
|
27
44
|
const STATIC_ASSETS = new Map([
|
|
28
45
|
['/index.html', { file: 'index.html', type: 'text/html; charset=utf-8' }],
|
|
@@ -67,7 +84,7 @@ function serveStatic(res, pathname) {
|
|
|
67
84
|
return true;
|
|
68
85
|
}
|
|
69
86
|
|
|
70
|
-
function readBody(req, maxBytes = MAX_BODY_BYTES) {
|
|
87
|
+
function readBody(req, maxBytes = MAX_BODY_BYTES, { gunzip = false, expandedMaxBytes = maxBytes } = {}) {
|
|
71
88
|
return new Promise((resolve, reject) => {
|
|
72
89
|
let size = 0;
|
|
73
90
|
let tooLarge = false;
|
|
@@ -85,7 +102,24 @@ function readBody(req, maxBytes = MAX_BODY_BYTES) {
|
|
|
85
102
|
chunks.push(chunk);
|
|
86
103
|
});
|
|
87
104
|
req.on('end', () => {
|
|
88
|
-
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'));
|
|
89
123
|
});
|
|
90
124
|
req.on('error', reject);
|
|
91
125
|
});
|
|
@@ -108,11 +142,41 @@ function projectIdFrom(parts) {
|
|
|
108
142
|
return parts[0] === 'v1' && parts[1] === 'projects' && parts[2] ? parts[2] : '';
|
|
109
143
|
}
|
|
110
144
|
|
|
111
|
-
function
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
+
};
|
|
116
180
|
}
|
|
117
181
|
|
|
118
182
|
export async function startObserverServer({
|
|
@@ -125,11 +189,35 @@ export async function startObserverServer({
|
|
|
125
189
|
throw new Error(`Observer HTTP aceita somente host loopback; recebido: ${host}`);
|
|
126
190
|
}
|
|
127
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);
|
|
128
204
|
const server = createServer(async (req, res) => {
|
|
129
205
|
try {
|
|
130
206
|
const pathname = new URL(req.url || '/', 'http://127.0.0.1').pathname;
|
|
131
207
|
if (req.method === 'GET' && pathname === '/healthz') {
|
|
132
|
-
json(res, 200, {
|
|
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
|
+
});
|
|
133
221
|
return;
|
|
134
222
|
}
|
|
135
223
|
if (req.method === 'GET' && (pathname === '/' || STATIC_ASSETS.has(pathname))) {
|
|
@@ -148,9 +236,20 @@ export async function startObserverServer({
|
|
|
148
236
|
|
|
149
237
|
if (parts.length === 2 && req.method === 'GET') {
|
|
150
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
|
+
}
|
|
151
250
|
json(res, 200, {
|
|
152
251
|
schema_version: index.schema_version,
|
|
153
|
-
projects:
|
|
252
|
+
projects: [...legacy.values()].sort((a, b) => a.projectId.localeCompare(b.projectId)),
|
|
154
253
|
});
|
|
155
254
|
return;
|
|
156
255
|
}
|
|
@@ -160,18 +259,78 @@ export async function startObserverServer({
|
|
|
160
259
|
errorResponse(res, 404, 'not_found', 'projeto não informado.');
|
|
161
260
|
return;
|
|
162
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
|
+
}
|
|
163
322
|
|
|
164
323
|
if (parts.length === 4 && parts[3] === 'sync' && req.method === 'GET') {
|
|
165
|
-
if (!
|
|
324
|
+
if (!ensureSqlProjectRegistration(dataDir, sqlDb, projectId)) {
|
|
166
325
|
errorResponse(res, 404, 'project_not_found', 'projeto não encontrado: ' + projectId);
|
|
167
326
|
return;
|
|
168
327
|
}
|
|
169
|
-
json(res, 200,
|
|
328
|
+
json(res, 200, { ...readSqlSync(sqlDb, projectId), mode: 'container-authority' });
|
|
170
329
|
return;
|
|
171
330
|
}
|
|
172
331
|
|
|
173
332
|
if (parts.length === 4 && parts[3] === 'sync' && req.method === 'PUT') {
|
|
174
|
-
if (!
|
|
333
|
+
if (!ensureSqlProjectRegistration(dataDir, sqlDb, projectId)) {
|
|
175
334
|
errorResponse(res, 404, 'project_not_found', 'projeto não encontrado: ' + projectId);
|
|
176
335
|
return;
|
|
177
336
|
}
|
|
@@ -185,20 +344,21 @@ export async function startObserverServer({
|
|
|
185
344
|
}
|
|
186
345
|
|
|
187
346
|
if (parts.length >= 4 && parts[3] === 'memory') {
|
|
188
|
-
if (!
|
|
347
|
+
if (!ensureSqlProjectRegistration(dataDir, sqlDb, projectId)) {
|
|
189
348
|
errorResponse(res, 404, 'project_not_found', 'projeto não encontrado: ' + projectId);
|
|
190
349
|
return;
|
|
191
350
|
}
|
|
192
351
|
const memoryAction = parts[4] || '';
|
|
193
352
|
if (memoryAction === 'tree' && req.method === 'GET') {
|
|
194
353
|
const query = new URL(req.url || '/', 'http://127.0.0.1').searchParams;
|
|
195
|
-
|
|
354
|
+
const tree = readSqlTree(sqlDb, projectId, query.get('prefix') || '');
|
|
355
|
+
json(res, 200, { ...tree, document_count: tree.documents.length });
|
|
196
356
|
return;
|
|
197
357
|
}
|
|
198
358
|
if (memoryAction === 'document' && req.method === 'GET') {
|
|
199
359
|
const query = new URL(req.url || '/', 'http://127.0.0.1').searchParams;
|
|
200
360
|
try {
|
|
201
|
-
json(res, 200,
|
|
361
|
+
json(res, 200, readSqlDocument(sqlDb, projectId, query.get('path') || ''));
|
|
202
362
|
} catch (error) {
|
|
203
363
|
const status = error?.code === 'memory_not_found' ? 404 : 400;
|
|
204
364
|
errorResponse(res, status, error?.code || 'invalid_memory_path', error?.message || 'documento inválido.');
|
|
@@ -207,11 +367,11 @@ export async function startObserverServer({
|
|
|
207
367
|
}
|
|
208
368
|
if (memoryAction === 'search' && req.method === 'GET') {
|
|
209
369
|
const query = new URL(req.url || '/', 'http://127.0.0.1').searchParams;
|
|
210
|
-
json(res, 200, { project_id: projectId, query: query.get('q') || '', results:
|
|
370
|
+
json(res, 200, { project_id: projectId, query: query.get('q') || '', results: searchSqlDocuments(sqlDb, projectId, query.get('q') || '') });
|
|
211
371
|
return;
|
|
212
372
|
}
|
|
213
373
|
if (memoryAction === 'export' && req.method === 'GET') {
|
|
214
|
-
json(res, 200,
|
|
374
|
+
json(res, 200, { ...exportSqlMemoryBundle(sqlDb, projectId), mode: 'container-authority' });
|
|
215
375
|
return;
|
|
216
376
|
}
|
|
217
377
|
if (memoryAction === 'events' && req.method === 'POST') {
|
|
@@ -221,30 +381,36 @@ export async function startObserverServer({
|
|
|
221
381
|
errorResponse(res, 400, 'invalid_memory_batch', 'events deve conter pelo menos um evento.');
|
|
222
382
|
return;
|
|
223
383
|
}
|
|
224
|
-
const
|
|
384
|
+
const validationErrors = [];
|
|
225
385
|
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
386
|
const validation = validateMemoryEvent(event);
|
|
231
|
-
|
|
387
|
+
if (!validation.ok) validationErrors.push(...validation.errors);
|
|
232
388
|
}
|
|
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 });
|
|
389
|
+
if (validationErrors.length) {
|
|
390
|
+
errorResponse(res, 400, 'invalid_memory_batch', validationErrors.join(' '));
|
|
239
391
|
return;
|
|
240
392
|
}
|
|
241
|
-
|
|
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);
|
|
242
396
|
return;
|
|
243
397
|
}
|
|
244
398
|
}
|
|
245
399
|
|
|
246
400
|
if (parts.length === 3 && req.method === 'GET') {
|
|
247
|
-
|
|
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
|
+
}
|
|
248
414
|
if (!project) {
|
|
249
415
|
errorResponse(res, 404, 'project_not_found', `projeto não encontrado: ${projectId}`);
|
|
250
416
|
return;
|
|
@@ -268,6 +434,11 @@ export async function startObserverServer({
|
|
|
268
434
|
errorResponse(res, 400, 'invalid_project', result.errors.join(' '));
|
|
269
435
|
return;
|
|
270
436
|
}
|
|
437
|
+
registerSqlProject(sqlDb, {
|
|
438
|
+
projectId,
|
|
439
|
+
projectName: body.project_name,
|
|
440
|
+
wendkeepVersion: body.wendkeep_version,
|
|
441
|
+
});
|
|
271
442
|
json(res, 201, result.project);
|
|
272
443
|
return;
|
|
273
444
|
}
|
|
@@ -275,7 +446,15 @@ export async function startObserverServer({
|
|
|
275
446
|
if (parts.length === 4 && parts[3] === 'changes' && req.method === 'GET') {
|
|
276
447
|
const project = getObserverProject(dataDir, projectId);
|
|
277
448
|
if (!project) {
|
|
278
|
-
|
|
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: [] });
|
|
279
458
|
return;
|
|
280
459
|
}
|
|
281
460
|
json(res, 200, { project_id: projectId, changes: project.snapshot?.changes || [] });
|
|
@@ -306,7 +485,7 @@ export async function startObserverServer({
|
|
|
306
485
|
errorResponse(res, 404, 'not_found', 'rota não encontrada.');
|
|
307
486
|
} catch (error) {
|
|
308
487
|
if (res.headersSent) return;
|
|
309
|
-
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;
|
|
310
489
|
errorResponse(res, status, error?.code || 'observer_error', error?.message || 'erro interno do Observer.');
|
|
311
490
|
}
|
|
312
491
|
});
|
|
@@ -328,6 +507,10 @@ export async function startObserverServer({
|
|
|
328
507
|
return {
|
|
329
508
|
server,
|
|
330
509
|
address: () => server.address(),
|
|
331
|
-
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
|
+
})),
|
|
332
515
|
};
|
|
333
516
|
}
|