mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-22 06:08:13 +00:00
refactor: isolate legacy update and tui imports
This commit is contained in:
@@ -58,7 +58,7 @@ import {
|
||||
import {
|
||||
importLegacyUpdateCheckFileToSqlite,
|
||||
legacyUpdateCheckFileExists,
|
||||
} from "../infra/update-startup.js";
|
||||
} from "../infra/update-startup-legacy.js";
|
||||
import {
|
||||
importLegacyVoiceWakeConfigFileToSqlite,
|
||||
legacyVoiceWakeConfigFileExists,
|
||||
@@ -92,7 +92,7 @@ import { note } from "../terminal/note.js";
|
||||
import {
|
||||
importLegacyTuiLastSessionStoreToSqlite,
|
||||
legacyTuiLastSessionFileExists,
|
||||
} from "../tui/tui-last-session.js";
|
||||
} from "../tui/tui-last-session-legacy.js";
|
||||
import type { DoctorPrompter } from "./doctor-prompter.js";
|
||||
|
||||
type LegacyStateProbe = {
|
||||
|
||||
89
src/infra/update-startup-legacy.ts
Normal file
89
src/infra/update-startup-legacy.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { resolveStateDir } from "../config/paths.js";
|
||||
import type { OpenClawStateDatabaseOptions } from "../state/openclaw-state-db.js";
|
||||
import {
|
||||
writeOpenClawStateKvJson,
|
||||
type OpenClawStateJsonValue,
|
||||
} from "../state/openclaw-state-kv.js";
|
||||
|
||||
type UpdateCheckState = {
|
||||
lastCheckedAt?: string;
|
||||
lastNotifiedVersion?: string;
|
||||
lastNotifiedTag?: string;
|
||||
lastAvailableVersion?: string;
|
||||
lastAvailableTag?: string;
|
||||
autoInstallId?: string;
|
||||
autoFirstSeenVersion?: string;
|
||||
autoFirstSeenTag?: string;
|
||||
autoFirstSeenAt?: string;
|
||||
autoLastAttemptVersion?: string;
|
||||
autoLastAttemptAt?: string;
|
||||
autoLastSuccessVersion?: string;
|
||||
autoLastSuccessAt?: string;
|
||||
};
|
||||
|
||||
const UPDATE_CHECK_FILENAME = "update-check.json";
|
||||
const UPDATE_CHECK_SCOPE = "runtime.update-check";
|
||||
const UPDATE_CHECK_KEY = "state";
|
||||
|
||||
function sqliteOptionsForEnv(env: NodeJS.ProcessEnv): OpenClawStateDatabaseOptions {
|
||||
return { env };
|
||||
}
|
||||
|
||||
function resolveLegacyUpdateCheckPath(env: NodeJS.ProcessEnv = process.env): string {
|
||||
return path.join(resolveStateDir(env), UPDATE_CHECK_FILENAME);
|
||||
}
|
||||
|
||||
function coerceUpdateCheckState(value: unknown): UpdateCheckState {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as UpdateCheckState)
|
||||
: {};
|
||||
}
|
||||
|
||||
function writeState(state: UpdateCheckState, env: NodeJS.ProcessEnv = process.env): void {
|
||||
writeOpenClawStateKvJson<OpenClawStateJsonValue>(
|
||||
UPDATE_CHECK_SCOPE,
|
||||
UPDATE_CHECK_KEY,
|
||||
state as unknown as OpenClawStateJsonValue,
|
||||
sqliteOptionsForEnv(env),
|
||||
);
|
||||
}
|
||||
|
||||
async function readLegacyStateFile(filePath: string): Promise<UpdateCheckState> {
|
||||
try {
|
||||
const raw = await fs.readFile(filePath, "utf-8");
|
||||
return coerceUpdateCheckState(JSON.parse(raw));
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export async function legacyUpdateCheckFileExists(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(resolveLegacyUpdateCheckPath(env));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function importLegacyUpdateCheckFileToSqlite(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): Promise<{ imported: boolean }> {
|
||||
const filePath = resolveLegacyUpdateCheckPath(env);
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
} catch (error) {
|
||||
if ((error as { code?: unknown })?.code === "ENOENT") {
|
||||
return { imported: false };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
const state = await readLegacyStateFile(filePath);
|
||||
writeState(state, env);
|
||||
await fs.rm(filePath, { force: true }).catch(() => undefined);
|
||||
return { imported: true };
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import { createHash, randomUUID } from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { formatCliCommand } from "../cli/command-format.js";
|
||||
import { resolveStateDir } from "../config/paths.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { runCommandWithTimeout } from "../process/exec.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||
@@ -65,7 +64,6 @@ export function resetUpdateAvailableStateForTest(): void {
|
||||
updateAvailableCache = null;
|
||||
}
|
||||
|
||||
const UPDATE_CHECK_FILENAME = "update-check.json";
|
||||
const UPDATE_CHECK_SCOPE = "runtime.update-check";
|
||||
const UPDATE_CHECK_KEY = "state";
|
||||
const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
||||
@@ -127,10 +125,6 @@ function sqliteOptionsForEnv(env: NodeJS.ProcessEnv): OpenClawStateDatabaseOptio
|
||||
return { env };
|
||||
}
|
||||
|
||||
function resolveLegacyUpdateCheckPath(env: NodeJS.ProcessEnv = process.env): string {
|
||||
return path.join(resolveStateDir(env), UPDATE_CHECK_FILENAME);
|
||||
}
|
||||
|
||||
function coerceUpdateCheckState(value: unknown): UpdateCheckState {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as UpdateCheckState)
|
||||
@@ -152,44 +146,6 @@ function writeState(state: UpdateCheckState, env: NodeJS.ProcessEnv = process.en
|
||||
);
|
||||
}
|
||||
|
||||
async function readLegacyStateFile(filePath: string): Promise<UpdateCheckState> {
|
||||
try {
|
||||
const raw = await fs.readFile(filePath, "utf-8");
|
||||
return coerceUpdateCheckState(JSON.parse(raw));
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export async function legacyUpdateCheckFileExists(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(resolveLegacyUpdateCheckPath(env));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function importLegacyUpdateCheckFileToSqlite(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): Promise<{ imported: boolean }> {
|
||||
const filePath = resolveLegacyUpdateCheckPath(env);
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
} catch (error) {
|
||||
if ((error as { code?: unknown })?.code === "ENOENT") {
|
||||
return { imported: false };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
const state = await readLegacyStateFile(filePath);
|
||||
writeState(state, env);
|
||||
await fs.rm(filePath, { force: true }).catch(() => undefined);
|
||||
return { imported: true };
|
||||
}
|
||||
|
||||
function sameUpdateAvailable(a: UpdateAvailable | null, b: UpdateAvailable | null): boolean {
|
||||
if (a === b) {
|
||||
return true;
|
||||
|
||||
97
src/tui/tui-last-session-legacy.ts
Normal file
97
src/tui/tui-last-session-legacy.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { resolveStateDir } from "../config/paths.js";
|
||||
import { privateFileStore } from "../infra/private-file-store.js";
|
||||
import { writeTuiLastSessionRecordForMigration } from "./tui-last-session.js";
|
||||
|
||||
type LastSessionRecord = {
|
||||
sessionKey: string;
|
||||
updatedAt: number;
|
||||
};
|
||||
|
||||
type LastSessionStore = Record<string, LastSessionRecord>;
|
||||
|
||||
export function resolveLegacyTuiLastSessionStatePath(stateDir = resolveStateDir()): string {
|
||||
return path.join(stateDir, "tui", "last-session.json");
|
||||
}
|
||||
|
||||
async function readStore(filePath: string): Promise<LastSessionStore> {
|
||||
try {
|
||||
const parsed = await privateFileStore(path.dirname(filePath)).readJsonIfExists(
|
||||
path.basename(filePath),
|
||||
);
|
||||
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
||||
? (parsed as LastSessionStore)
|
||||
: {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteStore(filePath: string): Promise<void> {
|
||||
await fs.rm(filePath, { force: true });
|
||||
}
|
||||
|
||||
function normalizeLastSessionRecord(value: unknown): LastSessionRecord | null {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return null;
|
||||
}
|
||||
const record = value as Record<string, unknown>;
|
||||
const sessionKey = typeof record.sessionKey === "string" ? record.sessionKey.trim() : "";
|
||||
const updatedAt = typeof record.updatedAt === "number" ? record.updatedAt : null;
|
||||
if (!sessionKey || updatedAt === null || !Number.isFinite(updatedAt)) {
|
||||
return null;
|
||||
}
|
||||
return { sessionKey, updatedAt };
|
||||
}
|
||||
|
||||
async function readLegacyTuiLastSessionStore(params: {
|
||||
stateDir?: string;
|
||||
}): Promise<LastSessionStore> {
|
||||
const filePath = resolveLegacyTuiLastSessionStatePath(params.stateDir);
|
||||
return await readStore(filePath);
|
||||
}
|
||||
|
||||
export async function legacyTuiLastSessionFileExists(
|
||||
params: {
|
||||
stateDir?: string;
|
||||
} = {},
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(resolveLegacyTuiLastSessionStatePath(params.stateDir));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function importLegacyTuiLastSessionStoreToSqlite(
|
||||
params: {
|
||||
stateDir?: string;
|
||||
} = {},
|
||||
): Promise<{ imported: boolean; pointers: number }> {
|
||||
const filePath = resolveLegacyTuiLastSessionStatePath(params.stateDir);
|
||||
const exists = await legacyTuiLastSessionFileExists(params);
|
||||
if (!exists) {
|
||||
return { imported: false, pointers: 0 };
|
||||
}
|
||||
const store = await readLegacyTuiLastSessionStore(params);
|
||||
let pointers = 0;
|
||||
for (const [scopeKey, value] of Object.entries(store)) {
|
||||
const record = normalizeLastSessionRecord(value);
|
||||
if (!record) {
|
||||
continue;
|
||||
}
|
||||
const wrote = await writeTuiLastSessionRecordForMigration({
|
||||
scopeKey,
|
||||
sessionKey: record.sessionKey,
|
||||
updatedAt: record.updatedAt,
|
||||
stateDir: params.stateDir,
|
||||
});
|
||||
if (wrote) {
|
||||
pointers += 1;
|
||||
}
|
||||
}
|
||||
await deleteStore(filePath);
|
||||
return { imported: true, pointers };
|
||||
}
|
||||
@@ -3,14 +3,16 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js";
|
||||
import {
|
||||
importLegacyTuiLastSessionStoreToSqlite,
|
||||
resolveLegacyTuiLastSessionStatePath,
|
||||
} from "./tui-last-session-legacy.js";
|
||||
import {
|
||||
buildTuiLastSessionScopeKey,
|
||||
clearTuiLastSessionPointers,
|
||||
importLegacyTuiLastSessionStoreToSqlite,
|
||||
isHeartbeatLikeTuiSession,
|
||||
readTuiLastSessionKey,
|
||||
resolveRememberedTuiSessionKey,
|
||||
resolveLegacyTuiLastSessionStatePath,
|
||||
writeTuiLastSessionKey,
|
||||
} from "./tui-last-session.js";
|
||||
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import type { Insertable, Selectable } from "kysely";
|
||||
import { resolveStateDir } from "../config/paths.js";
|
||||
import { executeSqliteQuerySync, getNodeSqliteKysely } from "../infra/kysely-sync.js";
|
||||
import { privateFileStore } from "../infra/private-file-store.js";
|
||||
import { normalizeAgentId, parseAgentSessionKey } from "../routing/session-key.js";
|
||||
import type { DB as OpenClawStateKyselyDatabase } from "../state/openclaw-state-db.generated.js";
|
||||
import {
|
||||
@@ -21,15 +17,10 @@ type LastSessionRecord = {
|
||||
updatedAt: number;
|
||||
};
|
||||
|
||||
type LastSessionStore = Record<string, LastSessionRecord>;
|
||||
type TuiLastSessionsTable = OpenClawStateKyselyDatabase["tui_last_sessions"];
|
||||
type TuiLastSessionRow = Selectable<TuiLastSessionsTable>;
|
||||
type TuiLastSessionDatabase = Pick<OpenClawStateKyselyDatabase, "tui_last_sessions">;
|
||||
|
||||
export function resolveLegacyTuiLastSessionStatePath(stateDir = resolveStateDir()): string {
|
||||
return path.join(stateDir, "tui", "last-session.json");
|
||||
}
|
||||
|
||||
export function buildTuiLastSessionScopeKey(params: {
|
||||
connectionUrl: string;
|
||||
agentId: string;
|
||||
@@ -43,23 +34,6 @@ export function buildTuiLastSessionScopeKey(params: {
|
||||
.slice(0, 32);
|
||||
}
|
||||
|
||||
async function readStore(filePath: string): Promise<LastSessionStore> {
|
||||
try {
|
||||
const parsed = await privateFileStore(path.dirname(filePath)).readJsonIfExists(
|
||||
path.basename(filePath),
|
||||
);
|
||||
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
||||
? (parsed as LastSessionStore)
|
||||
: {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteStore(filePath: string): Promise<void> {
|
||||
await fs.rm(filePath, { force: true });
|
||||
}
|
||||
|
||||
function sqliteOptionsForStateDir(stateDir?: string): OpenClawStateDatabaseOptions {
|
||||
return stateDir ? { env: { ...process.env, OPENCLAW_STATE_DIR: stateDir } } : {};
|
||||
}
|
||||
@@ -124,54 +98,6 @@ function writeTuiLastSessionRow(params: {
|
||||
}, sqliteOptionsForStateDir(params.stateDir));
|
||||
}
|
||||
|
||||
async function readLegacyTuiLastSessionStore(params: {
|
||||
stateDir?: string;
|
||||
}): Promise<LastSessionStore> {
|
||||
const filePath = resolveLegacyTuiLastSessionStatePath(params.stateDir);
|
||||
return await readStore(filePath);
|
||||
}
|
||||
|
||||
export async function legacyTuiLastSessionFileExists(
|
||||
params: {
|
||||
stateDir?: string;
|
||||
} = {},
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(resolveLegacyTuiLastSessionStatePath(params.stateDir));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function importLegacyTuiLastSessionStoreToSqlite(
|
||||
params: {
|
||||
stateDir?: string;
|
||||
} = {},
|
||||
): Promise<{ imported: boolean; pointers: number }> {
|
||||
const filePath = resolveLegacyTuiLastSessionStatePath(params.stateDir);
|
||||
const exists = await legacyTuiLastSessionFileExists(params);
|
||||
if (!exists) {
|
||||
return { imported: false, pointers: 0 };
|
||||
}
|
||||
const store = await readLegacyTuiLastSessionStore(params);
|
||||
let pointers = 0;
|
||||
for (const [scopeKey, value] of Object.entries(store)) {
|
||||
const record = normalizeLastSessionRecord(value);
|
||||
if (!record) {
|
||||
continue;
|
||||
}
|
||||
writeTuiLastSessionRow({
|
||||
scopeKey,
|
||||
record,
|
||||
stateDir: params.stateDir,
|
||||
});
|
||||
pointers += 1;
|
||||
}
|
||||
await deleteStore(filePath);
|
||||
return { imported: true, pointers };
|
||||
}
|
||||
|
||||
function normalizeMarker(value: unknown): string {
|
||||
return typeof value === "string" ? value.trim().toLowerCase() : "";
|
||||
}
|
||||
@@ -229,6 +155,27 @@ export async function writeTuiLastSessionKey(params: {
|
||||
});
|
||||
}
|
||||
|
||||
export async function writeTuiLastSessionRecordForMigration(params: {
|
||||
scopeKey: string;
|
||||
sessionKey: string;
|
||||
updatedAt: number;
|
||||
stateDir?: string;
|
||||
}): Promise<boolean> {
|
||||
const record = normalizeLastSessionRecord({
|
||||
sessionKey: params.sessionKey,
|
||||
updatedAt: params.updatedAt,
|
||||
});
|
||||
if (!record || isHeartbeatSessionKey(record.sessionKey)) {
|
||||
return false;
|
||||
}
|
||||
writeTuiLastSessionRow({
|
||||
scopeKey: params.scopeKey,
|
||||
record,
|
||||
stateDir: params.stateDir,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function clearTuiLastSessionPointers(params: {
|
||||
stateDir?: string;
|
||||
sessionKeys: ReadonlySet<string>;
|
||||
|
||||
Reference in New Issue
Block a user