feat: npm 源可配置,支持淘宝/官方/华为云镜像切换

- 所有 npm 操作使用用户配置的 registry,默认淘宝镜像
- 服务管理页面新增 npm 源设置区域(预设 + 自定义)
- 版本检测 API 同步使用配置源
- 配置持久化到 ~/.openclaw/npm-registry.txt
This commit is contained in:
晴天
2026-02-28 14:10:09 +08:00
parent da8932a3e0
commit a5c7760f25
4 changed files with 96 additions and 4 deletions

View File

@@ -99,6 +99,8 @@ function mockInvoke(cmd, args) {
upgrade_openclaw: () => '升级成功,当前版本: 2026.2.26-zh.3 (mock)',
install_gateway: () => 'Gateway 服务已安装 (mock)',
uninstall_gateway: () => 'Gateway 服务已卸载 (mock)',
get_npm_registry: () => 'https://registry.npmmirror.com',
set_npm_registry: () => true,
test_model: ({ modelId }) => `模型 ${modelId} 连通正常 (mock)`,
list_remote_models: () => ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'gpt-3.5-turbo', 'o3-mini', 'dall-e-3', 'text-embedding-3-small'],
write_env_file: () => true,
@@ -144,6 +146,8 @@ export const api = {
upgradeOpenclaw: (source = 'chinese') => invoke('upgrade_openclaw', { source }),
installGateway: () => invoke('install_gateway'),
uninstallGateway: () => invoke('uninstall_gateway'),
getNpmRegistry: () => invoke('get_npm_registry'),
setNpmRegistry: (registry) => invoke('set_npm_registry', { registry }),
testModel: (baseUrl, apiKey, modelId) => invoke('test_model', { baseUrl, apiKey, modelId }),
listRemoteModels: (baseUrl, apiKey) => invoke('list_remote_models', { baseUrl, apiKey }),

View File

@@ -27,6 +27,10 @@ export async function render() {
</div>
<div id="version-bar"></div>
<div id="services-list">加载中...</div>
<div class="config-section" id="registry-section">
<div class="config-section-title">npm 源设置</div>
<div id="registry-bar">加载中...</div>
</div>
<div class="config-section" id="backup-section">
<div class="config-section-title">配置备份</div>
<div id="backup-actions" style="margin-bottom:var(--space-md)">
@@ -45,6 +49,7 @@ async function loadAll(page) {
await Promise.all([
loadVersion(page),
loadServices(page),
loadRegistry(page),
loadBackups(page),
])
}
@@ -85,6 +90,41 @@ async function loadVersion(page) {
}
}
// ===== npm 源设置 =====
const REGISTRIES = [
{ label: '淘宝镜像 (推荐)', value: 'https://registry.npmmirror.com' },
{ label: 'npm 官方源', value: 'https://registry.npmjs.org' },
{ label: '华为云镜像', value: 'https://repo.huaweicloud.com/repository/npm/' },
]
async function loadRegistry(page) {
const bar = page.querySelector('#registry-bar')
try {
const current = await api.getNpmRegistry()
const isPreset = REGISTRIES.some(r => r.value === current)
bar.innerHTML = `
<div style="display:flex;align-items:center;gap:var(--space-sm);flex-wrap:wrap">
<select class="form-input" data-name="registry" style="max-width:320px">
${REGISTRIES.map(r => `<option value="${r.value}" ${r.value === current ? 'selected' : ''}>${r.label}</option>`).join('')}
<option value="custom" ${!isPreset ? 'selected' : ''}>自定义</option>
</select>
<input class="form-input" data-name="custom-registry" placeholder="https://..." value="${isPreset ? '' : escapeHtml(current)}" style="max-width:320px;${isPreset ? 'display:none' : ''}">
<button class="btn btn-primary btn-sm" data-action="save-registry">保存</button>
</div>
<div class="form-hint" style="margin-top:var(--space-xs)">升级和版本检测使用此源下载 npm 包,国内用户推荐淘宝镜像</div>
`
// 切换预设/自定义
const select = bar.querySelector('[data-name="registry"]')
const customInput = bar.querySelector('[data-name="custom-registry"]')
select.onchange = () => {
customInput.style.display = select.value === 'custom' ? '' : 'none'
}
} catch (e) {
bar.innerHTML = `<div style="color:var(--error)">加载失败: ${escapeHtml(String(e))}</div>`
}
}
// ===== 服务列表 =====
async function loadServices(page) {
@@ -214,6 +254,9 @@ function bindEvents(page) {
case 'uninstall-gateway':
await handleUninstallGateway(btn, page)
break
case 'save-registry':
await handleSaveRegistry(btn, page)
break
}
} catch (e) {
toast(e.toString(), 'error')
@@ -310,3 +353,13 @@ async function handleUninstallGateway(btn, page) {
toast('Gateway 服务已卸载', 'success')
await loadServices(page)
}
async function handleSaveRegistry(btn, page) {
const section = page.querySelector('#registry-section')
const select = section.querySelector('[data-name="registry"]')
const customInput = section.querySelector('[data-name="custom-registry"]')
const registry = select.value === 'custom' ? customInput.value.trim() : select.value
if (!registry) { toast('请输入源地址', 'error'); return }
await api.setNpmRegistry(registry)
toast('npm 源已保存', 'success')
}