mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-08-01 03:07:55 +08:00
v0.8.0: Ollama兼容、Git自动安装、Gitee镜像、会话重命名、消息渠道Agent绑定、仪表盘重设计、环境检测实时生效、#44修复
This commit is contained in:
@@ -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
113
src/lib/mirror-urls.js
Normal 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 }
|
||||
@@ -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 }),
|
||||
|
||||
Reference in New Issue
Block a user