feat: update custom mode draft

This commit is contained in:
duanfuxiang
2025-04-28 16:58:29 +08:00
parent 5558c96aa1
commit 497a9739d7
12 changed files with 2539 additions and 124 deletions

View File

@@ -1,4 +1,5 @@
export const ROOT_DIR = '.infio_json_db'
export const COMMAND_DIR = 'commands'
export const CHAT_DIR = 'chats'
export const CUSTOM_MODE_DIR = 'custom_modes'
export const INITIAL_MIGRATION_MARKER = '.initial_migration_completed'

View File

@@ -0,0 +1,153 @@
import fuzzysort from 'fuzzysort'
import { App } from 'obsidian'
import { v4 as uuidv4 } from 'uuid'
import { AbstractJsonRepository } from '../base'
import { CUSTOM_MODE_DIR, ROOT_DIR } from '../constants'
import {
DuplicateCustomModeException,
EmptyCustomModeNameException,
} from '../exception'
import { CUSTOM_MODE_SCHEMA_VERSION, CustomMode, CustomModeMetadata } from './types'
export class CustomModeManager extends AbstractJsonRepository<
CustomMode,
CustomModeMetadata
> {
constructor(app: App) {
super(app, `${ROOT_DIR}/${CUSTOM_MODE_DIR}`)
}
protected generateFileName(mode: CustomMode): string {
// Format: v{schemaVersion}_name_id.json (with name encoded)
const encodedName = encodeURIComponent(mode.name)
return `v${CUSTOM_MODE_SCHEMA_VERSION}_${encodedName}_${mode.id}.json`
}
protected parseFileName(fileName: string): CustomModeMetadata | null {
const match = fileName.match(
new RegExp(`^v${CUSTOM_MODE_SCHEMA_VERSION}_(.+)_([0-9a-f-]+)\\.json$`),
)
if (!match) return null
const encodedName = match[1]
const id = match[2]
const name = decodeURIComponent(encodedName)
return {
id,
name,
updatedAt: Date.now(),
schemaVersion: CUSTOM_MODE_SCHEMA_VERSION,
}
}
public async createCustomMode(
customMode: Omit<
CustomMode,
'id' | 'slug' | 'createdAt' | 'updatedAt' | 'schemaVersion'
>,
): Promise<CustomMode> {
if (customMode.name !== undefined && customMode.name.length === 0) {
throw new EmptyCustomModeNameException()
}
const existingCustomMode = await this.findByName(customMode.name)
if (existingCustomMode) {
throw new DuplicateCustomModeException(customMode.name)
}
const newCustomMode: CustomMode = {
id: uuidv4(),
...customMode,
slug: customMode.name.toLowerCase().replace(/ /g, '-'),
updatedAt: Date.now(),
schemaVersion: CUSTOM_MODE_SCHEMA_VERSION,
}
await this.create(newCustomMode)
return newCustomMode
}
public async ListCustomModes(): Promise<CustomMode[]> {
const allMetadata = await this.listMetadata()
const allCustomModes = await Promise.all(allMetadata.map(async (meta) => this.read(meta.fileName)))
return allCustomModes.sort((a, b) => b.updatedAt - a.updatedAt)
}
public async findById(id: string): Promise<CustomMode | null> {
const allMetadata = await this.listMetadata()
const targetMetadata = allMetadata.find((meta) => meta.id === id)
if (!targetMetadata) return null
return this.read(targetMetadata.fileName)
}
public async findByName(name: string): Promise<CustomMode | null> {
const allMetadata = await this.listMetadata()
const targetMetadata = allMetadata.find((meta) => meta.name === name)
if (!targetMetadata) return null
return this.read(targetMetadata.fileName)
}
public async updateCustomMode(
id: string,
updates: Partial<
Omit<CustomMode, 'id' | 'slug' | 'createdAt' | 'updatedAt' | 'schemaVersion'>
>,
): Promise<CustomMode | null> {
if (updates.name !== undefined && updates.name.length === 0) {
throw new EmptyCustomModeNameException()
}
const customMode = await this.findById(id)
if (!customMode) return null
if (updates.name && updates.name !== customMode.name) {
const existingCustomMode = await this.findByName(updates.name)
if (existingCustomMode) {
throw new DuplicateCustomModeException(updates.name)
}
}
const updatedCustomMode: CustomMode = {
...customMode,
...updates,
updatedAt: Date.now(),
}
await this.update(customMode, updatedCustomMode)
return updatedCustomMode
}
public async deleteCustomMode(id: string): Promise<boolean> {
const customMode = await this.findById(id)
if (!customMode) return false
const fileName = this.generateFileName(customMode)
await this.delete(fileName)
return true
}
public async searchCustomModes(query: string): Promise<CustomMode[]> {
const allMetadata = await this.listMetadata()
const results = fuzzysort.go(query, allMetadata, {
keys: ['name'],
threshold: 0.2,
limit: 20,
all: true,
})
const customModes = (
await Promise.all(
results.map(async (result) => this.read(result.obj.fileName)),
)
).filter((customMode): customMode is CustomMode => customMode !== null)
return customModes
}
}

