Ver Fonte

Merge pull request #2238 from seefs001/feature/doubao-coding-plan

feat: support doubao coding plan
Seefs há 3 meses atrás
pai
commit
7598753f4e

+ 1 - 0
.gitignore

@@ -17,6 +17,7 @@ tiktoken_cache
 .eslintcache
 .gocache
 .cache
+web/bun.lock
 
 electron/node_modules
 electron/dist

+ 7 - 0
controller/channel.go

@@ -11,6 +11,7 @@ import (
 	"github.com/QuantumNous/new-api/constant"
 	"github.com/QuantumNous/new-api/dto"
 	"github.com/QuantumNous/new-api/model"
+	"github.com/QuantumNous/new-api/relay/channel/volcengine"
 	"github.com/QuantumNous/new-api/service"
 
 	"github.com/gin-gonic/gin"
@@ -192,6 +193,12 @@ func FetchUpstreamModels(c *gin.Context) {
 		url = fmt.Sprintf("%s/compatible-mode/v1/models", baseURL)
 	case constant.ChannelTypeZhipu_v4:
 		url = fmt.Sprintf("%s/api/paas/v4/models", baseURL)
+	case constant.ChannelTypeVolcEngine:
+		if baseURL == volcengine.DoubaoCodingPlan {
+			url = fmt.Sprintf("%s/v1/models", volcengine.DoubaoCodingPlanOpenAIBaseURL)
+		} else {
+			url = fmt.Sprintf("%s/v1/models", baseURL)
+		}
 	default:
 		url = fmt.Sprintf("%s/v1/models", baseURL)
 	}

+ 11 - 2
relay/channel/volcengine/adaptor.go

@@ -23,8 +23,11 @@ import (
 )
 
 const (
-	contextKeyTTSRequest     = "volcengine_tts_request"
-	contextKeyResponseFormat = "response_format"
+	contextKeyTTSRequest          = "volcengine_tts_request"
+	contextKeyResponseFormat      = "response_format"
+	DoubaoCodingPlan              = "doubao-coding-plan"
+	DoubaoCodingPlanClaudeBaseURL = "https://ark.cn-beijing.volces.com/api/coding"
+	DoubaoCodingPlanOpenAIBaseURL = "https://ark.cn-beijing.volces.com/api/coding/v3"
 )
 
 type Adaptor struct {
@@ -238,6 +241,9 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
 
 	switch info.RelayFormat {
 	case types.RelayFormatClaude:
+		if baseUrl == DoubaoCodingPlan {
+			return fmt.Sprintf("%s/v1/messages", DoubaoCodingPlanClaudeBaseURL), nil
+		}
 		if strings.HasPrefix(info.UpstreamModelName, "bot") {
 			return fmt.Sprintf("%s/api/v3/bots/chat/completions", baseUrl), nil
 		}
@@ -245,6 +251,9 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
 	default:
 		switch info.RelayMode {
 		case constant.RelayModeChatCompletions:
+			if baseUrl == DoubaoCodingPlan {
+				return fmt.Sprintf("%s/chat/completions", DoubaoCodingPlanOpenAIBaseURL), nil
+			}
 			if strings.HasPrefix(info.UpstreamModelName, "bot") {
 				return fmt.Sprintf("%s/api/v3/bots/chat/completions", baseUrl), nil
 			}

+ 36 - 3
web/src/components/table/channels/modals/EditChannelModal.jsx

@@ -189,6 +189,7 @@ const EditChannelModal = (props) => {
   const [useManualInput, setUseManualInput] = useState(false); // 是否使用手动输入模式
   const [keyMode, setKeyMode] = useState('append'); // 密钥模式:replace(覆盖)或 append(追加)
   const [isEnterpriseAccount, setIsEnterpriseAccount] = useState(false); // 是否为企业账户
+  const [doubaoApiEditUnlocked, setDoubaoApiEditUnlocked] = useState(false); // 豆包渠道自定义 API 地址隐藏入口
 
   // 密钥显示状态
   const [keyDisplayState, setKeyDisplayState] = useState({
@@ -218,6 +219,7 @@ const EditChannelModal = (props) => {
     'channelExtraSettings',
   ];
   const formContainerRef = useRef(null);
+  const doubaoApiClickCountRef = useRef(0);
 
   // 2FA状态更新辅助函数
   const updateTwoFAState = (updates) => {
@@ -306,6 +308,20 @@ const EditChannelModal = (props) => {
     scrollToSection(availableSections[newIndex]);
   };
 
+  const handleApiConfigSecretClick = () => {
+    if (inputs.type !== 45) return;
+    const next = doubaoApiClickCountRef.current + 1;
+    doubaoApiClickCountRef.current = next;
+    if (next >= 10) {
+      setDoubaoApiEditUnlocked((unlocked) => {
+        if (!unlocked) {
+          showInfo(t('已解锁豆包自定义 API 地址编辑'));
+        }
+        return true;
+      });
+    }
+  };
+
   // 渠道额外设置状态
   const [channelSettings, setChannelSettings] = useState({
     force_format: false,
@@ -724,6 +740,13 @@ const EditChannelModal = (props) => {
     }
   };
 
+  useEffect(() => {
+    if (inputs.type !== 45) {
+      doubaoApiClickCountRef.current = 0;
+      setDoubaoApiEditUnlocked(false);
+    }
+  }, [inputs.type]);
+
   useEffect(() => {
     const modelMap = new Map();
 
@@ -823,6 +846,9 @@ const EditChannelModal = (props) => {
     setKeyMode('append');
     // 重置企业账户状态
     setIsEnterpriseAccount(false);
+    // 重置豆包隐藏入口状态
+    setDoubaoApiEditUnlocked(false);
+    doubaoApiClickCountRef.current = 0;
     // 清空表单中的key_mode字段
     if (formApiRef.current) {
       formApiRef.current.setValue('key_mode', undefined);
@@ -1959,7 +1985,10 @@ const EditChannelModal = (props) => {
                   <div ref={(el) => (formSectionRefs.current.apiConfig = el)}>
                     <Card className='!rounded-2xl shadow-sm border-0 mb-6'>
                       {/* Header: API Config */}
-                      <div className='flex items-center mb-2'>
+                      <div
+                        className='flex items-center mb-2'
+                        onClick={handleApiConfigSecretClick}
+                      >
                         <Avatar
                           size='small'
                           color='green'
@@ -2094,7 +2123,7 @@ const EditChannelModal = (props) => {
                         inputs.type !== 8 &&
                         inputs.type !== 22 &&
                         inputs.type !== 36 &&
-                        inputs.type !== 45 && (
+                        (inputs.type !== 45 || doubaoApiEditUnlocked) && (
                           <div>
                             <Form.Input
                               field='base_url'
@@ -2147,7 +2176,7 @@ const EditChannelModal = (props) => {
                         </div>
                       )}
 
-                      {inputs.type === 45 && (
+                      {inputs.type === 45 && !doubaoApiEditUnlocked && (
                         <div>
                           <Form.Select
                             field='base_url'
@@ -2167,6 +2196,10 @@ const EditChannelModal = (props) => {
                                 label:
                                   'https://ark.ap-southeast.bytepluses.com',
                               },
+                                {
+                                    value: 'doubao-coding-plan',
+                                    label: 'Doubao Coding Plan',
+                                },
                             ]}
                             defaultValue='https://ark.cn-beijing.volces.com'
                           />