|
|
@@ -2,7 +2,7 @@
|
|
|
import crypto from 'node:crypto'
|
|
|
import { and, desc, eq, ne } from 'drizzle-orm'
|
|
|
import { z } from 'zod'
|
|
|
-import type { ApiKeyDto, ApiKeyProfiles, FileAccessPolicy, KnowledgeRootDto, ProfileDto, ProfileInput } from '@agent-server/shared'
|
|
|
+import type { ApiKeyDto, ApiKeyProfiles, FileAccessPolicy, KnowledgeRootDto, ProfileDto, ProfileInput, ProviderId } from '@agent-server/shared'
|
|
|
import type { DB } from '../db'
|
|
|
import { apiKeys, auditLogs, knowledgeRoots, profiles } from '../db/schema'
|
|
|
import { AppError, badRequest, notFound } from '../lib/errors'
|
|
|
@@ -41,26 +41,42 @@ const toProfileDto = (r: ProfileRow): ProfileDto => ({
|
|
|
fileAccess: r.fileAccess, knowledgeRootIds: r.knowledgeRootIds, isDefault: r.isDefault, builtin: r.builtin, version: r.version, updatedAt: r.updatedAt,
|
|
|
})
|
|
|
|
|
|
+/** 内置的标准 Profile:每个引擎一个"通用助手",不可删除,引擎不可修改 */
|
|
|
+const BUILTIN_NAMES: Record<ProviderId, string> = { claude: '通用助手(Claude)', codex: '通用助手(Codex)' }
|
|
|
+const FALLBACK_MODELS: Record<ProviderId, string> = { claude: 'sonnet', codex: 'gpt-6.1-sol' }
|
|
|
+
|
|
|
export class ProfileService {
|
|
|
constructor(private db: DB, private settings: SettingsService) {
|
|
|
- if (!db.select({ id: profiles.id }).from(profiles).limit(1).get()) this.seed()
|
|
|
+ this.ensureBuiltins()
|
|
|
}
|
|
|
|
|
|
- private seed() {
|
|
|
- this.create({
|
|
|
- name: '通用助手',
|
|
|
- description: '数据分析、资料查找与文档生成',
|
|
|
- provider: 'claude',
|
|
|
- model: this.settings.get().providers.claude.defaultModel,
|
|
|
- systemPrompt: '',
|
|
|
- permissionMode: 'ask',
|
|
|
- allowedTools: [],
|
|
|
- disallowedTools: [],
|
|
|
- skills: 'all',
|
|
|
- fileAccess: { mode: this.settings.get().security.defaultFileAccessMode, read: [], write: [], deny: [] },
|
|
|
- knowledgeRootIds: [],
|
|
|
- isDefault: true,
|
|
|
- }, true)
|
|
|
+ /** 补齐每个引擎的内置 Profile;旧版本的"通用助手"改名为 Claude 版本 */
|
|
|
+ private ensureBuiltins() {
|
|
|
+ const hasAny = !!this.db.select({ id: profiles.id }).from(profiles).limit(1).get()
|
|
|
+ for (const provider of ['claude', 'codex'] as const) {
|
|
|
+ const existing = this.db.select().from(profiles).where(and(eq(profiles.builtin, true), eq(profiles.provider, provider))).get()
|
|
|
+ if (existing) {
|
|
|
+ if (existing.name === '通用助手') this.db.update(profiles).set({ name: BUILTIN_NAMES[provider] }).where(eq(profiles.id, existing.id)).run()
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ const s = this.settings.get()
|
|
|
+ this.create({
|
|
|
+ name: BUILTIN_NAMES[provider],
|
|
|
+ description: `数据分析、资料查找与文档生成(${provider === 'claude' ? 'Claude' : 'Codex'} 标准版)`,
|
|
|
+ provider,
|
|
|
+ // 设置中的默认模型可能为空(例如保存过空值),回落到内置默认
|
|
|
+ model: s.providers[provider].defaultModel || FALLBACK_MODELS[provider],
|
|
|
+ systemPrompt: '',
|
|
|
+ permissionMode: 'ask',
|
|
|
+ allowedTools: [],
|
|
|
+ disallowedTools: [],
|
|
|
+ skills: 'all',
|
|
|
+ fileAccess: { mode: s.security.defaultFileAccessMode, read: [], write: [], deny: [] },
|
|
|
+ knowledgeRootIds: [],
|
|
|
+ // 全新安装时 Claude 版本为默认 Profile
|
|
|
+ isDefault: provider === 'claude' && !hasAny,
|
|
|
+ }, true)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
list(): ProfileDto[] {
|
|
|
@@ -94,6 +110,7 @@ export class ProfileService {
|
|
|
const prev = this.get(id)
|
|
|
const data = parse(profileInputSchema, input)
|
|
|
if (prev.isDefault && !data.isDefault) throw badRequest('请先把另一个 Profile 设为默认')
|
|
|
+ if (prev.builtin && data.provider !== prev.provider) throw badRequest('内置 Profile 的引擎不能修改')
|
|
|
this.db.transaction(tx => {
|
|
|
if (data.isDefault) tx.update(profiles).set({ isDefault: false }).where(ne(profiles.id, id)).run()
|
|
|
tx.update(profiles).set({ ...data, version: prev.version + 1, updatedAt: nowIso() }).where(eq(profiles.id, id)).run()
|
|
|
@@ -101,8 +118,8 @@ export class ProfileService {
|
|
|
return this.get(id)
|
|
|
}
|
|
|
|
|
|
- getBuiltin(): ProfileDto {
|
|
|
- const row = this.db.select().from(profiles).where(eq(profiles.builtin, true)).get()
|
|
|
+ getBuiltin(provider: ProviderId = 'claude'): ProfileDto {
|
|
|
+ const row = this.db.select().from(profiles).where(and(eq(profiles.builtin, true), eq(profiles.provider, provider))).get()
|
|
|
return row ? toProfileDto(row) : this.getDefault()
|
|
|
}
|
|
|
|