添加完整项目文件
包含Go API项目的所有源代码、配置文件、Docker配置、文档和前端资源 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
115
relay/channel/dify/adaptor.go
Normal file
115
relay/channel/dify/adaptor.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package dify
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"one-api/dto"
|
||||
"one-api/relay/channel"
|
||||
relaycommon "one-api/relay/common"
|
||||
"one-api/types"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
BotTypeChatFlow = 1 // chatflow default
|
||||
BotTypeAgent = 2
|
||||
BotTypeWorkFlow = 3
|
||||
BotTypeCompletion = 4
|
||||
)
|
||||
|
||||
type Adaptor struct {
|
||||
BotType int
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) {
|
||||
//TODO implement me
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) {
|
||||
//TODO implement me
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
|
||||
//if strings.HasPrefix(info.UpstreamModelName, "agent") {
|
||||
// a.BotType = BotTypeAgent
|
||||
//} else if strings.HasPrefix(info.UpstreamModelName, "workflow") {
|
||||
// a.BotType = BotTypeWorkFlow
|
||||
//} else if strings.HasPrefix(info.UpstreamModelName, "chat") {
|
||||
// a.BotType = BotTypeCompletion
|
||||
//} else {
|
||||
//}
|
||||
a.BotType = BotTypeChatFlow
|
||||
|
||||
}
|
||||
|
||||
func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
||||
switch a.BotType {
|
||||
case BotTypeWorkFlow:
|
||||
return fmt.Sprintf("%s/v1/workflows/run", info.BaseUrl), nil
|
||||
case BotTypeCompletion:
|
||||
return fmt.Sprintf("%s/v1/completion-messages", info.BaseUrl), nil
|
||||
case BotTypeAgent:
|
||||
fallthrough
|
||||
default:
|
||||
return fmt.Sprintf("%s/v1/chat-messages", info.BaseUrl), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error {
|
||||
channel.SetupApiRequestHeader(info, c, req)
|
||||
req.Set("Authorization", "Bearer "+info.ApiKey)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) {
|
||||
if request == nil {
|
||||
return nil, errors.New("request is nil")
|
||||
}
|
||||
return requestOpenAI2Dify(c, info, *request), nil
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dto.RerankRequest) (any, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) {
|
||||
//TODO implement me
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertOpenAIResponsesRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.OpenAIResponsesRequest) (any, error) {
|
||||
// TODO implement me
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) {
|
||||
return channel.DoApiRequest(a, c, info, requestBody)
|
||||
}
|
||||
|
||||
func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
|
||||
if info.IsStream {
|
||||
return difyStreamHandler(c, info, resp)
|
||||
} else {
|
||||
return difyHandler(c, info, resp)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (a *Adaptor) GetModelList() []string {
|
||||
return ModelList
|
||||
}
|
||||
|
||||
func (a *Adaptor) GetChannelName() string {
|
||||
return ChannelName
|
||||
}
|
||||
5
relay/channel/dify/constants.go
Normal file
5
relay/channel/dify/constants.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package dify
|
||||
|
||||
var ModelList []string
|
||||
|
||||
var ChannelName = "dify"
|
||||
45
relay/channel/dify/dto.go
Normal file
45
relay/channel/dify/dto.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package dify
|
||||
|
||||
import "one-api/dto"
|
||||
|
||||
type DifyChatRequest struct {
|
||||
Inputs map[string]interface{} `json:"inputs"`
|
||||
Query string `json:"query"`
|
||||
ResponseMode string `json:"response_mode"`
|
||||
User string `json:"user"`
|
||||
AutoGenerateName bool `json:"auto_generate_name"`
|
||||
Files []DifyFile `json:"files"`
|
||||
}
|
||||
|
||||
type DifyFile struct {
|
||||
Type string `json:"type"`
|
||||
TransferMode string `json:"transfer_mode"`
|
||||
URL string `json:"url,omitempty"`
|
||||
UploadFileId string `json:"upload_file_id,omitempty"`
|
||||
}
|
||||
|
||||
type DifyMetaData struct {
|
||||
Usage dto.Usage `json:"usage"`
|
||||
}
|
||||
|
||||
type DifyData struct {
|
||||
WorkflowId string `json:"workflow_id"`
|
||||
NodeId string `json:"node_id"`
|
||||
NodeType string `json:"node_type"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type DifyChatCompletionResponse struct {
|
||||
ConversationId string `json:"conversation_id"`
|
||||
Answer string `json:"answer"`
|
||||
CreateAt int64 `json:"create_at"`
|
||||
MetaData DifyMetaData `json:"metadata"`
|
||||
}
|
||||
|
||||
type DifyChunkChatCompletionResponse struct {
|
||||
Event string `json:"event"`
|
||||
ConversationId string `json:"conversation_id"`
|
||||
Answer string `json:"answer"`
|
||||
Data DifyData `json:"data"`
|
||||
MetaData DifyMetaData `json:"metadata"`
|
||||
}
|
||||
289
relay/channel/dify/relay-dify.go
Normal file
289
relay/channel/dify/relay-dify.go
Normal file
@@ -0,0 +1,289 @@
|
||||
package dify
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/constant"
|
||||
"one-api/dto"
|
||||
relaycommon "one-api/relay/common"
|
||||
"one-api/relay/helper"
|
||||
"one-api/service"
|
||||
"one-api/types"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func uploadDifyFile(c *gin.Context, info *relaycommon.RelayInfo, user string, media dto.MediaContent) *DifyFile {
|
||||
uploadUrl := fmt.Sprintf("%s/v1/files/upload", info.BaseUrl)
|
||||
switch media.Type {
|
||||
case dto.ContentTypeImageURL:
|
||||
// Decode base64 data
|
||||
imageMedia := media.GetImageMedia()
|
||||
base64Data := imageMedia.Url
|
||||
// Remove base64 prefix if exists (e.g., "data:image/jpeg;base64,")
|
||||
if idx := strings.Index(base64Data, ","); idx != -1 {
|
||||
base64Data = base64Data[idx+1:]
|
||||
}
|
||||
|
||||
// Decode base64 string
|
||||
decodedData, err := base64.StdEncoding.DecodeString(base64Data)
|
||||
if err != nil {
|
||||
common.SysError("failed to decode base64: " + err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create temporary file
|
||||
tempFile, err := os.CreateTemp("", "dify-upload-*")
|
||||
if err != nil {
|
||||
common.SysError("failed to create temp file: " + err.Error())
|
||||
return nil
|
||||
}
|
||||
defer tempFile.Close()
|
||||
defer os.Remove(tempFile.Name())
|
||||
|
||||
// Write decoded data to temp file
|
||||
if _, err := tempFile.Write(decodedData); err != nil {
|
||||
common.SysError("failed to write to temp file: " + err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create multipart form
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
|
||||
// Add user field
|
||||
if err := writer.WriteField("user", user); err != nil {
|
||||
common.SysError("failed to add user field: " + err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create form file with proper mime type
|
||||
mimeType := imageMedia.MimeType
|
||||
if mimeType == "" {
|
||||
mimeType = "image/jpeg" // default mime type
|
||||
}
|
||||
|
||||
// Create form file
|
||||
part, err := writer.CreateFormFile("file", fmt.Sprintf("image.%s", strings.TrimPrefix(mimeType, "image/")))
|
||||
if err != nil {
|
||||
common.SysError("failed to create form file: " + err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
// Copy file content to form
|
||||
if _, err = io.Copy(part, bytes.NewReader(decodedData)); err != nil {
|
||||
common.SysError("failed to copy file content: " + err.Error())
|
||||
return nil
|
||||
}
|
||||
writer.Close()
|
||||
|
||||
// Create HTTP request
|
||||
req, err := http.NewRequest("POST", uploadUrl, body)
|
||||
if err != nil {
|
||||
common.SysError("failed to create request: " + err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", info.ApiKey))
|
||||
|
||||
// Send request
|
||||
client := service.GetHttpClient()
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
common.SysError("failed to send request: " + err.Error())
|
||||
return nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Parse response
|
||||
var result struct {
|
||||
Id string `json:"id"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
common.SysError("failed to decode response: " + err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
return &DifyFile{
|
||||
UploadFileId: result.Id,
|
||||
Type: "image",
|
||||
TransferMode: "local_file",
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func requestOpenAI2Dify(c *gin.Context, info *relaycommon.RelayInfo, request dto.GeneralOpenAIRequest) *DifyChatRequest {
|
||||
difyReq := DifyChatRequest{
|
||||
Inputs: make(map[string]interface{}),
|
||||
AutoGenerateName: false,
|
||||
}
|
||||
|
||||
user := request.User
|
||||
if user == "" {
|
||||
user = helper.GetResponseID(c)
|
||||
}
|
||||
difyReq.User = user
|
||||
|
||||
files := make([]DifyFile, 0)
|
||||
var content strings.Builder
|
||||
for _, message := range request.Messages {
|
||||
if message.Role == "system" {
|
||||
content.WriteString("SYSTEM: \n" + message.StringContent() + "\n")
|
||||
} else if message.Role == "assistant" {
|
||||
content.WriteString("ASSISTANT: \n" + message.StringContent() + "\n")
|
||||
} else {
|
||||
parseContent := message.ParseContent()
|
||||
for _, mediaContent := range parseContent {
|
||||
switch mediaContent.Type {
|
||||
case dto.ContentTypeText:
|
||||
content.WriteString("USER: \n" + mediaContent.Text + "\n")
|
||||
case dto.ContentTypeImageURL:
|
||||
media := mediaContent.GetImageMedia()
|
||||
var file *DifyFile
|
||||
if media.IsRemoteImage() {
|
||||
file.Type = media.MimeType
|
||||
file.TransferMode = "remote_url"
|
||||
file.URL = media.Url
|
||||
} else {
|
||||
file = uploadDifyFile(c, info, difyReq.User, mediaContent)
|
||||
}
|
||||
if file != nil {
|
||||
files = append(files, *file)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
difyReq.Query = content.String()
|
||||
difyReq.Files = files
|
||||
mode := "blocking"
|
||||
if request.Stream {
|
||||
mode = "streaming"
|
||||
}
|
||||
difyReq.ResponseMode = mode
|
||||
return &difyReq
|
||||
}
|
||||
|
||||
func streamResponseDify2OpenAI(difyResponse DifyChunkChatCompletionResponse) *dto.ChatCompletionsStreamResponse {
|
||||
response := dto.ChatCompletionsStreamResponse{
|
||||
Object: "chat.completion.chunk",
|
||||
Created: common.GetTimestamp(),
|
||||
Model: "dify",
|
||||
}
|
||||
var choice dto.ChatCompletionsStreamResponseChoice
|
||||
if strings.HasPrefix(difyResponse.Event, "workflow_") {
|
||||
if constant.DifyDebug {
|
||||
text := "Workflow: " + difyResponse.Data.WorkflowId
|
||||
if difyResponse.Event == "workflow_finished" {
|
||||
text += " " + difyResponse.Data.Status
|
||||
}
|
||||
choice.Delta.SetReasoningContent(text + "\n")
|
||||
}
|
||||
} else if strings.HasPrefix(difyResponse.Event, "node_") {
|
||||
if constant.DifyDebug {
|
||||
text := "Node: " + difyResponse.Data.NodeType
|
||||
if difyResponse.Event == "node_finished" {
|
||||
text += " " + difyResponse.Data.Status
|
||||
}
|
||||
choice.Delta.SetReasoningContent(text + "\n")
|
||||
}
|
||||
} else if difyResponse.Event == "message" || difyResponse.Event == "agent_message" {
|
||||
if difyResponse.Answer == "<details style=\"color:gray;background-color: #f8f8f8;padding: 8px;border-radius: 4px;\" open> <summary> Thinking... </summary>\n" {
|
||||
difyResponse.Answer = "<think>"
|
||||
} else if difyResponse.Answer == "</details>" {
|
||||
difyResponse.Answer = "</think>"
|
||||
}
|
||||
|
||||
choice.Delta.SetContentString(difyResponse.Answer)
|
||||
}
|
||||
response.Choices = append(response.Choices, choice)
|
||||
return &response
|
||||
}
|
||||
|
||||
func difyStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) {
|
||||
var responseText string
|
||||
usage := &dto.Usage{}
|
||||
var nodeToken int
|
||||
helper.SetEventStreamHeaders(c)
|
||||
helper.StreamScannerHandler(c, resp, info, func(data string) bool {
|
||||
var difyResponse DifyChunkChatCompletionResponse
|
||||
err := json.Unmarshal([]byte(data), &difyResponse)
|
||||
if err != nil {
|
||||
common.SysError("error unmarshalling stream response: " + err.Error())
|
||||
return true
|
||||
}
|
||||
var openaiResponse dto.ChatCompletionsStreamResponse
|
||||
if difyResponse.Event == "message_end" {
|
||||
usage = &difyResponse.MetaData.Usage
|
||||
return false
|
||||
} else if difyResponse.Event == "error" {
|
||||
return false
|
||||
} else {
|
||||
openaiResponse = *streamResponseDify2OpenAI(difyResponse)
|
||||
if len(openaiResponse.Choices) != 0 {
|
||||
responseText += openaiResponse.Choices[0].Delta.GetContentString()
|
||||
if openaiResponse.Choices[0].Delta.ReasoningContent != nil {
|
||||
nodeToken += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
err = helper.ObjectData(c, openaiResponse)
|
||||
if err != nil {
|
||||
common.SysError(err.Error())
|
||||
}
|
||||
return true
|
||||
})
|
||||
helper.Done(c)
|
||||
if usage.TotalTokens == 0 {
|
||||
usage = service.ResponseText2Usage(responseText, info.UpstreamModelName, info.PromptTokens)
|
||||
}
|
||||
usage.CompletionTokens += nodeToken
|
||||
return usage, nil
|
||||
}
|
||||
|
||||
func difyHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) {
|
||||
var difyResponse DifyChatCompletionResponse
|
||||
responseBody, err := io.ReadAll(resp.Body)
|
||||
|
||||
if err != nil {
|
||||
return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
|
||||
}
|
||||
common.CloseResponseBodyGracefully(resp)
|
||||
err = json.Unmarshal(responseBody, &difyResponse)
|
||||
if err != nil {
|
||||
return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
|
||||
}
|
||||
fullTextResponse := dto.OpenAITextResponse{
|
||||
Id: difyResponse.ConversationId,
|
||||
Object: "chat.completion",
|
||||
Created: common.GetTimestamp(),
|
||||
Usage: difyResponse.MetaData.Usage,
|
||||
}
|
||||
choice := dto.OpenAITextResponseChoice{
|
||||
Index: 0,
|
||||
Message: dto.Message{
|
||||
Role: "assistant",
|
||||
Content: difyResponse.Answer,
|
||||
},
|
||||
FinishReason: "stop",
|
||||
}
|
||||
fullTextResponse.Choices = append(fullTextResponse.Choices, choice)
|
||||
jsonResponse, err := json.Marshal(fullTextResponse)
|
||||
if err != nil {
|
||||
return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
|
||||
}
|
||||
c.Writer.Header().Set("Content-Type", "application/json")
|
||||
c.Writer.WriteHeader(resp.StatusCode)
|
||||
c.Writer.Write(jsonResponse)
|
||||
return &difyResponse.MetaData.Usage, nil
|
||||
}
|
||||
Reference in New Issue
Block a user