fix(portable): 微信扫码重定向/Telegram botToken/Windows 启动端口 (v2.1.11)
- 微信扫码卡死: config-server 跟随 IDC scaned_but_redirect 把后续轮询切到 redirect_host,刷新二维码时重置 pollBaseUrl(扫码后不再卡住等不到 confirmed)。新增测试。 - Telegram 渠道改用 botToken(顶层 telegram 只认 botToken,旧 token 别名被静默忽略 → 连不上)。 - Windows-Start: 先起 config-server 再探测 gateway 端口,从 runtime.json 读实际 configServerPort 并按该端口开 Config Center / 传给 wait-gateway(18788 被占也能开)。 - wait-gateway.bat: 用 ping 代替 timeout /t(避开 PATH 上 GNU coreutils timeout 冲突),接受 CONFIG_PORT 参数。 - Windows-Menu: IF 块内 echo 的 () 转义为 ^(^),修 v2.1.10 闪退回归。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -477,12 +477,12 @@ echo Fetching latest version...
|
||||
for /f "tokens=*" %%v in ('"%NODE_BIN%" -e "const https=require('https');https.get('https://registry.npmmirror.com/openclaw/latest',r=>{let d='';r.on('data',c=>d+=c);r.on('end',()=>{try{console.log(JSON.parse(d).version)}catch(e){console.log('error')}})})" 2^>nul') do set LATEST_VER=%%v
|
||||
|
||||
if "!LATEST_VER!"=="" (
|
||||
echo Could not fetch latest version (network issue?)
|
||||
echo Could not fetch latest version ^(network issue?^)
|
||||
pause
|
||||
goto :menu
|
||||
)
|
||||
if "!LATEST_VER!"=="error" (
|
||||
echo Could not fetch latest version (network issue?)
|
||||
echo Could not fetch latest version ^(network issue?^)
|
||||
pause
|
||||
goto :menu
|
||||
)
|
||||
@@ -550,7 +550,7 @@ if !BK_COUNT! gtr 3 (
|
||||
echo Cleaned !DEL_NUM! old backups.
|
||||
)
|
||||
) else (
|
||||
echo Backups: !BK_COUNT! (no cleanup needed)
|
||||
echo Backups: !BK_COUNT! ^(no cleanup needed^)
|
||||
)
|
||||
|
||||
REM Clean old logs (>7 days)
|
||||
@@ -598,7 +598,7 @@ if "%plgchoice%"=="b" (
|
||||
echo.
|
||||
echo Common plugins:
|
||||
echo @icesword760/openclaw-wechat - WeChat
|
||||
echo @nicepkg/openclaw-plugin-qq - QQ (community)
|
||||
echo @nicepkg/openclaw-plugin-qq - QQ ^(community^)
|
||||
echo.
|
||||
set /p plgname=" Plugin name (empty to cancel): "
|
||||
if not "!plgname!"=="" (
|
||||
|
||||
@@ -107,7 +107,8 @@ for /f "usebackq tokens=1,* delims==" %%a in (`""%NODE_BIN%" "%UCLAW_DIR%lib\res
|
||||
)
|
||||
if defined NO_PROXY (
|
||||
set "no_proxy=%NO_PROXY%"
|
||||
echo Direct-connect (NO_PROXY): %NO_PROXY%
|
||||
REM Note: no unescaped parens in echo inside this IF block - cmd treats ) as block-end.
|
||||
echo Direct-connect via NO_PROXY: %NO_PROXY%
|
||||
)
|
||||
|
||||
REM Async update check (non-blocking, 5s timeout, silent failure)
|
||||
@@ -134,7 +135,30 @@ if exist "%WECHAT_PLUGIN_SRC%\openclaw.plugin.json" (
|
||||
)
|
||||
)
|
||||
|
||||
REM Find available port
|
||||
REM Start Config Server in background
|
||||
echo Starting Config Center on port 18788...
|
||||
set "CONFIG_SERVER=%UCLAW_DIR%config-server"
|
||||
set "RUNTIME_JSON=%STATE_DIR%\runtime.json"
|
||||
del "%RUNTIME_JSON%" >nul 2>&1
|
||||
start /B "" "%NODE_BIN%" "%CONFIG_SERVER%\server.js" >nul 2>&1
|
||||
|
||||
REM Wait for Config Server with polling instead of a fixed delay.
|
||||
REM It writes runtime.json with the actual fallback port when ready.
|
||||
set /a CFG_TRIES=0
|
||||
:wait_config
|
||||
if exist "%RUNTIME_JSON%" goto :config_ready
|
||||
set /a CFG_TRIES+=1
|
||||
if %CFG_TRIES% geq 20 goto :config_ready
|
||||
ping -n 1 -w 300 127.0.0.1 >nul 2>&1
|
||||
goto :wait_config
|
||||
:config_ready
|
||||
set "CONFIG_PORT=18788"
|
||||
if exist "%RUNTIME_JSON%" (
|
||||
for /f "usebackq tokens=*" %%p in (`powershell -NoProfile -Command "try { (Get-Content -Raw '%RUNTIME_JSON%' | ConvertFrom-Json).configServerPort } catch {}" 2^>nul`) do set "CONFIG_PORT=%%p"
|
||||
)
|
||||
echo Config Center port: %CONFIG_PORT%
|
||||
|
||||
REM Find available gateway port after Config Center has bound its port.
|
||||
set PORT=18789
|
||||
:check_port
|
||||
netstat -an | findstr ":%PORT% " | findstr "LISTENING" >nul 2>&1
|
||||
@@ -152,24 +176,6 @@ if %errorlevel%==0 (
|
||||
echo Starting OpenClaw on port %PORT%...
|
||||
echo.
|
||||
|
||||
REM Start Config Server in background
|
||||
echo Starting Config Center on port 18788...
|
||||
set "CONFIG_SERVER=%UCLAW_DIR%config-server"
|
||||
start /B "" "%NODE_BIN%" "%CONFIG_SERVER%\server.js" >nul 2>&1
|
||||
|
||||
REM Wait for Config Server with polling instead of a fixed delay.
|
||||
REM It is usually ready in under one second. Poll about every 0.3 seconds,
|
||||
REM up to about 6 seconds, then continue anyway.
|
||||
set /a CFG_TRIES=0
|
||||
:wait_config
|
||||
netstat -an | findstr ":18788 " | findstr "LISTENING" >nul 2>&1
|
||||
if %errorlevel%==0 goto :config_ready
|
||||
set /a CFG_TRIES+=1
|
||||
if %CFG_TRIES% geq 20 goto :config_ready
|
||||
ping -n 1 -w 300 127.0.0.1 >nul 2>&1
|
||||
goto :wait_config
|
||||
:config_ready
|
||||
|
||||
REM Do not open Dashboard before the gateway is ready.
|
||||
REM Slow USB drives may need tens of seconds to stage bundled deps.
|
||||
REM Open the local startup page now, and always open Config Center too.
|
||||
@@ -182,11 +188,11 @@ set "LOADING_URL=file:///%LOADING_PATH:\=/%?port=%PORT%&token=uclaw"
|
||||
start "" "%LOADING_URL%"
|
||||
|
||||
echo Opening Config Center...
|
||||
start "" http://127.0.0.1:18788/
|
||||
start "" http://127.0.0.1:%CONFIG_PORT%/
|
||||
|
||||
REM Fallback watcher: if the startup page cannot poll from file URLs,
|
||||
REM keep polling and reopen Config Center after the gateway is ready.
|
||||
start /B "" cmd /c ""%UCLAW_DIR%lib\wait-gateway.bat" %PORT%"
|
||||
start /B "" cmd /c ""%UCLAW_DIR%lib\wait-gateway.bat" %PORT% %CONFIG_PORT%"
|
||||
|
||||
REM Prewarm gateway in the background after it becomes ready.
|
||||
start /B "" "%NODE_BIN%" "%UCLAW_DIR%lib\prewarm.mjs" %PORT% uclaw >nul 2>&1
|
||||
|
||||
@@ -678,7 +678,9 @@ async function saveChannelsAndOpen() {
|
||||
var channels = {};
|
||||
|
||||
var tgToken = document.getElementById('ch-telegram-token').value.trim();
|
||||
if (tgToken) channels.telegram = { enabled: true, token: tgToken, dmPolicy: 'pairing' };
|
||||
// OpenClaw 顶层 telegram 渠道只认 botToken(旧 token 别名仅在 accounts.<id> 里有效)。
|
||||
// 之前写成 token 会被静默忽略 → Telegram 接不上。
|
||||
if (tgToken) channels.telegram = { enabled: true, botToken: tgToken, dmPolicy: 'pairing' };
|
||||
|
||||
var qqId = document.getElementById('ch-qqbot-appid').value.trim();
|
||||
var qqSecret = document.getElementById('ch-qqbot-secret').value.trim();
|
||||
|
||||
@@ -246,7 +246,13 @@ async function handleWeChatStatus(sessionKey) {
|
||||
return { status: 'expired', message: 'Session expired' };
|
||||
}
|
||||
|
||||
const result = await pollWeChatQrStatus(login.apiBaseUrl, login.qrcode);
|
||||
// 状态轮询用 pollBaseUrl(IDC 重定向后会指向新主机);二维码获取/刷新始终用原始
|
||||
// apiBaseUrl(与官方插件一致:refresh 回到固定主机,只有 status 轮询跟随重定向)。
|
||||
const result = await pollWeChatQrStatus(login.pollBaseUrl || login.apiBaseUrl, login.qrcode);
|
||||
// 微信登录状态流转日志(跳过高频的 wait,便于排查"扫码卡死"类问题)。
|
||||
if (result.status && result.status !== 'wait') {
|
||||
console.log(`[wechat] status=${result.status}` + (result.redirect_host ? ` redirect_host=${result.redirect_host}` : ''));
|
||||
}
|
||||
|
||||
if (result.status === 'expired') {
|
||||
// Try to refresh QR code
|
||||
@@ -261,6 +267,8 @@ async function handleWeChatStatus(sessionKey) {
|
||||
login.qrcode = refreshed.qrcode;
|
||||
login.qrcodeUrl = newQr;
|
||||
login.startedAt = Date.now();
|
||||
// 新二维码来自原始主机,重置轮询主机,避免拿新码去轮询旧的重定向主机。
|
||||
login.pollBaseUrl = null;
|
||||
return { status: 'refreshed', qrcodeUrl: newQr };
|
||||
}
|
||||
|
||||
@@ -302,6 +310,18 @@ async function handleWeChatStatus(sessionKey) {
|
||||
};
|
||||
}
|
||||
|
||||
// IDC 重定向:用户扫码后,ilink 服务端可能要求把后续轮询切换到另一个数据中心主机
|
||||
// (status=scaned_but_redirect + redirect_host)。必须跟着切,否则一直轮询旧主机,
|
||||
// 扫码后永远等不到 confirmed——表现为「扫了码却卡死不前进」。
|
||||
// 同款逻辑见官方插件 openclaw-weixin/src/auth/login-qr.ts 的 scaned_but_redirect 分支。
|
||||
if (result.status === 'scaned_but_redirect') {
|
||||
if (result.redirect_host) {
|
||||
login.pollBaseUrl = 'https://' + result.redirect_host;
|
||||
}
|
||||
// 对前端按「已扫码」处理:显示提示并继续轮询,下一轮已指向新主机。
|
||||
return { status: 'scaned' };
|
||||
}
|
||||
|
||||
return { status: result.status };
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,13 @@ REM Slow USB drives can need tens of seconds before the gateway listens.
|
||||
REM
|
||||
REM Windows-Start opens Config Center immediately, so the ready path only
|
||||
REM exits quietly. On timeout, reopen Config Center as a recovery hint.
|
||||
REM Usage (called in background by Windows-Start.bat): wait-gateway.bat PORT
|
||||
REM Usage (called in background by Windows-Start.bat): wait-gateway.bat PORT CONFIG_PORT
|
||||
REM Polls every 2 seconds, up to about 5 minutes.
|
||||
|
||||
set "PORT=%~1"
|
||||
if "%PORT%"=="" set "PORT=18789"
|
||||
set "CONFIG_PORT=%~2"
|
||||
if "%CONFIG_PORT%"=="" set "CONFIG_PORT=18788"
|
||||
|
||||
set /a TRIES=0
|
||||
:wait_loop
|
||||
@@ -16,12 +18,15 @@ netstat -an | findstr ":%PORT% " | findstr "LISTENING" >nul 2>&1
|
||||
if %errorlevel%==0 goto :ready
|
||||
set /a TRIES+=1
|
||||
if %TRIES% geq 150 goto :timeout
|
||||
timeout /t 2 /nobreak >nul
|
||||
REM Use ping (not "timeout /t") for the ~2s delay: if a GNU coreutils "timeout"
|
||||
REM is on PATH (Git/MSYS bin), it rejects "/t" and this poll becomes a busy-loop.
|
||||
REM ping for the delay is PATH-robust and matches Windows-Start.bat's wait loop.
|
||||
ping -n 3 127.0.0.1 >nul 2>&1
|
||||
goto :wait_loop
|
||||
|
||||
:ready
|
||||
exit /b 0
|
||||
|
||||
:timeout
|
||||
start "" http://127.0.0.1:18788/
|
||||
start "" http://127.0.0.1:%CONFIG_PORT%/
|
||||
exit /b 1
|
||||
|
||||
68
tests/wechat-login-redirect.test.mjs
Normal file
68
tests/wechat-login-redirect.test.mjs
Normal file
@@ -0,0 +1,68 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { join } from 'node:path';
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
const repoRoot = fileURLToPath(new URL('..', import.meta.url));
|
||||
const server = readFileSync(join(repoRoot, 'portable', 'config-server', 'server.js'), 'utf8');
|
||||
const configUi = readFileSync(join(repoRoot, 'portable', 'config-server', 'public', 'index.html'), 'utf8');
|
||||
|
||||
// The ilink QR API (StatusResponse in openclaw-weixin/src/auth/login-qr.ts) can return
|
||||
// status "scaned_but_redirect" with a redirect_host: after the user scans, polling must
|
||||
// move to a new IDC host or "confirmed" never arrives and the QR screen hangs forever.
|
||||
// The config-server must mirror the plugin's redirect handling.
|
||||
|
||||
test('config-server follows the WeChat scaned_but_redirect IDC redirect', () => {
|
||||
// Status polling uses a redirect-aware host, not the fixed apiBaseUrl directly.
|
||||
assert.match(
|
||||
server,
|
||||
/pollWeChatQrStatus\(\s*login\.pollBaseUrl\s*\|\|\s*login\.apiBaseUrl/,
|
||||
'status polling must use login.pollBaseUrl (falls back to apiBaseUrl)',
|
||||
);
|
||||
|
||||
// On scaned_but_redirect, switch the poll host to redirect_host.
|
||||
assert.match(
|
||||
server,
|
||||
/scaned_but_redirect[\s\S]{0,200}login\.pollBaseUrl\s*=\s*['"]https:\/\/['"]\s*\+\s*result\.redirect_host/,
|
||||
'must set login.pollBaseUrl to https://<redirect_host> on scaned_but_redirect',
|
||||
);
|
||||
|
||||
// The redirect case is reported to the client as "scaned" so it keeps polling.
|
||||
assert.match(
|
||||
server,
|
||||
/scaned_but_redirect[\s\S]{0,260}return\s*\{\s*status:\s*['"]scaned['"]\s*\}/,
|
||||
'scaned_but_redirect should surface as status "scaned" to the client',
|
||||
);
|
||||
});
|
||||
|
||||
test('config-server writes the Telegram bot token under the field OpenClaw reads (botToken)', () => {
|
||||
// OpenClaw's top-level telegram channel schema only reads `botToken` (the legacy
|
||||
// `token` alias is honored only inside accounts.<id>). Writing flat `token` silently
|
||||
// disables Telegram, so the Config Center must write `botToken`.
|
||||
assert.match(
|
||||
configUi,
|
||||
/channels\.telegram\s*=\s*\{[^}]*botToken:\s*tgToken/,
|
||||
'telegram channel must be saved with botToken',
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
configUi,
|
||||
/channels\.telegram\s*=\s*\{[^}]*\btoken:\s*tgToken/,
|
||||
'telegram channel must not use the flat `token` field (ignored by OpenClaw)',
|
||||
);
|
||||
});
|
||||
|
||||
test('config-server resets the poll host when the QR is refreshed', () => {
|
||||
// A refreshed QR comes from the original host, so the redirected poll host must reset,
|
||||
// otherwise the new QR would be polled against a stale redirect host.
|
||||
assert.match(
|
||||
server,
|
||||
/status:\s*'refreshed'[\s\S]{0,400}/,
|
||||
'refresh branch should exist',
|
||||
);
|
||||
assert.match(
|
||||
server,
|
||||
/login\.pollBaseUrl\s*=\s*null;[\s\S]{0,200}status:\s*'refreshed'/,
|
||||
'QR refresh must reset login.pollBaseUrl before returning refreshed',
|
||||
);
|
||||
});
|
||||
@@ -50,7 +50,7 @@ test('Windows startup keeps Config Center available even after model setup', ()
|
||||
|
||||
assert.match(
|
||||
script,
|
||||
/Opening Config Center[\s\S]*start "" http:\/\/127\.0\.0\.1:18788\//,
|
||||
/Opening Config Center[\s\S]*start "" http:\/\/127\.0\.0\.1:%CONFIG_PORT%\//,
|
||||
'Windows-Start.bat should always open Config Center for model/channel changes',
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
@@ -65,7 +65,7 @@ test('Windows gateway fallback does not force-open Dashboard', () => {
|
||||
|
||||
assert.match(
|
||||
script,
|
||||
/:timeout[\s\S]*start "" http:\/\/127\.0\.0\.1:18788\//,
|
||||
/:timeout[\s\S]*start "" http:\/\/127\.0\.0\.1:%CONFIG_PORT%\//,
|
||||
'wait-gateway.bat should return users to Config Center on timeout',
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
@@ -147,6 +147,48 @@ test('OpenClaw doctor launcher is read-only (no destructive repair flags)', () =
|
||||
assert.doesNotMatch(bat, /doctor[^\n]*--force/);
|
||||
});
|
||||
|
||||
// cmd.exe treats ')' as the end of an IF/FOR ( ... ) block, so an unescaped paren in
|
||||
// an `echo` *inside* a block aborts parsing ("was unexpected at this time") and the
|
||||
// window flash-closes. This shipped once in v2.1.10 (echo Direct-connect (NO_PROXY)).
|
||||
// Track block depth structurally and flag any unescaped ( or ) in echoes inside a block.
|
||||
function unescapedParenEchoesInsideBlocks(bat) {
|
||||
const offenders = [];
|
||||
let depth = 0;
|
||||
for (const raw of bat.split(/\r?\n/)) {
|
||||
const line = raw.trim();
|
||||
if (depth > 0 && /^echo\b/i.test(line)) {
|
||||
// cmd eats an unescaped ')' that sits at the END of an echo (cosmetic: the ')'
|
||||
// just disappears). The flash-exit only happens when a ')' is followed by more
|
||||
// text on the line (e.g. "(NO_PROXY): value" → ')' closes the block, ": value"
|
||||
// then errors). So: drop escaped ^), drop trailing ')'/whitespace, and flag any
|
||||
// ')' that survives (meaning it had text after it).
|
||||
let s = line.replace(/\^\)/g, '').replace(/[)\s]+$/, '');
|
||||
if (s.includes(')')) offenders.push(raw.trim());
|
||||
}
|
||||
// structural depth: ') else (' keeps depth; leading ')' closes; if/for line ending in '(' opens
|
||||
if (/^\)\s*else\b.*\($/i.test(line)) { /* same depth */ }
|
||||
else if (/^\)/.test(line)) depth = Math.max(0, depth - 1);
|
||||
if (/^\($/.test(line) || /\b(if|for)\b.*[^^]\(\s*$/i.test(line)) depth += 1;
|
||||
}
|
||||
return offenders;
|
||||
}
|
||||
|
||||
test('Windows launchers have no unescaped parens in echoes inside IF/FOR blocks (v2.1.10 flash-exit regression)', () => {
|
||||
for (const name of [
|
||||
'Windows-Start.bat',
|
||||
'Windows-IntranetFix.bat',
|
||||
'Windows-LocalModel.bat',
|
||||
'OpenClaw-Doctor.bat',
|
||||
'Windows-Diagnose.bat',
|
||||
'Windows-Menu.bat',
|
||||
'Windows-Install.bat',
|
||||
]) {
|
||||
const bat = readRepoFile('portable', name);
|
||||
const offenders = unescapedParenEchoesInsideBlocks(bat);
|
||||
assert.deepEqual(offenders, [], `${name} has unescaped parens in block echo(es): ${offenders.join(' | ')}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('PowerShell installer generated start.bat disables OpenClaw bonjour discovery', () => {
|
||||
const script = readRepoFile('install', 'install.ps1');
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "u-claw",
|
||||
"version": "2.1.10",
|
||||
"version": "2.1.11",
|
||||
"description": "U-Claw - AI 助手桌面版,插上 U 盘就能用",
|
||||
"main": "src/main.js",
|
||||
"author": "U-Claw <hello@u-claw.org> (https://u-claw.org)",
|
||||
|
||||
Reference in New Issue
Block a user