mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-18 18:57:55 +08:00
228 lines
7.3 KiB
JavaScript
228 lines
7.3 KiB
JavaScript
// SQLite readers for plugin install indexes produced during E2E scenarios.
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { DatabaseSync } from "node:sqlite";
|
|
import { readPositiveIntEnv } from "./env-limits.mjs";
|
|
import { readTextFileBounded } from "./text-file-utils.mjs";
|
|
|
|
const INDEX_KEY = "installed-plugin-index";
|
|
const ERROR_DETAIL_TAIL_BYTES = 16 * 1024;
|
|
const JSON_ARTIFACT_MAX_BYTES = readPositiveIntEnv(
|
|
"OPENCLAW_PLUGIN_INDEX_JSON_MAX_BYTES",
|
|
1024 * 1024,
|
|
);
|
|
|
|
export function stateDir() {
|
|
return process.env.OPENCLAW_STATE_DIR || path.join(process.env.HOME, ".openclaw");
|
|
}
|
|
|
|
export function configPath() {
|
|
return process.env.OPENCLAW_CONFIG_PATH || path.join(stateDir(), "openclaw.json");
|
|
}
|
|
|
|
function readJsonMaybe(file) {
|
|
let text;
|
|
try {
|
|
text = readTextFileBounded(file, "plugin index JSON artifact", JSON_ARTIFACT_MAX_BYTES, {
|
|
tailBytes: ERROR_DETAIL_TAIL_BYTES,
|
|
});
|
|
} catch (error) {
|
|
if (error?.code === "ETOOBIG") {
|
|
throw error;
|
|
}
|
|
return {};
|
|
}
|
|
try {
|
|
return JSON.parse(text);
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
function textTooLargeError(message) {
|
|
return Object.assign(new Error(message), { code: "ETOOBIG" });
|
|
}
|
|
|
|
function parseIndexJsonText(text, label) {
|
|
const bytes = Buffer.byteLength(text, "utf8");
|
|
if (bytes > JSON_ARTIFACT_MAX_BYTES) {
|
|
throw textTooLargeError(`${label} exceeded ${JSON_ARTIFACT_MAX_BYTES} bytes (${bytes} bytes)`);
|
|
}
|
|
return JSON.parse(text);
|
|
}
|
|
|
|
function assertIndexJsonByteLength(bytesRaw, label) {
|
|
const bytes = Number(bytesRaw);
|
|
if (!Number.isFinite(bytes) || bytes < 0) {
|
|
throw new Error(`${label} byte length was invalid: ${String(bytesRaw)}`);
|
|
}
|
|
if (bytes > JSON_ARTIFACT_MAX_BYTES) {
|
|
throw textTooLargeError(`${label} exceeded ${JSON_ARTIFACT_MAX_BYTES} bytes (${bytes} bytes)`);
|
|
}
|
|
}
|
|
|
|
function sqlitePath(root = stateDir()) {
|
|
return path.join(root, "state", "openclaw.sqlite");
|
|
}
|
|
|
|
function legacyIndexPath(root = stateDir()) {
|
|
return path.join(root, "plugins", "installs.json");
|
|
}
|
|
|
|
function readSqlitePluginIndex(root = stateDir()) {
|
|
const dbPath = sqlitePath(root);
|
|
if (!fs.existsSync(dbPath)) {
|
|
return {};
|
|
}
|
|
let db;
|
|
try {
|
|
db = new DatabaseSync(dbPath, { readOnly: true });
|
|
const lengths = db
|
|
.prepare(
|
|
`
|
|
SELECT octet_length(install_records_json) AS install_records_json_bytes,
|
|
octet_length(plugins_json) AS plugins_json_bytes,
|
|
octet_length(diagnostics_json) AS diagnostics_json_bytes
|
|
FROM installed_plugin_index
|
|
WHERE index_key = ?
|
|
`,
|
|
)
|
|
.get(INDEX_KEY);
|
|
if (!lengths) {
|
|
return {};
|
|
}
|
|
assertIndexJsonByteLength(
|
|
lengths.install_records_json_bytes,
|
|
"plugin index install_records_json",
|
|
);
|
|
assertIndexJsonByteLength(lengths.plugins_json_bytes, "plugin index plugins_json");
|
|
assertIndexJsonByteLength(lengths.diagnostics_json_bytes, "plugin index diagnostics_json");
|
|
const row = db
|
|
.prepare(
|
|
`
|
|
SELECT version, warning, host_contract_version, compat_registry_version,
|
|
migration_version, policy_hash, generated_at_ms, refresh_reason,
|
|
install_records_json, plugins_json, diagnostics_json
|
|
FROM installed_plugin_index
|
|
WHERE index_key = ?
|
|
`,
|
|
)
|
|
.get(INDEX_KEY);
|
|
if (!row) {
|
|
return {};
|
|
}
|
|
return {
|
|
version: Number(row.version),
|
|
...(row.warning ? { warning: row.warning } : {}),
|
|
hostContractVersion: row.host_contract_version,
|
|
compatRegistryVersion: row.compat_registry_version,
|
|
migrationVersion: Number(row.migration_version),
|
|
policyHash: row.policy_hash,
|
|
generatedAtMs: Number(row.generated_at_ms),
|
|
...(row.refresh_reason ? { refreshReason: row.refresh_reason } : {}),
|
|
installRecords: parseIndexJsonText(
|
|
row.install_records_json,
|
|
"plugin index install_records_json",
|
|
),
|
|
plugins: parseIndexJsonText(row.plugins_json, "plugin index plugins_json"),
|
|
diagnostics: parseIndexJsonText(row.diagnostics_json, "plugin index diagnostics_json"),
|
|
};
|
|
} catch (error) {
|
|
if (error?.code === "ETOOBIG") {
|
|
throw error;
|
|
}
|
|
return {};
|
|
} finally {
|
|
db?.close();
|
|
}
|
|
}
|
|
|
|
export function readPluginInstallIndex(options = {}) {
|
|
const root = options.stateDir ?? stateDir();
|
|
const config = readJsonMaybe(options.configPath ?? configPath());
|
|
const sqliteIndex = readSqlitePluginIndex(root);
|
|
if (sqliteIndex.installRecords) {
|
|
return sqliteIndex;
|
|
}
|
|
const legacyIndex = readJsonMaybe(legacyIndexPath(root));
|
|
const installRecords =
|
|
legacyIndex.installRecords ??
|
|
legacyIndex.records ??
|
|
options.fallbackRecords ??
|
|
config.plugins?.installs ??
|
|
{};
|
|
return {
|
|
...legacyIndex,
|
|
installRecords,
|
|
};
|
|
}
|
|
|
|
export function readPluginInstallRecords(options = {}) {
|
|
return readPluginInstallIndex(options).installRecords ?? {};
|
|
}
|
|
|
|
export function writePluginInstallIndexForE2E(index, options = {}) {
|
|
const root = options.stateDir ?? stateDir();
|
|
const dbPath = sqlitePath(root);
|
|
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
const db = new DatabaseSync(dbPath);
|
|
try {
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS installed_plugin_index (
|
|
index_key TEXT NOT NULL PRIMARY KEY,
|
|
version INTEGER NOT NULL,
|
|
host_contract_version TEXT NOT NULL,
|
|
compat_registry_version TEXT NOT NULL,
|
|
migration_version INTEGER NOT NULL,
|
|
policy_hash TEXT NOT NULL,
|
|
generated_at_ms INTEGER NOT NULL,
|
|
refresh_reason TEXT,
|
|
install_records_json TEXT NOT NULL,
|
|
plugins_json TEXT NOT NULL,
|
|
diagnostics_json TEXT NOT NULL,
|
|
warning TEXT,
|
|
updated_at_ms INTEGER NOT NULL
|
|
);
|
|
`);
|
|
const now = Date.now();
|
|
db.prepare(
|
|
`
|
|
INSERT INTO installed_plugin_index (
|
|
index_key, version, host_contract_version, compat_registry_version,
|
|
migration_version, policy_hash, generated_at_ms, refresh_reason,
|
|
install_records_json, plugins_json, diagnostics_json, warning, updated_at_ms
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT(index_key) DO UPDATE SET
|
|
version = excluded.version,
|
|
host_contract_version = excluded.host_contract_version,
|
|
compat_registry_version = excluded.compat_registry_version,
|
|
migration_version = excluded.migration_version,
|
|
policy_hash = excluded.policy_hash,
|
|
generated_at_ms = excluded.generated_at_ms,
|
|
refresh_reason = excluded.refresh_reason,
|
|
install_records_json = excluded.install_records_json,
|
|
plugins_json = excluded.plugins_json,
|
|
diagnostics_json = excluded.diagnostics_json,
|
|
warning = excluded.warning,
|
|
updated_at_ms = excluded.updated_at_ms
|
|
`,
|
|
).run(
|
|
INDEX_KEY,
|
|
index.version ?? 1,
|
|
index.hostContractVersion ?? "docker-e2e",
|
|
index.compatRegistryVersion ?? "docker-e2e",
|
|
index.migrationVersion ?? 1,
|
|
index.policyHash ?? "docker-e2e",
|
|
index.generatedAtMs ?? now,
|
|
index.refreshReason ?? null,
|
|
JSON.stringify(index.installRecords ?? {}),
|
|
JSON.stringify(index.plugins ?? []),
|
|
JSON.stringify(index.diagnostics ?? []),
|
|
index.warning ?? "DO NOT EDIT. This row is generated by OpenClaw plugin registry commands.",
|
|
now,
|
|
);
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|