fix(ci): align Node v22.22.3 pins and unblock pre-push tests
All checks were successful
Tests / test (push) Successful in 1m8s

Sync install scripts and CI container image with NODE_VERSION, regenerate
Electron Config.html, and skip gitignored portable/app in origin scan.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-18 18:47:41 +08:00
parent dd8e0f55c3
commit 08f22edf44
15 changed files with 201 additions and 25 deletions

View File

@@ -19,7 +19,7 @@ jobs:
# Self-hosted Gitea runners are registered as internal_docker, not ubuntu-latest.
runs-on: internal_docker
container:
image: node:22.22.1
image: node:22.22.3
steps:
- uses: actions/checkout@v4

View File

@@ -15,7 +15,7 @@ jobs:
# Self-hosted Gitea runners are registered as internal_docker, not ubuntu-latest.
runs-on: internal_docker
container:
image: node:22.22.1
image: node:22.22.3
steps:
- uses: actions/checkout@v4

View File

@@ -7,7 +7,7 @@
set -euo pipefail
# ── All constants defined here (independent of any other project files) ──
NODE_VERSION="v22.22.1"
NODE_VERSION="v22.22.3"
NODE_MIRROR="https://nodejs.org/dist"
NODE_OFFICIAL="https://nodejs.org/dist"
NPM_MIRROR="https://registry.npmjs.org"

View File

@@ -59,8 +59,8 @@ if [ -x "$NODE_BIN" ]; then
NODE_VERSION=$("$NODE_BIN" --version 2>/dev/null || echo "")
echo -e "[OK] Node.js ${GREEN}$NODE_VERSION${NC}"
if [ "$NODE_VERSION" != "v22.22.1" ]; then
warn "Node.js version" "Expected v22.22.1, found $NODE_VERSION"
if [ "$NODE_VERSION" != "v22.22.3" ]; then
warn "Node.js version" "Expected v22.22.3, found $NODE_VERSION"
fi
else
echo -e "${RED}[!!] Node.js is missing or will not run${NC}"

View File

@@ -22,7 +22,7 @@ $RUNTIME_DIR = "$UCLAW_DIR\runtime"
$CORE_DIR = "$UCLAW_DIR\core"
$DATA_DIR = "$UCLAW_DIR\data"
$CONFIG_PATH = "$DATA_DIR\.openclaw\openclaw.json"
$NODE_VERSION = "v22.22.1"
$NODE_VERSION = "v22.22.3"
$MIRROR = "https://registry.npmjs.org"
$NODE_MIRROR = "https://nodejs.org/dist"
$OPENCLAW_VERSION = "2026.7.1-2"

View File

@@ -25,7 +25,7 @@ RUNTIME_DIR="$UCLAW_DIR/runtime"
CORE_DIR="$UCLAW_DIR/core"
DATA_DIR="$UCLAW_DIR/data"
CONFIG_PATH="$DATA_DIR/.openclaw/openclaw.json"
NODE_VERSION="v22.22.1"
NODE_VERSION="v22.22.3"
MIRROR="https://registry.npmjs.org"
NODE_MIRROR="https://nodejs.org/dist"
OPENCLAW_VERSION="2026.7.1-2"

View File

@@ -18,7 +18,7 @@ NC='\033[0m'
BOLD='\033[1m'
DIM='\033[2m'
NODE_VER="$(tr -d "[:space:]" < "$UCLAW_DIR/NODE_VERSION" 2>/dev/null || tr -d "[:space:]" < "$UCLAW_DIR/../NODE_VERSION" 2>/dev/null || echo v22.22.1)"
NODE_VER="$(tr -d "[:space:]" < "$UCLAW_DIR/NODE_VERSION" 2>/dev/null || tr -d "[:space:]" < "$UCLAW_DIR/../NODE_VERSION" 2>/dev/null || echo v22.22.3)"
MIRROR="https://registry.npmjs.org"
NODE_MIRROR="https://nodejs.org/dist"

