mirror of
https://github.com/coaidev/coai.git
synced 2025-05-19 21:10:18 +09:00
feat: add dify channel
This commit is contained in:
parent
1856dd0312
commit
628b0ba8d2
@ -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
|
||||
|
163
adapter/dify/chat.go
Normal file
163
adapter/dify/chat.go
Normal file
@ -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
|
||||
}
|
67
adapter/dify/processor.go
Normal file
67
adapter/dify/processor.go
Normal file
@ -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
|
||||
}
|
66
adapter/dify/struct.go
Normal file
66
adapter/dify/struct.go
Normal file
@ -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"`
|
||||
}
|
@ -67,6 +67,7 @@ export const ChannelTypes: Record<string, string> = {
|
||||
bing: "New Bing",
|
||||
slack: "Slack Claude",
|
||||
deepseek: "深度求索 DeepSeek",
|
||||
dify: "Dify",
|
||||
};
|
||||
|
||||
export const ShortChannelTypes: Record<string, string> = {
|
||||
@ -87,6 +88,7 @@ export const ShortChannelTypes: Record<string, string> = {
|
||||
bing: "Bing",
|
||||
slack: "Slack",
|
||||
deepseek: "深度求索",
|
||||
dify: "Dify",
|
||||
};
|
||||
|
||||
export const ChannelInfos: Record<string, ChannelInfo> = {
|
||||
@ -286,6 +288,15 @@ export const ChannelInfos: Record<string, ChannelInfo> = {
|
||||
format: "<api-key>",
|
||||
models: ["llama2-70b-4096", "mixtral-8x7b-32768", "gemma-7b-it"],
|
||||
},
|
||||
dify: {
|
||||
endpoint: "https://api.dify.ai/v1",
|
||||
format: "<api-key>",
|
||||
models: [""],
|
||||
description:
|
||||
"> 由于 Dify 平台一个 Key 对应一个 CHATFLOW (模型),所以模型名称仅在用户调用本系统时用于标识用户调用的对象,不代表调用 Dify 平台 CHATFLOW 时被调用 CHATFLOW 的名称 \n" +
|
||||
"> 因此,您需要为每一个 Dify 平台的 CHATFLOW 分别创建渠道 \n" +
|
||||
"> 如果需要让系统自动适配 Dify 平台的图标(商业版 / Pro),请将模型名称填写为 **dify** 开头的模型,如 **dify-chat** \n",
|
||||
},
|
||||
};
|
||||
|
||||
export const defaultChannelModels: string[] = getUniqueList(
|
||||
|
@ -78,6 +78,9 @@ export const modelColorMapper: Record<string, string> = {
|
||||
doubao: "sky-300",
|
||||
coze: "sky-300",
|
||||
|
||||
// Dify
|
||||
dify: "gray-300",
|
||||
|
||||
// OpenRouter
|
||||
openrouter: "purple-600",
|
||||
};
|
||||
|
@ -26,6 +26,7 @@ const (
|
||||
MoonshotChannelType = "moonshot"
|
||||
GroqChannelType = "groq"
|
||||
DeepseekChannelType = "deepseek"
|
||||
DifyChannelType = "dify"
|
||||
)
|
||||
|
||||
const (
|
||||
|
Loading…
Reference in New Issue
Block a user