From cc2984234b98ef81d1552a75f086b965a976df31 Mon Sep 17 00:00:00 2001 From: hfshfg <38004547@qq.com> Date: Sun, 3 May 2026 00:07:09 +0800 Subject: [PATCH] feat(v2.1.2): align with ClawX schema + config-server port fallback Three changes for v2.1.2: 1. bootstrap-xiapan now writes "agents.defaults.model.primary/fallbacks" (uclaw-cloud/deepseek-v4-flash + 3 fallbacks) and an empty models[], matching the ClawX commercial schema. Lets OpenClaw runtime auto-discover models from /v1/models instead of hard-coding a list that drifts. 2. config-server tries ports 18788..18798 with EADDRINUSE fallback and persists the live port to data/.openclaw/runtime.json so future tooling can discover it. Fixes silent failure when a second portable instance started while 18788 was occupied. 3. u-claw-app/package.json version 2.0.0 -> 2.1.2 so .exe/.dmg filenames align with the release tag. --- portable/config-server/server.js | 37 +++++++++++++--- portable/lib/bootstrap-xiapan.mjs | 56 +++++++++++++++++++++---- u-claw-app/package.json | 2 +- u-claw-app/src/lib/bootstrap-xiapan.mjs | 56 +++++++++++++++++++++---- 4 files changed, 130 insertions(+), 21 deletions(-) diff --git a/portable/config-server/server.js b/portable/config-server/server.js index 812c488..15232e4 100644 --- a/portable/config-server/server.js +++ b/portable/config-server/server.js @@ -5,8 +5,10 @@ const path = require('path'); const { deflateSync } = require('zlib'); const crypto = require('crypto'); -const PORT = 18788; +const PORT_RANGE_START = 18788; +const PORT_RANGE_END = 18798; const CONFIG_PATH = path.join(__dirname, '../data/.openclaw/openclaw.json'); +const RUNTIME_PATH = path.join(__dirname, '../data/.openclaw/runtime.json'); // ── WeChat Login State ────────────────────────────────────────────────────── const DEFAULT_WECHAT_BASE_URL = 'https://ilinkai.weixin.qq.com'; @@ -500,8 +502,31 @@ const server = http.createServer((req, res) => { } }); -server.listen(PORT, '127.0.0.1', () => { - console.log(`\n🦞 U-Claw Config Center`); - console.log(` http://127.0.0.1:${PORT}`); - console.log(`\n Config file: ${CONFIG_PATH}\n`); -}); +function listenWithFallback(port) { + server.once('error', (err) => { + if (err && err.code === 'EADDRINUSE' && port < PORT_RANGE_END) { + console.log(` Port ${port} busy, trying ${port + 1}…`); + setImmediate(() => listenWithFallback(port + 1)); + return; + } + console.error(`Config server failed to bind: ${err && err.message ? err.message : err}`); + process.exit(1); + }); + server.listen(port, '127.0.0.1', () => { + console.log(`\n🦞 U-Claw Config Center`); + console.log(` http://127.0.0.1:${port}`); + console.log(`\n Config file: ${CONFIG_PATH}\n`); + // Persist the live port so Config.html / launchers can discover it after restarts. + try { + fs.mkdirSync(path.dirname(RUNTIME_PATH), { recursive: true }); + const existing = fs.existsSync(RUNTIME_PATH) ? JSON.parse(fs.readFileSync(RUNTIME_PATH, 'utf8')) : {}; + existing.configServerPort = port; + existing.configServerUpdatedAt = new Date().toISOString(); + fs.writeFileSync(RUNTIME_PATH, JSON.stringify(existing, null, 2)); + } catch (err) { + console.warn(` Warning: could not write ${RUNTIME_PATH}: ${err.message}`); + } + }); +} + +listenWithFallback(PORT_RANGE_START); diff --git a/portable/lib/bootstrap-xiapan.mjs b/portable/lib/bootstrap-xiapan.mjs index 942d61c..3d07edd 100644 --- a/portable/lib/bootstrap-xiapan.mjs +++ b/portable/lib/bootstrap-xiapan.mjs @@ -12,16 +12,23 @@ import { buildApiKey } from './xiapan-client.mjs'; const PROVIDER_ID = 'uclaw-cloud'; +// Mirror ClawX commercial schema: keep models[] empty and steer model selection +// via agents.defaults.model.primary + fallbacks. OpenClaw 2026.4.x accepts both +// shapes, but the empty-models form lets the runtime auto-discover model ids +// from /v1/models without us hard-coding a list that drifts from the backend. const DEFAULT_PROVIDER_TEMPLATE = { baseUrl: 'https://api.u-claw.org/v1', api: 'openai-completions', - models: [ - { id: 'deepseek-chat', name: 'DeepSeek Chat' }, - { id: 'qwen-plus', name: 'Qwen Plus' }, - { id: 'qwen-turbo', name: 'Qwen Turbo' }, - ], + models: [], }; +const DEFAULT_PRIMARY_MODEL = `${PROVIDER_ID}/deepseek-v4-flash`; +const DEFAULT_FALLBACK_MODELS = [ + `${PROVIDER_ID}/deepseek-chat`, + `${PROVIDER_ID}/qwen-plus`, + `${PROVIDER_ID}/qwen-turbo`, +]; + function readJsonSafe(filePath) { if (!existsSync(filePath)) return null; try { @@ -48,6 +55,32 @@ function ensureModelsContainer(config) { return config.models.providers; } +// Set agents.defaults.model.primary to uclaw-cloud only when the user has not +// configured a primary model already. If they've picked a different provider +// (e.g. DeepSeek BYOK), leave their choice untouched. +function ensureAgentsDefaults(config) { + if (!config.agents || typeof config.agents !== 'object') { + config.agents = {}; + } + if (!config.agents.defaults || typeof config.agents.defaults !== 'object') { + config.agents.defaults = {}; + } + const defaults = config.agents.defaults; + if (!defaults.model || typeof defaults.model !== 'object') { + defaults.model = {}; + } + let changed = false; + if (!defaults.model.primary) { + defaults.model.primary = DEFAULT_PRIMARY_MODEL; + changed = true; + } + if (!Array.isArray(defaults.model.fallbacks) || defaults.model.fallbacks.length === 0) { + defaults.model.fallbacks = [...DEFAULT_FALLBACK_MODELS]; + changed = true; + } + return changed; +} + export async function bootstrapXiapan({ configPath, appRoot, log = console } = {}) { if (!configPath) { throw new Error('bootstrapXiapan: configPath is required.'); @@ -74,6 +107,9 @@ export async function bootstrapXiapan({ configPath, appRoot, log = console } = { `[bootstrap-xiapan] uclaw-cloud apiKey already configured (different fingerprint). ` + `Current source=${fingerprintInfo.source}. Use Config UI to rebind if needed.`, ); + // Still ensure agents.defaults exists so the runtime knows to use uclaw-cloud + const changedDefaults = ensureAgentsDefaults(config); + if (changedDefaults) writeJson(configPath, config); return { ok: true, action: 'kept', @@ -82,9 +118,14 @@ export async function bootstrapXiapan({ configPath, appRoot, log = console } = { }; } if (existing.apiKey === apiKey) { + const changedDefaults = ensureAgentsDefaults(config); + if (changedDefaults) { + writeJson(configPath, config); + log.info?.('[bootstrap-xiapan] Added agents.defaults to existing config'); + } return { ok: true, - action: 'noop', + action: changedDefaults ? 'agents-defaults-added' : 'noop', source: fingerprintInfo.source, apiKey, }; @@ -97,8 +138,9 @@ export async function bootstrapXiapan({ configPath, appRoot, log = console } = { baseUrl: existing?.baseUrl || DEFAULT_PROVIDER_TEMPLATE.baseUrl, api: existing?.api || DEFAULT_PROVIDER_TEMPLATE.api, apiKey, - models: existing?.models?.length ? existing.models : DEFAULT_PROVIDER_TEMPLATE.models, + models: existing?.models ?? DEFAULT_PROVIDER_TEMPLATE.models, }; + ensureAgentsDefaults(config); writeJson(configPath, config); log.info?.( diff --git a/u-claw-app/package.json b/u-claw-app/package.json index bcf45b7..b90911d 100644 --- a/u-claw-app/package.json +++ b/u-claw-app/package.json @@ -1,6 +1,6 @@ { "name": "u-claw", - "version": "2.0.0", + "version": "2.1.2", "description": "U-Claw - AI 助手桌面版,插上 U 盘就能用", "main": "src/main.js", "author": "U-Claw (https://u-claw.org)", diff --git a/u-claw-app/src/lib/bootstrap-xiapan.mjs b/u-claw-app/src/lib/bootstrap-xiapan.mjs index 942d61c..3d07edd 100644 --- a/u-claw-app/src/lib/bootstrap-xiapan.mjs +++ b/u-claw-app/src/lib/bootstrap-xiapan.mjs @@ -12,16 +12,23 @@ import { buildApiKey } from './xiapan-client.mjs'; const PROVIDER_ID = 'uclaw-cloud'; +// Mirror ClawX commercial schema: keep models[] empty and steer model selection +// via agents.defaults.model.primary + fallbacks. OpenClaw 2026.4.x accepts both +// shapes, but the empty-models form lets the runtime auto-discover model ids +// from /v1/models without us hard-coding a list that drifts from the backend. const DEFAULT_PROVIDER_TEMPLATE = { baseUrl: 'https://api.u-claw.org/v1', api: 'openai-completions', - models: [ - { id: 'deepseek-chat', name: 'DeepSeek Chat' }, - { id: 'qwen-plus', name: 'Qwen Plus' }, - { id: 'qwen-turbo', name: 'Qwen Turbo' }, - ], + models: [], }; +const DEFAULT_PRIMARY_MODEL = `${PROVIDER_ID}/deepseek-v4-flash`; +const DEFAULT_FALLBACK_MODELS = [ + `${PROVIDER_ID}/deepseek-chat`, + `${PROVIDER_ID}/qwen-plus`, + `${PROVIDER_ID}/qwen-turbo`, +]; + function readJsonSafe(filePath) { if (!existsSync(filePath)) return null; try { @@ -48,6 +55,32 @@ function ensureModelsContainer(config) { return config.models.providers; } +// Set agents.defaults.model.primary to uclaw-cloud only when the user has not +// configured a primary model already. If they've picked a different provider +// (e.g. DeepSeek BYOK), leave their choice untouched. +function ensureAgentsDefaults(config) { + if (!config.agents || typeof config.agents !== 'object') { + config.agents = {}; + } + if (!config.agents.defaults || typeof config.agents.defaults !== 'object') { + config.agents.defaults = {}; + } + const defaults = config.agents.defaults; + if (!defaults.model || typeof defaults.model !== 'object') { + defaults.model = {}; + } + let changed = false; + if (!defaults.model.primary) { + defaults.model.primary = DEFAULT_PRIMARY_MODEL; + changed = true; + } + if (!Array.isArray(defaults.model.fallbacks) || defaults.model.fallbacks.length === 0) { + defaults.model.fallbacks = [...DEFAULT_FALLBACK_MODELS]; + changed = true; + } + return changed; +} + export async function bootstrapXiapan({ configPath, appRoot, log = console } = {}) { if (!configPath) { throw new Error('bootstrapXiapan: configPath is required.'); @@ -74,6 +107,9 @@ export async function bootstrapXiapan({ configPath, appRoot, log = console } = { `[bootstrap-xiapan] uclaw-cloud apiKey already configured (different fingerprint). ` + `Current source=${fingerprintInfo.source}. Use Config UI to rebind if needed.`, ); + // Still ensure agents.defaults exists so the runtime knows to use uclaw-cloud + const changedDefaults = ensureAgentsDefaults(config); + if (changedDefaults) writeJson(configPath, config); return { ok: true, action: 'kept', @@ -82,9 +118,14 @@ export async function bootstrapXiapan({ configPath, appRoot, log = console } = { }; } if (existing.apiKey === apiKey) { + const changedDefaults = ensureAgentsDefaults(config); + if (changedDefaults) { + writeJson(configPath, config); + log.info?.('[bootstrap-xiapan] Added agents.defaults to existing config'); + } return { ok: true, - action: 'noop', + action: changedDefaults ? 'agents-defaults-added' : 'noop', source: fingerprintInfo.source, apiKey, }; @@ -97,8 +138,9 @@ export async function bootstrapXiapan({ configPath, appRoot, log = console } = { baseUrl: existing?.baseUrl || DEFAULT_PROVIDER_TEMPLATE.baseUrl, api: existing?.api || DEFAULT_PROVIDER_TEMPLATE.api, apiKey, - models: existing?.models?.length ? existing.models : DEFAULT_PROVIDER_TEMPLATE.models, + models: existing?.models ?? DEFAULT_PROVIDER_TEMPLATE.models, }; + ensureAgentsDefaults(config); writeJson(configPath, config); log.info?.(