Browse Source

feat: add adminOnly field to agent tools and update related API endpoints

- Added adminOnly field to CreateAgentToolInput and UpdateAgentToolInput interfaces.
- Updated API endpoints to handle adminOnly field in agent tools.
- Modified getEnabledToolInfos and getEnabledToolsForLlm functions to filter tools based on user role.
- Enhanced the chat tools API to retrieve enabled tools considering user roles.
- Updated migration journal to include new patch for adminOnly feature.
feat/ai-sdk-v6-upgrade
npmrun 2 days ago
parent
commit
2a112f9253
  1. 8
      app/components/AgentToolFormModal.vue
  2. 2
      app/components/TopNav.vue
  3. 35
      app/pages/admin/agent-tools/index.vue
  4. 0
      app/pages/ideas/index copy.vue
  5. 0
      app/pages/ideas/index.vue
  6. 41
      app/pages/index.vue
  7. BIN
      packages/drizzle-pkg/db.sqlite
  8. 1
      packages/drizzle-pkg/lib/schema/agent-tool.ts
  9. 1
      packages/drizzle-pkg/migrations/0016_broken_patch.sql
  10. 1885
      packages/drizzle-pkg/migrations/meta/0016_snapshot.json
  11. 7
      packages/drizzle-pkg/migrations/meta/_journal.json
  12. 3
      server/api/agent-tools/[id].put.ts
  13. 3
      server/api/agent-tools/index.post.ts
  14. 2
      server/api/llm/chat/index.post.ts
  15. 6
      server/api/llm/chat/tools.get.ts
  16. 18
      server/service/agent-tool/index.ts

8
app/components/AgentToolFormModal.vue

