新增: F-260619-03 PhaseA AI工具文件访问动态权限白名单
- state.rs: AllowedDirs(persistent HashSet + is_authorized workspace_root+starts_with + canonicalize) + AppState allowed_dirs Arc + reload/set/get Settings KV - tool_registry.rs: resolve_workspace_path 多目录白名单双校验(词法+canonicalize) + handler闭包引入AllowedDirs Arc(9文件工具) + run_command不走白名单 - commands/config.rs: ai_get/set_allowed_dirs IPC + lib.rs注册 - 前端: api/ai.ts + AllowedDirsPanel.vue(Settings授权目录UI) + i18n - 测试: 5 AllowedDirs单测 + 基线适配(29工具不变) workspace_root始终授权(零回归); Phase A仅持久化(不含B弹窗/C写约束) 主代兜底: cargo check devflow 0 + test 129 + vue-tsc 0
This commit is contained in:
168
src/components/settings/AllowedDirsPanel.vue
Normal file
168
src/components/settings/AllowedDirsPanel.vue
Normal file
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<!-- ═══ F-260619-03 Phase A: AI 工具授权目录 ═══ -->
|
||||
<section class="panel">
|
||||
<div class="panel-header">
|
||||
<h2>{{ $t('settings.panelAllowedDirs') }}</h2>
|
||||
</div>
|
||||
<div class="allowed-dirs">
|
||||
<!-- 说明 -->
|
||||
<p class="allowed-dirs-desc">{{ $t('settings.descAllowedDirs') }}</p>
|
||||
|
||||
<!-- 列表 -->
|
||||
<div v-if="loading" class="empty-hint">{{ $t('settings.allowedDirsLoading') }}</div>
|
||||
<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)">
|
||||
{{ $t('common.delete') }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="dirs.length === 0" class="empty-hint">{{ $t('settings.allowedDirsEmpty') }}</div>
|
||||
</template>
|
||||
|
||||
<!-- 新增输入 -->
|
||||
<div class="add-row">
|
||||
<input
|
||||
v-model="newDir"
|
||||
class="setting-select dir-input"
|
||||
:placeholder="$t('settings.allowedDirsInputPlaceholder')"
|
||||
@keyup.enter="addDir"
|
||||
/>
|
||||
<button class="btn btn-primary btn-sm" :disabled="!newDir.trim()" @click="addDir">
|
||||
{{ $t('settings.allowedDirsAdd') }}
|
||||
</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">
|
||||
{{ $t('settings.allowedDirsReload') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { aiApi } from '@/api'
|
||||
|
||||
// F-260619-03 Phase A: AI 工具文件访问授权目录白名单 UI
|
||||
// 后端 workspace_root 始终隐式在白名单(is_authorized 内置),前端只管理用户额外授权目录。
|
||||
const dirs = ref<string[]>([])
|
||||
const newDir = ref('')
|
||||
const loading = ref(true)
|
||||
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))
|
||||
} catch (e) {
|
||||
console.error('加载授权目录失败:', e)
|
||||
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')
|
||||
}
|
||||
|
||||
function addDir() {
|
||||
const d = newDir.value.trim()
|
||||
if (!d) return
|
||||
if (dirs.value.some(x => x.toLowerCase() === d.toLowerCase())) {
|
||||
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))
|
||||
} catch (e) {
|
||||
console.error('保存授权目录失败:', e)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function reload() {
|
||||
await load()
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.panel {
|
||||
background: var(--df-bg-card);
|
||||
border: 0.5px solid var(--df-border);
|
||||
border-radius: var(--df-radius-lg);
|
||||
padding: var(--df-pad-panel);
|
||||
}
|
||||
.panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--df-gap-head);
|
||||
}
|
||||
.panel-header h2 { font-size: 15px; font-weight: 500; }
|
||||
|
||||
.allowed-dirs { display: flex; flex-direction: column; gap: 8px; }
|
||||
.allowed-dirs-desc {
|
||||
font-size: 12px; color: var(--df-text-dim); margin: 0 0 4px; line-height: 1.5;
|
||||
}
|
||||
.dir-row {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: var(--df-bg);
|
||||
border: 0.5px solid var(--df-border);
|
||||
border-radius: var(--df-radius-sm);
|
||||
}
|
||||
.dir-path {
|
||||
font-size: 13px; color: var(--df-text);
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
flex: 1; margin-right: 12px;
|
||||
}
|
||||
.empty-hint { font-size: 12px; color: var(--df-text-dim); padding: 8px 0; }
|
||||
.add-row { display: flex; gap: 8px; margin-top: 4px; }
|
||||
.dir-input { flex: 1; min-width: 0; }
|
||||
.actions { display: flex; gap: 8px; margin-top: 8px; }
|
||||
|
||||
.btn {
|
||||
padding: 8px 16px; border: none; border-radius: var(--df-radius-sm);
|
||||
font-size: 13px; cursor: pointer; transition: all 0.15s;
|
||||
}
|
||||
.btn-primary { background: var(--df-accent); color: #fff; }
|
||||
.btn-primary:hover { background: var(--df-accent-hover); }
|
||||
.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-sm { padding: 4px 12px; font-size: 12px; }
|
||||
|
||||
.setting-select {
|
||||
padding: 6px 12px; background: var(--df-bg); border: 0.5px solid var(--df-border);
|
||||
border-radius: var(--df-radius-sm); color: var(--df-text); font-size: 13px; outline: none;
|
||||
}
|
||||
.setting-select:focus { border-color: var(--df-accent); }
|
||||
</style>
|
||||
Reference in New Issue
Block a user