mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-06 20:02:49 +08:00
fix(settings): support custom git path and robust skills bundled scanning
This commit is contained in:
@@ -129,6 +129,50 @@ function findCommandPath(command) {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeCommandPath(raw) {
|
||||
if (typeof raw !== 'string') return null
|
||||
const trimmed = raw.trim()
|
||||
if (!trimmed) return null
|
||||
const expanded = expandHomePath(trimmed)
|
||||
if (!expanded) return null
|
||||
const looksLikePath =
|
||||
trimmed.includes('/') || trimmed.includes('\\') || trimmed.startsWith('.') || /^~[\\/]/.test(trimmed) || /^[A-Za-z]:[\\/]/.test(trimmed)
|
||||
return looksLikePath ? path.resolve(expanded) : expanded
|
||||
}
|
||||
|
||||
function readConfiguredGitPath() {
|
||||
return normalizeCommandPath(readPanelConfig()?.gitPath || '')
|
||||
}
|
||||
|
||||
function resolveGitExecutable() {
|
||||
const gitPath = readConfiguredGitPath()
|
||||
const isCustom = !!gitPath
|
||||
const isPathLike = !!gitPath && (gitPath.includes('/') || gitPath.includes('\\') || /^[A-Za-z]:[\\/]/.test(gitPath))
|
||||
return { gitPath: gitPath || 'git', isCustom, isPathLike }
|
||||
}
|
||||
|
||||
function buildGitCommandEnv(extraEnv = {}, resolved = resolveGitExecutable()) {
|
||||
const env = { ...process.env, ...(extraEnv || {}) }
|
||||
if (resolved.isCustom && resolved.isPathLike) {
|
||||
const dir = path.dirname(resolved.gitPath)
|
||||
env.PATH = [dir, env.PATH || ''].filter(Boolean).join(path.delimiter)
|
||||
}
|
||||
if (resolved.isCustom) env.GIT = resolved.gitPath
|
||||
return env
|
||||
}
|
||||
|
||||
function runGitSync(args, options = {}) {
|
||||
const resolved = resolveGitExecutable()
|
||||
const env = buildGitCommandEnv(options.env, resolved)
|
||||
const result = spawnSync(resolved.gitPath, args, {
|
||||
encoding: 'utf8',
|
||||
windowsHide: true,
|
||||
...options,
|
||||
env,
|
||||
})
|
||||
return { ...resolved, result }
|
||||
}
|
||||
|
||||
function readConfiguredOpenclawSearchPaths() {
|
||||
const entries = readPanelConfig()?.openclawSearchPaths
|
||||
if (!Array.isArray(entries)) return []
|
||||
@@ -810,25 +854,24 @@ function pickRegistryForPackage(pkg) {
|
||||
}
|
||||
|
||||
function configureGitHttpsRules() {
|
||||
try { execSync('git config --global --unset-all url.https://github.com/.insteadOf 2>&1', { timeout: 5000, windowsHide: true }) } catch {}
|
||||
try { runGitSync(['config', '--global', '--unset-all', 'url.https://github.com/.insteadOf'], { timeout: 5000 }) } catch {}
|
||||
let success = 0
|
||||
for (const from of GIT_HTTPS_REWRITES) {
|
||||
try {
|
||||
execSync(`git config --global --add url.https://github.com/.insteadOf "${from}"`, { timeout: 5000, windowsHide: true })
|
||||
success++
|
||||
const { result } = runGitSync(['config', '--global', '--add', 'url.https://github.com/.insteadOf', from], { timeout: 5000 })
|
||||
if (!result?.error && result?.status === 0) success++
|
||||
} catch {}
|
||||
}
|
||||
return success
|
||||
}
|
||||
|
||||
function buildGitInstallEnv() {
|
||||
const env = {
|
||||
...process.env,
|
||||
const env = buildGitCommandEnv({
|
||||
GIT_TERMINAL_PROMPT: '0',
|
||||
GIT_SSH_COMMAND: 'ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o IdentitiesOnly=yes',
|
||||
GIT_ALLOW_PROTOCOL: 'https:http:file',
|
||||
GIT_CONFIG_COUNT: String(GIT_HTTPS_REWRITES.length),
|
||||
}
|
||||
})
|
||||
GIT_HTTPS_REWRITES.forEach((from, idx) => {
|
||||
env[`GIT_CONFIG_KEY_${idx}`] = 'url.https://github.com/.insteadOf'
|
||||
env[`GIT_CONFIG_VALUE_${idx}`] = from
|
||||
@@ -836,6 +879,155 @@ function buildGitInstallEnv() {
|
||||
return env
|
||||
}
|
||||
|
||||
function parseSkillFrontmatterFile(skillMdPath) {
|
||||
try {
|
||||
const raw = fs.readFileSync(skillMdPath, 'utf8').replace(/\r\n/g, '\n')
|
||||
if (!raw.startsWith('---\n')) return {}
|
||||
const end = raw.indexOf('\n---\n', 4)
|
||||
if (end < 0) return {}
|
||||
const frontmatter = raw.slice(4, end)
|
||||
const result = {}
|
||||
for (const line of frontmatter.split('\n')) {
|
||||
const match = line.match(/^([A-Za-z0-9_-]+)\s*:\s*(.+)$/)
|
||||
if (!match) continue
|
||||
result[match[1]] = match[2].trim().replace(/^['"]|['"]$/g, '')
|
||||
}
|
||||
return result
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function collectLocalSkillRoots() {
|
||||
const roots = []
|
||||
const seen = new Set()
|
||||
const pushRoot = (dir, source, bundled = false) => {
|
||||
if (!dir) return
|
||||
const normalized = path.resolve(dir)
|
||||
const key = isWindows ? normalized.toLowerCase() : normalized
|
||||
if (seen.has(key)) return
|
||||
seen.add(key)
|
||||
roots.push({ dir: normalized, source, bundled })
|
||||
}
|
||||
|
||||
pushRoot(path.join(OPENCLAW_DIR, 'skills'), 'OpenClaw 自定义', false)
|
||||
pushRoot(path.join(homedir(), '.claude', 'skills'), 'Claude 自定义', false)
|
||||
|
||||
const cliPath = resolveOpenclawCliPath()
|
||||
if (cliPath) {
|
||||
const resolvedCli = canonicalCliPath(cliPath) || cliPath
|
||||
const cliDir = path.dirname(resolvedCli)
|
||||
const pkgRoots = [cliDir, path.dirname(cliDir)]
|
||||
for (const pkgRoot of pkgRoots) {
|
||||
const bundledDir = path.join(pkgRoot, 'skills')
|
||||
if (fs.existsSync(bundledDir) && fs.statSync(bundledDir).isDirectory()) {
|
||||
pushRoot(bundledDir, 'openclaw-bundled', true)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isWindows) {
|
||||
const prefix = readWindowsNpmGlobalPrefix() || path.join(process.env.APPDATA || '', 'npm')
|
||||
for (const pkg of ['openclaw', path.join('@qingchencloud', 'openclaw-zh')]) {
|
||||
const bundledDir = path.join(prefix, 'node_modules', pkg, 'skills')
|
||||
if (fs.existsSync(bundledDir) && fs.statSync(bundledDir).isDirectory()) {
|
||||
pushRoot(bundledDir, 'openclaw-bundled', true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return roots
|
||||
}
|
||||
|
||||
function scanSingleSkill(root, name) {
|
||||
const skillPath = path.join(root.dir, name)
|
||||
const skillMd = path.join(skillPath, 'SKILL.md')
|
||||
const packageJson = path.join(skillPath, 'package.json')
|
||||
if (!fs.existsSync(skillMd) && !fs.existsSync(packageJson)) return null
|
||||
|
||||
const result = {
|
||||
name,
|
||||
source: root.source,
|
||||
bundled: !!root.bundled,
|
||||
filePath: skillPath,
|
||||
description: '',
|
||||
eligible: true,
|
||||
disabled: false,
|
||||
blockedByAllowlist: false,
|
||||
requirements: { bins: [], anyBins: [], env: [], config: [], os: [] },
|
||||
missing: { bins: [], anyBins: [], env: [], config: [], os: [] },
|
||||
install: [],
|
||||
}
|
||||
|
||||
try {
|
||||
if (fs.existsSync(packageJson)) {
|
||||
const pkg = JSON.parse(fs.readFileSync(packageJson, 'utf8'))
|
||||
if (pkg.description) result.description = pkg.description
|
||||
if (pkg.homepage) result.homepage = pkg.homepage
|
||||
if (pkg.version) result.version = pkg.version
|
||||
if (pkg.author) result.author = typeof pkg.author === 'string' ? pkg.author : (pkg.author?.name || '')
|
||||
}
|
||||
} catch {}
|
||||
|
||||
const frontmatter = parseSkillFrontmatterFile(skillMd)
|
||||
if (frontmatter.description) result.description = frontmatter.description
|
||||
if (frontmatter.fullPath) result.fullPath = frontmatter.fullPath
|
||||
if (frontmatter.emoji) result.emoji = frontmatter.emoji
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function scanLocalSkillsFallback(cliError = null) {
|
||||
const roots = collectLocalSkillRoots()
|
||||
const skills = []
|
||||
const seen = new Set()
|
||||
const scannedRoots = []
|
||||
|
||||
for (const root of roots) {
|
||||
if (!fs.existsSync(root.dir) || !fs.statSync(root.dir).isDirectory()) continue
|
||||
scannedRoots.push(root.dir)
|
||||
for (const entry of fs.readdirSync(root.dir, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory()) continue
|
||||
const key = isWindows ? entry.name.toLowerCase() : entry.name
|
||||
if (seen.has(key)) continue
|
||||
const skill = scanSingleSkill(root, entry.name)
|
||||
if (!skill) continue
|
||||
seen.add(key)
|
||||
skills.push(skill)
|
||||
}
|
||||
}
|
||||
|
||||
skills.sort((a, b) => String(a.name || '').localeCompare(String(b.name || '')))
|
||||
const eligible = skills.filter(s => s.eligible && !s.disabled)
|
||||
const missingRequirements = 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)
|
||||
|
||||
return {
|
||||
skills,
|
||||
source: 'local-scan',
|
||||
cliAvailable: false,
|
||||
summary: {
|
||||
total: skills.length,
|
||||
eligible: eligible.length,
|
||||
disabled: disabled.length,
|
||||
blocked: blocked.length,
|
||||
missingRequirements: missingRequirements.length,
|
||||
},
|
||||
eligible,
|
||||
disabled,
|
||||
blocked,
|
||||
missingRequirements,
|
||||
diagnostic: {
|
||||
status: 'scanned',
|
||||
scannedAt: new Date().toISOString(),
|
||||
scannedRoots,
|
||||
cli: cliError ? { status: 'exec-failed', message: String(cliError?.message || cliError) } : null,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function detectInstalledSource() {
|
||||
const activeCliPath = resolveOpenclawCliPath()
|
||||
const activeCliSource = classifyCliSource(activeCliPath)
|
||||
@@ -4196,12 +4388,15 @@ const handlers = {
|
||||
},
|
||||
|
||||
check_git() {
|
||||
const { gitPath, isCustom, result } = runGitSync(['--version'], { timeout: 5000 })
|
||||
const detectedPath = isCustom ? gitPath : findCommandPath('git')
|
||||
try {
|
||||
const ver = execSync('git --version', { encoding: 'utf8', timeout: 5000, windowsHide: true }).trim()
|
||||
if (result?.error || result?.status !== 0) throw new Error(result?.error?.message || result?.stderr || result?.stdout || 'git not found')
|
||||
const ver = String(result.stdout || result.stderr || '').trim()
|
||||
const match = ver.match(/(\d+\.\d+[\.\d]*)/)
|
||||
return { installed: true, version: match ? match[1] : ver, path: findCommandPath('git') }
|
||||
return { installed: true, version: match ? match[1] : ver, path: detectedPath, isCustom }
|
||||
} catch {
|
||||
return { installed: false, path: null }
|
||||
return { installed: false, version: null, path: detectedPath, isCustom }
|
||||
}
|
||||
},
|
||||
|
||||
@@ -5107,39 +5302,36 @@ const handlers = {
|
||||
|
||||
// Skills 管理(模拟 openclaw skills CLI JSON 输出)
|
||||
skills_list() {
|
||||
// 尝试真实 CLI
|
||||
try {
|
||||
const out = execSync('npx -y openclaw skills list --json', { encoding: 'utf8', timeout: 30000 })
|
||||
const out = execOpenclawSync(['skills', 'list', '--json'], { encoding: 'utf8', timeout: 30000, cwd: homedir(), windowsHide: true }, '读取 Skills 列表失败')
|
||||
return extractCliJson(out)
|
||||
} catch {
|
||||
// CLI 不可用时返回 mock 数据
|
||||
return {
|
||||
skills: [
|
||||
{ name: 'github', description: 'GitHub operations via gh CLI: issues, PRs, CI runs, code review.', source: 'openclaw-bundled', bundled: true, emoji: '🐙', eligible: true, disabled: false, blockedByAllowlist: false, requirements: { bins: ['gh'], anyBins: [], env: [], config: [], os: [] }, missing: { bins: [], anyBins: [], env: [], config: [], os: [] }, install: [{ id: 'brew', kind: 'brew', label: 'Install GitHub CLI (brew)', bins: ['gh'] }] },
|
||||
{ name: 'weather', description: 'Get current weather and forecasts via wttr.in. No API key needed.', source: 'openclaw-bundled', bundled: true, emoji: '🌤️', eligible: true, disabled: false, blockedByAllowlist: false, requirements: { bins: ['curl'], anyBins: [], env: [], config: [], os: [] }, missing: { bins: [], anyBins: [], env: [], config: [], os: [] }, install: [] },
|
||||
{ name: 'summarize', description: 'Summarize web pages, PDFs, images, audio and more.', source: 'openclaw-bundled', bundled: true, emoji: '📝', eligible: false, disabled: false, blockedByAllowlist: false, requirements: { bins: [], anyBins: [], env: [], config: [], os: [] }, missing: { bins: [], anyBins: [], env: [], config: [], os: [] }, install: [] },
|
||||
{ name: 'slack', description: 'Send and read Slack messages via CLI.', source: 'openclaw-bundled', bundled: true, emoji: '💬', eligible: false, disabled: false, blockedByAllowlist: false, requirements: { bins: ['slack-cli'], anyBins: [], env: [], config: [], os: [] }, missing: { bins: ['slack-cli'], anyBins: [], env: [], config: [], os: [] }, install: [{ id: 'brew', kind: 'brew', label: 'Install Slack CLI (brew)', bins: ['slack-cli'] }] },
|
||||
{ name: 'notion', description: 'Create and search Notion pages using the API.', source: 'openclaw-bundled', bundled: true, emoji: '📓', eligible: false, disabled: true, blockedByAllowlist: false, requirements: { bins: [], anyBins: [], env: ['NOTION_API_KEY'], config: [], os: [] }, missing: { bins: [], anyBins: [], env: ['NOTION_API_KEY'], config: [], os: [] }, install: [] },
|
||||
],
|
||||
source: 'mock',
|
||||
cliAvailable: false,
|
||||
}
|
||||
} catch (e) {
|
||||
return scanLocalSkillsFallback(e)
|
||||
}
|
||||
},
|
||||
skills_info({ name }) {
|
||||
try {
|
||||
const out = execSync(`npx -y openclaw skills info ${JSON.stringify(name)} --json`, { encoding: 'utf8', timeout: 30000 })
|
||||
const out = execOpenclawSync(['skills', 'info', String(name || '').trim(), '--json'], { encoding: 'utf8', timeout: 30000, cwd: homedir(), windowsHide: true }, '查看 Skill 详情失败')
|
||||
return extractCliJson(out)
|
||||
} catch (e) {
|
||||
const fallback = scanLocalSkillsFallback(e).skills.find(skill => skill.name === String(name || '').trim())
|
||||
if (fallback) return fallback
|
||||
throw new Error('查看详情失败: ' + (e.message || e))
|
||||
}
|
||||
},
|
||||
skills_check() {
|
||||
try {
|
||||
const out = execSync('npx -y openclaw skills check --json', { encoding: 'utf8', timeout: 30000 })
|
||||
const out = execOpenclawSync(['skills', 'check', '--json'], { encoding: 'utf8', timeout: 30000, cwd: homedir(), windowsHide: true }, '检查 Skills 依赖失败')
|
||||
return extractCliJson(out)
|
||||
} catch {
|
||||
return { summary: { total: 0, eligible: 0, disabled: 0, blocked: 0, missingRequirements: 0 }, eligible: [], disabled: [], blocked: [], missingRequirements: [] }
|
||||
} catch (e) {
|
||||
const fallback = scanLocalSkillsFallback(e)
|
||||
return {
|
||||
summary: fallback.summary,
|
||||
eligible: fallback.eligible,
|
||||
disabled: fallback.disabled,
|
||||
blocked: fallback.blocked,
|
||||
missingRequirements: fallback.missingRequirements,
|
||||
}
|
||||
}
|
||||
},
|
||||
skills_install_dep({ kind, spec }) {
|
||||
|
||||
@@ -275,13 +275,27 @@ fn recommended_version_for(source: &str) -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取用户配置的 git 可执行文件路径,回退到 "git"
|
||||
fn configured_git_path() -> Option<String> {
|
||||
super::read_panel_config_value()
|
||||
.and_then(|v| v.get("gitPath")?.as_str().map(String::from))
|
||||
.map(|custom| custom.trim().to_string())
|
||||
.filter(|custom| !custom.is_empty())
|
||||
}
|
||||
|
||||
/// 获取用户配置的 git 可执行文件路径,回退到 "git"
|
||||
pub fn git_executable() -> String {
|
||||
configured_git_path().unwrap_or_else(|| "git".into())
|
||||
}
|
||||
|
||||
fn configure_git_https_rules() -> usize {
|
||||
let git = git_executable();
|
||||
// Collect unique target prefixes to unset old rules
|
||||
let targets: std::collections::HashSet<&str> =
|
||||
GIT_HTTPS_REWRITES.iter().map(|(t, _)| *t).collect();
|
||||
for target in &targets {
|
||||
let key = format!("url.{target}.insteadOf");
|
||||
let mut unset = Command::new("git");
|
||||
let mut unset = Command::new(&git);
|
||||
unset.args(["config", "--global", "--unset-all", &key]);
|
||||
#[cfg(target_os = "windows")]
|
||||
unset.creation_flags(0x08000000);
|
||||
@@ -291,7 +305,7 @@ fn configure_git_https_rules() -> usize {
|
||||
let mut success = 0;
|
||||
for (target, from) in GIT_HTTPS_REWRITES {
|
||||
let key = format!("url.{target}.insteadOf");
|
||||
let mut cmd = Command::new("git");
|
||||
let mut cmd = Command::new(&git);
|
||||
cmd.args(["config", "--global", "--add", &key, from]);
|
||||
#[cfg(target_os = "windows")]
|
||||
cmd.creation_flags(0x08000000);
|
||||
@@ -303,6 +317,21 @@ fn configure_git_https_rules() -> usize {
|
||||
}
|
||||
|
||||
fn apply_git_install_env(cmd: &mut Command) {
|
||||
if let Some(custom_git) = configured_git_path() {
|
||||
let git_path = PathBuf::from(&custom_git);
|
||||
if let Some(parent) = git_path.parent() {
|
||||
let mut paths: Vec<PathBuf> = std::env::var_os("PATH")
|
||||
.map(|value| std::env::split_paths(&value).collect())
|
||||
.unwrap_or_default();
|
||||
if !paths.iter().any(|p| p == parent) {
|
||||
paths.insert(0, parent.to_path_buf());
|
||||
}
|
||||
if let Ok(joined) = std::env::join_paths(paths) {
|
||||
cmd.env("PATH", joined);
|
||||
}
|
||||
}
|
||||
cmd.env("GIT", &custom_git);
|
||||
}
|
||||
crate::commands::apply_proxy_env(cmd);
|
||||
cmd.env("GIT_TERMINAL_PROMPT", "0")
|
||||
.env(
|
||||
@@ -5297,8 +5326,15 @@ pub fn set_npm_registry(registry: String) -> Result<(), String> {
|
||||
#[tauri::command]
|
||||
pub fn check_git() -> Result<Value, String> {
|
||||
let mut result = serde_json::Map::new();
|
||||
let git_path = find_git_path();
|
||||
let mut cmd = Command::new("git");
|
||||
let configured = configured_git_path();
|
||||
let git = configured.clone().unwrap_or_else(|| "git".into());
|
||||
let is_custom = configured.is_some();
|
||||
let git_path = if is_custom {
|
||||
Some(git.clone())
|
||||
} else {
|
||||
find_git_path()
|
||||
};
|
||||
let mut cmd = Command::new(&git);
|
||||
cmd.arg("--version");
|
||||
#[cfg(target_os = "windows")]
|
||||
cmd.creation_flags(0x08000000);
|
||||
@@ -5311,11 +5347,13 @@ pub fn check_git() -> Result<Value, String> {
|
||||
"path".into(),
|
||||
git_path.map(Value::String).unwrap_or(Value::Null),
|
||||
);
|
||||
result.insert("isCustom".into(), Value::Bool(is_custom));
|
||||
}
|
||||
_ => {
|
||||
result.insert("installed".into(), Value::Bool(false));
|
||||
result.insert("version".into(), Value::Null);
|
||||
result.insert("path".into(), Value::Null);
|
||||
result.insert("isCustom".into(), Value::Bool(is_custom));
|
||||
}
|
||||
}
|
||||
Ok(Value::Object(result))
|
||||
|
||||
@@ -821,6 +821,36 @@ fn custom_skill_roots() -> Vec<(std::path::PathBuf, &'static str)> {
|
||||
roots.push((claude_skills, "Claude 自定义"));
|
||||
}
|
||||
}
|
||||
// 从已解析的 CLI 路径推导 npm 包内的 bundled skills 目录
|
||||
// 例如 CLI 在 /usr/lib/node_modules/openclaw/bin/openclaw
|
||||
// → 包根 /usr/lib/node_modules/openclaw/
|
||||
// → skills 目录 /usr/lib/node_modules/openclaw/skills/
|
||||
if let Some(cli_path) = crate::utils::resolve_openclaw_cli_path() {
|
||||
let cli = std::path::PathBuf::from(&cli_path);
|
||||
let cli = std::fs::canonicalize(&cli).unwrap_or(cli);
|
||||
// CLI 可能在 bin/ 子目录或包根目录
|
||||
for ancestor in [cli.parent(), cli.parent().and_then(|p| p.parent())] {
|
||||
if let Some(pkg_root) = ancestor {
|
||||
let bundled = pkg_root.join("skills");
|
||||
if bundled.is_dir() && !roots.iter().any(|(dir, _)| dir == &bundled) {
|
||||
roots.push((bundled, "OpenClaw 内置"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
if let Some(prefix) = super::windows_npm_global_prefix() {
|
||||
for pkg in ["openclaw", "@qingchencloud/openclaw-zh"] {
|
||||
let bundled = std::path::PathBuf::from(&prefix)
|
||||
.join("node_modules")
|
||||
.join(pkg)
|
||||
.join("skills");
|
||||
if bundled.is_dir() && !roots.iter().any(|(dir, _)| dir == &bundled) {
|
||||
roots.push((bundled, "OpenClaw 内置"));
|
||||
}
|
||||
}
|
||||
}
|
||||
roots
|
||||
}
|
||||
|
||||
|
||||
@@ -76,4 +76,10 @@ export default {
|
||||
proxyCleared: _('网络代理已关闭', 'Network proxy disabled', '網路代理已關閉', 'ネットワークプロキシ無効化', '네트워크 프록시 비활성화됨'),
|
||||
modelProxyOn: _('模型请求将走代理', 'Model requests will use proxy', '模型請求將走代理', 'モデルリクエストはプロキシを使用します', '모델 요청이 프록시를 사용합니다'),
|
||||
modelProxyOff: _('模型请求已关闭代理', 'Model requests proxy disabled', '模型請求已關閉代理', 'モデルリクエストプロキシ無効化', '모델 요청 프록시 비활성화됨'),
|
||||
gitPath: _('Git 可执行文件路径', 'Git Executable Path', 'Git 可執行檔路徑', 'Git 実行ファイルパス', 'Git 실행 파일 경로'),
|
||||
gitPathHint: _('自定义 Git 可执行文件路径。留空则自动从系统 PATH 中查找。当系统找不到 Git 时,可在此手动指定完整路径。', 'Custom Git executable path. Leave empty to auto-detect from system PATH. Specify the full path here if the system cannot find Git.', '自定義 Git 可執行檔路徑。留空則自動從系統 PATH 中尋找。當系統找不到 Git 時,可在此手動指定完整路徑。', 'カスタム Git 実行ファイルパス。空欄にするとシステム PATH から自動検出します。システムが Git を見つけられない場合はここにフルパスを指定してください。'),
|
||||
gitPathPlaceholder: _('留空自动检测,例如 C:\\Program Files\\Git\\cmd\\git.exe', 'Leave empty for auto-detect, e.g. C:\\Program Files\\Git\\cmd\\git.exe', '留空自動檢測,例如 /usr/local/bin/git', '空欄で自動検出、例: C:\\Program Files\\Git\\cmd\\git.exe'),
|
||||
gitPathSaved: _('Git 路径已保存', 'Git path saved', 'Git 路徑已儲存', 'Git パス保存済み', 'Git 경로 저장됨'),
|
||||
gitPathCleared: _('已恢复 Git 自动检测', 'Git auto-detect restored', '已恢復 Git 自動檢測', 'Git 自動検出に戻しました', 'Git 자동 감지로 복원됨'),
|
||||
gitPathInvalid: _('指定的 Git 路径不存在', 'The specified Git path does not exist', '指定的 Git 路徑不存在', '指定された Git パスが存在しません'),
|
||||
}
|
||||
|
||||
@@ -100,6 +100,11 @@ export async function render() {
|
||||
<div id="docker-defaults-bar"><div class="stat-card loading-placeholder" style="height:84px"></div></div>
|
||||
</div>
|
||||
|
||||
<div class="config-section" id="git-path-section">
|
||||
<div class="config-section-title">${t('settings.gitPath')}</div>
|
||||
<div id="git-path-bar"><div class="stat-card loading-placeholder" style="height:48px"></div></div>
|
||||
</div>
|
||||
|
||||
<div class="config-section" id="cli-binding-section">
|
||||
<div class="config-section-title">${t('settings.openclawCli')}</div>
|
||||
<div id="cli-binding-bar"><div class="stat-card loading-placeholder" style="height:48px"></div></div>
|
||||
@@ -123,7 +128,7 @@ export async function render() {
|
||||
}
|
||||
|
||||
async function loadAll(page) {
|
||||
const tasks = [loadProxyConfig(page), loadModelProxyConfig(page), loadOpenclawDir(page), loadOpenclawSearchPaths(page), loadDockerDefaults(page), loadCliBinding(page)]
|
||||
const tasks = [loadProxyConfig(page), loadModelProxyConfig(page), loadOpenclawDir(page), loadOpenclawSearchPaths(page), loadDockerDefaults(page), loadGitPath(page), loadCliBinding(page)]
|
||||
tasks.push(loadRegistry(page))
|
||||
if (window.__TAURI_INTERNALS__) tasks.push(loadAutostart(page))
|
||||
await Promise.all(tasks)
|
||||
@@ -460,6 +465,12 @@ function bindEvents(page) {
|
||||
case 'save-docker-defaults':
|
||||
await handleSaveDockerDefaults(page)
|
||||
break
|
||||
case 'save-git-path':
|
||||
await handleSaveGitPath(page)
|
||||
break
|
||||
case 'reset-git-path':
|
||||
await handleResetGitPath(page)
|
||||
break
|
||||
case 'bind-cli':
|
||||
await handleBindCli(page, btn.dataset.path)
|
||||
break
|
||||
@@ -548,6 +559,68 @@ async function handleSaveRegistry(page) {
|
||||
toast(t('settings.registrySaved'), 'success')
|
||||
}
|
||||
|
||||
// ===== Git 路径 =====
|
||||
|
||||
async function loadGitPath(page) {
|
||||
const bar = page.querySelector('#git-path-bar')
|
||||
if (!bar) return
|
||||
try {
|
||||
const gitInfo = await api.checkGit()
|
||||
const cfg = await api.readPanelConfig()
|
||||
const customValue = cfg?.gitPath || ''
|
||||
const invalidCustom = gitInfo.isCustom && !gitInfo.installed
|
||||
const statusText = gitInfo.installed
|
||||
? `<span style="color:var(--success)">✓ ${escapeHtml(gitInfo.version || 'Git')}</span>`
|
||||
: invalidCustom
|
||||
? `<span style="color:var(--error)">✗ ${t('settings.gitPathInvalid')}</span>`
|
||||
: `<span style="color:var(--error)">✗ Git ${t('setup.notInstalled')}</span>`
|
||||
const pathText = gitInfo.path ? `<span style="font-size:var(--font-size-xs);opacity:0.7">${escapeHtml(gitInfo.path)}</span>` : ''
|
||||
const customBadge = gitInfo.isCustom ? `<span class="badge" style="margin-left:6px;font-size:10px">${t('settings.customBadge')}</span>` : ''
|
||||
bar.innerHTML = `
|
||||
<div class="stat-card" style="padding:16px">
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">
|
||||
${statusText}${customBadge}
|
||||
</div>
|
||||
${pathText ? `<div style="margin-bottom:10px">${pathText}</div>` : ''}
|
||||
<p style="font-size:var(--font-size-xs);color:var(--text-tertiary);margin-bottom:12px;line-height:1.5">${t('settings.gitPathHint')}</p>
|
||||
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
|
||||
<input class="input" data-name="git-path" value="${escapeHtml(customValue)}" placeholder="${t('settings.gitPathPlaceholder')}" style="flex:1;min-width:200px">
|
||||
<button class="btn btn-primary btn-sm" data-action="save-git-path">${t('common.save')}</button>
|
||||
<button class="btn btn-secondary btn-sm" data-action="reset-git-path">${t('settings.resetDefault')}</button>
|
||||
</div>
|
||||
</div>`
|
||||
} catch (e) {
|
||||
bar.innerHTML = `<div class="stat-card" style="padding:16px;color:var(--error)">${e}</div>`
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaveGitPath(page) {
|
||||
const input = page.querySelector('[data-name="git-path"]')
|
||||
const value = (input?.value || '').trim()
|
||||
const cfg = await api.readPanelConfig()
|
||||
if (value) {
|
||||
cfg.gitPath = value
|
||||
} else {
|
||||
delete cfg.gitPath
|
||||
}
|
||||
await api.writePanelConfig(cfg)
|
||||
const gitInfo = await api.checkGit()
|
||||
if (value && gitInfo.isCustom && !gitInfo.installed) {
|
||||
toast(t('settings.gitPathInvalid'), 'error')
|
||||
} else {
|
||||
toast(value ? t('settings.gitPathSaved') : t('settings.gitPathCleared'), 'success')
|
||||
}
|
||||
await loadGitPath(page)
|
||||
}
|
||||
|
||||
async function handleResetGitPath(page) {
|
||||
const cfg = await api.readPanelConfig()
|
||||
delete cfg.gitPath
|
||||
await api.writePanelConfig(cfg)
|
||||
toast(t('settings.gitPathCleared'), 'success')
|
||||
await loadGitPath(page)
|
||||
}
|
||||
|
||||
// ===== CLI 绑定 =====
|
||||
|
||||
async function loadCliBinding(page) {
|
||||
|
||||
Reference in New Issue
Block a user