优化: AI授权目录改进(误弹窗根因+安全加固)
strip_verbatim比对侧收口/黑名单统一/symlink/bind双路径reload/toast接线;cargo test 18✅
This commit is contained in:
@@ -13,7 +13,7 @@
|
||||
<template v-else>
|
||||
<div v-for="(dir, idx) in dirs" :key="dir + idx" class="dir-row">
|
||||
<span class="dir-path" :title="dir">{{ dir }}</span>
|
||||
<button class="btn btn-ghost btn-sm" @click="removeDir(idx)">
|
||||
<button class="btn btn-ghost btn-sm" :disabled="saving" @click="removeDir(idx)">
|
||||
{{ $t('common.delete') }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -26,19 +26,16 @@
|
||||
v-model="newDir"
|
||||
class="setting-select dir-input"
|
||||
:placeholder="$t('settings.allowedDirsInputPlaceholder')"
|
||||
@keyup.enter="addDir"
|
||||
@keyup.enter="onAddClick"
|
||||
/>
|
||||
<button class="btn btn-primary btn-sm" :disabled="!newDir.trim()" @click="addDir">
|
||||
{{ $t('settings.allowedDirsAdd') }}
|
||||
<button class="btn btn-primary btn-sm" :disabled="saving" @click="onAddClick">
|
||||
{{ newDir.trim() ? $t('settings.allowedDirsAdd') : $t('settings.allowedDirsSelectDir') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 操作 -->
|
||||
<div class="actions">
|
||||
<button class="btn btn-primary btn-sm" :disabled="saving" @click="save">
|
||||
{{ $t('settings.allowedDirsSave') }}
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm" :disabled="saving" @click="reload">
|
||||
<button class="btn btn-ghost btn-sm" :disabled="loading" @click="reload">
|
||||
{{ $t('settings.allowedDirsReload') }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -48,10 +45,19 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { aiApi } from '@/api'
|
||||
|
||||
const { t } = useI18n()
|
||||
// F-260620: 接共享 toast 通道(Settings.vue showToast),替代 console.error/warn 用户零反馈
|
||||
const emit = defineEmits<{ (e: 'toast', msg: string, type: 'error' | 'warning' | 'info'): void }>()
|
||||
|
||||
// F-260619-03 Phase A: AI 工具文件访问授权目录白名单 UI
|
||||
// 后端 workspace_root 始终隐式在白名单(is_authorized 内置),前端只管理用户额外授权目录。
|
||||
// 即时持久化:添加/删除立即写后端 KV + 内存白名单,无需显式"保存"(符合"操作即生效"心智,
|
||||
// 避免暂存+刷新丢失的认知错配)。dirs 始终由后端 setAllowedDirs 返回值回填(canonicalize +
|
||||
// strip_verbatim 规范化后的真实路径),保证前后端读写一致。workspace_root 由后端 get 返回时
|
||||
// 过滤(内部隐式授权,不进用户管理列表),前端原样展示不再猜测 root 形态。
|
||||
const dirs = ref<string[]>([])
|
||||
const newDir = ref('')
|
||||
const loading = ref(true)
|
||||
@@ -60,47 +66,69 @@ const saving = ref(false)
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const all = await aiApi.getAllowedDirs()
|
||||
// 后端返回的列表含 workspace_root(隐式授权),前端展示时过滤掉以减少混淆
|
||||
// (workspace_root 始终授权无需用户管理)。保留其它用户授权目录。
|
||||
dirs.value = all.filter(d => !isWorkspaceRoot(d))
|
||||
dirs.value = await aiApi.getAllowedDirs()
|
||||
} catch (e) {
|
||||
console.error('加载授权目录失败:', e)
|
||||
emit('toast', t('settings.allowedDirsLoadFail'), 'error')
|
||||
dirs.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 粗略判定 workspace_root(src-tauri 上两级,即项目根):canonicalize 后路径以
|
||||
// src-tauri 结尾。Windows canonicalize 带 \\?\ 前缀,此处宽松匹配防大小写/分隔符差异。
|
||||
function isWorkspaceRoot(p: string): boolean {
|
||||
const lower = p.toLowerCase().replace(/\\/g, '/')
|
||||
return lower.endsWith('/src-tauri')
|
||||
// 单按钮双态:newDir 空 → 选目录(Tauri dialog::open 填入 newDir);有值 → 提交白名单
|
||||
async function onAddClick() {
|
||||
if (!newDir.value.trim()) {
|
||||
await pickDir()
|
||||
return
|
||||
}
|
||||
await addDir()
|
||||
}
|
||||
|
||||
function addDir() {
|
||||
async function pickDir() {
|
||||
try {
|
||||
const selected = await open({ directory: true, multiple: false })
|
||||
if (!selected || Array.isArray(selected)) return
|
||||
newDir.value = selected as string
|
||||
// F-260620: 选完即加(消除选目录后需二次点"添加"的摩擦)
|
||||
await addDir()
|
||||
} catch (e) {
|
||||
console.error('选择目录失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加:本地大小写不敏感去重预检(减少无效 IPC)+ 即时持久化。
|
||||
// dirs 由 setAllowedDirs 返回值回填,保证与后端规范化形态一致(读写不再漂移)。
|
||||
async function addDir() {
|
||||
const d = newDir.value.trim()
|
||||
if (!d) return
|
||||
if (dirs.value.some(x => x.toLowerCase() === d.toLowerCase())) {
|
||||
emit('toast', t('settings.allowedDirsDup'), 'warning')
|
||||
newDir.value = ''
|
||||
return
|
||||
}
|
||||
dirs.value.push(d)
|
||||
newDir.value = ''
|
||||
}
|
||||
|
||||
function removeDir(idx: number) {
|
||||
dirs.value.splice(idx, 1)
|
||||
}
|
||||
|
||||
async function save() {
|
||||
saving.value = true
|
||||
try {
|
||||
const normalized = await aiApi.setAllowedDirs(dirs.value)
|
||||
dirs.value = normalized.filter(d => !isWorkspaceRoot(d))
|
||||
const next = [...dirs.value, d]
|
||||
dirs.value = await aiApi.setAllowedDirs(next)
|
||||
newDir.value = ''
|
||||
} catch (e) {
|
||||
console.error('保存授权目录失败:', e)
|
||||
console.error('添加授权目录失败:', e)
|
||||
emit('toast', t('settings.allowedDirsAddFail'), 'error')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 删除:即时持久化,dirs 由后端返回值回填(与添加对称,删除也立即生效)。
|
||||
async function removeDir(idx: number) {
|
||||
const next = dirs.value.filter((_, i) => i !== idx)
|
||||
saving.value = true
|
||||
try {
|
||||
dirs.value = await aiApi.setAllowedDirs(next)
|
||||
} catch (e) {
|
||||
console.error('删除授权目录失败:', e)
|
||||
emit('toast', t('settings.allowedDirsRemoveFail'), 'error')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
@@ -158,6 +186,7 @@ onMounted(load)
|
||||
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.btn-ghost { background: transparent; color: var(--df-text-secondary); border: 0.5px solid var(--df-border); }
|
||||
.btn-ghost:hover { background: var(--df-bg-card); color: var(--df-text); }
|
||||
.btn-ghost:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.btn-sm { padding: 4px 12px; font-size: 12px; }
|
||||
|
||||
.setting-select {
|
||||
|
||||
Reference in New Issue
Block a user