v0.8.0: Ollama兼容、Git自动安装、Gitee镜像、会话重命名、消息渠道Agent绑定、仪表盘重设计、环境检测实时生效、#44修复

This commit is contained in:
晴天
2026-03-12 02:17:47 +08:00
parent 62c8fc9355
commit 3e24ceaa4d
32 changed files with 2972 additions and 487 deletions

View File

@@ -46,6 +46,32 @@ function escapeHtml(str) {
.replace(/'/g, ''')
}
// 预加载 Tauri convertFileSrc
let _convertFileSrc = null
if (typeof window !== 'undefined' && window.__TAURI_INTERNALS__) {
import('@tauri-apps/api/core').then(m => { _convertFileSrc = m.convertFileSrc }).catch(() => {})
}
/** 将本地文件路径转换为可加载的 URL */
function resolveImageSrc(src) {
if (!src) return src
// 已经是 http/https/data URL → 直接返回
if (/^(https?|data|blob):/.test(src)) return src
// Windows 绝对路径 (C:\... or C:/...)
const isWinPath = /^[A-Za-z]:[\\/]/.test(src)
// Unix 绝对路径 (/Users/... /home/... /tmp/...)
const isUnixPath = /^\/[^/]/.test(src)
if (isWinPath || isUnixPath) {
// Tauri 环境:使用 convertFileSrc 转换为 asset protocol URL
if (_convertFileSrc) {
try { return _convertFileSrc(src) } catch {}
}
// Tauri 未就绪或 Web 模式返回原始路径onerror 会处理显示)
return src
}
return src
}
export function renderMarkdown(text) {
if (!text) return ''
let html = text
@@ -122,7 +148,10 @@ function inlineFormat(text) {
.replace(/\*(.+?)\*/g, '<em>$1</em>')
.replace(/__(.+?)__/g, '<strong>$1</strong>')
.replace(/_(.+?)_/g, '<em>$1</em>')
.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" class="msg-img" />')
.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_, alt, src) => {
const safeSrc = resolveImageSrc(src.trim())
return `<img src="${safeSrc}" alt="${alt}" class="msg-img" onerror="this.onerror=null;this.style.display='none';this.insertAdjacentHTML('afterend','<span style=\\'color:var(--text-tertiary);font-size:12px\\'>[图片无法加载: ${escapeHtml(src)}]</span>')" />`
})
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, url) => {
const safe = /^https?:|^mailto:/i.test(url.trim()) ? url : '#'
return `<a href="${safe}" target="_blank" rel="noopener">${label}</a>`

113
src/lib/mirror-urls.js Normal file
View File

@@ -0,0 +1,113 @@
/**
* GitHub / Gitee 镜像 URL 管理
* 国内用户自动使用 Gitee 镜像,解决 GitHub 访问慢/不可达的问题
*/
const GITHUB_ORG = 'https://github.com/qingchencloud'
const GITEE_ORG = 'https://gitee.com/QtCodeCreators'
const GITHUB_RAW = 'https://raw.githubusercontent.com/qingchencloud'
const GITEE_RAW = 'https://gitee.com/QtCodeCreators'
// 仓库名映射GitHub → Gitee名称不同时需映射
const REPO_MAP = {
clawpanel: 'clawpanel',
clawapp: 'clawapp',
cftunnel: 'cftunnel',
'openclaw-zh': 'openclaw-zh',
}
/**
* 探测 GitHub 是否可达3s 超时)
* 结果缓存 5 分钟
*/
let _githubReachable = null
let _lastCheck = 0
const CHECK_TTL = 300000 // 5min
async function isGithubReachable() {
const now = Date.now()
if (_githubReachable !== null && now - _lastCheck < CHECK_TTL) return _githubReachable
try {
const ctrl = new AbortController()
const timer = setTimeout(() => ctrl.abort(), 3000)
await fetch('https://github.com/favicon.ico', { method: 'HEAD', mode: 'no-cors', signal: ctrl.signal })
clearTimeout(timer)
_githubReachable = true
} catch {
_githubReachable = false
}
_lastCheck = now
return _githubReachable
}
/**
* 获取仓库 URL优先 GitHub不可达时用 Gitee
* @param {string} repo - 仓库名,如 'clawpanel'
* @param {string} [path] - 可选路径,如 '/releases'、'/issues/new'
*/
export async function repoUrl(repo, path = '') {
const giteeRepo = REPO_MAP[repo] || repo
if (await isGithubReachable()) {
return `${GITHUB_ORG}/${repo}${path}`
}
return `${GITEE_ORG}/${giteeRepo}${path}`
}
/**
* 同步版本:同时返回 GitHub 和 Gitee URL让 UI 可以展示两个链接
* @param {string} repo
* @param {string} [path]
*/
export function repoBothUrls(repo, path = '') {
const giteeRepo = REPO_MAP[repo] || repo
return {
github: `${GITHUB_ORG}/${repo}${path}`,
gitee: `${GITEE_ORG}/${giteeRepo}${path}`,
}
}
/**
* 获取 raw 文件 URL用于 deploy.sh 等脚本下载)
* GitHub: raw.githubusercontent.com/org/repo/branch/file
* Gitee: gitee.com/org/repo/raw/branch/file
* @param {string} repo
* @param {string} branch
* @param {string} filePath
*/
export async function rawFileUrl(repo, branch, filePath) {
const giteeRepo = REPO_MAP[repo] || repo
if (await isGithubReachable()) {
return `${GITHUB_RAW}/${repo}/${branch}/${filePath}`
}
return `${GITEE_RAW}/${giteeRepo}/raw/${branch}/${filePath}`
}
/**
* deploy.sh 下载命令(国内用户自动切换为 Gitee 源)
*/
export function deployCommand() {
return {
github: `curl -fsSL ${GITHUB_RAW}/clawpanel/main/deploy.sh | bash`,
gitee: `curl -fsSL ${GITEE_RAW}/clawpanel/raw/main/deploy.sh | bash`,
}
}
/** 强制标记 GitHub 不可达(用户手动切换时调用) */
export function forceGiteeMirror() {
_githubReachable = false
_lastCheck = Date.now()
}
/** 强制标记 GitHub 可达 */
export function forceGithubDirect() {
_githubReachable = true
_lastCheck = Date.now()
}
/** 当前是否使用 Gitee 镜像 */
export function isUsingGitee() {
return _githubReachable === false
}
/** 手动触发一次 GitHub 可达性检测 */
export { isGithubReachable as checkGithubReachable }

View File

@@ -170,8 +170,8 @@ export const api = {
uninstallGateway: () => invoke('uninstall_gateway'),
getNpmRegistry: () => cachedInvoke('get_npm_registry', {}, 30000),
setNpmRegistry: (registry) => { invalidate('get_npm_registry'); return invoke('set_npm_registry', { registry }) },
testModel: (baseUrl, apiKey, modelId) => invoke('test_model', { baseUrl, apiKey, modelId }),
listRemoteModels: (baseUrl, apiKey) => invoke('list_remote_models', { baseUrl, apiKey }),
testModel: (baseUrl, apiKey, modelId, apiType = null) => invoke('test_model', { baseUrl, apiKey, modelId, apiType }),
listRemoteModels: (baseUrl, apiKey, apiType = null) => invoke('list_remote_models', { baseUrl, apiKey, apiType }),
// Agent 管理
listAgents: () => cachedInvoke('list_agents'),
@@ -199,7 +199,9 @@ export const api = {
toggleMessagingPlatform: (platform, enabled) => { invalidate('list_configured_platforms'); return invoke('toggle_messaging_platform', { platform, enabled }) },
verifyBotToken: (platform, form) => invoke('verify_bot_token', { platform, form }),
listConfiguredPlatforms: () => cachedInvoke('list_configured_platforms', {}, 5000),
getChannelPluginStatus: (pluginId) => invoke('get_channel_plugin_status', { pluginId }),
installQqbotPlugin: () => invoke('install_qqbot_plugin'),
installChannelPlugin: (packageName, pluginId) => invoke('install_channel_plugin', { packageName, pluginId }),
// 面板配置 (clawpanel.json)
readPanelConfig: () => invoke('read_panel_config'),
@@ -211,7 +213,11 @@ export const api = {
checkNode: () => cachedInvoke('check_node', {}, 60000),
checkNodeAtPath: (nodeDir) => invoke('check_node_at_path', { nodeDir }),
scanNodePaths: () => invoke('scan_node_paths'),
saveCustomNodePath: (nodeDir) => invoke('save_custom_node_path', { nodeDir }),
saveCustomNodePath: (nodeDir) => invoke('save_custom_node_path', { nodeDir }).then(r => { invalidate('check_node'); invoke('invalidate_path_cache').catch(() => {}); return r }),
invalidatePathCache: () => invoke('invalidate_path_cache'),
checkGit: () => cachedInvoke('check_git', {}, 60000),
autoInstallGit: () => invoke('auto_install_git'),
configureGitHttps: () => invoke('configure_git_https'),
getDeployConfig: () => cachedInvoke('get_deploy_config'),
patchModelVision: () => invoke('patch_model_vision'),
checkPanelUpdate: () => invoke('check_panel_update'),
@@ -229,6 +235,8 @@ export const api = {
// 设备配对
autoPairDevice: () => invoke('auto_pair_device'),
checkPairingStatus: () => invoke('check_pairing_status'),
pairingListChannel: (channel) => invoke('pairing_list_channel', { channel }),
pairingApproveChannel: (channel, code, notify = false) => invoke('pairing_approve_channel', { channel, code, notify }),
// AI 助手工具
assistantExec: (command, cwd) => invoke('assistant_exec', { command, cwd: cwd || null }),