create-openclaw-bot 5.8.18 → 5.8.19
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.
|
@@ -19,7 +19,7 @@ const { buildDockerArtifacts } = loadSharedModule('../setup/shared/docker-gen.js
|
|
|
19
19
|
const { OPENCLAW_NPM_SPEC, NINE_ROUTER_NPM_SPEC, build9RouterProviderConfig, get9RouterBaseUrl } = loadSharedModule('../setup/shared/common-gen.js', '__openclawCommon');
|
|
20
20
|
const dataExport = loadSharedModule('../setup/data/index.js', '__openclawData');
|
|
21
21
|
|
|
22
|
-
async function syncExecApprovals(projectDir, cfg) {
|
|
22
|
+
async function syncExecApprovals(projectDir, cfg) {
|
|
23
23
|
const openclawHome = join(projectDir, '.openclaw');
|
|
24
24
|
const agentMetas = (cfg.agents?.list || []).map((a) => ({ agentId: a.id }));
|
|
25
25
|
const approvals = buildExecApprovalsJson({ agentMetas });
|
|
@@ -41,8 +41,189 @@ async function syncExecApprovals(projectDir, cfg) {
|
|
|
41
41
|
if (existing.socket) {
|
|
42
42
|
approvals.socket = existing.socket;
|
|
43
43
|
}
|
|
44
|
-
await fsp.writeFile(path2, JSON.stringify(approvals, null, 2), 'utf8');
|
|
45
|
-
}
|
|
44
|
+
await fsp.writeFile(path2, JSON.stringify(approvals, null, 2), 'utf8');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function patchBrowserAutomationHostPreference(projectDir, aliases = [], sendLog = () => {}) {
|
|
48
|
+
const preferredCdpBlock = `const dns = require('dns').promises;
|
|
49
|
+
const DEFAULT_CDP_URLS = [
|
|
50
|
+
'host-gateway:9222',
|
|
51
|
+
'http://127.0.0.1:9222',
|
|
52
|
+
];
|
|
53
|
+
const CDP_URLS = (process.env.OPENCLAW_BROWSER_CDP_URLS || DEFAULT_CDP_URLS.join(','))
|
|
54
|
+
.split(',')
|
|
55
|
+
.map((u) => u.trim())
|
|
56
|
+
.filter(Boolean);
|
|
57
|
+
|
|
58
|
+
async function normalizeCdpUrl(url) {
|
|
59
|
+
if (url === 'host-gateway:9222') {
|
|
60
|
+
try {
|
|
61
|
+
const resolved = await dns.lookup('host.docker.internal');
|
|
62
|
+
return 'http://' + resolved.address + ':9222';
|
|
63
|
+
} catch (_) {
|
|
64
|
+
return 'http://host.docker.internal:9222';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return url;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function connectPreferredChrome() {
|
|
71
|
+
let lastError;
|
|
72
|
+
for (const rawUrl of CDP_URLS) {
|
|
73
|
+
const url = await normalizeCdpUrl(rawUrl);
|
|
74
|
+
try {
|
|
75
|
+
const connected = await chromium.connectOverCDP(url, { timeout: 2500 });
|
|
76
|
+
console.error('[Browser] Connected CDP: ' + url);
|
|
77
|
+
return connected;
|
|
78
|
+
} catch (err) {
|
|
79
|
+
lastError = err;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
throw lastError || new Error('No Chrome CDP endpoint available');
|
|
83
|
+
}`;
|
|
84
|
+
|
|
85
|
+
const patchContent = (content) => {
|
|
86
|
+
let next = content;
|
|
87
|
+
next = next.replace(
|
|
88
|
+
"const CDP_URL = 'http://127.0.0.1:9222';",
|
|
89
|
+
preferredCdpBlock
|
|
90
|
+
);
|
|
91
|
+
next = next.replace(
|
|
92
|
+
'browser = await chromium.connectOverCDP(CDP_URL, { timeout: 5000 });',
|
|
93
|
+
'browser = await connectPreferredChrome();'
|
|
94
|
+
);
|
|
95
|
+
return next;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const browserToolCandidates = new Set();
|
|
99
|
+
const extensionDirs = [];
|
|
100
|
+
for (const alias of aliases) {
|
|
101
|
+
const extensionDir = join(projectDir, '.openclaw', 'extensions', alias);
|
|
102
|
+
extensionDirs.push(extensionDir);
|
|
103
|
+
browserToolCandidates.add(join(extensionDir, 'browser-tool.js'));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const workspaceDirs = new Set();
|
|
107
|
+
try {
|
|
108
|
+
const cfgPath = join(projectDir, '.openclaw', 'openclaw.json');
|
|
109
|
+
if (existsSync(cfgPath)) {
|
|
110
|
+
const cfg = JSON.parse(await fsp.readFile(cfgPath, 'utf8'));
|
|
111
|
+
for (const a of cfg.agents?.list || []) {
|
|
112
|
+
const workspaceRel = a.workspace || cfg.agents?.defaults?.workspace;
|
|
113
|
+
if (!workspaceRel) continue;
|
|
114
|
+
const workspacePath = workspaceRel.startsWith('/') ? join(projectDir, workspaceRel.replace(/^\/home\/node\/project\/?/, '')) : join(projectDir, workspaceRel);
|
|
115
|
+
workspaceDirs.add(workspacePath);
|
|
116
|
+
browserToolCandidates.add(join(workspacePath, 'plugin-skills', 'browser-automation', 'browser-tool.js'));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} catch (err) {
|
|
120
|
+
sendLog(`[browser] Warning: could not scan workspaces for browser-tool.js: ${err.message}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let patched = 0;
|
|
124
|
+
for (const file of browserToolCandidates) {
|
|
125
|
+
if (!existsSync(file)) continue;
|
|
126
|
+
const content = await fsp.readFile(file, 'utf8');
|
|
127
|
+
if (content.includes('connectPreferredChrome')) continue;
|
|
128
|
+
const next = patchContent(content);
|
|
129
|
+
if (next !== content) {
|
|
130
|
+
await fsp.writeFile(file, next, 'utf8');
|
|
131
|
+
patched += 1;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (patched > 0) {
|
|
135
|
+
sendLog(`[browser] Patched ${patched} browser-tool.js file(s) to prefer host Chrome debug before headless Chromium.`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const browserMd = `# Browser Automation
|
|
139
|
+
|
|
140
|
+
This plugin skill owns browser automation only. For normal web search, use OpenClaw's built-in \`web_search\` capability.
|
|
141
|
+
|
|
142
|
+
Run commands from this folder or pass the full path from the workspace root:
|
|
143
|
+
|
|
144
|
+
- \`cd plugin-skills/browser-automation && node browser-tool.js status\`
|
|
145
|
+
- \`node plugin-skills/browser-automation/browser-tool.js status\`
|
|
146
|
+
|
|
147
|
+
## Chrome Debug Mode
|
|
148
|
+
|
|
149
|
+
On a desktop machine, start real Chrome in debug mode before asking the bot to browse:
|
|
150
|
+
|
|
151
|
+
- Windows: run \`start-chrome-debug.bat\`
|
|
152
|
+
- macOS/Linux: run \`./start-chrome-debug.sh\`
|
|
153
|
+
|
|
154
|
+
The tool will try real host Chrome first. If Chrome debug is not available, it falls back to local headless Chromium, which is suitable for VPS/server use.
|
|
155
|
+
|
|
156
|
+
## Browser Commands
|
|
157
|
+
|
|
158
|
+
- \`node plugin-skills/browser-automation/browser-tool.js status\`: check the active browser/tab
|
|
159
|
+
- \`node plugin-skills/browser-automation/browser-tool.js open <url>\`: open a page
|
|
160
|
+
- \`node plugin-skills/browser-automation/browser-tool.js get_text [max_chars]\`: read rendered page text
|
|
161
|
+
- \`node plugin-skills/browser-automation/browser-tool.js get_links [filter]\`: list links
|
|
162
|
+
- \`node plugin-skills/browser-automation/browser-tool.js click "<selector>"\`: click an element
|
|
163
|
+
- \`node plugin-skills/browser-automation/browser-tool.js fill "<selector>" "<text>"\`: fill an input
|
|
164
|
+
- \`node plugin-skills/browser-automation/browser-tool.js scroll [px]\`: scroll the page
|
|
165
|
+
- \`node plugin-skills/browser-automation/browser-tool.js screenshot [path]\`: capture the viewport
|
|
166
|
+
- \`node plugin-skills/browser-automation/browser-tool.js tabs\`: list tabs
|
|
167
|
+
|
|
168
|
+
Do not call \`search-tool.js\`; browser-automation does not own search. Use \`web_search\` for search and this browser tool only when a rendered browser is needed.
|
|
169
|
+
`;
|
|
170
|
+
|
|
171
|
+
const removeManagedBlock = (content, blockId) => {
|
|
172
|
+
const startTag = `<!-- OPENCLAW:${blockId}:START -->`;
|
|
173
|
+
const endTag = `<!-- OPENCLAW:${blockId}:END -->`;
|
|
174
|
+
const startIdx = content.indexOf(startTag);
|
|
175
|
+
const endIdx = content.indexOf(endTag);
|
|
176
|
+
if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) return content;
|
|
177
|
+
return `${content.substring(0, startIdx).trimEnd()}\n${content.substring(endIdx + endTag.length).trimStart()}`.trim() + '\n';
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const hostOs = normalizeHostOs(await resolveProjectHostOs(projectDir));
|
|
181
|
+
const shouldKeepBat = hostOs === 'win';
|
|
182
|
+
const scriptToKeep = shouldKeepBat ? 'start-chrome-debug.bat' : 'start-chrome-debug.sh';
|
|
183
|
+
const scriptToRemove = shouldKeepBat ? 'start-chrome-debug.sh' : 'start-chrome-debug.bat';
|
|
184
|
+
const sourceScript = extensionDirs.map((dir) => join(dir, scriptToKeep)).find((file) => existsSync(file));
|
|
185
|
+
const sourceBrowserTool = extensionDirs.map((dir) => join(dir, 'browser-tool.js')).find((file) => existsSync(file));
|
|
186
|
+
|
|
187
|
+
let sanitized = 0;
|
|
188
|
+
for (const workspacePath of workspaceDirs) {
|
|
189
|
+
if (!existsSync(workspacePath)) continue;
|
|
190
|
+
const pluginSkillPath = join(workspacePath, 'plugin-skills', 'browser-automation');
|
|
191
|
+
await fsp.mkdir(pluginSkillPath, { recursive: true }).catch(() => {});
|
|
192
|
+
await fsp.rm(join(workspacePath, 'search-tool.js'), { force: true }).catch(() => {});
|
|
193
|
+
await fsp.rm(join(workspacePath, 'browser-tool.js'), { force: true }).catch(() => {});
|
|
194
|
+
await fsp.rm(join(workspacePath, 'BROWSER.md'), { force: true }).catch(() => {});
|
|
195
|
+
await fsp.rm(join(workspacePath, scriptToRemove), { force: true }).catch(() => {});
|
|
196
|
+
await fsp.rm(join(workspacePath, scriptToKeep), { force: true }).catch(() => {});
|
|
197
|
+
await fsp.rm(join(pluginSkillPath, scriptToRemove), { force: true }).catch(() => {});
|
|
198
|
+
if (sourceBrowserTool) {
|
|
199
|
+
const targetBrowserTool = join(pluginSkillPath, 'browser-tool.js');
|
|
200
|
+
await fsp.copyFile(sourceBrowserTool, targetBrowserTool).catch(() => {});
|
|
201
|
+
if (existsSync(targetBrowserTool)) {
|
|
202
|
+
const content = await fsp.readFile(targetBrowserTool, 'utf8');
|
|
203
|
+
const next = content.includes('connectPreferredChrome') ? content : patchContent(content);
|
|
204
|
+
if (next !== content) await fsp.writeFile(targetBrowserTool, next, 'utf8');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (sourceScript) {
|
|
208
|
+
await fsp.copyFile(sourceScript, join(pluginSkillPath, scriptToKeep)).catch(() => {});
|
|
209
|
+
if (scriptToKeep.endsWith('.sh')) await fsp.chmod(join(pluginSkillPath, scriptToKeep), 0o755).catch(() => {});
|
|
210
|
+
}
|
|
211
|
+
await fsp.writeFile(join(pluginSkillPath, 'BROWSER.md'), browserMd, 'utf8');
|
|
212
|
+
for (const dirName of ['cl-stealth-search', 'openclaw-smart-search']) {
|
|
213
|
+
await fsp.rm(join(workspacePath, 'plugin-skills', dirName), { recursive: true, force: true }).catch(() => {});
|
|
214
|
+
}
|
|
215
|
+
const toolsMdPath = join(workspacePath, 'TOOLS.md');
|
|
216
|
+
if (existsSync(toolsMdPath)) {
|
|
217
|
+
const toolsContent = await fsp.readFile(toolsMdPath, 'utf8');
|
|
218
|
+
const cleaned = removeManagedBlock(toolsContent, 'STEALTH_BROWSER_GUIDE');
|
|
219
|
+
if (cleaned !== toolsContent) await fsp.writeFile(toolsMdPath, cleaned, 'utf8');
|
|
220
|
+
}
|
|
221
|
+
sanitized += 1;
|
|
222
|
+
}
|
|
223
|
+
if (sanitized > 0) {
|
|
224
|
+
sendLog(`[browser] Sanitized ${sanitized} workspace(s): browser assets are in plugin-skills/browser-automation; web_search remains responsible for search.`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
46
227
|
|
|
47
228
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
48
229
|
const WEB_DIR = resolve(__dirname, '../web');
|
|
@@ -114,13 +295,39 @@ function extractCompletePngBase64(stdout) {
|
|
|
114
295
|
return b64;
|
|
115
296
|
}
|
|
116
297
|
|
|
117
|
-
function detectOs() {
|
|
118
|
-
const platform = process.platform;
|
|
119
|
-
if (platform === 'win32') return 'win';
|
|
120
|
-
if (platform === 'darwin') return 'macos';
|
|
121
|
-
if (platform === 'linux') return os.release().toLowerCase().includes('microsoft') ? 'linux-desktop' : 'linux-desktop';
|
|
122
|
-
return 'linux-desktop';
|
|
123
|
-
}
|
|
298
|
+
function detectOs() {
|
|
299
|
+
const platform = process.platform;
|
|
300
|
+
if (platform === 'win32') return 'win';
|
|
301
|
+
if (platform === 'darwin') return 'macos';
|
|
302
|
+
if (platform === 'linux') return os.release().toLowerCase().includes('microsoft') ? 'linux-desktop' : 'linux-desktop';
|
|
303
|
+
return 'linux-desktop';
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function normalizeHostOs(value = '') {
|
|
307
|
+
const v = String(value || '').trim().toLowerCase();
|
|
308
|
+
if (['win', 'windows', 'win32'].includes(v)) return 'win';
|
|
309
|
+
if (['mac', 'macos', 'darwin'].includes(v)) return 'macos';
|
|
310
|
+
if (['vps', 'server'].includes(v)) return 'vps';
|
|
311
|
+
if (['linux', 'linux-desktop', 'ubuntu', 'debian'].includes(v)) return 'linux-desktop';
|
|
312
|
+
return '';
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function resolveProjectHostOs(projectDir = '') {
|
|
316
|
+
try {
|
|
317
|
+
const cfgPath = join(projectDir || '', '.openclaw', 'openclaw.json');
|
|
318
|
+
if (existsSync(cfgPath)) {
|
|
319
|
+
const cfg = JSON.parse(await fsp.readFile(cfgPath, 'utf8'));
|
|
320
|
+
const pluginEntries = cfg.plugins?.entries || {};
|
|
321
|
+
const browserEntry = pluginEntries['browser-automation'] || pluginEntries['openclaw-browser-automation'] || {};
|
|
322
|
+
const fromConfig = normalizeHostOs(browserEntry.config?.hostOs || cfg.meta?.osChoice || cfg.meta?.hostOs);
|
|
323
|
+
if (fromConfig) return fromConfig;
|
|
324
|
+
}
|
|
325
|
+
} catch {}
|
|
326
|
+
const fromState = normalizeHostOs(state.os);
|
|
327
|
+
if (fromState) return fromState;
|
|
328
|
+
if (/^[A-Za-z]:[\\/]/.test(String(projectDir || ''))) return 'win';
|
|
329
|
+
return detectOs();
|
|
330
|
+
}
|
|
124
331
|
|
|
125
332
|
function getRealHomedir() {
|
|
126
333
|
const home = os.homedir();
|
|
@@ -1608,8 +1815,9 @@ async function syncDockerInfra(projectDir, force = false) {
|
|
|
1608
1815
|
const botContainer = parseComposeServiceContainerName(compose, 'ai-bot') || `openclaw-${slugify(basename(projectDir))}`;
|
|
1609
1816
|
const routerContainer = parseComposeServiceContainerName(compose, '9router') || `9router-${slugify(basename(projectDir))}`;
|
|
1610
1817
|
const composeName = (compose.match(/^name:\s*(\S+)/m) || [])[1] || `oc-${slugify(basename(projectDir))}`;
|
|
1611
|
-
const gatewayPort = state.gatewayPort || 18789;
|
|
1612
|
-
const routerPort = state.routerPort || 20128;
|
|
1818
|
+
const gatewayPort = state.gatewayPort || 18789;
|
|
1819
|
+
const routerPort = state.routerPort || 20128;
|
|
1820
|
+
const osChoice = await resolveProjectHostOs(projectDir);
|
|
1613
1821
|
|
|
1614
1822
|
// Detect features from openclaw.json
|
|
1615
1823
|
const cfgPath = join(projectDir, '.openclaw', 'openclaw.json');
|
|
@@ -1624,9 +1832,10 @@ async function syncDockerInfra(projectDir, force = false) {
|
|
|
1624
1832
|
is9Router: true,
|
|
1625
1833
|
openClawNpmSpec: OPENCLAW_NPM_SPEC,
|
|
1626
1834
|
openClawRuntimePackages: '',
|
|
1627
|
-
allSkills: [],
|
|
1628
|
-
dockerfilePlugins: [],
|
|
1629
|
-
|
|
1835
|
+
allSkills: [],
|
|
1836
|
+
dockerfilePlugins: [],
|
|
1837
|
+
osChoice,
|
|
1838
|
+
gatewayPort,
|
|
1630
1839
|
routerPort,
|
|
1631
1840
|
singleComposeName: composeName,
|
|
1632
1841
|
singleAppContainerName: botContainer,
|
|
@@ -2109,32 +2318,32 @@ async function discoverProjects(rootProjectDir) {
|
|
|
2109
2318
|
return state.projects.slice(0, 20);
|
|
2110
2319
|
}
|
|
2111
2320
|
|
|
2112
|
-
async function resolveProjectDir(rootProjectDir, body = {}) {
|
|
2113
|
-
if (body.projectDir && existsSync(join(resolve(String(body.projectDir)), '.openclaw', 'openclaw.json'))) {
|
|
2114
|
-
state.projectDir = resolve(String(body.projectDir));
|
|
2115
|
-
await syncRuntimeState(state.projectDir);
|
|
2116
|
-
return state.projectDir;
|
|
2117
|
-
}
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
state.projectDir
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
await syncRuntimeState(state.projectDir);
|
|
2131
|
-
return state.projectDir;
|
|
2132
|
-
}
|
|
2133
|
-
const found = await findLatestProject(rootProjectDir);
|
|
2134
|
-
if (found) {
|
|
2135
|
-
await syncRuntimeState(found);
|
|
2136
|
-
await saveState(rootProjectDir);
|
|
2137
|
-
}
|
|
2321
|
+
async function resolveProjectDir(rootProjectDir, body = {}) {
|
|
2322
|
+
if (body.projectDir && existsSync(join(resolve(String(body.projectDir)), '.openclaw', 'openclaw.json'))) {
|
|
2323
|
+
state.projectDir = resolve(String(body.projectDir));
|
|
2324
|
+
await syncRuntimeState(state.projectDir);
|
|
2325
|
+
return state.projectDir;
|
|
2326
|
+
}
|
|
2327
|
+
if (state.projectDir && existsSync(join(state.projectDir, '.openclaw', 'openclaw.json'))) {
|
|
2328
|
+
await syncRuntimeState(state.projectDir);
|
|
2329
|
+
return state.projectDir;
|
|
2330
|
+
}
|
|
2331
|
+
await loadSavedState(rootProjectDir);
|
|
2332
|
+
if (state.projectDir && existsSync(join(state.projectDir, '.openclaw', 'openclaw.json'))) {
|
|
2333
|
+
await syncRuntimeState(state.projectDir);
|
|
2334
|
+
return state.projectDir;
|
|
2335
|
+
}
|
|
2336
|
+
const envProjectDir = process.env.OPENCLAW_PROJECT_DIR || (process.env.OPENCLAW_HOME ? dirname(process.env.OPENCLAW_HOME) : '');
|
|
2337
|
+
if (envProjectDir && existsSync(join(resolve(String(envProjectDir)), '.openclaw', 'openclaw.json'))) {
|
|
2338
|
+
state.projectDir = resolve(String(envProjectDir));
|
|
2339
|
+
await syncRuntimeState(state.projectDir);
|
|
2340
|
+
return state.projectDir;
|
|
2341
|
+
}
|
|
2342
|
+
const found = await findLatestProject(rootProjectDir);
|
|
2343
|
+
if (found) {
|
|
2344
|
+
await syncRuntimeState(found);
|
|
2345
|
+
await saveState(rootProjectDir);
|
|
2346
|
+
}
|
|
2138
2347
|
return state.projectDir;
|
|
2139
2348
|
}
|
|
2140
2349
|
|
|
@@ -2287,15 +2496,17 @@ async function applyFeatureToggle(projectDir, agentId, kind, id, enabled) {
|
|
|
2287
2496
|
cfg.plugins.allow = cfg.plugins.allow || [];
|
|
2288
2497
|
if (enabled) {
|
|
2289
2498
|
if (!cfg.plugins.allow.includes(existingKey)) cfg.plugins.allow.push(existingKey);
|
|
2290
|
-
} else {
|
|
2291
|
-
cfg.plugins.allow = cfg.plugins.allow.filter((x) => x !== existingKey);
|
|
2292
|
-
for (const a of cfg.agents.list) {
|
|
2293
|
-
const bf = await readWorkspaceText(projectDir, a, 'BROWSER.md');
|
|
2294
|
-
if (existsSync(bf.file)) await fsp.rm(bf.file, { force: true });
|
|
2295
|
-
const bt = await readWorkspaceText(projectDir, a, 'browser-tool.js');
|
|
2296
|
-
if (existsSync(bt.file)) await fsp.rm(bt.file, { force: true });
|
|
2297
|
-
|
|
2298
|
-
|
|
2499
|
+
} else {
|
|
2500
|
+
cfg.plugins.allow = cfg.plugins.allow.filter((x) => x !== existingKey);
|
|
2501
|
+
for (const a of cfg.agents.list) {
|
|
2502
|
+
const bf = await readWorkspaceText(projectDir, a, 'BROWSER.md');
|
|
2503
|
+
if (existsSync(bf.file)) await fsp.rm(bf.file, { force: true });
|
|
2504
|
+
const bt = await readWorkspaceText(projectDir, a, 'browser-tool.js');
|
|
2505
|
+
if (existsSync(bt.file)) await fsp.rm(bt.file, { force: true });
|
|
2506
|
+
const rel = workspaceRelForAgent(a, cfg, projectDir);
|
|
2507
|
+
await fsp.rm(join(projectDir, '.openclaw', rel, 'plugin-skills', 'browser-automation'), { recursive: true, force: true }).catch(() => {});
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2299
2510
|
|
|
2300
2511
|
// Write cfgPath early so syncDockerInfra reads the updated openclaw.json
|
|
2301
2512
|
await fsp.writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
|
|
@@ -2586,12 +2797,17 @@ async function installFeature(projectDir, agentId, kind, id) {
|
|
|
2586
2797
|
const cfg = ensureConfigShape(JSON.parse(await fsp.readFile(cfgPath, 'utf8')));
|
|
2587
2798
|
cfg.plugins = cfg.plugins || { entries: {} };
|
|
2588
2799
|
cfg.plugins.entries = cfg.plugins.entries || {};
|
|
2589
|
-
const existingKey = aliases.find((a) => cfg.plugins.entries[a]) || aliases[0];
|
|
2590
|
-
cfg.plugins.entries[existingKey] = cfg.plugins.entries[existingKey] || {};
|
|
2591
|
-
cfg.plugins.entries[existingKey].enabled = true;
|
|
2592
|
-
if (existingKey === '
|
|
2593
|
-
|
|
2594
|
-
|
|
2800
|
+
const existingKey = aliases.find((a) => cfg.plugins.entries[a]) || aliases[0];
|
|
2801
|
+
cfg.plugins.entries[existingKey] = cfg.plugins.entries[existingKey] || {};
|
|
2802
|
+
cfg.plugins.entries[existingKey].enabled = true;
|
|
2803
|
+
if (existingKey === 'browser-automation' || existingKey === 'openclaw-browser-automation') {
|
|
2804
|
+
cfg.plugins.entries[existingKey].config = Object.assign({}, cfg.plugins.entries[existingKey].config, {
|
|
2805
|
+
hostOs: await resolveProjectHostOs(projectDir),
|
|
2806
|
+
});
|
|
2807
|
+
}
|
|
2808
|
+
if (existingKey === 'zalo-mod' || existingKey === 'openclaw-zalo-mod') {
|
|
2809
|
+
ensureZaloModPluginConfig(cfg.plugins.entries[existingKey], cfg);
|
|
2810
|
+
}
|
|
2595
2811
|
// Only add the canonical config key to allow list (not all aliases)
|
|
2596
2812
|
if (!cfg.plugins.allow.includes(existingKey)) cfg.plugins.allow.push(existingKey);
|
|
2597
2813
|
await fsp.writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
|
|
@@ -2619,14 +2835,15 @@ async function installFeature(projectDir, agentId, kind, id) {
|
|
|
2619
2835
|
}
|
|
2620
2836
|
}
|
|
2621
2837
|
|
|
2622
|
-
// Browser-automation plugin needs Docker rebuild for Playwright/Chromium deps
|
|
2623
|
-
const isBrowserPlugin = id === 'openclaw-browser-automation' || id === 'browser-automation';
|
|
2624
|
-
if (isBrowserPlugin && composeDir) {
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2838
|
+
// Browser-automation plugin needs Docker rebuild for Playwright/Chromium deps
|
|
2839
|
+
const isBrowserPlugin = id === 'openclaw-browser-automation' || id === 'browser-automation';
|
|
2840
|
+
if (isBrowserPlugin && composeDir) {
|
|
2841
|
+
await patchBrowserAutomationHostPreference(projectDir, aliases, sendLog);
|
|
2842
|
+
sendLog(`[plugin] Browser plugin requires Docker rebuild for Playwright/Chromium...`);
|
|
2843
|
+
const svcName = getBotServiceName(projectDir);
|
|
2844
|
+
await run('docker', ['compose', '-f', join(composeDir, 'docker-compose.yml'), 'up', '-d', '--build', '--force-recreate', svcName], { shell: false }).catch((err) => {
|
|
2845
|
+
sendLog(`[plugin] Docker rebuild failed: ${err.message}. Falling back to restart...`);
|
|
2846
|
+
return run('docker', ['restart', botContainer], { shell: false });
|
|
2630
2847
|
});
|
|
2631
2848
|
} else if (isZaloMod && composeDir) {
|
|
2632
2849
|
// Use docker compose up to apply new port mappings from docker-compose.yml
|
|
@@ -2662,26 +2879,37 @@ async function installFeature(projectDir, agentId, kind, id) {
|
|
|
2662
2879
|
|
|
2663
2880
|
// Automatically enable it in config after install
|
|
2664
2881
|
const cfgPath = join(projectDir, '.openclaw', 'openclaw.json');
|
|
2665
|
-
if (existsSync(cfgPath)) {
|
|
2666
|
-
const cfg = ensureConfigShape(JSON.parse(await fsp.readFile(cfgPath, 'utf8')));
|
|
2667
|
-
cfg.plugins = cfg.plugins || { entries: {} };
|
|
2668
|
-
cfg.plugins.entries = cfg.plugins.entries || {};
|
|
2669
|
-
const pluginAliasMap = {
|
|
2670
|
-
'openclaw-
|
|
2671
|
-
'openclaw-
|
|
2672
|
-
'openclaw-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
const
|
|
2676
|
-
|
|
2677
|
-
cfg.plugins.entries[existingKey]
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2882
|
+
if (existsSync(cfgPath)) {
|
|
2883
|
+
const cfg = ensureConfigShape(JSON.parse(await fsp.readFile(cfgPath, 'utf8')));
|
|
2884
|
+
cfg.plugins = cfg.plugins || { entries: {} };
|
|
2885
|
+
cfg.plugins.entries = cfg.plugins.entries || {};
|
|
2886
|
+
const pluginAliasMap = {
|
|
2887
|
+
'openclaw-browser-automation': ['browser-automation', 'openclaw-browser-automation'],
|
|
2888
|
+
'openclaw-zalo-mod': ['zalo-mod', 'openclaw-zalo-mod'],
|
|
2889
|
+
'openclaw-facebook-crawler': ['openclaw-facebook-crawler', 'openclaw-n8n-facebook-crawler', 'n8n-facebook-crawler'],
|
|
2890
|
+
'openclaw-n8n-facebook-poster': ['openclaw-n8n-facebook-poster', 'openclaw-facebook-poster', 'facebook-poster'],
|
|
2891
|
+
};
|
|
2892
|
+
const aliases = pluginAliasMap[id] || [id];
|
|
2893
|
+
const existingKey = aliases.find((a) => cfg.plugins.entries[a]) || aliases[0];
|
|
2894
|
+
cfg.plugins.entries[existingKey] = cfg.plugins.entries[existingKey] || {};
|
|
2895
|
+
cfg.plugins.entries[existingKey].enabled = true;
|
|
2896
|
+
if (existingKey === 'browser-automation' || existingKey === 'openclaw-browser-automation') {
|
|
2897
|
+
cfg.plugins.entries[existingKey].config = Object.assign({}, cfg.plugins.entries[existingKey].config, {
|
|
2898
|
+
hostOs: await resolveProjectHostOs(projectDir),
|
|
2899
|
+
});
|
|
2900
|
+
}
|
|
2901
|
+
if (existingKey === 'zalo-mod' || existingKey === 'openclaw-zalo-mod') {
|
|
2902
|
+
ensureZaloModPluginConfig(cfg.plugins.entries[existingKey], cfg);
|
|
2903
|
+
}
|
|
2904
|
+
await fsp.writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
if (id === 'openclaw-browser-automation' || id === 'browser-automation') {
|
|
2908
|
+
const aliases = ['browser-automation', 'openclaw-browser-automation'];
|
|
2909
|
+
await patchBrowserAutomationHostPreference(projectDir, aliases, sendLog);
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2685
2913
|
return { ok: true };
|
|
2686
2914
|
}
|
|
2687
2915
|
|
|
@@ -2955,10 +3183,10 @@ async function handler(req, res, rootProjectDir) {
|
|
|
2955
3183
|
const body = await readJson(req);
|
|
2956
3184
|
return json(res, await connectPickedProject(body.projectName, rootProjectDir));
|
|
2957
3185
|
}
|
|
2958
|
-
if (url.pathname === '/api/bot/status' && req.method === 'GET') {
|
|
2959
|
-
await resolveProjectDir(rootProjectDir);
|
|
2960
|
-
return json(res, await buildBotStatus());
|
|
2961
|
-
}
|
|
3186
|
+
if (url.pathname === '/api/bot/status' && req.method === 'GET') {
|
|
3187
|
+
await resolveProjectDir(rootProjectDir, Object.fromEntries(url.searchParams));
|
|
3188
|
+
return json(res, await buildBotStatus());
|
|
3189
|
+
}
|
|
2962
3190
|
if (url.pathname === '/api/bot/credentials' && req.method === 'PUT') {
|
|
2963
3191
|
const body = await readJson(req);
|
|
2964
3192
|
const projectDir = await resolveProjectDir(rootProjectDir, body);
|
|
@@ -3085,29 +3313,34 @@ async function handler(req, res, rootProjectDir) {
|
|
|
3085
3313
|
});
|
|
3086
3314
|
return json(res, { ok: true, message: 'Zalo login initiated. QR will appear in UI.' });
|
|
3087
3315
|
}
|
|
3088
|
-
if (url.pathname.startsWith('/api/bot/') && req.method === 'DELETE' && !url.pathname.startsWith('/api/bot/files/')) {
|
|
3089
|
-
const agentId = decodeURIComponent(url.pathname.replace('/api/bot/', ''));
|
|
3090
|
-
const projectDir = await resolveProjectDir(rootProjectDir);
|
|
3091
|
-
const result = await deleteBotInProject(projectDir, agentId);
|
|
3092
|
-
sendLog(`? Bot deleted: ${agentId}`);
|
|
3093
|
-
await recreateDockerBot(projectDir).catch((err) => sendLog(`[docker] recreate skipped/failed: ${err.message}`));
|
|
3094
|
-
return json(res, result);
|
|
3095
|
-
}
|
|
3096
|
-
if (url.pathname === '/api/bot/files' && req.method === 'GET') {
|
|
3097
|
-
await resolveProjectDir(rootProjectDir);
|
|
3098
|
-
return json(res, { files: state.projectDir ? await listMarkdownFiles(state.projectDir, url.searchParams.get('agentId') || '') : [] });
|
|
3099
|
-
}
|
|
3100
|
-
if (url.pathname.startsWith('/api/bot/files/')
|
|
3101
|
-
const name = decodeURIComponent(url.pathname.replace('/api/bot/files/', ''));
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3316
|
+
if (url.pathname.startsWith('/api/bot/') && req.method === 'DELETE' && !url.pathname.startsWith('/api/bot/files/')) {
|
|
3317
|
+
const agentId = decodeURIComponent(url.pathname.replace('/api/bot/', ''));
|
|
3318
|
+
const projectDir = await resolveProjectDir(rootProjectDir, Object.fromEntries(url.searchParams));
|
|
3319
|
+
const result = await deleteBotInProject(projectDir, agentId);
|
|
3320
|
+
sendLog(`? Bot deleted: ${agentId}`);
|
|
3321
|
+
await recreateDockerBot(projectDir).catch((err) => sendLog(`[docker] recreate skipped/failed: ${err.message}`));
|
|
3322
|
+
return json(res, result);
|
|
3323
|
+
}
|
|
3324
|
+
if (url.pathname === '/api/bot/files' && req.method === 'GET') {
|
|
3325
|
+
await resolveProjectDir(rootProjectDir, Object.fromEntries(url.searchParams));
|
|
3326
|
+
return json(res, { files: state.projectDir ? await listMarkdownFiles(state.projectDir, url.searchParams.get('agentId') || '') : [] });
|
|
3327
|
+
}
|
|
3328
|
+
if (url.pathname.startsWith('/api/bot/files/')) {
|
|
3329
|
+
const name = decodeURIComponent(url.pathname.replace('/api/bot/files/', ''));
|
|
3330
|
+
if (req.method === 'GET') {
|
|
3331
|
+
const projectDir = await resolveProjectDir(rootProjectDir, Object.fromEntries(url.searchParams));
|
|
3332
|
+
const file = safeJoin(projectDir, name);
|
|
3333
|
+
return json(res, { name, content: await fsp.readFile(file, 'utf8') });
|
|
3334
|
+
}
|
|
3335
|
+
if (req.method === 'PUT') {
|
|
3336
|
+
if (!name.endsWith('.md')) throw httpError(400, 'Only markdown files (.md) can be modified');
|
|
3337
|
+
const body = await readJson(req);
|
|
3338
|
+
const projectDir = await resolveProjectDir(rootProjectDir, body);
|
|
3339
|
+
const file = safeJoin(projectDir, name);
|
|
3340
|
+
await fsp.writeFile(file, String(body.content || ''), 'utf8');
|
|
3341
|
+
return json(res, { ok: true });
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3111
3344
|
if (url.pathname === '/api/catalog' && req.method === 'GET') return json(res, {
|
|
3112
3345
|
skills: [
|
|
3113
3346
|
{ name: 'Browser', slug: 'browser' },
|
|
@@ -3122,20 +3355,20 @@ async function handler(req, res, rootProjectDir) {
|
|
|
3122
3355
|
{ name: 'openclaw-n8n-facebook-poster', package: 'openclaw-n8n-facebook-poster' },
|
|
3123
3356
|
]
|
|
3124
3357
|
});
|
|
3125
|
-
if (url.pathname === '/api/features' && req.method === 'GET') {
|
|
3126
|
-
const projectDir = await resolveProjectDir(rootProjectDir);
|
|
3127
|
-
return json(res, await getFeatureFlags(projectDir, url.searchParams.get('agentId') || ''));
|
|
3128
|
-
}
|
|
3129
|
-
if (url.pathname === '/api/features/toggle' && req.method === 'POST') {
|
|
3130
|
-
const body = await readJson(req);
|
|
3131
|
-
const projectDir = await resolveProjectDir(rootProjectDir);
|
|
3132
|
-
return json(res, await applyFeatureToggle(projectDir, body.agentId || '', body.kind, body.id, !!body.enabled));
|
|
3133
|
-
}
|
|
3134
|
-
if (url.pathname === '/api/features/install' && req.method === 'POST') {
|
|
3135
|
-
const body = await readJson(req);
|
|
3136
|
-
const projectDir = await resolveProjectDir(rootProjectDir);
|
|
3137
|
-
return json(res, await installFeature(projectDir, body.agentId || '', body.kind, body.id));
|
|
3138
|
-
}
|
|
3358
|
+
if (url.pathname === '/api/features' && req.method === 'GET') {
|
|
3359
|
+
const projectDir = await resolveProjectDir(rootProjectDir, Object.fromEntries(url.searchParams));
|
|
3360
|
+
return json(res, await getFeatureFlags(projectDir, url.searchParams.get('agentId') || ''));
|
|
3361
|
+
}
|
|
3362
|
+
if (url.pathname === '/api/features/toggle' && req.method === 'POST') {
|
|
3363
|
+
const body = await readJson(req);
|
|
3364
|
+
const projectDir = await resolveProjectDir(rootProjectDir, body);
|
|
3365
|
+
return json(res, await applyFeatureToggle(projectDir, body.agentId || '', body.kind, body.id, !!body.enabled));
|
|
3366
|
+
}
|
|
3367
|
+
if (url.pathname === '/api/features/install' && req.method === 'POST') {
|
|
3368
|
+
const body = await readJson(req);
|
|
3369
|
+
const projectDir = await resolveProjectDir(rootProjectDir, body);
|
|
3370
|
+
return json(res, await installFeature(projectDir, body.agentId || '', body.kind, body.id));
|
|
3371
|
+
}
|
|
3139
3372
|
if (await serveStatic(req, res)) return;
|
|
3140
3373
|
json(res, { error: 'Not found' }, 404);
|
|
3141
3374
|
} catch (err) {
|
|
@@ -91,7 +91,11 @@
|
|
|
91
91
|
}));
|
|
92
92
|
|
|
93
93
|
const cfg = {
|
|
94
|
-
meta: {
|
|
94
|
+
meta: {
|
|
95
|
+
lastTouchedVersion: (_common.OPENCLAW_NPM_SPEC || 'latest').replace('openclaw@', ''),
|
|
96
|
+
osChoice,
|
|
97
|
+
deployMode,
|
|
98
|
+
},
|
|
95
99
|
agents: {
|
|
96
100
|
defaults: {
|
|
97
101
|
model: { primary: model, fallbacks: [] },
|
|
@@ -221,11 +221,12 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
|
|
|
221
221
|
singleOllamaContainerName = 'ollama',
|
|
222
222
|
multiOllamaContainerName = 'ollama-multibot',
|
|
223
223
|
plainSingleExtraHosts = false,
|
|
224
|
-
multiOllamaNumParallel = 1,
|
|
225
|
-
singleOllamaNumParallel = 1,
|
|
226
|
-
gatewayPort = 18789,
|
|
227
|
-
routerPort = 20128,
|
|
228
|
-
|
|
224
|
+
multiOllamaNumParallel = 1,
|
|
225
|
+
singleOllamaNumParallel = 1,
|
|
226
|
+
gatewayPort = 18789,
|
|
227
|
+
routerPort = 20128,
|
|
228
|
+
osChoice = '',
|
|
229
|
+
} = options;
|
|
229
230
|
const skillLines = dockerfileSkillInstallMode === 'build' && allSkills.length > 0
|
|
230
231
|
? `\n# Install skills (ClawHub)\n${allSkills.map((skill) => `RUN openclaw skills install ${skill} || echo "Warning: Failed to install ${skill} due to rate limits."`).join('\n')}\n`
|
|
231
232
|
: '';
|
|
@@ -355,7 +356,7 @@ CMD ["/bin/sh", "/usr/local/bin/openclaw-entrypoint.sh"]`;
|
|
|
355
356
|
const docker9RouterEntrypointScript = build9RouterComposeEntrypointScript(routerPort);
|
|
356
357
|
const extraHostsBlock = ` extra_hosts:\n - "host.docker.internal:host-gateway"`;
|
|
357
358
|
|
|
358
|
-
const appEnvironmentBlock = ` environment:\n - HOME=/home/node/project/.openclaw\n - OPENCLAW_HOME=/home/node/project/.openclaw\n - OPENCLAW_STATE_DIR=/home/node/project/.openclaw\n - OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1\n - OPENCLAW_GATEWAY_PORT=${gatewayPort}\n - OPENCLAW_PORT=${gatewayPort}\n tmpfs:\n - /home/node/project/.openclaw/plugin-runtime-deps\n`;
|
|
359
|
+
const appEnvironmentBlock = ` environment:\n - HOME=/home/node/project/.openclaw\n - OPENCLAW_HOME=/home/node/project/.openclaw\n - OPENCLAW_STATE_DIR=/home/node/project/.openclaw\n - OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1\n - OPENCLAW_SETUP_OS=${osChoice || ''}\n - OPENCLAW_BROWSER_HOST_OS=${osChoice || ''}\n - OPENCLAW_GATEWAY_PORT=${gatewayPort}\n - OPENCLAW_PORT=${gatewayPort}\n tmpfs:\n - /home/node/project/.openclaw/plugin-runtime-deps\n`;
|
|
359
360
|
|
|
360
361
|
let compose;
|
|
361
362
|
if (isMultiBot) {
|
|
@@ -330,79 +330,61 @@ description: Startup and initialization instructions
|
|
|
330
330
|
- [Operating Manual](./AGENTS.md)`;
|
|
331
331
|
}
|
|
332
332
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
---
|
|
339
|
-
|
|
340
|
-
# ⏰ Cron / Lên lịch nhắc nhở (tool: \`cron\`)
|
|
341
|
-
- **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).
|
|
342
|
-
- **⛔ TUYỆT ĐỐI KHÔNG sửa trực tiếp file JSON** như \`jobs.json\`, \`jobs-state.json\` trong thư mục \`.openclaw/cron/\`. Dữ liệu cron được lưu trong SQLite database, file JSON chỉ là legacy format đã ngưng hỗ trợ. Mọi thao tác PHẢI thông qua tool \`cron\`.
|
|
343
|
-
- **Khi tạo cronjob mới (action \`add\`):**
|
|
344
|
-
- **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 đó.
|
|
345
|
-
- 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.
|
|
346
|
-
- **Session:** Luôn dùng \`sessionTarget: "isolated"\` cho các job chạy nền (báo cáo, nhắc nhở, gửi tin nhắn tự động). Chỉ dùng \`"main"\` cho system event/reminder ngắn.
|
|
347
|
-
- **Timezone:** Luôn chỉ định timezone rõ ràng bằng trường \`tz\` (ví dụ: \`"Asia/Ho_Chi_Minh"\`). Nếu không chỉ định, hệ thống sẽ dùng timezone của Gateway host (thường là UTC) và job sẽ chạy sai giờ.
|
|
348
|
-
- **Delivery:** Đối với job cần gửi kết quả ra chat, set \`delivery.mode: "announce"\` kèm \`delivery.channel\` và \`delivery.to\`.
|
|
349
|
-
- **Khi user yêu cầu tắt/bật/xóa cronjob:**
|
|
350
|
-
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.
|
|
351
|
-
2. **Bước 2 (Xử lý):**
|
|
352
|
-
- Để xóa: Gọi action \`remove\` với \`id\` tìm được.
|
|
353
|
-
- Để tắt/tạm dừng: Gọi action \`update\` với \`id\` và patch \`{"enabled": false}\`.
|
|
354
|
-
- Để bật lại: Gọi action \`update\` với \`id\` và patch \`{"enabled": true}\`.
|
|
355
|
-
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ế.
|
|
356
|
-
- 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.
|
|
357
|
-
- Khi thao tác tool cho cron/scheduler, **không điền \`current\` vào thư mục Session**.
|
|
358
|
-
- **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ố thích hợp trước ID của group. Với kênh Telegram/Matrix/Discord/Slack, dùng tiền tố \`group:\` (ví dụ: \`group:123456\`). RIÊNG với kênh Zalo (\`zalouser\`), **bắt buộc** phải sử dụng tiền tố \`g:\` (ví dụ: \`g:3815464776067464419\`) để tránh bị OpenClaw core lược bỏ tiền tố và gửi nhầm vào DM chat cá nhân.
|
|
359
|
-
- **One-shot job:** Dùng schedule kind \`"at"\` với ISO 8601 timestamp. Job sẽ tự xóa sau khi chạy thành công trừ khi set \`deleteAfterRun: false\`.
|
|
360
|
-
- 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.`;
|
|
361
|
-
}
|
|
362
|
-
return `---
|
|
363
|
-
name: cronjob
|
|
364
|
-
description: Schedule recurring tasks using the cron tool.
|
|
365
|
-
---
|
|
366
|
-
|
|
367
|
-
# ⏰ Cron / Scheduled Tasks (tool: \`cron\`)
|
|
368
|
-
- **Exact tool name:** The tool name is \`cron\` (never mistake it for \`native\` or external command lines).
|
|
369
|
-
- **⛔ NEVER edit JSON files directly** such as \`jobs.json\` or \`jobs-state.json\` in \`.openclaw/cron/\`. Cron data is stored in SQLite database; JSON files are legacy format no longer supported. All operations MUST go through the \`cron\` tool.
|
|
370
|
-
- **When creating a new cronjob (action \`add\`):**
|
|
371
|
-
- **ABSOLUTELY DO NOT specify the \`agentId\` field** in the \`job\` object (leave this field omitted). The OpenClaw system will automatically assign your correct agent ID to that job.
|
|
372
|
-
- Never manually specify \`agentId\` as \`"bot"\` or \`"main"\`, as this will cause the cronjob to belong to another agent and you will lose control to manage/delete it later.
|
|
373
|
-
- **Session:** Always use \`sessionTarget: "isolated"\` for background jobs (reports, reminders, automated messages). Only use \`"main"\` for short system events/reminders.
|
|
374
|
-
- **Timezone:** Always specify timezone explicitly via the \`tz\` field (e.g., \`"Asia/Ho_Chi_Minh"\`). If omitted, the system uses the Gateway host timezone (often UTC) and the job will run at the wrong time.
|
|
375
|
-
- **Delivery:** For jobs that should send results to chat, set \`delivery.mode: "announce"\` with \`delivery.channel\` and \`delivery.to\`.
|
|
376
|
-
- **When the user requests to disable/enable/delete a cronjob:**
|
|
377
|
-
1. **Step 1 (Search):** Call the \`cron\` tool with action \`list\` (and \`includeDisabled: true\`) to view all cron jobs on the system and find the matching \`jobId\`.
|
|
378
|
-
2. **Step 2 (Processing):**
|
|
379
|
-
- To delete: Call action \`remove\` with the \`id\` found.
|
|
380
|
-
- To disable/pause: Call action \`update\` with \`id\` and patch \`{"enabled": false}\`.
|
|
381
|
-
- To enable: Call action \`update\` with \`id\` and patch \`{"enabled": true}\`.
|
|
382
|
-
3. **Honest statement:** Never claim a job is "deleted" or "not found" based on guessing without calling the \`cron\` tool to verify the actual state.
|
|
383
|
-
- When the user asks to schedule tasks or reminders, use the built-in \`cron\` tool (action \`add\`) automatically. Do NOT ask users to run crontab or Task Scheduler manually on the host.
|
|
384
|
-
- When operating cron/scheduler tools, do **not** put \`current\` into the Session directory.
|
|
385
|
-
- **IMPORTANT ABOUT GROUP CHAT TARGETING**: When creating or configuring a cron job to send messages (announce mode) to a Group Chat, the value of the \`delivery.to\` field **must** use the appropriate prefix before the group ID. For Telegram/Matrix/Discord/Slack, use the \`group:\` prefix (e.g., \`group:123456\`). ESPECIALLY for Zalo (\`zalouser\`), you **must** use the \`g:\` prefix (e.g., \`g:3815464776067464419\`) to prevent the OpenClaw core from stripping the prefix and misrouting the message to a private DM.
|
|
386
|
-
- **One-shot jobs:** Use schedule kind \`"at"\` with an ISO 8601 timestamp. The job auto-deletes after successful run unless \`deleteAfterRun: false\` is set.
|
|
387
|
-
- Skip internal doc lookups such as \`cron-jobs.mdx\`; rely on the available tools and complete the scheduling task directly.`;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
function buildInfographicGeneratorSkillMd(botName = 'Williams') {
|
|
391
|
-
return '';
|
|
392
|
-
}
|
|
333
|
+
function buildCronjobSkillMd(isVi = true) {
|
|
334
|
+
return `---
|
|
335
|
+
name: cronjob
|
|
336
|
+
description: Lên lịch tác vụ định kỳ sử dụng công cụ cron.
|
|
337
|
+
---
|
|
393
338
|
|
|
394
|
-
|
|
395
|
-
return '';
|
|
396
|
-
}
|
|
339
|
+
# ⏰ Lập lịch & Nhắc nhở (tool: \`cron\`)
|
|
397
340
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
341
|
+
Sử dụng công cụ \`cron\` (không chạy command line ngoài, không sửa trực tiếp tệp JSON).
|
|
342
|
+
|
|
343
|
+
## 1. Tạo Lịch Nhắc Nhở Mới (action: \`add\`)
|
|
344
|
+
Truyền tham số \`job\` (object) gồm:
|
|
345
|
+
- **\`agentId\`**: Bỏ qua (không truyền). Hệ thống tự gán.
|
|
346
|
+
- **\`sessionTarget\`**: \`"isolated"\` (cho chạy nền) hoặc \`"main"\`.
|
|
347
|
+
- **\`wakeMode\`**: \`"now"\`.
|
|
348
|
+
- **\`schedule\`**:
|
|
349
|
+
- \`kind\`: \`"cron"\` (lặp lại) hoặc \`"at"\` (một lần).
|
|
350
|
+
- \`expr\`: Biểu thức cron (ví dụ: \`"0 7 * * *"\`).
|
|
351
|
+
- \`tz\`: Bắt buộc điền múi giờ, ví dụ: \`"Asia/Ho_Chi_Minh"\`.
|
|
352
|
+
- **\`payload\`**:
|
|
353
|
+
- \`kind\`: \`"agentTurn"\`.
|
|
354
|
+
- \`message\`: Nội dung tin nhắn nhắc nhở.
|
|
355
|
+
- **\`delivery\`**:
|
|
356
|
+
- \`mode\`: \`"announce"\`.
|
|
357
|
+
- \`channel\`: \`"zalouser"\`.
|
|
358
|
+
- \`to\`: ID người nhận hoặc ID nhóm.
|
|
359
|
+
- ⚠️ **QUAN TRỌNG:** Nếu gửi tới Group Zalo, ID nhóm bắt buộc phải thêm tiền tố **\`g:\`** ở đầu (Ví dụ: \`g:1925989252066183028\`). Nếu thiếu \`g:\`, tin nhắn sẽ bị gửi nhầm thành tin cá nhân (DM) hoặc lỗi.
|
|
360
|
+
|
|
361
|
+
## 2. Tìm kiếm, Tắt, Bật, Xóa Lịch
|
|
362
|
+
- **Xem danh sách:** Gọi \`cron\` với action \`list\` (kèm \`includeDisabled: true\`). Tìm \`id\` phù hợp.
|
|
363
|
+
- **Xóa job:** Gọi action \`remove\` với \`id\`.
|
|
364
|
+
- **Tắt job:** Gọi action \`update\` với \`id\` và patch \`{"enabled": false}\`.
|
|
365
|
+
- **Bật job:** Gọi action \`update\` với \`id\` and patch \`{"enabled": true}\`.
|
|
401
366
|
|
|
402
|
-
|
|
403
|
-
|
|
367
|
+
## 3. Chạy thử nghiệm ngay lập tức (Test Run)
|
|
368
|
+
- Sau khi thêm job, có thể chạy thử ngay lập tức để kiểm tra bằng cách gọi CLI trong container:
|
|
369
|
+
\`openclaw cron run <job_id> --wait\` (hoặc gọi tool \`cron\` với action \`run\` kèm \`id\`).`;
|
|
404
370
|
}
|
|
405
371
|
|
|
372
|
+
function buildInfographicGeneratorSkillMd(botName = 'Williams') {
|
|
373
|
+
return '';
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function buildInfographicGeneratorJs() {
|
|
377
|
+
return '';
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function buildStickerMentionSkillMd(botName = 'Williams') {
|
|
381
|
+
return '';
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function buildStickerMentionJs() {
|
|
385
|
+
return '';
|
|
386
|
+
}
|
|
387
|
+
|
|
406
388
|
function buildSecurityRules(isVi = true) {
|
|
407
389
|
if (isVi) {
|
|
408
390
|
return `\n\n## 🔐 Quy Tắc Bảo Mật — BẮT BUỘC (Red Lines)\n\n**GIỚI HẠN FILE & HỆ THỐNG:**\n- ✅ CHỈ làm việc trong thư mục project (workspace).\n- ❌ KHÔNG cung cấp file nội bộ của \`.openclaw\` (config.json, registry.json, task-memory.json, workspace-memory...)\n- ❌ KHÔNG đọc, sao chép, hoặc truy cập bất kỳ file nào ngoài thư mục project\n- ❌ KHÔNG quét hoặc liệt kê các thư mục hệ thống: Documents, Desktop, Downloads, AppData\n- ❌ KHÔNG truy cập registry, system32, hoặc Program Files\n- ❌ KHÔNG cài đặt phần mềm, driver, hoặc service ngoài Docker\n\n**API KEY & CREDENTIALS:**\n- ❌ KHÔNG BAO GIỜ hiển thị API key, token, hoặc mật khẩu trong chat\n- ❌ KHÔNG viết API key trực tiếp vào mã nguồn\n- ❌ KHÔNG commit file credentials lên Git\n- ✅ LUÔN lưu credentials trong file .env riêng\n- ✅ LUÔN dùng biến môi trường thay vì hardcode\n\n**VÍ CRYPTO & TÀI SẢN SỐ:**\n- ❌ TUYỆT ĐỐI KHÔNG truy cập, đọc, hoặc quét các thư mục ví crypto\n- ❌ KHÔNG quét clipboard (có thể chứa seed phrases)\n- ❌ KHÔNG truy cập browser profile, cookie, hoặc mật khẩu đã lưu\n- ❌ KHÔNG cài đặt npm package lạ (chỉ openclaw và plugin chính thức)\n\n**DOCKER:**\n- ✅ Chỉ mount đúng thư mục cần thiết (config + workspace)\n- ❌ KHÔNG mount nguyên ổ đĩa (C:/ hoặc D:/)\n- ❌ KHÔNG chạy container với --privileged\n- ✅ Giới hạn port expose (chỉ 18789)`;
|
package/dist/web/app.js
CHANGED
|
@@ -31,12 +31,25 @@ function staticChoiceCard(item) {
|
|
|
31
31
|
</div>`;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
async function api(path, opts) {
|
|
35
|
-
const res = await fetch(path, opts && opts.body ? { ...opts, headers: { 'content-type': 'application/json' }, body: JSON.stringify(opts.body) } : opts);
|
|
36
|
-
if (!res.ok) throw new Error((await res.json()).error || res.statusText);
|
|
37
|
-
return res.json();
|
|
38
|
-
}
|
|
39
|
-
function
|
|
34
|
+
async function api(path, opts) {
|
|
35
|
+
const res = await fetch(path, opts && opts.body ? { ...opts, headers: { 'content-type': 'application/json' }, body: JSON.stringify(opts.body) } : opts);
|
|
36
|
+
if (!res.ok) throw new Error((await res.json()).error || res.statusText);
|
|
37
|
+
return res.json();
|
|
38
|
+
}
|
|
39
|
+
function activeProjectDir() {
|
|
40
|
+
return state.selectedProjectDir || state.install?.projectDir || '';
|
|
41
|
+
}
|
|
42
|
+
function projectQuery(extra = {}) {
|
|
43
|
+
const params = new URLSearchParams();
|
|
44
|
+
const projectDir = activeProjectDir();
|
|
45
|
+
if (projectDir) params.set('projectDir', projectDir);
|
|
46
|
+
Object.entries(extra).forEach(([key, value]) => {
|
|
47
|
+
if (value !== undefined && value !== null && value !== '') params.set(key, value);
|
|
48
|
+
});
|
|
49
|
+
const query = params.toString();
|
|
50
|
+
return query ? `?${query}` : '';
|
|
51
|
+
}
|
|
52
|
+
function runtimeBadge(label, kind='') { return `<span class="mini-pill ${kind?`mini-pill--${kind}`:''}">${escapeHtml(label)}</span>`; }
|
|
40
53
|
async function withButtonLoading(btn, task) {
|
|
41
54
|
if (!btn || btn.classList.contains('is-loading')) return;
|
|
42
55
|
const prevDisabled = btn.disabled;
|
|
@@ -420,7 +433,7 @@ function renderFilesPanel() {
|
|
|
420
433
|
// Re-wire only file-panel-specific handlers
|
|
421
434
|
panel.querySelectorAll('[data-select-file]').forEach(btn => btn.onclick = () => { state.selectedFile = btn.dataset.selectFile; renderFilesPanel(); });
|
|
422
435
|
panel.querySelectorAll('[data-toggle-dir]').forEach(btn => btn.onclick = () => { const p = btn.dataset.toggleDir; state.openDirs[p] = !(state.openDirs[p] ?? true); renderFilesPanel(); });
|
|
423
|
-
panel.querySelectorAll('.save').forEach(btn => btn.onclick = () => withButtonLoading(btn, async () => { const name = btn.dataset.file; await api('/api/bot/files/'+encodeURIComponent(name), { method: 'PUT', body: { content: document.querySelector(`[data-editor="${CSS.escape(name)}"]`).value } }); showToast(t('Đã lưu', 'Saved'), t('Đã lưu tệp tin: ', 'Saved file: ') + fileBaseName(name), 'success'); btn.innerHTML=`${actionIcon('save')} ${ui('saved')}`; setTimeout(()=>btn.innerHTML=`${actionIcon('save')} ${ui('save')}`,1200); }));
|
|
436
|
+
panel.querySelectorAll('.save').forEach(btn => btn.onclick = () => withButtonLoading(btn, async () => { const name = btn.dataset.file; await api('/api/bot/files/'+encodeURIComponent(name), { method: 'PUT', body: { projectDir: activeProjectDir(), content: document.querySelector(`[data-editor="${CSS.escape(name)}"]`).value } }); showToast(t('Đã lưu', 'Saved'), t('Đã lưu tệp tin: ', 'Saved file: ') + fileBaseName(name), 'success'); btn.innerHTML=`${actionIcon('save')} ${ui('saved')}`; setTimeout(()=>btn.innerHTML=`${actionIcon('save')} ${ui('save')}`,1200); }));
|
|
424
437
|
}
|
|
425
438
|
|
|
426
439
|
function renderSkillsPanel() {
|
|
@@ -439,7 +452,7 @@ function wireSkillsHandlers(scope = document) {
|
|
|
439
452
|
state.featureLoading[key] = true;
|
|
440
453
|
renderSkillsPanel();
|
|
441
454
|
try {
|
|
442
|
-
await api('/api/features/toggle', { method:'POST', body:{ kind, id, enabled, agentId: currentBotId() } });
|
|
455
|
+
await api('/api/features/toggle', { method:'POST', body:{ projectDir: activeProjectDir(), kind, id, enabled, agentId: currentBotId() } });
|
|
443
456
|
await loadFeatureFlags(true);
|
|
444
457
|
if (kind === 'skill') await loadFiles(true);
|
|
445
458
|
} finally {
|
|
@@ -453,7 +466,7 @@ function wireSkillsHandlers(scope = document) {
|
|
|
453
466
|
state.featureLoading[key] = true;
|
|
454
467
|
renderSkillsPanel();
|
|
455
468
|
try {
|
|
456
|
-
await api('/api/features/install', { method:'POST', body:{ kind, id, agentId: currentBotId() } });
|
|
469
|
+
await api('/api/features/install', { method:'POST', body:{ projectDir: activeProjectDir(), kind, id, agentId: currentBotId() } });
|
|
457
470
|
await loadFeatureFlags(true);
|
|
458
471
|
await loadFiles(true);
|
|
459
472
|
const label = kind === 'skill' ? t('kỹ năng: ', 'skill: ') : t('plugin: ', 'plugin: ');
|
|
@@ -909,7 +922,7 @@ function wireTab() {
|
|
|
909
922
|
state.zaloLoginLines = [t('Đang chuẩn bị quét mã Zalo...', 'Preparing Zalo QR login...')];
|
|
910
923
|
render();
|
|
911
924
|
try {
|
|
912
|
-
await api('/api/zalo/login', { method: 'POST', body: { agentId: state.activeBotId } });
|
|
925
|
+
await api('/api/zalo/login', { method: 'POST', body: { projectDir: activeProjectDir(), agentId: state.activeBotId } });
|
|
913
926
|
} catch (err) {
|
|
914
927
|
state.zaloLoginOpen = false;
|
|
915
928
|
state.confirmModal = { title: t('Lỗi đăng nhập','Login error'), message: err.message, okText: t('Đóng','Close'), onConfirm: () => {} };
|
|
@@ -1016,12 +1029,12 @@ document.querySelectorAll('[data-project-pick-folder]').forEach(btn => btn.oncli
|
|
|
1016
1029
|
}
|
|
1017
1030
|
}));
|
|
1018
1031
|
document.querySelectorAll('[data-update-app]').forEach(btn => btn.onclick = () => withButtonLoading(btn, async () => {
|
|
1019
|
-
await api('/api/runtime/update', { method: 'POST', body: { target: 'openclaw' } });
|
|
1032
|
+
await api('/api/runtime/update', { method: 'POST', body: { projectDir: activeProjectDir(), target: 'openclaw' } });
|
|
1020
1033
|
await loadSystem();
|
|
1021
1034
|
await loadStatus();
|
|
1022
1035
|
}));
|
|
1023
1036
|
document.querySelectorAll('[data-update-router]').forEach(btn => btn.onclick = () => withButtonLoading(btn, async () => {
|
|
1024
|
-
await api('/api/runtime/update', { method: 'POST', body: { target: '9router' } });
|
|
1037
|
+
await api('/api/runtime/update', { method: 'POST', body: { projectDir: activeProjectDir(), target: '9router' } });
|
|
1025
1038
|
await loadSystem();
|
|
1026
1039
|
await loadStatus();
|
|
1027
1040
|
}));document.querySelectorAll('[data-project-remove]').forEach(btn => btn.onclick = (ev) => {
|
|
@@ -1070,7 +1083,7 @@ document.querySelectorAll('[data-project-pick-folder]').forEach(btn => btn.oncli
|
|
|
1070
1083
|
okText: t('Xóa bot','Delete bot'),
|
|
1071
1084
|
onConfirm: async () => {
|
|
1072
1085
|
try {
|
|
1073
|
-
await api('/api/bot/'+encodeURIComponent(id), { method: 'DELETE' });
|
|
1086
|
+
await api('/api/bot/'+encodeURIComponent(id) + projectQuery(), { method: 'DELETE' });
|
|
1074
1087
|
state.confirmModal = null;
|
|
1075
1088
|
showToast(t('Đã xóa bot', 'Bot deleted'), `${t('Đã xóa bot','Bot deleted')}: ${id}`, 'success');
|
|
1076
1089
|
if (state.activeBotId === id) { state.activeBotId = ''; state.selectedFile = ''; state.files = []; }
|
|
@@ -1106,7 +1119,7 @@ document.querySelectorAll('[data-project-pick-folder]').forEach(btn => btn.oncli
|
|
|
1106
1119
|
const submitBtn = ev.currentTarget.querySelector('button[type="submit"]');
|
|
1107
1120
|
withButtonLoading(submitBtn, async () => {
|
|
1108
1121
|
const nineRouterApiKey = ev.currentTarget.querySelector('[name="nineRouterApiKey"]')?.value || '';
|
|
1109
|
-
await api('/api/bot/credentials', { method: 'PUT', body: { nineRouterApiKey } });
|
|
1122
|
+
await api('/api/bot/credentials', { method: 'PUT', body: { projectDir: activeProjectDir(), nineRouterApiKey } });
|
|
1110
1123
|
const msg = ev.currentTarget.querySelector('[data-cred-msg]');
|
|
1111
1124
|
if (msg) msg.textContent = t('Đã lưu','Saved');
|
|
1112
1125
|
await loadStatus();
|
|
@@ -1181,9 +1194,10 @@ document.querySelectorAll('[data-project-pick-folder]').forEach(btn => btn.oncli
|
|
|
1181
1194
|
const submitBtn = ev.currentTarget.querySelector('button[type="submit"]');
|
|
1182
1195
|
if (submitBtn?.classList.contains('is-loading')) return;
|
|
1183
1196
|
await withButtonLoading(submitBtn, async () => {
|
|
1184
|
-
const fd = new FormData(ev.currentTarget);
|
|
1185
|
-
const body = Object.fromEntries(fd.entries());
|
|
1186
|
-
body.
|
|
1197
|
+
const fd = new FormData(ev.currentTarget);
|
|
1198
|
+
const body = Object.fromEntries(fd.entries());
|
|
1199
|
+
body.projectDir = activeProjectDir();
|
|
1200
|
+
body.channel = state.botChannel || 'telegram';
|
|
1187
1201
|
if (body.channel === 'zalo-personal') {
|
|
1188
1202
|
state.botModalOpen = false;
|
|
1189
1203
|
state.zaloLoginOpen = true;
|
|
@@ -1227,14 +1241,21 @@ document.querySelectorAll('[data-project-pick-folder]').forEach(btn => btn.oncli
|
|
|
1227
1241
|
}
|
|
1228
1242
|
});
|
|
1229
1243
|
});
|
|
1230
|
-
document.querySelectorAll('.save').forEach(btn => btn.onclick = () => withButtonLoading(btn, async () => { const name = btn.dataset.file; await api('/api/bot/files/'+encodeURIComponent(name), { method: 'PUT', body: { content: document.querySelector(`[data-editor="${CSS.escape(name)}"]`).value } }); showToast(t('Đã lưu', 'Saved'), t('Đã lưu tệp tin: ', 'Saved file: ') + fileBaseName(name), 'success'); btn.innerHTML=`${actionIcon('save')} ${ui('saved')}`; setTimeout(()=>btn.innerHTML=`${actionIcon('save')} ${ui('save')}`,1200); }));
|
|
1244
|
+
document.querySelectorAll('.save').forEach(btn => btn.onclick = () => withButtonLoading(btn, async () => { const name = btn.dataset.file; await api('/api/bot/files/'+encodeURIComponent(name), { method: 'PUT', body: { projectDir: activeProjectDir(), content: document.querySelector(`[data-editor="${CSS.escape(name)}"]`).value } }); showToast(t('Đã lưu', 'Saved'), t('Đã lưu tệp tin: ', 'Saved file: ') + fileBaseName(name), 'success'); btn.innerHTML=`${actionIcon('save')} ${ui('saved')}`; setTimeout(()=>btn.innerHTML=`${actionIcon('save')} ${ui('save')}`,1200); }));
|
|
1231
1245
|
wireSkillsHandlers(document);
|
|
1232
1246
|
}
|
|
1233
1247
|
|
|
1234
1248
|
function escapeHtml(s='') { return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
|
|
1235
1249
|
function fileBaseName(s='') { const clean = String(s).replace(/[\\/]+$/, ''); return clean.split(/[\\/]/).pop() || s; }
|
|
1236
|
-
async function loadSystem(silent=false){ state.system = await api('/api/system'); if (!silent) render(); }
|
|
1237
|
-
async function loadStatus(silent=false){
|
|
1250
|
+
async function loadSystem(silent=false){ state.system = await api('/api/system'); if (!silent) render(); }
|
|
1251
|
+
async function loadStatus(silent=false){
|
|
1252
|
+
const requestedProjectDir = activeProjectDir();
|
|
1253
|
+
const install = await api('/api/bot/status' + projectQuery());
|
|
1254
|
+
if (requestedProjectDir && install?.projectDir && install.projectDir !== requestedProjectDir) return;
|
|
1255
|
+
state.install = install;
|
|
1256
|
+
if (!state.selectedProjectDir && state.install?.projectDir) state.selectedProjectDir = state.install.projectDir;
|
|
1257
|
+
if (!silent) render();
|
|
1258
|
+
}
|
|
1238
1259
|
function autoSwitchBotChannel() {
|
|
1239
1260
|
const bots = state.install?.bots || [];
|
|
1240
1261
|
const ch = state.botChannel || 'telegram';
|
|
@@ -1255,14 +1276,14 @@ function currentBotId() {
|
|
|
1255
1276
|
}
|
|
1256
1277
|
return state.activeBotId || '';
|
|
1257
1278
|
}
|
|
1258
|
-
async function loadFiles(silent=false){
|
|
1259
|
-
const botId = currentBotId();
|
|
1260
|
-
if (!botId) { state.files = []; if (!silent) render(); return; }
|
|
1261
|
-
state.files = (await api('/api/bot/files' + (
|
|
1262
|
-
if (!silent) render();
|
|
1263
|
-
}
|
|
1264
|
-
async function loadCatalog(silent=false){ state.catalog = await api('/api/catalog'); if (!silent) render(); }
|
|
1265
|
-
async function loadFeatureFlags(silent=false){ const botId=currentBotId(); const data = (await api('/api/features' + (
|
|
1279
|
+
async function loadFiles(silent=false){
|
|
1280
|
+
const botId = currentBotId();
|
|
1281
|
+
if (!botId) { state.files = []; if (!silent) render(); return; }
|
|
1282
|
+
state.files = (await api('/api/bot/files' + projectQuery({ agentId: botId }))).files;
|
|
1283
|
+
if (!silent) render();
|
|
1284
|
+
}
|
|
1285
|
+
async function loadCatalog(silent=false){ state.catalog = await api('/api/catalog'); if (!silent) render(); }
|
|
1286
|
+
async function loadFeatureFlags(silent=false){ const botId=currentBotId(); const data = (await api('/api/features' + projectQuery({ agentId: botId }))) || {}; state.featureFlags = data.flags || {}; state.featureInstalled = data.installed || {}; state.featureVersions = data.versions || {}; if (!silent) render(); }
|
|
1266
1287
|
function appendLogLine(line) {
|
|
1267
1288
|
if (line.includes('Setup Wizard updated successfully! Please restart the installer.')) {
|
|
1268
1289
|
showToast(t('Đang khởi động lại UI', 'Restarting Setup UI'), t('Hệ thống đang tự khởi động lại để áp dụng phiên bản mới...', 'The system is restarting to apply the new version...'), 'info', 12000);
|