You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
340 lines
7.7 KiB
340 lines
7.7 KiB
<script setup lang="ts">
|
|
import { useAuthSession } from "~/composables/useAuthSession";
|
|
|
|
definePageMeta({
|
|
layout: "default",
|
|
});
|
|
|
|
const auth = useAuthSession();
|
|
const { $toast } = useNuxtApp() as any;
|
|
|
|
const systemPrompt = ref("");
|
|
const defaultModelId = ref<number | null>(null);
|
|
const publicToolSlugs = ref<string[]>([]);
|
|
const loading = ref(false);
|
|
const saving = ref(false);
|
|
|
|
interface ModelOption {
|
|
id: number;
|
|
name: string;
|
|
modelId: string;
|
|
}
|
|
|
|
const models = ref<ModelOption[]>([]);
|
|
|
|
const availableTools = ref<{ slug: string; name: string; description: string }[]>([]);
|
|
|
|
async function loadConfig() {
|
|
loading.value = true;
|
|
try {
|
|
const res = await $fetch<{ code: number; data: any }>("/api/admin/agent-config");
|
|
const d = res.data;
|
|
systemPrompt.value = d.systemPrompt ?? "";
|
|
defaultModelId.value = d.defaultModelId ?? null;
|
|
publicToolSlugs.value = d.publicToolSlugs ?? [];
|
|
} catch {
|
|
$toast?.error?.("加载配置失败");
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
async function loadModels() {
|
|
try {
|
|
const res = await $fetch<{ code: number; data: { list: any[] } }>(
|
|
"/api/admin/llm/models",
|
|
);
|
|
const list = res.data?.list ?? [];
|
|
models.value = list.map((m) => ({ id: m.id, name: m.name, modelId: m.modelId }));
|
|
} catch {
|
|
models.value = [];
|
|
}
|
|
}
|
|
|
|
async function loadTools() {
|
|
try {
|
|
const res = await $fetch<{ code: number; data: { list: any[] } }>(
|
|
"/api/agent-tools",
|
|
);
|
|
availableTools.value = (res.data?.list ?? []).map((t) => ({
|
|
slug: t.slug,
|
|
name: t.name,
|
|
description: t.description ?? "",
|
|
}));
|
|
} catch {
|
|
availableTools.value = [];
|
|
}
|
|
}
|
|
|
|
async function saveSystemPrompt() {
|
|
saving.value = true;
|
|
try {
|
|
await $fetch("/api/admin/agent-config/system-prompt", {
|
|
method: "PUT",
|
|
body: { systemPrompt: systemPrompt.value },
|
|
});
|
|
$toast?.success?.("系统提示词已保存");
|
|
} catch {
|
|
$toast?.error?.("保存失败");
|
|
} finally {
|
|
saving.value = false;
|
|
}
|
|
}
|
|
|
|
async function saveDefaultModel() {
|
|
saving.value = true;
|
|
try {
|
|
await $fetch("/api/admin/agent-config/default-model", {
|
|
method: "PUT",
|
|
body: { modelId: defaultModelId.value },
|
|
});
|
|
$toast?.success?.("默认模型已保存");
|
|
} catch {
|
|
$toast?.error?.("保存失败");
|
|
} finally {
|
|
saving.value = false;
|
|
}
|
|
}
|
|
|
|
async function savePublicTools() {
|
|
saving.value = true;
|
|
try {
|
|
await $fetch("/api/admin/agent-config/public-tools", {
|
|
method: "PUT",
|
|
body: { publicToolSlugs: publicToolSlugs.value },
|
|
});
|
|
$toast?.success?.("公开工具白名单已保存");
|
|
} catch {
|
|
$toast?.error?.("保存失败");
|
|
} finally {
|
|
saving.value = false;
|
|
}
|
|
}
|
|
|
|
function toggleTool(slug: string) {
|
|
const idx = publicToolSlugs.value.indexOf(slug);
|
|
if (idx >= 0) {
|
|
publicToolSlugs.value.splice(idx, 1);
|
|
} else {
|
|
publicToolSlugs.value.push(slug);
|
|
}
|
|
}
|
|
|
|
onMounted(async () => {
|
|
await Promise.all([loadConfig(), loadModels(), loadTools()]);
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="admin-agent-config">
|
|
<h1 class="page-title">Agent 配置</h1>
|
|
|
|
<div v-if="loading" class="loading-state">加载中...</div>
|
|
|
|
<template v-else>
|
|
<!-- System Prompt -->
|
|
<section class="config-section">
|
|
<h2 class="section-title">系统提示词</h2>
|
|
<p class="section-desc">设置 agent 对话的全局系统提示词</p>
|
|
<textarea
|
|
v-model="systemPrompt"
|
|
class="config-textarea"
|
|
rows="8"
|
|
placeholder="输入系统提示词..."
|
|
/>
|
|
<button class="save-btn" :disabled="saving" @click="saveSystemPrompt">
|
|
保存
|
|
</button>
|
|
</section>
|
|
|
|
<!-- Default Model -->
|
|
<section class="config-section">
|
|
<h2 class="section-title">默认模型</h2>
|
|
<p class="section-desc">新对话的默认模型选择</p>
|
|
<select v-model="defaultModelId" class="config-select">
|
|
<option :value="null">— 未设置 —</option>
|
|
<option v-for="m in models" :key="m.id" :value="m.id">
|
|
{{ m.name }} ({{ m.modelId }})
|
|
</option>
|
|
</select>
|
|
<button class="save-btn" :disabled="saving" @click="saveDefaultModel">
|
|
保存
|
|
</button>
|
|
</section>
|
|
|
|
<!-- Public Tools -->
|
|
<section class="config-section">
|
|
<h2 class="section-title">公开工具白名单</h2>
|
|
<p class="section-desc">未登录用户可自动执行的工具(无需审批)</p>
|
|
<div class="tools-list">
|
|
<label
|
|
v-for="t in availableTools"
|
|
:key="t.slug"
|
|
class="tool-item"
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
:checked="publicToolSlugs.includes(t.slug)"
|
|
@change="toggleTool(t.slug)"
|
|
/>
|
|
<div class="tool-info">
|
|
<div class="tool-name">{{ t.name }}</div>
|
|
<div class="tool-desc">{{ t.description }}</div>
|
|
</div>
|
|
</label>
|
|
<div v-if="availableTools.length === 0" class="empty-tools">
|
|
暂无可用工具
|
|
</div>
|
|
</div>
|
|
<button class="save-btn" :disabled="saving" @click="savePublicTools">
|
|
保存
|
|
</button>
|
|
</section>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.admin-agent-config {
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
padding: 32px 24px;
|
|
}
|
|
|
|
.page-title {
|
|
font-family: var(--font-display);
|
|
font-size: 28px;
|
|
font-weight: 400;
|
|
color: var(--color-ink);
|
|
margin: 0 0 32px;
|
|
}
|
|
|
|
.loading-state {
|
|
text-align: center;
|
|
padding: 48px;
|
|
color: var(--color-muted);
|
|
}
|
|
|
|
.config-section {
|
|
margin-bottom: 40px;
|
|
padding: 24px;
|
|
background: var(--color-surface-card);
|
|
border: 1px solid var(--color-hairline);
|
|
border-radius: 16px;
|
|
}
|
|
|
|
.section-title {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
color: var(--color-ink);
|
|
margin: 0 0 4px;
|
|
}
|
|
|
|
.section-desc {
|
|
font-size: 13px;
|
|
color: var(--color-muted);
|
|
margin: 0 0 16px;
|
|
}
|
|
|
|
.config-textarea {
|
|
width: 100%;
|
|
padding: 12px 14px;
|
|
border: 1px solid var(--color-hairline);
|
|
border-radius: 10px;
|
|
font-family: "SF Mono", "Fira Code", monospace;
|
|
font-size: 13px;
|
|
line-height: 1.6;
|
|
color: var(--color-ink);
|
|
background: var(--color-canvas);
|
|
resize: vertical;
|
|
outline: none;
|
|
transition: border-color 0.15s;
|
|
}
|
|
|
|
.config-textarea:focus {
|
|
border-color: var(--color-primary);
|
|
}
|
|
|
|
.config-select {
|
|
padding: 8px 12px;
|
|
border: 1px solid var(--color-hairline);
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
color: var(--color-ink);
|
|
background: var(--color-canvas);
|
|
outline: none;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.save-btn {
|
|
margin-top: 16px;
|
|
padding: 8px 20px;
|
|
background: var(--color-primary);
|
|
color: var(--color-on-primary);
|
|
border: none;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: background 0.15s;
|
|
}
|
|
|
|
.save-btn:hover:not(:disabled) {
|
|
background: var(--color-primary-active);
|
|
}
|
|
|
|
.save-btn:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.tools-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.tool-item {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 10px;
|
|
padding: 12px;
|
|
background: var(--color-surface-soft);
|
|
border: 1px solid var(--color-hairline);
|
|
border-radius: 10px;
|
|
cursor: pointer;
|
|
transition: border-color 0.15s;
|
|
}
|
|
|
|
.tool-item:hover {
|
|
border-color: var(--color-primary);
|
|
}
|
|
|
|
.tool-item input[type="checkbox"] {
|
|
margin-top: 2px;
|
|
accent-color: var(--color-primary);
|
|
}
|
|
|
|
.tool-info {
|
|
flex: 1;
|
|
}
|
|
|
|
.tool-name {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: var(--color-ink);
|
|
}
|
|
|
|
.tool-desc {
|
|
font-size: 12px;
|
|
color: var(--color-muted);
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.empty-tools {
|
|
padding: 24px;
|
|
text-align: center;
|
|
font-size: 13px;
|
|
color: var(--color-muted);
|
|
}
|
|
</style>
|
|
|