jishushell 0.4.2 → 0.4.10
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/Dockerfile.openclaw-slim +58 -0
- package/INSTALL-NOTICE +47 -0
- package/dist/auth.js +3 -3
- package/dist/auth.js.map +1 -1
- package/dist/cli.js +517 -1
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +21 -4
- package/dist/config.js +88 -54
- package/dist/config.js.map +1 -1
- package/dist/control.js +5 -5
- package/dist/control.js.map +1 -1
- package/dist/doctor.js +47 -14
- package/dist/doctor.js.map +1 -1
- package/dist/install.d.ts +1 -1
- package/dist/install.js +15 -29
- package/dist/install.js.map +1 -1
- package/dist/routes/backup.d.ts +2 -0
- package/dist/routes/backup.js +370 -0
- package/dist/routes/backup.js.map +1 -0
- package/dist/routes/instances.d.ts +1 -0
- package/dist/routes/instances.js +51 -11
- package/dist/routes/instances.js.map +1 -1
- package/dist/routes/setup.js +3 -5
- package/dist/routes/setup.js.map +1 -1
- package/dist/server.js +29 -1
- package/dist/server.js.map +1 -1
- package/dist/services/backup-manager.d.ts +253 -0
- package/dist/services/backup-manager.js +2014 -0
- package/dist/services/backup-manager.js.map +1 -0
- package/dist/services/backup-verify.d.ts +26 -0
- package/dist/services/backup-verify.js +240 -0
- package/dist/services/backup-verify.js.map +1 -0
- package/dist/services/instance-manager.d.ts +24 -4
- package/dist/services/instance-manager.js +218 -49
- package/dist/services/instance-manager.js.map +1 -1
- package/dist/services/nomad-manager.js +72 -131
- package/dist/services/nomad-manager.js.map +1 -1
- package/dist/services/process-manager.js +4 -3
- package/dist/services/process-manager.js.map +1 -1
- package/dist/services/setup-manager.d.ts +4 -2
- package/dist/services/setup-manager.js +268 -129
- package/dist/services/setup-manager.js.map +1 -1
- package/dist/services/telemetry/activation.js +10 -7
- package/dist/services/telemetry/activation.js.map +1 -1
- package/dist/services/telemetry/client.js +7 -18
- package/dist/services/telemetry/client.js.map +1 -1
- package/dist/services/telemetry/heartbeat.js +12 -6
- package/dist/services/telemetry/heartbeat.js.map +1 -1
- package/dist/utils/fs.d.ts +85 -0
- package/dist/utils/fs.js +111 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/safe-json.d.ts +2 -0
- package/dist/utils/safe-json.js +22 -16
- package/dist/utils/safe-json.js.map +1 -1
- package/install/jishu-install-china.sh +3092 -0
- package/install/jishu-install.sh +310 -108
- package/install/jishu-uninstall.sh +276 -391
- package/install/post-install.sh +23 -0
- package/openclaw-entry.sh +15 -0
- package/package.json +7 -4
- package/public/assets/Dashboard-DhsrzJ4F.js +1 -0
- package/public/assets/{InitPassword-CkehIkJG.js → InitPassword-BjubiVdd.js} +1 -1
- package/public/assets/InstanceDetail-DMcywsof.js +17 -0
- package/public/assets/{Login-RkjzTNWg.js → Login-CUoEZOWR.js} +1 -1
- package/public/assets/NewInstance-Bk0G4EiJ.js +1 -0
- package/public/assets/Settings-D5tHL_h5.js +1 -0
- package/public/assets/Setup-4t6E3Rut.js +1 -0
- package/public/assets/index-BJ47MWpF.css +1 -0
- package/public/assets/index-DbX85irc.js +16 -0
- package/public/assets/logo-black-theme-DywLAtFy.png +0 -0
- package/public/assets/logo-white-theme-DXffFAWw.png +0 -0
- package/public/assets/{usePolling-CqQ8hrNc.js → usePolling-CK0DfI4h.js} +1 -1
- package/public/assets/{vendor-i18n-Bvxxh8Di.js → vendor-i18n-CfW0RvgE.js} +1 -1
- package/public/assets/vendor-react-B1-3Yrt-.js +59 -0
- package/public/index.html +4 -4
- package/public/assets/Dashboard-CAOQDYDR.js +0 -1
- package/public/assets/InstanceDetail-CzW2S95J.js +0 -14
- package/public/assets/NewInstance-DdbErdjA.js +0 -1
- package/public/assets/Settings-BUD7zwv9.js +0 -1
- package/public/assets/Setup-RRTIERGG.js +0 -1
- package/public/assets/index-77Ug7feY.css +0 -1
- package/public/assets/index-DfRnVUQR.js +0 -16
- package/public/assets/vendor-react-DONn7uBV.js +0 -59
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { execFile, execFileSync } from "child_process";
|
|
2
2
|
import { randomBytes } from "crypto";
|
|
3
|
-
import { chmodSync, chownSync, copyFileSync, cpSync, existsSync,
|
|
3
|
+
import { chmodSync, chownSync, copyFileSync, cpSync, existsSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, } from "fs";
|
|
4
4
|
import { createServer as netCreateServer } from "net";
|
|
5
5
|
import { userInfo } from "os";
|
|
6
6
|
import { dirname, join, resolve } from "path";
|
|
7
|
-
import { INSTANCES_DIR, JISHUSHELL_HOME, getPanelConfig } from "../config.js";
|
|
7
|
+
import { BACKUPS_DIR, INSTANCES_DIR, JISHUSHELL_HOME, getPanelConfig } from "../config.js";
|
|
8
8
|
import { LEGACY_PROVIDER_API_ALIASES } from "../constants.js";
|
|
9
9
|
import { safeReadJson, safeWriteJson } from "../utils/safe-json.js";
|
|
10
|
+
import { ensureDirContainer, writeConfigFile, writeSecretFile } from "../utils/fs.js";
|
|
10
11
|
const _configChangeListeners = [];
|
|
11
12
|
export function onConfigChange(listener) {
|
|
12
13
|
_configChangeListeners.push(listener);
|
|
@@ -357,7 +358,7 @@ function quoteEnvValue(value) {
|
|
|
357
358
|
return JSON.stringify(value);
|
|
358
359
|
}
|
|
359
360
|
export function updateEnvFile(path, updates) {
|
|
360
|
-
|
|
361
|
+
ensureDirContainer(dirname(path));
|
|
361
362
|
const existing = existsSync(path) ? readFileSync(path, "utf-8").split("\n") : [];
|
|
362
363
|
const remaining = { ...updates };
|
|
363
364
|
const newLines = [];
|
|
@@ -389,7 +390,7 @@ export function updateEnvFile(path, updates) {
|
|
|
389
390
|
const content = output ? output + "\n" : "";
|
|
390
391
|
// Atomic write: tmp then rename to protect against RPi power loss
|
|
391
392
|
const tmp = path + ".tmp";
|
|
392
|
-
|
|
393
|
+
writeSecretFile(tmp, content);
|
|
393
394
|
renameSync(tmp, path);
|
|
394
395
|
}
|
|
395
396
|
// ── Provider key helpers ──
|
|
@@ -536,6 +537,40 @@ export const CHANNEL_PLUGIN_MAP = {
|
|
|
536
537
|
// Official vendor plugins — need install (not bundled)
|
|
537
538
|
"openclaw-weixin": "@tencent-weixin/openclaw-weixin",
|
|
538
539
|
};
|
|
540
|
+
/**
|
|
541
|
+
* Known IM plugin entry IDs as they appear under `config.plugins.entries`.
|
|
542
|
+
* This is the union of channel IDs and the dir-alias names (e.g. `feishu` may
|
|
543
|
+
* register the plugin as `openclaw-lark`), which is what must be scrubbed when
|
|
544
|
+
* dissociating an instance from its inherited IM bindings.
|
|
545
|
+
*/
|
|
546
|
+
const IM_PLUGIN_ENTRY_IDS = new Set([
|
|
547
|
+
...Object.keys(CHANNEL_PLUGIN_MAP),
|
|
548
|
+
...Object.values(CHANNEL_EXT_DIR_ALIAS),
|
|
549
|
+
]);
|
|
550
|
+
/**
|
|
551
|
+
* Dissociate a cloned/imported config from its source instance's IM bindings.
|
|
552
|
+
*
|
|
553
|
+
* Mutates the given config in place:
|
|
554
|
+
* - Deletes the entire `channels` block (same channel cannot serve multiple
|
|
555
|
+
* instances, so every inherited enabled/credential/account entry must go).
|
|
556
|
+
* - Deletes matching IM entries from `plugins.entries` so the plugin loader
|
|
557
|
+
* does not try to boot a channel whose config no longer exists.
|
|
558
|
+
*
|
|
559
|
+
* Used by both domain clone (`createInstance`'s `cloneFrom` path) and the
|
|
560
|
+
* backup import paths (`importInstance`, `createFromBackup`) so that a new
|
|
561
|
+
* instance never inherits a half-configured IM binding.
|
|
562
|
+
*/
|
|
563
|
+
export function stripImBindings(config) {
|
|
564
|
+
if (config?.channels)
|
|
565
|
+
delete config.channels;
|
|
566
|
+
const entries = config?.plugins?.entries;
|
|
567
|
+
if (entries && typeof entries === "object") {
|
|
568
|
+
for (const key of Object.keys(entries)) {
|
|
569
|
+
if (IM_PLUGIN_ENTRY_IDS.has(key))
|
|
570
|
+
delete entries[key];
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
539
574
|
/** Check if a channel plugin is installed for an instance. */
|
|
540
575
|
export function isChannelPluginInstalled(instanceId, channelId) {
|
|
541
576
|
const extDirName = CHANNEL_EXT_DIR_ALIAS[channelId] || channelId;
|
|
@@ -546,8 +581,9 @@ export function isChannelPluginInstalled(instanceId, channelId) {
|
|
|
546
581
|
|| (extDirName !== channelId && existsSync(join(stockExtDir, channelId)));
|
|
547
582
|
}
|
|
548
583
|
/**
|
|
549
|
-
* Install a single channel plugin
|
|
550
|
-
*
|
|
584
|
+
* Install a single channel plugin.
|
|
585
|
+
* Docker mode: runs install inside the running container via docker exec.
|
|
586
|
+
* Host mode (fallback): spawns the host openclaw binary directly.
|
|
551
587
|
*/
|
|
552
588
|
export async function installChannelPlugin(instanceId, channelId) {
|
|
553
589
|
const pkg = CHANNEL_PLUGIN_MAP[channelId];
|
|
@@ -556,14 +592,17 @@ export async function installChannelPlugin(instanceId, channelId) {
|
|
|
556
592
|
if (isChannelPluginInstalled(instanceId, channelId))
|
|
557
593
|
return;
|
|
558
594
|
const openclawHome = getOpenclawHomeInternal(instanceId);
|
|
559
|
-
const openclawBin = resolveOpenclawBin();
|
|
560
595
|
const extensionsDir = getChannelExtensionsDir(instanceId);
|
|
561
|
-
//
|
|
562
|
-
|
|
563
|
-
|
|
596
|
+
// Docker mode: always install inside container via docker exec
|
|
597
|
+
const { getNomadDriver } = await import("../config.js");
|
|
598
|
+
if (getNomadDriver() === "docker") {
|
|
599
|
+
await installChannelPluginViaDocker(instanceId, channelId, pkg, extensionsDir);
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
const openclawBin = resolveOpenclawBin();
|
|
603
|
+
// Host mode: spawn openclaw binary directly
|
|
564
604
|
const nodeBinDir = dirname(process.execPath);
|
|
565
605
|
const childPath = [nodeBinDir, process.env.PATH].filter(Boolean).join(":");
|
|
566
|
-
// Pass through proxy and TLS env vars so fetch works under systemd / restricted environments.
|
|
567
606
|
const proxyEnvKeys = [
|
|
568
607
|
"http_proxy", "HTTP_PROXY", "https_proxy", "HTTPS_PROXY",
|
|
569
608
|
"no_proxy", "NO_PROXY", "NODE_EXTRA_CA_CERTS", "NODE_TLS_REJECT_UNAUTHORIZED",
|
|
@@ -588,12 +627,9 @@ export async function installChannelPlugin(instanceId, channelId) {
|
|
|
588
627
|
env: childEnv,
|
|
589
628
|
timeout: 300_000,
|
|
590
629
|
}, (err, stdout, stderr) => {
|
|
591
|
-
// openclaw plugins install may exit non-zero due to "plugins.allow is empty" warning
|
|
592
|
-
// even though the plugin was installed successfully. Check actual directory.
|
|
593
630
|
if (err && !isChannelPluginInstalled(instanceId, channelId)) {
|
|
594
631
|
const msg = [stderr?.trim(), stdout?.trim(), err.message].filter(Boolean).join(" | ");
|
|
595
632
|
console.error(`[plugins] ${pkg} exit code ${err.code ?? '?'}, stderr: ${stderr?.trim() || '(empty)'}, stdout: ${stdout?.trim() || '(empty)'}`);
|
|
596
|
-
// Clean up leftover stage directories
|
|
597
633
|
try {
|
|
598
634
|
if (existsSync(extensionsDir)) {
|
|
599
635
|
for (const entry of readdirSync(extensionsDir)) {
|
|
@@ -604,7 +640,7 @@ export async function installChannelPlugin(instanceId, channelId) {
|
|
|
604
640
|
}
|
|
605
641
|
}
|
|
606
642
|
}
|
|
607
|
-
catch {
|
|
643
|
+
catch (_) { }
|
|
608
644
|
reject(new Error(msg));
|
|
609
645
|
}
|
|
610
646
|
else {
|
|
@@ -621,6 +657,20 @@ export async function installChannelPlugin(instanceId, channelId) {
|
|
|
621
657
|
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
622
658
|
try {
|
|
623
659
|
await attemptInstall();
|
|
660
|
+
const extDirName = CHANNEL_EXT_DIR_ALIAS[channelId] || channelId;
|
|
661
|
+
const installedExtDir = join(extensionsDir, extDirName);
|
|
662
|
+
if (existsSync(installedExtDir)) {
|
|
663
|
+
ensureDirContainer(installedExtDir);
|
|
664
|
+
try {
|
|
665
|
+
for (const entry of readdirSync(installedExtDir, { withFileTypes: true })) {
|
|
666
|
+
if (entry.isDirectory()) {
|
|
667
|
+
ensureDirContainer(join(installedExtDir, entry.name));
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
catch { /* best effort */ }
|
|
672
|
+
}
|
|
673
|
+
ensureDirContainer(extensionsDir);
|
|
624
674
|
return;
|
|
625
675
|
}
|
|
626
676
|
catch (err) {
|
|
@@ -637,6 +687,67 @@ export async function installChannelPlugin(instanceId, channelId) {
|
|
|
637
687
|
}
|
|
638
688
|
throw lastErr;
|
|
639
689
|
}
|
|
690
|
+
/**
|
|
691
|
+
* Install a channel plugin inside the running Docker container via nomad-manager.exec().
|
|
692
|
+
* Requires the instance to be running — the extensions dir is bind-mounted so
|
|
693
|
+
* the install persists on the host filesystem.
|
|
694
|
+
*/
|
|
695
|
+
async function installChannelPluginViaDocker(instanceId, channelId, pkg, extensionsDir) {
|
|
696
|
+
const { exec } = await import("./nomad-manager.js");
|
|
697
|
+
const MAX_ATTEMPTS = 3;
|
|
698
|
+
const RETRY_DELAY_MS = 5_000;
|
|
699
|
+
console.log(`[plugins] Installing ${pkg} for ${channelId} via docker exec (instance: ${instanceId})...`);
|
|
700
|
+
let lastErr;
|
|
701
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
702
|
+
try {
|
|
703
|
+
const result = await exec(instanceId, ["openclaw", "plugins", "install", pkg], 300_000);
|
|
704
|
+
// Check if plugin was actually installed (openclaw may exit non-zero with warnings)
|
|
705
|
+
if (result.exitCode !== 0 && !isChannelPluginInstalled(instanceId, channelId)) {
|
|
706
|
+
const msg = [result.stderr?.trim(), result.stdout?.trim()].filter(Boolean).join(" | ");
|
|
707
|
+
console.error(`[plugins] ${pkg} docker exec exit code ${result.exitCode}, output: ${msg}`);
|
|
708
|
+
throw new Error(msg || `openclaw plugins install exited with code ${result.exitCode}`);
|
|
709
|
+
}
|
|
710
|
+
if (result.exitCode !== 0) {
|
|
711
|
+
console.log(`[plugins] ${pkg} installed via docker (ignored non-zero exit: warning only)`);
|
|
712
|
+
}
|
|
713
|
+
else {
|
|
714
|
+
console.log(`[plugins] ${pkg} installed via docker`);
|
|
715
|
+
}
|
|
716
|
+
// Fix ownership on host side
|
|
717
|
+
const extDirName = CHANNEL_EXT_DIR_ALIAS[channelId] || channelId;
|
|
718
|
+
const installedExtDir = join(extensionsDir, extDirName);
|
|
719
|
+
if (existsSync(installedExtDir)) {
|
|
720
|
+
ensureDirContainer(installedExtDir);
|
|
721
|
+
try {
|
|
722
|
+
for (const entry of readdirSync(installedExtDir, { withFileTypes: true })) {
|
|
723
|
+
if (entry.isDirectory()) {
|
|
724
|
+
ensureDirContainer(join(installedExtDir, entry.name));
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
catch { /* best effort */ }
|
|
729
|
+
}
|
|
730
|
+
ensureDirContainer(extensionsDir);
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
catch (err) {
|
|
734
|
+
lastErr = err;
|
|
735
|
+
// "Instance is not running" from nomad-manager.exec() — give a clear user-facing message
|
|
736
|
+
if (/not running/i.test(err.message ?? "")) {
|
|
737
|
+
throw new Error("请先启动实例后再安装插件(Docker 模式下插件需在容器内安装)");
|
|
738
|
+
}
|
|
739
|
+
const isTransient = /fetch failed|ECONNREFUSED/i.test(err.message ?? "");
|
|
740
|
+
if (isTransient && attempt < MAX_ATTEMPTS) {
|
|
741
|
+
console.warn(`[plugins] ${pkg} docker install attempt ${attempt}/${MAX_ATTEMPTS} failed, retrying in ${RETRY_DELAY_MS / 1000}s...`);
|
|
742
|
+
await new Promise(r => setTimeout(r, RETRY_DELAY_MS));
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
console.error(`[plugins] Failed to install ${pkg} via docker:`, err.message);
|
|
746
|
+
break;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
throw lastErr;
|
|
750
|
+
}
|
|
640
751
|
function getChannelExtensionsDir(instanceId) {
|
|
641
752
|
return join(getOpenclawHomeInternal(instanceId), OPENCLAW_STATE_DIRNAME, "extensions");
|
|
642
753
|
}
|
|
@@ -678,7 +789,7 @@ export function getInstance(instanceId) {
|
|
|
678
789
|
throw new Error(`Cannot read instance '${instanceId}' metadata: ${e.message}. Check file ownership with: ls -la ${metaPath}`);
|
|
679
790
|
}
|
|
680
791
|
}
|
|
681
|
-
export async function createInstance(instanceId, name, description = "", cloneFrom, openclawHome) {
|
|
792
|
+
export async function createInstance(instanceId, name, description = "", cloneFrom, openclawHome, cloneOptions) {
|
|
682
793
|
const d = instanceDir(instanceId);
|
|
683
794
|
if (existsSync(d))
|
|
684
795
|
throw new Error(`Instance '${instanceId}' already exists`);
|
|
@@ -717,18 +828,15 @@ export async function createInstance(instanceId, name, description = "", cloneFr
|
|
|
717
828
|
// readdirSync failed — directory might not be readable, proceed cautiously
|
|
718
829
|
}
|
|
719
830
|
}
|
|
720
|
-
|
|
721
|
-
chmodSync(d, 0o750);
|
|
831
|
+
ensureDirContainer(d);
|
|
722
832
|
// Inherit group from INSTANCES_DIR so both root and the real user can access
|
|
723
833
|
try {
|
|
724
834
|
const parentGid = statSync(INSTANCES_DIR).gid;
|
|
725
835
|
chownSync(d, -1, parentGid);
|
|
726
836
|
}
|
|
727
837
|
catch { /* non-root without CAP_CHOWN — already correct owner */ }
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
mkdirSync(join(home, OPENCLAW_STATE_DIRNAME), { recursive: true, mode: 0o750 });
|
|
731
|
-
chmodSync(join(home, OPENCLAW_STATE_DIRNAME), 0o750);
|
|
838
|
+
ensureDirContainer(home);
|
|
839
|
+
ensureDirContainer(join(home, OPENCLAW_STATE_DIRNAME));
|
|
732
840
|
const runtime = await defaultRuntime(instanceId, home);
|
|
733
841
|
const allocatedPort = extractGatewayPort(runtime);
|
|
734
842
|
// Port already reserved inside defaultGatewayPort; just track for cleanup
|
|
@@ -744,9 +852,8 @@ export async function createInstance(instanceId, name, description = "", cloneFr
|
|
|
744
852
|
safeWriteJson(instanceMetaPath(instanceId), meta);
|
|
745
853
|
const envFiles = (runtime.env_files || []).map((p) => normalizePath(p));
|
|
746
854
|
for (const ef of envFiles) {
|
|
747
|
-
mkdirSync(dirname(ef), { recursive: true });
|
|
748
855
|
if (!existsSync(ef))
|
|
749
|
-
|
|
856
|
+
writeConfigFile(ef, "");
|
|
750
857
|
}
|
|
751
858
|
// After writing env files, ensure the runtime user can read them
|
|
752
859
|
try {
|
|
@@ -759,7 +866,7 @@ export async function createInstance(instanceId, name, description = "", cloneFr
|
|
|
759
866
|
}
|
|
760
867
|
catch { /* ignore - same user or no permission to chown */ }
|
|
761
868
|
const configPath = openclawConfigPathInternal(instanceId);
|
|
762
|
-
|
|
869
|
+
ensureDirContainer(dirname(configPath));
|
|
763
870
|
if (cloneFrom && !existsSync(configPath)) {
|
|
764
871
|
const srcConfig = resolveExistingConfigPath(cloneFrom);
|
|
765
872
|
if (existsSync(srcConfig)) {
|
|
@@ -782,12 +889,26 @@ export async function createInstance(instanceId, name, description = "", cloneFr
|
|
|
782
889
|
if (typeof defaultModel === "string" && (defaultModel.startsWith("jsproxy/") || defaultModel.startsWith("js-"))) {
|
|
783
890
|
delete cloned.agents.defaults.model;
|
|
784
891
|
}
|
|
785
|
-
// Strip IM channel configs
|
|
786
|
-
|
|
787
|
-
|
|
892
|
+
// Strip IM channel configs + matching plugin entries — same channel
|
|
893
|
+
// cannot serve multiple instances and we don't want the plugin
|
|
894
|
+
// loader to boot a half-configured binding.
|
|
895
|
+
stripImBindings(cloned);
|
|
788
896
|
// Copy extensions directory so plugin references in config remain valid
|
|
789
897
|
// Copy workspace directory to preserve agent personality (.md files)
|
|
790
|
-
|
|
898
|
+
const subdirs = ["extensions", "workspace"];
|
|
899
|
+
if (cloneOptions?.include_memory !== false) {
|
|
900
|
+
// Memory may exist at .openclaw/memory/ if created by OpenClaw runtime
|
|
901
|
+
const memDir = join(dirname(srcConfig), "memory");
|
|
902
|
+
if (existsSync(memDir))
|
|
903
|
+
subdirs.push("memory");
|
|
904
|
+
}
|
|
905
|
+
if (cloneOptions?.include_sessions) {
|
|
906
|
+
// Sessions at .openclaw/agents/main/sessions/
|
|
907
|
+
const sessDir = join(dirname(srcConfig), "agents");
|
|
908
|
+
if (existsSync(sessDir))
|
|
909
|
+
subdirs.push("agents");
|
|
910
|
+
}
|
|
911
|
+
for (const subdir of subdirs) {
|
|
791
912
|
const srcDir = join(dirname(srcConfig), subdir);
|
|
792
913
|
const dstDir = join(dirname(configPath), subdir);
|
|
793
914
|
if (existsSync(srcDir) && !existsSync(dstDir)) {
|
|
@@ -797,7 +918,7 @@ export async function createInstance(instanceId, name, description = "", cloneFr
|
|
|
797
918
|
catch { /* best effort */ }
|
|
798
919
|
}
|
|
799
920
|
}
|
|
800
|
-
|
|
921
|
+
writeConfigFile(configPath, JSON.stringify(cloned, null, 2));
|
|
801
922
|
// Copy x-jishushell upstream metadata from source instance.json
|
|
802
923
|
// (saveConfig stores x-jishushell in instance.json, not openclaw.json)
|
|
803
924
|
const srcMetaPath = join(instanceDir(cloneFrom), "instance.json");
|
|
@@ -813,7 +934,7 @@ export async function createInstance(instanceId, name, description = "", cloneFr
|
|
|
813
934
|
if (existsSync(metaPath)) {
|
|
814
935
|
const dstMeta = JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
815
936
|
dstMeta["x-jishushell"] = dstXj;
|
|
816
|
-
|
|
937
|
+
writeConfigFile(metaPath, JSON.stringify(dstMeta, null, 2));
|
|
817
938
|
}
|
|
818
939
|
}
|
|
819
940
|
}
|
|
@@ -827,7 +948,7 @@ export async function createInstance(instanceId, name, description = "", cloneFr
|
|
|
827
948
|
}
|
|
828
949
|
}
|
|
829
950
|
if (!existsSync(configPath)) {
|
|
830
|
-
|
|
951
|
+
writeConfigFile(configPath, JSON.stringify(starterConfig(), null, 2));
|
|
831
952
|
// Inject default provider API key from setup into both env files
|
|
832
953
|
const dp = getPanelConfig().default_provider;
|
|
833
954
|
if (dp?.apiKey && dp?.providerId && envFiles.length) {
|
|
@@ -895,11 +1016,28 @@ export function updateInstance(instanceId, name, description) {
|
|
|
895
1016
|
chownToServiceUser(instanceMetaPath(instanceId));
|
|
896
1017
|
return meta;
|
|
897
1018
|
}
|
|
898
|
-
|
|
1019
|
+
/** Update instance.json metadata fields (shallow merge at top level). */
|
|
1020
|
+
export function updateInstanceMeta(instanceId, patch) {
|
|
1021
|
+
const metaPath = instanceMetaPath(instanceId);
|
|
1022
|
+
const meta = safeReadJson(metaPath, "instance-meta") || {};
|
|
1023
|
+
Object.assign(meta, patch);
|
|
1024
|
+
safeWriteJson(metaPath, meta);
|
|
1025
|
+
}
|
|
1026
|
+
export function deleteInstance(instanceId, purgeBackups = false) {
|
|
899
1027
|
const d = instanceDir(instanceId);
|
|
900
1028
|
if (!existsSync(d))
|
|
901
1029
|
return { ok: false, warnings: ["Instance directory not found"] };
|
|
902
1030
|
const warnings = [];
|
|
1031
|
+
// Cancel auto-backup timer and any queued jobs for this instance
|
|
1032
|
+
import("./backup-manager.js").then(({ cancelAutoBackup, getQueueStatus, cancelJob }) => {
|
|
1033
|
+
cancelAutoBackup(instanceId);
|
|
1034
|
+
// Cancel queued (not yet running) jobs for this instance
|
|
1035
|
+
const q = getQueueStatus();
|
|
1036
|
+
for (const job of q.queued) {
|
|
1037
|
+
if (job.instanceId === instanceId)
|
|
1038
|
+
cancelJob(job.id);
|
|
1039
|
+
}
|
|
1040
|
+
}).catch(() => { });
|
|
903
1041
|
// Cache metadata BEFORE deletion so we can check custom openclaw_home after rm
|
|
904
1042
|
const meta = getInstance(instanceId);
|
|
905
1043
|
const home = meta?.openclaw_home;
|
|
@@ -929,6 +1067,19 @@ export function deleteInstance(instanceId) {
|
|
|
929
1067
|
if (home && !home.startsWith(d) && existsSync(home)) {
|
|
930
1068
|
warnings.push(`Custom openclaw_home '${home}' was preserved. Delete manually if no longer needed.`);
|
|
931
1069
|
}
|
|
1070
|
+
// Handle backups (stored in separate directory, not affected by instance rmSync)
|
|
1071
|
+
const backupDir = join(BACKUPS_DIR, instanceId);
|
|
1072
|
+
if (purgeBackups && existsSync(backupDir)) {
|
|
1073
|
+
try {
|
|
1074
|
+
rmSync(backupDir, { recursive: true, force: true });
|
|
1075
|
+
}
|
|
1076
|
+
catch (e) {
|
|
1077
|
+
warnings.push(`Failed to delete backups: ${e.message}`);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
else if (existsSync(backupDir)) {
|
|
1081
|
+
warnings.push(`Backups preserved at ${backupDir}`);
|
|
1082
|
+
}
|
|
932
1083
|
return { ok: dirDeleted, warnings: warnings.length ? warnings : undefined };
|
|
933
1084
|
}
|
|
934
1085
|
export function getConfig(instanceId) {
|
|
@@ -958,13 +1109,7 @@ export function saveConfig(instanceId, config) {
|
|
|
958
1109
|
return false;
|
|
959
1110
|
if (!existsSync(configPath)) {
|
|
960
1111
|
const legacyPath = legacyOpenclawConfigPath(instanceId);
|
|
961
|
-
|
|
962
|
-
// Ensure the state dir is group-writable so the Docker container (which may
|
|
963
|
-
// run as a different UID) can create sub-directories via shared group.
|
|
964
|
-
try {
|
|
965
|
-
chmodSync(dirname(configPath), 0o770);
|
|
966
|
-
}
|
|
967
|
-
catch { /* best effort */ }
|
|
1112
|
+
ensureDirContainer(dirname(configPath));
|
|
968
1113
|
if (existsSync(legacyPath))
|
|
969
1114
|
copyFileSync(legacyPath, configPath);
|
|
970
1115
|
}
|
|
@@ -1000,9 +1145,10 @@ export function saveConfig(instanceId, config) {
|
|
|
1000
1145
|
configToWrite.plugins.entries.feishu = { enabled: false };
|
|
1001
1146
|
}
|
|
1002
1147
|
}
|
|
1003
|
-
// Preserve
|
|
1004
|
-
//
|
|
1005
|
-
//
|
|
1148
|
+
// Preserve backend-managed fields from existing config on disk —
|
|
1149
|
+
// plugins.installs, plugins.entries, and channels written by scan-to-bind
|
|
1150
|
+
// flows (saveWeixinCredentials / saveFeishuCredentials) are not tracked by
|
|
1151
|
+
// the frontend and would be lost on a frontend config save.
|
|
1006
1152
|
if (existsSync(configPath)) {
|
|
1007
1153
|
try {
|
|
1008
1154
|
const existing = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
@@ -1010,6 +1156,28 @@ export function saveConfig(instanceId, config) {
|
|
|
1010
1156
|
configToWrite.plugins ??= {};
|
|
1011
1157
|
configToWrite.plugins.installs = { ...existing.plugins.installs, ...configToWrite.plugins?.installs };
|
|
1012
1158
|
}
|
|
1159
|
+
// Merge plugin entries: for keys present in configToWrite, deep-merge
|
|
1160
|
+
// backend-written sub-fields from disk. Keys absent from configToWrite
|
|
1161
|
+
// (intentionally deleted) are NOT resurrected from existing.
|
|
1162
|
+
if (existing.plugins?.entries && configToWrite.plugins?.entries) {
|
|
1163
|
+
for (const [key, val] of Object.entries(configToWrite.plugins.entries)) {
|
|
1164
|
+
const old = existing.plugins.entries[key];
|
|
1165
|
+
if (val && typeof val === "object" && !Array.isArray(val) && old && typeof old === "object") {
|
|
1166
|
+
configToWrite.plugins.entries[key] = { ...old, ...val };
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
// Merge channels: for keys present in configToWrite, deep-merge
|
|
1171
|
+
// backend-written sub-fields (e.g. openclaw-weixin accounts) from disk.
|
|
1172
|
+
// Keys absent from configToWrite (user-deleted channels) stay deleted.
|
|
1173
|
+
if (existing.channels && configToWrite.channels) {
|
|
1174
|
+
for (const [key, val] of Object.entries(configToWrite.channels)) {
|
|
1175
|
+
const old = existing.channels[key];
|
|
1176
|
+
if (val && typeof val === "object" && !Array.isArray(val) && old && typeof old === "object") {
|
|
1177
|
+
configToWrite.channels[key] = { ...old, ...val };
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1013
1181
|
}
|
|
1014
1182
|
catch { /* best effort */ }
|
|
1015
1183
|
}
|
|
@@ -1018,18 +1186,18 @@ export function saveConfig(instanceId, config) {
|
|
|
1018
1186
|
copyFileSync(configPath, configPath + ".bak");
|
|
1019
1187
|
}
|
|
1020
1188
|
const configJson = JSON.stringify(configToWrite, null, 2);
|
|
1021
|
-
|
|
1022
|
-
|
|
1189
|
+
ensureDirContainer(dirname(configPath));
|
|
1190
|
+
writeConfigFile(configPath + ".tmp", configJson);
|
|
1023
1191
|
// Verify tmp file is valid JSON before replacing (guards against disk-full partial writes)
|
|
1024
1192
|
JSON.parse(readFileSync(configPath + ".tmp", "utf-8"));
|
|
1025
1193
|
renameSync(configPath + ".tmp", configPath);
|
|
1026
1194
|
chownToServiceUser(configPath);
|
|
1027
|
-
// also write to legacy path
|
|
1195
|
+
// also write to legacy path
|
|
1028
1196
|
const legacyPath = legacyOpenclawConfigPath(instanceId);
|
|
1029
1197
|
if (existsSync(legacyPath)) {
|
|
1030
1198
|
copyFileSync(legacyPath, legacyPath + ".bak");
|
|
1031
1199
|
}
|
|
1032
|
-
|
|
1200
|
+
writeConfigFile(legacyPath + ".tmp", configJson);
|
|
1033
1201
|
JSON.parse(readFileSync(legacyPath + ".tmp", "utf-8"));
|
|
1034
1202
|
renameSync(legacyPath + ".tmp", legacyPath);
|
|
1035
1203
|
chownToServiceUser(legacyPath);
|
|
@@ -1081,7 +1249,8 @@ export function saveFeishuCredentials(instanceId, creds) {
|
|
|
1081
1249
|
appId: creds.appId,
|
|
1082
1250
|
appSecret: creds.appSecret,
|
|
1083
1251
|
domain: creds.domain,
|
|
1084
|
-
dmPolicy: "
|
|
1252
|
+
dmPolicy: "open",
|
|
1253
|
+
allowFrom: ["*"],
|
|
1085
1254
|
};
|
|
1086
1255
|
safeWriteJson(configPath, config);
|
|
1087
1256
|
chownToServiceUser(configPath);
|
|
@@ -1099,7 +1268,7 @@ export function saveWeixinCredentials(instanceId, creds) {
|
|
|
1099
1268
|
const home = getOpenclawHomeInternal(instanceId);
|
|
1100
1269
|
const stateDir = join(home, OPENCLAW_STATE_DIRNAME, "openclaw-weixin");
|
|
1101
1270
|
const accountsDir = join(stateDir, "accounts");
|
|
1102
|
-
|
|
1271
|
+
ensureDirContainer(accountsDir);
|
|
1103
1272
|
// Save account credentials file (via safeWriteJson for atomic + .bak protection)
|
|
1104
1273
|
const credObj = {
|
|
1105
1274
|
token: creds.token,
|