- Updated GetAssistants and DeleteAssistants methods to utilize jsonContainsValue for filtering tags and locales, improving query compatibility across database drivers. - Renamed related test functions to reflect the new JSON handling approach. - Removed deprecated jsonLikeExpr and whereJsonLike methods to streamline codebase.
924 lines
25 KiB
Go
924 lines
25 KiB
Go
package xun
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"time"
|
|
|
|
jsoniter "github.com/json-iterator/go"
|
|
"github.com/yaoapp/kun/log"
|
|
"github.com/yaoapp/xun/dbal/query"
|
|
"github.com/yaoapp/yao/agent/context"
|
|
"github.com/yaoapp/yao/agent/i18n"
|
|
searchTypes "github.com/yaoapp/yao/agent/search/types"
|
|
"github.com/yaoapp/yao/agent/store/types"
|
|
)
|
|
|
|
func sandboxForDB(a *types.AssistantModel) interface{} {
|
|
if a.SandboxV2 != nil {
|
|
return a.SandboxV2
|
|
}
|
|
return a.Sandbox
|
|
}
|
|
|
|
// SaveAssistant saves assistant information
|
|
func (store *Xun) SaveAssistant(assistant *types.AssistantModel) (string, error) {
|
|
if assistant == nil {
|
|
return "", fmt.Errorf("assistant cannot be nil")
|
|
}
|
|
|
|
// Validate required fields
|
|
if assistant.Name == "" {
|
|
return "", fmt.Errorf("field name is required")
|
|
}
|
|
if assistant.Type == "" {
|
|
return "", fmt.Errorf("field type is required")
|
|
}
|
|
if assistant.Connector == "" {
|
|
return "", fmt.Errorf("field connector is required")
|
|
}
|
|
|
|
// Generate assistant_id if not provided
|
|
if assistant.ID == "" {
|
|
var err error
|
|
assistant.ID, err = store.GenerateAssistantID()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
// Check if assistant exists
|
|
exists, err := store.query.New().
|
|
Table(store.getAssistantTable()).
|
|
Where("assistant_id", assistant.ID).
|
|
Exists()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// Convert model to map for database storage
|
|
data := make(map[string]interface{})
|
|
data["assistant_id"] = assistant.ID
|
|
data["type"] = assistant.Type
|
|
data["connector"] = assistant.Connector
|
|
data["built_in"] = assistant.BuiltIn
|
|
data["sort"] = assistant.Sort
|
|
data["readonly"] = assistant.Readonly
|
|
data["public"] = assistant.Public
|
|
data["mentionable"] = assistant.Mentionable
|
|
data["automated"] = assistant.Automated
|
|
data["disable_global_prompts"] = assistant.DisableGlobalPrompts
|
|
|
|
// Set timestamps
|
|
now := time.Now().UnixNano()
|
|
if exists {
|
|
// Update: set updated_at, keep created_at unchanged
|
|
if assistant.UpdatedAt == 0 {
|
|
data["updated_at"] = now
|
|
} else {
|
|
data["updated_at"] = assistant.UpdatedAt
|
|
}
|
|
// Don't modify created_at on update
|
|
} else {
|
|
// Create: set created_at, updated_at is null
|
|
if assistant.CreatedAt == 0 {
|
|
data["created_at"] = now
|
|
} else {
|
|
data["created_at"] = assistant.CreatedAt
|
|
}
|
|
data["updated_at"] = nil
|
|
}
|
|
|
|
// Handle nullable string fields from assistant.mod.yao
|
|
// Store as nil if empty string (this matches database nullable: true fields)
|
|
if assistant.Name != "" {
|
|
data["name"] = assistant.Name
|
|
} else {
|
|
data["name"] = nil
|
|
}
|
|
if assistant.Avatar != "" {
|
|
data["avatar"] = assistant.Avatar
|
|
} else {
|
|
data["avatar"] = nil
|
|
}
|
|
if assistant.Description != "" {
|
|
data["description"] = assistant.Description
|
|
} else {
|
|
data["description"] = nil
|
|
}
|
|
if assistant.Capabilities != "" {
|
|
data["capabilities"] = assistant.Capabilities
|
|
} else {
|
|
data["capabilities"] = nil
|
|
}
|
|
if assistant.Path != "" {
|
|
data["path"] = assistant.Path
|
|
} else {
|
|
data["path"] = nil
|
|
}
|
|
if assistant.Source != "" {
|
|
data["source"] = assistant.Source
|
|
} else {
|
|
data["source"] = nil
|
|
}
|
|
|
|
// Share field: nullable: false with default "private"
|
|
// Apply default if empty
|
|
if assistant.Share != "" {
|
|
data["share"] = assistant.Share
|
|
} else {
|
|
data["share"] = "private" // Apply default value
|
|
}
|
|
|
|
// Permission management fields - store as nil if empty
|
|
if assistant.YaoCreatedBy != "" {
|
|
data["__yao_created_by"] = assistant.YaoCreatedBy
|
|
} else {
|
|
data["__yao_created_by"] = nil
|
|
}
|
|
if assistant.YaoUpdatedBy != "" {
|
|
data["__yao_updated_by"] = assistant.YaoUpdatedBy
|
|
} else {
|
|
data["__yao_updated_by"] = nil
|
|
}
|
|
if assistant.YaoTeamID != "" {
|
|
data["__yao_team_id"] = assistant.YaoTeamID
|
|
} else {
|
|
data["__yao_team_id"] = nil
|
|
}
|
|
if assistant.YaoTenantID != "" {
|
|
data["__yao_tenant_id"] = assistant.YaoTenantID
|
|
} else {
|
|
data["__yao_tenant_id"] = nil
|
|
}
|
|
|
|
// DefaultMode is a simple string field
|
|
if assistant.DefaultMode != "" {
|
|
data["default_mode"] = assistant.DefaultMode
|
|
} else {
|
|
data["default_mode"] = nil
|
|
}
|
|
|
|
// Handle all JSON fields uniformly via marshalJSONFields.
|
|
// Uses isNil() to correctly skip typed nils stored in interface{}.
|
|
jsonFields := map[string]interface{}{
|
|
"options": assistant.Options,
|
|
"tags": assistant.Tags,
|
|
"modes": assistant.Modes,
|
|
"prompts": assistant.Prompts,
|
|
"prompt_presets": assistant.PromptPresets,
|
|
"connector_options": assistant.ConnectorOptions,
|
|
"kb": assistant.KB,
|
|
"db": assistant.DB,
|
|
"mcp": assistant.MCP,
|
|
"workflow": assistant.Workflow,
|
|
"sandbox": sandboxForDB(assistant),
|
|
"placeholder": assistant.Placeholder,
|
|
"locales": assistant.Locales,
|
|
"uses": assistant.Uses,
|
|
"search": assistant.Search,
|
|
"dependencies": assistant.Dependencies,
|
|
}
|
|
|
|
if err := marshalJSONFields(data, jsonFields); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// Update or insert
|
|
if exists {
|
|
_, err := store.query.New().
|
|
Table(store.getAssistantTable()).
|
|
Where("assistant_id", assistant.ID).
|
|
Update(data)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return assistant.ID, nil
|
|
}
|
|
|
|
err = store.query.New().
|
|
Table(store.getAssistantTable()).
|
|
Insert(data)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return assistant.ID, nil
|
|
}
|
|
|
|
// UpdateAssistant updates specific fields of an assistant
|
|
func (store *Xun) UpdateAssistant(assistantID string, updates map[string]interface{}) error {
|
|
if assistantID == "" {
|
|
return fmt.Errorf("assistant_id is required")
|
|
}
|
|
if len(updates) == 0 {
|
|
return fmt.Errorf("no fields to update")
|
|
}
|
|
|
|
// Check if assistant exists
|
|
exists, err := store.query.New().
|
|
Table(store.getAssistantTable()).
|
|
Where("assistant_id", assistantID).
|
|
Exists()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !exists {
|
|
return fmt.Errorf("assistant %s not found", assistantID)
|
|
}
|
|
|
|
// Prepare update data
|
|
data := make(map[string]interface{})
|
|
|
|
// List of fields that need JSON marshaling
|
|
jsonFields := []string{"options", "tags", "modes", "prompts", "prompt_presets", "connector_options", "kb", "db", "mcp", "workflow", "sandbox", "placeholder", "locales", "uses", "search", "dependencies"}
|
|
jsonFieldSet := make(map[string]bool)
|
|
for _, field := range jsonFields {
|
|
jsonFieldSet[field] = true
|
|
}
|
|
|
|
// List of nullable string fields
|
|
nullableStringFields := []string{"name", "avatar", "description", "capabilities", "path", "source", "default_mode", "__yao_created_by", "__yao_updated_by", "__yao_team_id", "__yao_tenant_id"}
|
|
nullableFieldSet := make(map[string]bool)
|
|
for _, field := range nullableStringFields {
|
|
nullableFieldSet[field] = true
|
|
}
|
|
|
|
// Process each update field
|
|
for key, value := range updates {
|
|
// Skip system fields that shouldn't be updated directly
|
|
if key == "assistant_id" || key == "created_at" {
|
|
continue
|
|
}
|
|
|
|
// Handle JSON fields
|
|
if jsonFieldSet[key] {
|
|
if isNil(value) {
|
|
data[key] = nil
|
|
} else {
|
|
jsonStr, err := jsoniter.MarshalToString(value)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal %s: %w", key, err)
|
|
}
|
|
data[key] = jsonStr
|
|
}
|
|
} else {
|
|
// Handle regular fields
|
|
// Convert empty strings to nil for nullable fields
|
|
if strVal, ok := value.(string); ok && strVal == "" && nullableFieldSet[key] {
|
|
data[key] = nil
|
|
continue
|
|
}
|
|
data[key] = value
|
|
}
|
|
}
|
|
|
|
// Always update updated_at timestamp
|
|
data["updated_at"] = types.ToMySQLTime(time.Now().UnixNano())
|
|
|
|
if len(data) == 0 {
|
|
return fmt.Errorf("no valid fields to update")
|
|
}
|
|
|
|
// Perform update
|
|
_, err = store.query.New().
|
|
Table(store.getAssistantTable()).
|
|
Where("assistant_id", assistantID).
|
|
Update(data)
|
|
|
|
return err
|
|
}
|
|
|
|
// DeleteAssistant deletes an assistant by assistant_id
|
|
func (store *Xun) DeleteAssistant(assistantID string) error {
|
|
// Check if assistant exists
|
|
exists, err := store.query.New().
|
|
Table(store.getAssistantTable()).
|
|
Where("assistant_id", assistantID).
|
|
Exists()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !exists {
|
|
return fmt.Errorf("assistant %s not found", assistantID)
|
|
}
|
|
|
|
_, err = store.query.New().
|
|
Table(store.getAssistantTable()).
|
|
Where("assistant_id", assistantID).
|
|
Delete()
|
|
return err
|
|
}
|
|
|
|
// GetAssistants retrieves assistants with pagination and filtering
|
|
func (store *Xun) GetAssistants(filter types.AssistantFilter, locale ...string) (*types.AssistantList, error) {
|
|
qb := store.query.New().
|
|
Table(store.getAssistantTable())
|
|
|
|
// Apply tag filter if provided
|
|
if len(filter.Tags) > 0 {
|
|
qb.Where(func(qb query.Query) {
|
|
for i, tag := range filter.Tags {
|
|
val := store.jsonContainsValue(fmt.Sprintf("%%\"%s\"%%", tag))
|
|
if i == 0 {
|
|
qb.WhereJSONContains("tags", val)
|
|
} else {
|
|
qb.OrWhereJSONContains("tags", val)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// Apply keyword filter if provided
|
|
if filter.Keywords != "" {
|
|
kw := fmt.Sprintf("%%%s%%", filter.Keywords)
|
|
qb.Where(func(qb query.Query) {
|
|
qb.Where("name", "like", kw).
|
|
OrWhere("description", "like", kw).
|
|
OrWhere("capabilities", "like", kw)
|
|
localeVal := store.jsonContainsValue(kw)
|
|
qb.OrWhereJSONContains("locales", localeVal)
|
|
})
|
|
}
|
|
|
|
// Apply type filter if provided (single value)
|
|
if filter.Type != "" {
|
|
qb.Where("type", filter.Type)
|
|
}
|
|
|
|
// Apply types filter if provided (multiple values, IN query)
|
|
if len(filter.Types) > 0 {
|
|
qb.WhereIn("type", filter.Types)
|
|
}
|
|
|
|
// Apply connector filter if provided
|
|
if filter.Connector != "" {
|
|
qb.Where("connector", filter.Connector)
|
|
}
|
|
|
|
// Apply assistant_id filter if provided
|
|
if filter.AssistantID != "" {
|
|
qb.Where("assistant_id", filter.AssistantID)
|
|
}
|
|
|
|
// Apply assistantIDs filter if provided
|
|
if len(filter.AssistantIDs) > 0 {
|
|
qb.WhereIn("assistant_id", filter.AssistantIDs)
|
|
}
|
|
|
|
// Apply mentionable filter if provided
|
|
if filter.Mentionable != nil {
|
|
qb.Where("mentionable", *filter.Mentionable)
|
|
}
|
|
|
|
// Apply automated filter if provided
|
|
if filter.Automated != nil {
|
|
qb.Where("automated", *filter.Automated)
|
|
}
|
|
|
|
// Apply built_in filter if provided
|
|
if filter.BuiltIn != nil {
|
|
qb.Where("built_in", *filter.BuiltIn)
|
|
}
|
|
|
|
// Apply sandbox filter (true = has sandbox config, false = no sandbox config)
|
|
// DB JSON columns distinguish between SQL NULL and JSON literal null.
|
|
// Dialect-specific: MySQL uses CAST(... AS CHAR), PG uses ::text, SQLite uses CAST(... AS TEXT).
|
|
if filter.Sandbox != nil {
|
|
notNull, isNull := store.sandboxRawSQL()
|
|
if *filter.Sandbox {
|
|
qb.WhereNotNull("sandbox").WhereRaw(notNull)
|
|
} else {
|
|
qb.Where(func(qb query.Query) {
|
|
qb.WhereNull("sandbox").OrWhereRaw(isNull)
|
|
})
|
|
}
|
|
}
|
|
|
|
// Apply custom query filter function (for permission filtering)
|
|
if filter.QueryFilter != nil {
|
|
qb.Where(filter.QueryFilter)
|
|
}
|
|
|
|
// Set defaults for pagination
|
|
if filter.PageSize <= 0 {
|
|
filter.PageSize = 20
|
|
}
|
|
if filter.Page <= 0 {
|
|
filter.Page = 1
|
|
}
|
|
|
|
// Get total count
|
|
total, err := qb.Clone().Count()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Calculate pagination
|
|
offset := (filter.Page - 1) * filter.PageSize
|
|
totalPages := int(math.Ceil(float64(total) / float64(filter.PageSize)))
|
|
nextPage := filter.Page + 1
|
|
if nextPage > totalPages {
|
|
nextPage = 0
|
|
}
|
|
prevPage := filter.Page - 1
|
|
if prevPage < 1 {
|
|
prevPage = 0
|
|
}
|
|
|
|
// Apply select fields with security validation (only if fields are explicitly specified)
|
|
if len(filter.Select) > 0 {
|
|
// ValidateAssistantFields will validate fields against whitelist
|
|
sanitized := types.ValidateAssistantFields(filter.Select)
|
|
selectFields := make([]interface{}, len(sanitized))
|
|
for i, field := range sanitized {
|
|
selectFields[i] = field
|
|
}
|
|
qb.Select(selectFields...)
|
|
}
|
|
// If no select fields specified, query will return all fields (SELECT *)
|
|
|
|
// Get paginated results
|
|
rows, err := qb.OrderBy("sort", "asc").
|
|
OrderBy("updated_at", "desc").
|
|
Offset(offset).
|
|
Limit(filter.PageSize).
|
|
Get()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Convert rows to types.AssistantModel slice
|
|
assistants := make([]*types.AssistantModel, 0, len(rows))
|
|
jsonFields := []string{"tags", "options", "prompts", "prompt_presets", "connector_options", "workflow", "sandbox", "kb", "mcp", "placeholder", "locales", "uses", "search", "dependencies"}
|
|
|
|
for _, row := range rows {
|
|
data := row.ToMap()
|
|
if data == nil {
|
|
continue
|
|
}
|
|
|
|
// Parse JSON fields
|
|
store.parseJSONFields(data, jsonFields)
|
|
|
|
// Convert map to types.AssistantModel using existing helper function
|
|
model, err := types.ToAssistantModel(data)
|
|
if err != nil {
|
|
log.Error("Failed to convert row to types.AssistantModel: %s", err.Error())
|
|
continue
|
|
}
|
|
|
|
// Apply i18n translations if locale is provided
|
|
if len(locale) > 0 && locale[0] != "" && model != nil {
|
|
store.translate(model, model.ID, locale[0])
|
|
}
|
|
|
|
assistants = append(assistants, model)
|
|
}
|
|
|
|
return &types.AssistantList{
|
|
Data: assistants,
|
|
Page: filter.Page,
|
|
PageSize: filter.PageSize,
|
|
PageCount: totalPages,
|
|
Next: nextPage,
|
|
Prev: prevPage,
|
|
Total: int(total),
|
|
}, nil
|
|
}
|
|
|
|
// GetAssistant retrieves a single assistant by ID
|
|
func (store *Xun) GetAssistant(assistantID string, fields []string, locale ...string) (*types.AssistantModel, error) {
|
|
qb := store.query.New().
|
|
Table(store.getAssistantTable()).
|
|
Where("assistant_id", assistantID)
|
|
|
|
// Apply select fields with security validation
|
|
// If no fields specified, use default fields
|
|
fieldsToSelect := fields
|
|
if len(fieldsToSelect) == 0 {
|
|
fieldsToSelect = types.AssistantDefaultFields
|
|
}
|
|
|
|
// ValidateAssistantFields will validate fields against whitelist
|
|
sanitized := types.ValidateAssistantFields(fieldsToSelect)
|
|
selectFields := make([]interface{}, len(sanitized))
|
|
for i, field := range sanitized {
|
|
selectFields[i] = field
|
|
}
|
|
qb.Select(selectFields...)
|
|
|
|
row, err := qb.First()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if row == nil {
|
|
return nil, fmt.Errorf("assistant %s not found", assistantID)
|
|
}
|
|
|
|
data := row.ToMap()
|
|
if len(data) == 0 {
|
|
return nil, fmt.Errorf("the assistant %s is empty", assistantID)
|
|
}
|
|
|
|
// Parse JSON fields
|
|
jsonFields := []string{"tags", "modes", "options", "prompts", "prompt_presets", "connector_options", "workflow", "sandbox", "kb", "db", "mcp", "placeholder", "locales", "uses", "search", "dependencies"}
|
|
store.parseJSONFields(data, jsonFields)
|
|
|
|
// Convert map to types.AssistantModel
|
|
model := &types.AssistantModel{
|
|
ID: getString(data, "assistant_id"),
|
|
Type: getString(data, "type"),
|
|
Name: getString(data, "name"),
|
|
Avatar: getString(data, "avatar"),
|
|
Connector: getString(data, "connector"),
|
|
Path: getString(data, "path"),
|
|
Source: getString(data, "source"),
|
|
BuiltIn: getBool(data, "built_in"),
|
|
Sort: getInt(data, "sort"),
|
|
Description: getString(data, "description"),
|
|
Capabilities: getString(data, "capabilities"),
|
|
DefaultMode: getString(data, "default_mode"),
|
|
Readonly: getBool(data, "readonly"),
|
|
Public: getBool(data, "public"),
|
|
Share: getString(data, "share"),
|
|
Mentionable: getBool(data, "mentionable"),
|
|
Automated: getBool(data, "automated"),
|
|
DisableGlobalPrompts: getBool(data, "disable_global_prompts"),
|
|
CreatedAt: getInt64(data, "created_at"),
|
|
UpdatedAt: getInt64(data, "updated_at"),
|
|
YaoCreatedBy: getString(data, "__yao_created_by"),
|
|
YaoUpdatedBy: getString(data, "__yao_updated_by"),
|
|
YaoTeamID: getString(data, "__yao_team_id"),
|
|
YaoTenantID: getString(data, "__yao_tenant_id"),
|
|
}
|
|
|
|
// Handle Tags
|
|
if tags, ok := data["tags"].([]interface{}); ok {
|
|
model.Tags = make([]string, len(tags))
|
|
for i, tag := range tags {
|
|
if s, ok := tag.(string); ok {
|
|
model.Tags[i] = s
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle Modes
|
|
if modes, ok := data["modes"].([]interface{}); ok {
|
|
model.Modes = make([]string, len(modes))
|
|
for i, mode := range modes {
|
|
if s, ok := mode.(string); ok {
|
|
model.Modes[i] = s
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle Options
|
|
if options, ok := data["options"].(map[string]interface{}); ok {
|
|
model.Options = options
|
|
}
|
|
|
|
// Handle typed fields with conversion
|
|
if prompts, has := data["prompts"]; has && prompts != nil {
|
|
// Try to unmarshal to []Prompt
|
|
raw, err := jsoniter.Marshal(prompts)
|
|
if err == nil {
|
|
var p []types.Prompt
|
|
if err := jsoniter.Unmarshal(raw, &p); err == nil {
|
|
model.Prompts = p
|
|
}
|
|
}
|
|
}
|
|
|
|
if promptPresets, has := data["prompt_presets"]; has && promptPresets != nil {
|
|
raw, err := jsoniter.Marshal(promptPresets)
|
|
if err == nil {
|
|
var pp map[string][]types.Prompt
|
|
if err := jsoniter.Unmarshal(raw, &pp); err == nil {
|
|
model.PromptPresets = pp
|
|
}
|
|
}
|
|
}
|
|
|
|
if connectorOptions, has := data["connector_options"]; has && connectorOptions != nil {
|
|
raw, err := jsoniter.Marshal(connectorOptions)
|
|
if err == nil {
|
|
var co types.ConnectorOptions
|
|
if err := jsoniter.Unmarshal(raw, &co); err == nil {
|
|
model.ConnectorOptions = &co
|
|
}
|
|
}
|
|
}
|
|
|
|
if kb, has := data["kb"]; has && kb != nil {
|
|
kbConverted, err := types.ToKnowledgeBase(kb)
|
|
if err == nil {
|
|
model.KB = kbConverted
|
|
}
|
|
}
|
|
|
|
if db, has := data["db"]; has && db != nil {
|
|
dbConverted, err := types.ToDatabase(db)
|
|
if err == nil {
|
|
model.DB = dbConverted
|
|
}
|
|
}
|
|
|
|
if mcp, has := data["mcp"]; has && mcp != nil {
|
|
mcpConverted, err := types.ToMCPServers(mcp)
|
|
if err == nil {
|
|
model.MCP = mcpConverted
|
|
}
|
|
}
|
|
|
|
if workflow, has := data["workflow"]; has && workflow != nil {
|
|
wf, err := types.ToWorkflow(workflow)
|
|
if err == nil {
|
|
model.Workflow = wf
|
|
}
|
|
}
|
|
|
|
if sandbox, has := data["sandbox"]; has && sandbox != nil {
|
|
sb, err := types.ToSandbox(sandbox)
|
|
if err == nil {
|
|
model.Sandbox = sb
|
|
}
|
|
}
|
|
|
|
if placeholder, has := data["placeholder"]; has && placeholder != nil {
|
|
raw, err := jsoniter.Marshal(placeholder)
|
|
if err == nil {
|
|
var ph types.Placeholder
|
|
if err := jsoniter.Unmarshal(raw, &ph); err == nil {
|
|
model.Placeholder = &ph
|
|
}
|
|
}
|
|
}
|
|
|
|
if locales, has := data["locales"]; has && locales != nil {
|
|
raw, err := jsoniter.Marshal(locales)
|
|
if err == nil {
|
|
var loc i18n.Map
|
|
if err := jsoniter.Unmarshal(raw, &loc); err == nil {
|
|
model.Locales = loc
|
|
}
|
|
}
|
|
}
|
|
|
|
if uses, has := data["uses"]; has && uses != nil {
|
|
raw, err := jsoniter.Marshal(uses)
|
|
if err == nil {
|
|
var u context.Uses
|
|
if err := jsoniter.Unmarshal(raw, &u); err == nil {
|
|
model.Uses = &u
|
|
}
|
|
}
|
|
}
|
|
|
|
if search, has := data["search"]; has && search != nil {
|
|
raw, err := jsoniter.Marshal(search)
|
|
if err == nil {
|
|
var s searchTypes.Config
|
|
if err := jsoniter.Unmarshal(raw, &s); err == nil {
|
|
model.Search = &s
|
|
}
|
|
}
|
|
}
|
|
|
|
if deps, has := data["dependencies"]; has && deps != nil {
|
|
raw, err := jsoniter.Marshal(deps)
|
|
if err == nil {
|
|
var d map[string]string
|
|
if err := jsoniter.Unmarshal(raw, &d); err == nil {
|
|
model.Dependencies = d
|
|
}
|
|
}
|
|
}
|
|
|
|
// Apply i18n translation if locale is provided
|
|
if len(locale) > 0 && locale[0] != "" {
|
|
store.translate(model, assistantID, locale[0])
|
|
}
|
|
|
|
return model, nil
|
|
}
|
|
|
|
// DeleteAssistants deletes assistants based on filter conditions
|
|
func (store *Xun) DeleteAssistants(filter types.AssistantFilter) (int64, error) {
|
|
qb := store.query.New().
|
|
Table(store.getAssistantTable())
|
|
|
|
// Apply tag filter if provided
|
|
if len(filter.Tags) > 0 {
|
|
qb.Where(func(qb query.Query) {
|
|
for i, tag := range filter.Tags {
|
|
val := store.jsonContainsValue(fmt.Sprintf("%%\"%s\"%%", tag))
|
|
if i == 0 {
|
|
qb.WhereJSONContains("tags", val)
|
|
} else {
|
|
qb.OrWhereJSONContains("tags", val)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// Apply keyword filter if provided
|
|
if filter.Keywords != "" {
|
|
kw := fmt.Sprintf("%%%s%%", filter.Keywords)
|
|
qb.Where(func(qb query.Query) {
|
|
qb.Where("name", "like", kw).
|
|
OrWhere("description", "like", kw)
|
|
})
|
|
}
|
|
|
|
// Apply connector filter if provided
|
|
if filter.Connector != "" {
|
|
qb.Where("connector", filter.Connector)
|
|
}
|
|
|
|
// Apply assistant_id filter if provided
|
|
if filter.AssistantID != "" {
|
|
qb.Where("assistant_id", filter.AssistantID)
|
|
}
|
|
|
|
// Apply assistantIDs filter if provided
|
|
if len(filter.AssistantIDs) > 0 {
|
|
qb.WhereIn("assistant_id", filter.AssistantIDs)
|
|
}
|
|
|
|
// Apply mentionable filter if provided
|
|
if filter.Mentionable != nil {
|
|
qb.Where("mentionable", *filter.Mentionable)
|
|
}
|
|
|
|
// Apply automated filter if provided
|
|
if filter.Automated != nil {
|
|
qb.Where("automated", *filter.Automated)
|
|
}
|
|
|
|
// Apply built_in filter if provided
|
|
if filter.BuiltIn != nil {
|
|
qb.Where("built_in", *filter.BuiltIn)
|
|
}
|
|
|
|
// Execute delete and return number of deleted records
|
|
return qb.Delete()
|
|
}
|
|
|
|
// GetAssistantTags retrieves all unique tags from assistants with filtering
|
|
func (store *Xun) GetAssistantTags(filter types.AssistantFilter, locale ...string) ([]types.Tag, error) {
|
|
qb := store.query.New().Table(store.getAssistantTable())
|
|
|
|
// Apply type filter
|
|
if len(filter.Types) > 0 {
|
|
qb.WhereIn("type", filter.Types)
|
|
} else if filter.Type != "" {
|
|
qb.Where("type", filter.Type)
|
|
}
|
|
|
|
// Apply custom query filter function (for permission filtering)
|
|
if filter.QueryFilter != nil {
|
|
qb.Where(filter.QueryFilter)
|
|
}
|
|
|
|
// Apply other filters if provided
|
|
if filter.Connector != "" {
|
|
qb.Where("connector", filter.Connector)
|
|
}
|
|
|
|
if filter.BuiltIn != nil {
|
|
qb.Where("built_in", *filter.BuiltIn)
|
|
}
|
|
|
|
if filter.Mentionable != nil {
|
|
qb.Where("mentionable", *filter.Mentionable)
|
|
}
|
|
|
|
if filter.Automated != nil {
|
|
qb.Where("automated", *filter.Automated)
|
|
}
|
|
|
|
// Apply keyword filter if provided
|
|
if filter.Keywords != "" {
|
|
qb.Where(func(qb query.Query) {
|
|
qb.Where("name", "like", fmt.Sprintf("%%%s%%", filter.Keywords)).
|
|
OrWhere("description", "like", fmt.Sprintf("%%%s%%", filter.Keywords))
|
|
})
|
|
}
|
|
|
|
rows, err := qb.Select("tags").GroupBy("tags").Get()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tagSet := map[string]bool{}
|
|
for _, row := range rows {
|
|
if tags, ok := row["tags"].(string); ok && tags != "" {
|
|
var tagList []string
|
|
if err := jsoniter.UnmarshalFromString(tags, &tagList); err == nil {
|
|
for _, tag := range tagList {
|
|
tagSet[tag] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
lang := "en"
|
|
if len(locale) > 0 {
|
|
lang = locale[0]
|
|
}
|
|
|
|
// Convert map keys to slice
|
|
tags := make([]types.Tag, 0, len(tagSet))
|
|
for tag := range tagSet {
|
|
tags = append(tags, types.Tag{
|
|
Value: tag,
|
|
Label: i18n.TranslateGlobal(lang, tag).(string),
|
|
})
|
|
}
|
|
return tags, nil
|
|
}
|
|
|
|
// translate applies i18n translation to assistant model fields
|
|
func (store *Xun) translate(model *types.AssistantModel, assistantID string, locale string) {
|
|
if model == nil {
|
|
return
|
|
}
|
|
|
|
// Translate name
|
|
if translated := i18n.Translate(assistantID, locale, model.Name); translated != nil {
|
|
if s, ok := translated.(string); ok {
|
|
model.Name = s
|
|
}
|
|
}
|
|
|
|
// Translate description
|
|
if translated := i18n.Translate(assistantID, locale, model.Description); translated != nil {
|
|
if s, ok := translated.(string); ok {
|
|
model.Description = s
|
|
}
|
|
}
|
|
|
|
// Translate capabilities
|
|
if translated := i18n.Translate(assistantID, locale, model.Capabilities); translated != nil {
|
|
if s, ok := translated.(string); ok {
|
|
model.Capabilities = s
|
|
}
|
|
}
|
|
|
|
// Translate prompts
|
|
if model.Prompts != nil {
|
|
for i := range model.Prompts {
|
|
if translated := i18n.Translate(assistantID, locale, model.Prompts[i].Name); translated != nil {
|
|
if s, ok := translated.(string); ok {
|
|
model.Prompts[i].Name = s
|
|
}
|
|
}
|
|
if translated := i18n.Translate(assistantID, locale, model.Prompts[i].Content); translated != nil {
|
|
if s, ok := translated.(string); ok {
|
|
model.Prompts[i].Content = s
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Translate placeholder
|
|
if model.Placeholder != nil {
|
|
if translated := i18n.Translate(assistantID, locale, model.Placeholder.Title); translated != nil {
|
|
if s, ok := translated.(string); ok {
|
|
model.Placeholder.Title = s
|
|
}
|
|
}
|
|
if translated := i18n.Translate(assistantID, locale, model.Placeholder.Description); translated != nil {
|
|
if s, ok := translated.(string); ok {
|
|
model.Placeholder.Description = s
|
|
}
|
|
}
|
|
if translated := i18n.Translate(assistantID, locale, model.Placeholder.Prompts); translated != nil {
|
|
if prompts, ok := translated.([]string); ok {
|
|
model.Placeholder.Prompts = prompts
|
|
}
|
|
}
|
|
}
|
|
|
|
// Tags are NOT translated — they serve as filter keys and must remain
|
|
// in their original (English) form so that filter.tags round-trips
|
|
// correctly through the LIKE query on the DB column.
|
|
}
|
|
|
|
// sandboxRawSQL returns dialect-specific raw SQL fragments for sandbox JSON null detection.
|
|
// Returns (notNullExpr, isNullExpr) for filtering sandbox field.
|
|
// WhereRaw is used here because this is a JSON literal `null` comparison, not a JSON array
|
|
// contains query. Each dialect requires different casting to compare the JSON value as text.
|
|
// No bind parameters are needed (pure string comparison), so no placeholder issues.
|
|
func (store *Xun) sandboxRawSQL() (string, string) {
|
|
switch store.getDriver() {
|
|
case "postgres":
|
|
return `"sandbox"::text <> 'null'`, `"sandbox"::text = 'null'`
|
|
case "sqlite3":
|
|
return `CAST(sandbox AS TEXT) <> 'null'`, `CAST(sandbox AS TEXT) = 'null'`
|
|
default:
|
|
return "CAST(`sandbox` AS CHAR) <> 'null'", "CAST(`sandbox` AS CHAR) = 'null'"
|
|
}
|
|
}
|