@memtensor/memos-local-openclaw-plugin 1.0.5 → 1.0.6-beta.10
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/dist/capture/index.d.ts.map +1 -1
- package/dist/capture/index.js +24 -0
- package/dist/capture/index.js.map +1 -1
- package/dist/client/connector.d.ts.map +1 -1
- package/dist/client/connector.js +33 -5
- package/dist/client/connector.js.map +1 -1
- package/dist/client/hub.d.ts.map +1 -1
- package/dist/client/hub.js +4 -0
- package/dist/client/hub.js.map +1 -1
- package/dist/hub/server.d.ts +2 -0
- package/dist/hub/server.d.ts.map +1 -1
- package/dist/hub/server.js +116 -54
- package/dist/hub/server.js.map +1 -1
- package/dist/ingest/providers/index.d.ts +4 -0
- package/dist/ingest/providers/index.d.ts.map +1 -1
- package/dist/ingest/providers/index.js +32 -86
- package/dist/ingest/providers/index.js.map +1 -1
- package/dist/ingest/providers/openai.d.ts.map +1 -1
- package/dist/ingest/providers/openai.js +29 -13
- package/dist/ingest/providers/openai.js.map +1 -1
- package/dist/recall/engine.d.ts.map +1 -1
- package/dist/recall/engine.js +33 -32
- package/dist/recall/engine.js.map +1 -1
- package/dist/storage/sqlite.d.ts +43 -7
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +179 -58
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/tools/memory-get.d.ts.map +1 -1
- package/dist/tools/memory-get.js +4 -1
- package/dist/tools/memory-get.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/update-check.d.ts.map +1 -1
- package/dist/update-check.js +2 -7
- package/dist/update-check.js.map +1 -1
- package/dist/viewer/html.d.ts.map +1 -1
- package/dist/viewer/html.js +115 -27
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +25 -0
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +503 -206
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +273 -282
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
- package/scripts/native-binding.cjs +32 -0
- package/scripts/postinstall.cjs +24 -11
- package/src/capture/index.ts +36 -0
- package/src/client/connector.ts +32 -5
- package/src/client/hub.ts +4 -0
- package/src/hub/server.ts +110 -50
- package/src/ingest/providers/index.ts +37 -92
- package/src/ingest/providers/openai.ts +31 -13
- package/src/recall/engine.ts +32 -30
- package/src/storage/sqlite.ts +196 -63
- package/src/tools/memory-get.ts +4 -1
- package/src/types.ts +2 -0
- package/src/update-check.ts +2 -7
- package/src/viewer/html.ts +115 -27
- package/src/viewer/server.ts +483 -172
- package/prebuilds/darwin-arm64/better_sqlite3.node +0 -0
- package/prebuilds/darwin-x64/better_sqlite3.node +0 -0
- package/prebuilds/linux-x64/better_sqlite3.node +0 -0
- package/prebuilds/win32-x64/better_sqlite3.node +0 -0
- package/telemetry.credentials.json +0 -5
package/src/viewer/server.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
|
-
import { execSync, exec } from "node:child_process";
|
|
4
|
+
import { execSync, exec, execFile } from "node:child_process";
|
|
5
5
|
import fs from "node:fs";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import readline from "node:readline";
|
|
@@ -22,9 +22,72 @@ import type { Logger, Chunk, PluginContext, MemosLocalConfig } from "../types";
|
|
|
22
22
|
import { viewerHTML } from "./html";
|
|
23
23
|
import { v4 as uuid } from "uuid";
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
export interface MigrationStepFailureCounts {
|
|
26
|
+
summarization: number;
|
|
27
|
+
dedup: number;
|
|
28
|
+
embedding: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface MigrationStateSnapshot {
|
|
32
|
+
phase: string;
|
|
33
|
+
stored: number;
|
|
34
|
+
skipped: number;
|
|
35
|
+
merged: number;
|
|
36
|
+
errors: number;
|
|
37
|
+
processed: number;
|
|
38
|
+
total: number;
|
|
39
|
+
lastItem: any;
|
|
40
|
+
done: boolean;
|
|
41
|
+
stopped: boolean;
|
|
42
|
+
stepFailures: MigrationStepFailureCounts;
|
|
43
|
+
success: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function createInitialStepFailures(): MigrationStepFailureCounts {
|
|
47
|
+
return { summarization: 0, dedup: 0, embedding: 0 };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function computeMigrationSuccess(state: Pick<MigrationStateSnapshot, "errors" | "stepFailures">): boolean {
|
|
51
|
+
const sf = state.stepFailures;
|
|
52
|
+
return state.errors === 0 && sf.summarization === 0 && sf.dedup === 0 && sf.embedding === 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function createInitialMigrationState(): MigrationStateSnapshot {
|
|
56
|
+
const stepFailures = createInitialStepFailures();
|
|
57
|
+
return {
|
|
58
|
+
phase: "",
|
|
59
|
+
stored: 0,
|
|
60
|
+
skipped: 0,
|
|
61
|
+
merged: 0,
|
|
62
|
+
errors: 0,
|
|
63
|
+
processed: 0,
|
|
64
|
+
total: 0,
|
|
65
|
+
lastItem: null,
|
|
66
|
+
done: false,
|
|
67
|
+
stopped: false,
|
|
68
|
+
stepFailures,
|
|
69
|
+
success: computeMigrationSuccess({ errors: 0, stepFailures }),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function applyMigrationItemToState(state: MigrationStateSnapshot, d: any): void {
|
|
74
|
+
if (d.status === "stored") state.stored++;
|
|
75
|
+
else if (d.status === "skipped" || d.status === "duplicate") state.skipped++;
|
|
76
|
+
else if (d.status === "merged") state.merged++;
|
|
77
|
+
else if (d.status === "error") state.errors++;
|
|
78
|
+
|
|
79
|
+
if (Array.isArray(d.stepFailures)) {
|
|
80
|
+
for (const step of d.stepFailures) {
|
|
81
|
+
if (step === "summarization") state.stepFailures.summarization++;
|
|
82
|
+
else if (step === "dedup") state.stepFailures.dedup++;
|
|
83
|
+
else if (step === "embedding") state.stepFailures.embedding++;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
state.processed = d.index ?? state.processed + 1;
|
|
88
|
+
state.total = d.total ?? state.total;
|
|
89
|
+
state.lastItem = d;
|
|
90
|
+
state.success = computeMigrationSuccess(state);
|
|
28
91
|
}
|
|
29
92
|
|
|
30
93
|
export interface ViewerServerOptions {
|
|
@@ -67,18 +130,7 @@ export class ViewerServer {
|
|
|
67
130
|
private resetToken: string;
|
|
68
131
|
private migrationRunning = false;
|
|
69
132
|
private migrationAbort = false;
|
|
70
|
-
private migrationState:
|
|
71
|
-
phase: string;
|
|
72
|
-
stored: number;
|
|
73
|
-
skipped: number;
|
|
74
|
-
merged: number;
|
|
75
|
-
errors: number;
|
|
76
|
-
processed: number;
|
|
77
|
-
total: number;
|
|
78
|
-
lastItem: any;
|
|
79
|
-
done: boolean;
|
|
80
|
-
stopped: boolean;
|
|
81
|
-
} = { phase: "", stored: 0, skipped: 0, merged: 0, errors: 0, processed: 0, total: 0, lastItem: null, done: false, stopped: false };
|
|
133
|
+
private migrationState: MigrationStateSnapshot = createInitialMigrationState();
|
|
82
134
|
private migrationSSEClients: http.ServerResponse[] = [];
|
|
83
135
|
|
|
84
136
|
private ppRunning = false;
|
|
@@ -491,13 +543,12 @@ export class ViewerServer {
|
|
|
491
543
|
if (chunkIds.length > 0) {
|
|
492
544
|
try {
|
|
493
545
|
const placeholders = chunkIds.map(() => "?").join(",");
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
}
|
|
546
|
+
if (this.sharingRole === "hub") {
|
|
547
|
+
const sharedRows = db.prepare(`SELECT source_chunk_id, visibility, group_id FROM hub_memories WHERE source_chunk_id IN (${placeholders})`).all(...chunkIds) as Array<{ source_chunk_id: string; visibility: string; group_id: string | null }>;
|
|
548
|
+
for (const r of sharedRows) sharingMap.set(r.source_chunk_id, r);
|
|
549
|
+
} else {
|
|
550
|
+
const teamMetaRows = db.prepare(`SELECT chunk_id, visibility, group_id FROM team_shared_chunks WHERE chunk_id IN (${placeholders})`).all(...chunkIds) as Array<{ chunk_id: string; visibility: string; group_id: string | null }>;
|
|
551
|
+
for (const r of teamMetaRows) sharingMap.set(r.chunk_id, { visibility: r.visibility, group_id: r.group_id });
|
|
501
552
|
}
|
|
502
553
|
const localRows = db.prepare(`SELECT chunk_id, original_owner, shared_at FROM local_shared_memories WHERE chunk_id IN (${placeholders})`).all(...chunkIds) as Array<{ chunk_id: string; original_owner: string; shared_at: number }>;
|
|
503
554
|
for (const r of localRows) localShareMap.set(r.chunk_id, r);
|
|
@@ -564,7 +615,7 @@ export class ViewerServer {
|
|
|
564
615
|
const db = (this.store as any).db;
|
|
565
616
|
const items = tasks.map((t) => {
|
|
566
617
|
const meta = db.prepare("SELECT skill_status, owner FROM tasks WHERE id = ?").get(t.id) as { skill_status: string | null; owner: string | null } | undefined;
|
|
567
|
-
const
|
|
618
|
+
const hubTask = this.getHubTaskForLocal(t.id);
|
|
568
619
|
return {
|
|
569
620
|
id: t.id,
|
|
570
621
|
sessionKey: t.sessionKey,
|
|
@@ -576,7 +627,7 @@ export class ViewerServer {
|
|
|
576
627
|
chunkCount: this.store.countChunksByTask(t.id),
|
|
577
628
|
skillStatus: meta?.skill_status ?? null,
|
|
578
629
|
owner: meta?.owner ?? "agent:main",
|
|
579
|
-
sharingVisibility:
|
|
630
|
+
sharingVisibility: hubTask?.visibility ?? null,
|
|
580
631
|
};
|
|
581
632
|
});
|
|
582
633
|
|
|
@@ -611,7 +662,7 @@ export class ViewerServer {
|
|
|
611
662
|
const db = (this.store as any).db;
|
|
612
663
|
const meta = db.prepare("SELECT skill_status, skill_reason FROM tasks WHERE id = ?").get(taskId) as
|
|
613
664
|
{ skill_status: string | null; skill_reason: string | null } | undefined;
|
|
614
|
-
const
|
|
665
|
+
const hubTask = this.getHubTaskForLocal(taskId);
|
|
615
666
|
|
|
616
667
|
this.jsonResponse(res, {
|
|
617
668
|
id: task.id,
|
|
@@ -626,9 +677,9 @@ export class ViewerServer {
|
|
|
626
677
|
skillStatus: meta?.skill_status ?? null,
|
|
627
678
|
skillReason: meta?.skill_reason ?? null,
|
|
628
679
|
skillLinks,
|
|
629
|
-
sharingVisibility:
|
|
630
|
-
sharingGroupId:
|
|
631
|
-
hubTaskId:
|
|
680
|
+
sharingVisibility: hubTask?.visibility ?? null,
|
|
681
|
+
sharingGroupId: hubTask?.group_id ?? null,
|
|
682
|
+
hubTaskId: hubTask ? true : false,
|
|
632
683
|
});
|
|
633
684
|
}
|
|
634
685
|
|
|
@@ -818,10 +869,9 @@ export class ViewerServer {
|
|
|
818
869
|
if (visibility) {
|
|
819
870
|
skills = skills.filter(s => s.visibility === visibility);
|
|
820
871
|
}
|
|
821
|
-
const db = (this.store as any).db;
|
|
822
872
|
const enriched = skills.map(s => {
|
|
823
|
-
const
|
|
824
|
-
return { ...s, sharingVisibility:
|
|
873
|
+
const hubSkill = this.getHubSkillForLocal(s.id);
|
|
874
|
+
return { ...s, sharingVisibility: hubSkill?.visibility ?? null };
|
|
825
875
|
});
|
|
826
876
|
this.jsonResponse(res, { skills: enriched });
|
|
827
877
|
}
|
|
@@ -839,11 +889,10 @@ export class ViewerServer {
|
|
|
839
889
|
const relatedTasks = this.store.getTasksBySkill(skillId);
|
|
840
890
|
const files = fs.existsSync(skill.dirPath) ? this.walkDir(skill.dirPath, skill.dirPath) : [];
|
|
841
891
|
|
|
842
|
-
const
|
|
843
|
-
const sharedSkill = db.prepare("SELECT visibility, group_id FROM hub_skills WHERE source_skill_id = ? ORDER BY updated_at DESC LIMIT 1").get(skillId) as { visibility: string | null; group_id: string | null } | undefined;
|
|
892
|
+
const hubSkill = this.getHubSkillForLocal(skillId);
|
|
844
893
|
|
|
845
894
|
this.jsonResponse(res, {
|
|
846
|
-
skill: { ...skill, sharingVisibility:
|
|
895
|
+
skill: { ...skill, sharingVisibility: hubSkill?.visibility ?? null, sharingGroupId: hubSkill?.group_id ?? null },
|
|
847
896
|
versions: versions.map(v => ({
|
|
848
897
|
id: v.id,
|
|
849
898
|
version: v.version,
|
|
@@ -982,7 +1031,7 @@ export class ViewerServer {
|
|
|
982
1031
|
method: "POST",
|
|
983
1032
|
body: JSON.stringify({ visibility: "public", groupId: null, metadata: bundle.metadata, bundle: bundle.bundle }),
|
|
984
1033
|
}) as any;
|
|
985
|
-
if (hubClient.userId) {
|
|
1034
|
+
if (this.sharingRole === "hub" && hubClient.userId) {
|
|
986
1035
|
const existing = this.store.getHubSkillBySource(hubClient.userId, skillId);
|
|
987
1036
|
this.store.upsertHubSkill({
|
|
988
1037
|
id: response?.skillId ?? existing?.id ?? crypto.randomUUID(),
|
|
@@ -992,6 +1041,14 @@ export class ViewerServer {
|
|
|
992
1041
|
bundle: JSON.stringify(bundle.bundle), qualityScore: skill.qualityScore,
|
|
993
1042
|
createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
|
|
994
1043
|
});
|
|
1044
|
+
} else {
|
|
1045
|
+
const conn = this.store.getClientHubConnection();
|
|
1046
|
+
this.store.upsertTeamSharedSkill(skillId, {
|
|
1047
|
+
hubSkillId: String(response?.skillId ?? ""),
|
|
1048
|
+
visibility: "public",
|
|
1049
|
+
groupId: null,
|
|
1050
|
+
hubInstanceId: conn?.hubInstanceId ?? "",
|
|
1051
|
+
});
|
|
995
1052
|
}
|
|
996
1053
|
hubSynced = true;
|
|
997
1054
|
this.log.info(`Skill "${skill.name}" published to Hub`);
|
|
@@ -1000,7 +1057,8 @@ export class ViewerServer {
|
|
|
1000
1057
|
method: "POST",
|
|
1001
1058
|
body: JSON.stringify({ sourceSkillId: skillId }),
|
|
1002
1059
|
});
|
|
1003
|
-
if (hubClient.userId) this.store.deleteHubSkillBySource(hubClient.userId, skillId);
|
|
1060
|
+
if (this.sharingRole === "hub" && hubClient.userId) this.store.deleteHubSkillBySource(hubClient.userId, skillId);
|
|
1061
|
+
else this.store.deleteTeamSharedSkill(skillId);
|
|
1004
1062
|
hubSynced = true;
|
|
1005
1063
|
this.log.info(`Skill "${skill.name}" unpublished from Hub`);
|
|
1006
1064
|
}
|
|
@@ -1271,7 +1329,8 @@ export class ViewerServer {
|
|
|
1271
1329
|
createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
|
|
1272
1330
|
});
|
|
1273
1331
|
} else if (hubClient.userId) {
|
|
1274
|
-
this.store.
|
|
1332
|
+
const conn = this.store.getClientHubConnection();
|
|
1333
|
+
this.store.upsertTeamSharedChunk(chunkId, { hubMemoryId: memoryId, visibility: "public", groupId: null, hubInstanceId: conn?.hubInstanceId ?? "" });
|
|
1275
1334
|
}
|
|
1276
1335
|
hubSynced = true;
|
|
1277
1336
|
} else {
|
|
@@ -1284,7 +1343,7 @@ export class ViewerServer {
|
|
|
1284
1343
|
await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/unshare", {
|
|
1285
1344
|
method: "POST", body: JSON.stringify({ sourceChunkId: chunkId }),
|
|
1286
1345
|
});
|
|
1287
|
-
if (hubClient.userId) this.store.deleteHubMemoryBySource(hubClient.userId, chunkId);
|
|
1346
|
+
if (this.sharingRole === "hub" && hubClient.userId) this.store.deleteHubMemoryBySource(hubClient.userId, chunkId);
|
|
1288
1347
|
this.store.deleteTeamSharedChunk(chunkId);
|
|
1289
1348
|
hubSynced = true;
|
|
1290
1349
|
} catch (err) { this.log.warn(`Failed to unshare memory from team: ${err}`); }
|
|
@@ -1297,7 +1356,7 @@ export class ViewerServer {
|
|
|
1297
1356
|
await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/unshare", {
|
|
1298
1357
|
method: "POST", body: JSON.stringify({ sourceChunkId: chunkId }),
|
|
1299
1358
|
});
|
|
1300
|
-
if (hubClient.userId) this.store.deleteHubMemoryBySource(hubClient.userId, chunkId);
|
|
1359
|
+
if (this.sharingRole === "hub" && hubClient.userId) this.store.deleteHubMemoryBySource(hubClient.userId, chunkId);
|
|
1301
1360
|
this.store.deleteTeamSharedChunk(chunkId);
|
|
1302
1361
|
hubSynced = true;
|
|
1303
1362
|
} catch (err) { this.log.warn(`Failed to unshare memory from team: ${err}`); }
|
|
@@ -1351,21 +1410,24 @@ export class ViewerServer {
|
|
|
1351
1410
|
chunks: chunks.map((c) => ({ id: c.id, hubTaskId: refreshedTask.id, sourceTaskId: refreshedTask.id, sourceChunkId: c.id, role: c.role, content: c.content, summary: c.summary, kind: c.kind, groupId: null, visibility: "public", createdAt: c.createdAt ?? Date.now() })),
|
|
1352
1411
|
}),
|
|
1353
1412
|
});
|
|
1354
|
-
|
|
1413
|
+
const hubTaskId = String((response as any)?.taskId ?? "");
|
|
1414
|
+
if (this.sharingRole === "hub" && hubClient.userId) {
|
|
1355
1415
|
const existing = this.store.getHubTaskBySource(hubClient.userId, taskId);
|
|
1356
1416
|
this.store.upsertHubTask({
|
|
1357
|
-
id:
|
|
1417
|
+
id: hubTaskId || existing?.id || crypto.randomUUID(),
|
|
1358
1418
|
sourceTaskId: taskId, sourceUserId: hubClient.userId, title: refreshedTask.title ?? "",
|
|
1359
1419
|
summary: refreshedTask.summary ?? "", groupId: null, visibility: "public",
|
|
1360
1420
|
createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
|
|
1361
1421
|
});
|
|
1362
1422
|
}
|
|
1423
|
+
const conn = this.store.getClientHubConnection();
|
|
1424
|
+
this.store.markTaskShared(taskId, hubTaskId, chunks.length, "public", null, conn?.hubInstanceId ?? "");
|
|
1363
1425
|
hubSynced = true;
|
|
1364
1426
|
}
|
|
1365
1427
|
if (!isLocalShared) {
|
|
1366
1428
|
const originalOwner = task.owner;
|
|
1367
1429
|
const db = (this.store as any).db;
|
|
1368
|
-
db.prepare("INSERT INTO local_shared_tasks (task_id, hub_task_id, original_owner, shared_at) VALUES (?, ?, ?, ?) ON CONFLICT(task_id) DO UPDATE SET original_owner = excluded.original_owner, shared_at = excluded.shared_at").run(taskId, "", originalOwner, Date.now());
|
|
1430
|
+
db.prepare("INSERT INTO local_shared_tasks (task_id, hub_task_id, original_owner, hub_instance_id, shared_at) VALUES (?, ?, ?, ?, ?) ON CONFLICT(task_id) DO UPDATE SET original_owner = excluded.original_owner, hub_instance_id = excluded.hub_instance_id, shared_at = excluded.shared_at").run(taskId, "", originalOwner, "", Date.now());
|
|
1369
1431
|
db.prepare("UPDATE tasks SET owner = 'public' WHERE id = ?").run(taskId);
|
|
1370
1432
|
}
|
|
1371
1433
|
}
|
|
@@ -1385,7 +1447,8 @@ export class ViewerServer {
|
|
|
1385
1447
|
await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/unshare", {
|
|
1386
1448
|
method: "POST", body: JSON.stringify({ sourceTaskId: taskId }),
|
|
1387
1449
|
});
|
|
1388
|
-
if (hubClient.userId) this.store.deleteHubTaskBySource(hubClient.userId, taskId);
|
|
1450
|
+
if (this.sharingRole === "hub" && hubClient.userId) this.store.deleteHubTaskBySource(hubClient.userId, taskId);
|
|
1451
|
+
else this.store.downgradeTeamSharedTaskToLocal(taskId);
|
|
1389
1452
|
hubSynced = true;
|
|
1390
1453
|
} catch (err) { this.log.warn(`Failed to unshare task from team: ${err}`); }
|
|
1391
1454
|
}
|
|
@@ -1397,7 +1460,8 @@ export class ViewerServer {
|
|
|
1397
1460
|
await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/unshare", {
|
|
1398
1461
|
method: "POST", body: JSON.stringify({ sourceTaskId: taskId }),
|
|
1399
1462
|
});
|
|
1400
|
-
if (hubClient.userId) this.store.deleteHubTaskBySource(hubClient.userId, taskId);
|
|
1463
|
+
if (this.sharingRole === "hub" && hubClient.userId) this.store.deleteHubTaskBySource(hubClient.userId, taskId);
|
|
1464
|
+
else if (!isLocalShared) this.store.unmarkTaskShared(taskId);
|
|
1401
1465
|
hubSynced = true;
|
|
1402
1466
|
} catch (err) { this.log.warn(`Failed to unshare task from team: ${err}`); }
|
|
1403
1467
|
}
|
|
@@ -1454,16 +1518,20 @@ export class ViewerServer {
|
|
|
1454
1518
|
method: "POST",
|
|
1455
1519
|
body: JSON.stringify({ visibility: "public", groupId: null, metadata: bundle.metadata, bundle: bundle.bundle }),
|
|
1456
1520
|
});
|
|
1457
|
-
|
|
1521
|
+
const hubSkillId = String((response as any)?.skillId ?? "");
|
|
1522
|
+
if (this.sharingRole === "hub" && hubClient.userId) {
|
|
1458
1523
|
const existing = this.store.getHubSkillBySource(hubClient.userId, skillId);
|
|
1459
1524
|
this.store.upsertHubSkill({
|
|
1460
|
-
id:
|
|
1525
|
+
id: hubSkillId || existing?.id || crypto.randomUUID(),
|
|
1461
1526
|
sourceSkillId: skillId, sourceUserId: hubClient.userId,
|
|
1462
1527
|
name: skill.name, description: skill.description, version: skill.version,
|
|
1463
1528
|
groupId: null, visibility: "public",
|
|
1464
1529
|
bundle: JSON.stringify(bundle.bundle), qualityScore: skill.qualityScore,
|
|
1465
1530
|
createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
|
|
1466
1531
|
});
|
|
1532
|
+
} else {
|
|
1533
|
+
const conn = this.store.getClientHubConnection();
|
|
1534
|
+
this.store.upsertTeamSharedSkill(skillId, { hubSkillId, visibility: "public", groupId: null, hubInstanceId: conn?.hubInstanceId ?? "" });
|
|
1467
1535
|
}
|
|
1468
1536
|
hubSynced = true;
|
|
1469
1537
|
}
|
|
@@ -1480,7 +1548,8 @@ export class ViewerServer {
|
|
|
1480
1548
|
await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/unpublish", {
|
|
1481
1549
|
method: "POST", body: JSON.stringify({ sourceSkillId: skillId }),
|
|
1482
1550
|
});
|
|
1483
|
-
if (hubClient.userId) this.store.deleteHubSkillBySource(hubClient.userId, skillId);
|
|
1551
|
+
if (this.sharingRole === "hub" && hubClient.userId) this.store.deleteHubSkillBySource(hubClient.userId, skillId);
|
|
1552
|
+
else this.store.deleteTeamSharedSkill(skillId);
|
|
1484
1553
|
hubSynced = true;
|
|
1485
1554
|
} catch (err) { this.log.warn(`Failed to unpublish skill from team: ${err}`); }
|
|
1486
1555
|
}
|
|
@@ -1492,7 +1561,8 @@ export class ViewerServer {
|
|
|
1492
1561
|
await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/unpublish", {
|
|
1493
1562
|
method: "POST", body: JSON.stringify({ sourceSkillId: skillId }),
|
|
1494
1563
|
});
|
|
1495
|
-
if (hubClient.userId) this.store.deleteHubSkillBySource(hubClient.userId, skillId);
|
|
1564
|
+
if (this.sharingRole === "hub" && hubClient.userId) this.store.deleteHubSkillBySource(hubClient.userId, skillId);
|
|
1565
|
+
else this.store.deleteTeamSharedSkill(skillId);
|
|
1496
1566
|
hubSynced = true;
|
|
1497
1567
|
} catch (err) { this.log.warn(`Failed to unpublish skill from team: ${err}`); }
|
|
1498
1568
|
}
|
|
@@ -1506,29 +1576,53 @@ export class ViewerServer {
|
|
|
1506
1576
|
});
|
|
1507
1577
|
}
|
|
1508
1578
|
|
|
1579
|
+
private get sharingRole(): string | undefined {
|
|
1580
|
+
return this.ctx?.config?.sharing?.role;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
private isCurrentClientHubInstance(hubInstanceId?: string): boolean {
|
|
1584
|
+
if (this.sharingRole !== "client") return true;
|
|
1585
|
+
const scopedHubInstanceId = String(hubInstanceId ?? "");
|
|
1586
|
+
if (!scopedHubInstanceId) return true;
|
|
1587
|
+
const currentHubInstanceId = this.store.getClientHubConnection()?.hubInstanceId ?? "";
|
|
1588
|
+
if (!currentHubInstanceId) return true;
|
|
1589
|
+
return scopedHubInstanceId === currentHubInstanceId;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1509
1592
|
private getHubMemoryForChunk(chunkId: string): any {
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1593
|
+
if (this.sharingRole === "hub") {
|
|
1594
|
+
const db = (this.store as any).db;
|
|
1595
|
+
return db.prepare("SELECT * FROM hub_memories WHERE source_chunk_id = ? LIMIT 1").get(chunkId);
|
|
1596
|
+
}
|
|
1513
1597
|
const ts = this.store.getTeamSharedChunk(chunkId);
|
|
1514
|
-
if (ts) {
|
|
1515
|
-
return {
|
|
1516
|
-
source_chunk_id: chunkId,
|
|
1517
|
-
visibility: ts.visibility,
|
|
1518
|
-
group_id: ts.groupId,
|
|
1519
|
-
};
|
|
1598
|
+
if (ts && this.isCurrentClientHubInstance(ts.hubInstanceId)) {
|
|
1599
|
+
return { source_chunk_id: chunkId, visibility: ts.visibility, group_id: ts.groupId };
|
|
1520
1600
|
}
|
|
1521
1601
|
return undefined;
|
|
1522
1602
|
}
|
|
1523
1603
|
|
|
1524
1604
|
private getHubTaskForLocal(taskId: string): any {
|
|
1525
|
-
|
|
1526
|
-
|
|
1605
|
+
if (this.sharingRole === "hub") {
|
|
1606
|
+
const db = (this.store as any).db;
|
|
1607
|
+
return db.prepare("SELECT * FROM hub_tasks WHERE source_task_id = ? LIMIT 1").get(taskId);
|
|
1608
|
+
}
|
|
1609
|
+
const shared = this.store.getLocalSharedTask(taskId);
|
|
1610
|
+
if (shared && shared.hubTaskId && this.isCurrentClientHubInstance(shared.hubInstanceId)) {
|
|
1611
|
+
return { source_task_id: taskId, visibility: shared.visibility, group_id: shared.groupId };
|
|
1612
|
+
}
|
|
1613
|
+
return undefined;
|
|
1527
1614
|
}
|
|
1528
1615
|
|
|
1529
1616
|
private getHubSkillForLocal(skillId: string): any {
|
|
1530
|
-
|
|
1531
|
-
|
|
1617
|
+
if (this.sharingRole === "hub") {
|
|
1618
|
+
const db = (this.store as any).db;
|
|
1619
|
+
return db.prepare("SELECT * FROM hub_skills WHERE source_skill_id = ? LIMIT 1").get(skillId);
|
|
1620
|
+
}
|
|
1621
|
+
const ts = this.store.getTeamSharedSkill(skillId);
|
|
1622
|
+
if (ts && this.isCurrentClientHubInstance(ts.hubInstanceId)) {
|
|
1623
|
+
return { source_skill_id: skillId, visibility: ts.visibility, group_id: ts.groupId };
|
|
1624
|
+
}
|
|
1625
|
+
return undefined;
|
|
1532
1626
|
}
|
|
1533
1627
|
|
|
1534
1628
|
private handleDeleteSession(res: http.ServerResponse, url: URL): void {
|
|
@@ -1821,18 +1915,25 @@ export class ViewerServer {
|
|
|
1821
1915
|
|
|
1822
1916
|
private handleRetryJoin(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
1823
1917
|
this.readBody(req, async (_body) => {
|
|
1824
|
-
if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
1918
|
+
if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable", errorCode: "sharing_unavailable" });
|
|
1825
1919
|
const sharing = this.ctx.config.sharing;
|
|
1826
1920
|
if (!sharing?.enabled || sharing.role !== "client") {
|
|
1827
|
-
return this.jsonResponse(res, { ok: false, error: "not_in_client_mode" });
|
|
1921
|
+
return this.jsonResponse(res, { ok: false, error: "not_in_client_mode", errorCode: "not_in_client_mode" });
|
|
1828
1922
|
}
|
|
1829
1923
|
const hubAddress = sharing.client?.hubAddress ?? "";
|
|
1830
1924
|
const teamToken = sharing.client?.teamToken ?? "";
|
|
1831
1925
|
if (!hubAddress || !teamToken) {
|
|
1832
|
-
return this.jsonResponse(res, { ok: false, error: "missing_hub_address_or_team_token" });
|
|
1926
|
+
return this.jsonResponse(res, { ok: false, error: "missing_hub_address_or_team_token", errorCode: "missing_config" });
|
|
1927
|
+
}
|
|
1928
|
+
const hubUrl = normalizeHubUrl(hubAddress);
|
|
1929
|
+
|
|
1930
|
+
try {
|
|
1931
|
+
await hubRequestJson(hubUrl, "", "/api/v1/hub/info", { method: "GET" });
|
|
1932
|
+
} catch {
|
|
1933
|
+
return this.jsonResponse(res, { ok: false, error: "hub_unreachable", errorCode: "hub_unreachable" });
|
|
1833
1934
|
}
|
|
1935
|
+
|
|
1834
1936
|
try {
|
|
1835
|
-
const hubUrl = normalizeHubUrl(hubAddress);
|
|
1836
1937
|
const os = await import("os");
|
|
1837
1938
|
const nickname = sharing.client?.nickname;
|
|
1838
1939
|
const username = nickname || os.userInfo().username || "user";
|
|
@@ -1844,6 +1945,11 @@ export class ViewerServer {
|
|
|
1844
1945
|
body: JSON.stringify({ teamToken, username, deviceName: hostname, reapply: true, identityKey: existingIdentityKey }),
|
|
1845
1946
|
}) as any;
|
|
1846
1947
|
const returnedIdentityKey = String(result.identityKey || existingIdentityKey || "");
|
|
1948
|
+
let hubInstanceId = persisted?.hubInstanceId || "";
|
|
1949
|
+
try {
|
|
1950
|
+
const info = await hubRequestJson(hubUrl, "", "/api/v1/hub/info", { method: "GET" }) as any;
|
|
1951
|
+
hubInstanceId = String(info?.hubInstanceId ?? hubInstanceId);
|
|
1952
|
+
} catch { /* best-effort */ }
|
|
1847
1953
|
this.store.setClientHubConnection({
|
|
1848
1954
|
hubUrl,
|
|
1849
1955
|
userId: String(result.userId || ""),
|
|
@@ -1853,10 +1959,21 @@ export class ViewerServer {
|
|
|
1853
1959
|
connectedAt: Date.now(),
|
|
1854
1960
|
identityKey: returnedIdentityKey,
|
|
1855
1961
|
lastKnownStatus: result.status || "",
|
|
1962
|
+
hubInstanceId,
|
|
1856
1963
|
});
|
|
1964
|
+
if (result.status === "blocked") {
|
|
1965
|
+
return this.jsonResponse(res, { ok: false, error: "blocked", errorCode: "blocked" });
|
|
1966
|
+
}
|
|
1857
1967
|
this.jsonResponse(res, { ok: true, status: result.status || "pending" });
|
|
1858
1968
|
} catch (err) {
|
|
1859
|
-
|
|
1969
|
+
const errStr = String(err);
|
|
1970
|
+
if (errStr.includes("(409)") || errStr.includes("username_taken")) {
|
|
1971
|
+
return this.jsonResponse(res, { ok: false, error: "username_taken", errorCode: "username_taken" });
|
|
1972
|
+
}
|
|
1973
|
+
if (errStr.includes("(403)") || errStr.includes("invalid_team_token")) {
|
|
1974
|
+
return this.jsonResponse(res, { ok: false, error: "invalid_team_token", errorCode: "invalid_team_token" });
|
|
1975
|
+
}
|
|
1976
|
+
this.jsonResponse(res, { ok: false, error: errStr, errorCode: "unknown" });
|
|
1860
1977
|
}
|
|
1861
1978
|
});
|
|
1862
1979
|
}
|
|
@@ -2060,9 +2177,10 @@ export class ViewerServer {
|
|
|
2060
2177
|
}),
|
|
2061
2178
|
});
|
|
2062
2179
|
const hubUserId = hubClient.userId;
|
|
2063
|
-
|
|
2180
|
+
const hubTaskId = String((response as any)?.taskId ?? task.id);
|
|
2181
|
+
if (this.sharingRole === "hub" && hubUserId) {
|
|
2064
2182
|
this.store.upsertHubTask({
|
|
2065
|
-
id:
|
|
2183
|
+
id: hubTaskId,
|
|
2066
2184
|
sourceTaskId: task.id,
|
|
2067
2185
|
sourceUserId: hubUserId,
|
|
2068
2186
|
title: task.title,
|
|
@@ -2072,6 +2190,9 @@ export class ViewerServer {
|
|
|
2072
2190
|
createdAt: task.startedAt ?? Date.now(),
|
|
2073
2191
|
updatedAt: task.updatedAt ?? Date.now(),
|
|
2074
2192
|
});
|
|
2193
|
+
} else {
|
|
2194
|
+
const conn = this.store.getClientHubConnection();
|
|
2195
|
+
this.store.markTaskShared(task.id, hubTaskId, chunks.length, visibility, groupId, conn?.hubInstanceId ?? "");
|
|
2075
2196
|
}
|
|
2076
2197
|
this.jsonResponse(res, { ok: true, taskId, visibility, response });
|
|
2077
2198
|
} catch (err) {
|
|
@@ -2094,7 +2215,9 @@ export class ViewerServer {
|
|
|
2094
2215
|
body: JSON.stringify({ sourceTaskId: task.id }),
|
|
2095
2216
|
});
|
|
2096
2217
|
const hubUserId = hubClient.userId;
|
|
2097
|
-
if (hubUserId) this.store.deleteHubTaskBySource(hubUserId, task.id);
|
|
2218
|
+
if (this.sharingRole === "hub" && hubUserId) this.store.deleteHubTaskBySource(hubUserId, task.id);
|
|
2219
|
+
else if (task.owner === "public") this.store.downgradeTeamSharedTaskToLocal(task.id);
|
|
2220
|
+
else this.store.unmarkTaskShared(task.id);
|
|
2098
2221
|
this.jsonResponse(res, { ok: true, taskId });
|
|
2099
2222
|
} catch (err) {
|
|
2100
2223
|
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
@@ -2146,7 +2269,8 @@ export class ViewerServer {
|
|
|
2146
2269
|
updatedAt: now,
|
|
2147
2270
|
});
|
|
2148
2271
|
} else if (hubClient.userId) {
|
|
2149
|
-
this.store.
|
|
2272
|
+
const conn = this.store.getClientHubConnection();
|
|
2273
|
+
this.store.upsertTeamSharedChunk(chunk.id, { hubMemoryId: mid, visibility, groupId, hubInstanceId: conn?.hubInstanceId ?? "" });
|
|
2150
2274
|
}
|
|
2151
2275
|
this.jsonResponse(res, { ok: true, chunkId, visibility, response });
|
|
2152
2276
|
} catch (err) {
|
|
@@ -2167,8 +2291,8 @@ export class ViewerServer {
|
|
|
2167
2291
|
body: JSON.stringify({ sourceChunkId: chunkId }),
|
|
2168
2292
|
});
|
|
2169
2293
|
const hubUserId = hubClient.userId;
|
|
2170
|
-
if (hubUserId) this.store.deleteHubMemoryBySource(hubUserId, chunkId);
|
|
2171
|
-
this.store.deleteTeamSharedChunk(chunkId);
|
|
2294
|
+
if (this.sharingRole === "hub" && hubUserId) this.store.deleteHubMemoryBySource(hubUserId, chunkId);
|
|
2295
|
+
else this.store.deleteTeamSharedChunk(chunkId);
|
|
2172
2296
|
this.jsonResponse(res, { ok: true, chunkId });
|
|
2173
2297
|
} catch (err) {
|
|
2174
2298
|
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
@@ -2213,7 +2337,7 @@ export class ViewerServer {
|
|
|
2213
2337
|
}),
|
|
2214
2338
|
});
|
|
2215
2339
|
const hubUserId = hubClient.userId;
|
|
2216
|
-
if (hubUserId) {
|
|
2340
|
+
if (this.sharingRole === "hub" && hubUserId) {
|
|
2217
2341
|
const existing = this.store.getHubSkillBySource(hubUserId, skillId);
|
|
2218
2342
|
this.store.upsertHubSkill({
|
|
2219
2343
|
id: (response as any)?.skillId ?? existing?.id ?? crypto.randomUUID(),
|
|
@@ -2229,6 +2353,14 @@ export class ViewerServer {
|
|
|
2229
2353
|
createdAt: existing?.createdAt ?? Date.now(),
|
|
2230
2354
|
updatedAt: Date.now(),
|
|
2231
2355
|
});
|
|
2356
|
+
} else {
|
|
2357
|
+
const conn = this.store.getClientHubConnection();
|
|
2358
|
+
this.store.upsertTeamSharedSkill(skillId, {
|
|
2359
|
+
hubSkillId: String((response as any)?.skillId ?? ""),
|
|
2360
|
+
visibility,
|
|
2361
|
+
groupId,
|
|
2362
|
+
hubInstanceId: conn?.hubInstanceId ?? "",
|
|
2363
|
+
});
|
|
2232
2364
|
}
|
|
2233
2365
|
this.jsonResponse(res, { ok: true, skillId, visibility, response });
|
|
2234
2366
|
} catch (err) {
|
|
@@ -2251,7 +2383,8 @@ export class ViewerServer {
|
|
|
2251
2383
|
body: JSON.stringify({ sourceSkillId: skill.id }),
|
|
2252
2384
|
});
|
|
2253
2385
|
const hubUserId = hubClient.userId;
|
|
2254
|
-
if (hubUserId) this.store.deleteHubSkillBySource(hubUserId, skill.id);
|
|
2386
|
+
if (this.sharingRole === "hub" && hubUserId) this.store.deleteHubSkillBySource(hubUserId, skill.id);
|
|
2387
|
+
else this.store.deleteTeamSharedSkill(skill.id);
|
|
2255
2388
|
this.jsonResponse(res, { ok: true, skillId });
|
|
2256
2389
|
} catch (err) {
|
|
2257
2390
|
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
@@ -2717,19 +2850,21 @@ export class ViewerServer {
|
|
|
2717
2850
|
const isClient = newEnabled && newRole === "client";
|
|
2718
2851
|
if (wasClient && !isClient) {
|
|
2719
2852
|
await this.withdrawOrLeaveHub();
|
|
2853
|
+
this.store.clearAllTeamSharingState();
|
|
2720
2854
|
this.store.clearClientHubConnection();
|
|
2721
|
-
this.log.info("Client hub connection cleared (sharing disabled or role changed)");
|
|
2855
|
+
this.log.info("Client hub connection and team sharing state cleared (sharing disabled or role changed)");
|
|
2722
2856
|
}
|
|
2723
2857
|
|
|
2724
2858
|
if (wasClient && isClient) {
|
|
2725
2859
|
const newClientAddr = String((merged.client as Record<string, unknown>)?.hubAddress || "");
|
|
2726
2860
|
if (newClientAddr && oldClientHubAddress && normalizeHubUrl(newClientAddr) !== normalizeHubUrl(oldClientHubAddress)) {
|
|
2727
2861
|
this.notifyHubLeave();
|
|
2862
|
+
this.store.clearAllTeamSharingState();
|
|
2728
2863
|
const oldConn = this.store.getClientHubConnection();
|
|
2729
2864
|
if (oldConn) {
|
|
2730
|
-
this.store.setClientHubConnection({ ...oldConn, hubUrl: normalizeHubUrl(newClientAddr), userToken: "", lastKnownStatus: "hub_changed" });
|
|
2865
|
+
this.store.setClientHubConnection({ ...oldConn, hubUrl: normalizeHubUrl(newClientAddr), userToken: "", hubInstanceId: "", lastKnownStatus: "hub_changed" });
|
|
2731
2866
|
}
|
|
2732
|
-
this.log.info("Client hub connection
|
|
2867
|
+
this.log.info("Client hub connection and team sharing state cleared (switched to different Hub)");
|
|
2733
2868
|
}
|
|
2734
2869
|
}
|
|
2735
2870
|
|
|
@@ -2751,20 +2886,25 @@ export class ViewerServer {
|
|
|
2751
2886
|
const nowClient = Boolean(finalSharing?.enabled) && finalSharing?.role === "client";
|
|
2752
2887
|
const previouslyClient = oldSharingEnabled && oldSharingRole === "client";
|
|
2753
2888
|
let joinStatus: string | undefined;
|
|
2889
|
+
let joinError: string | undefined;
|
|
2754
2890
|
if (nowClient && !previouslyClient) {
|
|
2755
2891
|
try {
|
|
2756
2892
|
joinStatus = await this.autoJoinOnSave(finalSharing);
|
|
2757
2893
|
} catch (e) {
|
|
2758
|
-
|
|
2894
|
+
const msg = String(e instanceof Error ? e.message : e);
|
|
2895
|
+
this.log.warn(`Auto-join on save failed: ${msg}`);
|
|
2896
|
+
if (msg === "hub_unreachable" || msg === "username_taken" || msg === "invalid_team_token") {
|
|
2897
|
+
joinError = msg;
|
|
2898
|
+
}
|
|
2759
2899
|
}
|
|
2760
2900
|
}
|
|
2761
2901
|
|
|
2762
|
-
|
|
2902
|
+
if (joinError) {
|
|
2903
|
+
this.jsonResponse(res, { ok: true, joinError, restart: false });
|
|
2904
|
+
return;
|
|
2905
|
+
}
|
|
2763
2906
|
|
|
2764
|
-
|
|
2765
|
-
this.log.info("config-save: triggering gateway restart via SIGUSR1...");
|
|
2766
|
-
try { process.kill(process.pid, "SIGUSR1"); } catch (sig) { this.log.warn(`SIGUSR1 failed: ${sig}`); }
|
|
2767
|
-
}, 500);
|
|
2907
|
+
this.jsonResponseAndRestart(res, { ok: true, joinStatus, restart: true }, "config-save");
|
|
2768
2908
|
} catch (e) {
|
|
2769
2909
|
this.log.warn(`handleSaveConfig error: ${e}`);
|
|
2770
2910
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
@@ -2779,17 +2919,43 @@ export class ViewerServer {
|
|
|
2779
2919
|
const teamToken = String(clientCfg?.teamToken || "");
|
|
2780
2920
|
if (!hubAddress || !teamToken) return undefined;
|
|
2781
2921
|
const hubUrl = normalizeHubUrl(hubAddress);
|
|
2922
|
+
|
|
2923
|
+
try {
|
|
2924
|
+
await hubRequestJson(hubUrl, "", "/api/v1/hub/info", { method: "GET" });
|
|
2925
|
+
} catch {
|
|
2926
|
+
throw new Error("hub_unreachable");
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2782
2929
|
const os = await import("os");
|
|
2783
2930
|
const nickname = String(clientCfg?.nickname || "");
|
|
2784
2931
|
const username = nickname || os.userInfo().username || "user";
|
|
2785
2932
|
const hostname = os.hostname() || "unknown";
|
|
2786
2933
|
const persisted = this.store.getClientHubConnection();
|
|
2787
2934
|
const existingIdentityKey = persisted?.identityKey || "";
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2935
|
+
|
|
2936
|
+
let result: any;
|
|
2937
|
+
try {
|
|
2938
|
+
result = await hubRequestJson(hubUrl, "", "/api/v1/hub/join", {
|
|
2939
|
+
method: "POST",
|
|
2940
|
+
body: JSON.stringify({ teamToken, username, deviceName: hostname, identityKey: existingIdentityKey }),
|
|
2941
|
+
});
|
|
2942
|
+
} catch (err) {
|
|
2943
|
+
const errStr = String(err);
|
|
2944
|
+
if (errStr.includes("(409)") || errStr.includes("username_taken")) {
|
|
2945
|
+
throw new Error("username_taken");
|
|
2946
|
+
}
|
|
2947
|
+
if (errStr.includes("(403)") || errStr.includes("invalid_team_token")) {
|
|
2948
|
+
throw new Error("invalid_team_token");
|
|
2949
|
+
}
|
|
2950
|
+
throw err;
|
|
2951
|
+
}
|
|
2952
|
+
|
|
2792
2953
|
const returnedIdentityKey = String(result.identityKey || existingIdentityKey || "");
|
|
2954
|
+
let hubInstanceId = persisted?.hubInstanceId || "";
|
|
2955
|
+
try {
|
|
2956
|
+
const info = await hubRequestJson(hubUrl, "", "/api/v1/hub/info", { method: "GET" }) as any;
|
|
2957
|
+
hubInstanceId = String(info?.hubInstanceId ?? hubInstanceId);
|
|
2958
|
+
} catch { /* best-effort */ }
|
|
2793
2959
|
this.store.setClientHubConnection({
|
|
2794
2960
|
hubUrl,
|
|
2795
2961
|
userId: String(result.userId || ""),
|
|
@@ -2799,6 +2965,7 @@ export class ViewerServer {
|
|
|
2799
2965
|
connectedAt: Date.now(),
|
|
2800
2966
|
identityKey: returnedIdentityKey,
|
|
2801
2967
|
lastKnownStatus: result.status || "",
|
|
2968
|
+
hubInstanceId,
|
|
2802
2969
|
});
|
|
2803
2970
|
this.log.info(`Auto-join on save: status=${result.status}, userId=${result.userId}`);
|
|
2804
2971
|
if (result.userToken) {
|
|
@@ -2811,6 +2978,7 @@ export class ViewerServer {
|
|
|
2811
2978
|
this.readBody(_req, async () => {
|
|
2812
2979
|
try {
|
|
2813
2980
|
await this.withdrawOrLeaveHub();
|
|
2981
|
+
this.store.clearAllTeamSharingState();
|
|
2814
2982
|
this.store.clearClientHubConnection();
|
|
2815
2983
|
|
|
2816
2984
|
const configPath = this.getOpenClawConfigPath();
|
|
@@ -2829,12 +2997,7 @@ export class ViewerServer {
|
|
|
2829
2997
|
}
|
|
2830
2998
|
}
|
|
2831
2999
|
|
|
2832
|
-
this.
|
|
2833
|
-
|
|
2834
|
-
setTimeout(() => {
|
|
2835
|
-
this.log.info("handleLeaveTeam: triggering gateway restart via SIGUSR1...");
|
|
2836
|
-
try { process.kill(process.pid, "SIGUSR1"); } catch (sig) { this.log.warn(`SIGUSR1 failed: ${sig}`); }
|
|
2837
|
-
}, 500);
|
|
3000
|
+
this.jsonResponseAndRestart(res, { ok: true, restart: true }, "handleLeaveTeam");
|
|
2838
3001
|
} catch (e) {
|
|
2839
3002
|
this.log.warn(`handleLeaveTeam error: ${e}`);
|
|
2840
3003
|
this.jsonResponse(res, { ok: false, error: String(e) });
|
|
@@ -3000,14 +3163,43 @@ export class ViewerServer {
|
|
|
3000
3163
|
}
|
|
3001
3164
|
}
|
|
3002
3165
|
} catch {}
|
|
3003
|
-
const
|
|
3166
|
+
const baseUrl = hubUrl.replace(/\/+$/, "");
|
|
3167
|
+
const infoUrl = baseUrl + "/api/v1/hub/info";
|
|
3004
3168
|
const ctrl = new AbortController();
|
|
3005
3169
|
const timeout = setTimeout(() => ctrl.abort(), 8000);
|
|
3006
3170
|
try {
|
|
3007
|
-
const r = await fetch(
|
|
3171
|
+
const r = await fetch(infoUrl, { signal: ctrl.signal });
|
|
3008
3172
|
clearTimeout(timeout);
|
|
3009
3173
|
if (!r.ok) { this.jsonResponse(res, { ok: false, error: `HTTP ${r.status}` }); return; }
|
|
3010
3174
|
const info = await r.json() as Record<string, unknown>;
|
|
3175
|
+
|
|
3176
|
+
const { teamToken, nickname } = JSON.parse(body);
|
|
3177
|
+
if (teamToken) {
|
|
3178
|
+
const username = (typeof nickname === "string" && nickname.trim()) || os.userInfo().username || "user";
|
|
3179
|
+
const persisted = this.store.getClientHubConnection();
|
|
3180
|
+
const identityKey = persisted?.identityKey || "";
|
|
3181
|
+
try {
|
|
3182
|
+
const joinR = await fetch(baseUrl + "/api/v1/hub/join", {
|
|
3183
|
+
method: "POST",
|
|
3184
|
+
headers: { "content-type": "application/json" },
|
|
3185
|
+
body: JSON.stringify({ teamToken, username, identityKey, deviceName: os.hostname(), dryRun: true }),
|
|
3186
|
+
});
|
|
3187
|
+
const joinData = await joinR.json() as Record<string, unknown>;
|
|
3188
|
+
if (!joinR.ok && joinData.error === "username_taken") {
|
|
3189
|
+
this.jsonResponse(res, { ok: false, error: "username_taken", teamName: info.teamName || "" });
|
|
3190
|
+
return;
|
|
3191
|
+
}
|
|
3192
|
+
if (!joinR.ok && joinData.error === "invalid_team_token") {
|
|
3193
|
+
this.jsonResponse(res, { ok: false, error: "invalid_team_token", teamName: info.teamName || "" });
|
|
3194
|
+
return;
|
|
3195
|
+
}
|
|
3196
|
+
if (joinR.ok && joinData.status === "blocked") {
|
|
3197
|
+
this.jsonResponse(res, { ok: false, error: "blocked", teamName: info.teamName || "" });
|
|
3198
|
+
return;
|
|
3199
|
+
}
|
|
3200
|
+
} catch { /* join check is best-effort; connection itself is OK */ }
|
|
3201
|
+
}
|
|
3202
|
+
|
|
3011
3203
|
this.jsonResponse(res, { ok: true, teamName: info.teamName || "", apiVersion: info.apiVersion || "" });
|
|
3012
3204
|
} catch (e: unknown) {
|
|
3013
3205
|
clearTimeout(timeout);
|
|
@@ -3092,26 +3284,35 @@ export class ViewerServer {
|
|
|
3092
3284
|
}
|
|
3093
3285
|
|
|
3094
3286
|
private async handleUpdateCheck(res: http.ServerResponse): Promise<void> {
|
|
3287
|
+
const sendNoStore = (data: unknown, statusCode = 200) => {
|
|
3288
|
+
res.writeHead(statusCode, {
|
|
3289
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
3290
|
+
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
|
|
3291
|
+
"Pragma": "no-cache",
|
|
3292
|
+
"Expires": "0",
|
|
3293
|
+
});
|
|
3294
|
+
res.end(JSON.stringify(data));
|
|
3295
|
+
};
|
|
3095
3296
|
try {
|
|
3096
3297
|
const pkgPath = this.findPluginPackageJson();
|
|
3097
3298
|
if (!pkgPath) {
|
|
3098
|
-
|
|
3299
|
+
sendNoStore({ updateAvailable: false, error: "package.json not found" });
|
|
3099
3300
|
return;
|
|
3100
3301
|
}
|
|
3101
3302
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
3102
3303
|
const current = pkg.version as string;
|
|
3103
3304
|
const name = pkg.name as string;
|
|
3104
3305
|
if (!current || !name) {
|
|
3105
|
-
|
|
3306
|
+
sendNoStore({ updateAvailable: false, current });
|
|
3106
3307
|
return;
|
|
3107
3308
|
}
|
|
3108
3309
|
const { computeUpdateCheck } = await import("../update-check");
|
|
3109
3310
|
const result = await computeUpdateCheck(name, current, fetch, 6_000);
|
|
3110
3311
|
if (!result) {
|
|
3111
|
-
|
|
3312
|
+
sendNoStore({ updateAvailable: false, current, packageName: name });
|
|
3112
3313
|
return;
|
|
3113
3314
|
}
|
|
3114
|
-
|
|
3315
|
+
sendNoStore({
|
|
3115
3316
|
updateAvailable: result.updateAvailable,
|
|
3116
3317
|
current: result.current,
|
|
3117
3318
|
latest: result.latest,
|
|
@@ -3122,7 +3323,7 @@ export class ViewerServer {
|
|
|
3122
3323
|
});
|
|
3123
3324
|
} catch (e) {
|
|
3124
3325
|
this.log.warn(`handleUpdateCheck error: ${e}`);
|
|
3125
|
-
|
|
3326
|
+
sendNoStore({ updateAvailable: false, error: String(e) });
|
|
3126
3327
|
}
|
|
3127
3328
|
}
|
|
3128
3329
|
|
|
@@ -3131,13 +3332,14 @@ export class ViewerServer {
|
|
|
3131
3332
|
req.on("data", (chunk: Buffer) => { body += chunk.toString(); });
|
|
3132
3333
|
req.on("end", () => {
|
|
3133
3334
|
try {
|
|
3134
|
-
const { packageSpec: rawSpec } = JSON.parse(body);
|
|
3335
|
+
const { packageSpec: rawSpec, targetVersion: rawTargetVersion } = JSON.parse(body);
|
|
3135
3336
|
if (!rawSpec || typeof rawSpec !== "string") {
|
|
3136
3337
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
3137
3338
|
res.end(JSON.stringify({ ok: false, error: "Missing packageSpec" }));
|
|
3138
3339
|
return;
|
|
3139
3340
|
}
|
|
3140
3341
|
const packageSpec = rawSpec.trim().replace(/^(?:npx\s+)?openclaw\s+plugins\s+install\s+/i, "");
|
|
3342
|
+
const targetVersion = typeof rawTargetVersion === "string" ? rawTargetVersion.trim() : "";
|
|
3141
3343
|
const allowed = /^@[\w-]+\/[\w.-]+(@[\w.-]+)?$/;
|
|
3142
3344
|
this.log.info(`update-install: received packageSpec="${packageSpec}" (len=${packageSpec.length})`);
|
|
3143
3345
|
if (!allowed.test(packageSpec)) {
|
|
@@ -3154,16 +3356,42 @@ export class ViewerServer {
|
|
|
3154
3356
|
const shortName = pluginName?.replace(/^@[\w-]+\//, "") ?? "memos-local-openclaw-plugin";
|
|
3155
3357
|
const extDir = path.join(os.homedir(), ".openclaw", "extensions", shortName);
|
|
3156
3358
|
const tmpDir = path.join(os.tmpdir(), `openclaw-update-${Date.now()}`);
|
|
3359
|
+
const backupDir = path.join(path.dirname(extDir), `${shortName}.backup-${Date.now()}`);
|
|
3360
|
+
let backupReady = false;
|
|
3361
|
+
|
|
3362
|
+
const cleanupTmpDir = () => {
|
|
3363
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
3364
|
+
};
|
|
3365
|
+
const rollbackInstall = () => {
|
|
3366
|
+
try { fs.rmSync(extDir, { recursive: true, force: true }); } catch {}
|
|
3367
|
+
if (!backupReady) return;
|
|
3368
|
+
try {
|
|
3369
|
+
fs.renameSync(backupDir, extDir);
|
|
3370
|
+
backupReady = false;
|
|
3371
|
+
this.log.info(`update-install: restored previous version from ${backupDir}`);
|
|
3372
|
+
} catch (restoreErr: any) {
|
|
3373
|
+
this.log.warn(`update-install: failed to restore previous version: ${restoreErr?.message ?? restoreErr}`);
|
|
3374
|
+
}
|
|
3375
|
+
};
|
|
3376
|
+
const discardBackup = () => {
|
|
3377
|
+
if (!backupReady) return;
|
|
3378
|
+
try {
|
|
3379
|
+
fs.rmSync(backupDir, { recursive: true, force: true });
|
|
3380
|
+
backupReady = false;
|
|
3381
|
+
} catch (cleanupErr: any) {
|
|
3382
|
+
this.log.warn(`update-install: failed to remove backup dir ${backupDir}: ${cleanupErr?.message ?? cleanupErr}`);
|
|
3383
|
+
}
|
|
3384
|
+
};
|
|
3157
3385
|
|
|
3158
3386
|
// Download via npm pack, extract, and replace extension dir.
|
|
3159
3387
|
// Does NOT touch openclaw.json → no config watcher SIGUSR1.
|
|
3160
3388
|
this.log.info(`update-install: downloading ${packageSpec} via npm pack...`);
|
|
3161
3389
|
fs.mkdirSync(tmpDir, { recursive: true });
|
|
3162
|
-
exec(`npm pack ${packageSpec} --pack-destination ${tmpDir}`, { timeout: 60_000 }, (packErr, packOut) => {
|
|
3390
|
+
exec(`npm pack ${packageSpec} --pack-destination ${tmpDir} --prefer-online`, { timeout: 60_000 }, (packErr, packOut) => {
|
|
3163
3391
|
if (packErr) {
|
|
3164
3392
|
this.log.warn(`update-install: npm pack failed: ${packErr.message}`);
|
|
3165
3393
|
this.jsonResponse(res, { ok: false, error: `Download failed: ${packErr.message}` });
|
|
3166
|
-
|
|
3394
|
+
cleanupTmpDir();
|
|
3167
3395
|
return;
|
|
3168
3396
|
}
|
|
3169
3397
|
const tgzFile = packOut.trim().split("\n").pop()!;
|
|
@@ -3176,7 +3404,7 @@ export class ViewerServer {
|
|
|
3176
3404
|
if (tarErr) {
|
|
3177
3405
|
this.log.warn(`update-install: tar extract failed: ${tarErr.message}`);
|
|
3178
3406
|
this.jsonResponse(res, { ok: false, error: `Extract failed: ${tarErr.message}` });
|
|
3179
|
-
|
|
3407
|
+
cleanupTmpDir();
|
|
3180
3408
|
return;
|
|
3181
3409
|
}
|
|
3182
3410
|
|
|
@@ -3184,61 +3412,79 @@ export class ViewerServer {
|
|
|
3184
3412
|
const srcDir = path.join(extractDir, "package");
|
|
3185
3413
|
if (!fs.existsSync(srcDir)) {
|
|
3186
3414
|
this.jsonResponse(res, { ok: false, error: "Extracted package has no 'package' dir" });
|
|
3187
|
-
|
|
3415
|
+
cleanupTmpDir();
|
|
3188
3416
|
return;
|
|
3189
3417
|
}
|
|
3190
3418
|
|
|
3191
3419
|
// Replace extension directory
|
|
3192
3420
|
this.log.info(`update-install: replacing ${extDir}...`);
|
|
3193
|
-
try {
|
|
3194
|
-
|
|
3195
|
-
|
|
3421
|
+
try {
|
|
3422
|
+
fs.mkdirSync(path.dirname(extDir), { recursive: true });
|
|
3423
|
+
try { fs.rmSync(backupDir, { recursive: true, force: true }); } catch {}
|
|
3424
|
+
if (fs.existsSync(extDir)) {
|
|
3425
|
+
fs.renameSync(extDir, backupDir);
|
|
3426
|
+
backupReady = true;
|
|
3427
|
+
}
|
|
3428
|
+
fs.renameSync(srcDir, extDir);
|
|
3429
|
+
} catch (replaceErr: any) {
|
|
3430
|
+
this.log.warn(`update-install: replace failed: ${replaceErr?.message ?? replaceErr}`);
|
|
3431
|
+
cleanupTmpDir();
|
|
3432
|
+
rollbackInstall();
|
|
3433
|
+
this.jsonResponse(res, { ok: false, error: `Replace failed: ${replaceErr?.message ?? replaceErr}` });
|
|
3434
|
+
return;
|
|
3435
|
+
}
|
|
3196
3436
|
|
|
3197
3437
|
// Install dependencies
|
|
3198
3438
|
this.log.info(`update-install: installing dependencies...`);
|
|
3199
|
-
|
|
3439
|
+
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
3440
|
+
execFile(npmCmd, ["install", "--omit=dev", "--ignore-scripts"], { cwd: extDir, timeout: 120_000 }, (npmErr, npmOut, npmStderr) => {
|
|
3200
3441
|
if (npmErr) {
|
|
3201
|
-
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
3202
3442
|
this.log.warn(`update-install: npm install failed: ${npmErr.message}`);
|
|
3443
|
+
cleanupTmpDir();
|
|
3444
|
+
rollbackInstall();
|
|
3203
3445
|
this.jsonResponse(res, { ok: false, error: `Dependency install failed: ${npmStderr || npmErr.message}` });
|
|
3204
3446
|
return;
|
|
3205
3447
|
}
|
|
3206
3448
|
|
|
3207
|
-
|
|
3208
|
-
exec(`cd ${extDir} && npm rebuild better-sqlite3`, { timeout: 60_000 }, (rebuildErr, rebuildOut, rebuildStderr) => {
|
|
3449
|
+
execFile(npmCmd, ["rebuild", "better-sqlite3"], { cwd: extDir, timeout: 60_000 }, (rebuildErr, rebuildOut, rebuildStderr) => {
|
|
3209
3450
|
if (rebuildErr) {
|
|
3210
3451
|
this.log.warn(`update-install: better-sqlite3 rebuild failed: ${rebuildErr.message}`);
|
|
3211
3452
|
const stderr = String(rebuildStderr || "").trim();
|
|
3212
3453
|
if (stderr) this.log.warn(`update-install: rebuild stderr: ${stderr.slice(0, 500)}`);
|
|
3213
|
-
// Continue so postinstall.cjs can run (it will try rebuild again and show user guidance)
|
|
3214
3454
|
}
|
|
3215
3455
|
|
|
3216
|
-
// Run postinstall.cjs: legacy cleanup, skill install, version marker, and optional sqlite re-check
|
|
3217
3456
|
this.log.info(`update-install: running postinstall...`);
|
|
3218
|
-
|
|
3219
|
-
|
|
3457
|
+
execFile(process.execPath, ["scripts/postinstall.cjs"], { cwd: extDir, timeout: 180_000 }, (postErr, postOut, postStderr) => {
|
|
3458
|
+
cleanupTmpDir();
|
|
3220
3459
|
|
|
3221
3460
|
if (postErr) {
|
|
3222
3461
|
this.log.warn(`update-install: postinstall failed: ${postErr.message}`);
|
|
3223
3462
|
const postStderrStr = String(postStderr || "").trim();
|
|
3224
3463
|
if (postStderrStr) this.log.warn(`update-install: postinstall stderr: ${postStderrStr.slice(0, 500)}`);
|
|
3225
|
-
|
|
3464
|
+
rollbackInstall();
|
|
3465
|
+
this.jsonResponse(res, { ok: false, error: `Postinstall failed: ${postStderrStr || postErr.message}` });
|
|
3466
|
+
return;
|
|
3226
3467
|
}
|
|
3227
3468
|
|
|
3228
|
-
// Read new version
|
|
3229
3469
|
let newVersion = "unknown";
|
|
3230
3470
|
try {
|
|
3231
3471
|
const newPkg = JSON.parse(fs.readFileSync(path.join(extDir, "package.json"), "utf-8"));
|
|
3232
3472
|
newVersion = newPkg.version ?? newVersion;
|
|
3233
3473
|
} catch {}
|
|
3234
3474
|
|
|
3235
|
-
|
|
3236
|
-
|
|
3475
|
+
if (targetVersion && newVersion !== targetVersion) {
|
|
3476
|
+
this.log.warn(`update-install: version mismatch! expected=${targetVersion}, got=${newVersion} — rolling back`);
|
|
3477
|
+
rollbackInstall();
|
|
3478
|
+
this.jsonResponse(res, {
|
|
3479
|
+
ok: false,
|
|
3480
|
+
error: `Version mismatch: expected ${targetVersion} but downloaded ${newVersion}. npm cache may be stale — please try again.`,
|
|
3481
|
+
});
|
|
3482
|
+
return;
|
|
3483
|
+
}
|
|
3237
3484
|
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
}, 500);
|
|
3485
|
+
discardBackup();
|
|
3486
|
+
this.log.info(`update-install: success! Updated to ${newVersion}`);
|
|
3487
|
+
this.jsonResponseAndRestart(res, { ok: true, version: newVersion }, "update-install");
|
|
3242
3488
|
});
|
|
3243
3489
|
});
|
|
3244
3490
|
});
|
|
@@ -3575,7 +3821,7 @@ export class ViewerServer {
|
|
|
3575
3821
|
} else if (this.migrationState.done) {
|
|
3576
3822
|
const evtName = this.migrationState.stopped ? "stopped" : "done";
|
|
3577
3823
|
res.write(`event: state\ndata: ${JSON.stringify(this.migrationState)}\n\n`);
|
|
3578
|
-
res.write(`event: ${evtName}\ndata: ${JSON.stringify({ ok:
|
|
3824
|
+
res.write(`event: ${evtName}\ndata: ${JSON.stringify({ ok: this.migrationState.success, ...this.migrationState })}\n\n`);
|
|
3579
3825
|
res.end();
|
|
3580
3826
|
} else {
|
|
3581
3827
|
res.end();
|
|
@@ -3616,19 +3862,12 @@ export class ViewerServer {
|
|
|
3616
3862
|
this.migrationSSEClients = this.migrationSSEClients.filter(c => c !== res);
|
|
3617
3863
|
});
|
|
3618
3864
|
|
|
3619
|
-
this.
|
|
3620
|
-
this.migrationState = { phase: "", stored: 0, skipped: 0, merged: 0, errors: 0, processed: 0, total: 0, lastItem: null, done: false, stopped: false };
|
|
3865
|
+
this.migrationState = createInitialMigrationState();
|
|
3621
3866
|
|
|
3622
3867
|
const send = (event: string, data: unknown) => {
|
|
3623
3868
|
if (event === "item") {
|
|
3624
3869
|
const d = data as any;
|
|
3625
|
-
|
|
3626
|
-
else if (d.status === "skipped" || d.status === "duplicate") this.migrationState.skipped++;
|
|
3627
|
-
else if (d.status === "merged") this.migrationState.merged++;
|
|
3628
|
-
else if (d.status === "error") this.migrationState.errors++;
|
|
3629
|
-
this.migrationState.processed = d.index ?? this.migrationState.processed + 1;
|
|
3630
|
-
this.migrationState.total = d.total ?? this.migrationState.total;
|
|
3631
|
-
this.migrationState.lastItem = d;
|
|
3870
|
+
applyMigrationItemToState(this.migrationState, d);
|
|
3632
3871
|
} else if (event === "phase") {
|
|
3633
3872
|
this.migrationState.phase = (data as any).phase;
|
|
3634
3873
|
} else if (event === "progress") {
|
|
@@ -3641,11 +3880,13 @@ export class ViewerServer {
|
|
|
3641
3880
|
this.runMigration(send, opts.sources, concurrency).finally(() => {
|
|
3642
3881
|
this.migrationRunning = false;
|
|
3643
3882
|
this.migrationState.done = true;
|
|
3883
|
+
this.migrationState.success = computeMigrationSuccess(this.migrationState);
|
|
3884
|
+
const donePayload = { ok: this.migrationState.success, ...this.migrationState };
|
|
3644
3885
|
if (this.migrationAbort) {
|
|
3645
3886
|
this.migrationState.stopped = true;
|
|
3646
|
-
this.broadcastSSE("stopped",
|
|
3887
|
+
this.broadcastSSE("stopped", donePayload);
|
|
3647
3888
|
} else {
|
|
3648
|
-
this.broadcastSSE("done",
|
|
3889
|
+
this.broadcastSSE("done", donePayload);
|
|
3649
3890
|
}
|
|
3650
3891
|
this.migrationAbort = false;
|
|
3651
3892
|
const clientsToClose = [...this.migrationSSEClients];
|
|
@@ -3742,11 +3983,24 @@ export class ViewerServer {
|
|
|
3742
3983
|
}
|
|
3743
3984
|
|
|
3744
3985
|
try {
|
|
3745
|
-
const
|
|
3986
|
+
const stepFailures: Array<"summarization" | "dedup" | "embedding"> = [];
|
|
3987
|
+
let summary = "";
|
|
3988
|
+
try {
|
|
3989
|
+
summary = await summarizer.summarize(row.text);
|
|
3990
|
+
} catch (err) {
|
|
3991
|
+
stepFailures.push("summarization");
|
|
3992
|
+
this.log.warn(`Migration summarization failed: ${err}`);
|
|
3993
|
+
}
|
|
3994
|
+
if (!summary) {
|
|
3995
|
+
stepFailures.push("summarization");
|
|
3996
|
+
summary = row.text.slice(0, 200);
|
|
3997
|
+
}
|
|
3998
|
+
|
|
3746
3999
|
let embedding: number[] | null = null;
|
|
3747
4000
|
try {
|
|
3748
4001
|
[embedding] = await this.embedder.embed([summary]);
|
|
3749
4002
|
} catch (err) {
|
|
4003
|
+
stepFailures.push("embedding");
|
|
3750
4004
|
this.log.warn(`Migration embed failed: ${err}`);
|
|
3751
4005
|
}
|
|
3752
4006
|
|
|
@@ -3765,26 +4019,31 @@ export class ViewerServer {
|
|
|
3765
4019
|
}).filter(c => c.summary);
|
|
3766
4020
|
|
|
3767
4021
|
if (candidates.length > 0) {
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
4022
|
+
try {
|
|
4023
|
+
const dedupResult = await summarizer.judgeDedup(summary, candidates);
|
|
4024
|
+
if (dedupResult?.action === "DUPLICATE" && dedupResult.targetIndex) {
|
|
4025
|
+
const targetId = candidates[dedupResult.targetIndex - 1]?.chunkId;
|
|
4026
|
+
if (targetId) {
|
|
4027
|
+
dedupStatus = "duplicate";
|
|
4028
|
+
dedupTarget = targetId;
|
|
4029
|
+
dedupReason = dedupResult.reason;
|
|
4030
|
+
}
|
|
4031
|
+
} else if (dedupResult?.action === "UPDATE" && dedupResult.targetIndex && dedupResult.mergedSummary) {
|
|
4032
|
+
const targetId = candidates[dedupResult.targetIndex - 1]?.chunkId;
|
|
4033
|
+
if (targetId) {
|
|
4034
|
+
this.store.updateChunkSummaryAndContent(targetId, dedupResult.mergedSummary, row.text);
|
|
4035
|
+
try {
|
|
4036
|
+
const [newEmb] = await this.embedder.embed([dedupResult.mergedSummary]);
|
|
4037
|
+
if (newEmb) this.store.upsertEmbedding(targetId, newEmb);
|
|
4038
|
+
} catch { /* best-effort */ }
|
|
4039
|
+
dedupStatus = "merged";
|
|
4040
|
+
dedupTarget = targetId;
|
|
4041
|
+
dedupReason = dedupResult.reason;
|
|
4042
|
+
}
|
|
3787
4043
|
}
|
|
4044
|
+
} catch (err) {
|
|
4045
|
+
stepFailures.push("dedup");
|
|
4046
|
+
this.log.warn(`Migration dedup judgment failed: ${err}`);
|
|
3788
4047
|
}
|
|
3789
4048
|
}
|
|
3790
4049
|
}
|
|
@@ -3810,8 +4069,8 @@ export class ViewerServer {
|
|
|
3810
4069
|
mergeCount: 0,
|
|
3811
4070
|
lastHitAt: null,
|
|
3812
4071
|
mergeHistory: "[]",
|
|
3813
|
-
createdAt:
|
|
3814
|
-
updatedAt:
|
|
4072
|
+
createdAt: Number(row.updated_at) < 1e12 ? Number(row.updated_at) * 1000 : Number(row.updated_at),
|
|
4073
|
+
updatedAt: Number(row.updated_at) < 1e12 ? Number(row.updated_at) * 1000 : Number(row.updated_at),
|
|
3815
4074
|
};
|
|
3816
4075
|
|
|
3817
4076
|
this.store.insertChunk(chunk);
|
|
@@ -3827,7 +4086,13 @@ export class ViewerServer {
|
|
|
3827
4086
|
preview: row.text.slice(0, 120),
|
|
3828
4087
|
summary: summary.slice(0, 80),
|
|
3829
4088
|
source: file,
|
|
4089
|
+
stepFailures,
|
|
3830
4090
|
});
|
|
4091
|
+
if (stepFailures.length > 0) {
|
|
4092
|
+
this.log.warn(`[MIGRATION] sqlite item imported with step failures: ${stepFailures.join(",")}`);
|
|
4093
|
+
} else {
|
|
4094
|
+
this.log.info("[MIGRATION] sqlite item imported successfully (all steps)");
|
|
4095
|
+
}
|
|
3831
4096
|
} catch (err) {
|
|
3832
4097
|
totalErrors++;
|
|
3833
4098
|
send("item", {
|
|
@@ -3957,11 +4222,24 @@ export class ViewerServer {
|
|
|
3957
4222
|
}
|
|
3958
4223
|
|
|
3959
4224
|
try {
|
|
3960
|
-
const
|
|
4225
|
+
const stepFailures: Array<"summarization" | "dedup" | "embedding"> = [];
|
|
4226
|
+
let summary = "";
|
|
4227
|
+
try {
|
|
4228
|
+
summary = await summarizer.summarize(content);
|
|
4229
|
+
} catch (err) {
|
|
4230
|
+
stepFailures.push("summarization");
|
|
4231
|
+
this.log.warn(`Migration summarization failed: ${err}`);
|
|
4232
|
+
}
|
|
4233
|
+
if (!summary) {
|
|
4234
|
+
stepFailures.push("summarization");
|
|
4235
|
+
summary = content.slice(0, 200);
|
|
4236
|
+
}
|
|
4237
|
+
|
|
3961
4238
|
let embedding: number[] | null = null;
|
|
3962
4239
|
try {
|
|
3963
4240
|
[embedding] = await this.embedder.embed([summary]);
|
|
3964
4241
|
} catch (err) {
|
|
4242
|
+
stepFailures.push("embedding");
|
|
3965
4243
|
this.log.warn(`Migration embed failed: ${err}`);
|
|
3966
4244
|
}
|
|
3967
4245
|
|
|
@@ -3980,17 +4258,22 @@ export class ViewerServer {
|
|
|
3980
4258
|
}).filter(c => c.summary);
|
|
3981
4259
|
|
|
3982
4260
|
if (candidates.length > 0) {
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
4261
|
+
try {
|
|
4262
|
+
const dedupResult = await summarizer.judgeDedup(summary, candidates);
|
|
4263
|
+
if (dedupResult?.action === "DUPLICATE" && dedupResult.targetIndex) {
|
|
4264
|
+
const targetId = candidates[dedupResult.targetIndex - 1]?.chunkId;
|
|
4265
|
+
if (targetId) { dedupStatus = "duplicate"; dedupTarget = targetId; dedupReason = dedupResult.reason; }
|
|
4266
|
+
} else if (dedupResult?.action === "UPDATE" && dedupResult.targetIndex && dedupResult.mergedSummary) {
|
|
4267
|
+
const targetId = candidates[dedupResult.targetIndex - 1]?.chunkId;
|
|
4268
|
+
if (targetId) {
|
|
4269
|
+
this.store.updateChunkSummaryAndContent(targetId, dedupResult.mergedSummary, content);
|
|
4270
|
+
try { const [newEmb] = await this.embedder.embed([dedupResult.mergedSummary]); if (newEmb) this.store.upsertEmbedding(targetId, newEmb); } catch { /* best-effort */ }
|
|
4271
|
+
dedupStatus = "merged"; dedupTarget = targetId; dedupReason = dedupResult.reason;
|
|
4272
|
+
}
|
|
3993
4273
|
}
|
|
4274
|
+
} catch (err) {
|
|
4275
|
+
stepFailures.push("dedup");
|
|
4276
|
+
this.log.warn(`Migration dedup judgment failed: ${err}`);
|
|
3994
4277
|
}
|
|
3995
4278
|
}
|
|
3996
4279
|
}
|
|
@@ -4010,7 +4293,12 @@ export class ViewerServer {
|
|
|
4010
4293
|
if (embedding && dedupStatus === "active") this.store.upsertEmbedding(chunkId, embedding);
|
|
4011
4294
|
|
|
4012
4295
|
totalStored++;
|
|
4013
|
-
send("item", { index: idx, total: totalMsgs, status: dedupStatus === "active" ? "stored" : dedupStatus, preview: content.slice(0, 120), summary: summary.slice(0, 80), source: file, agent: agentId, role: msgRole });
|
|
4296
|
+
send("item", { index: idx, total: totalMsgs, status: dedupStatus === "active" ? "stored" : dedupStatus, preview: content.slice(0, 120), summary: summary.slice(0, 80), source: file, agent: agentId, role: msgRole, stepFailures });
|
|
4297
|
+
if (stepFailures.length > 0) {
|
|
4298
|
+
this.log.warn(`[MIGRATION] session item imported with step failures: ${stepFailures.join(",")}`);
|
|
4299
|
+
} else {
|
|
4300
|
+
this.log.info("[MIGRATION] session item imported successfully (all steps)");
|
|
4301
|
+
}
|
|
4014
4302
|
} catch (err) {
|
|
4015
4303
|
totalErrors++;
|
|
4016
4304
|
send("item", { index: idx, total: totalMsgs, status: "error", preview: content.slice(0, 120), source: file, agent: agentId, error: String(err).slice(0, 200) });
|
|
@@ -4051,7 +4339,14 @@ export class ViewerServer {
|
|
|
4051
4339
|
}
|
|
4052
4340
|
|
|
4053
4341
|
send("progress", { total: totalProcessed, processed: totalProcessed, phase: "done" });
|
|
4054
|
-
send("summary", {
|
|
4342
|
+
send("summary", {
|
|
4343
|
+
totalProcessed,
|
|
4344
|
+
totalStored,
|
|
4345
|
+
totalSkipped,
|
|
4346
|
+
totalErrors,
|
|
4347
|
+
success: computeMigrationSuccess(this.migrationState),
|
|
4348
|
+
stepFailures: this.migrationState.stepFailures,
|
|
4349
|
+
});
|
|
4055
4350
|
}
|
|
4056
4351
|
|
|
4057
4352
|
// ─── Post-processing: independent task/skill generation ───
|
|
@@ -4322,6 +4617,22 @@ export class ViewerServer {
|
|
|
4322
4617
|
req.on("end", () => cb(body));
|
|
4323
4618
|
}
|
|
4324
4619
|
|
|
4620
|
+
private jsonResponseAndRestart(
|
|
4621
|
+
res: http.ServerResponse,
|
|
4622
|
+
data: unknown,
|
|
4623
|
+
source: string,
|
|
4624
|
+
delayMs = 1500,
|
|
4625
|
+
statusCode = 200,
|
|
4626
|
+
): void {
|
|
4627
|
+
res.writeHead(statusCode, { "Content-Type": "application/json; charset=utf-8" });
|
|
4628
|
+
res.end(JSON.stringify(data), () => {
|
|
4629
|
+
setTimeout(() => {
|
|
4630
|
+
this.log.info(`${source}: triggering gateway restart via SIGUSR1...`);
|
|
4631
|
+
try { process.kill(process.pid, "SIGUSR1"); } catch (sig) { this.log.warn(`SIGUSR1 failed: ${sig}`); }
|
|
4632
|
+
}, delayMs);
|
|
4633
|
+
});
|
|
4634
|
+
}
|
|
4635
|
+
|
|
4325
4636
|
private jsonResponse(res: http.ServerResponse, data: unknown, statusCode = 200): void {
|
|
4326
4637
|
res.writeHead(statusCode, { "Content-Type": "application/json; charset=utf-8" });
|
|
4327
4638
|
res.end(JSON.stringify(data));
|