@ -34,6 +34,7 @@ const form = reactive({
inputSchema: "", inputSchema: "",
enabled: props.tool ? props.tool.enabled === 1 : true, enabled: props.tool ? props.tool.enabled === 1 : true,
needsApproval: props.tool ? props.tool.needsApproval === 1 : false, needsApproval: props.tool ? props.tool.needsApproval === 1 : false,
adminOnly: props.tool ? props.tool.adminOnly === 1 : false,
sortOrder: props.tool?.sortOrder ?? 0, sortOrder: props.tool?.sortOrder ?? 0,
}) })
@ -92,6 +93,7 @@ async function save() {
config: parsedConfig, config: parsedConfig,
enabled: form.enabled ? 1 : 0, enabled: form.enabled ? 1 : 0,
needsApproval: form.needsApproval ? 1 : 0, needsApproval: form.needsApproval ? 1 : 0,
adminOnly: form.adminOnly ? 1 : 0,
sortOrder: Number(form.sortOrder) || 0, sortOrder: Number(form.sortOrder) || 0,
} }
@ -191,6 +193,12 @@ function onOverlayClick(e: MouseEvent) {
<span>需要审批</span> <span>需要审批</span>
</label> </label>
</div> </div>
<div class="form-group form-group-grow">
<label class="checkbox-label">
<input v-model="form.adminOnly" type="checkbox" class="form-checkbox" />
<span>仅管理员可用</span>
</label>
</div>
</div> </div>
</div> </div>

2
app/components/TopNav.vue

@ -9,7 +9,7 @@ const { loggedIn, user, clear, initialized } = useAuthSession()
const { config: globalConfig } = useGlobalConfig() const { config: globalConfig } = useGlobalConfig()
const links: any = [ const links: any = [
{ to: "/", label: "首页" }, { to: "/ideas", label: "创意" },
{ to: "/projects", label: "项目" }, { to: "/projects", label: "项目" },
{ to: "/articles", label: "文章" }, { to: "/articles", label: "文章" },
{ to: "/collect", label: "收藏" }, { to: "/collect", label: "收藏" },

35
app/pages/admin/agent-tools/index.vue

@ -8,6 +8,7 @@ interface AgentToolRow {
config: string config: string
enabled: number enabled: number
needsApproval: number needsApproval: number
adminOnly: number
sortOrder: number sortOrder: number
createdAt: string createdAt: string
updatedAt: string updatedAt: string
@ -82,6 +83,19 @@ async function handleToggleApproval(tool: AgentToolRow) {
} }
} }
async function handleToggleAdminOnly(tool: AgentToolRow) {
try {
await $fetch(`/api/agent-tools/${tool.id}`, {
method: "PUT",
body: { adminOnly: !tool.adminOnly },
})
$toast.success(tool.adminOnly ? "已设为所有人可用" : "已设为仅管理员可用")
refresh()
} catch (e: any) {
$toast.error(e?.data?.message ?? "操作失败")
}
}
function openEdit(tool: AgentToolRow) { function openEdit(tool: AgentToolRow) {
editingTool.value = tool editingTool.value = tool
showFormModal.value = true showFormModal.value = true
@ -134,13 +148,14 @@ function onExecuteClose() {
<th class="text-left">类型</th> <th class="text-left">类型</th>
<th class="text-left">状态</th> <th class="text-left">状态</th>
<th class="text-left">审批</th> <th class="text-left">审批</th>
<th class="text-left">权限</th>
<th class="text-left">排序</th> <th class="text-left">排序</th>
<th class="text-right">操作</th> <th class="text-right">操作</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-if="toolList.length === 0"> <tr v-if="toolList.length === 0">
<td colspan="7" class="text-center empty-cell">暂无工具</td> <td colspan="8" class="text-center empty-cell">暂无工具</td>
</tr> </tr>
<tr v-for="t in toolList" :key="t.id" class="table-row"> <tr v-for="t in toolList" :key="t.id" class="table-row">
<td class="name-cell"> <td class="name-cell">
@ -161,6 +176,11 @@ function onExecuteClose() {
{{ t.needsApproval ? '需审批' : '直接执行' }} {{ t.needsApproval ? '需审批' : '直接执行' }}
</span> </span>
</td> </td>
<td class="permission-cell">
<span :class="['status-badge', t.adminOnly ? 'badge-admin-only' : 'badge-all-users']">
{{ t.adminOnly ? '仅管理员' : '所有人' }}
</span>
</td>
<td class="sort-cell">{{ t.sortOrder }}</td> <td class="sort-cell">{{ t.sortOrder }}</td>
<td class="actions-cell"> <td class="actions-cell">
<div class="action-buttons"> <div class="action-buttons">
@ -171,6 +191,9 @@ function onExecuteClose() {
<button class="action-btn" @click="handleToggleApproval(t)"> <button class="action-btn" @click="handleToggleApproval(t)">
{{ t.needsApproval ? '关闭审批' : '开启审批' }} {{ t.needsApproval ? '关闭审批' : '开启审批' }}
</button> </button>
<button class="action-btn" @click="handleToggleAdminOnly(t)">
{{ t.adminOnly ? '设为所有人' : '设为仅管理员' }}
</button>
<button class="action-btn" @click="openEdit(t)">编辑</button> <button class="action-btn" @click="openEdit(t)">编辑</button>
<button class="action-btn action-btn-danger" @click="handleDelete(t.id)">删除</button> <button class="action-btn action-btn-danger" @click="handleDelete(t.id)">删除</button>
</div> </div>
@ -390,6 +413,16 @@ function onExecuteClose() {
color: #666; color: #666;
} }
.badge-admin-only {
background: #fef0f0;
color: #e53e3e;
}
.badge-all-users {
background: #f0f0f0;
color: #666;
}
.sort-cell { .sort-cell {
color: var(--color-muted); color: var(--color-muted);
font-size: 13px; font-size: 13px;

0
app/pages/index/index copy.vue → app/pages/ideas/index copy.vue

0
app/pages/index/index.vue → app/pages/ideas/index.vue

41
app/pages/index.vue

@ -0,0 +1,41 @@
<script setup lang="ts">
definePageMeta({
layout: 'home',
})
</script>
<template>
<div class="home-empty">
<BoContainer>
<div class="empty-content">
<h1 class="welcome-title">Dash</h1>
<p class="welcome-subtitle">欢迎来到 Dash</p>
</div>
</BoContainer>
</div>
</template>
<style scoped>
.home-empty {
min-height: 60vh;
display: flex;
align-items: center;
justify-content: center;
}
.empty-content {
text-align: center;
}
.welcome-title {
font-size: 3rem;
font-weight: 700;
margin: 0;
}
.welcome-subtitle {
font-size: 1.125rem;
color: var(--text-secondary, #666);
margin-top: 0.5rem;
}
</style>

BIN
packages/drizzle-pkg/db.sqlite

Binary file not shown.

1
packages/drizzle-pkg/lib/schema/agent-tool.ts

@ -28,6 +28,7 @@ export const agentTools = sqliteTable(
config: text("config").notNull(), config: text("config").notNull(),
enabled: integer("enabled").default(1).notNull(), enabled: integer("enabled").default(1).notNull(),
needsApproval: integer("needs_approval").default(0).notNull(), needsApproval: integer("needs_approval").default(0).notNull(),
adminOnly: integer("admin_only").default(0).notNull(),
sortOrder: integer("sort_order").default(0).notNull(), sortOrder: integer("sort_order").default(0).notNull(),
createdAt: integer("created_at", { mode: "timestamp_ms" }) createdAt: integer("created_at", { mode: "timestamp_ms" })
.defaultNow() .defaultNow()

1
packages/drizzle-pkg/migrations/0016_broken_patch.sql

@ -0,0 +1 @@
ALTER TABLE `agent_tools` ADD `admin_only` integer DEFAULT 0 NOT NULL;

1885
packages/drizzle-pkg/migrations/meta/0016_snapshot.json

File diff suppressed because it is too large

7
packages/drizzle-pkg/migrations/meta/_journal.json

@ -113,6 +113,13 @@
"when": 1785942019319, "when": 1785942019319,
"tag": "0015_high_red_shift", "tag": "0015_high_red_shift",
"breakpoints": true "breakpoints": true
},
{
"idx": 16,
"version": "6",
"when": 1785978733580,
"tag": "0016_broken_patch",
"breakpoints": true
} }
] ]
} }

3
server/api/agent-tools/[id].put.ts

@ -10,7 +10,7 @@ export default defineWrappedResponseHandler(async (event) => {
} }
const body = await readBody(event); const body = await readBody(event);
const { name, slug, description, type, config, enabled, needsApproval, sortOrder } = body; const { name, slug, description, type, config, enabled, needsApproval, adminOnly, sortOrder } = body;
try { try {
const result = await updateAgentTool(id, { const result = await updateAgentTool(id, {
@ -21,6 +21,7 @@ export default defineWrappedResponseHandler(async (event) => {
config, config,
enabled, enabled,
needsApproval, needsApproval,
adminOnly,
sortOrder, sortOrder,
}); });
if (!result) { if (!result) {

3
server/api/agent-tools/index.post.ts

@ -5,7 +5,7 @@ export default defineWrappedResponseHandler(async (event) => {
await requireAdmin(event); await requireAdmin(event);
const body = await readBody(event); const body = await readBody(event);
const { name, slug, description, type, config, enabled, needsApproval, sortOrder } = body; const { name, slug, description, type, config, enabled, needsApproval, adminOnly, sortOrder } = body;
if (!name || !slug || !type) { if (!name || !slug || !type) {
return R.throwError(422, "name、slug、type 不能为空", null); return R.throwError(422, "name、slug、type 不能为空", null);
@ -20,6 +20,7 @@ export default defineWrappedResponseHandler(async (event) => {
config: config ?? {}, config: config ?? {},
enabled, enabled,
needsApproval, needsApproval,
adminOnly,
sortOrder, sortOrder,
}); });
return R.success(result); return R.success(result);

2
server/api/llm/chat/index.post.ts

@ -84,7 +84,7 @@ export default defineEventHandler(async (event) => {
const languageModel = resolveModel(provider, model.modelId); const languageModel = resolveModel(provider, model.modelId);
const { tools, approvalConfig } = enableTools ? await getEnabledToolsForLlm(user.id) : { tools: undefined, approvalConfig: {} }; const { tools, approvalConfig } = enableTools ? await getEnabledToolsForLlm(user.id, user.role) : { tools: undefined, approvalConfig: {} };
const modelMessages = await convertToModelMessages(messages); const modelMessages = await convertToModelMessages(messages);

6
server/api/llm/chat/tools.get.ts

@ -1,6 +1,8 @@
import { getCurrentUser } from "#server/utils/context";
import { getEnabledToolInfos } from "#server/service/agent-tool"; import { getEnabledToolInfos } from "#server/service/agent-tool";
export default defineWrappedResponseHandler(async () => { export default defineWrappedResponseHandler(async (event) => {
const result = await getEnabledToolInfos(); const user = await getCurrentUser(event);
const result = await getEnabledToolInfos(user?.role ?? null);
return R.success(result); return R.success(result);
}); });

18
server/service/agent-tool/index.ts

@ -1,5 +1,6 @@
import { dbGlobal } from "drizzle-pkg/lib/db"; import { dbGlobal } from "drizzle-pkg/lib/db";
import { agentTools } from "drizzle-pkg/lib/schema/agent-tool"; import { agentTools } from "drizzle-pkg/lib/schema/agent-tool";
import type { UserRole } from "drizzle-pkg/lib/schema/auth";
import { eq, asc } from "drizzle-orm"; import { eq, asc } from "drizzle-orm";
import { tool, jsonSchema } from "ai"; import { tool, jsonSchema } from "ai";
import { z } from "zod"; import { z } from "zod";
@ -98,6 +99,7 @@ export interface CreateAgentToolInput {
config: Record<string, unknown>; config: Record<string, unknown>;
enabled?: boolean; enabled?: boolean;
needsApproval?: boolean; needsApproval?: boolean;
adminOnly?: boolean;
sortOrder?: number; sortOrder?: number;
} }
@ -109,6 +111,7 @@ export interface UpdateAgentToolInput {
config?: Record<string, unknown>; config?: Record<string, unknown>;
enabled?: boolean; enabled?: boolean;
needsApproval?: boolean; needsApproval?: boolean;
adminOnly?: boolean;
sortOrder?: number; sortOrder?: number;
} }
@ -186,6 +189,7 @@ export async function createAgentTool(input: CreateAgentToolInput): Promise<Agen
config: JSON.stringify(validatedConfig), config: JSON.stringify(validatedConfig),
enabled: input.enabled === false ? 0 : 1, enabled: input.enabled === false ? 0 : 1,
needsApproval: input.needsApproval ? 1 : 0, needsApproval: input.needsApproval ? 1 : 0,
adminOnly: input.adminOnly ? 1 : 0,
sortOrder: input.sortOrder ?? 0, sortOrder: input.sortOrder ?? 0,
}); });
const row = await getAgentToolById(id); const row = await getAgentToolById(id);
@ -215,6 +219,7 @@ export async function updateAgentTool(id: string, input: UpdateAgentToolInput):
} }
if (input.enabled !== undefined) updates.enabled = input.enabled ? 1 : 0; if (input.enabled !== undefined) updates.enabled = input.enabled ? 1 : 0;
if (input.needsApproval !== undefined) updates.needsApproval = input.needsApproval ? 1 : 0; if (input.needsApproval !== undefined) updates.needsApproval = input.needsApproval ? 1 : 0;
if (input.adminOnly !== undefined) updates.adminOnly = input.adminOnly ? 1 : 0;
if (input.sortOrder !== undefined) updates.sortOrder = input.sortOrder; if (input.sortOrder !== undefined) updates.sortOrder = input.sortOrder;
if (Object.keys(updates).length === 0) return existing; if (Object.keys(updates).length === 0) return existing;
@ -296,15 +301,18 @@ export interface EnabledToolInfo {
description: string; description: string;
} }
export async function getEnabledToolInfos(): Promise<EnabledToolInfo[]> { export async function getEnabledToolInfos(userRole: UserRole | null = null): Promise<EnabledToolInfo[]> {
const tools = await dbGlobal const tools = await dbGlobal
.select() .select()
.from(agentTools) .from(agentTools)
.where(eq(agentTools.enabled, 1)) .where(eq(agentTools.enabled, 1))
.orderBy(asc(agentTools.sortOrder)); .orderBy(asc(agentTools.sortOrder));
const isAdmin = userRole === "admin";
const result: EnabledToolInfo[] = []; const result: EnabledToolInfo[] = [];
for (const agentTool of tools) { for (const agentTool of tools) {
if (!isAdmin && agentTool.adminOnly) continue;
const executor = getExecutor(agentTool.type); const executor = getExecutor(agentTool.type);
if (!executor) continue; if (!executor) continue;
let config: unknown; let config: unknown;
@ -323,7 +331,10 @@ export async function getEnabledToolInfos(): Promise<EnabledToolInfo[]> {
return result; return result;
} }
export async function getEnabledToolsForLlm(userId: number | null = null): Promise<{ export async function getEnabledToolsForLlm(
userId: number | null = null,
userRole: UserRole | null = null,
): Promise<{
tools: Record<string, ReturnType<typeof tool>>; tools: Record<string, ReturnType<typeof tool>>;
approvalConfig: Record<string, "user-approval" | "approved">; approvalConfig: Record<string, "user-approval" | "approved">;
}> { }> {
@ -333,9 +344,12 @@ export async function getEnabledToolsForLlm(userId: number | null = null): Promi
.where(eq(agentTools.enabled, 1)) .where(eq(agentTools.enabled, 1))
.orderBy(asc(agentTools.sortOrder)); .orderBy(asc(agentTools.sortOrder));
const isAdmin = userRole === "admin";
const result: Record<string, any> = {}; const result: Record<string, any> = {};
const approvalConfig: Record<string, "user-approval" | "approved"> = {}; const approvalConfig: Record<string, "user-approval" | "approved"> = {};
for (const agentTool of tools) { for (const agentTool of tools) {
if (!isAdmin && agentTool.adminOnly) continue;
const executor = getExecutor(agentTool.type); const executor = getExecutor(agentTool.type);
if (!executor) continue; if (!executor) continue;
let config: unknown; let config: unknown;

Loading…
Cancel
Save