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: "",
enabled: props.tool ? props.tool.enabled === 1 : true,
needsApproval: props.tool ? props.tool.needsApproval === 1 : false,
adminOnly: props.tool ? props.tool.adminOnly === 1 : false,
sortOrder: props.tool?.sortOrder ?? 0,
})
@ -92,6 +93,7 @@ async function save() {
config: parsedConfig,
enabled: form.enabled ? 1 : 0,
needsApproval: form.needsApproval ? 1 : 0,
adminOnly: form.adminOnly ? 1 : 0,
sortOrder: Number(form.sortOrder) || 0,
}
@ -191,6 +193,12 @@ function onOverlayClick(e: MouseEvent) {
<span>需要审批</span>
</label>
</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>

2
app/components/TopNav.vue

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

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

@ -8,6 +8,7 @@ interface AgentToolRow {
config: string
enabled: number
needsApproval: number
adminOnly: number
sortOrder: number
createdAt: 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) {
editingTool.value = tool
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-right">操作</th>
</tr>
</thead>
<tbody>
<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 v-for="t in toolList" :key="t.id" class="table-row">
<td class="name-cell">
@ -161,6 +176,11 @@ function onExecuteClose() {
{{ t.needsApproval ? '需审批' : '直接执行' }}
</span>
</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="actions-cell">
<div class="action-buttons">
@ -171,6 +191,9 @@ function onExecuteClose() {
<button class="action-btn" @click="handleToggleApproval(t)">
{{ t.needsApproval ? '关闭审批' : '开启审批' }}
</button>
<button class="action-btn" @click="handleToggleAdminOnly(t)">
{{ t.adminOnly ? '设为所有人' : '设为仅管理员' }}
</button>
<button class="action-btn" @click="openEdit(t)">编辑</button>
<button class="action-btn action-btn-danger" @click="handleDelete(t.id)">删除</button>
</div>
@ -390,6 +413,16 @@ function onExecuteClose() {
color: #666;
}
.badge-admin-only {
background: #fef0f0;
color: #e53e3e;
}
.badge-all-users {
background: #f0f0f0;
color: #666;
}
.sort-cell {
color: var(--color-muted);
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(),
enabled: integer("enabled").default(1).notNull(),
needsApproval: integer("needs_approval").default(0).notNull(),
adminOnly: integer("admin_only").default(0).notNull(),
sortOrder: integer("sort_order").default(0).notNull(),
createdAt: integer("created_at", { mode: "timestamp_ms" })
.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,
"tag": "0015_high_red_shift",
"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 { name, slug, description, type, config, enabled, needsApproval, sortOrder } = body;
const { name, slug, description, type, config, enabled, needsApproval, adminOnly, sortOrder } = body;
try {
const result = await updateAgentTool(id, {
@ -21,6 +21,7 @@ export default defineWrappedResponseHandler(async (event) => {
config,
enabled,
needsApproval,
adminOnly,
sortOrder,
});
if (!result) {

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

@ -5,7 +5,7 @@ export default defineWrappedResponseHandler(async (event) => {
await requireAdmin(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) {
return R.throwError(422, "name、slug、type 不能为空", null);
@ -20,6 +20,7 @@ export default defineWrappedResponseHandler(async (event) => {
config: config ?? {},
enabled,
needsApproval,
adminOnly,
sortOrder,
});
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 { 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);

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";
export default defineWrappedResponseHandler(async () => {
const result = await getEnabledToolInfos();
export default defineWrappedResponseHandler(async (event) => {
const user = await getCurrentUser(event);
const result = await getEnabledToolInfos(user?.role ?? null);
return R.success(result);
});

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

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

Loading…
Cancel
Save