dify
This commit is contained in:
293
dify/web/app/components/goto-anything/actions/index.ts
Normal file
293
dify/web/app/components/goto-anything/actions/index.ts
Normal file
@@ -0,0 +1,293 @@
|
||||
/**
|
||||
* Goto Anything - Action System
|
||||
*
|
||||
* This file defines the action registry for the goto-anything search system.
|
||||
* Actions handle different types of searches: apps, knowledge bases, plugins, workflow nodes, and commands.
|
||||
*
|
||||
* ## How to Add a New Slash Command
|
||||
*
|
||||
* 1. **Create Command Handler File** (in `./commands/` directory):
|
||||
* ```typescript
|
||||
* // commands/my-command.ts
|
||||
* import type { SlashCommandHandler } from './types'
|
||||
* import type { CommandSearchResult } from '../types'
|
||||
* import { registerCommands, unregisterCommands } from './command-bus'
|
||||
*
|
||||
* interface MyCommandDeps {
|
||||
* myService?: (data: any) => Promise<void>
|
||||
* }
|
||||
*
|
||||
* export const myCommand: SlashCommandHandler<MyCommandDeps> = {
|
||||
* name: 'mycommand',
|
||||
* aliases: ['mc'], // Optional aliases
|
||||
* description: 'My custom command description',
|
||||
*
|
||||
* async search(args: string, locale: string = 'en') {
|
||||
* // Return search results based on args
|
||||
* return [{
|
||||
* id: 'my-result',
|
||||
* title: 'My Command Result',
|
||||
* description: 'Description of the result',
|
||||
* type: 'command' as const,
|
||||
* data: { command: 'my.action', args: { value: args } }
|
||||
* }]
|
||||
* },
|
||||
*
|
||||
* register(deps: MyCommandDeps) {
|
||||
* registerCommands({
|
||||
* 'my.action': async (args) => {
|
||||
* await deps.myService?.(args?.value)
|
||||
* }
|
||||
* })
|
||||
* },
|
||||
*
|
||||
* unregister() {
|
||||
* unregisterCommands(['my.action'])
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* **Example for Self-Contained Command (no external dependencies):**
|
||||
* ```typescript
|
||||
* // commands/calculator-command.ts
|
||||
* export const calculatorCommand: SlashCommandHandler = {
|
||||
* name: 'calc',
|
||||
* aliases: ['calculator'],
|
||||
* description: 'Simple calculator',
|
||||
*
|
||||
* async search(args: string) {
|
||||
* if (!args.trim()) return []
|
||||
* try {
|
||||
* // Safe math evaluation (implement proper parser in real use)
|
||||
* const result = Function('"use strict"; return (' + args + ')')()
|
||||
* return [{
|
||||
* id: 'calc-result',
|
||||
* title: `${args} = ${result}`,
|
||||
* description: 'Calculator result',
|
||||
* type: 'command' as const,
|
||||
* data: { command: 'calc.copy', args: { result: result.toString() } }
|
||||
* }]
|
||||
* } catch {
|
||||
* return [{
|
||||
* id: 'calc-error',
|
||||
* title: 'Invalid expression',
|
||||
* description: 'Please enter a valid math expression',
|
||||
* type: 'command' as const,
|
||||
* data: { command: 'calc.noop', args: {} }
|
||||
* }]
|
||||
* }
|
||||
* },
|
||||
*
|
||||
* register() {
|
||||
* registerCommands({
|
||||
* 'calc.copy': (args) => navigator.clipboard.writeText(args.result),
|
||||
* 'calc.noop': () => {} // No operation
|
||||
* })
|
||||
* },
|
||||
*
|
||||
* unregister() {
|
||||
* unregisterCommands(['calc.copy', 'calc.noop'])
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* 2. **Register Command** (in `./commands/slash.tsx`):
|
||||
* ```typescript
|
||||
* import { myCommand } from './my-command'
|
||||
* import { calculatorCommand } from './calculator-command' // For self-contained commands
|
||||
*
|
||||
* export const registerSlashCommands = (deps: Record<string, any>) => {
|
||||
* slashCommandRegistry.register(themeCommand, { setTheme: deps.setTheme })
|
||||
* slashCommandRegistry.register(languageCommand, { setLocale: deps.setLocale })
|
||||
* slashCommandRegistry.register(myCommand, { myService: deps.myService }) // With dependencies
|
||||
* slashCommandRegistry.register(calculatorCommand) // Self-contained, no dependencies
|
||||
* }
|
||||
*
|
||||
* export const unregisterSlashCommands = () => {
|
||||
* slashCommandRegistry.unregister('theme')
|
||||
* slashCommandRegistry.unregister('language')
|
||||
* slashCommandRegistry.unregister('mycommand')
|
||||
* slashCommandRegistry.unregister('calc') // Add this line
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* 3. **Update SlashCommandProvider** (in `./commands/slash.tsx`):
|
||||
* ```typescript
|
||||
* export const SlashCommandProvider = () => {
|
||||
* const theme = useTheme()
|
||||
* const myService = useMyService() // Add external dependency if needed
|
||||
*
|
||||
* useEffect(() => {
|
||||
* registerSlashCommands({
|
||||
* setTheme: theme.setTheme, // Required for theme command
|
||||
* setLocale: setLocaleOnClient, // Required for language command
|
||||
* myService: myService, // Required for your custom command
|
||||
* // Note: calculatorCommand doesn't need dependencies, so not listed here
|
||||
* })
|
||||
* return () => unregisterSlashCommands()
|
||||
* }, [theme.setTheme, myService]) // Update dependency array for all dynamic deps
|
||||
*
|
||||
* return null
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* **Note:** Self-contained commands (like calculator) don't require dependencies but are
|
||||
* still registered through the same system for consistent lifecycle management.
|
||||
*
|
||||
* 4. **Usage**: Users can now type `/mycommand` or `/mc` to use your command
|
||||
*
|
||||
* ## Command System Architecture
|
||||
* - Commands are registered via `SlashCommandRegistry`
|
||||
* - Each command is self-contained with its own dependencies
|
||||
* - Commands support aliases for easier access
|
||||
* - Command execution is handled by the command bus system
|
||||
* - All commands should be registered through `SlashCommandProvider` for consistent lifecycle management
|
||||
*
|
||||
* ## Command Types
|
||||
* **Commands with External Dependencies:**
|
||||
* - Require external services, APIs, or React hooks
|
||||
* - Must provide dependencies in `SlashCommandProvider`
|
||||
* - Example: theme commands (needs useTheme), API commands (needs service)
|
||||
*
|
||||
* **Self-Contained Commands:**
|
||||
* - Pure logic operations, no external dependencies
|
||||
* - Still recommended to register through `SlashCommandProvider` for consistency
|
||||
* - Example: calculator, text manipulation commands
|
||||
*
|
||||
* ## Available Actions
|
||||
* - `@app` - Search applications
|
||||
* - `@knowledge` / `@kb` - Search knowledge bases
|
||||
* - `@plugin` - Search plugins
|
||||
* - `@node` - Search workflow nodes (workflow pages only)
|
||||
* - `/` - Execute slash commands (theme, language, etc.)
|
||||
*/
|
||||
|
||||
import { appAction } from './app'
|
||||
import { knowledgeAction } from './knowledge'
|
||||
import { pluginAction } from './plugin'
|
||||
import { workflowNodesAction } from './workflow-nodes'
|
||||
import { ragPipelineNodesAction } from './rag-pipeline-nodes'
|
||||
import type { ActionItem, SearchResult } from './types'
|
||||
import { slashAction } from './commands'
|
||||
import { slashCommandRegistry } from './commands/registry'
|
||||
|
||||
// Create dynamic Actions based on context
|
||||
export const createActions = (isWorkflowPage: boolean, isRagPipelinePage: boolean) => {
|
||||
const baseActions = {
|
||||
slash: slashAction,
|
||||
app: appAction,
|
||||
knowledge: knowledgeAction,
|
||||
plugin: pluginAction,
|
||||
}
|
||||
|
||||
// Add appropriate node search based on context
|
||||
if (isRagPipelinePage) {
|
||||
return {
|
||||
...baseActions,
|
||||
node: ragPipelineNodesAction,
|
||||
}
|
||||
}
|
||||
else if (isWorkflowPage) {
|
||||
return {
|
||||
...baseActions,
|
||||
node: workflowNodesAction,
|
||||
}
|
||||
}
|
||||
|
||||
// Default actions without node search
|
||||
return baseActions
|
||||
}
|
||||
|
||||
// Legacy export for backward compatibility
|
||||
export const Actions = {
|
||||
slash: slashAction,
|
||||
app: appAction,
|
||||
knowledge: knowledgeAction,
|
||||
plugin: pluginAction,
|
||||
node: workflowNodesAction,
|
||||
}
|
||||
|
||||
export const searchAnything = async (
|
||||
locale: string,
|
||||
query: string,
|
||||
actionItem?: ActionItem,
|
||||
dynamicActions?: Record<string, ActionItem>,
|
||||
): Promise<SearchResult[]> => {
|
||||
if (actionItem) {
|
||||
const searchTerm = query.replace(actionItem.key, '').replace(actionItem.shortcut, '').trim()
|
||||
try {
|
||||
return await actionItem.search(query, searchTerm, locale)
|
||||
}
|
||||
catch (error) {
|
||||
console.warn(`Search failed for ${actionItem.key}:`, error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
if (query.startsWith('@') || query.startsWith('/'))
|
||||
return []
|
||||
|
||||
const globalSearchActions = Object.values(dynamicActions || Actions)
|
||||
|
||||
// Use Promise.allSettled to handle partial failures gracefully
|
||||
const searchPromises = globalSearchActions.map(async (action) => {
|
||||
try {
|
||||
const results = await action.search(query, query, locale)
|
||||
return { success: true, data: results, actionType: action.key }
|
||||
}
|
||||
catch (error) {
|
||||
console.warn(`Search failed for ${action.key}:`, error)
|
||||
return { success: false, data: [], actionType: action.key, error }
|
||||
}
|
||||
})
|
||||
|
||||
const settledResults = await Promise.allSettled(searchPromises)
|
||||
|
||||
const allResults: SearchResult[] = []
|
||||
const failedActions: string[] = []
|
||||
|
||||
settledResults.forEach((result, index) => {
|
||||
if (result.status === 'fulfilled' && result.value.success) {
|
||||
allResults.push(...result.value.data)
|
||||
}
|
||||
else {
|
||||
const actionKey = globalSearchActions[index]?.key || 'unknown'
|
||||
failedActions.push(actionKey)
|
||||
}
|
||||
})
|
||||
|
||||
if (failedActions.length > 0)
|
||||
console.warn(`Some search actions failed: ${failedActions.join(', ')}`)
|
||||
|
||||
return allResults
|
||||
}
|
||||
|
||||
export const matchAction = (query: string, actions: Record<string, ActionItem>) => {
|
||||
return Object.values(actions).find((action) => {
|
||||
// Special handling for slash commands
|
||||
if (action.key === '/') {
|
||||
// Get all registered commands from the registry
|
||||
const allCommands = slashCommandRegistry.getAllCommands()
|
||||
|
||||
// Check if query matches any registered command
|
||||
return allCommands.some((cmd) => {
|
||||
const cmdPattern = `/${cmd.name}`
|
||||
|
||||
// For direct mode commands, don't match (keep in command selector)
|
||||
if (cmd.mode === 'direct')
|
||||
return false
|
||||
|
||||
// For submenu mode commands, match when complete command is entered
|
||||
return query === cmdPattern || query.startsWith(`${cmdPattern} `)
|
||||
})
|
||||
}
|
||||
|
||||
const reg = new RegExp(`^(${action.key}|${action.shortcut})(?:\\s|$)`)
|
||||
return reg.test(query)
|
||||
})
|
||||
}
|
||||
|
||||
export * from './types'
|
||||
export * from './commands'
|
||||
export { appAction, knowledgeAction, pluginAction, workflowNodesAction }
|
||||
Reference in New Issue
Block a user