@memtensor/memos-local-openclaw-plugin 1.0.6-beta.1 → 1.0.6-beta.11
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/client/connector.d.ts.map +1 -1
- package/dist/client/connector.js +10 -4
- package/dist/client/connector.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 +108 -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 +20 -4
- package/dist/ingest/providers/index.js.map +1 -1
- package/dist/ingest/providers/openai.d.ts +0 -3
- package/dist/ingest/providers/openai.d.ts.map +1 -1
- package/dist/ingest/providers/openai.js +9 -8
- package/dist/ingest/providers/openai.js.map +1 -1
- package/dist/recall/engine.d.ts.map +1 -1
- package/dist/recall/engine.js +35 -43
- package/dist/recall/engine.js.map +1 -1
- package/dist/storage/sqlite.d.ts +13 -0
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +43 -1
- package/dist/storage/sqlite.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 +1 -0
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +191 -96
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +208 -253
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
- package/scripts/native-binding.cjs +32 -0
- package/scripts/postinstall.cjs +13 -16
- package/src/client/connector.ts +10 -4
- package/src/hub/server.ts +95 -51
- package/src/ingest/providers/index.ts +26 -18
- package/src/ingest/providers/openai.ts +5 -4
- package/src/recall/engine.ts +34 -41
- package/src/storage/sqlite.ts +43 -1
- package/src/update-check.ts +2 -7
- package/src/viewer/html.ts +115 -27
- package/src/viewer/server.ts +187 -64
package/src/viewer/server.ts
CHANGED
|
@@ -90,23 +90,6 @@ export function applyMigrationItemToState(state: MigrationStateSnapshot, d: any)
|
|
|
90
90
|
state.success = computeMigrationSuccess(state);
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
/** Epoch ms for Chunk; OpenClaw SQLite may store Unix seconds or ms. */
|
|
94
|
-
function normalizeTimestamp(value: unknown): number {
|
|
95
|
-
if (value == null) return Date.now();
|
|
96
|
-
if (typeof value === "string") {
|
|
97
|
-
const parsed = Date.parse(value.trim());
|
|
98
|
-
if (Number.isFinite(parsed)) return parsed;
|
|
99
|
-
const n = Number(value);
|
|
100
|
-
if (Number.isFinite(n)) return normalizeTimestamp(n);
|
|
101
|
-
return Date.now();
|
|
102
|
-
}
|
|
103
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
104
|
-
if (value > 0 && value < 5_000_000_000) return Math.round(value * 1000);
|
|
105
|
-
return Math.round(value);
|
|
106
|
-
}
|
|
107
|
-
return Date.now();
|
|
108
|
-
}
|
|
109
|
-
|
|
110
93
|
export interface ViewerServerOptions {
|
|
111
94
|
store: SqliteStore;
|
|
112
95
|
embedder: Embedder;
|
|
@@ -1932,18 +1915,25 @@ export class ViewerServer {
|
|
|
1932
1915
|
|
|
1933
1916
|
private handleRetryJoin(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
1934
1917
|
this.readBody(req, async (_body) => {
|
|
1935
|
-
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" });
|
|
1936
1919
|
const sharing = this.ctx.config.sharing;
|
|
1937
1920
|
if (!sharing?.enabled || sharing.role !== "client") {
|
|
1938
|
-
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" });
|
|
1939
1922
|
}
|
|
1940
1923
|
const hubAddress = sharing.client?.hubAddress ?? "";
|
|
1941
1924
|
const teamToken = sharing.client?.teamToken ?? "";
|
|
1942
1925
|
if (!hubAddress || !teamToken) {
|
|
1943
|
-
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" });
|
|
1944
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" });
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1945
1936
|
try {
|
|
1946
|
-
const hubUrl = normalizeHubUrl(hubAddress);
|
|
1947
1937
|
const os = await import("os");
|
|
1948
1938
|
const nickname = sharing.client?.nickname;
|
|
1949
1939
|
const username = nickname || os.userInfo().username || "user";
|
|
@@ -1971,9 +1961,19 @@ export class ViewerServer {
|
|
|
1971
1961
|
lastKnownStatus: result.status || "",
|
|
1972
1962
|
hubInstanceId,
|
|
1973
1963
|
});
|
|
1964
|
+
if (result.status === "blocked") {
|
|
1965
|
+
return this.jsonResponse(res, { ok: false, error: "blocked", errorCode: "blocked" });
|
|
1966
|
+
}
|
|
1974
1967
|
this.jsonResponse(res, { ok: true, status: result.status || "pending" });
|
|
1975
1968
|
} catch (err) {
|
|
1976
|
-
|
|
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" });
|
|
1977
1977
|
}
|
|
1978
1978
|
});
|
|
1979
1979
|
}
|
|
@@ -2886,20 +2886,25 @@ export class ViewerServer {
|
|
|
2886
2886
|
const nowClient = Boolean(finalSharing?.enabled) && finalSharing?.role === "client";
|
|
2887
2887
|
const previouslyClient = oldSharingEnabled && oldSharingRole === "client";
|
|
2888
2888
|
let joinStatus: string | undefined;
|
|
2889
|
+
let joinError: string | undefined;
|
|
2889
2890
|
if (nowClient && !previouslyClient) {
|
|
2890
2891
|
try {
|
|
2891
2892
|
joinStatus = await this.autoJoinOnSave(finalSharing);
|
|
2892
2893
|
} catch (e) {
|
|
2893
|
-
|
|
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
|
+
}
|
|
2894
2899
|
}
|
|
2895
2900
|
}
|
|
2896
2901
|
|
|
2897
|
-
|
|
2902
|
+
if (joinError) {
|
|
2903
|
+
this.jsonResponse(res, { ok: true, joinError, restart: false });
|
|
2904
|
+
return;
|
|
2905
|
+
}
|
|
2898
2906
|
|
|
2899
|
-
|
|
2900
|
-
this.log.info("config-save: triggering gateway restart via SIGUSR1...");
|
|
2901
|
-
try { process.kill(process.pid, "SIGUSR1"); } catch (sig) { this.log.warn(`SIGUSR1 failed: ${sig}`); }
|
|
2902
|
-
}, 500);
|
|
2907
|
+
this.jsonResponseAndRestart(res, { ok: true, joinStatus, restart: true }, "config-save");
|
|
2903
2908
|
} catch (e) {
|
|
2904
2909
|
this.log.warn(`handleSaveConfig error: ${e}`);
|
|
2905
2910
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
@@ -2914,16 +2919,37 @@ export class ViewerServer {
|
|
|
2914
2919
|
const teamToken = String(clientCfg?.teamToken || "");
|
|
2915
2920
|
if (!hubAddress || !teamToken) return undefined;
|
|
2916
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
|
+
|
|
2917
2929
|
const os = await import("os");
|
|
2918
2930
|
const nickname = String(clientCfg?.nickname || "");
|
|
2919
2931
|
const username = nickname || os.userInfo().username || "user";
|
|
2920
2932
|
const hostname = os.hostname() || "unknown";
|
|
2921
2933
|
const persisted = this.store.getClientHubConnection();
|
|
2922
2934
|
const existingIdentityKey = persisted?.identityKey || "";
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
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
|
+
|
|
2927
2953
|
const returnedIdentityKey = String(result.identityKey || existingIdentityKey || "");
|
|
2928
2954
|
let hubInstanceId = persisted?.hubInstanceId || "";
|
|
2929
2955
|
try {
|
|
@@ -2971,12 +2997,7 @@ export class ViewerServer {
|
|
|
2971
2997
|
}
|
|
2972
2998
|
}
|
|
2973
2999
|
|
|
2974
|
-
this.
|
|
2975
|
-
|
|
2976
|
-
setTimeout(() => {
|
|
2977
|
-
this.log.info("handleLeaveTeam: triggering gateway restart via SIGUSR1...");
|
|
2978
|
-
try { process.kill(process.pid, "SIGUSR1"); } catch (sig) { this.log.warn(`SIGUSR1 failed: ${sig}`); }
|
|
2979
|
-
}, 500);
|
|
3000
|
+
this.jsonResponseAndRestart(res, { ok: true, restart: true }, "handleLeaveTeam");
|
|
2980
3001
|
} catch (e) {
|
|
2981
3002
|
this.log.warn(`handleLeaveTeam error: ${e}`);
|
|
2982
3003
|
this.jsonResponse(res, { ok: false, error: String(e) });
|
|
@@ -3142,14 +3163,43 @@ export class ViewerServer {
|
|
|
3142
3163
|
}
|
|
3143
3164
|
}
|
|
3144
3165
|
} catch {}
|
|
3145
|
-
const
|
|
3166
|
+
const baseUrl = hubUrl.replace(/\/+$/, "");
|
|
3167
|
+
const infoUrl = baseUrl + "/api/v1/hub/info";
|
|
3146
3168
|
const ctrl = new AbortController();
|
|
3147
3169
|
const timeout = setTimeout(() => ctrl.abort(), 8000);
|
|
3148
3170
|
try {
|
|
3149
|
-
const r = await fetch(
|
|
3171
|
+
const r = await fetch(infoUrl, { signal: ctrl.signal });
|
|
3150
3172
|
clearTimeout(timeout);
|
|
3151
3173
|
if (!r.ok) { this.jsonResponse(res, { ok: false, error: `HTTP ${r.status}` }); return; }
|
|
3152
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
|
+
|
|
3153
3203
|
this.jsonResponse(res, { ok: true, teamName: info.teamName || "", apiVersion: info.apiVersion || "" });
|
|
3154
3204
|
} catch (e: unknown) {
|
|
3155
3205
|
clearTimeout(timeout);
|
|
@@ -3234,26 +3284,35 @@ export class ViewerServer {
|
|
|
3234
3284
|
}
|
|
3235
3285
|
|
|
3236
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
|
+
};
|
|
3237
3296
|
try {
|
|
3238
3297
|
const pkgPath = this.findPluginPackageJson();
|
|
3239
3298
|
if (!pkgPath) {
|
|
3240
|
-
|
|
3299
|
+
sendNoStore({ updateAvailable: false, error: "package.json not found" });
|
|
3241
3300
|
return;
|
|
3242
3301
|
}
|
|
3243
3302
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
3244
3303
|
const current = pkg.version as string;
|
|
3245
3304
|
const name = pkg.name as string;
|
|
3246
3305
|
if (!current || !name) {
|
|
3247
|
-
|
|
3306
|
+
sendNoStore({ updateAvailable: false, current });
|
|
3248
3307
|
return;
|
|
3249
3308
|
}
|
|
3250
3309
|
const { computeUpdateCheck } = await import("../update-check");
|
|
3251
3310
|
const result = await computeUpdateCheck(name, current, fetch, 6_000);
|
|
3252
3311
|
if (!result) {
|
|
3253
|
-
|
|
3312
|
+
sendNoStore({ updateAvailable: false, current, packageName: name });
|
|
3254
3313
|
return;
|
|
3255
3314
|
}
|
|
3256
|
-
|
|
3315
|
+
sendNoStore({
|
|
3257
3316
|
updateAvailable: result.updateAvailable,
|
|
3258
3317
|
current: result.current,
|
|
3259
3318
|
latest: result.latest,
|
|
@@ -3264,7 +3323,7 @@ export class ViewerServer {
|
|
|
3264
3323
|
});
|
|
3265
3324
|
} catch (e) {
|
|
3266
3325
|
this.log.warn(`handleUpdateCheck error: ${e}`);
|
|
3267
|
-
|
|
3326
|
+
sendNoStore({ updateAvailable: false, error: String(e) });
|
|
3268
3327
|
}
|
|
3269
3328
|
}
|
|
3270
3329
|
|
|
@@ -3273,13 +3332,14 @@ export class ViewerServer {
|
|
|
3273
3332
|
req.on("data", (chunk: Buffer) => { body += chunk.toString(); });
|
|
3274
3333
|
req.on("end", () => {
|
|
3275
3334
|
try {
|
|
3276
|
-
const { packageSpec: rawSpec } = JSON.parse(body);
|
|
3335
|
+
const { packageSpec: rawSpec, targetVersion: rawTargetVersion } = JSON.parse(body);
|
|
3277
3336
|
if (!rawSpec || typeof rawSpec !== "string") {
|
|
3278
3337
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
3279
3338
|
res.end(JSON.stringify({ ok: false, error: "Missing packageSpec" }));
|
|
3280
3339
|
return;
|
|
3281
3340
|
}
|
|
3282
3341
|
const packageSpec = rawSpec.trim().replace(/^(?:npx\s+)?openclaw\s+plugins\s+install\s+/i, "");
|
|
3342
|
+
const targetVersion = typeof rawTargetVersion === "string" ? rawTargetVersion.trim() : "";
|
|
3283
3343
|
const allowed = /^@[\w-]+\/[\w.-]+(@[\w.-]+)?$/;
|
|
3284
3344
|
this.log.info(`update-install: received packageSpec="${packageSpec}" (len=${packageSpec.length})`);
|
|
3285
3345
|
if (!allowed.test(packageSpec)) {
|
|
@@ -3296,16 +3356,42 @@ export class ViewerServer {
|
|
|
3296
3356
|
const shortName = pluginName?.replace(/^@[\w-]+\//, "") ?? "memos-local-openclaw-plugin";
|
|
3297
3357
|
const extDir = path.join(os.homedir(), ".openclaw", "extensions", shortName);
|
|
3298
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
|
+
};
|
|
3299
3385
|
|
|
3300
3386
|
// Download via npm pack, extract, and replace extension dir.
|
|
3301
3387
|
// Does NOT touch openclaw.json → no config watcher SIGUSR1.
|
|
3302
3388
|
this.log.info(`update-install: downloading ${packageSpec} via npm pack...`);
|
|
3303
3389
|
fs.mkdirSync(tmpDir, { recursive: true });
|
|
3304
|
-
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) => {
|
|
3305
3391
|
if (packErr) {
|
|
3306
3392
|
this.log.warn(`update-install: npm pack failed: ${packErr.message}`);
|
|
3307
3393
|
this.jsonResponse(res, { ok: false, error: `Download failed: ${packErr.message}` });
|
|
3308
|
-
|
|
3394
|
+
cleanupTmpDir();
|
|
3309
3395
|
return;
|
|
3310
3396
|
}
|
|
3311
3397
|
const tgzFile = packOut.trim().split("\n").pop()!;
|
|
@@ -3318,7 +3404,7 @@ export class ViewerServer {
|
|
|
3318
3404
|
if (tarErr) {
|
|
3319
3405
|
this.log.warn(`update-install: tar extract failed: ${tarErr.message}`);
|
|
3320
3406
|
this.jsonResponse(res, { ok: false, error: `Extract failed: ${tarErr.message}` });
|
|
3321
|
-
|
|
3407
|
+
cleanupTmpDir();
|
|
3322
3408
|
return;
|
|
3323
3409
|
}
|
|
3324
3410
|
|
|
@@ -3326,23 +3412,36 @@ export class ViewerServer {
|
|
|
3326
3412
|
const srcDir = path.join(extractDir, "package");
|
|
3327
3413
|
if (!fs.existsSync(srcDir)) {
|
|
3328
3414
|
this.jsonResponse(res, { ok: false, error: "Extracted package has no 'package' dir" });
|
|
3329
|
-
|
|
3415
|
+
cleanupTmpDir();
|
|
3330
3416
|
return;
|
|
3331
3417
|
}
|
|
3332
3418
|
|
|
3333
3419
|
// Replace extension directory
|
|
3334
3420
|
this.log.info(`update-install: replacing ${extDir}...`);
|
|
3335
|
-
try {
|
|
3336
|
-
|
|
3337
|
-
|
|
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
|
+
}
|
|
3338
3436
|
|
|
3339
3437
|
// Install dependencies
|
|
3340
3438
|
this.log.info(`update-install: installing dependencies...`);
|
|
3341
3439
|
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
3342
3440
|
execFile(npmCmd, ["install", "--omit=dev", "--ignore-scripts"], { cwd: extDir, timeout: 120_000 }, (npmErr, npmOut, npmStderr) => {
|
|
3343
3441
|
if (npmErr) {
|
|
3344
|
-
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
3345
3442
|
this.log.warn(`update-install: npm install failed: ${npmErr.message}`);
|
|
3443
|
+
cleanupTmpDir();
|
|
3444
|
+
rollbackInstall();
|
|
3346
3445
|
this.jsonResponse(res, { ok: false, error: `Dependency install failed: ${npmStderr || npmErr.message}` });
|
|
3347
3446
|
return;
|
|
3348
3447
|
}
|
|
@@ -3356,28 +3455,36 @@ export class ViewerServer {
|
|
|
3356
3455
|
|
|
3357
3456
|
this.log.info(`update-install: running postinstall...`);
|
|
3358
3457
|
execFile(process.execPath, ["scripts/postinstall.cjs"], { cwd: extDir, timeout: 180_000 }, (postErr, postOut, postStderr) => {
|
|
3359
|
-
|
|
3458
|
+
cleanupTmpDir();
|
|
3360
3459
|
|
|
3361
3460
|
if (postErr) {
|
|
3362
3461
|
this.log.warn(`update-install: postinstall failed: ${postErr.message}`);
|
|
3363
3462
|
const postStderrStr = String(postStderr || "").trim();
|
|
3364
3463
|
if (postStderrStr) this.log.warn(`update-install: postinstall stderr: ${postStderrStr.slice(0, 500)}`);
|
|
3464
|
+
rollbackInstall();
|
|
3465
|
+
this.jsonResponse(res, { ok: false, error: `Postinstall failed: ${postStderrStr || postErr.message}` });
|
|
3466
|
+
return;
|
|
3365
3467
|
}
|
|
3366
3468
|
|
|
3367
|
-
// Read new version
|
|
3368
3469
|
let newVersion = "unknown";
|
|
3369
3470
|
try {
|
|
3370
3471
|
const newPkg = JSON.parse(fs.readFileSync(path.join(extDir, "package.json"), "utf-8"));
|
|
3371
3472
|
newVersion = newPkg.version ?? newVersion;
|
|
3372
3473
|
} catch {}
|
|
3373
3474
|
|
|
3374
|
-
|
|
3375
|
-
|
|
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
|
+
}
|
|
3376
3484
|
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
}, 500);
|
|
3485
|
+
discardBackup();
|
|
3486
|
+
this.log.info(`update-install: success! Updated to ${newVersion}`);
|
|
3487
|
+
this.jsonResponseAndRestart(res, { ok: true, version: newVersion }, "update-install");
|
|
3381
3488
|
});
|
|
3382
3489
|
});
|
|
3383
3490
|
});
|
|
@@ -3962,8 +4069,8 @@ export class ViewerServer {
|
|
|
3962
4069
|
mergeCount: 0,
|
|
3963
4070
|
lastHitAt: null,
|
|
3964
4071
|
mergeHistory: "[]",
|
|
3965
|
-
createdAt:
|
|
3966
|
-
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),
|
|
3967
4074
|
};
|
|
3968
4075
|
|
|
3969
4076
|
this.store.insertChunk(chunk);
|
|
@@ -4510,6 +4617,22 @@ export class ViewerServer {
|
|
|
4510
4617
|
req.on("end", () => cb(body));
|
|
4511
4618
|
}
|
|
4512
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
|
+
|
|
4513
4636
|
private jsonResponse(res: http.ServerResponse, data: unknown, statusCode = 200): void {
|
|
4514
4637
|
res.writeHead(statusCode, { "Content-Type": "application/json; charset=utf-8" });
|
|
4515
4638
|
res.end(JSON.stringify(data));
|