| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115 |
- <script setup lang="ts">
- import { onMounted, ref, watch } from 'vue'
- import type { KnowledgeRootDto, PathCheck } from '@agent-server/shared'
- import PageHeader from '../components/PageHeader.vue'
- import { admin, errorMessage, type KnowledgeInput } from '../lib/api'
- import { toast } from '../lib/ui'
- const roots = ref<KnowledgeRootDto[]>([])
- const editing = ref<(KnowledgeInput & { id?: string }) | null>(null)
- const check = ref<PathCheck | null>(null)
- const saving = ref(false)
- async function load() {
- roots.value = await admin.knowledge().catch(err => (toast(errorMessage(err), 'error'), []))
- }
- function edit(k?: KnowledgeRootDto) {
- editing.value = k
- ? { id: k.id, name: k.name, path: k.path, description: k.description, access: k.access, enabled: k.enabled }
- : { name: '', path: '/srv/agent-server/knowledge/', description: '', access: 'read', enabled: true }
- }
- let timer: ReturnType<typeof setTimeout> | undefined
- watch(() => editing.value?.path, p => {
- clearTimeout(timer)
- check.value = null
- if (!p?.trim()) return
- timer = setTimeout(async () => (check.value = await admin.checkPath(p).catch(() => null)), 300)
- })
- const valid = (c: PathCheck | null) => !!c && c.exists && c.readable && c.isDirectory && !c.denied
- async function save() {
- const e = editing.value
- if (!e) return
- saving.value = true
- try {
- const { id, ...body } = e
- if (id) await admin.updateKnowledge(id, body)
- else await admin.createKnowledge(body)
- editing.value = null
- toast('已保存', 'success')
- await load()
- } catch (err) {
- toast(errorMessage(err), 'error')
- } finally {
- saving.value = false
- }
- }
- async function remove(k: KnowledgeRootDto) {
- if (!confirm(`删除资料库「${k.name}」?(不会删除目录中的文件)`)) return
- await admin.deleteKnowledge(k.id).catch(err => toast(errorMessage(err), 'error'))
- await load()
- }
- onMounted(load)
- </script>
- <template>
- <div class="h-full flex flex-col">
- <PageHeader title="资料库" subtitle="登记服务器上的资料目录,在 Profile 中启用后 Agent 才能访问">
- <button type="button" class="btn-primary" @click="edit()"><span class="codicon codicon-add" />添加资料库</button>
- </PageHeader>
- <div class="flex-1 overflow-y-auto p-6">
- <div v-if="!roots.length" class="hint">还没有资料库。建议把资料放在 home 目录以外(例如 /srv/agent-server/knowledge),以 systemd 加固方式部署时无需额外配置。</div>
- <div class="flex flex-col gap-2 max-w-240">
- <div v-for="k in roots" :key="k.id" class="card p-3 flex items-start gap-3" :class="{ 'opacity-60': !k.enabled }">
- <span class="codicon codicon-library text-[20px] mt-0.5" :class="valid(k.check) ? 'text-accent' : 'text-error'" />
- <div class="flex-1 min-w-0">
- <div class="flex items-center gap-2">
- <span class="font-600">{{ k.name }}</span>
- <span class="badge bg-code">{{ k.access === 'write' ? '读写' : '只读' }}</span>
- <span v-if="!k.enabled" class="badge bg-code">已停用</span>
- </div>
- <div class="font-mono text-[12px] text-fg-muted break-all">{{ k.path }}</div>
- <div v-if="k.description" class="mt-1">{{ k.description }}</div>
- <div v-if="k.check.message" class="mt-1 text-[12px]" :class="valid(k.check) ? 'text-warning' : 'text-error'">{{ k.check.message }}</div>
- </div>
- <button type="button" class="icon-btn" title="编辑" @click="edit(k)"><span class="codicon codicon-edit" /></button>
- <button type="button" class="icon-btn" title="删除" @click="remove(k)"><span class="codicon codicon-trash" /></button>
- </div>
- </div>
- </div>
- <div v-if="editing" class="fixed inset-0 z-40 flex items-center justify-center bg-[var(--app-overlay)]" @click.self="editing = null">
- <form class="w-[520px] card bg-input-alt p-5 flex flex-col gap-3 shadow-xl" @submit.prevent="save">
- <div class="font-600">{{ editing.id ? '编辑资料库' : '添加资料库' }}</div>
- <div><label class="label" for="k-name">名称</label><input id="k-name" v-model="editing.name" class="field" placeholder="例如:家庭账单"></div>
- <div>
- <label class="label" for="k-path">目录路径(服务器上的绝对路径,支持 ~)</label>
- <input id="k-path" v-model="editing.path" class="field font-mono">
- <div v-if="check" class="mt-1 text-[12px] flex items-center gap-1" :class="valid(check) ? (check.message ? 'text-warning' : 'text-success') : 'text-error'">
- <span class="codicon" :class="valid(check) ? 'codicon-pass' : 'codicon-error'" />
- {{ check.message ?? '路径可用' }}
- </div>
- </div>
- <div>
- <label class="label" for="k-desc">说明(会告诉 Agent 这里有什么资料)</label>
- <textarea id="k-desc" v-model="editing.description" class="field-area h-20 font-sans" placeholder="例如:2020 年以来的银行流水 CSV 和发票 PDF,按年份分目录" />
- </div>
- <div class="flex gap-4">
- <label class="flex items-center gap-1.5 cursor-pointer"><input v-model="editing.access" type="radio" value="read">只读</label>
- <label class="flex items-center gap-1.5 cursor-pointer"><input v-model="editing.access" type="radio" value="write">可读写</label>
- <span class="flex-1" />
- <label class="flex items-center gap-1.5 cursor-pointer"><input v-model="editing.enabled" type="checkbox">启用</label>
- </div>
- <div class="flex justify-end gap-2 mt-2">
- <button type="button" class="btn-secondary" @click="editing = null">取消</button>
- <button type="submit" class="btn-primary" :disabled="saving || !editing.name.trim() || !valid(check)">保存</button>
- </div>
- </form>
- </div>
- </div>
- </template>
|