Browse Source

feat: 增加审批设置功能,支持手动和自动审批模式

feat/agent-independence
npmrun 2 months ago
parent
commit
43dc8b6da2
  1. 129
      app/pages/settings/preferences.vue
  2. BIN
      packages/drizzle-pkg/db.sqlite
  3. 3
      server/api/agents/[agentSlug]/chat/index.post.ts
  4. 11
      server/service/agent/chat-engine.ts
  5. 8
      server/service/config/registry.ts

129
app/pages/settings/preferences.vue

@ -12,8 +12,10 @@ interface ModelOption {
const models = ref<ModelOption[]>([]);
const preferredModelId = ref<number | null>(null);
const reviewApprovalMode = ref<string>("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 () => {
</div>
</div>
</div>
<div class="settings-card">
<div class="card-header">
<h2 class="card-title">审批设置</h2>
<p class="card-desc">Agent 调用工具时的审批策略</p>
</div>
<div class="card-body">
<div class="approval-options">
<label
class="approval-option"
:class="{ active: reviewApprovalMode === 'manual' }"
>
<input
v-model="reviewApprovalMode"
type="radio"
value="manual"
class="approval-radio"
/>
<div class="approval-content">
<span class="approval-label">手动审批</span>
<span class="approval-desc">每次工具调用需要您手动确认后才会执行</span>
</div>
</label>
<label
class="approval-option"
:class="{ active: reviewApprovalMode === 'all-pass' }"
>
<input
v-model="reviewApprovalMode"
type="radio"
value="all-pass"
class="approval-radio"
/>
<div class="approval-content">
<span class="approval-label">全部通过</span>
<span class="approval-desc">所有工具调用自动批准,无需手动确认</span>
</div>
</label>
</div>
<div class="form-actions">
<button
type="button"
class="btn-primary"
:disabled="savingApproval"
@click="handleSaveApproval"
>
{{ savingApproval ? "保存中..." : "保存" }}
</button>
</div>
</div>
</div>
</div>
</template>
@ -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;
}
</style>

BIN
packages/drizzle-pkg/db.sqlite

Binary file not shown.

3
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,

11
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<ChatEngineR
logger.info("[%s] [APPROVAL-PREFIX] written %d bytes to buffer before stream start", requestId, approvalPrefixChunk.length);
}
const toolApproval = params.reviewApprovalMode === "all-pass"
? () => "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
? {

8
server/service/config/registry.ts

@ -134,6 +134,14 @@ const CONFIG_REGISTRY = {
userOverridable: true,
validate: (value: number) => Number.isInteger(value) && value >= 0,
}),
reviewApprovalMode: defineConfig<string>({
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;

Loading…
Cancel
Save