View File

@@ -15,7 +15,7 @@ set "APP_DIR=%UCLAW_DIR%app"
set "INSTALL_TARGET=%USERPROFILE%\.uclaw"
set "MIRROR=https://registry.npmjs.org"
set "NODE_MIRROR=https://nodejs.org/dist"
set "NODE_VER=v22.22.1"
set "NODE_VER=v22.22.3"
REM ---- Step 1: Check environment ----
echo [1/4] Checking

View File

@@ -9,7 +9,7 @@ set "CORE_DIR=%APP_DIR%\core"
set "RUNTIME_DIR=%APP_DIR%\runtime"
set "MIRROR=https://registry.npmjs.org"
set "NODE_MIRROR=https://nodejs.org/dist"
set "NODE_VERSION=v22.22.1"
set "NODE_VERSION=v22.22.3"
set "ALL_PLATFORMS=false"
if "%~1"=="--all-platforms" set "ALL_PLATFORMS=true"

View File

@@ -11,7 +11,7 @@ $runtimeDir = Join-Path $appDir "runtime"
$mirror = "https://registry.npmjs.org"
$nodeMirror = "https://nodejs.org/dist"
$nodeVersion = "v22.22.1"
$nodeVersion = "v22.22.3"
function Write-Step {
param(

View File

@@ -13,7 +13,7 @@ CORE_DIR="$APP_DIR/core"
RUNTIME_DIR="$APP_DIR/runtime"
MIRROR="https://registry.npmjs.org"
NODE_MIRROR="https://nodejs.org/dist"
NODE_VERSION="$(tr -d "[:space:]" < "$SCRIPT_DIR/NODE_VERSION" 2>/dev/null || tr -d "[:space:]" < "$SCRIPT_DIR/../NODE_VERSION" 2>/dev/null || echo v22.22.1)"
NODE_VERSION="$(tr -d "[:space:]" < "$SCRIPT_DIR/NODE_VERSION" 2>/dev/null || tr -d "[:space:]" < "$SCRIPT_DIR/../NODE_VERSION" 2>/dev/null || echo v22.22.3)"
ALL_PLATFORMS=false
[ "$1" = "--all-platforms" ] && ALL_PLATFORMS=true

View File

@@ -8,6 +8,8 @@ const repoRoot = fileURLToPath(new URL('..', import.meta.url));
const origin = JSON.parse(readFileSync(join(repoRoot, 'origin.json'), 'utf8'));
const SKIPPED_DIRS = new Set(['.git', 'node_modules', 'dist', '.download-cache', 'tests']);
// portable/app/ is gitignored — setup downloads Node/OpenClaw there for local runs.
const SKIPPED_PATH_PREFIXES = ['portable/app/'];
// Lockfiles are full of third-party funding and repository links. They say
// nothing about where *this* build fetches from.
const SKIPPED_FILES = new Set(['package-lock.json']);
@@ -17,6 +19,8 @@ function* walk(dir) {
for (const entry of readdirSync(dir)) {
if (SKIPPED_DIRS.has(entry)) continue;
const full = join(dir, entry);
const rel = relative(repoRoot, full);
if (rel === 'portable/app' || SKIPPED_PATH_PREFIXES.some((p) => rel.startsWith(p))) continue;
if (statSync(full).isDirectory()) yield* walk(full);
else yield full;
}

View File

@@ -59,7 +59,9 @@ h1 .lobster { color: #ff6b35; }
}
.form-group input,
input[type="text"],
input[type="password"] {
input[type="password"],
.form-group select,
select {
display: block;
width: 100%;
min-width: 0;
@@ -74,6 +76,20 @@ input[type="password"] {
outline: none;
transition: border-color 0.2s;
}
.form-group select,
select {
padding-right: 36px;
cursor: pointer;
appearance: none;
-webkit-appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='%23888' d='M1 1l5 5 5-5'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 14px center;
}
.form-group select:focus,
select:focus { border-color: #ff6b35; }
.form-group select option,
select option { background: #222; color: #fff; }
input:focus { border-color: #ff6b35; }
.input-wrap { position: relative; display: block; width: 100%; min-width: 0; }
.input-wrap input { padding-right: 70px; }
@@ -114,6 +130,37 @@ input:focus { border-color: #ff6b35; }
.btn-secondary:hover { background: #444; }
.btn-row { display: flex; gap: 10px; margin-top: 20px; justify-content: flex-end; }
/* Key verified: model picker + continue on one row */
.key-actions {
display: flex;
align-items: flex-end;
gap: 12px;
margin-top: 16px;
margin-bottom: 4px;
}
.key-actions .model-picker {
flex: 1;
min-width: 0;
margin-bottom: 0;
}
.key-actions .model-picker label {
margin-bottom: 6px;
font-size: 0.85em;
color: #aaa;
}
.key-actions .btn-primary {
flex-shrink: 0;
min-width: 128px;
min-height: 46px;
padding: 12px 22px;
margin-left: auto;
}
.key-actions.has-model .btn-primary { margin-left: 0; }
@media (max-width: 520px) {
.key-actions { flex-direction: column; align-items: stretch; }
.key-actions .btn-primary { margin-left: 0; width: 100%; }
}
/* Result */
.result { text-align: center; padding: 40px 20px; }
.result .icon { font-size: 3em; margin-bottom: 12px; }
@@ -126,15 +173,29 @@ input:focus { border-color: #ff6b35; }
.toast.show { opacity: 1; transform: translateY(0); }
.toast.error { background: #f44336; }
/* Channel form */
/* Channel form — open state must scroll (Telegram pairing steps are tall) */
.channel-card { transition: all 0.2s; }
.channel-card.selected { border-color: #ff6b35; background: rgba(255,107,53,0.06); }
.channel-form { padding-top: 0; margin-top: 10px; border-top: 1px solid #333; max-height: 0; overflow: hidden; transition: max-height 0.3s ease, padding 0.3s ease; }
.channel-form.open { max-height: 300px; padding-top: 12px; }
.channel-form { padding-top: 0; margin-top: 10px; border-top: 1px solid #333; max-height: 0; overflow: hidden; transition: max-height 0.35s ease, padding 0.35s ease; }
.channel-form.open {
max-height: min(72vh, 560px);
overflow-x: hidden;
overflow-y: auto;
padding-top: 12px;
padding-bottom: 4px;
-webkit-overflow-scrolling: touch;
scrollbar-gutter: stable;
}
.channel-form.open::-webkit-scrollbar { width: 8px; }
.channel-form.open::-webkit-scrollbar-thumb { background: #444; border-radius: 4px; }
.channel-form.open::-webkit-scrollbar-thumb:hover { background: #555; }
/* Telegram needs the full row when expanded */
.channel-card.channel-expanded { grid-column: 1 / -1; }
.channel-form .form-group { margin-bottom: 10px; }
.channel-form label { font-size: 0.82em; color: #aaa; margin-bottom: 4px; }
.channel-form input { font-size: 0.88em; padding: 9px 12px; }
.channel-form .hint { font-size: 0.75em; margin-top: 4px; }
.tg-steps code { background: #2a2a2a; padding: 1px 5px; border-radius: 4px; color: #ffb84d; font-size: 0.95em; }
/* Status bar */
.status-bar {
@@ -203,12 +264,11 @@ body[data-tier="standard"] [data-tier-min="expert"] { display: none !important;
<!-- Appears once the key checks out. Without it, picking a key would
also lock the model, and there would be no way to switch later. -->
<div class="form-group" id="modelPicker" style="display:none;" data-tier-min="standard">
<div class="key-actions" id="keyActions">
<div class="form-group model-picker" id="modelPicker" style="display:none;" data-tier-min="standard">
<label data-i18n="key.which_model">Which model?</label>
<select id="modelChoice"></select>
</div>
<div class="btn-row">
<button class="btn btn-primary" id="continueBtn" onclick="saveConfig()" disabled data-i18n="key.continue">Continue →</button>
</div>
@@ -268,6 +328,16 @@ body[data-tier="standard"] [data-tier-min="expert"] { display: none !important;
<input type="text" id="ch-telegram-token" placeholder="123456:ABC-DEF...">
<p class="hint"><a href="https://t.me/BotFather" target="_blank" rel="noopener" data-i18n="ch.telegram_get">→ Get one from @BotFather</a></p>
</div>
<div class="telegram-pairing-panel" id="telegram-pairing-panel" style="margin-top:12px;padding-top:12px;border-top:1px solid #333;">
<p class="hint" style="color:#ccc;margin-bottom:8px;" data-i18n="tg.steps_title">After saving, connect your Telegram account:</p>
<ol class="hint tg-steps" style="color:#aaa;font-size:0.82em;margin:0 0 12px 18px;line-height:1.55;">
<li data-i18n="tg.step1">Save the bot token above (click “Open dashboard” or save channels).</li>
<li data-i18n="tg.step2">On your phone, open Telegram, find your bot, and send <code>/start</code>.</li>
<li data-i18n="tg.step3">Come back here — a button will appear. Click it once. No command line.</li>
</ol>
<div id="telegram-pairing-status" class="hint" style="margin-bottom:10px;color:#888;"></div>
<button type="button" class="btn btn-primary" id="telegram-approve-btn" style="display:none;width:100%;margin-bottom:4px;" onclick="approveTelegramPairing()" data-i18n="tg.approve">Allow this Telegram account</button>
</div>
</div>
</div>
<div class="model-card channel-card" onclick="toggleChannel(this, 'slack')">
@@ -495,11 +565,16 @@ async function checkKey() {
const modelPicker = document.getElementById('modelPicker');
const modelChoice = document.getElementById('modelChoice');
const keyActions = document.getElementById('keyActions');
function populateModels(provider, current) {
var options = (provider && provider.models) ? provider.models.slice() : [];
if (current && options.indexOf(current) === -1) options.unshift(current);
if (options.length < 2) { modelPicker.style.display = 'none'; return; }
if (options.length < 2) {
modelPicker.style.display = 'none';
keyActions.classList.remove('has-model');
return;
}
modelChoice.innerHTML = '';
options.forEach(function (name) {
var opt = document.createElement('option');
@@ -509,6 +584,7 @@ function populateModels(provider, current) {
modelChoice.appendChild(opt);
});
modelPicker.style.display = 'block';
keyActions.classList.add('has-model');
}
// Check as they paste, but wait for typing to settle first.
@@ -581,6 +657,14 @@ async function saveConfig() {
var config = buildConfig(detected.id, detected.baseUrl, model, apiKey);
try {
var existingRes = await fetch('/api/config');
var existing = existingRes.ok ? await existingRes.json() : {};
delete existing.agent;
config = Object.assign(existing, config);
if (existing.channels) config.channels = existing.channels;
if (existing.commands) {
config.commands = Object.assign({}, existing.commands, config.commands || {});
}
var res = await fetch('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -601,11 +685,93 @@ function toggleChannel(card, type) {
if (isOpen) {
form.classList.remove('open');
card.classList.remove('selected');
card.classList.remove('channel-expanded');
if (type === 'telegram') stopTelegramPairingPoll();
} else {
form.classList.add('open');
card.classList.add('selected');
if (type === 'telegram') card.classList.add('channel-expanded');
var firstInput = form.querySelector('input');
if (firstInput) setTimeout(function() { firstInput.focus(); }, 100);
if (type === 'telegram') startTelegramPairingPoll();
}
}
// --- Telegram pairing (Control Panel, no CLI) ---
var telegramPairingTimer = null;
var telegramPendingCode = null;
function stopTelegramPairingPoll() {
if (telegramPairingTimer) {
clearTimeout(telegramPairingTimer);
telegramPairingTimer = null;
}
}
function formatTelegramRequestLabel(req) {
if (req.username) return '@' + req.username;
if (req.firstName) return req.firstName;
return req.id || '';
}
async function pollTelegramPairing() {
var statusEl = document.getElementById('telegram-pairing-status');
var btn = document.getElementById('telegram-approve-btn');
if (!statusEl || !btn) return;
try {
var res = await fetch('/api/telegram/pairing');
var data = res.ok ? await res.json() : { requests: [] };
var requests = data.requests || [];
if (requests.length > 0) {
var req = requests[0];
telegramPendingCode = req.code;
var who = formatTelegramRequestLabel(req);
statusEl.innerHTML = '<span style="color:#ffb84d;">' + t('tg.pending', { who: who || req.id }) + '</span>';
btn.style.display = 'block';
btn.disabled = false;
} else {
telegramPendingCode = null;
btn.style.display = 'none';
btn.disabled = true;
statusEl.textContent = t('tg.waiting');
}
} catch (e) {
statusEl.textContent = t('tg.poll_failed');
}
telegramPairingTimer = setTimeout(pollTelegramPairing, 2000);
}
function startTelegramPairingPoll() {
stopTelegramPairingPoll();
pollTelegramPairing();
}
async function approveTelegramPairing() {
if (!telegramPendingCode) return;
var btn = document.getElementById('telegram-approve-btn');
var statusEl = document.getElementById('telegram-pairing-status');
btn.disabled = true;
statusEl.textContent = t('tg.approving');
try {
var res = await fetch('/api/telegram/pairing/approve', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code: telegramPendingCode })
});
var data = await res.json();
if (!res.ok) throw new Error(data.error || String(res.status));
telegramPendingCode = null;
btn.style.display = 'none';
statusEl.innerHTML = '<span style="color:#4caf50;font-weight:600;">' + t('tg.approved') + '</span>';
showToast(t('tg.approved_toast'));
} catch (e) {
statusEl.textContent = t('tg.approve_failed', { reason: e.message });
btn.disabled = false;
showToast(t('tg.approve_failed', { reason: e.message }), true);
}
}
@@ -748,6 +914,7 @@ async function saveChannelsAndOpen() {
body: JSON.stringify(existing)
});
showToast(t('ch.saved'));
if (channels.telegram) startTelegramPairingPoll();
} catch (e) {
showToast(t('ch.err_save'), true);
}
@@ -865,6 +1032,11 @@ async function loadConfig() {
if (modelChoice.options[i].value === model) { modelChoice.selectedIndex = i; break; }
}
}
if (cfg.channels && cfg.channels.telegram && cfg.channels.telegram.botToken) {
var tgInput = document.getElementById('ch-telegram-token');
if (tgInput) tgInput.value = cfg.channels.telegram.botToken;
}
} catch (e) { /* first run: nothing configured yet */ }
}

View File

@@ -10,7 +10,7 @@ echo ========================================
echo.
set "APP_DIR=%~dp0"
set "NODE_VER=v22.22.1"
set "NODE_VER=v22.22.3"
set "MIRROR=https://registry.npmjs.org"
set "NODE_MIRROR=https://nodejs.org/dist"

View File

@@ -14,7 +14,7 @@ NC='\033[0m'
BOLD='\033[1m'
APP_DIR="$(cd "$(dirname "$0")" && pwd)"
NODE_VER="$(tr -d "[:space:]" < "$(dirname "$0")/../NODE_VERSION" 2>/dev/null || echo v22.22.1)"
NODE_VER="$(tr -d "[:space:]" < "$(dirname "$0")/../NODE_VERSION" 2>/dev/null || echo v22.22.3)"
MIRROR="https://registry.npmjs.org"
NODE_MIRROR="https://nodejs.org/dist"