mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-24 07:30:52 +08:00
427 lines
12 KiB
TypeScript
427 lines
12 KiB
TypeScript
/**
|
|
* 助手管理器
|
|
* 负责助手配置的加载、CRUD 和内置助手导入
|
|
*
|
|
* 存储策略(导入模型):
|
|
* - 内置助手作为模板目录打包在 BUILTIN_CONFIGS 中
|
|
* - 启动时仅自动导入 general 助手
|
|
* - 用户通过"助手市场"主动导入其他内置助手
|
|
* - 导入后完全属于用户,可自由编辑/删除(general 除外)
|
|
* - 市场可查看内置助手是否有新版本,用户可手动重新导入
|
|
*/
|
|
|
|
import * as fs from 'fs'
|
|
import * as path from 'path'
|
|
import { randomUUID } from 'crypto'
|
|
import { getAiDataDir, ensureDir } from '../../paths'
|
|
import { aiLogger } from '../logger'
|
|
import type {
|
|
AssistantConfig,
|
|
AssistantSummary,
|
|
AssistantInitResult,
|
|
AssistantSaveResult,
|
|
BuiltinAssistantInfo,
|
|
} from './types'
|
|
|
|
import builtinGeneral from './builtins/general.json'
|
|
import builtinCommunityAnalyst from './builtins/community_analyst.json'
|
|
import builtinEmotionAnalyst from './builtins/emotion_analyst.json'
|
|
import builtinCustomerService from './builtins/customer_service.json'
|
|
|
|
const BUILTIN_CONFIGS: AssistantConfig[] = [
|
|
builtinGeneral as AssistantConfig,
|
|
builtinCommunityAnalyst as AssistantConfig,
|
|
builtinEmotionAnalyst as AssistantConfig,
|
|
builtinCustomerService as AssistantConfig,
|
|
]
|
|
|
|
const ASSISTANTS_DIR_NAME = 'assistants'
|
|
|
|
let cachedAssistants: Map<string, AssistantConfig> = new Map()
|
|
let initialized = false
|
|
|
|
function getAssistantsDir(): string {
|
|
return path.join(getAiDataDir(), ASSISTANTS_DIR_NAME)
|
|
}
|
|
|
|
// ==================== 初始化 ====================
|
|
|
|
/**
|
|
* 初始化助手管理器
|
|
* - 确保目录存在
|
|
* - 确保 general 助手已导入
|
|
* - 加载所有用户助手配置
|
|
*/
|
|
export function initAssistantManager(): AssistantInitResult {
|
|
const assistantsDir = getAssistantsDir()
|
|
ensureDir(assistantsDir)
|
|
|
|
const generalCreated = ensureGeneralAssistant()
|
|
loadAllAssistants()
|
|
|
|
initialized = true
|
|
aiLogger.info('AssistantManager', 'Initialized', {
|
|
total: cachedAssistants.size,
|
|
generalCreated,
|
|
})
|
|
|
|
return { total: cachedAssistants.size, generalCreated }
|
|
}
|
|
|
|
/**
|
|
* 确保 general 助手存在于用户目录(首次启动自动导入)
|
|
*/
|
|
function ensureGeneralAssistant(): boolean {
|
|
const generalConfig = BUILTIN_CONFIGS.find((c) => c.id === 'general')
|
|
if (!generalConfig) return false
|
|
|
|
const userFilePath = path.join(getAssistantsDir(), 'general.json')
|
|
if (fs.existsSync(userFilePath)) return false
|
|
|
|
const configToWrite: AssistantConfig = {
|
|
...generalConfig,
|
|
builtinId: generalConfig.id,
|
|
}
|
|
writeJsonFile(userFilePath, configToWrite)
|
|
return true
|
|
}
|
|
|
|
/**
|
|
* 从用户目录加载所有助手配置到内存缓存
|
|
*/
|
|
function loadAllAssistants(): void {
|
|
cachedAssistants.clear()
|
|
|
|
const assistantsDir = getAssistantsDir()
|
|
if (!fs.existsSync(assistantsDir)) return
|
|
|
|
const files = fs.readdirSync(assistantsDir).filter((f) => f.endsWith('.json'))
|
|
|
|
for (const file of files) {
|
|
try {
|
|
const raw = readJsonFile<AssistantConfig & { responseRules?: string }>(path.join(assistantsDir, file))
|
|
if (raw && raw.id) {
|
|
const config = migrateResponseRules(raw, path.join(assistantsDir, file))
|
|
cachedAssistants.set(config.id, config)
|
|
}
|
|
} catch (error) {
|
|
aiLogger.warn('AssistantManager', `Failed to load assistant: ${file}`, { error: String(error) })
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 迁移旧数据:将 responseRules 合并到 systemPrompt,删除旧字段并持久化
|
|
*/
|
|
function migrateResponseRules(
|
|
raw: AssistantConfig & { responseRules?: string },
|
|
filePath: string
|
|
): AssistantConfig {
|
|
if (!raw.responseRules) return raw as AssistantConfig
|
|
|
|
const merged: AssistantConfig = {
|
|
...raw,
|
|
systemPrompt: `${raw.systemPrompt}\n\n## 回答要求\n${raw.responseRules}`,
|
|
}
|
|
delete (merged as Record<string, unknown>).responseRules
|
|
|
|
try {
|
|
writeJsonFile(filePath, merged)
|
|
aiLogger.info('AssistantManager', `Migrated responseRules for assistant: ${raw.id}`)
|
|
} catch (error) {
|
|
aiLogger.warn('AssistantManager', `Failed to persist migration for: ${raw.id}`, { error: String(error) })
|
|
}
|
|
|
|
return merged
|
|
}
|
|
|
|
// ==================== 查询 API ====================
|
|
|
|
/**
|
|
* 获取所有已导入助手的摘要列表(用于前端展示)
|
|
*/
|
|
export function getAllAssistants(): AssistantSummary[] {
|
|
ensureInitialized()
|
|
|
|
return Array.from(cachedAssistants.values())
|
|
.sort((a, b) => {
|
|
const orderDiff = (a.order ?? 100) - (b.order ?? 100)
|
|
if (orderDiff !== 0) return orderDiff
|
|
return a.name.localeCompare(b.name)
|
|
})
|
|
.map(toSummary)
|
|
}
|
|
|
|
/**
|
|
* 获取单个助手的完整配置
|
|
*/
|
|
export function getAssistantConfig(id: string): AssistantConfig | null {
|
|
ensureInitialized()
|
|
return cachedAssistants.get(id) ?? null
|
|
}
|
|
|
|
/**
|
|
* 检查助手是否存在
|
|
*/
|
|
export function hasAssistant(id: string): boolean {
|
|
ensureInitialized()
|
|
return cachedAssistants.has(id)
|
|
}
|
|
|
|
// ==================== 内置助手目录(市场) ====================
|
|
|
|
/**
|
|
* 获取内置助手模板目录(用于助手市场展示)
|
|
* 对每个内置助手检查用户是否已导入、是否有版本更新
|
|
*/
|
|
export function getBuiltinCatalog(): BuiltinAssistantInfo[] {
|
|
ensureInitialized()
|
|
|
|
return BUILTIN_CONFIGS.map((builtin) => {
|
|
const userAssistant = findImportedByBuiltinId(builtin.id)
|
|
const imported = !!userAssistant
|
|
const hasUpdate = imported && builtin.version > (userAssistant!.version || 0)
|
|
|
|
return {
|
|
id: builtin.id,
|
|
name: builtin.name,
|
|
systemPrompt: builtin.systemPrompt,
|
|
version: builtin.version,
|
|
order: builtin.order,
|
|
applicableChatTypes: builtin.applicableChatTypes,
|
|
supportedLocales: builtin.supportedLocales,
|
|
imported,
|
|
hasUpdate,
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 从内置模板导入助手到用户目录
|
|
* - 同一 builtinId 不可重复导入
|
|
*/
|
|
export function importAssistant(builtinId: string): AssistantSaveResult {
|
|
ensureInitialized()
|
|
|
|
const builtinConfig = BUILTIN_CONFIGS.find((c) => c.id === builtinId)
|
|
if (!builtinConfig) {
|
|
return { success: false, error: `Builtin assistant not found: ${builtinId}` }
|
|
}
|
|
|
|
const existing = findImportedByBuiltinId(builtinId)
|
|
if (existing) {
|
|
return { success: false, error: `Assistant already imported: ${builtinId}` }
|
|
}
|
|
|
|
const newConfig: AssistantConfig = {
|
|
...builtinConfig,
|
|
builtinId: builtinConfig.id,
|
|
}
|
|
|
|
return saveAssistantToDisk(newConfig)
|
|
}
|
|
|
|
/**
|
|
* 重新导入内置助手(覆盖用户副本为最新模板版本,保留 id)
|
|
*/
|
|
export function reimportAssistant(id: string): AssistantSaveResult {
|
|
ensureInitialized()
|
|
|
|
const existing = cachedAssistants.get(id)
|
|
if (!existing) {
|
|
return { success: false, error: `Assistant not found: ${id}` }
|
|
}
|
|
if (!existing.builtinId) {
|
|
return { success: false, error: 'Only imported builtin assistants can be reimported' }
|
|
}
|
|
|
|
const builtinConfig = BUILTIN_CONFIGS.find((c) => c.id === existing.builtinId)
|
|
if (!builtinConfig) {
|
|
return { success: false, error: `Builtin template not found: ${existing.builtinId}` }
|
|
}
|
|
|
|
const updatedConfig: AssistantConfig = {
|
|
...builtinConfig,
|
|
id: existing.id,
|
|
builtinId: existing.builtinId,
|
|
}
|
|
|
|
return saveAssistantToDisk(updatedConfig)
|
|
}
|
|
|
|
// ==================== 修改 API ====================
|
|
|
|
/**
|
|
* 更新助手配置(用于配置弹窗保存)
|
|
*/
|
|
export function updateAssistant(id: string, updates: Partial<AssistantConfig>): AssistantSaveResult {
|
|
ensureInitialized()
|
|
|
|
const existing = cachedAssistants.get(id)
|
|
if (!existing) {
|
|
return { success: false, error: `Assistant not found: ${id}` }
|
|
}
|
|
|
|
const updated: AssistantConfig = {
|
|
...existing,
|
|
...updates,
|
|
id,
|
|
}
|
|
|
|
return saveAssistantToDisk(updated)
|
|
}
|
|
|
|
/**
|
|
* 创建自定义助手
|
|
*/
|
|
export function createAssistant(config: Omit<AssistantConfig, 'id' | 'version'>): AssistantSaveResult & { id?: string } {
|
|
ensureInitialized()
|
|
|
|
const id = `custom_${randomUUID().replace(/-/g, '').slice(0, 12)}`
|
|
const newConfig: AssistantConfig = {
|
|
...config,
|
|
id,
|
|
version: 1,
|
|
builtinId: undefined,
|
|
}
|
|
|
|
const result = saveAssistantToDisk(newConfig)
|
|
return { ...result, id: result.success ? id : undefined }
|
|
}
|
|
|
|
/**
|
|
* 删除助手
|
|
* general 助手不可删除,其他导入的内置助手可以删除
|
|
*/
|
|
export function deleteAssistant(id: string): AssistantSaveResult {
|
|
ensureInitialized()
|
|
|
|
if (id === 'general') {
|
|
return { success: false, error: 'Cannot delete the default assistant (general)' }
|
|
}
|
|
|
|
const existing = cachedAssistants.get(id)
|
|
if (!existing) {
|
|
return { success: false, error: `Assistant not found: ${id}` }
|
|
}
|
|
|
|
try {
|
|
const filePath = path.join(getAssistantsDir(), `${id}.json`)
|
|
if (fs.existsSync(filePath)) {
|
|
fs.unlinkSync(filePath)
|
|
}
|
|
cachedAssistants.delete(id)
|
|
return { success: true }
|
|
} catch (error) {
|
|
return { success: false, error: String(error) }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 重置内置助手为出厂默认
|
|
*/
|
|
export function resetAssistant(id: string): AssistantSaveResult {
|
|
ensureInitialized()
|
|
|
|
const existing = cachedAssistants.get(id)
|
|
if (!existing?.builtinId) {
|
|
return { success: false, error: 'Only builtin assistants can be reset' }
|
|
}
|
|
|
|
const builtinConfig = BUILTIN_CONFIGS.find((c) => c.id === existing.builtinId)
|
|
if (!builtinConfig) {
|
|
return { success: false, error: `Builtin config not found: ${existing.builtinId}` }
|
|
}
|
|
|
|
const resetConfig: AssistantConfig = {
|
|
...builtinConfig,
|
|
id: existing.id,
|
|
builtinId: existing.builtinId,
|
|
}
|
|
|
|
return saveAssistantToDisk(resetConfig)
|
|
}
|
|
|
|
// ==================== 提示词预设迁移 ====================
|
|
|
|
/**
|
|
* 备份旧的提示词预设数据到 data/backup 目录
|
|
*/
|
|
export function backupOldPromptPresets(data: {
|
|
customPresets?: unknown[]
|
|
builtinOverrides?: Record<string, unknown>
|
|
remotePresetIds?: string[]
|
|
}): { success: boolean; filePath?: string; error?: string } {
|
|
try {
|
|
const backupDir = path.join(getAiDataDir(), '..', 'backup')
|
|
ensureDir(backupDir)
|
|
|
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)
|
|
const filePath = path.join(backupDir, `prompt-presets-${timestamp}.json`)
|
|
|
|
const backupContent = {
|
|
backupTime: new Date().toISOString(),
|
|
description: 'ChatLab 旧提示词预设系统备份(已被多助手系统替代)',
|
|
...data,
|
|
}
|
|
|
|
writeJsonFile(filePath, backupContent)
|
|
aiLogger.info('AssistantManager', 'Old prompt presets backed up', { filePath })
|
|
|
|
return { success: true, filePath }
|
|
} catch (error) {
|
|
aiLogger.error('AssistantManager', 'Failed to backup prompt presets', { error: String(error) })
|
|
return { success: false, error: String(error) }
|
|
}
|
|
}
|
|
|
|
// ==================== 内部工具函数 ====================
|
|
|
|
function ensureInitialized(): void {
|
|
if (!initialized) {
|
|
initAssistantManager()
|
|
}
|
|
}
|
|
|
|
function findImportedByBuiltinId(builtinId: string): AssistantConfig | undefined {
|
|
return Array.from(cachedAssistants.values()).find((c) => c.builtinId === builtinId)
|
|
}
|
|
|
|
function toSummary(config: AssistantConfig): AssistantSummary {
|
|
return {
|
|
id: config.id,
|
|
name: config.name,
|
|
systemPrompt: config.systemPrompt,
|
|
presetQuestions: config.presetQuestions,
|
|
order: config.order,
|
|
builtinId: config.builtinId,
|
|
applicableChatTypes: config.applicableChatTypes,
|
|
supportedLocales: config.supportedLocales,
|
|
}
|
|
}
|
|
|
|
function saveAssistantToDisk(config: AssistantConfig): AssistantSaveResult {
|
|
try {
|
|
const filePath = path.join(getAssistantsDir(), `${config.id}.json`)
|
|
writeJsonFile(filePath, config)
|
|
cachedAssistants.set(config.id, config)
|
|
return { success: true }
|
|
} catch (error) {
|
|
aiLogger.error('AssistantManager', `Failed to save assistant: ${config.id}`, { error: String(error) })
|
|
return { success: false, error: String(error) }
|
|
}
|
|
}
|
|
|
|
function readJsonFile<T>(filePath: string): T | null {
|
|
try {
|
|
const content = fs.readFileSync(filePath, 'utf-8')
|
|
return JSON.parse(content) as T
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
function writeJsonFile(filePath: string, data: unknown): void {
|
|
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8')
|
|
}
|