View File

@@ -0,0 +1,84 @@
import { z } from "zod"
export const CUSTOM_MODE_SCHEMA_VERSION = 1
export const toolGroups = [
"read",
"edit",
"research",
// "browser",
// "command",
// "mcp",
"modes",
] as const
export const toolGroupsSchema = z.enum(toolGroups)
export type ToolGroup = z.infer<typeof toolGroupsSchema>
export const groupOptionsSchema = z.object({
fileRegex: z
.string()
.optional()
.refine(
(pattern) => {
if (!pattern) {
return true // Optional, so empty is valid.
}
try {
new RegExp(pattern)
return true
} catch {
return false
}
},
{ message: "Invalid regular expression pattern" },
),
description: z.string().optional(),
})
export const groupEntrySchema = z.union([toolGroupsSchema, z.tuple([toolGroupsSchema, groupOptionsSchema])])
export type GroupEntry = z.infer<typeof groupEntrySchema>
const groupEntryArraySchema = z.array(groupEntrySchema).refine(
(groups) => {
const seen = new Set()
return groups.every((group) => {
// For tuples, check the group name (first element).
const groupName = Array.isArray(group) ? group[0] : group
if (seen.has(groupName)) {
return false
}
seen.add(groupName)
return true
})
},
{ message: "Duplicate groups are not allowed" },
)
export const modeConfigSchema = z.object({
id: z.string().uuid("Invalid ID"),
slug: z.string().regex(/^[a-zA-Z0-9-]+$/, "Slug must contain only letters numbers and dashes"),
name: z.string().min(1, "Name is required"),
roleDefinition: z.string().min(1, "Role definition is required"),
customInstructions: z.string().optional(),
groups: groupEntryArraySchema,
source: z.enum(["global", "project"]).optional(),
updatedAt: z.number().int().positive(),
schemaVersion: z.literal(CUSTOM_MODE_SCHEMA_VERSION),
})
export type CustomMode = z.infer<typeof modeConfigSchema>
export type CustomModeMetadata = {
id: string
name: string
updatedAt: number
schemaVersion: number
}

View File

@@ -5,6 +5,13 @@ export class DuplicateCommandException extends Error {
}
}
export class DuplicateCustomModeException extends Error {
constructor(customModeName: string) {
super(`Custom mode with name "${customModeName}" already exists`)
this.name = 'DuplicateCustomModeException'
}
}
export class EmptyCommandNameException extends Error {
constructor() {
super('Command name cannot be empty')
@@ -18,3 +25,10 @@ export class EmptyChatTitleException extends Error {
this.name = 'EmptyChatTitleException'
}
}
export class EmptyCustomModeNameException extends Error {
constructor() {
super('Custom mode name cannot be empty')
this.name = 'EmptyCustomModeNameException'
}
}