mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-31 05:10:14 +08:00
feat: 飞书官方插件迁移 + 配对审批 + Gateway防卡死 + 微信升级修复 + 更新检测修复
- 飞书渠道从 @openclaw/feishu 迁移到 @larksuite/openclaw-lark 官方插件 - 保存飞书配置时自动禁用旧 feishu 插件,防止新旧插件冲突 - 所有主要渠道(飞书/Telegram/Discord/Slack)启用配对审批UI - gateway_command 增加20s超时,超时后force-kill+fresh start - 全平台启动前端口占用检查,防止Guardian无限拉起 - Linux gateway_command 补齐 Duration 导入和 cleanup_zombie 实现 - Guardian自动守护在Tauri桌面端也启用,轮询间隔30s→15s - 微信渠道:升级操作不再弹出扫码二维码,按钮文案区分安装/升级 - 版本更新检测:CI不再将minAppVersion写死为当前版本 - 部署脚本增强OpenClaw检测,支持已安装的官方版 - 日间/夜间模式圆形扩散切换动画(View Transitions API) - API错误信息完整展示(429限流等),URL自动转可点击链接 - 第三方API接入引导优化:移除内置密钥,引导式流程 - 修复全平台 Clippy 警告(strip_prefix/dead_code/unnecessary_unwrap等) - Rust代码格式化修复(cargo fmt) - toast组件支持HTML内容渲染 - Rust后端test_model返回详细错误信息
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
import { api, invalidate } from '../lib/tauri-api.js'
|
||||
import { toast } from '../components/toast.js'
|
||||
import { showModal, showConfirm } from '../components/modal.js'
|
||||
import { CHANNEL_LABELS } from '../lib/channel-labels.js'
|
||||
|
||||
export async function render() {
|
||||
const page = document.createElement('div')
|
||||
@@ -25,7 +26,7 @@ export async function render() {
|
||||
</div>
|
||||
`
|
||||
|
||||
const state = { agents: [] }
|
||||
const state = { agents: [], bindings: [] }
|
||||
// 非阻塞:先返回 DOM,后台加载数据
|
||||
loadAgents(page, state)
|
||||
|
||||
@@ -52,7 +53,12 @@ async function loadAgents(page, state) {
|
||||
const container = page.querySelector('#agents-list')
|
||||
renderSkeleton(container)
|
||||
try {
|
||||
state.agents = await api.listAgents()
|
||||
const [agents, config] = await Promise.all([
|
||||
api.listAgents(),
|
||||
api.readOpenclawConfig().catch(() => null),
|
||||
])
|
||||
state.agents = agents
|
||||
state.bindings = Array.isArray(config?.bindings) ? config.bindings : []
|
||||
renderAgents(page, state)
|
||||
|
||||
// 只在第一次加载时绑定事件(避免重复绑定)
|
||||
@@ -61,11 +67,27 @@ async function loadAgents(page, state) {
|
||||
state.eventsAttached = true
|
||||
}
|
||||
} catch (e) {
|
||||
container.innerHTML = '<div style="color:var(--error);padding:20px">加载失败: ' + e + '</div>'
|
||||
container.innerHTML = '<div style="color:var(--error);padding:20px">加载失败: ' + String(e).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') + '</div>'
|
||||
toast('加载 Agent 列表失败: ' + e, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
/** 为指定 agent 生成绑定渠道的 badge HTML */
|
||||
function renderBindingBadges(agentId, bindings) {
|
||||
const matched = (bindings || []).filter(b => (b.agentId || 'main') === agentId)
|
||||
if (!matched.length) {
|
||||
return '<span style="color:var(--text-tertiary)">未绑定渠道</span>'
|
||||
}
|
||||
return matched.map(b => {
|
||||
const channel = b.match?.channel || ''
|
||||
const label = CHANNEL_LABELS[channel] || channel
|
||||
const accountId = b.match?.accountId
|
||||
const text = accountId ? `${label} · ${accountId}` : label
|
||||
const escaped = text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
||||
return `<span style="font-size:var(--font-size-xs);color:var(--accent);background:var(--accent-muted);padding:1px 6px;border-radius:10px;white-space:nowrap">${escaped}</span>`
|
||||
}).join(' ')
|
||||
}
|
||||
|
||||
function renderAgents(page, state) {
|
||||
const container = page.querySelector('#agents-list')
|
||||
if (!state.agents.length) {
|
||||
@@ -102,6 +124,10 @@ function renderAgents(page, state) {
|
||||
<span class="agent-info-label">工作区:</span>
|
||||
<span class="agent-info-value" style="font-family:var(--font-mono);font-size:var(--font-size-xs)">${a.workspace || '未设置'}</span>
|
||||
</div>
|
||||
<div class="agent-info-row">
|
||||
<span class="agent-info-label">绑定渠道:</span>
|
||||
<span class="agent-info-value">${renderBindingBadges(a.id, state.bindings)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
@@ -149,7 +149,7 @@ ${personality}
|
||||
- openclaw skills check — 检查所有 Skills 的依赖是否满足
|
||||
- Skill 依赖安装: 根据 install spec 执行 brew/npm/go/uv 安装缺少的命令行工具
|
||||
- ClawHub (clawhub.com): 社区 Skill 市场,可搜索和安装新 Skill
|
||||
- Skills 目录: 捆绑 Skills 在 openclaw 安装包内,自定义 Skills 放在 ~/.openclaw/skills/<name>/
|
||||
- Skills 目录: 捆绑 Skills 在 openclaw 安装包内,自定义 Skills 通常位于 ~/.openclaw/skills/<name>/ 或 ~/.claude/skills/<name>/
|
||||
|
||||
### 聊天与调试
|
||||
- openclaw chat — 进入交互式聊天
|
||||
@@ -442,7 +442,7 @@ const TOOL_DEFS = {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'skills_clawhub_install',
|
||||
description: '从 ClawHub 社区市场安装一个 Skill 到本地 ~/.openclaw/skills/ 目录。',
|
||||
description: '从 ClawHub 社区市场安装一个 Skill 到本地自定义 Skills 目录(通常为 ~/.openclaw/skills/ 或 ~/.claude/skills/)。',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -2468,8 +2468,18 @@ function renderMessages() {
|
||||
})
|
||||
}
|
||||
|
||||
function _linkify(str) { return str.replace(/(https?:\/\/[^\s,,。;))'"]+)/g, '<a href="$1" target="_blank" style="color:var(--primary)">$1</a>') }
|
||||
|
||||
function buildTestResult({ success, elapsed, usedApi, reqUrl, reqBody, respStatus, respBody, reply, error }) {
|
||||
let html = ''
|
||||
// 尝试解析 API 返回的错误信息
|
||||
let apiErrMsg = ''
|
||||
if (!success && respBody) {
|
||||
try {
|
||||
const errJson = JSON.parse(respBody)
|
||||
apiErrMsg = errJson.error?.message || errJson.message || ''
|
||||
} catch {}
|
||||
}
|
||||
// 状态行
|
||||
if (error) {
|
||||
html += `<span style="color:var(--error)">✗ 请求失败: ${escHtml(error)}</span>`
|
||||
@@ -2478,6 +2488,10 @@ function buildTestResult({ success, elapsed, usedApi, reqUrl, reqBody, respStatu
|
||||
} else {
|
||||
html += `<span style="color:var(--warning)">${statusIcon('warn', 14)} HTTP ${respStatus} — 请求完成但未解析到回复内容</span>`
|
||||
}
|
||||
// API 错误信息(完整展示,URL 可点击)
|
||||
if (apiErrMsg) {
|
||||
html += `<div style="margin-top:6px;padding:8px 10px;background:var(--bg-tertiary);border-left:3px solid var(--warning);border-radius:4px;font-size:12px;color:var(--text-secondary);line-height:1.6;word-break:break-all">${_linkify(escHtml(apiErrMsg))}</div>`
|
||||
}
|
||||
// 回复预览
|
||||
if (reply) {
|
||||
const short = reply.length > 80 ? reply.slice(0, 80) + '...' : reply
|
||||
@@ -2562,18 +2576,29 @@ function showSettings() {
|
||||
</div>
|
||||
<div class="form-hint" id="ast-api-hint" style="margin-top:-4px">${apiHintText(c.apiType)}</div>
|
||||
|
||||
<div id="ast-qtcool-promo" style="margin-top:14px;border-radius:var(--radius-lg);background:var(--bg-tertiary);border:1px solid var(--border-primary);overflow:hidden">
|
||||
<div style="padding:14px 16px 10px">
|
||||
<div style="display:flex;align-items:center;gap:6px;margin-bottom:4px">
|
||||
${icon('zap', 16)}
|
||||
<span style="font-weight:600;font-size:var(--font-size-sm)">晴辰云快捷接入</span>
|
||||
<span style="font-size:10px;background:var(--primary);color:#fff;padding:1px 6px;border-radius:8px">推荐</span>
|
||||
<div id="ast-qtcool-promo" style="margin-top:14px;border-radius:var(--radius-lg);border:1px solid var(--border-primary);border-left:3px solid var(--primary);background:var(--bg-secondary);overflow:hidden">
|
||||
<div style="padding:14px 16px 12px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px;margin-bottom:10px">
|
||||
<div>
|
||||
<div style="display:flex;align-items:center;gap:6px;margin-bottom:3px">
|
||||
<span style="font-weight:700;font-size:var(--font-size-sm)">${icon('zap', 14)} 晴辰云</span>
|
||||
<span style="font-size:10px;background:var(--primary);color:#fff;padding:1px 7px;border-radius:8px">推荐</span>
|
||||
</div>
|
||||
<div style="font-size:11px;color:var(--text-tertiary);line-height:1.4">
|
||||
GPT-5 / Codex 全系列,低至官方价 2-3 折,不满意随时可退
|
||||
</div>
|
||||
</div>
|
||||
<a href="${QTCOOL.checkinUrl}" target="_blank" class="btn btn-primary btn-xs" style="flex-shrink:0">${icon('gift', 11)} 签到领额度</a>
|
||||
</div>
|
||||
<div style="font-size:var(--font-size-xs);color:var(--text-secondary);line-height:1.5;margin-bottom:10px">
|
||||
无需自行申请 API Key,选择模型即可一键接入。基础模型免费体验,高级模型低至官方价 2-3 折。
|
||||
<div style="font-size:var(--font-size-xs);color:var(--text-secondary);margin-bottom:8px">
|
||||
填入 API Key 后选择模型即可接入。没有密钥?<a href="${QTCOOL.checkinUrl}" target="_blank" style="color:var(--primary)">每日签到</a> 领取,在 <a href="${QTCOOL.usageUrl}" target="_blank" style="color:var(--primary)">用户后台</a> 复制
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:8px">
|
||||
<input class="form-input" id="ast-qtcool-key" placeholder="粘贴 API Key" style="font-size:12px;padding:5px 10px;flex:1;min-width:120px">
|
||||
<input type="checkbox" id="ast-qtcool-customkey" style="display:none">
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
|
||||
<select id="ast-qtcool-model" class="form-input" style="font-size:12px;padding:5px 10px;min-width:140px;flex:1">
|
||||
<select id="ast-qtcool-model" class="form-input" style="font-size:12px;padding:5px 10px;min-width:130px;flex:1">
|
||||
<option value="" disabled selected>加载模型列表...</option>
|
||||
</select>
|
||||
<button class="btn btn-sm btn-secondary" id="ast-qtcool-test">${icon('search', 12)} 测试</button>
|
||||
@@ -2581,16 +2606,12 @@ function showSettings() {
|
||||
</div>
|
||||
<div id="ast-qtcool-status" style="margin-top:8px;font-size:11px;min-height:16px;line-height:1.5"></div>
|
||||
</div>
|
||||
<div style="border-top:1px solid var(--border-primary);padding:8px 16px;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:6px;background:var(--bg-secondary)">
|
||||
<label style="cursor:pointer;display:flex;align-items:center;gap:5px;font-size:11px;color:var(--text-tertiary)">
|
||||
<input type="checkbox" id="ast-qtcool-customkey" style="accent-color:var(--primary);width:13px;height:13px"> 使用自定义密钥
|
||||
</label>
|
||||
<div style="display:flex;gap:12px;font-size:11px">
|
||||
<a href="${QTCOOL.site}" target="_blank" style="color:var(--primary);text-decoration:none">${icon('external-link', 12)} 了解更多</a>
|
||||
<div style="border-top:1px solid var(--border-primary);padding:6px 16px;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:6px;background:var(--bg-tertiary)">
|
||||
<div style="display:flex;gap:8px;align-items:center">
|
||||
<button class="btn btn-xs btn-secondary" id="ast-qtcool-sync-to" title="将助手配置同步到 OpenClaw 全局">${icon('upload', 11)} 同步到 OpenClaw</button>
|
||||
<button class="btn btn-xs btn-secondary" id="ast-qtcool-sync-from" title="从 OpenClaw 全局配置读取">${icon('download', 11)} 从 OpenClaw 读取</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="ast-qtcool-keyrow" style="display:none;border-top:1px solid var(--border-primary);padding:8px 16px;background:var(--bg-tertiary)">
|
||||
<input class="form-input" id="ast-qtcool-key" placeholder="粘贴你的密钥" style="font-size:12px;padding:6px 10px">
|
||||
<a href="${QTCOOL.site}" target="_blank" style="color:var(--primary);text-decoration:none;font-size:11px">${icon('external-link', 11)} 了解更多</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2934,9 +2955,7 @@ function showSettings() {
|
||||
// ── gpt.qt.cool 一键配置 ──
|
||||
const qtcoolModelSelect = overlay.querySelector('#ast-qtcool-model')
|
||||
const qtcoolCustomKeyCheckbox = overlay.querySelector('#ast-qtcool-customkey')
|
||||
const qtcoolKeyRow = overlay.querySelector('#ast-qtcool-keyrow')
|
||||
const qtcoolKeyInput = overlay.querySelector('#ast-qtcool-key')
|
||||
const qtcoolUsageLink = overlay.querySelector('#ast-qtcool-usage')
|
||||
|
||||
// 动态获取模型列表(共享逻辑)
|
||||
;(async () => {
|
||||
@@ -2946,14 +2965,7 @@ function showSettings() {
|
||||
).join('')
|
||||
})()
|
||||
|
||||
qtcoolCustomKeyCheckbox.onchange = () => {
|
||||
qtcoolKeyRow.style.display = qtcoolCustomKeyCheckbox.checked ? '' : 'none'
|
||||
if (qtcoolCustomKeyCheckbox.checked) qtcoolKeyInput.focus()
|
||||
}
|
||||
qtcoolKeyInput.oninput = () => {
|
||||
const key = qtcoolKeyInput.value.trim()
|
||||
qtcoolUsageLink.href = QTCOOL.usageUrl + (key || QTCOOL.defaultKey)
|
||||
}
|
||||
// key input is always visible now (no more built-in key)
|
||||
const qtcoolStatus = overlay.querySelector('#ast-qtcool-status')
|
||||
|
||||
// 测试按钮:快速验证接口可用性
|
||||
@@ -2961,8 +2973,8 @@ function showSettings() {
|
||||
const btn = e.target
|
||||
const selectedModel = qtcoolModelSelect.value
|
||||
if (!selectedModel) { qtcoolStatus.innerHTML = `<span style="color:#fbbf24">${statusIcon('warn', 14)} 请先选择模型</span>`; return }
|
||||
const customKey = qtcoolCustomKeyCheckbox.checked ? qtcoolKeyInput.value.trim() : ''
|
||||
const key = customKey || QTCOOL.defaultKey
|
||||
const key = qtcoolKeyInput.value.trim()
|
||||
if (!key) { qtcoolStatus.innerHTML = `<span style="color:#fbbf24">${statusIcon('warn', 14)} 请先输入 API Key(<a href="${QTCOOL.checkinUrl}" target="_blank" style="color:var(--primary)">签到领取</a>)</span>`; return }
|
||||
|
||||
btn.disabled = true
|
||||
btn.textContent = '测试中...'
|
||||
@@ -2982,10 +2994,17 @@ function showSettings() {
|
||||
qtcoolStatus.innerHTML = `<span style="color:#34d399">${statusIcon('ok', 14)} 测试通过(${(ms/1000).toFixed(1)}s)</span><span style="color:rgba(255,255,255,0.4);margin-left:6px">${selectedModel} 响应正常</span>`
|
||||
} else {
|
||||
const errText = await resp.text().catch(() => '')
|
||||
qtcoolStatus.innerHTML = `<span style="color:#f87171">${statusIcon('err', 14)} 测试失败(HTTP ${resp.status})</span><span style="color:rgba(255,255,255,0.4);margin-left:6px">${errText.slice(0, 80)}</span>`
|
||||
let errMsg = `HTTP ${resp.status}`
|
||||
try {
|
||||
const errJson = JSON.parse(errText)
|
||||
if (errJson.error?.message) errMsg = errJson.error.message
|
||||
} catch { if (errText) errMsg += ' — ' + errText.slice(0, 200) }
|
||||
// 将 URL 转为可点击链接
|
||||
const errHtml = errMsg.replace(/(https?:\/\/[^\s,,。))]+)/g, '<a href="$1" target="_blank" style="color:var(--primary)">$1</a>')
|
||||
qtcoolStatus.innerHTML = `<div style="color:#f87171;line-height:1.5">${statusIcon('err', 14)} <strong>测试失败</strong></div><div style="color:var(--text-secondary);font-size:11px;line-height:1.5;margin-top:4px;word-break:break-all">${errHtml}</div>`
|
||||
}
|
||||
} catch (err) {
|
||||
qtcoolStatus.innerHTML = `<span style="color:#f87171">${statusIcon('err', 14)} 连接失败:${err.message}</span>`
|
||||
qtcoolStatus.innerHTML = `<div style="color:#f87171">${statusIcon('err', 14)} 连接失败:${err.message}</div>`
|
||||
}
|
||||
btn.disabled = false
|
||||
btn.innerHTML = `${icon('search', 12)} 测试`
|
||||
@@ -2995,8 +3014,8 @@ function showSettings() {
|
||||
overlay.querySelector('#ast-qtcool-apply').onclick = async () => {
|
||||
const selectedModel = qtcoolModelSelect.value
|
||||
if (!selectedModel) { qtcoolStatus.innerHTML = `<span style="color:#fbbf24">${statusIcon('warn', 14)} 请先选择模型</span>`; return }
|
||||
const customKey = qtcoolCustomKeyCheckbox.checked ? qtcoolKeyInput.value.trim() : ''
|
||||
const key = customKey || QTCOOL.defaultKey
|
||||
const key = qtcoolKeyInput.value.trim()
|
||||
if (!key) { qtcoolStatus.innerHTML = `<span style="color:#fbbf24">${statusIcon('warn', 14)} 请先输入 API Key(<a href="${QTCOOL.checkinUrl}" target="_blank" style="color:var(--primary)">签到领取</a>)</span>`; return }
|
||||
|
||||
// 1) 填充助手配置
|
||||
overlay.querySelector('#ast-baseurl').value = QTCOOL.baseUrl
|
||||
@@ -3052,6 +3071,76 @@ function showSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
// 同步到 OpenClaw:将助手的 baseUrl/apiKey/model 写入 openclaw.json
|
||||
overlay.querySelector('#ast-qtcool-sync-to')?.addEventListener('click', async () => {
|
||||
const baseUrl = overlay.querySelector('#ast-baseurl').value.trim()
|
||||
const apiKey = overlay.querySelector('#ast-apikey').value.trim()
|
||||
const model = overlay.querySelector('#ast-model').value.trim()
|
||||
if (!baseUrl || !apiKey || !model) {
|
||||
toast('请先在上方配置好 Base URL、API Key 和模型', 'warning')
|
||||
return
|
||||
}
|
||||
const yes = await showConfirm(
|
||||
'同步到 OpenClaw',
|
||||
`将当前助手的模型配置写入 OpenClaw 全局:\n\n• 服务商:晴辰云(qtcool)\n• 模型:${model}\n• 设为全局主模型\n\n此操作会覆盖已有的晴辰云服务商配置并重启 Gateway。`,
|
||||
{ confirmText: '确认同步', cancelText: '取消' }
|
||||
)
|
||||
if (!yes) return
|
||||
try {
|
||||
let config = {}
|
||||
try { config = await api.readOpenclawConfig() } catch {}
|
||||
if (!config.models) config.models = {}
|
||||
if (!config.models.providers) config.models.providers = {}
|
||||
config.models.providers.qtcool = {
|
||||
baseUrl,
|
||||
apiKey,
|
||||
api: 'openai-completions',
|
||||
models: [{ id: model, name: model, contextWindow: 128000, reasoning: model.includes('codex') }]
|
||||
}
|
||||
if (!config.agents) config.agents = {}
|
||||
if (!config.agents.defaults) config.agents.defaults = {}
|
||||
if (!config.agents.defaults.model) config.agents.defaults.model = {}
|
||||
config.agents.defaults.model.primary = 'qtcool/' + model
|
||||
await api.writeOpenclawConfig(config)
|
||||
toast('已同步到 OpenClaw 全局配置,主模型: qtcool/' + model, 'success')
|
||||
try { await api.restartGateway() } catch {}
|
||||
} catch (e) {
|
||||
toast('同步失败: ' + e, 'error')
|
||||
}
|
||||
})
|
||||
|
||||
// 从 OpenClaw 读取:将 openclaw.json 的 qtcool provider 配置填入助手
|
||||
overlay.querySelector('#ast-qtcool-sync-from')?.addEventListener('click', async () => {
|
||||
try {
|
||||
const config = await api.readOpenclawConfig()
|
||||
const qtProvider = config?.models?.providers?.qtcool
|
||||
if (!qtProvider?.baseUrl) {
|
||||
toast('OpenClaw 中尚未配置晴辰云服务商,请先在模型配置页添加', 'info')
|
||||
return
|
||||
}
|
||||
const primary = config?.agents?.defaults?.model?.primary || ''
|
||||
const primaryModel = primary.startsWith('qtcool/') ? primary.slice(7) : ''
|
||||
const firstModel = (qtProvider.models || [])[0]
|
||||
const modelId = primaryModel || (typeof firstModel === 'string' ? firstModel : firstModel?.id) || ''
|
||||
const yes = await showConfirm(
|
||||
'从 OpenClaw 读取',
|
||||
`将 OpenClaw 全局配置填入助手:\n\n• Base URL:${qtProvider.baseUrl}\n• API Key:${qtProvider.apiKey ? '****' + qtProvider.apiKey.slice(-6) : '(空)'}\n${modelId ? '• 模型:' + modelId : ''}\n\n这会覆盖当前助手的模型配置。`,
|
||||
{ confirmText: '确认读取', cancelText: '取消' }
|
||||
)
|
||||
if (!yes) return
|
||||
overlay.querySelector('#ast-baseurl').value = qtProvider.baseUrl
|
||||
if (qtProvider.apiKey) {
|
||||
overlay.querySelector('#ast-apikey').value = qtProvider.apiKey
|
||||
qtcoolKeyInput.value = qtProvider.apiKey
|
||||
}
|
||||
overlay.querySelector('#ast-apitype').value = qtProvider.api || 'openai-completions'
|
||||
if (modelId) overlay.querySelector('#ast-model').value = modelId
|
||||
toast('已从 OpenClaw 读取配置', 'success')
|
||||
} catch (e) {
|
||||
toast('读取 OpenClaw 配置失败: ' + e, 'error')
|
||||
}
|
||||
})
|
||||
|
||||
const resultEl = overlay.querySelector('#ast-test-result')
|
||||
const modelInput = overlay.querySelector('#ast-model')
|
||||
const dropdown = overlay.querySelector('#ast-model-dropdown')
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -25,20 +25,26 @@ export async function render() {
|
||||
服务商是模型的来源(如 OpenAI、DeepSeek 等)。每个服务商下可添加多个模型。
|
||||
标记为「主模型」的将优先使用,其余作为备选自动切换。配置修改后自动保存。
|
||||
</div>
|
||||
<div id="qtcool-promo" style="margin-bottom:var(--space-md);border-radius:var(--radius-lg);background:var(--bg-secondary);border:1px solid var(--border-primary);padding:14px 18px;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:12px">
|
||||
<div style="flex:1;min-width:200px">
|
||||
<div style="display:flex;align-items:center;gap:6px;margin-bottom:4px">
|
||||
${icon('zap', 16)}
|
||||
<span style="font-weight:600;font-size:var(--font-size-sm)">晴辰云</span>
|
||||
<span style="font-size:10px;background:var(--primary);color:#fff;padding:1px 6px;border-radius:8px">推荐</span>
|
||||
</div>
|
||||
<div style="font-size:var(--font-size-xs);color:var(--text-secondary);line-height:1.5">
|
||||
无需自行注册 API,一键添加即可使用。基础模型免费,高级模型低至官方价 2-3 折
|
||||
<div id="qtcool-promo" style="margin-bottom:var(--space-md);border-radius:var(--radius-lg);border:1px solid var(--border-primary);border-left:3px solid var(--primary);background:var(--bg-secondary);padding:16px 20px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:12px;margin-bottom:12px">
|
||||
<div style="flex:1;min-width:200px">
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px">
|
||||
<span style="font-weight:700;font-size:var(--font-size-base);color:var(--text-primary)">${icon('zap', 15)} 晴辰云</span>
|
||||
<span style="font-size:10px;background:var(--primary);color:#fff;padding:1px 7px;border-radius:8px">推荐</span>
|
||||
</div>
|
||||
<div style="font-size:var(--font-size-xs);color:var(--text-secondary);line-height:1.5">
|
||||
GPT-5 / Codex 全系列,低至官方价 2-3 折,不满意随时可退。
|
||||
<a href="${QTCOOL.site}" target="_blank" style="color:var(--primary);text-decoration:none">了解更多 →</a>
|
||||
</div>
|
||||
</div>
|
||||
<a href="${QTCOOL.checkinUrl}" target="_blank" class="btn btn-primary btn-sm">${icon('gift', 12)} 每日签到领额度</a>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;align-items:center;flex-shrink:0">
|
||||
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
|
||||
<input class="form-input" id="qtcool-apikey" placeholder="粘贴 API Key(签到后在用户后台获取)" style="font-size:12px;padding:6px 10px;flex:1;min-width:180px">
|
||||
<button class="btn btn-primary btn-sm" id="btn-qtcool-oneclick">${icon('plus', 14)} 获取模型列表</button>
|
||||
<a href="${QTCOOL.site}" target="_blank" class="btn btn-secondary btn-sm">${icon('external-link', 12)} 了解更多</a>
|
||||
</div>
|
||||
<div style="font-size:11px;color:var(--text-tertiary);margin-top:6px">
|
||||
没有密钥?前往 <a href="${QTCOOL.checkinUrl}" target="_blank" style="color:var(--primary)">签到页</a> 每日签到即可领取免费额度,在 <a href="${QTCOOL.usageUrl}" target="_blank" style="color:var(--primary)">用户后台</a> 复制你的 Key
|
||||
</div>
|
||||
</div>
|
||||
<div id="default-model-bar"></div>
|
||||
@@ -755,11 +761,14 @@ function bindTopActions(page, state) {
|
||||
page.querySelector('#btn-qtcool-oneclick').onclick = async () => {
|
||||
if (!state.config) { toast('配置未加载完成,请稍候', 'warning'); return }
|
||||
|
||||
const bannerKeyInput = page.querySelector('#qtcool-apikey')
|
||||
const bannerKey = bannerKeyInput ? bannerKeyInput.value.trim() : ''
|
||||
|
||||
const btn = page.querySelector('#btn-qtcool-oneclick')
|
||||
btn.textContent = '获取中...'
|
||||
btn.disabled = true
|
||||
|
||||
const models = await fetchQtcoolModels()
|
||||
const models = await fetchQtcoolModels(bannerKey || undefined)
|
||||
|
||||
btn.innerHTML = `${icon('plus', 14)} 获取模型列表`
|
||||
btn.disabled = false
|
||||
@@ -780,6 +789,10 @@ function bindTopActions(page, state) {
|
||||
<div class="modal" style="max-height:80vh;overflow-y:auto">
|
||||
<div class="modal-title">选择要添加的模型</div>
|
||||
<div class="form-hint" style="margin-bottom:12px">从晴辰云获取到 ${models.length} 个可用模型,勾选需要的模型后点击添加。</div>
|
||||
${!existingProvider ? `<div style="margin-bottom:12px">
|
||||
<label class="form-label" style="font-size:var(--font-size-xs)">API Key <a href="${QTCOOL.checkinUrl}" target="_blank" style="color:var(--primary);font-weight:400">每日签到领免费额度 →</a></label>
|
||||
<input class="form-input" id="qtsel-apikey" placeholder="粘贴你的 API Key" style="font-size:12px">
|
||||
</div>` : ''}
|
||||
<div style="margin-bottom:12px;display:flex;gap:8px">
|
||||
<button class="btn btn-sm btn-secondary" id="qtsel-all">全选</button>
|
||||
<button class="btn btn-sm btn-secondary" id="qtsel-none">全不选</button>
|
||||
@@ -801,6 +814,9 @@ function bindTopActions(page, state) {
|
||||
</div>
|
||||
`
|
||||
document.body.appendChild(overlay)
|
||||
// 从横幅预填充 key
|
||||
const dialogKeyInput = overlay.querySelector('#qtsel-apikey')
|
||||
if (dialogKeyInput && bannerKey) dialogKeyInput.value = bannerKey
|
||||
overlay.querySelector('#qtsel-cancel').onclick = () => overlay.remove()
|
||||
overlay.querySelector('#qtsel-all').onclick = () => {
|
||||
overlay.querySelectorAll('#qtmodel-list input:not(:disabled)').forEach(cb => cb.checked = true)
|
||||
@@ -810,9 +826,18 @@ function bindTopActions(page, state) {
|
||||
}
|
||||
overlay.querySelector('#qtsel-confirm').onclick = () => {
|
||||
const selected = [...overlay.querySelectorAll('#qtmodel-list input:checked:not(:disabled)')].map(cb => cb.value)
|
||||
overlay.remove()
|
||||
if (!selected.length) { toast('未选择任何模型', 'info'); return }
|
||||
|
||||
// 新建服务商时需要 API Key
|
||||
const keyInput = overlay.querySelector('#qtsel-apikey')
|
||||
const apiKey = keyInput ? keyInput.value.trim() : ''
|
||||
if (!existingProvider && !apiKey) {
|
||||
toast('请输入 API Key(可通过每日签到免费获取)', 'warning')
|
||||
keyInput?.focus()
|
||||
return
|
||||
}
|
||||
overlay.remove()
|
||||
|
||||
pushUndo(state)
|
||||
if (!state.config.models) state.config.models = {}
|
||||
if (!state.config.models.providers) state.config.models.providers = {}
|
||||
@@ -827,7 +852,7 @@ function bindTopActions(page, state) {
|
||||
} else {
|
||||
state.config.models.providers[QTCOOL.providerKey] = {
|
||||
baseUrl: QTCOOL.baseUrl,
|
||||
apiKey: QTCOOL.defaultKey,
|
||||
apiKey: apiKey,
|
||||
api: QTCOOL.api,
|
||||
models: selectedModels.map(m => ({ ...m })),
|
||||
}
|
||||
@@ -1377,16 +1402,29 @@ async function testModel(btn, state, providerKey, idx) {
|
||||
model.testStatus = 'ok'
|
||||
delete model.testError
|
||||
}
|
||||
toast(`${modelId} 连通正常 (${(elapsed / 1000).toFixed(1)}s): "${reply.slice(0, 50)}"`, 'success')
|
||||
// 包含 ⚠ 的是非致命错误(429 等),拆分显示
|
||||
if (reply.startsWith('⚠')) {
|
||||
const lines = reply.split('\n')
|
||||
const summary = lines[0]
|
||||
const detail = lines.slice(1).join('\n').trim()
|
||||
if (detail) {
|
||||
const detailHtml = detail.replace(/</g, '<').replace(/(https?:\/\/[^\s,,。;))'"&]+)/g, '<a href="$1" target="_blank" style="color:var(--primary);text-decoration:underline">$1</a>')
|
||||
toast(`<strong>${modelId}</strong> ${summary.replace(/</g, '<')}<br><span style="font-size:11px;line-height:1.5;word-break:break-all">${detailHtml}</span>`, 'warning', { duration: 10000, html: true })
|
||||
} else {
|
||||
toast(`${modelId} ${summary}`, 'warning', { duration: 6000 })
|
||||
}
|
||||
} else {
|
||||
toast(`${modelId} 连通正常 (${(elapsed / 1000).toFixed(1)}s): "${reply.slice(0, 50)}"`, 'success')
|
||||
}
|
||||
} catch (e) {
|
||||
const elapsed = Date.now() - start
|
||||
if (typeof model === 'object') {
|
||||
model.latency = null
|
||||
model.lastTestAt = Date.now()
|
||||
model.testStatus = 'fail'
|
||||
model.testError = String(e).slice(0, 100)
|
||||
model.testError = String(e).slice(0, 200)
|
||||
}
|
||||
toast(`${modelId} 不可用 (${(elapsed / 1000).toFixed(1)}s): ${e}`, 'error')
|
||||
toast(`${modelId} 不可用 (${(elapsed / 1000).toFixed(1)}s): ${e}`, 'error', { duration: 8000 })
|
||||
} finally {
|
||||
btn.disabled = false
|
||||
btn.textContent = origText
|
||||
|
||||
@@ -78,19 +78,30 @@ async function loadSkills(page) {
|
||||
function renderSkills(el, data) {
|
||||
const skills = data?.skills || []
|
||||
const cliAvailable = data?.cliAvailable !== false
|
||||
const source = data?.source || ''
|
||||
const cliDiag = data?.diagnostic?.cli || null
|
||||
const eligible = skills.filter(s => s.eligible && !s.disabled)
|
||||
const missing = skills.filter(s => !s.eligible && !s.disabled && !s.blockedByAllowlist)
|
||||
const disabled = skills.filter(s => s.disabled)
|
||||
const blocked = skills.filter(s => s.blockedByAllowlist && !s.disabled)
|
||||
|
||||
const summary = `${eligible.length} 可用 / ${missing.length} 缺依赖 / ${disabled.length} 已禁用`
|
||||
let sourceHint = ''
|
||||
if (source === 'local-scan') {
|
||||
if (cliDiag?.status === 'timeout') sourceHint = 'CLI 可用,但本次调用超时,当前显示本地扫描结果'
|
||||
else if (cliDiag?.status === 'parse-failed') sourceHint = 'CLI 可用,但返回结果解析失败,当前显示本地扫描结果'
|
||||
else if (cliDiag?.status === 'exec-failed') sourceHint = 'CLI 调用失败,当前显示本地扫描结果'
|
||||
else sourceHint = cliAvailable ? '当前显示本地扫描结果' : 'CLI 不可用,当前显示本地扫描结果'
|
||||
} else if (cliAvailable) {
|
||||
sourceHint = '当前已使用 OpenClaw CLI 结果'
|
||||
}
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="clawhub-toolbar">
|
||||
<input class="input clawhub-search-input" id="skill-filter-input" placeholder="过滤 Skills..." type="text">
|
||||
<button class="btn btn-secondary btn-sm" data-action="skill-retry">刷新</button>
|
||||
<a class="btn btn-secondary btn-sm" href="https://clawhub.ai/skills" target="_blank" rel="noopener">ClawHub</a>
|
||||
${!cliAvailable ? '<span class="form-hint" style="margin-left:auto;color:var(--warning)">CLI 不可用,仅显示本地扫描结果</span>' : ''}
|
||||
${sourceHint ? `<span class="form-hint" style="margin-left:auto;color:${source === 'local-scan' ? 'var(--warning)' : 'var(--text-tertiary)'}">${esc(sourceHint)}</span>` : ''}
|
||||
</div>
|
||||
|
||||
<div class="skills-summary" style="margin-bottom:var(--space-lg);color:var(--text-secondary);font-size:var(--font-size-sm)">
|
||||
@@ -136,7 +147,7 @@ function renderSkills(el, data) {
|
||||
<div class="clawhub-panel">
|
||||
<div class="clawhub-empty" style="text-align:center;padding:var(--space-xl)">
|
||||
<div style="margin-bottom:var(--space-sm)">未检测到任何 Skills</div>
|
||||
<div class="form-hint">请确认 OpenClaw 已正确安装。Skills 随 OpenClaw 捆绑提供,也可自定义放置在 <code>~/.openclaw/skills/</code> 目录下。</div>
|
||||
<div class="form-hint">请确认 OpenClaw 已正确安装。Skills 随 OpenClaw 捆绑提供;自定义 Skills 可能位于 <code>~/.openclaw/skills/</code> 或 <code>~/.claude/skills/</code>。</div>
|
||||
</div>
|
||||
</div>` : ''}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user