diff --git a/app/pages/settings/preferences.vue b/app/pages/settings/preferences.vue index 4356882..1642998 100644 --- a/app/pages/settings/preferences.vue +++ b/app/pages/settings/preferences.vue @@ -12,8 +12,10 @@ interface ModelOption { const models = ref([]); const preferredModelId = ref(null); +const reviewApprovalMode = ref("manual"); const loading = ref(false); const saving = ref(false); +const savingApproval = ref(false); async function loadModels() { try { @@ -37,6 +39,8 @@ async function loadUserConfig() { ); const val = res.data?.config?.preferredLlmModelId; preferredModelId.value = typeof val === "number" && val > 0 ? val : null; + const approvalVal = res.data?.config?.reviewApprovalMode; + reviewApprovalMode.value = typeof approvalVal === "string" && ["manual", "all-pass"].includes(approvalVal) ? approvalVal : "manual"; } catch { preferredModelId.value = null; } @@ -64,6 +68,23 @@ async function handleSave() { } } +async function handleSaveApproval() { + savingApproval.value = true; + try { + await $fetch("/api/config/me", { + method: "PUT", + body: { key: "reviewApprovalMode", value: reviewApprovalMode.value }, + }); + const { $toast } = useNuxtApp(); + $toast?.success?.("审批设置已保存"); + } catch { + const { $toast } = useNuxtApp(); + $toast?.error?.("保存失败,请稍后重试"); + } finally { + savingApproval.value = false; + } +} + onMounted(async () => { loading.value = true; await Promise.all([loadModels(), loadUserConfig()]); @@ -116,6 +137,60 @@ onMounted(async () => { + +
+
+

审批设置

+

Agent 调用工具时的审批策略

+
+ +
+
+ + + +
+ +
+ +
+
+
@@ -247,4 +322,58 @@ onMounted(async () => { padding: 24px; } } + +.approval-options { + display: flex; + flex-direction: column; + gap: 12px; +} + +.approval-option { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 16px; + border: 1px solid var(--color-hairline); + border-radius: 8px; + cursor: pointer; + transition: all 0.15s ease; +} + +.approval-option:hover { + border-color: var(--color-primary); +} + +.approval-option.active { + border-color: var(--color-primary); + background: rgba(204, 120, 92, 0.05); +} + +.approval-radio { + margin-top: 2px; + accent-color: var(--color-primary); + cursor: pointer; +} + +.approval-content { + display: flex; + flex-direction: column; + gap: 4px; +} + +.approval-label { + font-size: 14px; + font-weight: 500; + color: var(--color-ink); +} + +.approval-desc { + font-size: 13px; + color: var(--color-muted); + line-height: 1.5; +} + +.settings-card + .settings-card { + margin-top: 24px; +} diff --git a/packages/drizzle-pkg/db.sqlite b/packages/drizzle-pkg/db.sqlite index fa66cd8..1cc6767 100644 Binary files a/packages/drizzle-pkg/db.sqlite and b/packages/drizzle-pkg/db.sqlite differ diff --git a/server/api/agents/[agentSlug]/chat/index.post.ts b/server/api/agents/[agentSlug]/chat/index.post.ts index 1613d1d..b04d248 100644 --- a/server/api/agents/[agentSlug]/chat/index.post.ts +++ b/server/api/agents/[agentSlug]/chat/index.post.ts @@ -57,8 +57,10 @@ export default defineEventHandler(async (event) => { const ip = getRequestIP(event, { xForwardedFor: true }) || "unknown"; let preferredModelId: number | null = null; + let reviewApprovalMode: string = "manual"; if (identity.userId) { preferredModelId = (await getConfigUser(event, "preferredLlmModelId")) ?? null; + reviewApprovalMode = (await getConfigUser(event, "reviewApprovalMode")) ?? "manual"; } const globalDefaultModelId = (await getConfigGlobal("agentDefaultModelId")) ?? null; @@ -84,6 +86,7 @@ export default defineEventHandler(async (event) => { ip, preferredModelId, globalDefaultModelId, + reviewApprovalMode, requestId: event.context.requestId, registerAbort: registerAbortController, unregisterAbort: unregisterAbortController, diff --git a/server/service/agent/chat-engine.ts b/server/service/agent/chat-engine.ts index 56d6e39..7315d06 100644 --- a/server/service/agent/chat-engine.ts +++ b/server/service/agent/chat-engine.ts @@ -46,6 +46,7 @@ export interface ChatEngineParams { ip: string; preferredModelId?: number | null; globalDefaultModelId?: number | null; + reviewApprovalMode?: string; requestId?: string; registerAbort: (sessionId: string, controller: AbortController) => void; unregisterAbort: (sessionId: string) => void; @@ -434,13 +435,21 @@ export async function executeChat(params: ChatEngineParams): Promise "approved" as const + : undefined; + const result = streamText({ model: languageModel, system: systemPrompt, messages: modelMessages, abortSignal: serverAbortController.signal, ...(Object.keys(tools).length > 0 - ? { tools, stopWhen: stepCountIs(maxSteps) } + ? { + tools, + stopWhen: stepCountIs(maxSteps), + ...(toolApproval ? { toolApproval } : {}), + } : {}), ...(thinkingEnabled ? { diff --git a/server/service/config/registry.ts b/server/service/config/registry.ts index cd8078f..eb17d8b 100644 --- a/server/service/config/registry.ts +++ b/server/service/config/registry.ts @@ -134,6 +134,14 @@ const CONFIG_REGISTRY = { userOverridable: true, validate: (value: number) => Number.isInteger(value) && value >= 0, }), + reviewApprovalMode: defineConfig({ + key: "reviewApprovalMode", + scope: "user", + valueType: "string", + defaultValue: "manual", + userOverridable: true, + validate: (value: string) => ["manual", "all-pass"].includes(value), + }), } as const; export type KnownConfigKey = keyof typeof CONFIG_REGISTRY;