mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 12:50:42 +00:00
Refactor file access to use fs-safe primitives (#78255)
* refactor: use fs-safe primitives across file access * fix: preserve invalid managed npm manifests * fix: keep fs seams for startup metadata
This commit is contained in:
committed by
GitHub
parent
0d73f174a9
commit
b85b1c68d1
@@ -2,6 +2,7 @@ import fsSync from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import { readJsonFileWithFallback } from "openclaw/plugin-sdk/json-store";
|
||||
import { resolveAcpxPluginRoot } from "./config.js";
|
||||
import type { ResolvedAcpxPluginConfig } from "./config.js";
|
||||
|
||||
@@ -113,7 +114,10 @@ async function resolveInstalledAcpPackageBinPath(
|
||||
): Promise<string | undefined> {
|
||||
try {
|
||||
const packageJsonPath = requireFromHere.resolve(`${packageName}/package.json`);
|
||||
const manifest = JSON.parse(await fs.readFile(packageJsonPath, "utf8")) as PackageManifest;
|
||||
const { value: manifest } = await readJsonFileWithFallback<PackageManifest>(
|
||||
packageJsonPath,
|
||||
{},
|
||||
);
|
||||
if (manifest.name !== packageName) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { loadJsonFile, saveJsonFile } from "openclaw/plugin-sdk/json-store";
|
||||
import {
|
||||
DEFAULT_OPENCLAW_BROWSER_COLOR,
|
||||
DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME,
|
||||
@@ -10,24 +11,14 @@ function decoratedMarkerPath(userDataDir: string) {
|
||||
}
|
||||
|
||||
function safeReadJson(filePath: string): Record<string, unknown> | null {
|
||||
try {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return null;
|
||||
}
|
||||
const raw = fs.readFileSync(filePath, "utf-8");
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
||||
return null;
|
||||
}
|
||||
return parsed as Record<string, unknown>;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
const parsed = loadJsonFile(filePath);
|
||||
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
|
||||
? (parsed as Record<string, unknown>)
|
||||
: null;
|
||||
}
|
||||
|
||||
function safeWriteJson(filePath: string, data: Record<string, unknown>) {
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
||||
saveJsonFile(filePath, data);
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | null {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { readJsonFileWithFallback } from "openclaw/plugin-sdk/json-store";
|
||||
import { pathExists } from "openclaw/plugin-sdk/security-runtime";
|
||||
|
||||
export async function exists(filePath: string): Promise<boolean> {
|
||||
@@ -47,10 +48,8 @@ export async function readJsonObject(
|
||||
if (!filePath) {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(await fs.readFile(filePath, "utf8"));
|
||||
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
const { value: parsed } = await readJsonFileWithFallback<unknown>(filePath, {});
|
||||
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
||||
? (parsed as Record<string, unknown>)
|
||||
: {};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import { createRequire } from "node:module";
|
||||
import type { ThemeRegistrationResolved } from "@pierre/diffs";
|
||||
import { RegisteredCustomThemes, ResolvedThemes, ResolvingThemes } from "@pierre/diffs";
|
||||
import { readJsonFileWithFallback } from "openclaw/plugin-sdk/json-store";
|
||||
|
||||
type PierreThemeName = "pierre-dark" | "pierre-light";
|
||||
const themeRequire = createRequire(import.meta.url);
|
||||
@@ -20,8 +20,9 @@ function createThemeLoader(
|
||||
return cachedTheme;
|
||||
}
|
||||
const themePath = themeRequire.resolve(themeSpecifier);
|
||||
const { value: theme } = await readJsonFileWithFallback<Record<string, unknown>>(themePath, {});
|
||||
cachedTheme = {
|
||||
...(JSON.parse(await fs.readFile(themePath, "utf8")) as Record<string, unknown>),
|
||||
...theme,
|
||||
name: themeName,
|
||||
} as ThemeRegistrationResolved;
|
||||
return cachedTheme;
|
||||
|
||||
@@ -174,13 +174,13 @@ export class DiffArtifactStore {
|
||||
}
|
||||
|
||||
async cleanupExpired(): Promise<void> {
|
||||
await this.ensureRoot();
|
||||
const entries = await fs.readdir(this.rootDir, { withFileTypes: true }).catch(() => []);
|
||||
const root = await this.artifactRoot();
|
||||
const entries = await root.list("", { withFileTypes: true }).catch(() => []);
|
||||
const now = Date.now();
|
||||
|
||||
await Promise.all(
|
||||
entries
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.filter((entry) => entry.isDirectory)
|
||||
.map(async (entry) => {
|
||||
const id = entry.name;
|
||||
const meta = await this.readMeta(id);
|
||||
@@ -199,12 +199,7 @@ export class DiffArtifactStore {
|
||||
return;
|
||||
}
|
||||
|
||||
const artifactPath = this.artifactDir(id);
|
||||
const stat = await fs.stat(artifactPath).catch(() => null);
|
||||
if (!stat) {
|
||||
return;
|
||||
}
|
||||
if (now - stat.mtimeMs > SWEEP_FALLBACK_AGE_MS) {
|
||||
if (now - entry.mtimeMs > SWEEP_FALLBACK_AGE_MS) {
|
||||
await this.deleteArtifact(id);
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -2,7 +2,10 @@ import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
||||
import { writeJsonFileAtomically as writeJsonFileAtomicallyImpl } from "openclaw/plugin-sdk/json-store";
|
||||
import {
|
||||
loadJsonFile,
|
||||
writeJsonFileAtomically as writeJsonFileAtomicallyImpl,
|
||||
} from "openclaw/plugin-sdk/json-store";
|
||||
import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
|
||||
import { resolveConfiguredMatrixAccountIds } from "./account-selection.js";
|
||||
import { isMatrixLegacyCryptoInspectorAvailable } from "./legacy-crypto-inspector-availability.js";
|
||||
@@ -208,20 +211,13 @@ function resolveLegacyMatrixFlatStorePlan(params: {
|
||||
function loadLegacyBotSdkMetadata(cryptoRootDir: string): MatrixLegacyBotSdkMetadata {
|
||||
const metadataPath = path.join(cryptoRootDir, "bot-sdk.json");
|
||||
const fallback: MatrixLegacyBotSdkMetadata = { deviceId: null };
|
||||
try {
|
||||
if (!fs.existsSync(metadataPath)) {
|
||||
return fallback;
|
||||
}
|
||||
const parsed = JSON.parse(fs.readFileSync(metadataPath, "utf8")) as {
|
||||
deviceId?: unknown;
|
||||
};
|
||||
return {
|
||||
deviceId:
|
||||
typeof parsed.deviceId === "string" && parsed.deviceId.trim() ? parsed.deviceId : null,
|
||||
};
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
const parsed = loadJsonFile<{ deviceId?: unknown }>(metadataPath);
|
||||
return {
|
||||
deviceId:
|
||||
typeof parsed?.deviceId === "string" && parsed.deviceId.trim()
|
||||
? parsed.deviceId
|
||||
: fallback.deviceId,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveMatrixLegacyCryptoPlans(params: {
|
||||
@@ -288,25 +284,11 @@ function resolveMatrixLegacyCryptoPlans(params: {
|
||||
}
|
||||
|
||||
function loadStoredRecoveryKey(filePath: string): MatrixStoredRecoveryKey | null {
|
||||
try {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return null;
|
||||
}
|
||||
return JSON.parse(fs.readFileSync(filePath, "utf8")) as MatrixStoredRecoveryKey;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return loadJsonFile<MatrixStoredRecoveryKey>(filePath) ?? null;
|
||||
}
|
||||
|
||||
function loadLegacyCryptoMigrationState(filePath: string): MatrixLegacyCryptoMigrationState | null {
|
||||
try {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return null;
|
||||
}
|
||||
return JSON.parse(fs.readFileSync(filePath, "utf8")) as MatrixLegacyCryptoMigrationState;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return loadJsonFile<MatrixLegacyCryptoMigrationState>(filePath) ?? null;
|
||||
}
|
||||
|
||||
async function persistLegacyMigrationState(params: {
|
||||
|
||||
@@ -3,6 +3,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
||||
import { loadJsonFile, saveJsonFile } from "openclaw/plugin-sdk/json-store";
|
||||
import {
|
||||
requiresExplicitMatrixDefaultAccount,
|
||||
resolveMatrixDefaultOrOnlyAccountId,
|
||||
@@ -105,10 +106,10 @@ function resolveStorageRootMtimeMs(rootDir: string): number {
|
||||
function readStoredRootMetadata(rootDir: string): StoredRootMetadata {
|
||||
const metadata: StoredRootMetadata = {};
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(
|
||||
fs.readFileSync(path.join(rootDir, STORAGE_META_FILENAME), "utf8"),
|
||||
) as Partial<StoredRootMetadata>;
|
||||
const parsed = loadJsonFile<Partial<StoredRootMetadata>>(
|
||||
path.join(rootDir, STORAGE_META_FILENAME),
|
||||
);
|
||||
if (parsed) {
|
||||
if (typeof parsed.homeserver === "string" && parsed.homeserver.trim()) {
|
||||
metadata.homeserver = parsed.homeserver.trim();
|
||||
}
|
||||
@@ -130,19 +131,17 @@ function readStoredRootMetadata(rootDir: string): StoredRootMetadata {
|
||||
if (typeof parsed.createdAt === "string" && parsed.createdAt.trim()) {
|
||||
metadata.createdAt = parsed.createdAt.trim();
|
||||
}
|
||||
} catch {
|
||||
// ignore missing or malformed storage metadata
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(
|
||||
fs.readFileSync(path.join(rootDir, STARTUP_VERIFICATION_FILENAME), "utf8"),
|
||||
) as { deviceId?: unknown };
|
||||
if (!metadata.deviceId && typeof parsed.deviceId === "string" && parsed.deviceId.trim()) {
|
||||
metadata.deviceId = parsed.deviceId.trim();
|
||||
}
|
||||
} catch {
|
||||
// ignore missing or malformed verification state
|
||||
const verification = loadJsonFile<{ deviceId?: unknown }>(
|
||||
path.join(rootDir, STARTUP_VERIFICATION_FILENAME),
|
||||
);
|
||||
if (
|
||||
!metadata.deviceId &&
|
||||
typeof verification?.deviceId === "string" &&
|
||||
verification.deviceId.trim()
|
||||
) {
|
||||
metadata.deviceId = verification.deviceId.trim();
|
||||
}
|
||||
|
||||
return metadata;
|
||||
@@ -473,8 +472,7 @@ function writeStoredRootMetadata(
|
||||
},
|
||||
): boolean {
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(metaPath), { recursive: true });
|
||||
fs.writeFileSync(metaPath, JSON.stringify(payload, null, 2), "utf-8");
|
||||
saveJsonFile(metaPath, payload);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { decodeRecoveryKey } from "matrix-js-sdk/lib/crypto-api/recovery-key.js";
|
||||
import { loadJsonFile, saveJsonFile } from "openclaw/plugin-sdk/json-store";
|
||||
import { formatMatrixErrorMessage, formatMatrixErrorReason } from "../errors.js";
|
||||
import { LogService } from "./logger.js";
|
||||
import type {
|
||||
@@ -399,13 +398,9 @@ export class MatrixRecoveryKeyStore {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
if (!fs.existsSync(this.recoveryKeyPath)) {
|
||||
return null;
|
||||
}
|
||||
const raw = fs.readFileSync(this.recoveryKeyPath, "utf8");
|
||||
const parsed = JSON.parse(raw) as Partial<MatrixStoredRecoveryKey>;
|
||||
const parsed = loadJsonFile<Partial<MatrixStoredRecoveryKey>>(this.recoveryKeyPath);
|
||||
if (
|
||||
parsed.version !== 1 ||
|
||||
parsed?.version !== 1 ||
|
||||
typeof parsed.createdAt !== "string" ||
|
||||
typeof parsed.privateKeyBase64 !== "string" || // pragma: allowlist secret
|
||||
!parsed.privateKeyBase64.trim()
|
||||
@@ -450,9 +445,7 @@ export class MatrixRecoveryKeyStore {
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
fs.mkdirSync(path.dirname(this.recoveryKeyPath), { recursive: true });
|
||||
fs.writeFileSync(this.recoveryKeyPath, JSON.stringify(payload, null, 2), "utf8");
|
||||
fs.chmodSync(this.recoveryKeyPath, 0o600);
|
||||
saveJsonFile(this.recoveryKeyPath, payload);
|
||||
} catch (err) {
|
||||
LogService.warn("MatrixClientLite", "Failed to persist recovery key:", err);
|
||||
}
|
||||
|
||||
@@ -396,10 +396,6 @@ type DailyIngestionState = {
|
||||
files: Record<string, DailyIngestionFileState>;
|
||||
};
|
||||
|
||||
function resolveDailyIngestionStatePath(workspaceDir: string): string {
|
||||
return path.join(workspaceDir, DAILY_INGESTION_STATE_RELATIVE_PATH);
|
||||
}
|
||||
|
||||
function normalizeDailyIngestionState(raw: unknown): DailyIngestionState {
|
||||
const record = asRecord(raw);
|
||||
const filesRaw = asRecord(record?.files);
|
||||
@@ -442,10 +438,9 @@ function normalizeMemoryDay(value: unknown): string | undefined {
|
||||
}
|
||||
|
||||
async function readDailyIngestionState(workspaceDir: string): Promise<DailyIngestionState> {
|
||||
const statePath = resolveDailyIngestionStatePath(workspaceDir);
|
||||
try {
|
||||
return normalizeDailyIngestionState(
|
||||
await privateFileStore(workspaceDir).readJsonIfExists(path.relative(workspaceDir, statePath)),
|
||||
await privateFileStore(workspaceDir).readJsonIfExists(DAILY_INGESTION_STATE_RELATIVE_PATH),
|
||||
);
|
||||
} catch (err) {
|
||||
if (err instanceof SyntaxError) {
|
||||
@@ -459,8 +454,7 @@ async function writeDailyIngestionState(
|
||||
workspaceDir: string,
|
||||
state: DailyIngestionState,
|
||||
): Promise<void> {
|
||||
const statePath = resolveDailyIngestionStatePath(workspaceDir);
|
||||
await privateFileStore(workspaceDir).writeJson(path.relative(workspaceDir, statePath), state, {
|
||||
await privateFileStore(workspaceDir).writeJson(DAILY_INGESTION_STATE_RELATIVE_PATH, state, {
|
||||
trailingNewline: true,
|
||||
});
|
||||
}
|
||||
@@ -496,10 +490,6 @@ function normalizeWorkspaceKey(workspaceDir: string): string {
|
||||
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
||||
}
|
||||
|
||||
function resolveSessionIngestionStatePath(workspaceDir: string): string {
|
||||
return path.join(workspaceDir, SESSION_INGESTION_STATE_RELATIVE_PATH);
|
||||
}
|
||||
|
||||
function normalizeSessionIngestionState(raw: unknown): SessionIngestionState {
|
||||
const record = asRecord(raw);
|
||||
const filesRaw = asRecord(record?.files);
|
||||
@@ -554,10 +544,9 @@ function normalizeSessionIngestionState(raw: unknown): SessionIngestionState {
|
||||
}
|
||||
|
||||
async function readSessionIngestionState(workspaceDir: string): Promise<SessionIngestionState> {
|
||||
const statePath = resolveSessionIngestionStatePath(workspaceDir);
|
||||
try {
|
||||
return normalizeSessionIngestionState(
|
||||
await privateFileStore(workspaceDir).readJsonIfExists(path.relative(workspaceDir, statePath)),
|
||||
await privateFileStore(workspaceDir).readJsonIfExists(SESSION_INGESTION_STATE_RELATIVE_PATH),
|
||||
);
|
||||
} catch (err) {
|
||||
if (err instanceof SyntaxError) {
|
||||
@@ -571,8 +560,7 @@ async function writeSessionIngestionState(
|
||||
workspaceDir: string,
|
||||
state: SessionIngestionState,
|
||||
): Promise<void> {
|
||||
const statePath = resolveSessionIngestionStatePath(workspaceDir);
|
||||
await privateFileStore(workspaceDir).writeJson(path.relative(workspaceDir, statePath), state, {
|
||||
await privateFileStore(workspaceDir).writeJson(SESSION_INGESTION_STATE_RELATIVE_PATH, state, {
|
||||
trailingNewline: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -757,10 +757,9 @@ async function withShortTermLock<T>(workspaceDir: string, task: () => Promise<T>
|
||||
}
|
||||
|
||||
async function readStore(workspaceDir: string, nowIso: string): Promise<ShortTermRecallStore> {
|
||||
const storePath = resolveStorePath(workspaceDir);
|
||||
try {
|
||||
return normalizeStore(
|
||||
await privateFileStore(workspaceDir).readJsonIfExists(path.relative(workspaceDir, storePath)),
|
||||
await privateFileStore(workspaceDir).readJsonIfExists(SHORT_TERM_STORE_RELATIVE_PATH),
|
||||
nowIso,
|
||||
);
|
||||
} catch (err) {
|
||||
@@ -830,12 +829,9 @@ async function readPhaseSignalStore(
|
||||
workspaceDir: string,
|
||||
nowIso: string,
|
||||
): Promise<ShortTermPhaseSignalStore> {
|
||||
const phaseSignalPath = resolvePhaseSignalPath(workspaceDir);
|
||||
try {
|
||||
return normalizePhaseSignalStore(
|
||||
await privateFileStore(workspaceDir).readJsonIfExists(
|
||||
path.relative(workspaceDir, phaseSignalPath),
|
||||
),
|
||||
await privateFileStore(workspaceDir).readJsonIfExists(SHORT_TERM_PHASE_SIGNAL_RELATIVE_PATH),
|
||||
nowIso,
|
||||
);
|
||||
} catch {
|
||||
@@ -847,21 +843,15 @@ async function writePhaseSignalStore(
|
||||
workspaceDir: string,
|
||||
store: ShortTermPhaseSignalStore,
|
||||
): Promise<void> {
|
||||
const phaseSignalPath = resolvePhaseSignalPath(workspaceDir);
|
||||
await ensureShortTermArtifactsDir(workspaceDir);
|
||||
await privateFileStore(workspaceDir).writeJson(
|
||||
path.relative(workspaceDir, phaseSignalPath),
|
||||
store,
|
||||
{
|
||||
trailingNewline: true,
|
||||
},
|
||||
);
|
||||
await privateFileStore(workspaceDir).writeJson(SHORT_TERM_PHASE_SIGNAL_RELATIVE_PATH, store, {
|
||||
trailingNewline: true,
|
||||
});
|
||||
}
|
||||
|
||||
async function writeStore(workspaceDir: string, store: ShortTermRecallStore): Promise<void> {
|
||||
const storePath = resolveStorePath(workspaceDir);
|
||||
await ensureShortTermArtifactsDir(workspaceDir);
|
||||
await privateFileStore(workspaceDir).writeJson(path.relative(workspaceDir, storePath), store, {
|
||||
await privateFileStore(workspaceDir).writeJson(SHORT_TERM_STORE_RELATIVE_PATH, store, {
|
||||
trailingNewline: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { writeJsonFileAtomically } from "openclaw/plugin-sdk/json-store";
|
||||
import {
|
||||
replaceManagedMarkdownBlock,
|
||||
withTrailingNewline,
|
||||
@@ -679,8 +680,7 @@ async function writeImportRunRecord(
|
||||
record: ChatGptImportRunRecord,
|
||||
): Promise<void> {
|
||||
const recordPath = resolveImportRunPath(vaultRoot, record.runId);
|
||||
await fs.mkdir(path.dirname(recordPath), { recursive: true });
|
||||
await fs.writeFile(recordPath, `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
||||
await writeJsonFileAtomically(recordPath, record);
|
||||
}
|
||||
|
||||
async function readImportRunRecord(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { readJsonFileWithFallback, writeJsonFileAtomically } from "openclaw/plugin-sdk/json-store";
|
||||
|
||||
export type MemoryWikiImportedSourceGroup = "bridge" | "unsafe-local";
|
||||
|
||||
@@ -30,30 +31,14 @@ export async function readMemoryWikiSourceSyncState(
|
||||
vaultRoot: string,
|
||||
): Promise<MemoryWikiImportedSourceState> {
|
||||
const statePath = resolveMemoryWikiSourceSyncStatePath(vaultRoot);
|
||||
const raw = await fs.readFile(statePath, "utf8").catch((err: unknown) => {
|
||||
if ((err as NodeJS.ErrnoException)?.code === "ENOENT") {
|
||||
return "";
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
if (!raw.trim()) {
|
||||
return {
|
||||
version: EMPTY_STATE.version,
|
||||
entries: {},
|
||||
};
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as Partial<MemoryWikiImportedSourceState>;
|
||||
return {
|
||||
version: 1,
|
||||
entries: { ...parsed.entries },
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
version: EMPTY_STATE.version,
|
||||
entries: {},
|
||||
};
|
||||
}
|
||||
const { value: parsed } = await readJsonFileWithFallback<Partial<MemoryWikiImportedSourceState>>(
|
||||
statePath,
|
||||
EMPTY_STATE,
|
||||
);
|
||||
return {
|
||||
version: 1,
|
||||
entries: { ...parsed.entries },
|
||||
};
|
||||
}
|
||||
|
||||
export async function writeMemoryWikiSourceSyncState(
|
||||
@@ -61,8 +46,7 @@ export async function writeMemoryWikiSourceSyncState(
|
||||
state: MemoryWikiImportedSourceState,
|
||||
): Promise<void> {
|
||||
const statePath = resolveMemoryWikiSourceSyncStatePath(vaultRoot);
|
||||
await fs.mkdir(path.dirname(statePath), { recursive: true });
|
||||
await fs.writeFile(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
||||
await writeJsonFileAtomically(statePath, state);
|
||||
}
|
||||
|
||||
export async function shouldSkipImportedSourceWrite(params: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import fs from "node:fs/promises";
|
||||
import { replaceFileAtomic } from "openclaw/plugin-sdk/security-runtime";
|
||||
import { writeJsonFileAtomically } from "openclaw/plugin-sdk/json-store";
|
||||
|
||||
/** Default cooldown between reflections per session (5 minutes). */
|
||||
export const DEFAULT_COOLDOWN_MS = 300_000;
|
||||
@@ -93,11 +93,7 @@ export async function storeSessionLearning(params: {
|
||||
learnings = learnings.slice(-10);
|
||||
}
|
||||
|
||||
await replaceFileAtomic({
|
||||
filePath: learningsFile,
|
||||
content: JSON.stringify(learnings, null, 2),
|
||||
tempPrefix: ".msteams-learnings",
|
||||
});
|
||||
await writeJsonFileAtomically(learningsFile, learnings);
|
||||
if (!exists && legacyLearningsFile !== learningsFile) {
|
||||
await fs.rm(legacyLearningsFile, { force: true }).catch(() => undefined);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import fs from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import { loadJsonFile } from "openclaw/plugin-sdk/json-store";
|
||||
import {
|
||||
buildExecRemoteCommand,
|
||||
createSshSandboxSessionFromConfigText,
|
||||
@@ -37,11 +37,11 @@ function resolveBundledOpenShellCommand(): string | null {
|
||||
}
|
||||
try {
|
||||
const packageJsonPath = require.resolve("openshell/package.json");
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as {
|
||||
const packageJson = loadJsonFile<{
|
||||
bin?: string | Record<string, string>;
|
||||
};
|
||||
}>(packageJsonPath);
|
||||
const relativeBin =
|
||||
typeof packageJson.bin === "string" ? packageJson.bin : packageJson.bin?.openshell;
|
||||
typeof packageJson?.bin === "string" ? packageJson.bin : packageJson?.bin?.openshell;
|
||||
cachedBundledOpenShellCommand = relativeBin
|
||||
? path.resolve(path.dirname(packageJsonPath), relativeBin)
|
||||
: null;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { loadJsonFile } from "openclaw/plugin-sdk/json-store";
|
||||
import { getHomeDir, getQQBotDataDir, isWindows } from "../../utils/platform.js";
|
||||
import type { SlashCommandResult } from "../slash-commands.js";
|
||||
|
||||
@@ -10,10 +11,7 @@ function getConfiguredLogFiles(): string[] {
|
||||
for (const cli of ["openclaw", "clawdbot", "moltbot"]) {
|
||||
try {
|
||||
const cfgPath = path.join(homeDir, `.${cli}`, `${cli}.json`);
|
||||
if (!fs.existsSync(cfgPath)) {
|
||||
continue;
|
||||
}
|
||||
const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
|
||||
const cfg = loadJsonFile<{ logging?: { file?: unknown } }>(cfgPath);
|
||||
const logFile = cfg?.logging?.file;
|
||||
if (logFile && typeof logFile === "string") {
|
||||
files.push(path.resolve(logFile));
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
*/
|
||||
|
||||
import fs from "node:fs";
|
||||
import { loadJsonFile } from "openclaw/plugin-sdk/json-store";
|
||||
import { replaceFileAtomicSync } from "openclaw/plugin-sdk/security-runtime";
|
||||
import { getCredentialBackupFile, getLegacyCredentialBackupFile } from "../utils/data-paths.js";
|
||||
|
||||
@@ -70,17 +71,15 @@ export function loadCredentialBackup(accountId?: string): CredentialBackup | nul
|
||||
try {
|
||||
if (accountId) {
|
||||
const newPath = getCredentialBackupFile(accountId);
|
||||
if (fs.existsSync(newPath)) {
|
||||
const data = JSON.parse(fs.readFileSync(newPath, "utf8")) as CredentialBackup;
|
||||
if (data?.appId && data.clientSecret) {
|
||||
return data;
|
||||
}
|
||||
const data = loadJsonFile<CredentialBackup>(newPath);
|
||||
if (data?.appId && data.clientSecret) {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
const legacy = getLegacyCredentialBackupFile();
|
||||
if (fs.existsSync(legacy)) {
|
||||
const data = JSON.parse(fs.readFileSync(legacy, "utf8")) as CredentialBackup;
|
||||
const data = loadJsonFile<CredentialBackup>(legacy);
|
||||
if (data) {
|
||||
if (!data?.appId || !data?.clientSecret) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -42,10 +42,8 @@ async function withLock<T>(key: string, task: () => Promise<T>): Promise<T> {
|
||||
}
|
||||
}
|
||||
|
||||
async function readJson(rootDir: string, filePath: string): Promise<StoreFile> {
|
||||
const parsed = await privateFileStore(rootDir).readJsonIfExists<StoreFile>(
|
||||
path.relative(rootDir, filePath),
|
||||
);
|
||||
async function readJson(rootDir: string, relativePath: string): Promise<StoreFile> {
|
||||
const parsed = await privateFileStore(rootDir).readJsonIfExists<StoreFile>(relativePath);
|
||||
if (!parsed) {
|
||||
return { version: 1, proposals: [] };
|
||||
}
|
||||
@@ -77,8 +75,12 @@ function normalizeReviewState(
|
||||
};
|
||||
}
|
||||
|
||||
async function atomicWriteJson(rootDir: string, filePath: string, data: StoreFile): Promise<void> {
|
||||
await privateFileStore(rootDir).writeJson(path.relative(rootDir, filePath), data, {
|
||||
async function atomicWriteJson(
|
||||
rootDir: string,
|
||||
relativePath: string,
|
||||
data: StoreFile,
|
||||
): Promise<void> {
|
||||
await privateFileStore(rootDir).writeJson(relativePath, data, {
|
||||
trailingNewline: true,
|
||||
});
|
||||
}
|
||||
@@ -86,18 +88,16 @@ async function atomicWriteJson(rootDir: string, filePath: string, data: StoreFil
|
||||
export class SkillWorkshopStore {
|
||||
readonly stateDir: string;
|
||||
readonly filePath: string;
|
||||
private readonly relativePath: string;
|
||||
|
||||
constructor(params: { stateDir: string; workspaceDir: string }) {
|
||||
this.stateDir = path.resolve(params.stateDir);
|
||||
this.filePath = path.join(
|
||||
this.stateDir,
|
||||
"skill-workshop",
|
||||
`${workspaceKey(params.workspaceDir)}.json`,
|
||||
);
|
||||
this.relativePath = path.join("skill-workshop", `${workspaceKey(params.workspaceDir)}.json`);
|
||||
this.filePath = path.join(this.stateDir, this.relativePath);
|
||||
}
|
||||
|
||||
async list(status?: SkillWorkshopStatus): Promise<SkillProposal[]> {
|
||||
const file = await readJson(this.stateDir, this.filePath);
|
||||
const file = await readJson(this.stateDir, this.relativePath);
|
||||
const proposals = status
|
||||
? file.proposals.filter((proposal) => proposal.status === status)
|
||||
: file.proposals;
|
||||
@@ -110,7 +110,7 @@ export class SkillWorkshopStore {
|
||||
|
||||
async add(proposal: SkillProposal, maxPending: number): Promise<SkillProposal> {
|
||||
return await withLock(this.filePath, async () => {
|
||||
const file = await readJson(this.stateDir, this.filePath);
|
||||
const file = await readJson(this.stateDir, this.relativePath);
|
||||
const duplicate = file.proposals.find(
|
||||
(item) =>
|
||||
(item.status === "pending" || item.status === "quarantined") &&
|
||||
@@ -132,7 +132,7 @@ export class SkillWorkshopStore {
|
||||
).length <= maxPending
|
||||
);
|
||||
});
|
||||
await atomicWriteJson(this.stateDir, this.filePath, {
|
||||
await atomicWriteJson(this.stateDir, this.relativePath, {
|
||||
...file,
|
||||
version: 1,
|
||||
proposals: nextProposals,
|
||||
@@ -143,41 +143,41 @@ export class SkillWorkshopStore {
|
||||
|
||||
async updateStatus(id: string, status: SkillWorkshopStatus): Promise<SkillProposal> {
|
||||
return await withLock(this.filePath, async () => {
|
||||
const file = await readJson(this.stateDir, this.filePath);
|
||||
const file = await readJson(this.stateDir, this.relativePath);
|
||||
const index = file.proposals.findIndex((proposal) => proposal.id === id);
|
||||
if (index < 0) {
|
||||
throw new Error(`proposal not found: ${id}`);
|
||||
}
|
||||
const updated = { ...file.proposals[index], status, updatedAt: Date.now() };
|
||||
file.proposals[index] = updated;
|
||||
await atomicWriteJson(this.stateDir, this.filePath, file);
|
||||
await atomicWriteJson(this.stateDir, this.relativePath, file);
|
||||
return updated;
|
||||
});
|
||||
}
|
||||
|
||||
async recordReviewTurn(toolCalls: number): Promise<SkillWorkshopReviewState> {
|
||||
return await withLock(this.filePath, async () => {
|
||||
const file = await readJson(this.stateDir, this.filePath);
|
||||
const file = await readJson(this.stateDir, this.relativePath);
|
||||
const current = normalizeReviewState(file.review);
|
||||
const next = {
|
||||
...current,
|
||||
turnsSinceReview: current.turnsSinceReview + 1,
|
||||
toolCallsSinceReview: current.toolCallsSinceReview + Math.max(0, Math.trunc(toolCalls)),
|
||||
};
|
||||
await atomicWriteJson(this.stateDir, this.filePath, { ...file, review: next });
|
||||
await atomicWriteJson(this.stateDir, this.relativePath, { ...file, review: next });
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
async markReviewed(): Promise<SkillWorkshopReviewState> {
|
||||
return await withLock(this.filePath, async () => {
|
||||
const file = await readJson(this.stateDir, this.filePath);
|
||||
const file = await readJson(this.stateDir, this.relativePath);
|
||||
const next = {
|
||||
turnsSinceReview: 0,
|
||||
toolCallsSinceReview: 0,
|
||||
lastReviewAt: Date.now(),
|
||||
};
|
||||
await atomicWriteJson(this.stateDir, this.filePath, { ...file, review: next });
|
||||
await atomicWriteJson(this.stateDir, this.relativePath, { ...file, review: next });
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { writeJsonFileAtomically } from "openclaw/plugin-sdk/json-store";
|
||||
import { readJsonFileWithFallback, writeJsonFileAtomically } from "openclaw/plugin-sdk/json-store";
|
||||
import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
|
||||
|
||||
const STORE_VERSION = 2;
|
||||
@@ -45,30 +45,30 @@ function extractBotIdFromToken(token?: string): string | null {
|
||||
return rawBotId;
|
||||
}
|
||||
|
||||
function safeParseState(raw: string): TelegramUpdateOffsetState | null {
|
||||
function safeParseState(parsed: unknown): TelegramUpdateOffsetState | null {
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as {
|
||||
const state = parsed as {
|
||||
version?: number;
|
||||
lastUpdateId?: number | null;
|
||||
botId?: string | null;
|
||||
};
|
||||
if (parsed?.version !== STORE_VERSION && parsed?.version !== 1) {
|
||||
if (state?.version !== STORE_VERSION && state?.version !== 1) {
|
||||
return null;
|
||||
}
|
||||
if (parsed.lastUpdateId !== null && !isValidUpdateId(parsed.lastUpdateId)) {
|
||||
if (state.lastUpdateId !== null && !isValidUpdateId(state.lastUpdateId)) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
parsed.version === STORE_VERSION &&
|
||||
parsed.botId !== null &&
|
||||
typeof parsed.botId !== "string"
|
||||
state.version === STORE_VERSION &&
|
||||
state.botId !== null &&
|
||||
typeof state.botId !== "string"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
version: STORE_VERSION,
|
||||
lastUpdateId: parsed.lastUpdateId ?? null,
|
||||
botId: parsed.version === STORE_VERSION ? (parsed.botId ?? null) : null,
|
||||
lastUpdateId: state.lastUpdateId ?? null,
|
||||
botId: state.version === STORE_VERSION ? (state.botId ?? null) : null,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
@@ -81,24 +81,16 @@ export async function readTelegramUpdateOffset(params: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): Promise<number | null> {
|
||||
const filePath = resolveTelegramUpdateOffsetPath(params.accountId, params.env);
|
||||
try {
|
||||
const raw = await fs.readFile(filePath, "utf-8");
|
||||
const parsed = safeParseState(raw);
|
||||
const expectedBotId = extractBotIdFromToken(params.botToken);
|
||||
if (expectedBotId && parsed?.botId && parsed.botId !== expectedBotId) {
|
||||
return null;
|
||||
}
|
||||
if (expectedBotId && parsed?.botId === null) {
|
||||
return null;
|
||||
}
|
||||
return parsed?.lastUpdateId ?? null;
|
||||
} catch (err) {
|
||||
const code = (err as { code?: string }).code;
|
||||
if (code === "ENOENT") {
|
||||
return null;
|
||||
}
|
||||
const { value } = await readJsonFileWithFallback<unknown>(filePath, null);
|
||||
const parsed = safeParseState(value);
|
||||
const expectedBotId = extractBotIdFromToken(params.botToken);
|
||||
if (expectedBotId && parsed?.botId && parsed.botId !== expectedBotId) {
|
||||
return null;
|
||||
}
|
||||
if (expectedBotId && parsed?.botId === null) {
|
||||
return null;
|
||||
}
|
||||
return parsed?.lastUpdateId ?? null;
|
||||
}
|
||||
|
||||
export async function writeTelegramUpdateOffset(params: {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
||||
import { root } from "openclaw/plugin-sdk/security-runtime";
|
||||
import type { VoiceCallConfig } from "./config.js";
|
||||
import type { CoreAgentDeps, CoreConfig } from "./core-bridge.js";
|
||||
|
||||
@@ -42,15 +41,6 @@ function resolveAgentSystemPromptOverride(cfg: CoreConfig, agentId: string): str
|
||||
);
|
||||
}
|
||||
|
||||
function isSafeWorkspaceRelativeFile(file: string): boolean {
|
||||
if (!file.trim() || path.isAbsolute(file)) {
|
||||
return false;
|
||||
}
|
||||
const normalized = path.normalize(file);
|
||||
const parts = normalized.split(/[\\/]+/);
|
||||
return normalized !== "." && !parts.includes("..") && !normalized.includes("\0");
|
||||
}
|
||||
|
||||
function limitText(text: string, maxChars: number): string {
|
||||
if (text.length <= maxChars) {
|
||||
return text;
|
||||
@@ -65,12 +55,15 @@ async function readWorkspaceVoiceContextFiles(params: {
|
||||
}): Promise<string[]> {
|
||||
const sections: string[] = [];
|
||||
let remaining = params.maxChars;
|
||||
const workspaceRoot = await root(params.workspaceDir).catch(() => null);
|
||||
if (!workspaceRoot) {
|
||||
return sections;
|
||||
}
|
||||
for (const file of params.files) {
|
||||
if (remaining <= 0 || !isSafeWorkspaceRelativeFile(file)) {
|
||||
if (remaining <= 0) {
|
||||
continue;
|
||||
}
|
||||
const fullPath = path.join(params.workspaceDir, path.normalize(file));
|
||||
const content = await readFile(fullPath, "utf8").catch(() => undefined);
|
||||
const content = await workspaceRoot.readText(file).catch(() => undefined);
|
||||
const trimmed = content?.trim();
|
||||
if (!trimmed) {
|
||||
continue;
|
||||
|
||||
Reference in New Issue
Block a user