Compare commits

...

7 Commits

Author SHA1 Message Date
root
e6ddbc8186 fix(install): copy no-reply-api to dist during installation 2026-03-03 19:03:55 +00:00
root
9af20b07c7 fix: add debug logging for no-reply-api and moderator bot startup 2026-03-03 19:03:55 +00:00
3d91d7ed95 Merge pull request 'dev/fix-issues' (#12) from dev/fix-issues into main
Reviewed-on: #12
2026-03-03 18:41:27 +00:00
zhi
92799176bf fix(install): fix uninstall order to satisfy config validation
- Remove from plugins.allow BEFORE deleting plugins.entries.dirigent
- OpenClaw validates that allow[] entries must exist in entries{}
- New order: allow → entry → paths → provider
2026-03-03 18:28:44 +00:00
zhi
cd0ce6a910 fix(install): use unsetPath for plugin entry removal
- Use openclaw config unset instead of get-modify-set
- Avoids triggering full plugins config validation
- Handles both added and replaced entries uniformly
2026-03-03 18:23:01 +00:00
zhi
a177150554 fix(install): uninstall always deletes plugin entry
- For uninstall, always delete plugins.entries.dirigent
- Don't try to restore old config (which may fail validation due to missing required fields)
- Provider restoration still works (replaced providers are restored)
2026-03-03 18:20:48 +00:00
zhi
b79cc1eb84 fix: use api.resolvePath for plugin directory
- Replace import.meta.url with api.resolvePath('.') for reliable path resolution
- Fixes no-reply API not starting due to incorrect pluginDir calculation
2026-03-03 18:16:47 +00:00
2 changed files with 63 additions and 38 deletions

View File

@@ -10,17 +10,23 @@ import { startModeratorPresence, stopModeratorPresence } from "./moderator-prese
let noReplyProcess: ChildProcess | null = null; let noReplyProcess: ChildProcess | null = null;
function startNoReplyApi(logger: { info: (m: string) => void; warn: (m: string) => void }, pluginDir: string, port = 8787): void { function startNoReplyApi(logger: { info: (m: string) => void; warn: (m: string) => void }, pluginDir: string, port = 8787): void {
logger.info(`dirigent: startNoReplyApi called, pluginDir=${pluginDir}`);
if (noReplyProcess) { if (noReplyProcess) {
logger.info("dirigent: no-reply API already running, skipping"); logger.info("dirigent: no-reply API already running, skipping");
return; return;
} }
const serverPath = path.resolve(pluginDir, "..", "no-reply-api", "server.mjs"); const serverPath = path.resolve(pluginDir, "..", "no-reply-api", "server.mjs");
logger.info(`dirigent: resolved serverPath=${serverPath}`);
if (!fs.existsSync(serverPath)) { if (!fs.existsSync(serverPath)) {
logger.warn(`dirigent: no-reply API server not found at ${serverPath}, skipping`); logger.warn(`dirigent: no-reply API server not found at ${serverPath}, skipping`);
return; return;
} }
logger.info(`dirigent: no-reply API server found, spawning process...`);
noReplyProcess = spawn(process.execPath, [serverPath], { noReplyProcess = spawn(process.execPath, [serverPath], {
env: { ...process.env, PORT: String(port) }, env: { ...process.env, PORT: String(port) },
stdio: ["ignore", "pipe", "pipe"], stdio: ["ignore", "pipe", "pipe"],
@@ -508,16 +514,28 @@ export default {
ensurePolicyStateLoaded(api, liveAtRegister); ensurePolicyStateLoaded(api, liveAtRegister);
// Resolve plugin directory for locating sibling modules (no-reply-api/) // Resolve plugin directory for locating sibling modules (no-reply-api/)
const pluginDir = path.dirname(new URL(import.meta.url).pathname); // Use api.resolvePath to get the actual plugin directory in OpenClaw environment
const pluginDir = api.resolvePath(".");
// Gateway lifecycle: start/stop no-reply API and moderator bot with the gateway // Gateway lifecycle: start/stop no-reply API and moderator bot with the gateway
api.on("gateway_start", () => { api.on("gateway_start", () => {
api.logger.info(`dirigent: gateway_start event received, pluginDir=${pluginDir}`);
// Check no-reply-api server file exists
const serverPath = path.resolve(pluginDir, "..", "no-reply-api", "server.mjs");
api.logger.info(`dirigent: checking no-reply-api server at ${serverPath}, exists=${fs.existsSync(serverPath)}`);
startNoReplyApi(api.logger, pluginDir); startNoReplyApi(api.logger, pluginDir);
const live = getLivePluginConfig(api, baseConfig as DirigentConfig); const live = getLivePluginConfig(api, baseConfig as DirigentConfig);
api.logger.info(`dirigent: config loaded, moderatorBotToken=${live.moderatorBotToken ? "[set]" : "[not set]"}`);
if (live.moderatorBotToken) { if (live.moderatorBotToken) {
api.logger.info("dirigent: starting moderator bot presence...");
startModeratorPresence(live.moderatorBotToken, api.logger); startModeratorPresence(live.moderatorBotToken, api.logger);
api.logger.info("dirigent: moderator bot presence starting"); api.logger.info("dirigent: moderator bot presence started");
} else {
api.logger.info("dirigent: moderator bot not starting - no moderatorBotToken in config");
} }
}); });

View File

@@ -22,12 +22,27 @@ const __dirname = path.dirname(new URL(import.meta.url).pathname);
// Ensure dist/dirigent exists (handles git clone without npm prepare) // Ensure dist/dirigent exists (handles git clone without npm prepare)
const DIST_DIR = path.resolve(__dirname, "..", "dist", "dirigent"); const DIST_DIR = path.resolve(__dirname, "..", "dist", "dirigent");
const PLUGIN_SRC_DIR = path.resolve(__dirname, "..", "plugin"); const PLUGIN_SRC_DIR = path.resolve(__dirname, "..", "plugin");
if (mode === "install" && !fs.existsSync(DIST_DIR)) { const NO_REPLY_API_SRC_DIR = path.resolve(__dirname, "..", "no-reply-api");
console.log("[dirigent] dist/ not found, syncing from plugin/..."); const NO_REPLY_API_DIST_DIR = path.resolve(__dirname, "..", "dist", "no-reply-api");
if (mode === "install") {
// Copy plugin files to dist/dirigent
if (!fs.existsSync(DIST_DIR)) {
console.log("[dirigent] dist/dirigent/ not found, syncing from plugin/...");
fs.mkdirSync(DIST_DIR, { recursive: true }); fs.mkdirSync(DIST_DIR, { recursive: true });
for (const f of fs.readdirSync(PLUGIN_SRC_DIR)) { for (const f of fs.readdirSync(PLUGIN_SRC_DIR)) {
fs.copyFileSync(path.join(PLUGIN_SRC_DIR, f), path.join(DIST_DIR, f)); fs.copyFileSync(path.join(PLUGIN_SRC_DIR, f), path.join(DIST_DIR, f));
} }
}
// Copy no-reply-api files to dist/no-reply-api
if (!fs.existsSync(NO_REPLY_API_DIST_DIR)) {
console.log("[dirigent] dist/no-reply-api/ not found, syncing from no-reply-api/...");
fs.mkdirSync(NO_REPLY_API_DIST_DIR, { recursive: true });
for (const f of fs.readdirSync(NO_REPLY_API_SRC_DIR)) {
fs.copyFileSync(path.join(NO_REPLY_API_SRC_DIR, f), path.join(NO_REPLY_API_DIST_DIR, f));
}
}
} }
const PLUGIN_PATH = env.PLUGIN_PATH || DIST_DIR; const PLUGIN_PATH = env.PLUGIN_PATH || DIST_DIR;
@@ -279,22 +294,25 @@ else {
const delta = rec.delta || { added: {}, replaced: {}, removed: {} }; const delta = rec.delta || { added: {}, replaced: {}, removed: {} };
try { try {
// ── Handle ADDED entries: remove them ───────────────────────────────── // ── IMPORTANT: Order matters for OpenClaw config validation ────────────
if (delta.added[PATH_PLUGIN_ENTRY] !== undefined) { // 1. First remove from allow (before deleting entry, otherwise validation fails)
const plugins = getJson("plugins") || {}; if (delta.added[PATH_PLUGINS_ALLOW] !== undefined) {
plugins.entries = plugins.entries || {}; const allowList = getJson(PATH_PLUGINS_ALLOW) || [];
delete plugins.entries.dirigent; const idx = allowList.indexOf("dirigent");
setJson("plugins", plugins); if (idx !== -1) {
allowList.splice(idx, 1);
setJson(PATH_PLUGINS_ALLOW, allowList);
console.log("[dirigent] removed 'dirigent' from plugins.allow");
}
}
// 2. Then remove entry
if (delta.added[PATH_PLUGIN_ENTRY] !== undefined || delta.replaced[PATH_PLUGIN_ENTRY] !== undefined) {
unsetPath(PATH_PLUGIN_ENTRY);
console.log("[dirigent] removed plugins.entries.dirigent"); console.log("[dirigent] removed plugins.entries.dirigent");
} }
if (delta.added[PATH_PROVIDER_ENTRY] !== undefined) { // 3. Then remove plugin path (after entry is gone)
const providers = getJson(PATH_PROVIDERS) || {};
delete providers[NO_REPLY_PROVIDER_ID];
setJson(PATH_PROVIDERS, providers);
console.log(`[dirigent] removed models.providers.${NO_REPLY_PROVIDER_ID}`);
}
if (delta.added[PATH_PLUGINS_LOAD] !== undefined) { if (delta.added[PATH_PLUGINS_LOAD] !== undefined) {
const plugins = getJson("plugins") || {}; const plugins = getJson("plugins") || {};
const paths = plugins.load?.paths || []; const paths = plugins.load?.paths || [];
@@ -307,26 +325,15 @@ else {
} }
} }
// ── Handle plugins.allow ────────────────────────────────────────────── // 4. Finally remove provider
if (delta.added[PATH_PLUGINS_ALLOW] !== undefined) { if (delta.added[PATH_PROVIDER_ENTRY] !== undefined) {
const allowList = getJson(PATH_PLUGINS_ALLOW) || []; const providers = getJson(PATH_PROVIDERS) || {};
const idx = allowList.indexOf("dirigent"); delete providers[NO_REPLY_PROVIDER_ID];
if (idx !== -1) { setJson(PATH_PROVIDERS, providers);
allowList.splice(idx, 1); console.log(`[dirigent] removed models.providers.${NO_REPLY_PROVIDER_ID}`);
setJson(PATH_PLUGINS_ALLOW, allowList);
console.log("[dirigent] removed 'dirigent' from plugins.allow");
}
}
// ── Handle REPLACED entries: restore old value ────────────────────────
if (delta.replaced[PATH_PLUGIN_ENTRY] !== undefined) {
const plugins = getJson("plugins") || {};
plugins.entries = plugins.entries || {};
plugins.entries.dirigent = delta.replaced[PATH_PLUGIN_ENTRY];
setJson("plugins", plugins);
console.log("[dirigent] restored previous plugins.entries.dirigent");
} }
// ── Handle REPLACED provider: restore old value ───────────────────────
if (delta.replaced[PATH_PROVIDER_ENTRY] !== undefined) { if (delta.replaced[PATH_PROVIDER_ENTRY] !== undefined) {
const providers = getJson(PATH_PROVIDERS) || {}; const providers = getJson(PATH_PROVIDERS) || {};
providers[NO_REPLY_PROVIDER_ID] = delta.replaced[PATH_PROVIDER_ENTRY]; providers[NO_REPLY_PROVIDER_ID] = delta.replaced[PATH_PROVIDER_ENTRY];