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.
This commit is contained in:
@@ -5,8 +5,10 @@ const path = require('path');
|
|||||||
const { deflateSync } = require('zlib');
|
const { deflateSync } = require('zlib');
|
||||||
const crypto = require('crypto');
|
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 CONFIG_PATH = path.join(__dirname, '../data/.openclaw/openclaw.json');
|
||||||
|
const RUNTIME_PATH = path.join(__dirname, '../data/.openclaw/runtime.json');
|
||||||
|
|
||||||
// ── WeChat Login State ──────────────────────────────────────────────────────
|
// ── WeChat Login State ──────────────────────────────────────────────────────
|
||||||
const DEFAULT_WECHAT_BASE_URL = 'https://ilinkai.weixin.qq.com';
|
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', () => {
|
function listenWithFallback(port) {
|
||||||
console.log(`\n🦞 U-Claw Config Center`);
|
server.once('error', (err) => {
|
||||||
console.log(` http://127.0.0.1:${PORT}`);
|
if (err && err.code === 'EADDRINUSE' && port < PORT_RANGE_END) {
|
||||||
console.log(`\n Config file: ${CONFIG_PATH}\n`);
|
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);
|
||||||
|
|||||||
@@ -12,16 +12,23 @@ import { buildApiKey } from './xiapan-client.mjs';
|
|||||||
|
|
||||||
const PROVIDER_ID = 'uclaw-cloud';
|
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 = {
|
const DEFAULT_PROVIDER_TEMPLATE = {
|
||||||
baseUrl: 'https://api.u-claw.org/v1',
|
baseUrl: 'https://api.u-claw.org/v1',
|
||||||
api: 'openai-completions',
|
api: 'openai-completions',
|
||||||
models: [
|
models: [],
|
||||||
{ id: 'deepseek-chat', name: 'DeepSeek Chat' },
|
|
||||||
{ id: 'qwen-plus', name: 'Qwen Plus' },
|
|
||||||
{ id: 'qwen-turbo', name: 'Qwen Turbo' },
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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) {
|
function readJsonSafe(filePath) {
|
||||||
if (!existsSync(filePath)) return null;
|
if (!existsSync(filePath)) return null;
|
||||||
try {
|
try {
|
||||||
@@ -48,6 +55,32 @@ function ensureModelsContainer(config) {
|
|||||||
return config.models.providers;
|
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 } = {}) {
|
export async function bootstrapXiapan({ configPath, appRoot, log = console } = {}) {
|
||||||
if (!configPath) {
|
if (!configPath) {
|
||||||
throw new Error('bootstrapXiapan: configPath is required.');
|
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). `
|
`[bootstrap-xiapan] uclaw-cloud apiKey already configured (different fingerprint). `
|
||||||
+ `Current source=${fingerprintInfo.source}. Use Config UI to rebind if needed.`,
|
+ `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 {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
action: 'kept',
|
action: 'kept',
|
||||||
@@ -82,9 +118,14 @@ export async function bootstrapXiapan({ configPath, appRoot, log = console } = {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (existing.apiKey === apiKey) {
|
if (existing.apiKey === apiKey) {
|
||||||
|
const changedDefaults = ensureAgentsDefaults(config);
|
||||||
|
if (changedDefaults) {
|
||||||
|
writeJson(configPath, config);
|
||||||
|
log.info?.('[bootstrap-xiapan] Added agents.defaults to existing config');
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
action: 'noop',
|
action: changedDefaults ? 'agents-defaults-added' : 'noop',
|
||||||
source: fingerprintInfo.source,
|
source: fingerprintInfo.source,
|
||||||
apiKey,
|
apiKey,
|
||||||
};
|
};
|
||||||
@@ -97,8 +138,9 @@ export async function bootstrapXiapan({ configPath, appRoot, log = console } = {
|
|||||||
baseUrl: existing?.baseUrl || DEFAULT_PROVIDER_TEMPLATE.baseUrl,
|
baseUrl: existing?.baseUrl || DEFAULT_PROVIDER_TEMPLATE.baseUrl,
|
||||||
api: existing?.api || DEFAULT_PROVIDER_TEMPLATE.api,
|
api: existing?.api || DEFAULT_PROVIDER_TEMPLATE.api,
|
||||||
apiKey,
|
apiKey,
|
||||||
models: existing?.models?.length ? existing.models : DEFAULT_PROVIDER_TEMPLATE.models,
|
models: existing?.models ?? DEFAULT_PROVIDER_TEMPLATE.models,
|
||||||
};
|
};
|
||||||
|
ensureAgentsDefaults(config);
|
||||||
|
|
||||||
writeJson(configPath, config);
|
writeJson(configPath, config);
|
||||||
log.info?.(
|
log.info?.(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "u-claw",
|
"name": "u-claw",
|
||||||
"version": "2.0.0",
|
"version": "2.1.2",
|
||||||
"description": "U-Claw - AI 助手桌面版,插上 U 盘就能用",
|
"description": "U-Claw - AI 助手桌面版,插上 U 盘就能用",
|
||||||
"main": "src/main.js",
|
"main": "src/main.js",
|
||||||
"author": "U-Claw <hello@u-claw.org> (https://u-claw.org)",
|
"author": "U-Claw <hello@u-claw.org> (https://u-claw.org)",
|
||||||
|
|||||||
@@ -12,16 +12,23 @@ import { buildApiKey } from './xiapan-client.mjs';
|
|||||||
|
|
||||||
const PROVIDER_ID = 'uclaw-cloud';
|
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 = {
|
const DEFAULT_PROVIDER_TEMPLATE = {
|
||||||
baseUrl: 'https://api.u-claw.org/v1',
|
baseUrl: 'https://api.u-claw.org/v1',
|
||||||
api: 'openai-completions',
|
api: 'openai-completions',
|
||||||
models: [
|
models: [],
|
||||||
{ id: 'deepseek-chat', name: 'DeepSeek Chat' },
|
|
||||||
{ id: 'qwen-plus', name: 'Qwen Plus' },
|
|
||||||
{ id: 'qwen-turbo', name: 'Qwen Turbo' },
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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) {
|
function readJsonSafe(filePath) {
|
||||||
if (!existsSync(filePath)) return null;
|
if (!existsSync(filePath)) return null;
|
||||||
try {
|
try {
|
||||||
@@ -48,6 +55,32 @@ function ensureModelsContainer(config) {
|
|||||||
return config.models.providers;
|
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 } = {}) {
|
export async function bootstrapXiapan({ configPath, appRoot, log = console } = {}) {
|
||||||
if (!configPath) {
|
if (!configPath) {
|
||||||
throw new Error('bootstrapXiapan: configPath is required.');
|
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). `
|
`[bootstrap-xiapan] uclaw-cloud apiKey already configured (different fingerprint). `
|
||||||
+ `Current source=${fingerprintInfo.source}. Use Config UI to rebind if needed.`,
|
+ `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 {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
action: 'kept',
|
action: 'kept',
|
||||||
@@ -82,9 +118,14 @@ export async function bootstrapXiapan({ configPath, appRoot, log = console } = {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (existing.apiKey === apiKey) {
|
if (existing.apiKey === apiKey) {
|
||||||
|
const changedDefaults = ensureAgentsDefaults(config);
|
||||||
|
if (changedDefaults) {
|
||||||
|
writeJson(configPath, config);
|
||||||
|
log.info?.('[bootstrap-xiapan] Added agents.defaults to existing config');
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
action: 'noop',
|
action: changedDefaults ? 'agents-defaults-added' : 'noop',
|
||||||
source: fingerprintInfo.source,
|
source: fingerprintInfo.source,
|
||||||
apiKey,
|
apiKey,
|
||||||
};
|
};
|
||||||
@@ -97,8 +138,9 @@ export async function bootstrapXiapan({ configPath, appRoot, log = console } = {
|
|||||||
baseUrl: existing?.baseUrl || DEFAULT_PROVIDER_TEMPLATE.baseUrl,
|
baseUrl: existing?.baseUrl || DEFAULT_PROVIDER_TEMPLATE.baseUrl,
|
||||||
api: existing?.api || DEFAULT_PROVIDER_TEMPLATE.api,
|
api: existing?.api || DEFAULT_PROVIDER_TEMPLATE.api,
|
||||||
apiKey,
|
apiKey,
|
||||||
models: existing?.models?.length ? existing.models : DEFAULT_PROVIDER_TEMPLATE.models,
|
models: existing?.models ?? DEFAULT_PROVIDER_TEMPLATE.models,
|
||||||
};
|
};
|
||||||
|
ensureAgentsDefaults(config);
|
||||||
|
|
||||||
writeJson(configPath, config);
|
writeJson(configPath, config);
|
||||||
log.info?.(
|
log.info?.(
|
||||||
|
|||||||
Reference in New Issue
Block a user