create-openclaw-bot 5.8.5 → 5.8.9
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 +18 -7
- package/README.vi.md +12 -6
- package/dist/server/local-server.js +252 -38
- package/dist/setup/data/index.js +1 -1
- package/dist/setup/shared/bot-config-gen.js +1 -1
- package/dist/setup/shared/common-gen.js +1 -0
- package/dist/setup/shared/docker-gen.js +5 -5
- package/dist/setup/shared/workspace-gen.js +1200 -645
- package/dist/web/app.js +3 -2
- package/package.json +1 -1
- package/dist/setup/data/skills.js +0 -159
|
@@ -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, buildCronjobSkillMd, buildInfographicGeneratorSkillMd, buildInfographicGeneratorJs } = loadSharedModule('../setup/shared/workspace-gen.js', '__openclawWorkspace');
|
|
16
|
+
const { buildWorkspaceFileMap, buildCronjobSkillMd, buildInfographicGeneratorSkillMd, buildInfographicGeneratorJs, buildStickerMentionSkillMd, buildStickerMentionJs } = 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');
|
|
@@ -96,6 +96,31 @@ function detectOs() {
|
|
|
96
96
|
return 'linux-desktop';
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
function getRealHomedir() {
|
|
100
|
+
const home = os.homedir();
|
|
101
|
+
if (process.platform === 'win32') return home;
|
|
102
|
+
const sudoUser = process.env.SUDO_USER;
|
|
103
|
+
if (sudoUser && (home === '/root' || home.startsWith('/root/'))) {
|
|
104
|
+
const userHome = process.platform === 'darwin' ? `/Users/${sudoUser}` : `/home/${sudoUser}`;
|
|
105
|
+
if (existsSync(userHome)) {
|
|
106
|
+
return userHome;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return home;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function resolveBinPath(cmd) {
|
|
113
|
+
if (!cmd || cmd.includes('/') || cmd.includes('\\')) return cmd;
|
|
114
|
+
const nodeBinDir = dirname(process.argv[0]);
|
|
115
|
+
const localPath = join(nodeBinDir, process.platform === 'win32' ? `${cmd}.cmd` : cmd);
|
|
116
|
+
if (existsSync(localPath)) return localPath;
|
|
117
|
+
const localExe = join(nodeBinDir, process.platform === 'win32' ? `${cmd}.exe` : cmd);
|
|
118
|
+
if (existsSync(localExe)) return localExe;
|
|
119
|
+
const nodeModulesBin = join(process.cwd(), 'node_modules', '.bin', process.platform === 'win32' ? `${cmd}.cmd` : cmd);
|
|
120
|
+
if (existsSync(nodeModulesBin)) return nodeModulesBin;
|
|
121
|
+
return cmd;
|
|
122
|
+
}
|
|
123
|
+
|
|
99
124
|
// Blacklist of Windows system/large directories that should never be walked
|
|
100
125
|
const SYSTEM_DIR_BLACKLIST = new Set([
|
|
101
126
|
'windows', 'program files', 'program files (x86)', 'programdata',
|
|
@@ -128,7 +153,7 @@ function recommendedMode(osChoice) {
|
|
|
128
153
|
function commandExists(cmd, args = ['--version']) {
|
|
129
154
|
return new Promise((resolve) => {
|
|
130
155
|
const shell = process.platform === 'win32';
|
|
131
|
-
execFile(cmd, args, { windowsHide: true, timeout: 5000, shell }, (err, stdout, stderr) => {
|
|
156
|
+
execFile(resolveBinPath(cmd), args, { windowsHide: true, timeout: 5000, shell }, (err, stdout, stderr) => {
|
|
132
157
|
resolve({ ok: !err, output: String(stdout || stderr || '').trim() });
|
|
133
158
|
});
|
|
134
159
|
});
|
|
@@ -137,7 +162,7 @@ function commandExists(cmd, args = ['--version']) {
|
|
|
137
162
|
function run(cmd, args, opts = {}) {
|
|
138
163
|
return new Promise((resolve, reject) => {
|
|
139
164
|
sendLog(`$ ${cmd} ${args.join(' ')}`);
|
|
140
|
-
const child = spawn(cmd, args, { cwd: opts.cwd, shell: process.platform === 'win32', env: { ...process.env, ...(opts.env || {}) } });
|
|
165
|
+
const child = spawn(resolveBinPath(cmd), args, { cwd: opts.cwd, shell: process.platform === 'win32', env: { ...process.env, ...(opts.env || {}) } });
|
|
141
166
|
let stdout = '';
|
|
142
167
|
let resolved = false;
|
|
143
168
|
child.stdout.on('data', (d) => {
|
|
@@ -167,7 +192,7 @@ function run(cmd, args, opts = {}) {
|
|
|
167
192
|
|
|
168
193
|
function startDetached(cmd, args, opts = {}) {
|
|
169
194
|
sendLog(`$ ${cmd} ${args.join(' ')} &`);
|
|
170
|
-
const child = spawn(cmd, args, {
|
|
195
|
+
const child = spawn(resolveBinPath(cmd), args, {
|
|
171
196
|
cwd: opts.cwd,
|
|
172
197
|
shell: process.platform === 'win32',
|
|
173
198
|
detached: true,
|
|
@@ -175,6 +200,10 @@ function startDetached(cmd, args, opts = {}) {
|
|
|
175
200
|
windowsHide: opts.windowsHide ?? true,
|
|
176
201
|
env: { ...process.env, ...(opts.env || {}) },
|
|
177
202
|
});
|
|
203
|
+
child.on('error', (err) => {
|
|
204
|
+
sendLog(`[error] Failed to start background command "${cmd}": ${err.message}`);
|
|
205
|
+
console.error(`Failed to start background command "${cmd}":`, err);
|
|
206
|
+
});
|
|
178
207
|
child.unref();
|
|
179
208
|
return child.pid;
|
|
180
209
|
}
|
|
@@ -264,7 +293,7 @@ function runCapture(cmd, args, opts = {}) {
|
|
|
264
293
|
return new Promise((resolve) => {
|
|
265
294
|
let stdout = '';
|
|
266
295
|
let stderr = '';
|
|
267
|
-
const child = spawn(cmd, args, {
|
|
296
|
+
const child = spawn(resolveBinPath(cmd), args, {
|
|
268
297
|
cwd: opts.cwd,
|
|
269
298
|
shell: opts.shell ?? process.platform === 'win32',
|
|
270
299
|
windowsHide: opts.windowsHide ?? true,
|
|
@@ -446,6 +475,8 @@ async function detectRuntime(projectDir) {
|
|
|
446
475
|
|
|
447
476
|
async function syncRuntimeState(projectDir) {
|
|
448
477
|
if (!projectDir || !existsSync(join(projectDir, '.openclaw', 'openclaw.json'))) return;
|
|
478
|
+
// Auto-migrate legacy /root/project paths → /home/node/project in openclaw.json
|
|
479
|
+
await migrateContainerPaths(projectDir).catch(() => {});
|
|
449
480
|
await applyResolved9RouterApiKey(projectDir).catch(() => {});
|
|
450
481
|
const rt = await detectRuntime(projectDir).catch(() => null);
|
|
451
482
|
if (!rt) return;
|
|
@@ -465,6 +496,38 @@ async function syncRuntimeState(projectDir) {
|
|
|
465
496
|
}
|
|
466
497
|
}
|
|
467
498
|
|
|
499
|
+
/**
|
|
500
|
+
* Migrate legacy /root/project/ paths to /home/node/project/ in openclaw.json.
|
|
501
|
+
* Old projects may have been created with /root/project/ which doesn't match the
|
|
502
|
+
* Docker volume mount point (/home/node/project/.openclaw).
|
|
503
|
+
* Also clears stale workspace attestation files to prevent WorkspaceVanishedError.
|
|
504
|
+
*/
|
|
505
|
+
async function migrateContainerPaths(projectDir) {
|
|
506
|
+
const cfgPath = join(projectDir, '.openclaw', 'openclaw.json');
|
|
507
|
+
if (!existsSync(cfgPath)) return;
|
|
508
|
+
let raw = await fsp.readFile(cfgPath, 'utf8');
|
|
509
|
+
if (!raw.includes('/root/project/')) return;
|
|
510
|
+
// Replace all /root/project/ references with /home/node/project/
|
|
511
|
+
const updated = raw.replace(/\/root\/project\//g, '/home/node/project/');
|
|
512
|
+
if (updated !== raw) {
|
|
513
|
+
await fsp.writeFile(cfgPath, updated, 'utf8');
|
|
514
|
+
sendLog('[migrate] Fixed legacy /root/project/ paths → /home/node/project/ in openclaw.json');
|
|
515
|
+
// Clear stale workspace attestations to avoid WorkspaceVanishedError
|
|
516
|
+
const attestDir = join(projectDir, '.openclaw', 'workspace-attestations');
|
|
517
|
+
if (existsSync(attestDir)) {
|
|
518
|
+
try {
|
|
519
|
+
const files = await fsp.readdir(attestDir);
|
|
520
|
+
for (const f of files) {
|
|
521
|
+
if (f.endsWith('.attested')) {
|
|
522
|
+
await fsp.unlink(join(attestDir, f)).catch(() => {});
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
sendLog('[migrate] Cleared stale workspace attestation files');
|
|
526
|
+
} catch {}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
468
531
|
function uniqueSlug(base, used) {
|
|
469
532
|
let out = base;
|
|
470
533
|
let i = 2;
|
|
@@ -546,6 +609,10 @@ function ensureConfigShape(cfg) {
|
|
|
546
609
|
delete agent.desc;
|
|
547
610
|
delete agent.description;
|
|
548
611
|
delete agent.persona;
|
|
612
|
+
// Auto-fix legacy /root/project paths → /home/node/project (Docker container path)
|
|
613
|
+
if (agent.workspace && agent.workspace.includes('/root/project/')) {
|
|
614
|
+
agent.workspace = agent.workspace.replace('/root/project/', '/home/node/project/');
|
|
615
|
+
}
|
|
549
616
|
}
|
|
550
617
|
agent.model = agent.model || { primary: cfg.agents.defaults.model.primary, fallbacks: [] };
|
|
551
618
|
if (!agent.model.primary || agent.model.primary === '9router/smart-route' || agent.model.primary === 'openai/smart-route') agent.model.primary = DEFAULT_MODEL;
|
|
@@ -717,7 +784,7 @@ async function resolveProject9RouterApiKey(projectDir, cfg = null) {
|
|
|
717
784
|
}
|
|
718
785
|
const nativeApiKey = read9RouterApiKeyFromSqlite(join(projectDir || '', '.9router', 'db', 'data.sqlite'));
|
|
719
786
|
if (nativeApiKey) return nativeApiKey;
|
|
720
|
-
const homeApiKey = read9RouterApiKeyFromSqlite(join(
|
|
787
|
+
const homeApiKey = read9RouterApiKeyFromSqlite(join(getRealHomedir(), '.9router', 'db', 'data.sqlite'));
|
|
721
788
|
if (homeApiKey) return homeApiKey;
|
|
722
789
|
return '';
|
|
723
790
|
}
|
|
@@ -975,7 +1042,7 @@ async function createBotInProject(projectDir, body = {}, runtime = {}) {
|
|
|
975
1042
|
providerKey: '9router',
|
|
976
1043
|
deployMode: runtime.mode || state.mode || 'docker',
|
|
977
1044
|
osChoice: runtime.os || state.os || detectOs(),
|
|
978
|
-
selectedSkills: [],
|
|
1045
|
+
selectedSkills: ['memory', 'image-gen', 'web-search', 'scheduler'],
|
|
979
1046
|
skills: dataExport.SKILLS || [],
|
|
980
1047
|
agentMetas: [],
|
|
981
1048
|
}));
|
|
@@ -995,7 +1062,7 @@ async function createBotInProject(projectDir, body = {}, runtime = {}) {
|
|
|
995
1062
|
cfg.agents.list.push({
|
|
996
1063
|
id: agentId,
|
|
997
1064
|
name: botName,
|
|
998
|
-
workspace: `/
|
|
1065
|
+
workspace: `/home/node/project/.openclaw/${workspaceDir}`,
|
|
999
1066
|
agentDir: `agents/${agentId}/agent`,
|
|
1000
1067
|
model: { primary: model === '9router/smart-route' || model === 'openai/smart-route' ? DEFAULT_MODEL : model, fallbacks: [] },
|
|
1001
1068
|
});
|
|
@@ -1043,7 +1110,9 @@ async function createBotInProject(projectDir, body = {}, runtime = {}) {
|
|
|
1043
1110
|
userInfo,
|
|
1044
1111
|
agentWorkspaceDir: workspaceDir,
|
|
1045
1112
|
workspacePath: `.openclaw/${workspaceDir}`,
|
|
1113
|
+
channel,
|
|
1046
1114
|
hasZaloMod: channel === 'zalo-personal',
|
|
1115
|
+
hasZaloSticker: channel === 'zalo-personal',
|
|
1047
1116
|
hasScheduler,
|
|
1048
1117
|
hasImageGen,
|
|
1049
1118
|
});
|
|
@@ -1074,7 +1143,7 @@ async function updateBotInProject(projectDir, agentId, body = {}, runtime = {})
|
|
|
1074
1143
|
const userDesc = String(body.userDescription || body.userDesc || '').trim();
|
|
1075
1144
|
const userInfo = [userName ? `- **Tên:** ${userName}` : '', userDesc ? `- **Mô tả:** ${userDesc}` : ''].filter(Boolean).join('\n');
|
|
1076
1145
|
const workspaceDir = workspaceRelForAgent(agent, cfg, projectDir) || `workspace-${agentId}`;
|
|
1077
|
-
agent.workspace = `/
|
|
1146
|
+
agent.workspace = `/home/node/project/.openclaw/${workspaceDir}`;
|
|
1078
1147
|
agent.agentDir = `agents/${agentId}/agent`;
|
|
1079
1148
|
|
|
1080
1149
|
// Find the existing accountId from bindings BEFORE removing them
|
|
@@ -1145,7 +1214,9 @@ async function updateBotInProject(projectDir, agentId, body = {}, runtime = {})
|
|
|
1145
1214
|
userInfo,
|
|
1146
1215
|
agentWorkspaceDir: workspaceDir,
|
|
1147
1216
|
workspacePath: `.openclaw/${workspaceDir}`,
|
|
1217
|
+
channel,
|
|
1148
1218
|
hasZaloMod: channel === 'zalo-personal',
|
|
1219
|
+
hasZaloSticker: channel === 'zalo-personal',
|
|
1149
1220
|
hasScheduler,
|
|
1150
1221
|
hasImageGen,
|
|
1151
1222
|
});
|
|
@@ -1154,6 +1225,7 @@ async function updateBotInProject(projectDir, agentId, body = {}, runtime = {})
|
|
|
1154
1225
|
await fsp.mkdir(dirname(join(wsRoot, name)), { recursive: true });
|
|
1155
1226
|
await fsp.writeFile(join(wsRoot, name), content || '', 'utf8');
|
|
1156
1227
|
}
|
|
1228
|
+
|
|
1157
1229
|
return { ok: true, agentId, channel, workspace: `.openclaw/${workspaceDir}` };
|
|
1158
1230
|
}
|
|
1159
1231
|
|
|
@@ -1217,8 +1289,46 @@ async function waitForGatewayZaloReady(botContainer, projectDir, timeoutMs = 900
|
|
|
1217
1289
|
return ready;
|
|
1218
1290
|
}
|
|
1219
1291
|
|
|
1220
|
-
async function startZaloUserLogin(projectDir, mode = state.mode) {
|
|
1221
|
-
|
|
1292
|
+
async function startZaloUserLogin(projectDir, mode = state.mode, agentId = '') {
|
|
1293
|
+
let profile = 'default';
|
|
1294
|
+
if (agentId) {
|
|
1295
|
+
try {
|
|
1296
|
+
const cfgPath = join(projectDir, '.openclaw', 'openclaw.json');
|
|
1297
|
+
if (existsSync(cfgPath)) {
|
|
1298
|
+
const cfg = JSON.parse(await fsp.readFile(cfgPath, 'utf8'));
|
|
1299
|
+
const binding = (cfg.bindings || []).find(b => b.agentId === agentId && b.match?.channel === 'zalouser');
|
|
1300
|
+
if (binding?.match?.accountId) {
|
|
1301
|
+
profile = binding.match.accountId;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
} catch (e) {
|
|
1305
|
+
sendLog(`[zalouser] Warning: Failed to parse openclaw.json: ${e.message}`);
|
|
1306
|
+
}
|
|
1307
|
+
} else if (state.activeBotId) {
|
|
1308
|
+
try {
|
|
1309
|
+
const cfgPath = join(projectDir, '.openclaw', 'openclaw.json');
|
|
1310
|
+
if (existsSync(cfgPath)) {
|
|
1311
|
+
const cfg = JSON.parse(await fsp.readFile(cfgPath, 'utf8'));
|
|
1312
|
+
const binding = (cfg.bindings || []).find(b => b.agentId === state.activeBotId && b.match?.channel === 'zalouser');
|
|
1313
|
+
if (binding?.match?.accountId) {
|
|
1314
|
+
profile = binding.match.accountId;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
} catch (e) {}
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
const qrPaths = [
|
|
1321
|
+
`/tmp/openclaw/openclaw-zalouser-qr-${profile}.png`,
|
|
1322
|
+
`/tmp/openclaw-1000/openclaw-zalouser-qr-${profile}.png`
|
|
1323
|
+
];
|
|
1324
|
+
if (profile === 'default') {
|
|
1325
|
+
qrPaths.push(
|
|
1326
|
+
'/tmp/openclaw/openclaw-zalouser-qr.png',
|
|
1327
|
+
'/tmp/openclaw-1000/openclaw-zalouser-qr.png',
|
|
1328
|
+
'/tmp/openclaw/openclaw-zalouser-qr-default.png'
|
|
1329
|
+
);
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1222
1332
|
if (zaloLoginInFlight) {
|
|
1223
1333
|
setImmediate(async () => {
|
|
1224
1334
|
try {
|
|
@@ -1235,7 +1345,7 @@ async function startZaloUserLogin(projectDir, mode = state.mode) {
|
|
|
1235
1345
|
return { message: 'Zalo login is already running. Keep this modal open...' };
|
|
1236
1346
|
}
|
|
1237
1347
|
zaloLoginInFlight = true;
|
|
1238
|
-
sendLog(
|
|
1348
|
+
sendLog(`[zalouser] Preparing login for profile [${profile}]. QR will be generated for the UI modal.`);
|
|
1239
1349
|
const composeFile = join(projectDir, 'docker', 'openclaw', 'docker-compose.yml');
|
|
1240
1350
|
if ((mode === 'docker' || existsSync(composeFile)) && existsSync(composeFile)) {
|
|
1241
1351
|
const botContainer = getBotContainerName(projectDir);
|
|
@@ -1244,8 +1354,8 @@ async function startZaloUserLogin(projectDir, mode = state.mode) {
|
|
|
1244
1354
|
const checkRegistryScript = `
|
|
1245
1355
|
const fs = require('fs');
|
|
1246
1356
|
try {
|
|
1247
|
-
const dist = '/
|
|
1248
|
-
const inst = '/
|
|
1357
|
+
const dist = '/home/node/project/.openclaw/npm/node_modules/@openclaw/zalouser/dist/index.js';
|
|
1358
|
+
const inst = '/home/node/project/.openclaw/plugins/installs.json';
|
|
1249
1359
|
if (!fs.existsSync(dist)) { console.log('MISSING'); process.exit(0); }
|
|
1250
1360
|
if (!fs.existsSync(inst)) { console.log('MISSING_CHANNELS'); process.exit(0); }
|
|
1251
1361
|
const j = JSON.parse(fs.readFileSync(inst, 'utf8'));
|
|
@@ -1265,17 +1375,17 @@ try {
|
|
|
1265
1375
|
const fixScript = `
|
|
1266
1376
|
const fs=require('fs');
|
|
1267
1377
|
const cp=require('child_process');
|
|
1268
|
-
const cfg='/
|
|
1269
|
-
const bk='/
|
|
1378
|
+
const cfg='/home/node/project/.openclaw/openclaw.json';
|
|
1379
|
+
const bk='/home/node/project/.openclaw/openclaw.json.zalo-backup';
|
|
1270
1380
|
try{if(fs.existsSync(cfg))fs.copyFileSync(cfg,bk);}catch(e){}
|
|
1271
1381
|
// Detect gateway version and pin zalouser plugin to match, preventing createSetupTranslator mismatch
|
|
1272
1382
|
let gatewayVer='';
|
|
1273
1383
|
try{gatewayVer=cp.execSync('openclaw --version 2>/dev/null',{encoding:'utf8'}).trim().replace(/[^0-9.]/g,'');}catch(e){}
|
|
1274
1384
|
const pluginSpec=gatewayVer ? '@openclaw/zalouser@'+gatewayVer : '@openclaw/zalouser';
|
|
1275
1385
|
console.log('Installing plugin via CLI: '+pluginSpec+'...');
|
|
1276
|
-
try{cp.execSync('cd /
|
|
1386
|
+
try{cp.execSync('cd /home/node/project && openclaw plugins install '+pluginSpec+' --force',{stdio:'inherit'});}catch(e){
|
|
1277
1387
|
// Fallback: try without version pin if exact version not found on registry
|
|
1278
|
-
if(gatewayVer){console.log('Pinned version failed, trying latest...');try{cp.execSync('cd /
|
|
1388
|
+
if(gatewayVer){console.log('Pinned version failed, trying latest...');try{cp.execSync('cd /home/node/project && openclaw plugins install @openclaw/zalouser --force',{stdio:'inherit'});}catch(e2){console.error('Install failed');}}
|
|
1279
1389
|
else{console.error('Install failed');}
|
|
1280
1390
|
}
|
|
1281
1391
|
try{
|
|
@@ -1299,10 +1409,10 @@ try{
|
|
|
1299
1409
|
}catch(e){}
|
|
1300
1410
|
try{
|
|
1301
1411
|
console.log('Patching zalouser stability settings...');
|
|
1302
|
-
cp.execSync('ZALO_JS=$(find "/
|
|
1412
|
+
cp.execSync('ZALO_JS=$(find "/home/node/project/.openclaw" -path "*/zalouser/dist/zalo-js*.js" -type f 2>/dev/null | head -1); if [ -n "$ZALO_JS" ]; then sed -i "s/LISTENER_WATCHDOG_MAX_GAP_MS = 35e3/LISTENER_WATCHDOG_MAX_GAP_MS = 120e3/g" "$ZALO_JS"; echo "Patched watchdog gap to 120s"; fi', {shell:true,stdio:'inherit'});
|
|
1303
1413
|
}catch(e){}
|
|
1304
1414
|
try{
|
|
1305
|
-
const ep = '/
|
|
1415
|
+
const ep = '/home/node/project/docker/openclaw/entrypoint.sh';
|
|
1306
1416
|
if (fs.existsSync(ep)) {
|
|
1307
1417
|
let content = fs.readFileSync(ep, 'utf8');
|
|
1308
1418
|
if (!content.includes('zalo-monitor')) {
|
|
@@ -1333,11 +1443,12 @@ try{
|
|
|
1333
1443
|
}
|
|
1334
1444
|
|
|
1335
1445
|
// Clean old credentials & QR files inside container
|
|
1336
|
-
const
|
|
1337
|
-
|
|
1446
|
+
const credFile = profile === 'default' ? 'credentials.json' : `credentials-${profile}.json`;
|
|
1447
|
+
const credPath = `/home/node/project/.openclaw/credentials/zalouser/${credFile}`;
|
|
1448
|
+
await runCapture('docker', ['exec', botContainer, 'sh', '-lc', `rm -f ${credPath} ${qrPaths.join(' ')}`], { cwd: projectDir, shell: false }).catch(() => {});
|
|
1338
1449
|
|
|
1339
1450
|
sendLog('[zalouser] Generating Zalo QR. The image will appear automatically.');
|
|
1340
|
-
const loginCmd =
|
|
1451
|
+
const loginCmd = `cd /home/node/project && openclaw channels login --channel zalouser --account ${profile} --verbose`;
|
|
1341
1452
|
|
|
1342
1453
|
// Retry-based login: the zalouser plugin may need time to connect to Zalo servers.
|
|
1343
1454
|
// The CLI often exits with "Still preparing QR" on the first attempt.
|
|
@@ -1528,6 +1639,33 @@ async function recreateDockerBot(projectDir) {
|
|
|
1528
1639
|
sendLog(`[docker] Recreating ${serviceName} to reload openclaw.json/.env...`);
|
|
1529
1640
|
await run('docker', ['compose', '-f', composeFile, 'up', '-d', '--build', '--force-recreate', serviceName], { cwd: projectDir });
|
|
1530
1641
|
await waitForDockerContainer(containerName);
|
|
1642
|
+
|
|
1643
|
+
// Automatically run Zalo sticker-mention patch if skill is enabled and agent is zalo-personal
|
|
1644
|
+
try {
|
|
1645
|
+
const cfgPath = join(projectDir, '.openclaw', 'openclaw.json');
|
|
1646
|
+
if (existsSync(cfgPath)) {
|
|
1647
|
+
const cfg = JSON.parse(await fsp.readFile(cfgPath, 'utf8'));
|
|
1648
|
+
const stickerMentionOn = !!cfg.skills?.entries?.['sticker-mention']?.enabled;
|
|
1649
|
+
if (stickerMentionOn) {
|
|
1650
|
+
for (const a of cfg.agents?.list || []) {
|
|
1651
|
+
const binding = (cfg.bindings || []).find((b) => b.agentId === a.id);
|
|
1652
|
+
const channel = binding?.match?.channel || 'telegram';
|
|
1653
|
+
if (channel === 'zalo-personal') {
|
|
1654
|
+
const workspaceDir = workspaceRelForAgent(a, cfg, projectDir) || `workspace-${a.id}`;
|
|
1655
|
+
const mentionsJsPath = join(projectDir, '.openclaw', workspaceDir, 'skills/sticker-mention/mentions.js');
|
|
1656
|
+
if (existsSync(mentionsJsPath)) {
|
|
1657
|
+
sendLog(`[zalo-patch] Automatically running mentions.js inside container ${containerName}...`);
|
|
1658
|
+
const patchCmd = await runCapture('docker', ['exec', containerName, 'node', `/home/node/project/.openclaw/${workspaceDir}/skills/sticker-mention/mentions.js`], { cwd: projectDir, shell: false });
|
|
1659
|
+
sendLog(`[zalo-patch] Output: ${patchCmd.stdout || ''} ${patchCmd.stderr || ''}`);
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
} catch (err) {
|
|
1666
|
+
sendLog(`[zalo-patch] Failed to auto-run mentions.js: ${err.message}`);
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1531
1669
|
return true;
|
|
1532
1670
|
}
|
|
1533
1671
|
|
|
@@ -1588,7 +1726,7 @@ async function writeCoreProject({ projectDir, osChoice, mode, gatewayPort = 1878
|
|
|
1588
1726
|
await fsp.mkdir(openclawHome, { recursive: true });
|
|
1589
1727
|
await fsp.mkdir(join(openclawHome, 'plugin-runtime-deps'), { recursive: true });
|
|
1590
1728
|
|
|
1591
|
-
const selectedSkills = ['memory', 'image-gen', 'web-search'];
|
|
1729
|
+
const selectedSkills = ['memory', 'image-gen', 'web-search', 'scheduler'];
|
|
1592
1730
|
const agentMetas = [];
|
|
1593
1731
|
const common = { channelKey: 'telegram', providerKey: '9router', model: DEFAULT_MODEL, deployMode: mode, osChoice, selectedSkills, skills: dataExport.SKILLS || [], agentMetas, gatewayPort, routerPort };
|
|
1594
1732
|
const cfg = buildOpenclawJson(common);
|
|
@@ -1652,7 +1790,7 @@ async function installCore({ osChoice, mode, projectDir, gatewayPort = 18789, ro
|
|
|
1652
1790
|
await fsp.mkdir(dockerDir, { recursive: true });
|
|
1653
1791
|
const envContent = existsSync(rootEnvPath)
|
|
1654
1792
|
? await fsp.readFile(rootEnvPath, 'utf8')
|
|
1655
|
-
: buildEnvFileContent({ channelKey: 'telegram', providerKey: '9router', deployMode: mode, osChoice, selectedSkills: [], skills: dataExport.SKILLS || [], agentMetas: [], apiKey: '', botToken: '' });
|
|
1793
|
+
: buildEnvFileContent({ channelKey: 'telegram', providerKey: '9router', deployMode: mode, osChoice, selectedSkills: ['memory', 'image-gen', 'web-search', 'scheduler'], skills: dataExport.SKILLS || [], agentMetas: [], apiKey: '', botToken: '' });
|
|
1656
1794
|
await fsp.writeFile(dockerEnvPath, envContent, 'utf8');
|
|
1657
1795
|
sendLog(`Docker env ready: ${dockerEnvPath}`);
|
|
1658
1796
|
await run('docker', ['compose', 'up', '-d', '--build'], { cwd: dockerDir });
|
|
@@ -1786,23 +1924,39 @@ function isRestrictedSystemDir(dirPath) {
|
|
|
1786
1924
|
}
|
|
1787
1925
|
|
|
1788
1926
|
if (lower.includes(':\\users\\') || lower.endsWith(':\\users')) {
|
|
1789
|
-
const home = resolve(
|
|
1927
|
+
const home = resolve(getRealHomedir()).toLowerCase();
|
|
1790
1928
|
if (lower !== home && !lower.startsWith(home + '\\') && !lower.startsWith(home + '/')) {
|
|
1791
1929
|
return true;
|
|
1792
1930
|
}
|
|
1793
1931
|
}
|
|
1932
|
+
|
|
1933
|
+
if (process.platform !== 'win32') {
|
|
1934
|
+
const unixBlacklist = new Set([
|
|
1935
|
+
'usr', 'var', 'proc', 'sys', 'dev', 'etc', 'sbin', 'bin', 'lib', 'lib64', 'run', 'tmp', 'boot', 'lost+found', 'srv', 'mnt', 'media', 'opt'
|
|
1936
|
+
]);
|
|
1937
|
+
if (unixBlacklist.has(basename(lower))) return true;
|
|
1938
|
+
|
|
1939
|
+
if (lower.startsWith('/home') || lower.startsWith('/root')) {
|
|
1940
|
+
const realHome = resolve(getRealHomedir()).toLowerCase();
|
|
1941
|
+
if (lower !== realHome && !lower.startsWith(realHome + '/')) {
|
|
1942
|
+
return true;
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1794
1947
|
return false;
|
|
1795
1948
|
}
|
|
1796
1949
|
|
|
1797
1950
|
async function findLatestProject(rootProjectDir) {
|
|
1951
|
+
const realHome = getRealHomedir();
|
|
1798
1952
|
const roots = [
|
|
1799
1953
|
process.env.OPENCLAW_PROJECT_DIR,
|
|
1800
1954
|
process.env.OPENCLAW_HOME ? dirname(process.env.OPENCLAW_HOME) : '',
|
|
1801
1955
|
rootProjectDir,
|
|
1802
1956
|
join(rootProjectDir, DEFAULT_PROJECT_NAME),
|
|
1803
1957
|
dirname(rootProjectDir),
|
|
1804
|
-
|
|
1805
|
-
join(
|
|
1958
|
+
realHome,
|
|
1959
|
+
join(realHome, 'Documents'),
|
|
1806
1960
|
].filter(Boolean);
|
|
1807
1961
|
|
|
1808
1962
|
const drives = await getAvailableDrives();
|
|
@@ -1844,13 +1998,14 @@ async function findLatestProject(rootProjectDir) {
|
|
|
1844
1998
|
}
|
|
1845
1999
|
|
|
1846
2000
|
async function discoverProjects(rootProjectDir) {
|
|
2001
|
+
const realHome = getRealHomedir();
|
|
1847
2002
|
const roots = [
|
|
1848
2003
|
process.env.OPENCLAW_PROJECT_DIR,
|
|
1849
2004
|
rootProjectDir,
|
|
1850
2005
|
dirname(rootProjectDir),
|
|
1851
2006
|
process.env.OPENCLAW_HOME ? dirname(process.env.OPENCLAW_HOME) : '',
|
|
1852
|
-
|
|
1853
|
-
join(
|
|
2007
|
+
realHome,
|
|
2008
|
+
join(realHome, 'Documents'),
|
|
1854
2009
|
].filter(Boolean);
|
|
1855
2010
|
|
|
1856
2011
|
const drives = await getAvailableDrives();
|
|
@@ -1973,9 +2128,10 @@ async function connectPickedProject(projectName, rootProjectDir) {
|
|
|
1973
2128
|
|
|
1974
2129
|
async function deleteProjectFolder(projectDir, rootProjectDir) {
|
|
1975
2130
|
const resolved = resolve(String(projectDir || ''));
|
|
1976
|
-
const home = resolve(
|
|
2131
|
+
const home = resolve(getRealHomedir());
|
|
2132
|
+
const rootHome = resolve(os.homedir());
|
|
1977
2133
|
if (!existsSync(join(resolved, '.openclaw', 'openclaw.json'))) throw httpError(404, 'openclaw.json not found in selected project');
|
|
1978
|
-
if (resolved === home || /^[A-Za-z]:\\?$/.test(resolved)) throw httpError(403, 'Refusing to delete home/root folder');
|
|
2134
|
+
if (resolved === home || resolved === rootHome || /^[A-Za-z]:\\?$/.test(resolved)) throw httpError(403, 'Refusing to delete home/root folder');
|
|
1979
2135
|
const projects = await discoverProjects(rootProjectDir).catch(() => []);
|
|
1980
2136
|
const meta = projects.find((p) => resolve(p.projectDir) === resolved);
|
|
1981
2137
|
if (!meta || !meta.botCount) throw httpError(403, 'Refusing to delete a folder that is not a detected bot project');
|
|
@@ -2086,6 +2242,12 @@ async function applyFeatureToggle(projectDir, agentId, kind, id, enabled) {
|
|
|
2086
2242
|
}
|
|
2087
2243
|
|
|
2088
2244
|
if (kind === 'skill' && id === 'cron') {
|
|
2245
|
+
cfg.skills = cfg.skills || { entries: {} };
|
|
2246
|
+
cfg.skills.entries = cfg.skills.entries || {};
|
|
2247
|
+
delete cfg.skills.entries['cron'];
|
|
2248
|
+
cfg.skills.entries['cronjob'] = cfg.skills.entries['cronjob'] || {};
|
|
2249
|
+
cfg.skills.entries['cronjob'].enabled = !!enabled;
|
|
2250
|
+
|
|
2089
2251
|
if (enabled) {
|
|
2090
2252
|
cfg.tools = cfg.tools || { profile: 'full', exec: { host: 'gateway', security: 'full', ask: 'off' } };
|
|
2091
2253
|
cfg.tools.alsoAllow = Array.from(new Set([...(cfg.tools.alsoAllow || []), 'group:automation']));
|
|
@@ -2169,6 +2331,51 @@ async function applyFeatureToggle(projectDir, agentId, kind, id, enabled) {
|
|
|
2169
2331
|
}
|
|
2170
2332
|
}
|
|
2171
2333
|
|
|
2334
|
+
if (kind === 'skill' && id === 'sticker-mention') {
|
|
2335
|
+
cfg.skills = cfg.skills || { entries: {} };
|
|
2336
|
+
cfg.skills.entries = cfg.skills.entries || {};
|
|
2337
|
+
cfg.skills.entries['sticker-mention'] = cfg.skills.entries['sticker-mention'] || {};
|
|
2338
|
+
cfg.skills.entries['sticker-mention'].enabled = !!enabled;
|
|
2339
|
+
|
|
2340
|
+
for (const a of cfg.agents.list) {
|
|
2341
|
+
const sf = await readWorkspaceText(projectDir, a, 'skills/sticker-mention/SKILL.md');
|
|
2342
|
+
const jsf = await readWorkspaceText(projectDir, a, 'skills/sticker-mention/mentions.js');
|
|
2343
|
+
if (enabled) {
|
|
2344
|
+
await fsp.mkdir(dirname(sf.file), { recursive: true });
|
|
2345
|
+
await fsp.writeFile(sf.file, buildStickerMentionSkillMd(), 'utf8');
|
|
2346
|
+
await fsp.writeFile(jsf.file, buildStickerMentionJs(), 'utf8');
|
|
2347
|
+
} else {
|
|
2348
|
+
const binding = (cfg.bindings || []).find((b) => b.agentId === a.id);
|
|
2349
|
+
const channel = binding?.match?.channel || 'telegram';
|
|
2350
|
+
if (channel === 'zalo-personal' && existsSync(jsf.file)) {
|
|
2351
|
+
try {
|
|
2352
|
+
const hasDocker = existsSync(join(projectDir, 'docker', 'openclaw', 'docker-compose.yml'));
|
|
2353
|
+
if (hasDocker) {
|
|
2354
|
+
const botContainer = getBotContainerName(projectDir);
|
|
2355
|
+
const workspaceDir = workspaceRelForAgent(a, cfg, projectDir) || `workspace-${a.id}`;
|
|
2356
|
+
sendLog('[zalo-patch] Running restore before disabling skill...');
|
|
2357
|
+
await runCapture('docker', ['exec', botContainer, 'node', `/home/node/project/.openclaw/${workspaceDir}/skills/sticker-mention/mentions.js`, '--restore'], { cwd: projectDir, shell: false });
|
|
2358
|
+
}
|
|
2359
|
+
} catch (e) {
|
|
2360
|
+
sendLog(`[zalo-patch] Restore failed: ${e.message}`);
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
if (existsSync(sf.file)) await fsp.rm(sf.file, { force: true });
|
|
2364
|
+
if (existsSync(jsf.file)) await fsp.rm(jsf.file, { force: true });
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
// Write cfgPath early so recreation reads updated openclaw.json
|
|
2369
|
+
await fsp.writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
|
|
2370
|
+
|
|
2371
|
+
// Recreate container to apply updated openclaw.json
|
|
2372
|
+
const hasDocker = existsSync(join(projectDir, 'docker', 'openclaw', 'docker-compose.yml'));
|
|
2373
|
+
if (hasDocker) {
|
|
2374
|
+
sendLog(`[docker] Sticker & Mention skill toggled to ${enabled}. Recreating containers...`);
|
|
2375
|
+
await recreateDockerBot(projectDir).catch((err) => sendLog(`[docker] Warning: Failed to recreate container: ${err.message}`));
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2172
2379
|
if (kind === 'plugin') {
|
|
2173
2380
|
cfg.plugins = cfg.plugins || { entries: {} };
|
|
2174
2381
|
cfg.plugins.entries = cfg.plugins.entries || {};
|
|
@@ -2226,7 +2433,7 @@ async function installFeature(projectDir, agentId, kind, id) {
|
|
|
2226
2433
|
const botContainer = getBotContainerName(projectDir);
|
|
2227
2434
|
sendLog(`[plugin] Installing/updating clawhub:${id} inside container ${botContainer}...`);
|
|
2228
2435
|
|
|
2229
|
-
const cmd = `cd /
|
|
2436
|
+
const cmd = `cd /home/node/project && openclaw plugins install clawhub:${id} --force`;
|
|
2230
2437
|
const cmdOut = await runCapture('docker', ['exec', botContainer, 'sh', '-lc', cmd], { cwd: projectDir, shell: false });
|
|
2231
2438
|
|
|
2232
2439
|
if (cmdOut) {
|
|
@@ -2382,7 +2589,7 @@ async function getFeatureFlags(projectDir, agentId = '') {
|
|
|
2382
2589
|
const cfg = existsSync(cfgPath) ? ensureConfigShape(JSON.parse(await fsp.readFile(cfgPath, 'utf8').catch(() => '{}'))) : {};
|
|
2383
2590
|
const aid = agentId || cfg.agents?.list?.[0]?.id || 'bot';
|
|
2384
2591
|
const browserOn = !!cfg.browser?.enabled;
|
|
2385
|
-
const cronOn = !!(cfg.tools?.alsoAllow || []).includes('group:automation');
|
|
2592
|
+
const cronOn = !!cfg.skills?.entries?.['cronjob']?.enabled || !!cfg.skills?.entries?.['cron']?.enabled || !!(cfg.tools?.alsoAllow || []).includes('group:automation');
|
|
2386
2593
|
const fresh = cfg;
|
|
2387
2594
|
const freshSaved = {};
|
|
2388
2595
|
const installsPath = join(projectDir || '', '.openclaw', 'plugins', 'installs.json');
|
|
@@ -2411,6 +2618,7 @@ async function getFeatureFlags(projectDir, agentId = '') {
|
|
|
2411
2618
|
);
|
|
2412
2619
|
const imageGenOn = !!cfg.skills?.entries?.['image-gen']?.enabled;
|
|
2413
2620
|
const webSearchOn = isEnabled(['duckduckgo']);
|
|
2621
|
+
const stickerMentionOn = !!cfg.skills?.entries?.['sticker-mention']?.enabled;
|
|
2414
2622
|
const aliases = {
|
|
2415
2623
|
browser: ['openclaw-browser-automation', 'browser-automation'],
|
|
2416
2624
|
zalo: ['openclaw-zalo-mod', 'zalo-mod'],
|
|
@@ -2422,6 +2630,7 @@ async function getFeatureFlags(projectDir, agentId = '') {
|
|
|
2422
2630
|
'skill:cron': cronOn,
|
|
2423
2631
|
'skill:image-gen': imageGenOn,
|
|
2424
2632
|
'skill:web-search': webSearchOn,
|
|
2633
|
+
'skill:sticker-mention': stickerMentionOn,
|
|
2425
2634
|
'plugin:openclaw-browser-automation': isEnabled(aliases.browser),
|
|
2426
2635
|
'plugin:openclaw-zalo-mod': isEnabled(aliases.zalo),
|
|
2427
2636
|
'plugin:openclaw-facebook-crawler': isEnabled(aliases.crawler),
|
|
@@ -2641,7 +2850,7 @@ async function handler(req, res, rootProjectDir) {
|
|
|
2641
2850
|
const token = String(body.token || '').trim();
|
|
2642
2851
|
sendLog(`[telegram] Registering Telegram channel via CLI inside ${botContainer}...`);
|
|
2643
2852
|
try {
|
|
2644
|
-
const regResult = await runCapture('docker', ['exec', botContainer, 'sh', '-lc', `cd /
|
|
2853
|
+
const regResult = await runCapture('docker', ['exec', botContainer, 'sh', '-lc', `cd /home/node/project && openclaw channels add telegram --token "${token}"`], { cwd: projectDir, shell: false });
|
|
2645
2854
|
sendLog(`[telegram] CLI registration output:\n${regResult.stdout}\n${regResult.stderr}`);
|
|
2646
2855
|
sendLog(`[telegram] Restarting ${botContainer} container to load the registered channel...`);
|
|
2647
2856
|
await restartDockerBotContainer(projectDir).catch((err) => sendLog(`[telegram] Container restart failed: ${err.message}`));
|
|
@@ -2658,7 +2867,7 @@ async function handler(req, res, rootProjectDir) {
|
|
|
2658
2867
|
// Delay login start to let the recreated container fully boot gateway + plugins
|
|
2659
2868
|
setTimeout(async () => {
|
|
2660
2869
|
try {
|
|
2661
|
-
const login = await startZaloUserLogin(projectDir, state.mode);
|
|
2870
|
+
const login = await startZaloUserLogin(projectDir, state.mode, result.agentId);
|
|
2662
2871
|
if (login?.qrDataUrl) sendLog(`[zalouser:qr] ${login.qrDataUrl}`);
|
|
2663
2872
|
if (login?.message) sendLog(`[zalouser] ${login.message}`);
|
|
2664
2873
|
} catch (err) {
|
|
@@ -2678,10 +2887,12 @@ async function handler(req, res, rootProjectDir) {
|
|
|
2678
2887
|
return json(res, result);
|
|
2679
2888
|
}
|
|
2680
2889
|
if (url.pathname === '/api/zalo/login' && req.method === 'POST') {
|
|
2681
|
-
const
|
|
2890
|
+
const body = await readJson(req).catch(() => ({}));
|
|
2891
|
+
const agentId = body.agentId || '';
|
|
2892
|
+
const projectDir = await resolveProjectDir(rootProjectDir, body);
|
|
2682
2893
|
setImmediate(async () => {
|
|
2683
2894
|
try {
|
|
2684
|
-
const login = await startZaloUserLogin(projectDir, state.mode);
|
|
2895
|
+
const login = await startZaloUserLogin(projectDir, state.mode, agentId);
|
|
2685
2896
|
if (login?.qrDataUrl) sendLog(`[zalouser:qr] ${login.qrDataUrl}`);
|
|
2686
2897
|
if (login?.message) sendLog(`[zalouser] ${login.message}`);
|
|
2687
2898
|
} catch (err) {
|
|
@@ -2764,6 +2975,9 @@ function openUrl(url) {
|
|
|
2764
2975
|
const cmd = process.platform === 'win32' ? 'cmd' : process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
2765
2976
|
const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
2766
2977
|
const child = spawn(cmd, args, { detached: true, stdio: 'ignore', shell: false, windowsHide: true });
|
|
2978
|
+
child.on('error', (err) => {
|
|
2979
|
+
sendLog(`[openUrl] Warning: Could not open browser automatically (${err.message}). Please navigate to ${url} manually.`);
|
|
2980
|
+
});
|
|
2767
2981
|
child.unref();
|
|
2768
2982
|
}
|
|
2769
2983
|
|
package/dist/setup/data/index.js
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
// Keep in sync with setup/data/skills.js skill IDs
|
|
31
31
|
const SKILLS = [
|
|
32
32
|
{ value: 'memory', name: '🧠 Long-term Memory (⭐ Khuyên dùng)', slug: 'memory' },
|
|
33
|
-
{ value: 'scheduler', name: '⏰ Native Cron Scheduler (⭐ Khuyên dùng)', slug:
|
|
33
|
+
{ value: 'scheduler', name: '⏰ Native Cron Scheduler (⭐ Khuyên dùng)', slug: 'cronjob' },
|
|
34
34
|
{ value: 'rag', name: '📚 RAG / Knowledge Base', slug: 'rag' },
|
|
35
35
|
{ value: 'image-gen', name: '🎨 Image Generation (DALL·E / Flux)', slug: 'image-gen' },
|
|
36
36
|
{ value: 'code-interpreter', name: '💻 Code Interpreter (Python/JS)', slug: 'code-interpreter' },
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
const agentsList = agentMetas.map((meta) => ({
|
|
86
86
|
id: meta.agentId,
|
|
87
87
|
...(meta.name ? { name: meta.name } : {}),
|
|
88
|
-
workspace: `/
|
|
88
|
+
workspace: `/home/node/project/.openclaw/${meta.workspaceDir || 'workspace-' + meta.agentId}`,
|
|
89
89
|
agentDir: `agents/${meta.agentId}/agent`,
|
|
90
90
|
model: { primary: model, fallbacks: [] },
|
|
91
91
|
}));
|