feat: 统一应用配置和内容过滤规则

This commit is contained in:
chuan
2026-08-10 15:33:12 +08:00
parent f6e5c1ceca
commit 9f0911c596
16 changed files with 226 additions and 98 deletions
+2 -2
View File
@@ -8,7 +8,7 @@
```powershell
$env:LIBCLANG_PATH = "D:\tools\dht\.tools\libclang\clang\native"
cargo run -p dht-search --bin dht-search -- --config dht-search.toml
cargo run -p dht-search --bin dht-search -- --config config.toml
```
然后打开另一个终端启动前端开发服务
@@ -40,7 +40,7 @@ cd src/web
bun install --frozen-lockfile
bun run build
cd ../..
cargo run --release -p dht-search --bin dht-search -- --config dht-search.toml
cargo run --release -p dht-search --bin dht-search -- --config config.toml
```
打开 `http://127.0.0.1:8080`
+70 -5
View File
@@ -1,12 +1,12 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { AlertTriangle, Check, FileCog, LoaderCircle, RotateCcw, Save } from '@lucide/vue'
import { AlertTriangle, Check, FileCog, LoaderCircle, Plus, RotateCcw, Save, Trash2 } from '@lucide/vue'
import { TabsContent, TabsList, TabsRoot, TabsTrigger } from 'reka-ui'
import { onBeforeRouteLeave } from 'vue-router'
import { Button } from '@/components/ui/button'
import { getConfig, updateConfig } from '@/lib/api'
import type { AppConfigDto, ConfigSnapshot } from '@/types/api'
import type { AppConfigDto, ConfigSnapshot, ContentFilterRule } from '@/types/api'
type ConfigFieldType = 'text' | 'number' | 'boolean' | 'select'
@@ -23,12 +23,12 @@ interface ConfigSection {
title: string
description: string
fields: ConfigField[]
kind?: 'fields' | 'content-filter'
}
const sections: ConfigSection[] = [
{ title: '基础', description: '数据目录、任务队列和索引提交', fields: [
{ path: 'data_dir', label: '数据目录', description: 'RocksDB 和 Tantivy 的根目录', type: 'text' },
{ path: 'content_filter_file', label: '内容过滤规则', description: '无效文件隐藏规则 TOML', type: 'text' },
{ path: 'persistence_queue_capacity', label: '持久化队列容量', description: 'Metadata 到 RocksDB 的有界队列', type: 'number' },
{ path: 'stats_interval_secs', label: '状态日志间隔', description: '运行状态日志输出秒数', type: 'number' },
{ path: 'run_duration_secs', label: '运行时长', description: '留空表示持续运行', type: 'number', nullable: true },
@@ -63,6 +63,7 @@ const sections: ConfigSection[] = [
{ path: 'metadata_limits.max_path_bytes', label: '最大路径长度', description: '文件路径最大字节数', type: 'number' },
{ path: 'metadata_limits.max_path_depth', label: '最大目录层级', description: '文件路径允许的目录深度', type: 'number' },
] },
{ title: '内容过滤', description: '从展示、统计、搜索和内容聚合中隐藏无效文件', kind: 'content-filter', fields: [] },
{ title: '磁盘与备份', description: '空间保护和 RocksDB 在线检查点', fields: [
{ path: 'disk_guard.enabled', label: '磁盘保护', description: '空间不足时自动进入只读状态', type: 'boolean' },
{ path: 'disk_guard.check_interval_secs', label: '磁盘检查间隔', description: '剩余空间检查秒数', type: 'number' },
@@ -112,6 +113,20 @@ const error = ref('')
const saved = ref(false)
const dirty = computed(() => config.value !== null && JSON.stringify(config.value) !== original.value)
const filterFieldOptions = [
{ value: 'file-name', label: '文件名' },
{ value: 'file-path', label: '文件路径' },
]
const filterMatchOptions = [
{ value: 'exact', label: '精确匹配' },
{ value: 'prefix', label: '前缀' },
{ value: 'suffix', label: '后缀' },
{ value: 'contains', label: '包含' },
{ value: 'wildcard', label: '通配符' },
{ value: 'regex', label: '正则表达式' },
]
function fieldValue(path: string): unknown {
if (!config.value) return ''
return path.split('.').reduce<unknown>((value, key) => (value as Record<string, unknown>)[key], config.value)
@@ -133,6 +148,35 @@ function updateField(field: ConfigField, event: Event) {
saved.value = false
}
function markChanged() {
saved.value = false
}
function addFilterRule() {
if (!config.value) return
const used = new Set(config.value.content_filter.file_rules.map((rule) => rule.id))
let index = config.value.content_filter.file_rules.length + 1
while (used.has(`rule-${index}`)) index += 1
const rule: ContentFilterRule = {
id: `rule-${index}`,
enabled: true,
field: 'file-name',
match: 'contains',
value: '',
case_sensitive: false,
action: 'hide',
reason: '',
}
config.value.content_filter.file_rules.push(rule)
markChanged()
}
function removeFilterRule(index: number) {
if (!config.value) return
config.value.content_filter.file_rules.splice(index, 1)
markChanged()
}
async function load() {
loading.value = true
error.value = ''
@@ -202,8 +246,11 @@ onBeforeRouteLeave(() => !dirty.value || window.confirm('配置尚未保存,
<TabsContent v-for="section in sections" :key="section.title" :value="section.title" class="mt-4 outline-none">
<section class="rounded-xl border bg-card shadow-sm">
<div class="border-b px-5 py-4"><h2 class="font-semibold">{{ section.title }}</h2><p class="mt-1 text-sm text-muted-foreground">{{ section.description }}</p></div>
<div class="grid gap-x-6 px-5 sm:grid-cols-2">
<div class="flex items-center gap-3 border-b px-5 py-4">
<div><h2 class="font-semibold">{{ section.title }}</h2><p class="mt-1 text-sm text-muted-foreground">{{ section.description }}</p></div>
<Button v-if="section.kind === 'content-filter'" class="ml-auto" size="sm" type="button" variant="outline" @click="addFilterRule"><Plus />添加规则</Button>
</div>
<div v-if="section.kind !== 'content-filter'" class="grid gap-x-6 px-5 sm:grid-cols-2">
<label v-for="field in section.fields" :key="field.path" class="config-field" :class="field.type === 'boolean' ? 'flex-row items-center' : 'flex-col'">
<span class="min-w-0 flex-1"><b>{{ field.label }}</b><small>{{ field.description }}</small></span>
<input v-if="field.type === 'boolean'" class="size-4 accent-foreground" type="checkbox" :checked="Boolean(fieldValue(field.path))" @change="updateField(field, $event)" />
@@ -211,6 +258,24 @@ onBeforeRouteLeave(() => !dirty.value || window.confirm('配置尚未保存,
<input v-else class="config-input" :type="field.type" :value="fieldValue(field.path) ?? ''" :min="field.type === 'number' ? 0 : undefined" step="1" @input="updateField(field, $event)" />
</label>
</div>
<div v-else class="space-y-3 p-5">
<p v-if="config.content_filter.file_rules.length === 0" class="rounded-lg border border-dashed p-8 text-center text-sm text-muted-foreground">当前没有内容过滤规则</p>
<article v-for="(rule, index) in config.content_filter.file_rules" :key="index" class="rounded-xl border bg-background p-4">
<div class="mb-4 flex items-center gap-3">
<label class="flex items-center gap-2 text-sm font-medium"><input v-model="rule.enabled" class="size-4 accent-foreground" type="checkbox" @change="markChanged" />启用</label>
<span class="text-xs text-muted-foreground">规则 {{ index + 1 }}</span>
<Button class="ml-auto" size="icon-sm" type="button" variant="ghost" aria-label="删除过滤规则" title="删除规则" @click="removeFilterRule(index)"><Trash2 /></Button>
</div>
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<label class="field-label">规则 ID<input v-model="rule.id" class="config-input mt-0" type="text" @input="markChanged" /></label>
<label class="field-label">匹配字段<select v-model="rule.field" class="config-input mt-0" @change="markChanged"><option v-for="option in filterFieldOptions" :key="option.value" :value="option.value">{{ option.label }}</option></select></label>
<label class="field-label">匹配方式<select v-model="rule.match" class="config-input mt-0" @change="markChanged"><option v-for="option in filterMatchOptions" :key="option.value" :value="option.value">{{ option.label }}</option></select></label>
<label class="field-label sm:col-span-2">匹配值<input v-model="rule.value" class="config-input mt-0 font-mono" type="text" @input="markChanged" /></label>
<label class="field-label">区分大小写<span class="flex h-9 items-center"><input v-model="rule.case_sensitive" class="size-4 accent-foreground" type="checkbox" @change="markChanged" /></span></label>
<label class="field-label sm:col-span-2 lg:col-span-3">规则说明<input v-model="rule.reason" class="config-input mt-0" type="text" @input="markChanged" /></label>
</div>
</article>
</div>
</section>
</TabsContent>
</TabsRoot>
+19 -1
View File
@@ -209,10 +209,28 @@ export interface DiagnosticHistory {
export type NetworkMode = 'ipv4-only' | 'ipv6-only' | 'dual-stack'
export type LogRotation = 'minutely' | 'hourly' | 'daily' | 'never'
export type ContentFilterField = 'file-name' | 'file-path'
export type ContentFilterMatch = 'exact' | 'prefix' | 'suffix' | 'contains' | 'wildcard' | 'regex'
export interface ContentFilterRule {
id: string
enabled: boolean
field: ContentFilterField
match: ContentFilterMatch
value: string
case_sensitive: boolean
action: 'hide'
reason: string
}
export interface ContentFilterConfig {
version: number
file_rules: ContentFilterRule[]
}
export interface AppConfigDto {
data_dir: string
content_filter_file: string
content_filter: ContentFilterConfig
persistence_queue_capacity: number
stats_interval_secs: number
run_duration_secs: number | null