create-openclaw-bot 5.8.2 → 5.8.4
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/README.md +2 -2
- package/README.vi.md +2 -2
- package/dist/cli.js +11 -15
- package/dist/server/local-server.js +266 -188
- package/dist/setup/data/header.js +1 -1
- package/dist/setup/data/skills.js +2 -4
- package/dist/setup/shared/bot-config-gen.js +21 -16
- package/dist/setup/shared/workspace-gen.js +307 -475
- package/dist/web/app.js +107 -33
- package/dist/web/styles.css +477 -10
- package/node_modules/color-convert/CHANGELOG.md +54 -0
- package/package.json +1 -1
- package/dist/legacy-cli.js +0 -2812
- package/dist/setup.js +0 -6931
|
@@ -13,7 +13,7 @@ function loadSharedModule(modulePath, globalName) {
|
|
|
13
13
|
if (loaded && Object.keys(loaded).length > 0) return loaded;
|
|
14
14
|
return globalThis[globalName] || loaded || {};
|
|
15
15
|
}
|
|
16
|
-
const { buildWorkspaceFileMap } = loadSharedModule('../setup/shared/workspace-gen.js', '__openclawWorkspace');
|
|
16
|
+
const { buildWorkspaceFileMap, buildCronjobSkillMd, buildInfographicGeneratorSkillMd, buildInfographicGeneratorJs } = loadSharedModule('../setup/shared/workspace-gen.js', '__openclawWorkspace');
|
|
17
17
|
const { buildOpenclawJson, buildEnvFileContent, buildExecApprovalsJson } = loadSharedModule('../setup/shared/bot-config-gen.js', '__openclawBotConfig');
|
|
18
18
|
const { buildDockerArtifacts } = loadSharedModule('../setup/shared/docker-gen.js', '__openclawDockerGen');
|
|
19
19
|
const { OPENCLAW_NPM_SPEC, NINE_ROUTER_NPM_SPEC, build9RouterProviderConfig, get9RouterBaseUrl } = loadSharedModule('../setup/shared/common-gen.js', '__openclawCommon');
|
|
@@ -27,6 +27,7 @@ const STATE_FILE = '.openclaw-setup-state.json';
|
|
|
27
27
|
const DEFAULT_MODEL = 'smart-route';
|
|
28
28
|
const logClients = new Set();
|
|
29
29
|
let zaloLoginInFlight = false;
|
|
30
|
+
let activeServerInstance = null;
|
|
30
31
|
const state = {
|
|
31
32
|
installing: false,
|
|
32
33
|
installed: false,
|
|
@@ -74,6 +75,30 @@ function detectOs() {
|
|
|
74
75
|
return 'linux-desktop';
|
|
75
76
|
}
|
|
76
77
|
|
|
78
|
+
// Blacklist of Windows system/large directories that should never be walked
|
|
79
|
+
const SYSTEM_DIR_BLACKLIST = new Set([
|
|
80
|
+
'windows', 'program files', 'program files (x86)', 'programdata',
|
|
81
|
+
'$recycle.bin', 'system volume information', 'recovery', 'boot',
|
|
82
|
+
'perflogs', 'msocache', 'intel', 'amd', 'nvidia',
|
|
83
|
+
'$windows.~bt', '$windows.~ws', 'config.msi', 'documents and settings',
|
|
84
|
+
'swapfile.sys', 'pagefile.sys', 'hiberfil.sys',
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
/** Discover all available drive letters on Windows (A-Z). Returns ['C:\\', 'D:\\', ...] */
|
|
88
|
+
async function getAvailableDrives() {
|
|
89
|
+
if (process.platform !== 'win32') return ['/'];
|
|
90
|
+
const drives = [];
|
|
91
|
+
for (let code = 65; code <= 90; code++) { // A-Z
|
|
92
|
+
const letter = String.fromCharCode(code);
|
|
93
|
+
const drive = `${letter}:\\`;
|
|
94
|
+
try {
|
|
95
|
+
await fsp.access(drive);
|
|
96
|
+
drives.push(drive);
|
|
97
|
+
} catch {}
|
|
98
|
+
}
|
|
99
|
+
return drives.length ? drives : ['C:\\', 'D:\\'];
|
|
100
|
+
}
|
|
101
|
+
|
|
77
102
|
function recommendedMode(osChoice) {
|
|
78
103
|
if (osChoice === 'win' || osChoice === 'macos') return 'docker';
|
|
79
104
|
return 'native';
|
|
@@ -566,6 +591,26 @@ function ensureZaloApiChannel(cfg, token) {
|
|
|
566
591
|
});
|
|
567
592
|
}
|
|
568
593
|
|
|
594
|
+
function ensureZaloModPluginConfig(entry, cfg) {
|
|
595
|
+
entry.hooks = entry.hooks || {};
|
|
596
|
+
entry.hooks.allowConversationAccess = true;
|
|
597
|
+
entry.config = entry.config || {};
|
|
598
|
+
// Auto-assign dashboardPort = gateway port + 1
|
|
599
|
+
if (!entry.config.dashboardPort) {
|
|
600
|
+
const gwPort = Number(cfg.gateway?.port) || state.gatewayPort || 18789;
|
|
601
|
+
entry.config.dashboardPort = gwPort + 1;
|
|
602
|
+
}
|
|
603
|
+
// Auto-assign botName from first agent name
|
|
604
|
+
if (!entry.config.botName) {
|
|
605
|
+
const agentName = cfg.agents?.list?.[0]?.name;
|
|
606
|
+
if (agentName) entry.config.botName = agentName;
|
|
607
|
+
}
|
|
608
|
+
// Auto-assign zaloDisplayNames from botName
|
|
609
|
+
if ((!entry.config.zaloDisplayNames || entry.config.zaloDisplayNames.length === 0) && entry.config.botName) {
|
|
610
|
+
entry.config.zaloDisplayNames = [entry.config.botName];
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
569
614
|
function readProjectConfig(projectDir) {
|
|
570
615
|
const cfgPath = join(projectDir || '', '.openclaw', 'openclaw.json');
|
|
571
616
|
if (!projectDir || !existsSync(cfgPath)) return null;
|
|
@@ -857,7 +902,32 @@ async function buildBotStatus() {
|
|
|
857
902
|
resolveProjectRuntimeVersions(state.projectDir, state.mode).catch(() => ({ openclaw: '', nineRouter: '', node: process.version || '' })),
|
|
858
903
|
]);
|
|
859
904
|
const credentials = await readBotCredentials(state.projectDir).catch(() => ({ openclawToken: '', nineRouterApiKey: '' }));
|
|
860
|
-
|
|
905
|
+
|
|
906
|
+
let activeModel = 'smart-route';
|
|
907
|
+
let activeProvider = '9Router';
|
|
908
|
+
if (state.projectDir) {
|
|
909
|
+
const cfgPath = join(state.projectDir, '.openclaw', 'openclaw.json');
|
|
910
|
+
if (existsSync(cfgPath)) {
|
|
911
|
+
try {
|
|
912
|
+
const raw = await fsp.readFile(cfgPath, 'utf8');
|
|
913
|
+
const cfg = JSON.parse(raw);
|
|
914
|
+
const modelStr = cfg.agents?.defaults?.model?.primary || cfg.agents?.list?.[0]?.model?.primary || 'smart-route';
|
|
915
|
+
if (modelStr.includes('/')) {
|
|
916
|
+
const parts = modelStr.split('/');
|
|
917
|
+
activeProvider = parts[0];
|
|
918
|
+
activeModel = parts.slice(1).join('/');
|
|
919
|
+
} else {
|
|
920
|
+
activeModel = modelStr;
|
|
921
|
+
activeProvider = cfg.models?.providers?.openai ? 'openai' : '9router';
|
|
922
|
+
}
|
|
923
|
+
} catch (e) {}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
const cap = (s) => String(s).toLowerCase() === 'openai' ? 'OpenAI' : String(s).toLowerCase() === '9router' ? '9Router' : s;
|
|
928
|
+
activeProvider = cap(activeProvider);
|
|
929
|
+
|
|
930
|
+
return { ...state, gatewayStatus, routerStatus, bots, credentials, runtimeVersions, activeModel, activeProvider };
|
|
861
931
|
}
|
|
862
932
|
|
|
863
933
|
async function createBotInProject(projectDir, body = {}, runtime = {}) {
|
|
@@ -889,10 +959,16 @@ async function createBotInProject(projectDir, body = {}, runtime = {}) {
|
|
|
889
959
|
agentMetas: [],
|
|
890
960
|
}));
|
|
891
961
|
|
|
892
|
-
const existingAgentCount = cfg.agents.list.length;
|
|
893
962
|
const used = new Set(cfg.agents.list.map((a) => a.id));
|
|
894
963
|
const botName = uniqueDisplayName(requestedBotName, new Set(cfg.agents.list.map((a) => a.name || a.id)));
|
|
895
|
-
|
|
964
|
+
let agentId = body.agentId ? String(body.agentId).trim().toLowerCase().replace(/[^a-z0-9-_]+/g, '-') : '';
|
|
965
|
+
if (!agentId) {
|
|
966
|
+
agentId = uniqueSlug(slugify(botName), used);
|
|
967
|
+
} else {
|
|
968
|
+
if (used.has(agentId)) {
|
|
969
|
+
throw httpError(400, `Bot ID "${agentId}" đã tồn tại. Vui lòng chọn ID khác.`);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
896
972
|
const workspaceDir = `workspace-${agentId}`;
|
|
897
973
|
const model = cfg.agents.defaults?.model?.primary || cfg.agents.list[0]?.model?.primary || DEFAULT_MODEL;
|
|
898
974
|
cfg.agents.list.push({
|
|
@@ -935,6 +1011,8 @@ async function createBotInProject(projectDir, body = {}, runtime = {}) {
|
|
|
935
1011
|
if (existsSync(cfgPath)) await fsp.copyFile(cfgPath, `${cfgPath}.bak`);
|
|
936
1012
|
await fsp.writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
|
|
937
1013
|
|
|
1014
|
+
const hasScheduler = !!(cfg.tools?.alsoAllow || []).includes('group:automation');
|
|
1015
|
+
const hasImageGen = !!(cfg.skills?.entries?.['image-gen']?.enabled);
|
|
938
1016
|
const files = buildWorkspaceFileMap({
|
|
939
1017
|
isVi: true,
|
|
940
1018
|
botName,
|
|
@@ -945,6 +1023,8 @@ async function createBotInProject(projectDir, body = {}, runtime = {}) {
|
|
|
945
1023
|
agentWorkspaceDir: workspaceDir,
|
|
946
1024
|
workspacePath: `.openclaw/${workspaceDir}`,
|
|
947
1025
|
hasZaloMod: channel === 'zalo-personal',
|
|
1026
|
+
hasScheduler,
|
|
1027
|
+
hasImageGen,
|
|
948
1028
|
});
|
|
949
1029
|
const wsRoot = join(openclawHome, workspaceDir);
|
|
950
1030
|
for (const [name, content] of Object.entries(files)) {
|
|
@@ -1033,6 +1113,8 @@ async function updateBotInProject(projectDir, agentId, body = {}, runtime = {})
|
|
|
1033
1113
|
}
|
|
1034
1114
|
}
|
|
1035
1115
|
|
|
1116
|
+
const hasScheduler = !!(cfg.tools?.alsoAllow || []).includes('group:automation');
|
|
1117
|
+
const hasImageGen = !!(cfg.skills?.entries?.['image-gen']?.enabled);
|
|
1036
1118
|
const files = buildWorkspaceFileMap({
|
|
1037
1119
|
isVi: true,
|
|
1038
1120
|
botName,
|
|
@@ -1043,6 +1125,8 @@ async function updateBotInProject(projectDir, agentId, body = {}, runtime = {})
|
|
|
1043
1125
|
agentWorkspaceDir: workspaceDir,
|
|
1044
1126
|
workspacePath: `.openclaw/${workspaceDir}`,
|
|
1045
1127
|
hasZaloMod: channel === 'zalo-personal',
|
|
1128
|
+
hasScheduler,
|
|
1129
|
+
hasImageGen,
|
|
1046
1130
|
});
|
|
1047
1131
|
const wsRoot = join(projectDir, '.openclaw', workspaceDir);
|
|
1048
1132
|
for (const [name, content] of Object.entries(files)) {
|
|
@@ -1388,6 +1472,23 @@ async function syncDockerInfra(projectDir, force = false) {
|
|
|
1388
1472
|
sendLog(`[sync] Updating Docker infrastructure files (v${existingVersion} \u2192 v${SETUP_VERSION})`);
|
|
1389
1473
|
await fsp.writeFile(join(dockerDir, 'Dockerfile'), docker.dockerfile, 'utf8');
|
|
1390
1474
|
await fsp.writeFile(join(dockerDir, 'docker-compose.yml'), newCompose, 'utf8');
|
|
1475
|
+
// Preserve zalo-mod dashboard port if plugin is active
|
|
1476
|
+
try {
|
|
1477
|
+
const syncCfg = JSON.parse(await fsp.readFile(cfgPath, 'utf8'));
|
|
1478
|
+
const zmEntry = syncCfg.plugins?.entries?.['zalo-mod'] || syncCfg.plugins?.entries?.['openclaw-zalo-mod'];
|
|
1479
|
+
if (zmEntry?.enabled !== false && zmEntry?.config?.dashboardPort) {
|
|
1480
|
+
const dp = zmEntry.config.dashboardPort;
|
|
1481
|
+
let cc = await fsp.readFile(join(dockerDir, 'docker-compose.yml'), 'utf8');
|
|
1482
|
+
if (!cc.includes(`:${dp}`)) {
|
|
1483
|
+
const gpStr = String(gatewayPort);
|
|
1484
|
+
cc = cc.replace(
|
|
1485
|
+
new RegExp(`^(\\s*-\\s*"(?:\\d+:)?${gpStr}(?::${gpStr})?"\\s*)$`, 'm'),
|
|
1486
|
+
`$1\n - "127.0.0.1:${dp}:${dp}" # zalo-mod dashboard`
|
|
1487
|
+
);
|
|
1488
|
+
await fsp.writeFile(join(dockerDir, 'docker-compose.yml'), cc, 'utf8');
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
} catch {}
|
|
1391
1492
|
await fsp.writeFile(entrypointPath, entryScript, 'utf8');
|
|
1392
1493
|
if (docker.syncScript) await fsp.writeFile(join(dockerDir, 'sync.js'), docker.syncScript, 'utf8');
|
|
1393
1494
|
if (docker.patchScript) await fsp.writeFile(join(dockerDir, 'patch-9router.js'), docker.patchScript, 'utf8');
|
|
@@ -1466,10 +1567,9 @@ async function writeCoreProject({ projectDir, osChoice, mode, gatewayPort = 1878
|
|
|
1466
1567
|
await fsp.mkdir(openclawHome, { recursive: true });
|
|
1467
1568
|
await fsp.mkdir(join(openclawHome, 'plugin-runtime-deps'), { recursive: true });
|
|
1468
1569
|
|
|
1469
|
-
const selectedSkills = [];
|
|
1470
|
-
const
|
|
1471
|
-
const
|
|
1472
|
-
const common = { botName, channelKey: 'telegram', providerKey: '9router', model: DEFAULT_MODEL, deployMode: mode, osChoice, selectedSkills, skills: dataExport.SKILLS || [], agentMetas, gatewayPort, routerPort };
|
|
1570
|
+
const selectedSkills = ['memory', 'image-gen', 'web-search'];
|
|
1571
|
+
const agentMetas = [];
|
|
1572
|
+
const common = { channelKey: 'telegram', providerKey: '9router', model: DEFAULT_MODEL, deployMode: mode, osChoice, selectedSkills, skills: dataExport.SKILLS || [], agentMetas, gatewayPort, routerPort };
|
|
1473
1573
|
const cfg = buildOpenclawJson(common);
|
|
1474
1574
|
const env = buildEnvFileContent({ ...common, apiKey: '', botToken: '' });
|
|
1475
1575
|
const approvals = buildExecApprovalsJson({ agentMetas });
|
|
@@ -1478,26 +1578,6 @@ async function writeCoreProject({ projectDir, osChoice, mode, gatewayPort = 1878
|
|
|
1478
1578
|
await fsp.writeFile(join(projectDir, '.env'), env, 'utf8');
|
|
1479
1579
|
await fsp.writeFile(join(openclawHome, 'exec-approvals.json'), JSON.stringify(approvals, null, 2), 'utf8');
|
|
1480
1580
|
|
|
1481
|
-
const workspaceDir = 'workspace-bot';
|
|
1482
|
-
const workspace = buildWorkspaceFileMap({
|
|
1483
|
-
isVi: true,
|
|
1484
|
-
botName,
|
|
1485
|
-
channelKey: 'telegram',
|
|
1486
|
-
providerKey: '9router',
|
|
1487
|
-
selectedSkills,
|
|
1488
|
-
skillsCatalog: dataExport.SKILLS || [],
|
|
1489
|
-
agentMetas,
|
|
1490
|
-
deployMode: mode,
|
|
1491
|
-
osChoice,
|
|
1492
|
-
agentWorkspaceDir: workspaceDir,
|
|
1493
|
-
workspacePath: `.openclaw/${workspaceDir}`,
|
|
1494
|
-
});
|
|
1495
|
-
const wsRoot = join(openclawHome, workspaceDir);
|
|
1496
|
-
for (const [name, content] of Object.entries(workspace)) {
|
|
1497
|
-
await fsp.mkdir(dirname(join(wsRoot, name)), { recursive: true });
|
|
1498
|
-
await fsp.writeFile(join(wsRoot, name), content || '', 'utf8');
|
|
1499
|
-
}
|
|
1500
|
-
|
|
1501
1581
|
if (mode === 'docker') {
|
|
1502
1582
|
const projectName = slugify(basename(projectDir)) || 'bot';
|
|
1503
1583
|
const docker = buildDockerArtifacts({
|
|
@@ -1551,7 +1631,7 @@ async function installCore({ osChoice, mode, projectDir, gatewayPort = 18789, ro
|
|
|
1551
1631
|
await fsp.mkdir(dockerDir, { recursive: true });
|
|
1552
1632
|
const envContent = existsSync(rootEnvPath)
|
|
1553
1633
|
? await fsp.readFile(rootEnvPath, 'utf8')
|
|
1554
|
-
: buildEnvFileContent({
|
|
1634
|
+
: buildEnvFileContent({ channelKey: 'telegram', providerKey: '9router', deployMode: mode, osChoice, selectedSkills: [], skills: dataExport.SKILLS || [], agentMetas: [], apiKey: '', botToken: '' });
|
|
1555
1635
|
await fsp.writeFile(dockerEnvPath, envContent, 'utf8');
|
|
1556
1636
|
sendLog(`Docker env ready: ${dockerEnvPath}`);
|
|
1557
1637
|
await run('docker', ['compose', 'up', '-d', '--build'], { cwd: dockerDir });
|
|
@@ -1678,11 +1758,16 @@ async function findLatestProject(rootProjectDir) {
|
|
|
1678
1758
|
join(rootProjectDir, DEFAULT_PROJECT_NAME),
|
|
1679
1759
|
dirname(rootProjectDir),
|
|
1680
1760
|
os.homedir(),
|
|
1681
|
-
'D:\\tmp',
|
|
1682
1761
|
];
|
|
1683
|
-
|
|
1762
|
+
// Scan all available drives, walking top-level dirs but skipping system folders
|
|
1763
|
+
const drives = await getAvailableDrives();
|
|
1764
|
+
for (const drive of drives) {
|
|
1684
1765
|
const entries = await fsp.readdir(drive, { withFileTypes: true }).catch(() => []);
|
|
1685
|
-
for (const e of entries)
|
|
1766
|
+
for (const e of entries) {
|
|
1767
|
+
if (e.isDirectory() && !e.name.startsWith('$') && !SYSTEM_DIR_BLACKLIST.has(e.name.toLowerCase())) {
|
|
1768
|
+
roots.push(join(drive, e.name));
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1686
1771
|
}
|
|
1687
1772
|
const candidates = [];
|
|
1688
1773
|
async function walk(dir, depth = 0) {
|
|
@@ -1693,7 +1778,11 @@ async function findLatestProject(rootProjectDir) {
|
|
|
1693
1778
|
return;
|
|
1694
1779
|
}
|
|
1695
1780
|
const entries = await fsp.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
1696
|
-
for (const e of entries)
|
|
1781
|
+
for (const e of entries) {
|
|
1782
|
+
if (e.isDirectory() && !e.name.startsWith('.') && e.name !== 'node_modules' && !SYSTEM_DIR_BLACKLIST.has(e.name.toLowerCase())) {
|
|
1783
|
+
await walk(join(dir, e.name), depth + 1);
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1697
1786
|
}
|
|
1698
1787
|
for (const r of roots) await walk(r);
|
|
1699
1788
|
candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
@@ -1706,10 +1795,10 @@ async function discoverProjects(rootProjectDir) {
|
|
|
1706
1795
|
rootProjectDir,
|
|
1707
1796
|
dirname(rootProjectDir),
|
|
1708
1797
|
process.env.OPENCLAW_HOME ? dirname(process.env.OPENCLAW_HOME) : '',
|
|
1709
|
-
'D:\\tmp',
|
|
1710
|
-
'D:\\',
|
|
1711
|
-
'E:\\',
|
|
1712
1798
|
];
|
|
1799
|
+
// Add all available drives for scanning
|
|
1800
|
+
const drives = await getAvailableDrives();
|
|
1801
|
+
for (const drive of drives) roots.push(drive);
|
|
1713
1802
|
const seen = new Set();
|
|
1714
1803
|
const hits = [];
|
|
1715
1804
|
async function walk(dir, depth = 0) {
|
|
@@ -1743,7 +1832,7 @@ async function discoverProjects(rootProjectDir) {
|
|
|
1743
1832
|
const entries = await fsp.readdir(full, { withFileTypes: true }).catch(() => []);
|
|
1744
1833
|
for (const e of entries) {
|
|
1745
1834
|
if (!e.isDirectory()) continue;
|
|
1746
|
-
if (e.name === 'node_modules' || e.name.startsWith('.git')) continue;
|
|
1835
|
+
if (e.name === 'node_modules' || e.name.startsWith('.git') || SYSTEM_DIR_BLACKLIST.has(e.name.toLowerCase())) continue;
|
|
1747
1836
|
await walk(join(full, e.name), depth + 1);
|
|
1748
1837
|
}
|
|
1749
1838
|
}
|
|
@@ -1897,72 +1986,23 @@ async function applyFeatureToggle(projectDir, agentId, kind, id, enabled) {
|
|
|
1897
1986
|
const k = `${kind}:${id}`;
|
|
1898
1987
|
|
|
1899
1988
|
if (kind === 'skill' && id === 'browser') {
|
|
1989
|
+
delete cfg.browser;
|
|
1990
|
+
cfg.plugins = cfg.plugins || { entries: {} };
|
|
1991
|
+
cfg.plugins.entries = cfg.plugins.entries || {};
|
|
1992
|
+
const aliases = ['browser-automation', 'openclaw-browser-automation'];
|
|
1993
|
+
const existingKey = aliases.find((a) => cfg.plugins.entries[a]) || aliases[0];
|
|
1994
|
+
cfg.plugins.entries[existingKey] = cfg.plugins.entries[existingKey] || {};
|
|
1995
|
+
cfg.plugins.entries[existingKey].enabled = !!enabled;
|
|
1996
|
+
cfg.plugins.allow = cfg.plugins.allow || [];
|
|
1900
1997
|
if (enabled) {
|
|
1901
|
-
cfg.
|
|
1902
|
-
enabled: true,
|
|
1903
|
-
defaultProfile: 'host-chrome',
|
|
1904
|
-
profiles: { 'host-chrome': { cdpUrl: 'http://127.0.0.1:9222', color: '#4285F4' } },
|
|
1905
|
-
};
|
|
1906
|
-
const isHeadlessServer = process.platform === 'linux';
|
|
1907
|
-
const docVariant = 'cli-server';
|
|
1908
|
-
|
|
1909
|
-
for (const a of cfg.agents.list) {
|
|
1910
|
-
const wm = buildWorkspaceFileMap({
|
|
1911
|
-
isVi: true,
|
|
1912
|
-
botName: a.name || a.id,
|
|
1913
|
-
botDesc: '',
|
|
1914
|
-
hasBrowser: false,
|
|
1915
|
-
hasScheduler: true,
|
|
1916
|
-
workspacePath: `.openclaw/${workspaceRelForAgent(a, cfg, projectDir)}/`,
|
|
1917
|
-
agentWorkspaceDir: workspaceRelForAgent(a, cfg, projectDir),
|
|
1918
|
-
variant: cfg.agents.list.length > 1 ? 'relay' : 'single',
|
|
1919
|
-
browserDocVariant: docVariant,
|
|
1920
|
-
});
|
|
1921
|
-
const browserDoc = wm['BROWSER.md'] || '# BROWSER';
|
|
1922
|
-
const browserTool = wm['browser-tool.js'] || '';
|
|
1923
|
-
const bf = await readWorkspaceText(projectDir, a, 'BROWSER.md');
|
|
1924
|
-
await fsp.writeFile(bf.file, browserDoc, 'utf8');
|
|
1925
|
-
const bt = await readWorkspaceText(projectDir, a, 'browser-tool.js');
|
|
1926
|
-
if (browserTool) await fsp.writeFile(bt.file, browserTool, 'utf8');
|
|
1927
|
-
|
|
1928
|
-
const af = await readWorkspaceText(projectDir, a, 'AGENTS.md');
|
|
1929
|
-
const agentsManaged = upsertManagedBlock(af.content, 'BROWSER_LINK', '- Browser docs: `BROWSER.md`');
|
|
1930
|
-
await fsp.writeFile(af.file, agentsManaged, 'utf8');
|
|
1931
|
-
|
|
1932
|
-
// Add to TOOLS.md
|
|
1933
|
-
const tf = await readWorkspaceText(projectDir, a, 'TOOLS.md');
|
|
1934
|
-
const browserGuide = isHeadlessServer
|
|
1935
|
-
? `## 🌐 Browser Automation
|
|
1936
|
-
- Xem hướng dẫn chi tiết tại **BROWSER.md**
|
|
1937
|
-
- Script điều khiển: \`browser-tool.js\`
|
|
1938
|
-
- Chế độ hiện tại: Chạy ngầm độc lập qua Docker hoặc Xvfb trên VPS.
|
|
1939
|
-
- **Tìm kiếm Web:** Nếu không có công cụ Web Search (hoặc Web Search không khả dụng/bị lỗi), hãy **luôn sử dụng ngay công cụ terminal (exec/run_command) để chạy lệnh: \`node search-tool.js "<từ khóa>" 5\`**! Lệnh này sẽ tự động chạy ngầm qua DuckDuckGo/Google/Bing bằng trình duyệt ngầm tàng hình của bạn và trả về kết quả JSON sạch ngay lập tức. Tuyệt đối KHÔNG được mở trình duyệt thủ công, chụp ảnh màn hình hay click tìm kiếm bằng tay từng bước!
|
|
1940
|
-
- Nếu browser lỗi, thử lại 1 lần rồi mới báo user với lỗi cụ thể`
|
|
1941
|
-
: `## 🌐 Browser Automation
|
|
1942
|
-
- Xem hướng dẫn chi tiết tại **BROWSER.md**
|
|
1943
|
-
- Script điều khiển: \`browser-tool.js\`
|
|
1944
|
-
- Chế độ hiện tại:
|
|
1945
|
-
- **Mặc định:** Chạy ngầm độc lập qua Docker hoặc Server.
|
|
1946
|
-
- **Chế độ xem Chrome thật:** Chạy file \`start-chrome-debug.bat\` / \`start-chrome-debug.sh\` trên host trước để bot kết nối điều khiển trực quan.
|
|
1947
|
-
- Kết nối mặc định: \`http://127.0.0.1:9222\`
|
|
1948
|
-
- **Tìm kiếm Web:** Nếu không có công cụ Web Search (hoặc Web Search không khả dụng/bị lỗi), hãy **luôn sử dụng ngay công cụ terminal (exec/run_command) để chạy lệnh: \`node search-tool.js "<từ khóa>" 5\`**! Lệnh này sẽ tự động chạy ngầm qua DuckDuckGo/Google/Bing bằng trình duyệt ngầm tàng hình của bạn và trả về kết quả JSON sạch ngay lập tức. Tuyệt đối KHÔNG được mở trình duyệt thủ công, chụp ảnh màn hình hay click tìm kiếm bằng tay từng bước!
|
|
1949
|
-
- Nếu browser lỗi, thử lại 1 lần rồi mới báo user với lỗi cụ thể`;
|
|
1950
|
-
await fsp.writeFile(tf.file, upsertManagedBlock(tf.content, 'BROWSER_GUIDE', browserGuide), 'utf8');
|
|
1951
|
-
}
|
|
1998
|
+
if (!cfg.plugins.allow.includes(existingKey)) cfg.plugins.allow.push(existingKey);
|
|
1952
1999
|
} else {
|
|
1953
|
-
|
|
2000
|
+
cfg.plugins.allow = cfg.plugins.allow.filter((x) => x !== existingKey);
|
|
1954
2001
|
for (const a of cfg.agents.list) {
|
|
1955
2002
|
const bf = await readWorkspaceText(projectDir, a, 'BROWSER.md');
|
|
1956
2003
|
if (existsSync(bf.file)) await fsp.rm(bf.file, { force: true });
|
|
1957
2004
|
const bt = await readWorkspaceText(projectDir, a, 'browser-tool.js');
|
|
1958
2005
|
if (existsSync(bt.file)) await fsp.rm(bt.file, { force: true });
|
|
1959
|
-
|
|
1960
|
-
const af = await readWorkspaceText(projectDir, a, 'AGENTS.md');
|
|
1961
|
-
await fsp.writeFile(af.file, removeManagedBlock(af.content, 'BROWSER_LINK'), 'utf8');
|
|
1962
|
-
|
|
1963
|
-
// Remove from TOOLS.md
|
|
1964
|
-
const tf = await readWorkspaceText(projectDir, a, 'TOOLS.md');
|
|
1965
|
-
await fsp.writeFile(tf.file, removeManagedBlock(tf.content, 'BROWSER_GUIDE'), 'utf8');
|
|
1966
2006
|
}
|
|
1967
2007
|
}
|
|
1968
2008
|
|
|
@@ -1985,32 +2025,17 @@ async function applyFeatureToggle(projectDir, agentId, kind, id, enabled) {
|
|
|
1985
2025
|
cfg.tools.alsoAllow = Array.from(new Set([...(cfg.tools.alsoAllow || []), 'group:automation']));
|
|
1986
2026
|
cfg.commands = cfg.commands || {};
|
|
1987
2027
|
cfg.commands.ownerAllowFrom = Array.from(new Set([...(cfg.commands.ownerAllowFrom || []), '*']));
|
|
1988
|
-
const cronGuide = `## ⏰ Cron / Lên lịch nhắc nhở (tool: \`cron\`)
|
|
1989
|
-
- **Tên tool chính xác:** Tên công cụ là \`cron\` (tuyệt đối không nhầm là \`native\` hay command line bên ngoài).
|
|
1990
|
-
- **Khi tạo cronjob mới (action \`add\`):**
|
|
1991
|
-
- **TUYỆT ĐỐI KHÔNG điền trường \`agentId\`** trong object \`job\` (hãy bỏ qua/omitted trường này). Hệ thống OpenClaw sẽ tự động gán chính xác ID của bạn vào job đó.
|
|
1992
|
-
- Tuyệt đối **không tự điền** \`agentId\` là \`"bot"\` hay \`"main"\`, vì làm vậy sẽ khiến cronjob thuộc về agent khác và bạn sẽ mất quyền kiểm soát/xóa nó sau này.
|
|
1993
|
-
- **Khi user yêu cầu tắt/bật/xóa cronjob:**
|
|
1994
|
-
1. **Bước 1 (Tìm kiếm):** Gọi tool \`cron\` với action \`list\` (và \`includeDisabled: true\`) để xem danh sách tất cả cronjob đang chạy trên hệ thống và tìm đúng \`jobId\` phù hợp với yêu cầu.
|
|
1995
|
-
2. **Bước 2 (Xử lý):**
|
|
1996
|
-
- Để xóa: Gọi action \`remove\` với \`id\` tìm được.
|
|
1997
|
-
- Để tắt/tạm dừng: Gọi action \`update\` với \`id\` và patch \`{"enabled": false}\`.
|
|
1998
|
-
- Để bật lại: Gọi action \`update\` với \`id\` và patch \`{"enabled": true}\`.
|
|
1999
|
-
3. **Tuyên bố trung thực:** Tuyệt đối không bao giờ trả lời "đã xóa" hay "không có" dựa trên suy đoán của bản thân mà chưa gọi tool \`cron\` để kiểm tra thực tế.
|
|
2000
|
-
- Khi user yêu cầu tạo nhắc nhở / lệnh tự động định kỳ, bạn hãy TỰ ĐỘNG dùng tool \`cron\` (action \`add\`) để tạo. **Tuyệt đối không** bắt user dùng crontab hay Task Scheduler chạy tay trên host.
|
|
2001
|
-
- Khi thao tác tool cho cron/scheduler, **không điền \`current\` vào thư mục Session**.
|
|
2002
|
-
- **QUAN TRỌNG VỀ TARGETING GROUP CHAT**: Khi tạo hoặc cấu hình cron job gửi tin nhắn thông báo (announce mode) đến một Group Chat, giá trị của trường \`delivery.to\` **bắt buộc** phải sử dụng tiền tố \`group:\` trước ID của group (ví dụ: \`group:3815464776067464419\` hoặc \`group:xxxx\`). Tuyệt đối không được chỉ điền ID thuần túy vì hệ thống sẽ hiểu nhầm đó là một DM chat cá nhân (direct message) và gửi sai địa chỉ.
|
|
2003
|
-
- Bỏ qua việc tra cứu docs nội bộ như \`cron-jobs.mdx\`; tin tưởng khả năng dùng tool hiện có để hoàn thành yêu cầu.`;
|
|
2004
2028
|
for (const a of cfg.agents.list) {
|
|
2005
|
-
const
|
|
2006
|
-
await fsp.
|
|
2029
|
+
const sf = await readWorkspaceText(projectDir, a, 'skills/cronjob/SKILL.md');
|
|
2030
|
+
await fsp.mkdir(dirname(sf.file), { recursive: true });
|
|
2031
|
+
await fsp.writeFile(sf.file, buildCronjobSkillMd(true), 'utf8');
|
|
2007
2032
|
}
|
|
2008
2033
|
} else {
|
|
2009
2034
|
if (cfg.tools?.alsoAllow) cfg.tools.alsoAllow = cfg.tools.alsoAllow.filter((x) => x !== 'group:automation');
|
|
2010
2035
|
if (cfg.commands?.ownerAllowFrom) cfg.commands.ownerAllowFrom = cfg.commands.ownerAllowFrom.filter((x) => x !== '*');
|
|
2011
2036
|
for (const a of cfg.agents.list) {
|
|
2012
|
-
const
|
|
2013
|
-
await fsp.
|
|
2037
|
+
const sf = await readWorkspaceText(projectDir, a, 'skills/cronjob/SKILL.md');
|
|
2038
|
+
if (existsSync(sf.file)) await fsp.rm(sf.file, { force: true });
|
|
2014
2039
|
}
|
|
2015
2040
|
}
|
|
2016
2041
|
|
|
@@ -2025,6 +2050,59 @@ async function applyFeatureToggle(projectDir, agentId, kind, id, enabled) {
|
|
|
2025
2050
|
}
|
|
2026
2051
|
}
|
|
2027
2052
|
|
|
2053
|
+
if (kind === 'skill' && id === 'image-gen') {
|
|
2054
|
+
cfg.skills = cfg.skills || { entries: {} };
|
|
2055
|
+
cfg.skills.entries = cfg.skills.entries || {};
|
|
2056
|
+
cfg.skills.entries['image-gen'] = cfg.skills.entries['image-gen'] || {};
|
|
2057
|
+
cfg.skills.entries['image-gen'].enabled = !!enabled;
|
|
2058
|
+
|
|
2059
|
+
for (const a of cfg.agents.list) {
|
|
2060
|
+
const sf = await readWorkspaceText(projectDir, a, 'skills/infographic-generator/SKILL.md');
|
|
2061
|
+
const js = await readWorkspaceText(projectDir, a, 'skills/infographic-generator/image-generator.js');
|
|
2062
|
+
if (enabled) {
|
|
2063
|
+
await fsp.mkdir(dirname(sf.file), { recursive: true });
|
|
2064
|
+
await fsp.writeFile(sf.file, buildInfographicGeneratorSkillMd(), 'utf8');
|
|
2065
|
+
await fsp.writeFile(js.file, buildInfographicGeneratorJs(), 'utf8');
|
|
2066
|
+
} else {
|
|
2067
|
+
if (existsSync(sf.file)) await fsp.rm(sf.file, { force: true });
|
|
2068
|
+
if (existsSync(js.file)) await fsp.rm(js.file, { force: true });
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
// Write cfgPath early so recreation reads updated openclaw.json
|
|
2073
|
+
await fsp.writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
|
|
2074
|
+
|
|
2075
|
+
// Recreate container to apply updated openclaw.json
|
|
2076
|
+
const hasDocker = existsSync(join(projectDir, 'docker', 'openclaw', 'docker-compose.yml'));
|
|
2077
|
+
if (hasDocker) {
|
|
2078
|
+
sendLog(`[docker] Infographic skill toggled to ${enabled}. Recreating containers...`);
|
|
2079
|
+
await recreateDockerBot(projectDir).catch((err) => sendLog(`[docker] Warning: Failed to recreate container: ${err.message}`));
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
if (kind === 'skill' && id === 'web-search') {
|
|
2084
|
+
cfg.plugins = cfg.plugins || { entries: {} };
|
|
2085
|
+
cfg.plugins.entries = cfg.plugins.entries || {};
|
|
2086
|
+
cfg.plugins.entries['duckduckgo'] = cfg.plugins.entries['duckduckgo'] || {};
|
|
2087
|
+
cfg.plugins.entries['duckduckgo'].enabled = !!enabled;
|
|
2088
|
+
cfg.plugins.allow = cfg.plugins.allow || [];
|
|
2089
|
+
if (enabled) {
|
|
2090
|
+
if (!cfg.plugins.allow.includes('duckduckgo')) cfg.plugins.allow.push('duckduckgo');
|
|
2091
|
+
} else {
|
|
2092
|
+
cfg.plugins.allow = cfg.plugins.allow.filter((x) => x !== 'duckduckgo');
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
// Write cfgPath early so recreation reads updated openclaw.json
|
|
2096
|
+
await fsp.writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
|
|
2097
|
+
|
|
2098
|
+
// Recreate container to apply updated openclaw.json
|
|
2099
|
+
const hasDocker = existsSync(join(projectDir, 'docker', 'openclaw', 'docker-compose.yml'));
|
|
2100
|
+
if (hasDocker) {
|
|
2101
|
+
sendLog(`[docker] Web Search skill toggled to ${enabled}. Recreating containers...`);
|
|
2102
|
+
await recreateDockerBot(projectDir).catch((err) => sendLog(`[docker] Warning: Failed to recreate container: ${err.message}`));
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2028
2106
|
if (kind === 'plugin') {
|
|
2029
2107
|
cfg.plugins = cfg.plugins || { entries: {} };
|
|
2030
2108
|
cfg.plugins.entries = cfg.plugins.entries || {};
|
|
@@ -2038,13 +2116,31 @@ async function applyFeatureToggle(projectDir, agentId, kind, id, enabled) {
|
|
|
2038
2116
|
const existingKey = aliases.find((a) => cfg.plugins.entries[a]) || aliases[0];
|
|
2039
2117
|
cfg.plugins.entries[existingKey] = cfg.plugins.entries[existingKey] || {};
|
|
2040
2118
|
cfg.plugins.entries[existingKey].enabled = !!enabled;
|
|
2041
|
-
if (existingKey === 'zalo-mod') {
|
|
2042
|
-
cfg.plugins.entries[existingKey]
|
|
2043
|
-
cfg.plugins.entries[existingKey].hooks.allowConversationAccess = true;
|
|
2119
|
+
if (existingKey === 'zalo-mod' || existingKey === 'openclaw-zalo-mod') {
|
|
2120
|
+
ensureZaloModPluginConfig(cfg.plugins.entries[existingKey], cfg);
|
|
2044
2121
|
}
|
|
2045
2122
|
// Only add the canonical config key to allow list (not all aliases)
|
|
2046
2123
|
cfg.plugins.allow = cfg.plugins.allow || [];
|
|
2047
2124
|
if (!cfg.plugins.allow.includes(existingKey)) cfg.plugins.allow.push(existingKey);
|
|
2125
|
+
// Auto-expose zalo-mod dashboard port in docker-compose.yml when enabling
|
|
2126
|
+
if (enabled && (existingKey === 'zalo-mod' || existingKey === 'openclaw-zalo-mod')) {
|
|
2127
|
+
const composeFile = join(projectDir, 'docker', 'openclaw', 'docker-compose.yml');
|
|
2128
|
+
if (existsSync(composeFile)) {
|
|
2129
|
+
try {
|
|
2130
|
+
let composeContent = await fsp.readFile(composeFile, 'utf8');
|
|
2131
|
+
const dashPort = cfg.plugins.entries[existingKey].config?.dashboardPort;
|
|
2132
|
+
if (dashPort && !composeContent.includes(`:${dashPort}`)) {
|
|
2133
|
+
const gwPortStr = String(Number(cfg.gateway?.port) || state.gatewayPort || 18789);
|
|
2134
|
+
composeContent = composeContent.replace(
|
|
2135
|
+
new RegExp(`^(\\s*-\\s*"(?:\\d+:)?${gwPortStr}(?::${gwPortStr})?"\\s*)$`, 'm'),
|
|
2136
|
+
`$1\n - "127.0.0.1:${dashPort}:${dashPort}" # zalo-mod dashboard`
|
|
2137
|
+
);
|
|
2138
|
+
await fsp.writeFile(composeFile, composeContent, 'utf8');
|
|
2139
|
+
sendLog(`[plugin] Added dashboard port ${dashPort} to docker-compose.yml`);
|
|
2140
|
+
}
|
|
2141
|
+
} catch (e) { sendLog(`[plugin] Warning: could not add dashboard port: ${e.message}`); }
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2048
2144
|
}
|
|
2049
2145
|
|
|
2050
2146
|
await fsp.writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
|
|
@@ -2062,8 +2158,15 @@ async function installFeature(projectDir, agentId, kind, id) {
|
|
|
2062
2158
|
|
|
2063
2159
|
if (composeDir) {
|
|
2064
2160
|
const botContainer = getBotContainerName(projectDir);
|
|
2161
|
+
sendLog(`[plugin] Installing/updating clawhub:${id} inside container ${botContainer}...`);
|
|
2065
2162
|
|
|
2066
|
-
|
|
2163
|
+
const cmd = `cd /root/project && openclaw plugins install clawhub:${id} --force`;
|
|
2164
|
+
const cmdOut = await runCapture('docker', ['exec', botContainer, 'sh', '-lc', cmd], { cwd: projectDir, shell: false });
|
|
2165
|
+
|
|
2166
|
+
if (cmdOut) {
|
|
2167
|
+
for (const line of `${cmdOut.stdout}\n${cmdOut.stderr}`.split(/\r?\n/).filter(Boolean)) sendLog(line);
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2067
2170
|
const cfgPath = join(projectDir, '.openclaw', 'openclaw.json');
|
|
2068
2171
|
const pluginAliasMap = {
|
|
2069
2172
|
'openclaw-browser-automation': ['browser-automation', 'openclaw-browser-automation'],
|
|
@@ -2072,53 +2175,12 @@ async function installFeature(projectDir, agentId, kind, id) {
|
|
|
2072
2175
|
'openclaw-n8n-facebook-poster': ['openclaw-n8n-facebook-poster', 'openclaw-facebook-poster', 'facebook-poster'],
|
|
2073
2176
|
};
|
|
2074
2177
|
const aliases = pluginAliasMap[id] || [id];
|
|
2075
|
-
|
|
2076
|
-
if (existsSync(cfgPath)) {
|
|
2077
|
-
try {
|
|
2078
|
-
const cfg = ensureConfigShape(JSON.parse(await fsp.readFile(cfgPath, 'utf8')));
|
|
2079
|
-
cfg.plugins = cfg.plugins || { entries: {} };
|
|
2080
|
-
cfg.plugins.entries = cfg.plugins.entries || {};
|
|
2081
|
-
const existingKey = aliases.find((a) => cfg.plugins.entries[a]) || aliases[0];
|
|
2082
|
-
|
|
2083
|
-
if (cfg.plugins.entries[existingKey]?.enabled) {
|
|
2084
|
-
sendLog(`[plugin] Temporarily disabling ${existingKey} and restarting bot to release file locks...`);
|
|
2085
|
-
cfg.plugins.entries[existingKey].enabled = false;
|
|
2086
|
-
await fsp.writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
|
|
2087
|
-
await run('docker', ['restart', botContainer], { shell: false }).catch(() => {});
|
|
2088
|
-
// Sleep 2 seconds to let container fully boot and release locks
|
|
2089
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2090
|
-
}
|
|
2091
|
-
} catch (_) {}
|
|
2092
|
-
}
|
|
2093
|
-
|
|
2094
|
-
sendLog(`[plugin] Installing clawhub:${id} inside container ${botContainer}...`);
|
|
2095
|
-
|
|
2096
|
-
let installSuccess = true;
|
|
2097
|
-
const cleanCmd = `cd /root/project && (openclaw plugins uninstall ${id} --force 2>/dev/null || true) && (openclaw plugins uninstall ${id.replace('openclaw-', '')} --force 2>/dev/null || true) && rm -rf .openclaw/extensions/${id} .openclaw/extensions/${id.replace('openclaw-', '')} && openclaw plugins install clawhub:${id}`;
|
|
2098
|
-
const cmdOut = await runCapture('docker', ['exec', botContainer, 'sh', '-lc', cleanCmd], { cwd: projectDir, shell: false });
|
|
2099
|
-
|
|
2100
|
-
if (cmdOut) {
|
|
2101
|
-
for (const line of `${cmdOut.stdout}\n${cmdOut.stderr}`.split(/\r?\n/).filter(Boolean)) sendLog(line);
|
|
2102
|
-
}
|
|
2103
2178
|
|
|
2104
2179
|
if (cmdOut.code !== 0) {
|
|
2105
2180
|
const folderExists = aliases.some((a) => existsSync(join(projectDir, '.openclaw', 'extensions', a)));
|
|
2106
2181
|
if (folderExists) {
|
|
2107
2182
|
sendLog(`[plugin] Warning: installation reported errors, but plugin folder successfully written. Proceeding.`);
|
|
2108
2183
|
} else {
|
|
2109
|
-
installSuccess = false;
|
|
2110
|
-
// Re-enable in config on failure to restore state
|
|
2111
|
-
if (existsSync(cfgPath)) {
|
|
2112
|
-
try {
|
|
2113
|
-
const cfg = ensureConfigShape(JSON.parse(await fsp.readFile(cfgPath, 'utf8')));
|
|
2114
|
-
cfg.plugins = cfg.plugins || { entries: {} };
|
|
2115
|
-
cfg.plugins.entries = cfg.plugins.entries || {};
|
|
2116
|
-
const existingKey = aliases.find((a) => cfg.plugins.entries[a]) || aliases[0];
|
|
2117
|
-
cfg.plugins.entries[existingKey] = cfg.plugins.entries[existingKey] || {};
|
|
2118
|
-
cfg.plugins.entries[existingKey].enabled = true;
|
|
2119
|
-
await fsp.writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
|
|
2120
|
-
} catch (_) {}
|
|
2121
|
-
}
|
|
2122
2184
|
throw new Error(cmdOut.stderr || cmdOut.stdout || `Failed to install plugin ${id} inside container.`);
|
|
2123
2185
|
}
|
|
2124
2186
|
}
|
|
@@ -2127,25 +2189,11 @@ async function installFeature(projectDir, agentId, kind, id) {
|
|
|
2127
2189
|
const cfg = ensureConfigShape(JSON.parse(await fsp.readFile(cfgPath, 'utf8')));
|
|
2128
2190
|
cfg.plugins = cfg.plugins || { entries: {} };
|
|
2129
2191
|
cfg.plugins.entries = cfg.plugins.entries || {};
|
|
2130
|
-
const pluginAliasMap = {
|
|
2131
|
-
'openclaw-browser-automation': ['browser-automation', 'openclaw-browser-automation'],
|
|
2132
|
-
'openclaw-zalo-mod': ['zalo-mod', 'openclaw-zalo-mod'],
|
|
2133
|
-
'openclaw-facebook-crawler': ['openclaw-facebook-crawler', 'openclaw-n8n-facebook-crawler', 'n8n-facebook-crawler'],
|
|
2134
|
-
'openclaw-n8n-facebook-poster': ['openclaw-n8n-facebook-poster', 'openclaw-facebook-poster', 'facebook-poster'],
|
|
2135
|
-
};
|
|
2136
|
-
const aliases = pluginAliasMap[id] || [id];
|
|
2137
2192
|
const existingKey = aliases.find((a) => cfg.plugins.entries[a]) || aliases[0];
|
|
2138
2193
|
cfg.plugins.entries[existingKey] = cfg.plugins.entries[existingKey] || {};
|
|
2139
2194
|
cfg.plugins.entries[existingKey].enabled = true;
|
|
2140
|
-
if (existingKey === 'zalo-mod') {
|
|
2141
|
-
cfg.plugins.entries[existingKey]
|
|
2142
|
-
cfg.plugins.entries[existingKey].hooks.allowConversationAccess = true;
|
|
2143
|
-
// Auto-assign dashboard port = gateway port + 1 to avoid conflicts between bots
|
|
2144
|
-
const gwPort = Number(cfg.gateway?.port) || state.gatewayPort || 18789;
|
|
2145
|
-
cfg.plugins.entries[existingKey].config = cfg.plugins.entries[existingKey].config || {};
|
|
2146
|
-
if (!cfg.plugins.entries[existingKey].config.dashboardPort) {
|
|
2147
|
-
cfg.plugins.entries[existingKey].config.dashboardPort = gwPort + 1;
|
|
2148
|
-
}
|
|
2195
|
+
if (existingKey === 'zalo-mod' || existingKey === 'openclaw-zalo-mod') {
|
|
2196
|
+
ensureZaloModPluginConfig(cfg.plugins.entries[existingKey], cfg);
|
|
2149
2197
|
}
|
|
2150
2198
|
// Only add the canonical config key to allow list (not all aliases)
|
|
2151
2199
|
if (!cfg.plugins.allow.includes(existingKey)) cfg.plugins.allow.push(existingKey);
|
|
@@ -2199,7 +2247,7 @@ async function installFeature(projectDir, agentId, kind, id) {
|
|
|
2199
2247
|
sendLog(`[plugin] Installing clawhub:${id}...`);
|
|
2200
2248
|
|
|
2201
2249
|
let installSuccess = true;
|
|
2202
|
-
await run('openclaw', ['plugins', 'install', `clawhub:${id}
|
|
2250
|
+
await run('openclaw', ['plugins', 'install', `clawhub:${id}`, '--force'], {
|
|
2203
2251
|
cwd: projectDir,
|
|
2204
2252
|
env: openclawProjectEnv(projectDir),
|
|
2205
2253
|
resolveOnPattern: /Installed plugin:/
|
|
@@ -2230,15 +2278,8 @@ async function installFeature(projectDir, agentId, kind, id) {
|
|
|
2230
2278
|
const existingKey = aliases.find((a) => cfg.plugins.entries[a]) || aliases[0];
|
|
2231
2279
|
cfg.plugins.entries[existingKey] = cfg.plugins.entries[existingKey] || {};
|
|
2232
2280
|
cfg.plugins.entries[existingKey].enabled = true;
|
|
2233
|
-
if (existingKey === 'zalo-mod') {
|
|
2234
|
-
cfg.plugins.entries[existingKey]
|
|
2235
|
-
cfg.plugins.entries[existingKey].hooks.allowConversationAccess = true;
|
|
2236
|
-
// Auto-assign dashboard port = gateway port + 1 to avoid conflicts between bots
|
|
2237
|
-
const gwPort = Number(cfg.gateway?.port) || state.gatewayPort || 18789;
|
|
2238
|
-
cfg.plugins.entries[existingKey].config = cfg.plugins.entries[existingKey].config || {};
|
|
2239
|
-
if (!cfg.plugins.entries[existingKey].config.dashboardPort) {
|
|
2240
|
-
cfg.plugins.entries[existingKey].config.dashboardPort = gwPort + 1;
|
|
2241
|
-
}
|
|
2281
|
+
if (existingKey === 'zalo-mod' || existingKey === 'openclaw-zalo-mod') {
|
|
2282
|
+
ensureZaloModPluginConfig(cfg.plugins.entries[existingKey], cfg);
|
|
2242
2283
|
}
|
|
2243
2284
|
await fsp.writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
|
|
2244
2285
|
}
|
|
@@ -2302,6 +2343,8 @@ async function getFeatureFlags(projectDir, agentId = '') {
|
|
|
2302
2343
|
Array.from(installedSpecs).some((spec) => spec.includes(a)) ||
|
|
2303
2344
|
allowSet.has(a)
|
|
2304
2345
|
);
|
|
2346
|
+
const imageGenOn = !!cfg.skills?.entries?.['image-gen']?.enabled;
|
|
2347
|
+
const webSearchOn = isEnabled(['duckduckgo']);
|
|
2305
2348
|
const aliases = {
|
|
2306
2349
|
browser: ['openclaw-browser-automation', 'browser-automation'],
|
|
2307
2350
|
zalo: ['openclaw-zalo-mod', 'zalo-mod'],
|
|
@@ -2311,6 +2354,8 @@ async function getFeatureFlags(projectDir, agentId = '') {
|
|
|
2311
2354
|
const flags = {
|
|
2312
2355
|
'skill:browser': browserOn,
|
|
2313
2356
|
'skill:cron': cronOn,
|
|
2357
|
+
'skill:image-gen': imageGenOn,
|
|
2358
|
+
'skill:web-search': webSearchOn,
|
|
2314
2359
|
'plugin:openclaw-browser-automation': isEnabled(aliases.browser),
|
|
2315
2360
|
'plugin:openclaw-zalo-mod': isEnabled(aliases.zalo),
|
|
2316
2361
|
'plugin:openclaw-facebook-crawler': isEnabled(aliases.crawler),
|
|
@@ -2502,6 +2547,7 @@ async function handler(req, res, rootProjectDir) {
|
|
|
2502
2547
|
await run('npm', ['install'], { cwd: rootProjectDir });
|
|
2503
2548
|
await run('npm', ['run', 'build'], { cwd: rootProjectDir });
|
|
2504
2549
|
sendLog('[update-setup] Setup Wizard updated successfully! Please restart the installer.');
|
|
2550
|
+
restartInstaller();
|
|
2505
2551
|
} catch (err) {
|
|
2506
2552
|
sendLog(`[update-setup] Error updating: ${err.message}`);
|
|
2507
2553
|
}
|
|
@@ -2513,6 +2559,7 @@ async function handler(req, res, rootProjectDir) {
|
|
|
2513
2559
|
try {
|
|
2514
2560
|
await run('npm', ['install', '-g', 'create-openclaw-bot@latest'], { cwd: rootProjectDir });
|
|
2515
2561
|
sendLog('[update-setup] Setup Wizard updated successfully! Please restart the installer.');
|
|
2562
|
+
restartInstaller();
|
|
2516
2563
|
} catch (err) {
|
|
2517
2564
|
sendLog(`[update-setup] Error updating: ${err.message}`);
|
|
2518
2565
|
}
|
|
@@ -2610,6 +2657,8 @@ async function handler(req, res, rootProjectDir) {
|
|
|
2610
2657
|
skills: [
|
|
2611
2658
|
{ name: 'Browser', slug: 'browser' },
|
|
2612
2659
|
{ name: 'Cron', slug: 'cron' },
|
|
2660
|
+
{ name: 'Tạo ảnh Infographic', slug: 'image-gen' },
|
|
2661
|
+
{ name: 'Web Search', slug: 'web-search' },
|
|
2613
2662
|
],
|
|
2614
2663
|
plugins: [
|
|
2615
2664
|
{ name: 'openclaw-browser-automation', package: 'openclaw-browser-automation' },
|
|
@@ -2658,13 +2707,42 @@ function openUrl(url) {
|
|
|
2658
2707
|
child.unref();
|
|
2659
2708
|
}
|
|
2660
2709
|
|
|
2710
|
+
function restartInstaller() {
|
|
2711
|
+
sendLog('[update-setup] Restarting Setup Wizard to apply update...');
|
|
2712
|
+
setTimeout(() => {
|
|
2713
|
+
try {
|
|
2714
|
+
if (activeServerInstance) {
|
|
2715
|
+
activeServerInstance.close();
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
const entryFile = process.argv[1];
|
|
2719
|
+
const args = process.argv.slice(2);
|
|
2720
|
+
|
|
2721
|
+
if (!args.includes('--no-open')) {
|
|
2722
|
+
args.push('--no-open');
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2725
|
+
const child = spawn(process.argv[0], [entryFile, ...args], {
|
|
2726
|
+
detached: true,
|
|
2727
|
+
stdio: 'inherit',
|
|
2728
|
+
shell: process.platform === 'win32'
|
|
2729
|
+
});
|
|
2730
|
+
child.unref();
|
|
2731
|
+
|
|
2732
|
+
process.exit(0);
|
|
2733
|
+
} catch (err) {
|
|
2734
|
+
sendLog(`[update-setup] Failed to restart: ${err.message}`);
|
|
2735
|
+
}
|
|
2736
|
+
}, 2000);
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2661
2739
|
export async function startLocalInstaller({ host = '127.0.0.1', preferredPort = 51789, openBrowser = true, projectDir = process.cwd() } = {}) {
|
|
2662
2740
|
const port = await findPort(host, preferredPort);
|
|
2663
2741
|
const server = http.createServer((req, res) => handler(req, res, projectDir));
|
|
2742
|
+
activeServerInstance = server;
|
|
2664
2743
|
await new Promise((resolve) => server.listen(port, host, resolve));
|
|
2665
2744
|
const url = `http://${host}:${port}`;
|
|
2666
2745
|
console.log(`OpenClaw Setup UI: ${url}`);
|
|
2667
|
-
console.log('Legacy CLI: create-openclaw-bot legacy');
|
|
2668
2746
|
if (openBrowser) openUrl(url);
|
|
2669
2747
|
}
|
|
2670
2748
|
|