feat: support for channel configuration proxy (#101)

This commit is contained in:
Zhang Minghan 2024-03-14 18:03:28 +08:00
parent 9323abb318
commit e037276387
39 changed files with 311 additions and 93 deletions

View File

@ -42,6 +42,7 @@ var channelFactories = map[string]adaptercommon.FactoryCreator{
func createChatRequest(conf globals.ChannelConfig, props *adaptercommon.ChatProps, hook globals.Hook) error {
props.Model = conf.GetModelReflect(props.OriginalModel)
props.Proxy = conf.GetProxy()
factoryType := conf.GetType()
if factory, ok := channelFactories[factoryType]; ok {

View File

@ -66,6 +66,7 @@ func (c *ChatInstance) CreateChatRequest(props *adaptercommon.ChatProps) (string
c.GetChatEndpoint(props),
c.GetHeader(),
c.GetChatBody(props, false),
props.Proxy,
)
if err != nil || res == nil {
@ -108,7 +109,7 @@ func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, c
}
return callback(partial)
},
})
}, props.Proxy)
if err != nil {
if form := processChatErrorResponse(err.Body); form != nil {

View File

@ -12,6 +12,7 @@ type ImageProps struct {
Model string
Prompt string
Size ImageSize
Proxy globals.ProxyConfig
}
func (c *ChatInstance) GetImageEndpoint(model string) string {
@ -31,7 +32,7 @@ func (c *ChatInstance) CreateImageRequest(props ImageProps) (string, error) {
ImageSize512,
),
N: 1,
})
}, props.Proxy)
if err != nil || res == nil {
return "", fmt.Errorf("openai error: %s", err.Error())
}
@ -51,6 +52,7 @@ func (c *ChatInstance) CreateImage(props *adaptercommon.ChatProps) (string, erro
url, err := c.CreateImageRequest(ImageProps{
Model: props.Model,
Prompt: c.GetLatestPrompt(props),
Proxy: props.Proxy,
})
if err != nil {
if strings.Contains(err.Error(), "safety") {

View File

@ -48,6 +48,7 @@ func (c *ChatInstance) CreateChatRequest(props *adaptercommon.ChatProps) (string
c.GetChatEndpoint(),
c.GetHeader(),
c.GetChatBody(props, false),
props.Proxy,
)
if err != nil || res == nil {
@ -77,7 +78,7 @@ func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, c
}
return callback(partial)
},
})
}, props.Proxy)
if err != nil {
if form := processChatErrorResponse(err.Body); form != nil {

View File

@ -11,18 +11,19 @@ import (
const defaultTokens = 2500
func (c *ChatInstance) GetChatEndpoint() string {
return fmt.Sprintf("%s/v1/complete", c.GetEndpoint())
return fmt.Sprintf("%s/v1/messages", c.GetEndpoint())
}
func (c *ChatInstance) GetChatHeaders() map[string]string {
return map[string]string{
"content-type": "application/json",
"accept": "application/json",
"anthropic-version": "2023-06-01",
"x-api-key": c.GetApiKey(),
}
}
func (c *ChatInstance) ConvertMessage(message []globals.Message) string {
// ConvertCompletionMessage converts the completion message to anthropic complete format (deprecated)
func (c *ChatInstance) ConvertCompletionMessage(message []globals.Message) string {
mapper := map[string]string{
globals.System: "Assistant",
globals.User: "Human",
@ -54,8 +55,8 @@ func (c *ChatInstance) GetTokens(props *adaptercommon.ChatProps) int {
func (c *ChatInstance) GetChatBody(props *adaptercommon.ChatProps, stream bool) *ChatBody {
return &ChatBody{
Prompt: c.ConvertMessage(props.Message),
MaxTokensToSample: c.GetTokens(props),
Messages: props.Message,
MaxTokens: c.GetTokens(props),
Model: props.Model,
Stream: stream,
Temperature: props.Temperature,
@ -66,7 +67,7 @@ func (c *ChatInstance) GetChatBody(props *adaptercommon.ChatProps, stream bool)
// CreateChatRequest is the request for anthropic claude
func (c *ChatInstance) CreateChatRequest(props *adaptercommon.ChatProps) (string, error) {
data, err := utils.Post(c.GetChatEndpoint(), c.GetChatHeaders(), c.GetChatBody(props, false))
data, err := utils.Post(c.GetChatEndpoint(), c.GetChatHeaders(), c.GetChatBody(props, false), props.Proxy)
if err != nil {
return "", fmt.Errorf("claude error: %s", err.Error())
}
@ -126,5 +127,7 @@ func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, h
}
return nil
})
},
props.Proxy,
)
}

View File

@ -1,9 +1,11 @@
package claude
import "chat/globals"
// ChatBody is the request body for anthropic claude
type ChatBody struct {
Prompt string `json:"prompt"`
MaxTokensToSample int `json:"max_tokens_to_sample"`
Messages []globals.Message `json:"messages"`
MaxTokens int `json:"max_tokens"`
Model string `json:"model"`
Stream bool `json:"stream"`
Temperature *float32 `json:"temperature,omitempty"`

View File

@ -9,6 +9,8 @@ type RequestProps struct {
MaxRetries *int
Current int
Group string
Proxy globals.ProxyConfig
}
type ChatProps struct {

View File

@ -127,5 +127,6 @@ func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, c
return nil
},
props.Proxy,
)
}

View File

@ -1,6 +1,7 @@
package midjourney
import (
"chat/globals"
"chat/utils"
"fmt"
)
@ -29,11 +30,12 @@ func (c *ChatInstance) GetChangeRequest(action string, task string, index *int)
}
}
func (c *ChatInstance) CreateImagineRequest(prompt string) (*CommonResponse, error) {
func (c *ChatInstance) CreateImagineRequest(proxy globals.ProxyConfig, prompt string) (*CommonResponse, error) {
content, err := utils.PostRaw(
c.GetImagineEndpoint(),
c.GetMidjourneyHeaders(),
c.GetImagineRequest(prompt),
proxy,
)
if err != nil {
@ -47,11 +49,12 @@ func (c *ChatInstance) CreateImagineRequest(prompt string) (*CommonResponse, err
}
}
func (c *ChatInstance) CreateChangeRequest(action string, task string, index *int) (*CommonResponse, error) {
func (c *ChatInstance) CreateChangeRequest(proxy globals.ProxyConfig, action string, task string, index *int) (*CommonResponse, error) {
res, err := utils.Post(
c.GetChangeEndpoint(),
c.GetMidjourneyHeaders(),
c.GetChangeRequest(action, task, index),
proxy,
)
if err != nil {

View File

@ -95,7 +95,7 @@ func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, c
var begin bool
form, err := c.CreateStreamTask(action, prompt, func(form *StorageForm, progress int) error {
form, err := c.CreateStreamTask(props, action, prompt, func(form *StorageForm, progress int) error {
if progress == -1 {
// ping event
return callback(&globals.Chunk{Content: ""})

View File

@ -1,6 +1,8 @@
package midjourney
import (
adaptercommon "chat/adapter/common"
"chat/globals"
"chat/utils"
"fmt"
"strings"
@ -66,21 +68,21 @@ func (c *ChatInstance) ExtractCommand(input string) (task string, index *int) {
return
}
func (c *ChatInstance) CreateRequest(action string, prompt string) (*CommonResponse, error) {
func (c *ChatInstance) CreateRequest(proxy globals.ProxyConfig, action string, prompt string) (*CommonResponse, error) {
switch action {
case ImagineCommand:
return c.CreateImagineRequest(prompt)
return c.CreateImagineRequest(proxy, prompt)
case VariationCommand, UpscaleCommand, RerollCommand:
task, index := c.ExtractCommand(prompt)
return c.CreateChangeRequest(c.GetAction(action), task, index)
return c.CreateChangeRequest(proxy, c.GetAction(action), task, index)
default:
return nil, fmt.Errorf("unknown action: %s", action)
}
}
func (c *ChatInstance) CreateStreamTask(action string, prompt string, hook func(form *StorageForm, progress int) error) (*StorageForm, error) {
res, err := c.CreateRequest(action, prompt)
func (c *ChatInstance) CreateStreamTask(props *adaptercommon.ChatProps, action string, prompt string, hook func(form *StorageForm, progress int) error) (*StorageForm, error) {
res, err := c.CreateRequest(props.Proxy, action, prompt)
if err != nil {
return nil, err
}

View File

@ -69,6 +69,7 @@ func (c *ChatInstance) CreateChatRequest(props *adaptercommon.ChatProps) (string
c.GetChatEndpoint(props),
c.GetHeader(),
c.GetChatBody(props, false),
props.Proxy,
)
if err != nil || res == nil {
@ -120,7 +121,7 @@ func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, c
}
return callback(partial)
},
})
}, props.Proxy)
if err != nil {
if form := processChatErrorResponse(err.Body); form != nil {

View File

@ -12,6 +12,7 @@ type ImageProps struct {
Model string
Prompt string
Size ImageSize
Proxy globals.ProxyConfig
}
func (c *ChatInstance) GetImageEndpoint() string {
@ -31,7 +32,7 @@ func (c *ChatInstance) CreateImageRequest(props ImageProps) (string, error) {
ImageSize512,
),
N: 1,
})
}, props.Proxy)
if err != nil || res == nil {
return "", fmt.Errorf(err.Error())
}
@ -51,6 +52,7 @@ func (c *ChatInstance) CreateImage(props *adaptercommon.ChatProps) (string, erro
original, err := c.CreateImageRequest(ImageProps{
Model: props.Model,
Prompt: c.GetLatestPrompt(props),
Proxy: props.Proxy,
})
if err != nil {
if strings.Contains(err.Error(), "safety") {

View File

@ -93,7 +93,7 @@ func (c *ChatInstance) CreateChatRequest(props *adaptercommon.ChatProps) (string
if props.Model == globals.ChatBison001 {
data, err := utils.Post(uri, map[string]string{
"Content-Type": "application/json",
}, c.GetPalm2ChatBody(props))
}, c.GetPalm2ChatBody(props), props.Proxy)
if err != nil {
return "", fmt.Errorf("palm2 error: %s", err.Error())
@ -103,7 +103,7 @@ func (c *ChatInstance) CreateChatRequest(props *adaptercommon.ChatProps) (string
data, err := utils.Post(uri, map[string]string{
"Content-Type": "application/json",
}, c.GetGeminiChatBody(props))
}, c.GetGeminiChatBody(props), props.Proxy)
if err != nil {
return "", fmt.Errorf("gemini error: %s", err.Error())

View File

@ -51,6 +51,7 @@ func (c *ChatInstance) CreateChatRequest(props *adaptercommon.ChatProps) (string
c.GetChatEndpoint(),
c.GetHeader(),
c.GetChatBody(props, false),
props.Proxy,
)
if err != nil || res == nil {
@ -100,6 +101,7 @@ func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, c
}
return nil
},
props.Proxy,
)
if err != nil {

View File

@ -69,5 +69,6 @@ func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, h
data = strings.TrimPrefix(data, "data:")
return hook(&globals.Chunk{Content: data})
},
props.Proxy,
)
}

View File

@ -20,6 +20,24 @@ export type Channel = {
mapper: string;
state: boolean;
group?: string[];
proxy?: {
proxy: string;
proxy_type: number;
};
};
export enum proxyType {
NoneProxy = 0,
HttpProxy = 1,
HttpsProxy = 2,
Socks5Proxy = 3,
}
export const ProxyTypes: Record<number, string> = {
[proxyType.NoneProxy]: "None Proxy",
[proxyType.HttpProxy]: "HTTP Proxy",
[proxyType.HttpsProxy]: "HTTPS Proxy",
[proxyType.Socks5Proxy]: "SOCKS5 Proxy",
};
export type ChannelInfo = {
@ -133,7 +151,16 @@ export const ChannelInfos: Record<string, ChannelInfo> = {
claude: {
endpoint: "https://api.anthropic.com",
format: "<x-api-key>",
models: ["claude-instant-1", "claude-2", "claude-2.1"],
description:
"> Anthropic Claude 密钥格式为 **x-api-key**Anthropic 对请求 IP 地域有限制,可能出现 **Request not allowed** 的错误,请尝试更换 IP 或者使用代理。\n",
models: [
"claude-instant-1.2",
"claude-2",
"claude-2.1",
"claude-3-opus-20240229",
"claude-3-sonnet-20240229",
"claude-3-haiku-20240307",
],
},
slack: {
endpoint: "your-channel",

View File

@ -40,6 +40,10 @@ export const modelColorMapper: Record<string, string> = {
"claude-2": "orange-400",
"claude-2.1": "orange-400",
"claude-2-100k": "orange-400",
"claude-instant-1.2": "orange-400",
"claude-3-opus-20240229": "orange-400",
"claude-3-sonnet-20240229": "orange-400",
"claude-3-haiku-20240307": "orange-400",
"spark-desk-v1.5": "blue-400",
"spark-desk-v2": "blue-400",

View File

@ -88,6 +88,7 @@ export const pricing: PricingDataset = [
"claude-1.2",
"claude-1.3",
"claude-instant",
"claude-instant-1.2",
"claude-slack",
],
input: 0.0008,

View File

@ -91,7 +91,7 @@ strong {
.flex-dialog {
border-radius: var(--radius) !important;
max-height: calc(95vh - 2rem) !important;
max-height: calc(95% - 2rem) !important;
overflow-x: hidden;
overflow-y: auto;
scrollbar-width: none;
@ -128,7 +128,7 @@ strong {
.fixed-dialog {
border-radius: var(--radius) !important;
max-height: calc(95vh - 2rem) !important;
max-height: calc(95% - 2rem) !important;
min-height: 60vh;
overflow-x: hidden;
overflow-y: auto;

View File

@ -237,6 +237,7 @@
border: 1px solid var(--assistant-border);
border-radius: var(--radius);
margin: 0.25rem 0;
white-space: nowrap;
.grow {
min-width: 0.75rem;

View File

@ -1,4 +1,4 @@
.share-table {
max-width: 85vw;
width: 100%;
height: max-content;
}

View File

@ -16,6 +16,10 @@ const initialState: Channel = {
mapper: "",
state: true,
group: [],
proxy: {
proxy: "",
proxy_type: 0,
},
};
function reducer(state: Channel, action: any): Channel {
@ -77,6 +81,22 @@ function reducer(state: Channel, action: any): Channel {
};
case "set-group":
return { ...state, group: action.value };
case "set-proxy":
return {
...state,
proxy: {
proxy: action.value as string,
proxy_type: state?.proxy?.proxy_type || 0,
},
};
case "set-proxy-type":
return {
...state,
proxy: {
proxy: state?.proxy?.proxy || "",
proxy_type: action.value as number,
},
};
case "set":
return { ...state, ...action.value };
default:

View File

@ -13,9 +13,11 @@ import {
channelGroups,
ChannelTypes,
getChannelInfo,
proxyType,
ProxyTypes,
} from "@/admin/channel.ts";
import { CommonResponse, toastState } from "@/api/common.ts";
import { Textarea } from "@/components/ui/textarea.tsx";
import { FlexibleTextarea, Textarea } from "@/components/ui/textarea.tsx";
import { NumberInput } from "@/components/ui/number-input.tsx";
import { Button } from "@/components/ui/button.tsx";
import { useTranslation } from "react-i18next";
@ -44,6 +46,7 @@ import { useEffectAsync } from "@/utils/hook.ts";
import Paragraph, {
ParagraphDescription,
ParagraphItem,
ParagraphSpace,
} from "@/components/Paragraph.tsx";
import { MultiCombobox } from "@/components/ui/multi-combobox.tsx";
import { useChannelModels } from "@/admin/hook.tsx";
@ -116,6 +119,14 @@ function handler(data: Channel): Channel {
data.group = data.group
? data.group.filter((group) => group.trim() !== "")
: [];
if (
data.proxy &&
data.proxy.proxy.trim() === "" &&
data.proxy.proxy_type !== 0
) {
data.proxy.proxy_type = 0;
}
return data;
}
@ -361,7 +372,8 @@ function ChannelEditor({
{t("admin.channels.mapper")}
<Tips content={t("admin.channels.mapper-tip")} />
</div>
<Textarea
<FlexibleTextarea
rows={5}
value={edit.mapper}
placeholder={t("admin.channels.mapper-placeholder")}
onChange={(e) =>
@ -394,6 +406,56 @@ function ChannelEditor({
<ParagraphDescription>
{t("admin.channels.group-desc")}
</ParagraphDescription>
<ParagraphSpace />
<ParagraphItem>
<div className={`channel-row column-layout`}>
<div className={`channel-content`}>
{t("admin.channels.proxy-type")}
</div>
<Select
value={(edit.proxy?.proxy_type || 0).toString()}
onValueChange={(value: string) =>
dispatch({ type: "set-proxy-type", value: parseInt(value) })
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value={"0"}>
{ProxyTypes[proxyType.NoneProxy]}
</SelectItem>
<SelectItem value={"1"}>
{ProxyTypes[proxyType.HttpProxy]}
</SelectItem>
<SelectItem value={"2"}>
{ProxyTypes[proxyType.HttpsProxy]}
</SelectItem>
<SelectItem value={"3"}>
{ProxyTypes[proxyType.Socks5Proxy]}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
</ParagraphItem>
<ParagraphItem>
<div className={`channel-content`}>
{t("admin.channels.proxy-endpoint")}
</div>
<Input
value={edit.proxy?.proxy || ""}
placeholder={t("admin.channels.proxy-endpoint-placeholder")}
onChange={(e) =>
dispatch({ type: "set-proxy", value: e.target.value })
}
disabled={edit.proxy?.proxy_type === 0}
/>
</ParagraphItem>
<ParagraphDescription>
{t("admin.channels.proxy-desc")}
</ParagraphDescription>
</Paragraph>
</div>
<div className={`mt-4 flex flex-row w-full h-max pr-2 items-center`}>

View File

@ -100,6 +100,7 @@ function SyncDialog({ dispatch, open, setOpen }: SyncDialogProps) {
mapper: "",
state: true,
group: [],
proxy: { proxy: "", proxy_type: 0 },
};
dispatch({ type: "set", value: data });

View File

@ -32,7 +32,7 @@ function QuotaExceededForm({
const dispatch = useDispatch();
return (
<div className={`flex flex-col items-center w-[40vw] max-w-[320px] py-2`}>
<div className={`flex flex-col items-center min-w-[40vw] p-2`}>
<img src={appLogo} alt={""} className={`w-16 h-16 m-6 inline-block`} />
<div
className={`prompt-row flex flex-row w-full items-center justify-center px-4 py-2`}

View File

@ -31,12 +31,12 @@ export interface FlexibleTextareaProps extends TextareaProps {
const FlexibleTextarea = React.forwardRef<
HTMLTextAreaElement,
FlexibleTextareaProps
>(({ rows = 1, className, ...props }, ref) => {
>(({ rows = 1, minRows, className, ...props }, ref) => {
const lines = useMemo(() => {
const value = props.value?.toString() || "";
const count = value.split("\n").length + 1;
return Math.max(rows, count);
}, [props.value]);
return Math.max(rows, count, minRows || 1);
}, [props.value, rows, minRows]);
return (
<Textarea

View File

@ -14,7 +14,6 @@ import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog.tsx";
@ -26,7 +25,7 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table.tsx";
import { closeDialog, setDialog } from "@/store/sharing.ts";
import { setDialog } from "@/store/sharing.ts";
import { Button } from "@/components/ui/button.tsx";
import { useMemo } from "react";
import { Eye, MoreHorizontal, Trash2 } from "lucide-react";
@ -60,7 +59,9 @@ function ShareTable({ data }: ShareTableProps) {
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead>{t("share.name")}</TableHead>
<TableHead className={`whitespace-nowrap`}>
{t("share.name")}
</TableHead>
<TableHead>{t("share.time")}</TableHead>
<TableHead>{t("share.action")}</TableHead>
</TableRow>
@ -69,7 +70,7 @@ function ShareTable({ data }: ShareTableProps) {
{data.map((row, idx) => (
<TableRow key={idx}>
<TableCell>{row.conversation_id}</TableCell>
<TableCell>{row.name}</TableCell>
<TableCell className={`whitespace-nowrap`}>{row.name}</TableCell>
<TableCell className={`whitespace-nowrap`}>{time[idx]}</TableCell>
<TableCell>
<DropdownMenu>
@ -146,11 +147,6 @@ function ShareManagementDialog() {
</DialogDescription>
)}
</DialogHeader>
<DialogFooter>
<Button variant={`outline`} onClick={() => dispatch(closeDialog())}>
{t("close")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);

View File

@ -629,7 +629,11 @@
"standard": "标准版订阅用户",
"pro": "专业版订阅用户",
"admin": "管理员用户"
}
},
"proxy-type": "代理类型",
"proxy-endpoint": "代理地址",
"proxy-endpoint-placeholder": "请输入正向代理地址socks5://example.com:1080",
"proxy-desc": "正向代理,支持 HTTP/HTTPS/SOCKS5 代理 (反向代理请填写接入点, 非特殊情况无需设置正向代理)"
},
"charge": {
"id": "ID",

View File

@ -457,7 +457,11 @@
"sync-success": "Sync successfully.",
"sync-success-prompt": "{{length}} models were added from upstream synchronization.",
"upstream-endpoint-placeholder": "Please enter the upstream OpenAI address, e.g. https://api.openai.com",
"sync-secret-placeholder": "Please enter API key for upstream channel"
"sync-secret-placeholder": "Please enter API key for upstream channel",
"proxy-type": "Delegate Type",
"proxy-endpoint": "proxy address",
"proxy-endpoint-placeholder": "Please enter forward proxy address, ex: socks5://example.com: 1080",
"proxy-desc": "Forward proxy, supports HTTP/HTTPS/SOCKS5 proxy (reverse proxy please fill in the access point, no need to set forward proxy in non-special cases)"
},
"charge": {
"id": "ID",

View File

@ -457,7 +457,11 @@
"sync-success": "同期成功",
"sync-success-prompt": "{{length}}モデルがアップストリーム同期から追加されました。",
"upstream-endpoint-placeholder": "上流のOpenAIアドレスを入力してください。例 https://api.openai.com",
"sync-secret-placeholder": "アップストリームチャネルのAPIキーを入力してください"
"sync-secret-placeholder": "アップストリームチャネルのAPIキーを入力してください",
"proxy-type": "プロキシのタイプ",
"proxy-endpoint": "プロキシアドレス",
"proxy-endpoint-placeholder": "転送プロキシアドレスを入力してください。例: socks 5 :// example.com: 1080",
"proxy-desc": "フォワードプロキシ、HTTP/HTTPS/SOCKS 5プロキシをサポートリバースプロキシはアクセスポイントに記入してください。特別な場合以外はフォワードプロキシを設定する必要はありません"
},
"charge": {
"id": "ID",

View File

@ -457,7 +457,11 @@
"sync-success": "Успешная синхронизация",
"sync-success-prompt": "{{length}} модели были добавлены из синхронизации восходящего потока.",
"upstream-endpoint-placeholder": "Введите вышестоящий адрес OpenAI, например, https://api.openai.com",
"sync-secret-placeholder": "Пожалуйста, введите ключ API для восходящего канала"
"sync-secret-placeholder": "Пожалуйста, введите ключ API для восходящего канала",
"proxy-type": "Тип прокси- сервера",
"proxy-endpoint": "Адрес прокси-сервера",
"proxy-endpoint-placeholder": "Введите адрес прямого прокси-сервера, например: socks5://example.com: 1080",
"proxy-desc": "Прямой прокси-сервер, поддерживает HTTP/HTTPS/Socks5 прокси-сервер (обратный прокси-сервер, пожалуйста, заполните точку доступа, нет необходимости устанавливать прокси-сервер в неспециальных случаях)"
},
"charge": {
"id": "ID",

View File

@ -1,6 +1,7 @@
package channel
import (
"chat/globals"
"chat/utils"
"errors"
"fmt"
@ -174,6 +175,10 @@ func (c *Channel) GetGroup() []string {
return c.Group
}
func (c *Channel) GetProxy() globals.ProxyConfig {
return c.Proxy
}
func (c *Channel) IsHitGroup(group string) bool {
if len(c.GetGroup()) == 0 {
return true

View File

@ -1,5 +1,9 @@
package channel
import (
"chat/globals"
)
type Channel struct {
Id int `json:"id" mapstructure:"id"`
Name string `json:"name" mapstructure:"name"`
@ -13,6 +17,7 @@ type Channel struct {
Mapper string `json:"mapper" mapstructure:"mapper"`
State bool `json:"state" mapstructure:"state"`
Group []string `json:"group" mapstructure:"group"`
Proxy globals.ProxyConfig `json:"proxy" mapstructure:"proxy"`
Reflect *map[string]string `json:"-"`
HitModels *[]string `json:"-"`
ExcludeModels *[]string `json:"-"`

View File

@ -41,3 +41,10 @@ const (
ProType = "pro" // pro subscription
AdminType = "admin"
)
const (
NoneProxyType = iota
HttpProxyType
HttpsProxyType
Socks5ProxyType
)

View File

@ -11,6 +11,7 @@ type ChannelConfig interface {
GetEndpoint() string
ProcessError(err error) error
GetId() int
GetProxy() ProxyConfig
}
type AuthLike interface {

View File

@ -45,3 +45,8 @@ type ListModelsItem struct {
Created int64 `json:"created"`
OwnedBy string `json:"owned_by"`
}
type ProxyConfig struct {
ProxyType int `json:"proxy_type" mapstructure:"proxytype"`
Proxy string `json:"proxy" mapstructure:"proxy"`
}

View File

@ -3,11 +3,15 @@ package utils
import (
"bytes"
"chat/globals"
"context"
"crypto/tls"
"fmt"
"github.com/goccy/go-json"
"golang.org/x/net/proxy"
"io"
"net"
"net/http"
"net/url"
"runtime/debug"
"strings"
"time"
@ -15,13 +19,52 @@ import (
var maxTimeout = 30 * time.Minute
func newClient() *http.Client {
return &http.Client{
func newClient(c []globals.ProxyConfig) *http.Client {
client := &http.Client{
Timeout: maxTimeout,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
if len(c) == 0 {
return client
}
config := c[0]
if config.ProxyType == globals.NoneProxyType {
return client
}
if config.ProxyType == globals.HttpProxyType || config.ProxyType == globals.HttpsProxyType {
proxyUrl, err := url.Parse(config.Proxy)
if err != nil {
globals.Warn(fmt.Sprintf("failed to parse proxy url: %s", err))
return client
}
client.Transport = &http.Transport{
Proxy: http.ProxyURL(proxyUrl),
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
} else if config.ProxyType == globals.Socks5ProxyType {
dialer, err := proxy.SOCKS5("tcp", config.Proxy, nil, proxy.Direct)
if err != nil {
globals.Warn(fmt.Sprintf("failed to create socks5 proxy: %s", err))
return client
}
dialContext := func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialer.Dial(network, addr)
}
client.Transport = &http.Transport{
DialContext: dialContext,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
}
globals.Debug(fmt.Sprintf("[proxy] configured proxy: %s", config.Proxy))
return client
}
func fillHeaders(req *http.Request, headers map[string]string) {
@ -30,14 +73,14 @@ func fillHeaders(req *http.Request, headers map[string]string) {
}
}
func Http(uri string, method string, ptr interface{}, headers map[string]string, body io.Reader) (err error) {
func Http(uri string, method string, ptr interface{}, headers map[string]string, body io.Reader, config []globals.ProxyConfig) (err error) {
req, err := http.NewRequest(method, uri, body)
if err != nil {
return err
}
fillHeaders(req, headers)
client := newClient()
client := newClient(config)
resp, err := client.Do(req)
if err != nil {
return err
@ -51,14 +94,14 @@ func Http(uri string, method string, ptr interface{}, headers map[string]string,
return nil
}
func HttpRaw(uri string, method string, headers map[string]string, body io.Reader) (data []byte, err error) {
func HttpRaw(uri string, method string, headers map[string]string, body io.Reader, config []globals.ProxyConfig) (data []byte, err error) {
req, err := http.NewRequest(method, uri, body)
if err != nil {
return nil, err
}
fillHeaders(req, headers)
client := newClient()
client := newClient(config)
resp, err := client.Do(req)
if err != nil {
return nil, err
@ -72,26 +115,26 @@ func HttpRaw(uri string, method string, headers map[string]string, body io.Reade
return data, nil
}
func Get(uri string, headers map[string]string) (data interface{}, err error) {
err = Http(uri, http.MethodGet, &data, headers, nil)
func Get(uri string, headers map[string]string, config ...globals.ProxyConfig) (data interface{}, err error) {
err = Http(uri, http.MethodGet, &data, headers, nil, config)
return data, err
}
func GetRaw(uri string, headers map[string]string) (data string, err error) {
buffer, err := HttpRaw(uri, http.MethodGet, headers, nil)
func GetRaw(uri string, headers map[string]string, config ...globals.ProxyConfig) (data string, err error) {
buffer, err := HttpRaw(uri, http.MethodGet, headers, nil, config)
if err != nil {
return "", err
}
return string(buffer), nil
}
func Post(uri string, headers map[string]string, body interface{}) (data interface{}, err error) {
err = Http(uri, http.MethodPost, &data, headers, ConvertBody(body))
func Post(uri string, headers map[string]string, body interface{}, config ...globals.ProxyConfig) (data interface{}, err error) {
err = Http(uri, http.MethodPost, &data, headers, ConvertBody(body), config)
return data, err
}
func PostRaw(uri string, headers map[string]string, body interface{}) (data string, err error) {
buffer, err := HttpRaw(uri, http.MethodPost, headers, ConvertBody(body))
func PostRaw(uri string, headers map[string]string, body interface{}, config ...globals.ProxyConfig) (data string, err error) {
buffer, err := HttpRaw(uri, http.MethodPost, headers, ConvertBody(body), config)
if err != nil {
return "", err
}
@ -105,7 +148,7 @@ func ConvertBody(body interface{}) (form io.Reader) {
return form
}
func EventSource(method string, uri string, headers map[string]string, body interface{}, callback func(string) error) error {
func EventSource(method string, uri string, headers map[string]string, body interface{}, callback func(string) error, config ...globals.ProxyConfig) error {
// panic recovery
defer func() {
if err := recover(); err != nil {
@ -116,7 +159,7 @@ func EventSource(method string, uri string, headers map[string]string, body inte
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
client := newClient()
client := newClient(config)
req, err := http.NewRequest(method, uri, ConvertBody(body))
if err != nil {
return err

View File

@ -36,7 +36,7 @@ func getErrorBody(resp *http.Response) string {
return ""
}
func EventScanner(props *EventScannerProps) *EventScannerError {
func EventScanner(props *EventScannerProps, config ...globals.ProxyConfig) *EventScannerError {
// panic recovery
defer func() {
if r := recover(); r != nil {
@ -45,7 +45,7 @@ func EventScanner(props *EventScannerProps) *EventScannerError {
}
}()
client := newClient()
client := newClient(config)
req, err := http.NewRequest(props.Method, props.Uri, ConvertBody(props.Body))
if err != nil {
return &EventScannerError{Error: err}