diff --git a/adapter/adapter.go b/adapter/adapter.go index d73abc4..78ee57b 100644 --- a/adapter/adapter.go +++ b/adapter/adapter.go @@ -8,6 +8,7 @@ import ( adaptercommon "chat/adapter/common" "chat/adapter/dashscope" "chat/adapter/deepseek" + "chat/adapter/dify" "chat/adapter/hunyuan" "chat/adapter/midjourney" "chat/adapter/openai" @@ -37,6 +38,7 @@ var channelFactories = map[string]adaptercommon.FactoryCreator{ globals.ZhinaoChannelType: zhinao.NewChatInstanceFromConfig, globals.MidjourneyChannelType: midjourney.NewChatInstanceFromConfig, globals.DeepseekChannelType: deepseek.NewChatInstanceFromConfig, + globals.DifyChannelType: dify.NewChatInstanceFromConfig, globals.MoonshotChannelType: openai.NewChatInstanceFromConfig, // openai format globals.GroqChannelType: openai.NewChatInstanceFromConfig, // openai format diff --git a/adapter/dify/chat.go b/adapter/dify/chat.go new file mode 100644 index 0000000..41174a4 --- /dev/null +++ b/adapter/dify/chat.go @@ -0,0 +1,163 @@ +package dify + +import ( + adaptercommon "chat/adapter/common" + "chat/globals" + "chat/utils" + "encoding/json" + "errors" + "fmt" + "strings" + "time" +) + +type ChatInstance struct { + Endpoint string + ApiKey string + responseComplete bool +} + +func (c *ChatInstance) GetEndpoint() string { + return c.Endpoint +} + +func (c *ChatInstance) GetApiKey() string { + return c.ApiKey +} + +func (c *ChatInstance) GetHeader() map[string]string { + return map[string]string{ + "Content-Type": "application/json", + "Authorization": fmt.Sprintf("Bearer %s", c.GetApiKey()), + } +} + +func NewChatInstance(endpoint, apiKey string) *ChatInstance { + return &ChatInstance{ + Endpoint: endpoint, + ApiKey: apiKey, + } +} + +func NewChatInstanceFromConfig(conf globals.ChannelConfig) adaptercommon.Factory { + return NewChatInstance( + conf.GetEndpoint(), + conf.GetRandomSecret(), + ) +} + +func (c *ChatInstance) GetChatEndpoint() string { + return fmt.Sprintf("%s/chat-messages", c.GetEndpoint()) +} + +func (c *ChatInstance) GetChatBody(props *adaptercommon.ChatProps, stream bool) interface{} { + timestamp := time.Now().UnixNano() + userID := fmt.Sprintf("user_%d", timestamp) + + query := "" + for _, msg := range props.Message { + if msg.Role == "user" { + query = msg.Content + break + } + } + + return ChatRequest{ + Inputs: map[string]interface{}{}, + Query: query, + ResponseMode: "streaming", + User: userID, + AutoGenerateName: true, + } +} + +func (c *ChatInstance) ProcessLine(data string) (string, error) { + if c.responseComplete { + return "", nil + } + + if data == "" { + return "", nil + } + + chunk, complete, err := processStreamResponse(data) + if err != nil { + return "", err + } + + if complete { + c.responseComplete = true + } + + return chunk.Content, nil +} + +func (c *ChatInstance) CreateChatRequest(props *adaptercommon.ChatProps) (string, error) { + res, err := utils.Post( + c.GetChatEndpoint(), + c.GetHeader(), + c.GetChatBody(props, false), + props.Proxy, + ) + + if err != nil || res == nil { + return "", fmt.Errorf("dify error: %s", err.Error()) + } + + responseBody := utils.Marshal(res) + response := processChatResponse(responseBody) + if response == nil { + return "", fmt.Errorf("dify error: cannot parse response") + } + + return response.Answer, nil +} + +func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, callback globals.Hook) error { + c.responseComplete = false + + err := utils.EventScanner(&utils.EventScannerProps{ + Method: "POST", + Uri: c.GetChatEndpoint(), + Headers: c.GetHeader(), + Body: c.GetChatBody(props, true), + Callback: func(data string) error { + partial, err := c.ProcessLine(data) + if err != nil { + return err + } + + if partial != "" { + err = callback(&globals.Chunk{Content: partial}) + if err != nil { + return err + } + } + return nil + }, + }, props.Proxy) + + c.responseComplete = true + + if err != nil { + if strings.Contains(err.Body, "\"code\":") { + errorResp := processChatErrorResponse(err.Body) + if errorResp != nil { + return errors.New(fmt.Sprintf("dify error: %s (code: %s)", errorResp.Message, errorResp.Code)) + } + + var genericResp map[string]interface{} + if jsonErr := json.Unmarshal([]byte(err.Body), &genericResp); jsonErr == nil { + errMsg, _ := json.Marshal(genericResp) + return errors.New(fmt.Sprintf("dify error: %s", string(errMsg))) + } + } + + if err.Error != nil { + return err.Error + } + return errors.New(fmt.Sprintf("dify error: unexpected error in stream request")) + } + + return nil +} diff --git a/adapter/dify/processor.go b/adapter/dify/processor.go new file mode 100644 index 0000000..946dd47 --- /dev/null +++ b/adapter/dify/processor.go @@ -0,0 +1,67 @@ +package dify + +import ( + "chat/globals" + "chat/utils" + "errors" + "fmt" +) + +func processChatResponse(data string) *ChatResponse { + if form := utils.UnmarshalForm[ChatResponse](data); form != nil { + return form + } + return nil +} + +func processChatStreamResponse(data string) *ChatStreamResponse { + if form := utils.UnmarshalForm[ChatStreamResponse](data); form != nil { + return form + } + return nil +} + +func processChatErrorResponse(data string) *ChatStreamErrorResponse { + if form := utils.UnmarshalForm[ChatStreamErrorResponse](data); form != nil { + return form + } + return nil +} + +func processStreamResponse(data string) (*globals.Chunk, bool, error) { + if data == "" { + return &globals.Chunk{Content: ""}, false, nil + } + + streamData := processChatStreamResponse(data) + if streamData == nil { + return &globals.Chunk{Content: ""}, false, nil + } + + switch streamData.Event { + case "message": + if streamData.Answer != "" { + return &globals.Chunk{ + Content: streamData.Answer, + }, false, nil + } + case "message_end": + return &globals.Chunk{ + Content: "", + }, true, nil + case "error": + if streamData.Code != "" && streamData.Message != "" { + return nil, false, errors.New(fmt.Sprintf("dify error: %s (code: %s)", streamData.Message, streamData.Code)) + } + return nil, false, errors.New("dify error: conversation failed") + case "workflow_started", "node_started", "node_finished", "workflow_finished", "iteration_started", "iteration_next", "iteration_finished", "iteration_completed", "parallel_branch_started", "parallel_branch_finished", "ping": + return &globals.Chunk{Content: ""}, false, nil + } + + errorResp := processChatErrorResponse(data) + if errorResp != nil { + return nil, false, errors.New(fmt.Sprintf("dify error: %s (code: %s)", errorResp.Message, errorResp.Code)) + } + + return &globals.Chunk{Content: ""}, false, nil +} diff --git a/adapter/dify/struct.go b/adapter/dify/struct.go new file mode 100644 index 0000000..2ec649d --- /dev/null +++ b/adapter/dify/struct.go @@ -0,0 +1,66 @@ +package dify + +type ChatRequest struct { + Inputs map[string]interface{} `json:"inputs"` + Query string `json:"query"` + ResponseMode string `json:"response_mode"` + ConversationID string `json:"conversation_id,omitempty"` + User string `json:"user"` + Files []File `json:"files,omitempty"` + AutoGenerateName bool `json:"auto_generate_name,omitempty"` +} + +type File struct { + Type string `json:"type"` + TransferMethod string `json:"transfer_method"` + URL string `json:"url,omitempty"` + UploadFileID string `json:"upload_file_id,omitempty"` +} + +type ChatResponse struct { + MessageID string `json:"message_id"` + ConversationID string `json:"conversation_id"` + Mode string `json:"mode"` + Answer string `json:"answer"` + Metadata map[string]interface{} `json:"metadata"` + Usage Usage `json:"usage"` + RetrieverResources []RetrieverResource `json:"retriever_resources"` + CreatedAt int64 `json:"created_at"` +} + +type Usage struct { + TokenCount int `json:"token_count"` + OutputTokens int `json:"output_tokens"` + InputTokens int `json:"input_tokens"` +} + +type RetrieverResource struct { + SegmentID string `json:"segment_id"` + Content string `json:"content"` + Source string `json:"source"` +} + +type ChatStreamResponse struct { + Event string `json:"event"` + TaskID string `json:"task_id"` + MessageID string `json:"message_id,omitempty"` + ConversationID string `json:"conversation_id,omitempty"` + Answer string `json:"answer,omitempty"` + CreatedAt int64 `json:"created_at,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` + Usage *Usage `json:"usage,omitempty"` + RetrieverResources []RetrieverResource `json:"retriever_resources,omitempty"` + Audio string `json:"audio,omitempty"` + Status int `json:"status,omitempty"` + Code string `json:"code,omitempty"` + Message string `json:"message,omitempty"` +} + +type ChatStreamErrorResponse struct { + Event string `json:"event"` + TaskID string `json:"task_id"` + MessageID string `json:"message_id"` + Status int `json:"status"` + Code string `json:"code"` + Message string `json:"message"` +} diff --git a/app/src/admin/channel.ts b/app/src/admin/channel.ts index 98478fd..93c3e82 100644 --- a/app/src/admin/channel.ts +++ b/app/src/admin/channel.ts @@ -67,6 +67,7 @@ export const ChannelTypes: Record = { bing: "New Bing", slack: "Slack Claude", deepseek: "深度求索 DeepSeek", + dify: "Dify", }; export const ShortChannelTypes: Record = { @@ -87,6 +88,7 @@ export const ShortChannelTypes: Record = { bing: "Bing", slack: "Slack", deepseek: "深度求索", + dify: "Dify", }; export const ChannelInfos: Record = { @@ -286,6 +288,15 @@ export const ChannelInfos: Record = { format: "", models: ["llama2-70b-4096", "mixtral-8x7b-32768", "gemma-7b-it"], }, + dify: { + endpoint: "https://api.dify.ai/v1", + format: "", + models: [""], + description: + "> 由于 Dify 平台一个 Key 对应一个 CHATFLOW (模型),所以模型名称仅在用户调用本系统时用于标识用户调用的对象,不代表调用 Dify 平台 CHATFLOW 时被调用 CHATFLOW 的名称 \n" + + "> 因此,您需要为每一个 Dify 平台的 CHATFLOW 分别创建渠道 \n" + + "> 如果需要让系统自动适配 Dify 平台的图标(商业版 / Pro),请将模型名称填写为 **dify** 开头的模型,如 **dify-chat** \n", + }, }; export const defaultChannelModels: string[] = getUniqueList( diff --git a/app/src/admin/colors.ts b/app/src/admin/colors.ts index b409f47..24e0dea 100644 --- a/app/src/admin/colors.ts +++ b/app/src/admin/colors.ts @@ -78,6 +78,9 @@ export const modelColorMapper: Record = { doubao: "sky-300", coze: "sky-300", + // Dify + dify: "gray-300", + // OpenRouter openrouter: "purple-600", }; diff --git a/globals/constant.go b/globals/constant.go index 39ca3db..fb380ea 100644 --- a/globals/constant.go +++ b/globals/constant.go @@ -26,6 +26,7 @@ const ( MoonshotChannelType = "moonshot" GroqChannelType = "groq" DeepseekChannelType = "deepseek" + DifyChannelType = "dify" ) const (