create-openclaw-bot 5.9.0 → 5.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/README.vi.md +2 -2
- package/dist/server/local-server.js +187 -6
- package/dist/setup/shared/bot-config-gen.js +15 -9
- package/dist/setup/shared/docker-gen.js +80 -83
- package/dist/web/app.js +24 -16
- package/dist/web/styles.css +101 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*Run one command → open the dashboard → your bot is live. Windows · macOS · Linux · VPS, on Docker or native.*
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
10
|
-
<a href="https://github.com/tuanminhhole/openclaw-setup/releases"><img src="https://img.shields.io/badge/RELEASE-v5.
|
|
10
|
+
<a href="https://github.com/tuanminhhole/openclaw-setup/releases"><img src="https://img.shields.io/badge/RELEASE-v5.10.0-0EA5E9?style=for-the-badge" alt="Version 5.10.0" /></a>
|
|
11
11
|
<a href="https://github.com/tuanminhhole/openclaw-setup?tab=MIT-1-ov-file"><img src="https://img.shields.io/badge/LICENSE-MIT-success?style=for-the-badge" alt="MIT License" /></a>
|
|
12
12
|
<a href="https://www.npmjs.com/package/create-openclaw-bot"><img src="https://img.shields.io/npm/v/create-openclaw-bot?style=for-the-badge&label=CLI&color=2563EB&logo=npm&logoColor=white" alt="NPM Version" /></a>
|
|
13
13
|
<a href="https://github.com/tuanminhhole/openclaw-setup/stargazers"><img src="https://img.shields.io/github/stars/tuanminhhole/openclaw-setup?style=for-the-badge&color=eab308&logo=github&logoColor=white" alt="GitHub Stars" /></a>
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
|
|
28
28
|
---
|
|
29
29
|
|
|
30
|
-
## 🆕 What's New in v5.
|
|
30
|
+
## 🆕 What's New in v5.10.0
|
|
31
31
|
|
|
32
32
|
- 🧠 **TencentDB Agent Memory — 1-click install**: New memory plugin in the UI. A 4-tier (L0–L3) layered-memory + context-compression engine that keeps long sessions coherent and **cuts token usage by up to ~61%**. Fully local (SQLite), no API key, Docker-ready.
|
|
33
33
|
- ⚡ **Token-lean defaults for every new bot**: Ships with context pruning (cache-TTL) + `safeguard` compaction — **cheaper & sharper long conversations** with zero tuning.
|
package/README.vi.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*Chạy một lệnh → mở dashboard → bot lên sóng. Windows · macOS · Linux · VPS, chạy Docker hoặc native.*
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
10
|
-
<a href="https://github.com/tuanminhhole/openclaw-setup/releases"><img src="https://img.shields.io/badge/RELEASE-v5.
|
|
10
|
+
<a href="https://github.com/tuanminhhole/openclaw-setup/releases"><img src="https://img.shields.io/badge/RELEASE-v5.10.0-0EA5E9?style=for-the-badge" alt="Version 5.10.0" /></a>
|
|
11
11
|
<a href="https://github.com/tuanminhhole/openclaw-setup?tab=MIT-1-ov-file"><img src="https://img.shields.io/badge/LICENSE-MIT-success?style=for-the-badge" alt="MIT License" /></a>
|
|
12
12
|
<a href="https://www.npmjs.com/package/create-openclaw-bot"><img src="https://img.shields.io/npm/v/create-openclaw-bot?style=for-the-badge&label=CLI&color=2563EB&logo=npm&logoColor=white" alt="NPM Version" /></a>
|
|
13
13
|
<a href="https://github.com/tuanminhhole/openclaw-setup/stargazers"><img src="https://img.shields.io/github/stars/tuanminhhole/openclaw-setup?style=for-the-badge&color=eab308&logo=github&logoColor=white" alt="GitHub Stars" /></a>
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
|
|
28
28
|
---
|
|
29
29
|
|
|
30
|
-
## 🆕 Có gì mới trong v5.
|
|
30
|
+
## 🆕 Có gì mới trong v5.10.0
|
|
31
31
|
|
|
32
32
|
- 🧠 **TencentDB Agent Memory — cài 1 chạm**: Plugin bộ nhớ mới ngay trên UI. Bộ nhớ phân tầng 4 lớp (L0–L3) + nén ngữ cảnh, giữ session dài mạch lạc và **tiết kiệm tới ~61% token**. Chạy local hoàn toàn (SQLite), không cần API key, sẵn sàng cho Docker.
|
|
33
33
|
- ⚡ **Cấu hình tiết kiệm token mặc định cho mọi bot mới**: Tự có context pruning (cache-TTL) + compaction `safeguard` — **hội thoại dài rẻ hơn & sắc nét hơn**, không cần chỉnh tay.
|
|
@@ -605,6 +605,136 @@ Timed out after ${opts.timeout}ms`.trim() : stderr });
|
|
|
605
605
|
});
|
|
606
606
|
}
|
|
607
607
|
|
|
608
|
+
// ── Native process supervision (auto-restart) ───────────────────────────────
|
|
609
|
+
// Docker containers get `restart: always` for free. For native installs we register the gateway
|
|
610
|
+
// and 9router as OS services so they survive crashes and reboots, mirroring that guarantee:
|
|
611
|
+
// • macOS → per-user launchd LaunchAgent (KeepAlive + RunAtLoad)
|
|
612
|
+
// • Linux → systemd unit (system unit when root, else --user) with Restart=always
|
|
613
|
+
// • Windows / anything else → plain detached process (no supervision; unchanged behaviour)
|
|
614
|
+
// Every step is best-effort: any failure falls back to startDetached, so a native install can
|
|
615
|
+
// never end up worse off than before this feature existed.
|
|
616
|
+
function escapeXml(s) { return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); }
|
|
617
|
+
function nativeServiceId(projectDir) { return slugify(basename(projectDir || 'openclaw')) || 'bot'; }
|
|
618
|
+
function nativeServiceLabels(projectDir) {
|
|
619
|
+
const id = nativeServiceId(projectDir);
|
|
620
|
+
return {
|
|
621
|
+
'9router': { launchd: `com.openclaw.9router.${id}`, systemd: `openclaw-9router-${id}.service` },
|
|
622
|
+
sync: { launchd: `com.openclaw.sync.${id}`, systemd: `openclaw-sync-${id}.service` },
|
|
623
|
+
gateway: { launchd: `com.openclaw.gateway.${id}`, systemd: `openclaw-gateway-${id}.service` },
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
async function resolveAbsoluteBin(cmd) {
|
|
627
|
+
try {
|
|
628
|
+
const out = await runCapture(process.platform === 'win32' ? 'where' : 'which', [cmd], { shell: false, timeout: 5000 });
|
|
629
|
+
const first = String(out.stdout || '').split(/\r?\n/).map((s) => s.trim()).find(Boolean);
|
|
630
|
+
if (out.code === 0 && first) return first;
|
|
631
|
+
} catch {}
|
|
632
|
+
return resolveBinPath(cmd);
|
|
633
|
+
}
|
|
634
|
+
function systemdUserUnitDir() { return join(getRealHomedir(), '.config', 'systemd', 'user'); }
|
|
635
|
+
function isRootUser() { return typeof process.getuid === 'function' && process.getuid() === 0; }
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Start one native process under OS supervision (auto-restart). Falls back to a plain detached
|
|
639
|
+
* process on Windows/unknown platforms or if the service tooling errors. Returns the method used:
|
|
640
|
+
* 'launchd' | 'systemd' | 'detached'.
|
|
641
|
+
*/
|
|
642
|
+
async function startNativeService({ projectDir, name, cmd, args, desc, env }) {
|
|
643
|
+
const labels = nativeServiceLabels(projectDir)[name];
|
|
644
|
+
const logDir = join(projectDir, '.openclaw', 'logs');
|
|
645
|
+
await fsp.mkdir(logDir, { recursive: true }).catch(() => {});
|
|
646
|
+
const serviceEnv = { PATH: process.env.PATH || '', HOME: getRealHomedir(), ...env };
|
|
647
|
+
try {
|
|
648
|
+
const absBin = await resolveAbsoluteBin(cmd);
|
|
649
|
+
if (process.platform === 'darwin') {
|
|
650
|
+
const agentsDir = join(getRealHomedir(), 'Library', 'LaunchAgents');
|
|
651
|
+
await fsp.mkdir(agentsDir, { recursive: true });
|
|
652
|
+
const progArgs = [absBin, ...args].map((a) => ` <string>${escapeXml(a)}</string>`).join('\n');
|
|
653
|
+
const envDict = Object.entries(serviceEnv).map(([k, v]) => ` <key>${escapeXml(k)}</key><string>${escapeXml(String(v))}</string>`).join('\n');
|
|
654
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
655
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
656
|
+
<plist version="1.0"><dict>
|
|
657
|
+
<key>Label</key><string>${labels.launchd}</string>
|
|
658
|
+
<key>ProgramArguments</key><array>
|
|
659
|
+
${progArgs}
|
|
660
|
+
</array>
|
|
661
|
+
<key>WorkingDirectory</key><string>${escapeXml(projectDir)}</string>
|
|
662
|
+
<key>EnvironmentVariables</key><dict>
|
|
663
|
+
${envDict}
|
|
664
|
+
</dict>
|
|
665
|
+
<key>RunAtLoad</key><true/>
|
|
666
|
+
<key>KeepAlive</key><true/>
|
|
667
|
+
<key>StandardOutPath</key><string>${escapeXml(join(logDir, name + '.out.log'))}</string>
|
|
668
|
+
<key>StandardErrorPath</key><string>${escapeXml(join(logDir, name + '.err.log'))}</string>
|
|
669
|
+
</dict></plist>`;
|
|
670
|
+
const plistPath = join(agentsDir, `${labels.launchd}.plist`);
|
|
671
|
+
await fsp.writeFile(plistPath, plist, 'utf8');
|
|
672
|
+
await run('launchctl', ['unload', plistPath], {}).catch(() => {});
|
|
673
|
+
await run('launchctl', ['load', '-w', plistPath], {});
|
|
674
|
+
sendLog(`[native] launchd service ${labels.launchd} loaded (auto-restart)`);
|
|
675
|
+
return 'launchd';
|
|
676
|
+
}
|
|
677
|
+
if (process.platform === 'linux') {
|
|
678
|
+
const root = isRootUser();
|
|
679
|
+
const unitDir = root ? '/etc/systemd/system' : systemdUserUnitDir();
|
|
680
|
+
await fsp.mkdir(unitDir, { recursive: true });
|
|
681
|
+
const sc = (a) => run('systemctl', root ? a : ['--user', ...a], {});
|
|
682
|
+
const envLines = Object.entries(serviceEnv).map(([k, v]) => `Environment="${k}=${String(v).replace(/\n/g, ' ')}"`).join('\n');
|
|
683
|
+
const execStart = [absBin, ...args].map((a) => (/[\s"']/.test(a) ? JSON.stringify(a) : a)).join(' ');
|
|
684
|
+
const unit = `[Unit]
|
|
685
|
+
Description=${desc}
|
|
686
|
+
After=network-online.target
|
|
687
|
+
Wants=network-online.target
|
|
688
|
+
|
|
689
|
+
[Service]
|
|
690
|
+
Type=simple
|
|
691
|
+
WorkingDirectory=${projectDir}
|
|
692
|
+
${envLines}
|
|
693
|
+
ExecStart=${execStart}
|
|
694
|
+
Restart=always
|
|
695
|
+
RestartSec=3
|
|
696
|
+
|
|
697
|
+
[Install]
|
|
698
|
+
WantedBy=${root ? 'multi-user.target' : 'default.target'}
|
|
699
|
+
`;
|
|
700
|
+
await fsp.writeFile(join(unitDir, labels.systemd), unit, 'utf8');
|
|
701
|
+
await sc(['daemon-reload']);
|
|
702
|
+
await sc(['enable', '--now', labels.systemd]);
|
|
703
|
+
sendLog(`[native] systemd ${root ? 'system' : 'user'} service ${labels.systemd} enabled (Restart=always)`);
|
|
704
|
+
return 'systemd';
|
|
705
|
+
}
|
|
706
|
+
} catch (e) {
|
|
707
|
+
sendLog(`[native] ${name} service setup failed (${e.message}); falling back to detached process`);
|
|
708
|
+
}
|
|
709
|
+
const pid = startDetached(cmd, args, { cwd: projectDir, env });
|
|
710
|
+
if (name === 'gateway') state.botPid = pid;
|
|
711
|
+
return 'detached';
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/** Remove any launchd/systemd services registered for a native project (used on delete). */
|
|
715
|
+
async function removeNativeAutostart(projectDir) {
|
|
716
|
+
const labels = nativeServiceLabels(projectDir);
|
|
717
|
+
try {
|
|
718
|
+
if (process.platform === 'darwin') {
|
|
719
|
+
const agentsDir = join(getRealHomedir(), 'Library', 'LaunchAgents');
|
|
720
|
+
for (const k of ['9router', 'sync', 'gateway']) {
|
|
721
|
+
const p = join(agentsDir, `${labels[k].launchd}.plist`);
|
|
722
|
+
if (existsSync(p)) { await run('launchctl', ['unload', p], {}).catch(() => {}); await fsp.unlink(p).catch(() => {}); }
|
|
723
|
+
}
|
|
724
|
+
} else if (process.platform === 'linux') {
|
|
725
|
+
const root = isRootUser();
|
|
726
|
+
const unitDir = root ? '/etc/systemd/system' : systemdUserUnitDir();
|
|
727
|
+
const sc = (a) => run('systemctl', root ? a : ['--user', ...a], {}).catch(() => {});
|
|
728
|
+
let changed = false;
|
|
729
|
+
for (const k of ['9router', 'sync', 'gateway']) {
|
|
730
|
+
const u = join(unitDir, labels[k].systemd);
|
|
731
|
+
if (existsSync(u)) { await sc(['disable', '--now', labels[k].systemd]); await fsp.unlink(u).catch(() => {}); changed = true; }
|
|
732
|
+
}
|
|
733
|
+
if (changed) await sc(['daemon-reload']);
|
|
734
|
+
}
|
|
735
|
+
} catch (e) { sendLog(`[native] autostart teardown skipped: ${e.message}`); }
|
|
736
|
+
}
|
|
737
|
+
|
|
608
738
|
function safeJoin(root, name) {
|
|
609
739
|
const clean = normalize(String(name || '')).replace(/^([/\\])+/, '');
|
|
610
740
|
if (!clean || clean.includes('..')) throw httpError(400, 'Invalid file path');
|
|
@@ -1445,7 +1575,9 @@ async function createBotInProject(projectDir, body = {}, runtime = {}) {
|
|
|
1445
1575
|
cfg.agents.list.push({
|
|
1446
1576
|
id: agentId,
|
|
1447
1577
|
name: botName,
|
|
1448
|
-
workspace
|
|
1578
|
+
// Relative workspace path — resolves against the process cwd (project root) in both docker
|
|
1579
|
+
// and native. See buildOpenclawJson() for the full rationale.
|
|
1580
|
+
workspace: `.openclaw/${workspaceDir}`,
|
|
1449
1581
|
agentDir: `agents/${agentId}/agent`,
|
|
1450
1582
|
model: { primary: model === '9router/smart-route' || model === 'openai/smart-route' ? DEFAULT_MODEL : model, fallbacks: [] },
|
|
1451
1583
|
});
|
|
@@ -2436,17 +2568,62 @@ async function installCore({ osChoice, mode, projectDir, gatewayPort = 18789, ro
|
|
|
2436
2568
|
await applyResolved9RouterApiKey(projectDir).catch(() => {});
|
|
2437
2569
|
await recreateDockerBot(projectDir).catch(() => {});
|
|
2438
2570
|
} else {
|
|
2571
|
+
const nineRouterDataDir = join(projectDir, '.9router');
|
|
2572
|
+
const nineRouterDbPath = join(nineRouterDataDir, 'db', 'data.sqlite');
|
|
2439
2573
|
const runtimeEnv = {
|
|
2440
2574
|
OPENCLAW_HOME: join(projectDir, '.openclaw'),
|
|
2441
2575
|
OPENCLAW_STATE_DIR: join(projectDir, '.openclaw'),
|
|
2442
|
-
DATA_DIR:
|
|
2576
|
+
DATA_DIR: nineRouterDataDir,
|
|
2443
2577
|
OPENCLAW_GATEWAY_PORT: String(gatewayPort),
|
|
2444
2578
|
OPENCLAW_PORT: String(gatewayPort),
|
|
2579
|
+
// Parity with the docker runtime env (docker-gen.js) so the gateway and browser plugin
|
|
2580
|
+
// behave the same natively: allow the private-network control-UI websocket and tell the
|
|
2581
|
+
// browser plugin which host OS it is running on.
|
|
2582
|
+
OPENCLAW_ALLOW_INSECURE_PRIVATE_WS: '1',
|
|
2583
|
+
OPENCLAW_SETUP_OS: osChoice || '',
|
|
2584
|
+
OPENCLAW_BROWSER_HOST_OS: osChoice || '',
|
|
2445
2585
|
};
|
|
2446
2586
|
await run('openclaw', ['gateway', 'stop'], { cwd: projectDir, env: runtimeEnv }).catch(() => {});
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2587
|
+
// 9router binds loopback in native mode on purpose: openclaw runs on the SAME host and
|
|
2588
|
+
// reaches it via localhost (see get9RouterBaseUrl), so there is no reason to expose the LLM
|
|
2589
|
+
// proxy on 0.0.0.0 — even on a VPS that would be an open relay. Only the gateway is exposed.
|
|
2590
|
+
// Start 9router first (auto-restart supervised), then sync, then the gateway — so the gateway
|
|
2591
|
+
// boots after 9router is reachable and after the API key has been written to its config.
|
|
2592
|
+
const routerMethod = await startNativeService({
|
|
2593
|
+
projectDir, name: '9router', cmd: '9router', desc: `OpenClaw 9router (${nativeServiceId(projectDir)})`,
|
|
2594
|
+
args: ['-n', '-l', '-H', '127.0.0.1', '-p', String(routerPort), '--skip-update'], env: runtimeEnv,
|
|
2595
|
+
});
|
|
2596
|
+
// Smart-route auto-sync: the docker sidecar runs a script that disables 9router's login
|
|
2597
|
+
// requirement and builds the default `smart-route` combo from the active providers. Native
|
|
2598
|
+
// mode skipped this, so the default model had no backing models. Reuse the exact docker
|
|
2599
|
+
// script with the native DB path (passed via env to stay cross-platform / shell-safe).
|
|
2600
|
+
try {
|
|
2601
|
+
const artifacts = buildDockerArtifacts({ is9Router: true, osChoice, openClawNpmSpec: OPENCLAW_NPM_SPEC, gatewayPort, routerPort });
|
|
2602
|
+
if (artifacts.syncScript) {
|
|
2603
|
+
await fsp.mkdir(nineRouterDataDir, { recursive: true });
|
|
2604
|
+
const syncPath = join(nineRouterDataDir, 'sync.js');
|
|
2605
|
+
await fsp.writeFile(syncPath, artifacts.syncScript, 'utf8');
|
|
2606
|
+
// Run the sync under supervision too (managed service) so it is torn down cleanly on
|
|
2607
|
+
// delete instead of leaking an orphaned `node sync.js` process.
|
|
2608
|
+
await startNativeService({
|
|
2609
|
+
projectDir, name: 'sync', cmd: process.execPath, args: [syncPath],
|
|
2610
|
+
desc: `OpenClaw 9router smart-route sync (${nativeServiceId(projectDir)})`,
|
|
2611
|
+
env: { ...runtimeEnv, NINEROUTER_DB_PATH: nineRouterDbPath, PORT: String(routerPort) },
|
|
2612
|
+
});
|
|
2613
|
+
sendLog('[native] 9router smart-route sync started');
|
|
2614
|
+
}
|
|
2615
|
+
} catch (e) {
|
|
2616
|
+
sendLog(`[native] smart-route sync setup skipped: ${e.message}`);
|
|
2617
|
+
}
|
|
2618
|
+
// Give 9router a moment to boot + sync to disable its login gate, then resolve its API key
|
|
2619
|
+
// into openclaw.json BEFORE the gateway starts (so the gateway reads it on first boot).
|
|
2620
|
+
await new Promise((r) => setTimeout(r, 8000));
|
|
2621
|
+
await applyResolved9RouterApiKey(projectDir).catch(() => {});
|
|
2622
|
+
const gatewayMethod = await startNativeService({
|
|
2623
|
+
projectDir, name: 'gateway', cmd: 'openclaw', desc: `OpenClaw gateway (${nativeServiceId(projectDir)})`,
|
|
2624
|
+
args: ['gateway', 'run'], env: runtimeEnv,
|
|
2625
|
+
});
|
|
2626
|
+
sendLog(`Native runtime started — 9router via ${routerMethod}, gateway via ${gatewayMethod}${state.botPid ? ` (pid=${state.botPid})` : ''}`);
|
|
2450
2627
|
}
|
|
2451
2628
|
state.installed = true;
|
|
2452
2629
|
sendLog('✅ Install completed');
|
|
@@ -2851,6 +3028,10 @@ async function deleteProjectFolder(projectDir, rootProjectDir) {
|
|
|
2851
3028
|
});
|
|
2852
3029
|
// Sleep 2.5 seconds to let Windows file system release overlays/locks
|
|
2853
3030
|
await new Promise((resolve) => setTimeout(resolve, 2500));
|
|
3031
|
+
} else {
|
|
3032
|
+
// Native project: stop & remove any launchd/systemd auto-restart services so they don't keep
|
|
3033
|
+
// respawning a deleted bot. No-op when no services were registered.
|
|
3034
|
+
await removeNativeAutostart(resolved);
|
|
2854
3035
|
}
|
|
2855
3036
|
|
|
2856
3037
|
try {
|
|
@@ -4073,7 +4254,7 @@ export async function startLocalInstaller({ host = '127.0.0.1', preferredPort =
|
|
|
4073
4254
|
printRemoteAccessHint(port).catch(() => {});
|
|
4074
4255
|
}
|
|
4075
4256
|
|
|
4076
|
-
export { createBotInProject, updateBotInProject, deleteBotInProject, validateOpenclawConfig, startZaloUserLogin, readBotCredentials, resolveProject9RouterApiKey };
|
|
4257
|
+
export { createBotInProject, updateBotInProject, deleteBotInProject, validateOpenclawConfig, startZaloUserLogin, readBotCredentials, resolveProject9RouterApiKey, installCore, deleteProjectFolder, removeNativeAutostart };
|
|
4077
4258
|
|
|
4078
4259
|
|
|
4079
4260
|
|
|
@@ -82,23 +82,29 @@
|
|
|
82
82
|
const isLocal = !!provider.isLocal;
|
|
83
83
|
|
|
84
84
|
// ── agents ────────────────────────────────────────────────────────────────
|
|
85
|
+
// Workspace is a RELATIVE path (resolved by OpenClaw against the process cwd, which is the
|
|
86
|
+
// project root in both modes: WORKDIR=/home/node/project in docker, cwd=projectDir natively).
|
|
87
|
+
// OpenClaw's resolveUserPath() keeps absolute paths as-is, so the old container-absolute
|
|
88
|
+
// "/home/node/project/.openclaw/…" pointed at a non-existent path on a native host (the bot's
|
|
89
|
+
// persona/memory/skills silently lived in the wrong place). `agentDir` is already relative and
|
|
90
|
+
// proven to resolve correctly in docker, so relative `workspace` is byte-identical for docker
|
|
91
|
+
// and finally correct for native. See workspaceRelForAgent() which accepts both forms.
|
|
85
92
|
const agentsList = agentMetas.map((meta) => ({
|
|
86
93
|
id: meta.agentId,
|
|
87
94
|
...(meta.name ? { name: meta.name } : {}),
|
|
88
|
-
workspace:
|
|
95
|
+
workspace: `.openclaw/${meta.workspaceDir || 'workspace-' + meta.agentId}`,
|
|
89
96
|
agentDir: `agents/${meta.agentId}/agent`,
|
|
90
97
|
model: { primary: model, fallbacks: [] },
|
|
91
98
|
}));
|
|
92
99
|
|
|
93
100
|
const cfg = {
|
|
94
|
-
// NOTE: do NOT seed `
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
// `
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
},
|
|
101
|
+
// NOTE: do NOT seed a `meta` block here. `meta` is owned by OpenClaw — it stamps
|
|
102
|
+
// `{ lastTouchedVersion, lastTouchedAt }` itself on first run and adds the block if
|
|
103
|
+
// missing. Writing our own fields into it (osChoice/deployMode), or seeding
|
|
104
|
+
// `lastTouchedVersion` from OPENCLAW_NPM_SPEC which is a range/`latest`
|
|
105
|
+
// (e.g. `>=2026.6.10`) not a concrete version, makes OpenClaw fail to parse the config
|
|
106
|
+
// on boot and crash the container. osChoice/deployMode are installer-only concerns and
|
|
107
|
+
// are persisted via the browser plugin's `config.hostOs` + in-memory state instead.
|
|
102
108
|
agents: {
|
|
103
109
|
defaults: {
|
|
104
110
|
model: { primary: model, fallbacks: [] },
|
|
@@ -18,103 +18,100 @@
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
function build9RouterSmartRouteSyncScript() {
|
|
21
|
+
// First-install auto-sync: đăng nhập 9router bằng MẬT KHẨU MẶC ĐỊNH 123456 (9router cấp),
|
|
22
|
+
// gom model của các provider đang active → tạo combo 'smart-route' MỘT LẦN rồi DỪNG.
|
|
23
|
+
// KHÔNG tự tắt Require login nữa (require login giữ ON, pass mặc định 123456, user đổi sau).
|
|
21
24
|
const lines = [
|
|
22
25
|
"const fs = require('fs');",
|
|
23
26
|
"const INTERVAL = 30000;",
|
|
24
|
-
"const
|
|
27
|
+
"const MAX_ATTEMPTS = 20; // ~10 phút; tạo combo lần đầu rồi dừng, không lặp vĩnh viễn",
|
|
28
|
+
"const DEFAULT_PASSWORD = '123456'; // mật khẩu mặc định 9router",
|
|
29
|
+
// Docker mounts the DB at /root/.9router; native installs pass NINEROUTER_DB_PATH so the
|
|
30
|
+
// same sync script works on the host without hardcoding the container path.
|
|
31
|
+
"const DB_PATH = process.env.NINEROUTER_DB_PATH || '/root/.9router/db/data.sqlite';",
|
|
25
32
|
"const PORT = process.env.PORT || 20128;",
|
|
26
33
|
"const COMBO_NAME = 'smart-route';",
|
|
27
34
|
"const API_BASE = `http://localhost:${PORT}`;",
|
|
28
35
|
"",
|
|
29
|
-
"
|
|
36
|
+
"// Đăng nhập bằng mật khẩu mặc định → lấy cookie auth_token để gọi API quản trị.",
|
|
37
|
+
"async function login() {",
|
|
30
38
|
" try {",
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
" }
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
" }",
|
|
42
|
-
' const existing = db.prepare("SELECT * FROM settings WHERE id = 1").get();',
|
|
43
|
-
" if (!existing) {",
|
|
44
|
-
' db.prepare("INSERT INTO settings (id, data) VALUES (1, ?)").run(JSON.stringify({ requireLogin: false }));',
|
|
45
|
-
" } else {",
|
|
46
|
-
" try {",
|
|
47
|
-
" const data = JSON.parse(existing.data || '{}');",
|
|
48
|
-
" if (data.requireLogin !== false) {",
|
|
49
|
-
" data.requireLogin = false;",
|
|
50
|
-
' db.prepare("UPDATE settings SET data = ? WHERE id = 1").run(JSON.stringify(data));',
|
|
51
|
-
" }",
|
|
52
|
-
" } catch {}",
|
|
53
|
-
" }",
|
|
54
|
-
" db.close();",
|
|
55
|
-
" } catch (e) {}",
|
|
39
|
+
" const r = await fetch(`${API_BASE}/api/auth/login`, {",
|
|
40
|
+
" method: 'POST',",
|
|
41
|
+
" headers: { 'Content-Type': 'application/json' },",
|
|
42
|
+
" body: JSON.stringify({ password: DEFAULT_PASSWORD })",
|
|
43
|
+
" });",
|
|
44
|
+
" if (!r.ok) return '';",
|
|
45
|
+
" const sc = r.headers.get('set-cookie') || '';",
|
|
46
|
+
" const m = sc.match(/auth_token=[^;]+/);",
|
|
47
|
+
" return m ? m[0] : '';",
|
|
48
|
+
" } catch (e) { return ''; }",
|
|
56
49
|
"}",
|
|
57
50
|
"",
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"",
|
|
62
|
-
" let existingCombo = null;",
|
|
63
|
-
" try {",
|
|
64
|
-
" const resp = await fetch(`${API_BASE}/api/combos`);",
|
|
65
|
-
" if (resp.status === 401) {",
|
|
66
|
-
" ensureSettings();",
|
|
67
|
-
" return;",
|
|
68
|
-
" }",
|
|
69
|
-
" const data = await resp.json();",
|
|
70
|
-
" if (data.combos) {",
|
|
71
|
-
" existingCombo = data.combos.find(c => c.name === COMBO_NAME);",
|
|
72
|
-
" }",
|
|
73
|
-
" } catch (e) { return; }",
|
|
51
|
+
"async function trySync() {",
|
|
52
|
+
" if (!fs.existsSync(DB_PATH)) return false;",
|
|
53
|
+
" const cookie = await login(); // mk mặc định 123456; nếu user đã đổi thì login fail",
|
|
54
|
+
" const authHeaders = cookie ? { Cookie: cookie } : {};",
|
|
74
55
|
"",
|
|
75
|
-
"
|
|
76
|
-
"",
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"
|
|
82
|
-
" activeProviders = [...new Set(",
|
|
83
|
-
" conns.filter(c => c && c.provider && c.isActive !== false && !c.disabled).map(c => c.provider)",
|
|
84
|
-
" )];",
|
|
85
|
-
" } catch (e) { return; }",
|
|
86
|
-
"",
|
|
87
|
-
" if (!activeProviders.length) return;",
|
|
56
|
+
" // Combo đã tồn tại? → coi như xong (chỉ tạo lần đầu).",
|
|
57
|
+
" try {",
|
|
58
|
+
" const r = await fetch(`${API_BASE}/api/combos`, { headers: authHeaders });",
|
|
59
|
+
" if (r.status === 401) return false;",
|
|
60
|
+
" const d = await r.json();",
|
|
61
|
+
" if ((d.combos || []).some(c => c.name === COMBO_NAME)) return true;",
|
|
62
|
+
" } catch (e) { return false; }",
|
|
88
63
|
"",
|
|
89
|
-
"
|
|
90
|
-
"
|
|
91
|
-
"
|
|
92
|
-
"
|
|
93
|
-
"
|
|
94
|
-
"
|
|
95
|
-
"
|
|
96
|
-
"
|
|
97
|
-
"
|
|
98
|
-
"
|
|
99
|
-
"
|
|
100
|
-
" } catch (e) { return; }",
|
|
64
|
+
" // Provider đang active.",
|
|
65
|
+
" let activeProviders = [];",
|
|
66
|
+
" try {",
|
|
67
|
+
" const r = await fetch(`${API_BASE}/api/providers`, { headers: authHeaders });",
|
|
68
|
+
" const d = await r.json();",
|
|
69
|
+
" const conns = d.connections || d.providerConnections || [];",
|
|
70
|
+
" activeProviders = [...new Set(",
|
|
71
|
+
" conns.filter(c => c && c.provider && c.isActive !== false && !c.disabled).map(c => c.provider)",
|
|
72
|
+
" )];",
|
|
73
|
+
" } catch (e) { return false; }",
|
|
74
|
+
" if (!activeProviders.length) return false;",
|
|
101
75
|
"",
|
|
102
|
-
"
|
|
76
|
+
" // Model của provider active (bỏ embedding/image/tts/…).",
|
|
77
|
+
" let models = [];",
|
|
78
|
+
" try {",
|
|
79
|
+
" const r = await fetch(`${API_BASE}/api/models`, { headers: authHeaders });",
|
|
80
|
+
" const d = await r.json();",
|
|
81
|
+
" if (Array.isArray(d.models)) {",
|
|
82
|
+
" models = [...new Set(d.models",
|
|
83
|
+
" .filter(m => activeProviders.includes(m.provider))",
|
|
84
|
+
" .filter(m => !/(embedding|image|tts|stt|audio|vision)/i.test(m.model))",
|
|
85
|
+
" .map(m => m.fullModel))];",
|
|
86
|
+
" }",
|
|
87
|
+
" } catch (e) { return false; }",
|
|
88
|
+
" if (!models.length) return false;",
|
|
103
89
|
"",
|
|
104
|
-
"
|
|
105
|
-
"
|
|
106
|
-
"
|
|
107
|
-
"
|
|
108
|
-
"
|
|
109
|
-
" })
|
|
110
|
-
"
|
|
111
|
-
"
|
|
112
|
-
"
|
|
113
|
-
"};",
|
|
90
|
+
" // Tạo combo smart-route (lần đầu).",
|
|
91
|
+
" try {",
|
|
92
|
+
" await fetch(`${API_BASE}/api/combos`, {",
|
|
93
|
+
" method: 'POST',",
|
|
94
|
+
" headers: { 'Content-Type': 'application/json', ...authHeaders },",
|
|
95
|
+
" body: JSON.stringify({ name: COMBO_NAME, models })",
|
|
96
|
+
" });",
|
|
97
|
+
" console.log('[sync-combo] Created smart-route with ' + models.length + ' models');",
|
|
98
|
+
" return true;",
|
|
99
|
+
" } catch (e) { return false; }",
|
|
100
|
+
"}",
|
|
114
101
|
"",
|
|
115
|
-
"
|
|
116
|
-
"
|
|
117
|
-
"
|
|
102
|
+
"let attempts = 0;",
|
|
103
|
+
"let timer = null;",
|
|
104
|
+
"async function tick() {",
|
|
105
|
+
" attempts++;",
|
|
106
|
+
" let done = false;",
|
|
107
|
+
" try { done = await trySync(); } catch (e) {}",
|
|
108
|
+
" if (done || attempts >= MAX_ATTEMPTS) {",
|
|
109
|
+
" if (timer) clearInterval(timer);",
|
|
110
|
+
" console.log('[sync-combo] finished (attempts=' + attempts + ', done=' + done + ')');",
|
|
111
|
+
" }",
|
|
112
|
+
"}",
|
|
113
|
+
"timer = setInterval(tick, INTERVAL);",
|
|
114
|
+
"setTimeout(tick, 10000);",
|
|
118
115
|
];
|
|
119
116
|
return lines.join('\n');
|
|
120
117
|
}
|
|
@@ -187,7 +184,7 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
|
|
|
187
184
|
return [
|
|
188
185
|
`npm install -g ` + nineRouterSpec,
|
|
189
186
|
'node /tmp/patch-9router.js || true',
|
|
190
|
-
'node -e "const fs=require(\'fs\'),path=require(\'path\'); const DB_PATH=\'/root/.9router/db/data.sqlite\'; const dir=path.dirname(DB_PATH); if(!fs.existsSync(dir))fs.mkdirSync(dir,{recursive:true}); try{ const {DatabaseSync}=require(\'node:sqlite\'); const db=new DatabaseSync(DB_PATH); db.prepare(\'CREATE TABLE IF NOT EXISTS settings (id INTEGER PRIMARY KEY CHECK (id = 1), data TEXT NOT NULL)\').run(); const existing=db.prepare(\'SELECT * FROM settings WHERE id = 1\').get(); if(!existing){ db.prepare(\'INSERT INTO settings (id, data) VALUES (1, ?)\').run(JSON.stringify({requireLogin:
|
|
187
|
+
'node -e "const fs=require(\'fs\'),path=require(\'path\'); const DB_PATH=\'/root/.9router/db/data.sqlite\'; const dir=path.dirname(DB_PATH); if(!fs.existsSync(dir))fs.mkdirSync(dir,{recursive:true}); try{ const {DatabaseSync}=require(\'node:sqlite\'); const db=new DatabaseSync(DB_PATH); db.prepare(\'CREATE TABLE IF NOT EXISTS settings (id INTEGER PRIMARY KEY CHECK (id = 1), data TEXT NOT NULL)\').run(); const existing=db.prepare(\'SELECT * FROM settings WHERE id = 1\').get(); if(!existing){ db.prepare(\'INSERT INTO settings (id, data) VALUES (1, ?)\').run(JSON.stringify({requireLogin:true})); } db.close(); }catch(e){}" || true',
|
|
191
188
|
'node /tmp/sync.js > /tmp/sync.log 2>&1 &',
|
|
192
189
|
`exec 9router -n -l -H 0.0.0.0 -p ${port} --skip-update`
|
|
193
190
|
].join('\n');
|
package/dist/web/app.js
CHANGED
|
@@ -405,6 +405,9 @@ function topbarActionsHtml() {
|
|
|
405
405
|
|
|
406
406
|
function render() {
|
|
407
407
|
applyPrefs();
|
|
408
|
+
// Expose the active tab on <body> so CSS can drop the redundant page-title header on the
|
|
409
|
+
// dashboard (the dashboard hero already carries its own title).
|
|
410
|
+
document.body.dataset.activeTab = state.tab;
|
|
408
411
|
const tabs = [['dashboard',t('Dashboard','Dashboard')],['setup',ui('setup')],['bot',ui('bot')],['logs',ui('logs')]];
|
|
409
412
|
|
|
410
413
|
let mainContainer = $('#app-main-content');
|
|
@@ -617,9 +620,9 @@ function dashboardView() {
|
|
|
617
620
|
<h2>${t('Dashboard v\u1eadn h\u00e0nh', 'Operational Dashboard')}</h2>
|
|
618
621
|
<p class="lead" style="margin-top:6px">${t('M\u1edf website, xem version, tr\u1ea1ng th\u00e1i, bot v\u00e0 project.', 'Open website, view versions, status, bots and projects.')}</p>
|
|
619
622
|
</div>
|
|
620
|
-
<div class="dash-actions" style="flex-direction:
|
|
621
|
-
<button class="primary icon-btn2" data-tab-jump="bot" type="button" style="justify-content:center; min-width:
|
|
622
|
-
<button class="secondary icon-btn2" data-tab-jump="setup" type="button" style="justify-content:center; border-width:2px; min-width:
|
|
623
|
+
<div class="dash-actions" style="flex-direction:row; gap:10px;">
|
|
624
|
+
<button class="primary icon-btn2" data-tab-jump="bot" type="button" style="justify-content:center; flex:1; min-width:0;"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a2 2 0 0 1 2 2c0 .74-.4 1.39-1 1.73V7h1a7 7 0 0 1 7 7h1a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-1v1a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-1H2a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h1a7 7 0 0 1 7-7h1V5.73c-.6-.34-1-.99-1-1.73a2 2 0 0 1 2-2z"></path></svg>${t('Bot','Bot')}</button>
|
|
625
|
+
<button class="secondary icon-btn2" data-tab-jump="setup" type="button" style="justify-content:center; border-width:2px; flex:1; min-width:0;"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15V3m0 12l-4-4m4 4l4-4M2 17l.621 2.485A2 2 0 0 0 4.561 21h14.878a2 2 0 0 0 1.94-1.515L22 17"></path></svg>${t('C\u00e0i \u0111\u1eb7t','Setup')}</button>
|
|
623
626
|
</div>
|
|
624
627
|
</div>
|
|
625
628
|
<div class="project-tabs" style="display:flex; gap:8px; flex-wrap:wrap; padding-top:10px; border-top:1px solid rgba(255,255,255,0.06);">
|
|
@@ -627,7 +630,7 @@ function dashboardView() {
|
|
|
627
630
|
</div>
|
|
628
631
|
</section>
|
|
629
632
|
|
|
630
|
-
<section class="dash-layout"
|
|
633
|
+
<section class="dash-layout">
|
|
631
634
|
<div style="display:flex; flex-direction:column; gap:18px;">
|
|
632
635
|
<div class="dash-grid" style="grid-template-columns: repeat(2, minmax(0, 1fr)); align-content: start;">
|
|
633
636
|
${widgets.map(w => `<article class="card dash-metric"><span>${escapeHtml(w.label)}</span><strong style="font-size:22px; word-break:break-all;">${w.value}</strong><small>${escapeHtml(w.meta)}</small></article>`).join('')}
|
|
@@ -985,26 +988,31 @@ function botSkillsPanel() {
|
|
|
985
988
|
const requiresInstall = group === 'plugin' || (group === 'skill' && (item.id === 'image-gen' || item.id === 'sticker-mention' || item.id === 'learning-memory'));
|
|
986
989
|
const isInstalled = !requiresInstall || !!state.featureInstalled?.[key];
|
|
987
990
|
|
|
988
|
-
|
|
991
|
+
const version = requiresInstall && isInstalled ? (state.featureVersions?.[key] || '') : '';
|
|
992
|
+
const versionBadge = version ? `<span class="plugin-version-badge" style="display:inline-block; font-size: 11px; background: rgba(66, 133, 244, 0.15); color: #4285F4; padding: 2px 6px; border-radius: 4px; font-weight: 600; border: 1px solid rgba(66,133,244,0.25); flex:0 0 auto;">v${escapeHtml(version)}</span>` : '';
|
|
993
|
+
|
|
994
|
+
// The on/off toggle stays inline with the title (one line, name truncates with ellipsis if
|
|
995
|
+
// long). The wider Open / Update / Install controls drop to their own row below the description.
|
|
996
|
+
let toggleHtml = '';
|
|
997
|
+
const secs = [];
|
|
989
998
|
if (isInstalled) {
|
|
990
|
-
|
|
999
|
+
toggleHtml = `<label class="feature-switch"><input type="checkbox" data-feature-toggle="${key}" ${flags[key] ? 'checked' : ''} ${loading ? 'disabled' : ''}/><span></span></label>`;
|
|
991
1000
|
if (item.openWebPort) {
|
|
992
|
-
|
|
1001
|
+
secs.push(`<a class="secondary icon-btn2" href="${sameHostUrl('', item.openWebPort)}${item.openWebPath || ''}" target="_blank" rel="noopener" title="${t('Mở dashboard của plugin','Open the plugin dashboard')}" style="padding: 4px 8px; font-size: 11px; height: 28px; border-width: 1px;">${actionIcon('link')}<span>${t('Mở web','Open')}</span></a>`);
|
|
993
1002
|
}
|
|
994
1003
|
if (requiresInstall) {
|
|
995
|
-
|
|
1004
|
+
secs.push(`<button class="secondary icon-btn2 update-plugin-btn" type="button" data-feature-install="${key}" ${loading ? 'disabled' : ''} title="${t('Cập nhật lên bản mới nhất','Update to latest version')}" style="padding: 4px 8px; font-size: 11px; height: 28px; border-width: 1px; color:#ffb020; border-color: rgba(255,176,32,0.25); background: rgba(255,176,32,0.05);">${actionIcon('refresh')}<span>${t('Cập nhật','Update')}</span></button>`);
|
|
996
1005
|
}
|
|
997
|
-
actionsHtml += `<label class="feature-switch"><input type="checkbox" data-feature-toggle="${key}" ${flags[key] ? 'checked' : ''} ${loading ? 'disabled' : ''}/><span></span></label></div>`;
|
|
998
1006
|
} else {
|
|
999
|
-
|
|
1007
|
+
secs.push(`<button class="secondary icon-btn2" type="button" data-feature-install="${key}" ${loading ? 'disabled' : ''}>${actionIcon('download')} ${ui('installVerb')}</button>`);
|
|
1000
1008
|
}
|
|
1009
|
+
const secondaryHtml = secs.length ? `<div class="feature-actions">${secs.join('')}</div>` : '';
|
|
1001
1010
|
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
`</div>${loading ? '<div class="feature-progress"><i></i></div>' : ''}</article>`;
|
|
1011
|
+
return `<article class="card feature-card ${loading ? 'is-loading' : ''}">` +
|
|
1012
|
+
`<div class="feature-head"><div class="feature-title"><b class="feature-name">${escapeHtml(item.title)}</b>${versionBadge}</div>${toggleHtml}</div>` +
|
|
1013
|
+
`<p class="feature-desc">${escapeHtml(item.desc)}</p>` +
|
|
1014
|
+
secondaryHtml +
|
|
1015
|
+
`${loading ? '<div class="feature-progress"><i></i></div>' : ''}</article>`;
|
|
1008
1016
|
};
|
|
1009
1017
|
return `
|
|
1010
1018
|
<h4 class="feature-group">⚡ ${t('Skills','Skills')}</h4>
|
package/dist/web/styles.css
CHANGED
|
@@ -49,7 +49,7 @@ html[data-theme="light"] .topbar{background:rgba(255,255,255,.82);box-shadow:0 1
|
|
|
49
49
|
.setup-copy{display:grid!important;grid-template-columns:minmax(240px,.78fr) 1fr;align-items:center!important;gap:22px!important;min-height:128px;padding:22px 24px!important;background:linear-gradient(135deg,rgba(18,201,255,.075),rgba(255,255,255,.035),rgba(124,58,237,.055))!important;border-color:rgba(96,165,250,.20)!important}.setup-copy h2{margin:12px 0 0;font-size:22px;letter-spacing:-.035em;line-height:1.15}.setup-copy .lead{margin:0;font-size:18px;line-height:1.55;max-width:560px;color:var(--body);font-weight:500}.mini-pill{border-color:rgba(18,201,255,.38)!important;background:rgba(18,201,255,.09)!important;color:var(--soft)!important}
|
|
50
50
|
.setup-section,.card,.choice-card,.system-card,.terminal{backdrop-filter:blur(14px);background:linear-gradient(145deg,rgba(8,18,32,.72),rgba(5,8,14,.82))!important;border-color:var(--hair)!important}.choice-card:has(input:checked),.choice-card.is-selected{border-color:var(--primary)!important;background:linear-gradient(145deg,rgba(18,201,255,.13),rgba(8,18,32,.88))!important;box-shadow:0 0 0 1px rgba(18,201,255,.25) inset,0 24px 70px rgba(18,201,255,.10)!important}.choice-card:has(input:checked) .choice-card__meta i,.choice-card.is-selected .choice-card__meta i{border-color:var(--primary)!important;background:radial-gradient(circle,var(--primary) 0 42%,transparent 46%)!important;box-shadow:0 0 0 5px rgba(18,201,255,.14),0 0 28px rgba(18,201,255,.34)!important}.install-cta{background:linear-gradient(135deg,var(--primary),var(--soft) 48%,#0284c7)!important;color:#001018!important}.install-cta small{color:rgba(0,16,24,.72)!important}
|
|
51
51
|
html[data-theme="light"] .setup-section,html[data-theme="light"] .card,html[data-theme="light"] .choice-card,html[data-theme="light"] .system-card,html[data-theme="light"] .terminal{background:rgba(255,255,255,.82)!important;color:var(--ink)!important}.choice-card__body small{color:var(--body)!important}
|
|
52
|
-
@media(max-width:900px){.topbar{height:auto
|
|
52
|
+
@media(max-width:900px){.topbar{height:auto}.setup-copy{grid-template-columns:1fr!important}.setup-copy .lead{font-size:16px}}
|
|
53
53
|
|
|
54
54
|
|
|
55
55
|
/* OpenClaw red theme + overflow containment fix */
|
|
@@ -651,6 +651,10 @@ html[data-theme="light"] .donut-chart::before {
|
|
|
651
651
|
.chart-info span{color:var(--muted);font-size:12px;font-weight:800;text-transform:uppercase}
|
|
652
652
|
.chart-info strong{font-size:18px;letter-spacing:-0.02em;line-height:1.2}
|
|
653
653
|
.chart-info small{color:var(--body);font-size:13px}
|
|
654
|
+
.chart-info span,.chart-info strong,.chart-info small{overflow-wrap:anywhere}
|
|
655
|
+
/* On narrow screens the chart cards sit 2-up (~162px each); stack the donut above the
|
|
656
|
+
text so the labels ("Installed Extensions", model id) get the full card width. */
|
|
657
|
+
@media(max-width:760px){.chart-card{flex-direction:column;text-align:center;gap:10px;padding:14px}.chart-info{align-items:center}}
|
|
654
658
|
|
|
655
659
|
/* Premium Toast Notification System */
|
|
656
660
|
.toast-container {
|
|
@@ -1912,3 +1916,99 @@ main{transition:margin .2s}
|
|
|
1912
1916
|
/* Tablet/mobile bot page: stop horizontal overflow so .channel-tabs scrolls instead of stretching the layout */
|
|
1913
1917
|
.bot-page,.bot-top-row,.bot-main,.bot-main-content,.bot-projects-and-bots,.bot-side-col,.bot-list{min-width:0}
|
|
1914
1918
|
@media(max-width:1180px){.bot-top-row{grid-template-columns:minmax(0,1fr)}}
|
|
1919
|
+
|
|
1920
|
+
/* ════════════════════════════════════════════════════════════════════════════
|
|
1921
|
+
Mobile UX refinements (user feedback)
|
|
1922
|
+
════════════════════════════════════════════════════════════════════════════ */
|
|
1923
|
+
|
|
1924
|
+
/* [Hero] Dashboard already shows its title inside the hero card — drop the duplicate
|
|
1925
|
+
page-title header so it doesn't repeat "Dashboard vận hành" twice. */
|
|
1926
|
+
body[data-active-tab="dashboard"] .top{display:none}
|
|
1927
|
+
|
|
1928
|
+
/* [Modals] While any modal is open, hide the fixed bottom nav so it can't cover the modal's
|
|
1929
|
+
lower content (and footer buttons) on mobile. The backdrop owns the screen at that point. */
|
|
1930
|
+
body:has(.modal-backdrop) .bottom{display:none!important}
|
|
1931
|
+
/* Bot / Cài đặt hero buttons sit side-by-side — keep their labels on one line. */
|
|
1932
|
+
.dash-actions .icon-btn2{white-space:nowrap}
|
|
1933
|
+
|
|
1934
|
+
@media(max-width:760px){
|
|
1935
|
+
/* [Topbar] Keep search + theme + language on ONE row; theme becomes icon-only and the
|
|
1936
|
+
language globe icon is dropped so the whole bar stays compact. */
|
|
1937
|
+
.topbar{flex-direction:row!important;align-items:center!important;gap:8px;padding:8px 10px}
|
|
1938
|
+
.topbar__actions{width:auto!important;flex-wrap:nowrap!important;justify-content:flex-end!important;gap:6px}
|
|
1939
|
+
.topbar__actions .seg{padding:3px;gap:2px}
|
|
1940
|
+
.topbar__actions .seg[aria-label="theme"] .seg__btn span{display:none}
|
|
1941
|
+
.topbar__actions .seg[aria-label="theme"] .seg__btn{padding:8px 9px}
|
|
1942
|
+
.topbar__actions .seg[aria-label="lang"]>span{display:none!important}
|
|
1943
|
+
.topbar__actions .seg__btn{padding:6px 9px;font-size:11px}
|
|
1944
|
+
.search input{font-size:13px}
|
|
1945
|
+
|
|
1946
|
+
/* [Connect project] The action buttons were absolutely positioned and overlapped the
|
|
1947
|
+
path text. Put the card into normal flow with full-width buttons below the content.
|
|
1948
|
+
The card used overflow:hidden + a fixed min-height and the list capped at 260px with an
|
|
1949
|
+
inner scroll — all of which clipped the now-in-flow buttons. Let everything grow. */
|
|
1950
|
+
.detected-projects{max-height:none!important;overflow:visible!important}
|
|
1951
|
+
.detected-project{padding:12px!important;min-height:0!important;height:auto!important;overflow:visible!important}
|
|
1952
|
+
.detected-project__actions{position:static!important;transform:none!important;width:100%;margin-top:4px}
|
|
1953
|
+
.detected-project__actions .secondary{min-width:0!important;flex:1}
|
|
1954
|
+
.detected-project b{font-size:14px}
|
|
1955
|
+
.detected-project small{font-size:11px}
|
|
1956
|
+
.detected-project__meta{gap:6px}
|
|
1957
|
+
|
|
1958
|
+
/* [Connect project header] Title was squeezed to a 3-line column on the left with the two
|
|
1959
|
+
buttons crammed to the right. Stack it: full-width title on top, buttons full-width below. */
|
|
1960
|
+
.card-head:has(.existing-project-actions){flex-direction:column;align-items:stretch;gap:10px}
|
|
1961
|
+
.existing-project-actions{width:100%;flex-direction:column}
|
|
1962
|
+
.existing-project-actions .icon-btn2{width:100%;justify-content:center}
|
|
1963
|
+
|
|
1964
|
+
/* [Donate modal] The QR cards went full-width (one per row) and filled the screen. Cap each
|
|
1965
|
+
card so the QR is just big enough to scan comfortably, centered in the modal. */
|
|
1966
|
+
.donate-grid article{max-width:248px;width:100%;margin:0 auto}
|
|
1967
|
+
|
|
1968
|
+
/* [Skills/plugins] Mobile: tighter padding + smaller text (see the global feature-card design
|
|
1969
|
+
block below for the one-line name + inline toggle layout). */
|
|
1970
|
+
.feature-card{padding:14px}
|
|
1971
|
+
.feature-name{font-size:13.5px!important}
|
|
1972
|
+
.feature-desc{font-size:12px!important}
|
|
1973
|
+
|
|
1974
|
+
/* [Maintenance row] Restart / Rebuild / Grant-disk were locked to a 34px height in one row,
|
|
1975
|
+
so "Cấp quyền ổ đĩa" overflowed. Let them wrap and grow vertically. */
|
|
1976
|
+
.bot-docker-actions{flex-wrap:wrap}
|
|
1977
|
+
.bot-docker-actions button{height:auto!important;min-height:40px;white-space:normal;line-height:1.2;flex:1 1 92px}
|
|
1978
|
+
|
|
1979
|
+
/* [File tree] Give the tree a touch more room on mobile and let the editor breathe. */
|
|
1980
|
+
.file-tree{max-height:300px!important}
|
|
1981
|
+
.tree-editor{min-height:300px!important}
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
@media(max-width:520px){
|
|
1985
|
+
/* [Setup OS/mode] Shrink the logo cards: keep them 2-up (compact, less scrolling) with
|
|
1986
|
+
much smaller icons — just large enough to tap comfortably. */
|
|
1987
|
+
.os-grid,.mode-grid{grid-template-columns:repeat(2,minmax(0,1fr))!important}
|
|
1988
|
+
.logo-card{min-height:0!important;gap:8px!important;padding:12px 8px!important;border-radius:14px!important}
|
|
1989
|
+
.logo-card .choice-card__icon{width:40px!important;height:40px!important;border-radius:11px!important}
|
|
1990
|
+
.logo-card .choice-card__icon img{width:24px!important;height:24px!important}
|
|
1991
|
+
.logo-card .choice-card__body strong{font-size:13px!important}
|
|
1992
|
+
.logo-card .choice-card__body small{font-size:10px!important}
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
/* ── Feature (skill/plugin) card design ──────────────────────────────────────
|
|
1996
|
+
Name + a compact native-style toggle sit on ONE line; the name truncates with an ellipsis
|
|
1997
|
+
instead of ever wrapping to a second line. Description goes below, and the wider
|
|
1998
|
+
Open / Update / Install controls (when present) get their own row. */
|
|
1999
|
+
/* min-width:0 lets the card (a grid item, default min-width:auto) shrink below its nowrap
|
|
2000
|
+
content so the name can ellipsis-truncate instead of overflowing the row off-screen. */
|
|
2001
|
+
.feature-card{min-width:0}
|
|
2002
|
+
.feature-head{display:flex;align-items:center;justify-content:space-between;gap:10px;min-width:0}
|
|
2003
|
+
.feature-title{display:flex;align-items:center;gap:8px;min-width:0;flex:1 1 auto}
|
|
2004
|
+
.feature-name{display:block;font-size:15px;font-weight:800;line-height:1.2;margin:0;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
2005
|
+
.feature-desc{margin:8px 0 0;color:var(--body);font-size:13px;line-height:1.45}
|
|
2006
|
+
.feature-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}
|
|
2007
|
+
/* Keep the Install / Update / Open buttons small & compact (in line with the toggle), so the
|
|
2008
|
+
"Cài đặt" button isn't the default oversized size. */
|
|
2009
|
+
.feature-actions .secondary,.feature-actions .icon-btn2{height:28px;min-height:0;padding:4px 11px;font-size:11px;font-weight:700;border-radius:8px;border-width:1px}
|
|
2010
|
+
.feature-actions svg{width:13px;height:13px}
|
|
2011
|
+
/* Smaller, native-app style on/off toggle (was 52×30). */
|
|
2012
|
+
.feature-switch span{width:42px;height:24px}
|
|
2013
|
+
.feature-switch span:after{top:2px;left:2px;width:18px;height:18px}
|
|
2014
|
+
.feature-switch input:checked + span:after{left:22px}
|