@@ -1978,6 +1919,19 @@ const I18N = {
'actions.setupEmpty':'请填写路径','actions.setupSwitching':'目录已切换,正在刷新...','actions.setupInvalid':'路径无效','actions.setupReqFail':'请求失败: ',
'common.loading':'加载中...','common.close':'关闭',
'btn.save':'保存','btn.delete':'删除','btn.cancel':'取消','btn.add':'添加','btn.remove':'移除',
+ 'agents.addAgent':'+ Add Agent','agents.addSub':'+ Add Sub-Agent',
+ 'agents.addAgentTitle':'Add Agent (Main Bot)','agents.addSubTitle':'Add Sub-Agent',
+ 'agents.botToken':'Bot Token','agents.botName':'Bot Name','agents.botNamePh':'My Bot',
+ 'agents.addAgentSubmit':'Create Agent','agents.addSubSubmit':'Create Sub-Agent',
+ 'agents.errToken':'Please enter Bot Token','agents.errName':'Please enter Bot name',
+ 'agents.agentCreated':'✅ Agent created',
+ 'guide.title':'📖 Setup Guide','guide.agent.s1':'Open Telegram, search for
@BotFather',
+ 'guide.agent.s2':'Send
/newbot and follow prompts to name your Bot',
+ 'guide.agent.s3':'Copy the
Bot Token from BotFather and paste below',
+ 'guide.agent.s4':'Send
/setprivacy → select your Bot → click
Disable (allow Bot to read group messages)',
+ 'guide.sub.s1':'Add the Bot to your target Telegram group',
+ 'guide.sub.s2':'Send a message in the group, find
peer.id (negative number) in gateway logs',
+ 'guide.sub.s3':'Fill in the Group ID and Agent config below',
'agents.empty':'暂无 Agent','agents.main':'主 Agent','agents.bound':'已绑群',
'agents.saveModel':'保存模型','agents.viewFiles':'查看文件',
'agents.defaultModel':'使用全局默认','agents.custom':'自定义','agents.noModel':'默认',
@@ -2071,6 +2025,19 @@ const I18N = {
'actions.setupEmpty':'Please enter a path','actions.setupSwitching':'Directory switched, refreshing...','actions.setupInvalid':'Invalid path','actions.setupReqFail':'Request failed: ',
'common.loading':'Loading...','common.close':'Close',
'btn.save':'Save','btn.delete':'Delete','btn.cancel':'Cancel','btn.add':'Add','btn.remove':'Remove',
+ 'agents.addAgent':'+ Add Agent','agents.addSub':'+ Add Sub-Agent',
+ 'agents.addAgentTitle':'Add Agent (Main Bot)','agents.addSubTitle':'Add Sub-Agent',
+ 'agents.botToken':'Bot Token','agents.botName':'Bot Name','agents.botNamePh':'My Bot',
+ 'agents.addAgentSubmit':'Create Agent','agents.addSubSubmit':'Create Sub-Agent',
+ 'agents.errToken':'Please enter Bot Token','agents.errName':'Please enter Bot name',
+ 'agents.agentCreated':'✅ Agent created',
+ 'guide.title':'📖 Setup Guide','guide.agent.s1':'Open Telegram, search for
@BotFather',
+ 'guide.agent.s2':'Send
/newbot and follow prompts to name your Bot',
+ 'guide.agent.s3':'Copy the
Bot Token from BotFather and paste below',
+ 'guide.agent.s4':'Send
/setprivacy → select your Bot → click
Disable (allow Bot to read group messages)',
+ 'guide.sub.s1':'Add the Bot to your target Telegram group',
+ 'guide.sub.s2':'Send a message in the group, find
peer.id (negative number) in gateway logs',
+ 'guide.sub.s3':'Fill in the Group ID and Agent config below',
'agents.empty':'No Agents','agents.main':'Main Agent','agents.bound':'Bound',
'agents.saveModel':'Save Model','agents.viewFiles':'View Files',
'agents.defaultModel':'Use Global Default','agents.custom':'custom','agents.noModel':'Default',
@@ -2187,53 +2154,6 @@ const LANDING_TEXT = {
enterBtn: 'Enter Sub-agent Mode →',
},
};
-let landingLang = LS.get('ocm_lang', 'zh');
-function setLandingLang(l) {
- try {
- landingLang = l;
- lang = l;
- LS.set('ocm_lang', l);
- document.getElementById('lbtn-zh').className = 'lang-btn' + (l === 'zh' ? ' active' : '');
- document.getElementById('lbtn-en').className = 'lang-btn' + (l === 'en' ? ' active' : '');
- const tx = LANDING_TEXT[l] || LANDING_TEXT.zh;
- document.getElementById('l-sub').textContent = tx.sub;
- document.getElementById('l-active-badge').textContent = tx.activeBadge;
- document.getElementById('l-soon-badge').textContent = tx.soonBadge;
- document.getElementById('l-sub-title').textContent = tx.subTitle;
- document.getElementById('l-sub-desc').textContent = tx.subDesc;
- document.getElementById('l-sub-reqs').innerHTML = tx.subReqs;
- document.getElementById('l-multi-title').textContent = tx.multiTitle;
- document.getElementById('l-multi-desc').textContent = tx.multiDesc;
- document.getElementById('l-multi-reqs').innerHTML = tx.multiReqs;
- } catch(e) { console.error('setLandingLang error:', e); }
-}
-function enterApp() {
- try {
- document.getElementById('landingPage').style.display = 'none';
- document.getElementById('mainApp').style.display = '';
- applyLang();
- checkStatus().then(() => loadAll());
- startHealthPolling();
- } catch(e) {
- console.error('enterApp error:', e);
- const lp = document.getElementById('landingPage');
- const ma = document.getElementById('mainApp');
- if(lp) lp.style.display = 'none';
- if(ma) ma.style.display = '';
- checkStatus().then(() => loadAll()).catch(console.error);
- }
-}
-function backToLanding() {
- document.getElementById('landingPage').style.display = 'flex';
- document.getElementById('mainApp').style.display = 'none';
-}
-
-// On load: always show landing page(清除可能残留的旧 ocm_mode 值)
-(function() {
- LS.del('ocm_mode');
- try { setLandingLang(landingLang); } catch(e) { console.error('Landing init error:', e); }
-})();
-
// ── 全局状态 ────────────────────────────────────────────────
let S = { agents:[], channels:[], models:{}, authProfiles:{}, knownModels:[], authProviders:[], primaryModel:'', fallbacks:[] };
let wizCur = 1;
@@ -2301,29 +2221,180 @@ async function loadChannels(){
}
// ── 渲染 Agents ─────────────────────────────────────────────
-function renderAgents(){
- const el=document.getElementById('agentList');
- if(!S.agents.length){el.innerHTML='
'+t('agents.empty')+'
';return;}
- el.innerHTML=S.agents.map(a=>{
- const isMain=a.id==='main';
- const selOpts=buildModelOpts(a.effectiveModel!=='默认'?a.effectiveModel:'__default__');
- return \`
-
- \${esc(a.name||a.id)}
- \${isMain?t('agents.main'):'subagent'}
- \${a.groupId?''+t('agents.bound')+'':''}
-
- \${a.groupId?\`
📱 \${esc(a.groupId)}
\`:''}
-
🧠 \${esc(a.effectiveModel)}
- \${a.workspace?\`
📁 \${esc(a.workspace)}
\`:''}
-
-
-
- \${!isMain?\`
- \`:''}
-
-
\`;
- }).join('');
+// ── Show Add Form ─────────────────────────────────────────────
+function clearAddForm() { document.getElementById('addFormArea').innerHTML = ''; }
+function showAddForm(type) {
+ const area = document.getElementById('addFormArea');
+ if (type === 'agent') {
+ area.innerHTML = buildAddAgentForm();
+ } else {
+ area.innerHTML = buildAddSubForm();
+ }
+ applyLang();
+}
+
+function buildAddAgentForm() {
+ const modelOpts = buildModelOpts('__default__');
+ return '
';
+}
+
+function buildAddSubForm() {
+ // Build parent agent dropdown (only main agents)
+ const cfg = S.agents || [];
+ const mainAgent = cfg.find(a => a.id === 'main');
+ const parentOpts = mainAgent ? '
' : '
';
+ const modelOpts = buildModelOpts('__default__');
+ return '
';
+}
+
+// ── Submit Add Agent (Main Bot) ──────────────────────────────
+async function submitAddAgent() {
+ const token = document.getElementById('fa-token').value.trim();
+ const name = document.getElementById('fa-name').value.trim();
+ const model = document.getElementById('fa-model').value;
+ if (!token) { toast(t('agents.errToken'), 'err'); return; }
+ if (!name) { toast(t('agents.errName'), 'err'); return; }
+ try {
+ const r = await api('POST', '/api/agents/main', { botToken: token, name, model: model === '__default__' ? '' : model });
+ if (r.error) { toast(r.error, 'err'); return; }
+ toast(t('agents.agentCreated'), 'ok');
+ document.getElementById('addFormArea').innerHTML = '';
+ showRestartBanner();
+ await loadAll();
+ } catch (e) { toast(e.message, 'err'); }
+}
+
+// ── Submit Add Sub-Agent ────────────────────────────────────
+async function submitAddSub() {
+ const groupId = document.getElementById('fs-gid').value.trim();
+ const agentId = document.getElementById('fs-aid').value.trim();
+ const displayName = document.getElementById('fs-name').value.trim();
+ const model = document.getElementById('fs-model').value;
+ const purpose = document.getElementById('fs-purpose').value.trim();
+ const personality = document.getElementById('fs-soul').value.trim();
+ const initialMemory = document.getElementById('fs-mem').value.trim();
+ if (!groupId) { toast(t('wiz.errGroupId'), 'err'); return; }
+ if (!agentId) { toast(t('wiz.errAgentId'), 'err'); return; }
+ if (!/^[a-zA-Z0-9_-]+$/.test(agentId)) { toast(t('wiz.errIdFormat'), 'err'); return; }
+ if (agentId.toLowerCase() === 'main') { toast(t('wiz.errIdReserved'), 'err'); return; }
+ try {
+ const r = await api('POST', '/api/agents', { agentId, displayName: displayName || agentId, groupId, workspaceFolder: agentId, model: model === '__default__' ? '' : model, purpose, personality, initialMemory });
+ if (r.error) { toast(r.error, 'err'); return; }
+ toast(t('wiz.created'), 'ok');
+ document.getElementById('addFormArea').innerHTML = '';
+ showRestartBanner();
+ await loadAll();
+ } catch (e) { toast(e.message, 'err'); }
+}
+
+// ── Render Agent Tree ──────────────────────────────────────
+function renderAgents() {
+ const el = document.getElementById('agentTree');
+ if (!S.agents.length) { el.innerHTML = '
' + t('agents.empty') + '
'; return; }
+
+ // Build tree: main agent as root, subagents as children
+ const mainAgent = S.agents.find(a => a.id === 'main');
+ const subs = S.agents.filter(a => a.id !== 'main');
+
+ let html = '';
+
+ // Main agent tree
+ if (mainAgent) {
+ html += '
';
+ html += '
';
+ html += '
🤖 ' + esc(mainAgent.name || 'main') + ' ' + t('agents.main') + '
';
+ html += '
🧠 ' + esc(mainAgent.effectiveModel) + '
';
+ if (mainAgent.workspace) html += '
📁 ' + esc(mainAgent.workspace) + '
';
+ html += '
';
+ html += '';
+ html += '';
+ html += '
';
+
+ // Sub-agents as children
+ if (subs.length) {
+ html += '
';
+ subs.forEach(a => {
+ html += '
';
+ html += '
📱 ' + esc(a.name || a.id);
+ if (a.groupId) html += ' ' + esc(a.groupId) + '';
+ html += '
';
+ html += '
🧠 ' + esc(a.effectiveModel) + '
';
+ if (a.workspace) html += '
📁 ' + esc(a.workspace) + '
';
+ html += '
';
+ html += '';
+ html += '';
+ html += '';
+ html += '';
+ html += '
';
+ });
+ html += '
';
+ }
+ html += '
';
+ } else if (subs.length) {
+ // No main agent but has subs (edge case)
+ subs.forEach(a => {
+ html += '
';
+ html += '
📱 ' + esc(a.name || a.id);
+ if (a.groupId) html += ' ' + esc(a.groupId) + '';
+ html += '
';
+ html += '
🧠 ' + esc(a.effectiveModel) + '
';
+ if (a.workspace) html += '
📁 ' + esc(a.workspace) + '
';
+ html += '
';
+ html += '';
+ html += '';
+ html += '';
+ html += '';
+ html += '
';
+ });
+ }
+
+ el.innerHTML = html;
}
function buildModelOpts(selected){
@@ -2633,107 +2704,6 @@ function copyText(txt){
navigator.clipboard.writeText(txt).then(()=>toast('已复制','success')).catch(()=>toast('复制失败','error'));
}
-// ── 向导: 新建 Agent ─────────────────────────────────────────
-function openCreateWizard(){
- wizCur=1;
- ['f-gid','f-aid','f-name','f-folder','f-purpose','f-soul','f-mem'].forEach(id=>{
- const el=document.getElementById(id); if(el) el.value='';
- });
- document.getElementById('createResult').innerHTML='';
- buildWizModelSel(); updateWizUI(); openModal('createModal'); applyLang();
-}
-function buildWizModelSel(){
- const sel=document.getElementById('f-model');
- sel.innerHTML=\`
\`;
- S.knownModels.filter(m=>m.id!=='__default__').forEach(m=>{
- sel.innerHTML+=\`
\`;
- });
-}
-function wizStep(d){
- if(d===1){
- if(!validateWizStep(wizCur))return;
- if(wizCur===4){submitCreateAgent();return;}
- wizCur++;
- if(wizCur===4)buildPreview();
- } else { wizCur=Math.max(1,wizCur-1); }
- updateWizUI();
-}
-function updateWizUI(){
- for(let i=1;i<=4;i++){
- document.getElementById('sp'+i).className='step-page'+(i===wizCur?' active':'');
- document.getElementById('si'+i).className='step'+(i===wizCur?' active':i
1?'':'none';
- document.getElementById('wNext').textContent=wizCur===4?t('wiz.confirm'):t('wiz.next');
-}
-function validateWizStep(step){
- if(step!==1)return true;
- let ok=true;
- const gid=document.getElementById('f-gid').value.trim();
- const aid=document.getElementById('f-aid').value.trim();
- const ge=document.getElementById('e-gid'); const ae=document.getElementById('e-aid');
- if(!gid){ge.textContent=t('wiz.errGroupId');ok=false;}else ge.textContent='';
- if(!aid){ae.textContent=t('wiz.errAgentId');ok=false;}
- else if(!/^[a-zA-Z0-9_-]+$/.test(aid)){ae.textContent=t('wiz.errIdFormat');ok=false;}
- else if(aid.toLowerCase()==='main'){ae.textContent=t('wiz.errIdReserved');ok=false;}
- else if(S.agents.find(a=>a.id===aid)){ae.textContent=t('wiz.errIdDup');ok=false;}
- else ae.textContent='';
- return ok;
-}
-function autoFillId(){
- const aid=document.getElementById('f-aid');
- if(!aid.value){
- const gid=document.getElementById('f-gid').value.replace(/[^0-9]/g,'').slice(-6);
- if(gid) aid.value='agent_'+gid;
- }
-}
-function validateId(){
- const v=document.getElementById('f-aid').value;
- const e=document.getElementById('e-aid');
- if(!v){e.textContent='';return;}
- if(!/^[a-zA-Z0-9_-]+$/.test(v)) e.textContent=t('wiz.errIdFormat');
- else if(v.toLowerCase()==='main') e.textContent=t('wiz.errIdReserved');
- else e.textContent='';
-}
-function buildPreview(){
- const aid=document.getElementById('f-aid').value.trim();
- const name=document.getElementById('f-name').value.trim()||aid;
- const gid=document.getElementById('f-gid').value.trim();
- const folder=document.getElementById('f-folder').value.trim()||aid;
- const model=document.getElementById('f-model').value;
- const soul=document.getElementById('f-soul').value.trim();
- document.getElementById('previewBox').innerHTML=\`
- \${t('wiz.preview')}
- 🤖 Agent: \${aid} (\${esc(name)})
- 📱 \${t('wiz.group')}: \${gid}
- 🧠 \${t('wiz.modelLabel')}: \${model==='__default__'?t('wiz.globalDefault')+' ('+S.primaryModel+')':model}
- 📁 Workspace: ~/.openclaw/workspaces/\${folder}
- SOUL.md: \${soul?t('wiz.soulYes'):t('wiz.soulDefault')}
- \${t('wiz.autoBackup')}
- \`;
-}
-async function submitCreateAgent(){
- const btn=document.getElementById('wNext');
- btn.disabled=true; btn.textContent=t('wiz.creating');
- try{
- const r=await api('POST','/api/agents',{
- agentId:document.getElementById('f-aid').value.trim(),
- displayName:document.getElementById('f-name').value.trim(),
- groupId:document.getElementById('f-gid').value.trim(),
- workspaceFolder:document.getElementById('f-folder').value.trim(),
- model:document.getElementById('f-model').value,
- purpose:document.getElementById('f-purpose').value.trim(),
- personality:document.getElementById('f-soul').value.trim(),
- initialMemory:document.getElementById('f-mem').value.trim(),
- });
- document.getElementById('createResult').innerHTML=
- \`\${r.notes.map(n=>\`- \${n}
\`).join('')}
\`;
- btn.textContent=t('wiz.created'); btn.disabled=false;
- await loadAgents(); await loadChannels(); showRestartBanner();
- setTimeout(()=>closeModal('createModal'),3500);
- toast('Subagent '+r.agentId+' 创建成功!','success');
- }catch(e){toast('创建失败: '+e.message,'error'); btn.disabled=false; btn.textContent='✅ 确认创建';}
-}
async function deleteAgent(agentId, name){
if(!confirm(\`确定要删除 Agent "\${name}"?\\n\\n• 将从 openclaw.json 移除(自动备份)\\n• Workspace 目录不删除\\n• 可通过"回滚"功能恢复配置\\n\\n继续?\`))return;
@@ -3421,7 +3391,15 @@ document.querySelectorAll('.tab').forEach(t=>{
if(t.dataset.tab==='stats') loadStats();
if(t.dataset.tab==='cron') loadCrons();
});
-});`;
+});
+
+// ── On load: initialize app ──────────────────────────────────
+(function() {
+ LS.del('ocm_mode');
+ applyLang();
+ checkStatus().then(() => loadAll()).catch(e => console.error('Init error:', e));
+ startHealthPolling();
+})();`;
const MAIN_HTML = `
diff --git a/start.bat b/start.bat
index 7e4153f..09f616e 100644
--- a/start.bat
+++ b/start.bat
@@ -1,13 +1,13 @@
@echo off
:: ================================================================
-:: OpenClaw Manager v0.5.2 — Windows 启动脚本
+:: OpenClaw Manager v0.6.0 — Start Script (Windows)
::
-:: 特性:
-:: * 自动检测 Node.js,未安装时给出安装指引(winget/scoop/官网)
-:: * 自动检测 %USERPROFILE%\.openclaw 目录
-:: * 首次运行自动创建 manager-config.json
-:: * 自动检测端口占用并建议替代端口
-:: * 支持 --dir / --port / --help 参数
+:: Features:
+:: * Auto-detect Node.js, show install instructions if missing
+:: * Auto-detect %USERPROFILE%\.openclaw directory
+:: * Auto-create manager-config.json on first run
+:: * Auto-kill previous OCM process if port is in use
+:: * Supports --dir / --port / --host / --help
:: ================================================================
setlocal enabledelayedexpansion
chcp 65001 >nul 2>&1
@@ -19,48 +19,47 @@ set "MIN_NODE_MAJOR=18"
echo.
echo ===================================
-echo OpenClaw Manager v0.5.2
+echo OpenClaw Manager v0.6.0
echo -----------------------------------
echo.
-:: ── 检查主文件 ────────────────────────────────────────────────
+:: ── Check main file ─────────────────────────────────────────
if not exist "%MANAGER_JS%" (
- echo [X] 找不到 openclaw-manager.js
- echo 期望路径: %MANAGER_JS%
+ echo [X] Cannot find openclaw-manager.js
+ echo Expected: %MANAGER_JS%
echo.
pause
exit /b 1
)
-:: ── 检查 Node.js ─────────────────────────────────────────────
+:: ── Check Node.js ───────────────────────────────────────────
where node >nul 2>&1
if errorlevel 1 (
- echo [X] 未找到 Node.js
+ echo [X] Node.js not found
echo.
- echo 安装方式(选一种即可):
+ echo Install Node.js (pick one):
echo.
echo 1^) winget: winget install OpenJS.NodeJS.LTS
echo 2^) scoop: scoop install nodejs-lts
- echo 3^) 官网下载: https://nodejs.org/
+ echo 3^) Official: https://nodejs.org/
echo 4^) nvm-windows: https://github.com/coreybutler/nvm-windows
echo.
pause
exit /b 1
)
-:: 获取主版本号并检查
for /f "tokens=1 delims=." %%v in ('node -e "process.stdout.write(process.versions.node)"') do (
set "NODE_MAJOR=%%v"
)
if !NODE_MAJOR! LSS %MIN_NODE_MAJOR% (
- echo [!] Node.js 版本过低(需要 ^>= v%MIN_NODE_MAJOR%)
+ echo [!] Node.js too old (need ^>= v%MIN_NODE_MAJOR%)
echo.
- echo 安装方式(选一种即可):
+ echo Install Node.js (pick one):
echo.
echo 1^) winget: winget install OpenJS.NodeJS.LTS
echo 2^) scoop: scoop install nodejs-lts
- echo 3^) 官网下载: https://nodejs.org/
+ echo 3^) Official: https://nodejs.org/
echo.
pause
exit /b 1
@@ -70,9 +69,10 @@ for /f "tokens=*" %%v in ('node -e "process.stdout.write('v'+process.versions.no
echo [OK] Node.js %%v
)
-:: ── 解析参数 ─────────────────────────────────────────────────
+:: ── Parse arguments ─────────────────────────────────────────
set "DIR_ARG="
set "PORT_ARG="
+set "HOST_ARG="
set "EXTRA_ARGS="
:parse_args
@@ -89,23 +89,29 @@ if /i "%~1"=="--port" (
shift & shift
goto :parse_args
)
+if /i "%~1"=="--host" (
+ set "HOST_ARG=%~2"
+ shift & shift
+ goto :parse_args
+)
set "EXTRA_ARGS=!EXTRA_ARGS! %~1"
shift
goto :parse_args
:show_help
-echo 用法: start.bat [选项]
+echo Usage: start.bat [options]
echo.
-echo 选项:
-echo --dir ^<路径^> OpenClaw 配置目录(默认 %%USERPROFILE%%\.openclaw)
-echo --port ^<端口^> 监听端口(默认 3333)
-echo --help 显示帮助
+echo Options:
+echo --dir ^ OpenClaw config directory (default: %%USERPROFILE%%\.openclaw)
+echo --port ^ Listen port (default: 3333)
+echo --host ^ Bind address (default: 0.0.0.0)
+echo --help Show this help
echo.
exit /b 0
:after_args
-:: ── 检测 OpenClaw 配置目录 ───────────────────────────────────
+:: ── Detect OpenClaw config directory ────────────────────────
set "OPENCLAW_DIR="
if defined DIR_ARG (
set "OPENCLAW_DIR=!DIR_ARG!"
@@ -117,54 +123,48 @@ if defined DIR_ARG (
if not defined OPENCLAW_DIR set "OPENCLAW_DIR=%USERPROFILE%\.openclaw"
if exist "!OPENCLAW_DIR!" (
- echo [OK] 配置目录: !OPENCLAW_DIR!
+ echo [OK] Config dir: !OPENCLAW_DIR!
) else (
- echo [!] 配置目录不存在: !OPENCLAW_DIR!
- echo 首次使用?请先运行 openclaw onboard 初始化。
+ echo [!] Config dir not found: !OPENCLAW_DIR!
+ echo First time? Run: openclaw onboard
)
-:: ── 首次运行:自动创建 manager-config.json ────────────────────
+:: ── First run: auto-create manager-config.json ──────────────
if not exist "%CONFIG_JSON%" (
- echo [-^>] 首次运行,创建 manager-config.json
+ echo [-^>] First run, creating manager-config.json
node -e "require('fs').writeFileSync('%CONFIG_JSON%',JSON.stringify({dir:'~/.openclaw'},null,2))"
)
-:: ── 检查端口可用性 ───────────────────────────────────────────
+:: ── Port handling: kill old process if occupied ─────────────
set "PORT=3333"
if defined PORT_ARG set "PORT=!PORT_ARG!"
-netstat -an 2>nul | findstr /r ":%PORT% .*LISTENING" >nul 2>&1
-if not errorlevel 1 (
- echo [!] 端口 !PORT! 已被占用
- for %%p in (3334 3335 3336 8080 8888) do (
- netstat -an 2>nul | findstr /r ":%%p .*LISTENING" >nul 2>&1
- if errorlevel 1 (
- echo 尝试备用端口 %%p...
- set "PORT=%%p"
- set "PORT_ARG=%%p"
- goto :port_ok
- )
- )
+:: Find PID using the port and kill it
+for /f "tokens=5" %%p in ('netstat -ano 2^>nul ^| findstr /r ":!PORT! .*LISTENING"') do (
+ echo [!] Port !PORT! in use (PID %%p), stopping old process...
+ taskkill /PID %%p /F >nul 2>&1
+ timeout /t 1 /nobreak >nul 2>&1
+ echo [OK] Old process stopped
)
-:port_ok
-:: ── 构建启动命令 ─────────────────────────────────────────────
+:: ── Build launch command ────────────────────────────────────
set "CMD_ARGS="
if defined DIR_ARG set "CMD_ARGS=!CMD_ARGS! --dir "!DIR_ARG!""
if defined PORT_ARG set "CMD_ARGS=!CMD_ARGS! --port !PORT_ARG!"
+if defined HOST_ARG set "CMD_ARGS=!CMD_ARGS! --host !HOST_ARG!"
if defined EXTRA_ARGS set "CMD_ARGS=!CMD_ARGS! !EXTRA_ARGS!"
echo.
-echo [^>] 启动中 -^> http://localhost:!PORT!
-echo Ctrl+C 停止
+echo [^>] Starting -^> http://localhost:!PORT!
+echo Ctrl+C to stop
echo.
-:: ── 启动 ─────────────────────────────────────────────────────
+:: ── Launch ──────────────────────────────────────────────────
node "%MANAGER_JS%"!CMD_ARGS!
if errorlevel 1 (
echo.
- echo [X] 启动失败,请检查上方错误信息。
+ echo [X] Failed to start. Check errors above.
pause
)
endlocal
diff --git a/start.sh b/start.sh
index 9c26f8c..23ced45 100755
--- a/start.sh
+++ b/start.sh
@@ -1,13 +1,13 @@
#!/usr/bin/env bash
# ================================================================
-# OpenClaw Manager v0.5.2 — 跨平台启动脚本 (macOS / Linux)
+# OpenClaw Manager v0.6.0 — Start Script (macOS / Linux)
#
-# 特性:
-# • 自动检测 Node.js,未安装时给出安装指引
-# • 自动检测 ~/.openclaw 目录,不存在时提示
-# • 首次运行自动创建 manager-config.json
-# • 自动检测端口占用并建议替代端口
-# • 支持 --dir / --port / --help 参数
+# Features:
+# - Auto-detect Node.js, show install instructions if missing
+# - Auto-detect ~/.openclaw directory
+# - Auto-create manager-config.json on first run
+# - Auto-kill previous OCM process if port is in use
+# - Supports --dir / --port / --host / --help
# ================================================================
set -e
@@ -16,62 +16,63 @@ MANAGER_JS="$SCRIPT_DIR/openclaw-manager.js"
CONFIG_JSON="$SCRIPT_DIR/manager-config.json"
MIN_NODE_MAJOR=18
-# ── 颜色 ─────────────────────────────────────────────────────
+# ── Colors ────────────────────────────────────────────────────
RED='\033[0;31m'; YELLOW='\033[1;33m'; GREEN='\033[0;32m'
CYAN='\033[0;36m'; BOLD='\033[1m'; DIM='\033[2m'; RESET='\033[0m'
banner() {
echo ""
- echo -e "${CYAN}${BOLD} 🦀 OpenClaw Manager v0.5.2${RESET}"
+ echo -e "${CYAN}${BOLD} 🦀 OpenClaw Manager v0.6.0${RESET}"
echo -e "${DIM} ─────────────────────────────${RESET}"
echo ""
}
banner
-# ── 检查主文件 ────────────────────────────────────────────────
+# ── Check main file ──────────────────────────────────────────
if [ ! -f "$MANAGER_JS" ]; then
- echo -e "${RED}✗ 找不到 openclaw-manager.js${RESET}"
- echo -e " 期望路径: ${DIM}$MANAGER_JS${RESET}"
+ echo -e "${RED}✗ Cannot find openclaw-manager.js${RESET}"
+ echo -e " Expected: ${DIM}$MANAGER_JS${RESET}"
exit 1
fi
-# ── 检查 Node.js ─────────────────────────────────────────────
+# ── Check Node.js ────────────────────────────────────────────
install_node_hint() {
echo ""
- echo -e " ${BOLD}安装方式(选一种即可):${RESET}"
+ echo -e " ${BOLD}Install Node.js (pick one):${RESET}"
echo ""
if [[ "$(uname)" == "Darwin" ]]; then
echo -e " ${GREEN}1)${RESET} Homebrew: ${CYAN}brew install node${RESET}"
- echo -e " ${GREEN}2)${RESET} 官网下载: ${CYAN}https://nodejs.org/${RESET}"
+ echo -e " ${GREEN}2)${RESET} Official: ${CYAN}https://nodejs.org/${RESET}"
echo -e " ${GREEN}3)${RESET} nvm: ${CYAN}curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash${RESET}"
- echo -e " ${DIM}然后 nvm install --lts${RESET}"
+ echo -e " ${DIM}then: nvm install --lts${RESET}"
else
echo -e " ${GREEN}1)${RESET} apt: ${CYAN}sudo apt install -y nodejs npm${RESET}"
- echo -e " ${GREEN}2)${RESET} 官网下载: ${CYAN}https://nodejs.org/${RESET}"
+ echo -e " ${GREEN}2)${RESET} Official: ${CYAN}https://nodejs.org/${RESET}"
echo -e " ${GREEN}3)${RESET} nvm: ${CYAN}curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash${RESET}"
- echo -e " ${DIM}然后 nvm install --lts${RESET}"
+ echo -e " ${DIM}then: nvm install --lts${RESET}"
fi
echo ""
}
if ! command -v node &>/dev/null; then
- echo -e "${RED}✗ 未找到 Node.js${RESET}"
+ echo -e "${RED}✗ Node.js not found${RESET}"
install_node_hint
exit 1
fi
NODE_VER=$(node -e "process.stdout.write(String(process.versions.node.split('.')[0]))")
if [ "$NODE_VER" -lt "$MIN_NODE_MAJOR" ]; then
- echo -e "${YELLOW}⚠ Node.js 版本过低(当前 v$(node -v),需要 >= v${MIN_NODE_MAJOR})${RESET}"
+ echo -e "${YELLOW}⚠ Node.js too old (current $(node -v), need >= v${MIN_NODE_MAJOR})${RESET}"
install_node_hint
exit 1
fi
echo -e " ${GREEN}✓${RESET} Node.js $(node -v)"
-# ── 解析参数 ─────────────────────────────────────────────────
+# ── Parse arguments ──────────────────────────────────────────
DIR_ARG=""
PORT_ARG=""
+HOST_ARG=""
EXTRA_ARGS=()
while [[ $# -gt 0 ]]; do
@@ -80,20 +81,22 @@ while [[ $# -gt 0 ]]; do
--dir=*) DIR_ARG="${1#*=}"; shift;;
--port) PORT_ARG="$2"; shift 2;;
--port=*) PORT_ARG="${1#*=}"; shift;;
+ --host) HOST_ARG="$2"; shift 2;;
+ --host=*) HOST_ARG="${1#*=}"; shift;;
--help|-h)
- echo "用法: $0 [选项]"
+ echo "Usage: $0 [options]"
echo ""
- echo "选项:"
- echo " --dir <路径> OpenClaw 配置目录(默认 ~/.openclaw)"
- echo " --port <端口> 监听端口(默认 3333)"
- echo " --help 显示帮助"
+ echo "Options:"
+ echo " --dir OpenClaw config directory (default: ~/.openclaw)"
+ echo " --port Listen port (default: 3333)"
+ echo " --host Bind address (default: 0.0.0.0)"
+ echo " --help Show this help"
exit 0;;
*) EXTRA_ARGS+=("$1"); shift;;
esac
done
-# ── 检测 OpenClaw 配置目录 ───────────────────────────────────
-# 优先级: --dir 参数 > manager-config.json > 环境变量 > 默认
+# ── Detect OpenClaw config directory ─────────────────────────
OPENCLAW_DIR=""
if [ -n "$DIR_ARG" ]; then
OPENCLAW_DIR="${DIR_ARG/#\~/$HOME}"
@@ -106,55 +109,53 @@ fi
[ -z "$OPENCLAW_DIR" ] && OPENCLAW_DIR="$HOME/.openclaw"
if [ -d "$OPENCLAW_DIR" ]; then
- echo -e " ${GREEN}✓${RESET} 配置目录: ${DIM}$OPENCLAW_DIR${RESET}"
+ echo -e " ${GREEN}✓${RESET} Config dir: ${DIM}$OPENCLAW_DIR${RESET}"
else
- echo -e " ${YELLOW}⚠${RESET} 配置目录不存在: ${DIM}$OPENCLAW_DIR${RESET}"
- echo -e " ${DIM}首次使用?请先运行 openclaw onboard 初始化。${RESET}"
+ echo -e " ${YELLOW}⚠${RESET} Config dir not found: ${DIM}$OPENCLAW_DIR${RESET}"
+ echo -e " ${DIM}First time? Run: openclaw onboard${RESET}"
fi
-# ── 首次运行:自动创建 manager-config.json ────────────────────
+# ── First run: auto-create manager-config.json ───────────────
if [ ! -f "$CONFIG_JSON" ]; then
- echo -e " ${CYAN}→${RESET} 首次运行,创建 ${DIM}manager-config.json${RESET}"
+ echo -e " ${CYAN}→${RESET} First run, creating ${DIM}manager-config.json${RESET}"
node -e "require('fs').writeFileSync('$CONFIG_JSON',JSON.stringify({dir:'~/.openclaw'},null,2))"
fi
-# ── 检查端口可用性 ───────────────────────────────────────────
+# ── Port handling: kill old process if occupied ──────────────
PORT="${PORT_ARG:-3333}"
-check_port() {
+get_port_pid() {
if command -v lsof &>/dev/null; then
- lsof -i :"$1" -sTCP:LISTEN >/dev/null 2>&1 && return 1
+ lsof -ti :"$1" -sTCP:LISTEN 2>/dev/null || true
elif command -v ss &>/dev/null; then
- ss -tlnp 2>/dev/null | grep -q ":$1 " && return 1
- elif command -v netstat &>/dev/null; then
- netstat -tlnp 2>/dev/null | grep -q ":$1 " && return 1
+ ss -tlnp 2>/dev/null | grep ":$1 " | grep -oP 'pid=\K[0-9]+' || true
fi
- return 0
}
-if ! check_port "$PORT"; then
- echo -e " ${YELLOW}⚠${RESET} 端口 ${PORT} 已被占用"
- # 尝试找一个可用端口
- for ALT in 3334 3335 3336 8080 8888; do
- if check_port "$ALT"; then
- echo -e " ${DIM}尝试备用端口 ${ALT}...${RESET}"
- PORT="$ALT"
- PORT_ARG="$ALT"
- break
- fi
- done
+PORT_PID=$(get_port_pid "$PORT")
+if [ -n "$PORT_PID" ]; then
+ echo -e " ${YELLOW}⚠${RESET} Port ${PORT} in use (PID ${PORT_PID}), stopping old process..."
+ kill "$PORT_PID" 2>/dev/null || true
+ sleep 1
+ # Force kill if still alive
+ if kill -0 "$PORT_PID" 2>/dev/null; then
+ kill -9 "$PORT_PID" 2>/dev/null || true
+ sleep 0.5
+ fi
+ echo -e " ${GREEN}✓${RESET} Old process stopped"
fi
-# ── 构建启动命令 ─────────────────────────────────────────────
+# ── Build launch command ─────────────────────────────────────
CMD_ARGS=()
[ -n "$DIR_ARG" ] && CMD_ARGS+=(--dir "$DIR_ARG")
[ -n "$PORT_ARG" ] && CMD_ARGS+=(--port "$PORT_ARG")
+[ -n "$HOST_ARG" ] && CMD_ARGS+=(--host "$HOST_ARG")
CMD_ARGS+=("${EXTRA_ARGS[@]}")
echo ""
-echo -e " ${CYAN}▶${RESET} 启动中 → ${BOLD}http://localhost:${PORT}${RESET}"
-echo -e " ${DIM}Ctrl+C 停止${RESET}"
+echo -e " ${CYAN}▶${RESET} Starting → ${BOLD}http://localhost:${PORT}${RESET}"
+echo -e " ${DIM}Ctrl+C to stop${RESET}"
echo ""
-# ── 启动 ─────────────────────────────────────────────────────
+# ── Launch ───────────────────────────────────────────────────
exec node "$MANAGER_JS" "${CMD_ARGS[@]}"