You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

2136 regels
55 KiB

  1. package controller
  2. import (
  3. "context"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "net/http"
  8. "strconv"
  9. "strings"
  10. "time"
  11. "github.com/QuantumNous/new-api/common"
  12. "github.com/QuantumNous/new-api/constant"
  13. "github.com/QuantumNous/new-api/dto"
  14. "github.com/QuantumNous/new-api/model"
  15. "github.com/QuantumNous/new-api/relay/channel/gemini"
  16. "github.com/QuantumNous/new-api/relay/channel/ollama"
  17. "github.com/QuantumNous/new-api/service"
  18. "github.com/gin-gonic/gin"
  19. )
  20. type OpenAIModel struct {
  21. ID string `json:"id"`
  22. Object string `json:"object"`
  23. Created int64 `json:"created"`
  24. OwnedBy string `json:"owned_by"`
  25. Metadata map[string]any `json:"metadata,omitempty"`
  26. Permission []struct {
  27. ID string `json:"id"`
  28. Object string `json:"object"`
  29. Created int64 `json:"created"`
  30. AllowCreateEngine bool `json:"allow_create_engine"`
  31. AllowSampling bool `json:"allow_sampling"`
  32. AllowLogprobs bool `json:"allow_logprobs"`
  33. AllowSearchIndices bool `json:"allow_search_indices"`
  34. AllowView bool `json:"allow_view"`
  35. AllowFineTuning bool `json:"allow_fine_tuning"`
  36. Organization string `json:"organization"`
  37. Group string `json:"group"`
  38. IsBlocking bool `json:"is_blocking"`
  39. } `json:"permission"`
  40. Root string `json:"root"`
  41. Parent string `json:"parent"`
  42. }
  43. type OpenAIModelsResponse struct {
  44. Data []OpenAIModel `json:"data"`
  45. Success bool `json:"success"`
  46. }
  47. func parseStatusFilter(statusParam string) int {
  48. switch strings.ToLower(statusParam) {
  49. case "enabled", "1":
  50. return common.ChannelStatusEnabled
  51. case "disabled", "0":
  52. return 0
  53. default:
  54. return -1
  55. }
  56. }
  57. func clearChannelInfo(channel *model.Channel) {
  58. if channel.ChannelInfo.IsMultiKey {
  59. channel.ChannelInfo.MultiKeyDisabledReason = nil
  60. channel.ChannelInfo.MultiKeyDisabledTime = nil
  61. }
  62. }
  63. func GetAllChannels(c *gin.Context) {
  64. pageInfo := common.GetPageQuery(c)
  65. channelData := make([]*model.Channel, 0)
  66. idSort, _ := strconv.ParseBool(c.Query("id_sort"))
  67. enableTagMode, _ := strconv.ParseBool(c.Query("tag_mode"))
  68. statusParam := c.Query("status")
  69. // statusFilter: -1 all, 1 enabled, 0 disabled (include auto & manual)
  70. statusFilter := parseStatusFilter(statusParam)
  71. // type filter
  72. typeStr := c.Query("type")
  73. typeFilter := -1
  74. if typeStr != "" {
  75. if t, err := strconv.Atoi(typeStr); err == nil {
  76. typeFilter = t
  77. }
  78. }
  79. var total int64
  80. if enableTagMode {
  81. tags, err := model.GetPaginatedTags(pageInfo.GetStartIdx(), pageInfo.GetPageSize())
  82. if err != nil {
  83. common.SysError("failed to get paginated tags: " + err.Error())
  84. c.JSON(http.StatusOK, gin.H{"success": false, "message": "获取标签失败,请稍后重试"})
  85. return
  86. }
  87. for _, tag := range tags {
  88. if tag == nil || *tag == "" {
  89. continue
  90. }
  91. tagChannels, err := model.GetChannelsByTag(*tag, idSort, false)
  92. if err != nil {
  93. continue
  94. }
  95. filtered := make([]*model.Channel, 0)
  96. for _, ch := range tagChannels {
  97. if statusFilter == common.ChannelStatusEnabled && ch.Status != common.ChannelStatusEnabled {
  98. continue
  99. }
  100. if statusFilter == 0 && ch.Status == common.ChannelStatusEnabled {
  101. continue
  102. }
  103. if typeFilter >= 0 && ch.Type != typeFilter {
  104. continue
  105. }
  106. filtered = append(filtered, ch)
  107. }
  108. channelData = append(channelData, filtered...)
  109. }
  110. total, _ = model.CountAllTags()
  111. } else {
  112. baseQuery := model.DB.Model(&model.Channel{})
  113. if typeFilter >= 0 {
  114. baseQuery = baseQuery.Where("type = ?", typeFilter)
  115. }
  116. if statusFilter == common.ChannelStatusEnabled {
  117. baseQuery = baseQuery.Where("status = ?", common.ChannelStatusEnabled)
  118. } else if statusFilter == 0 {
  119. baseQuery = baseQuery.Where("status != ?", common.ChannelStatusEnabled)
  120. }
  121. baseQuery.Count(&total)
  122. order := "priority desc"
  123. if idSort {
  124. order = "id desc"
  125. }
  126. err := baseQuery.Order(order).Limit(pageInfo.GetPageSize()).Offset(pageInfo.GetStartIdx()).Omit("key").Find(&channelData).Error
  127. if err != nil {
  128. common.SysError("failed to get channels: " + err.Error())
  129. c.JSON(http.StatusOK, gin.H{"success": false, "message": "获取渠道列表失败,请稍后重试"})
  130. return
  131. }
  132. }
  133. for _, datum := range channelData {
  134. clearChannelInfo(datum)
  135. }
  136. countQuery := model.DB.Model(&model.Channel{})
  137. if statusFilter == common.ChannelStatusEnabled {
  138. countQuery = countQuery.Where("status = ?", common.ChannelStatusEnabled)
  139. } else if statusFilter == 0 {
  140. countQuery = countQuery.Where("status != ?", common.ChannelStatusEnabled)
  141. }
  142. var results []struct {
  143. Type int64
  144. Count int64
  145. }
  146. _ = countQuery.Select("type, count(*) as count").Group("type").Find(&results).Error
  147. typeCounts := make(map[int64]int64)
  148. for _, r := range results {
  149. typeCounts[r.Type] = r.Count
  150. }
  151. common.ApiSuccess(c, gin.H{
  152. "items": channelData,
  153. "total": total,
  154. "page": pageInfo.GetPage(),
  155. "page_size": pageInfo.GetPageSize(),
  156. "type_counts": typeCounts,
  157. })
  158. return
  159. }
  160. func buildFetchModelsHeaders(channel *model.Channel, key string) (http.Header, error) {
  161. var headers http.Header
  162. switch channel.Type {
  163. case constant.ChannelTypeAnthropic:
  164. headers = GetClaudeAuthHeader(key)
  165. default:
  166. headers = GetAuthHeader(key)
  167. }
  168. headerOverride := channel.GetHeaderOverride()
  169. for k, v := range headerOverride {
  170. str, ok := v.(string)
  171. if !ok {
  172. return nil, fmt.Errorf("invalid header override for key %s", k)
  173. }
  174. if strings.Contains(str, "{api_key}") {
  175. str = strings.ReplaceAll(str, "{api_key}", key)
  176. }
  177. headers.Set(k, str)
  178. }
  179. return headers, nil
  180. }
  181. func FetchUpstreamModels(c *gin.Context) {
  182. id, err := strconv.Atoi(c.Param("id"))
  183. if err != nil {
  184. common.ApiError(c, err)
  185. return
  186. }
  187. channel, err := model.GetChannelById(id, true)
  188. if err != nil {
  189. common.ApiError(c, err)
  190. return
  191. }
  192. baseURL := constant.ChannelBaseURLs[channel.Type]
  193. if channel.GetBaseURL() != "" {
  194. baseURL = channel.GetBaseURL()
  195. }
  196. // 对于 Ollama 渠道,使用特殊处理
  197. if channel.Type == constant.ChannelTypeOllama {
  198. key := strings.Split(channel.Key, "\n")[0]
  199. models, err := ollama.FetchOllamaModels(baseURL, key)
  200. if err != nil {
  201. c.JSON(http.StatusOK, gin.H{
  202. "success": false,
  203. "message": fmt.Sprintf("获取Ollama模型失败: %s", err.Error()),
  204. })
  205. return
  206. }
  207. result := OpenAIModelsResponse{
  208. Data: make([]OpenAIModel, 0, len(models)),
  209. }
  210. for _, modelInfo := range models {
  211. metadata := map[string]any{}
  212. if modelInfo.Size > 0 {
  213. metadata["size"] = modelInfo.Size
  214. }
  215. if modelInfo.Digest != "" {
  216. metadata["digest"] = modelInfo.Digest
  217. }
  218. if modelInfo.ModifiedAt != "" {
  219. metadata["modified_at"] = modelInfo.ModifiedAt
  220. }
  221. details := modelInfo.Details
  222. if details.ParentModel != "" || details.Format != "" || details.Family != "" || len(details.Families) > 0 || details.ParameterSize != "" || details.QuantizationLevel != "" {
  223. metadata["details"] = modelInfo.Details
  224. }
  225. if len(metadata) == 0 {
  226. metadata = nil
  227. }
  228. result.Data = append(result.Data, OpenAIModel{
  229. ID: modelInfo.Name,
  230. Object: "model",
  231. Created: 0,
  232. OwnedBy: "ollama",
  233. Metadata: metadata,
  234. })
  235. }
  236. c.JSON(http.StatusOK, gin.H{
  237. "success": true,
  238. "data": result.Data,
  239. })
  240. return
  241. }
  242. // 对于 Gemini 渠道,使用特殊处理
  243. if channel.Type == constant.ChannelTypeGemini {
  244. // 获取用于请求的可用密钥(多密钥渠道优先使用启用状态的密钥)
  245. key, _, apiErr := channel.GetNextEnabledKey()
  246. if apiErr != nil {
  247. c.JSON(http.StatusOK, gin.H{
  248. "success": false,
  249. "message": fmt.Sprintf("获取渠道密钥失败: %s", apiErr.Error()),
  250. })
  251. return
  252. }
  253. key = strings.TrimSpace(key)
  254. models, err := gemini.FetchGeminiModels(baseURL, key, channel.GetSetting().Proxy)
  255. if err != nil {
  256. c.JSON(http.StatusOK, gin.H{
  257. "success": false,
  258. "message": fmt.Sprintf("获取Gemini模型失败: %s", err.Error()),
  259. })
  260. return
  261. }
  262. c.JSON(http.StatusOK, gin.H{
  263. "success": true,
  264. "message": "",
  265. "data": models,
  266. })
  267. return
  268. }
  269. var url string
  270. switch channel.Type {
  271. case constant.ChannelTypeAli:
  272. url = fmt.Sprintf("%s/compatible-mode/v1/models", baseURL)
  273. case constant.ChannelTypeZhipu_v4:
  274. if plan, ok := constant.ChannelSpecialBases[baseURL]; ok && plan.OpenAIBaseURL != "" {
  275. url = fmt.Sprintf("%s/models", plan.OpenAIBaseURL)
  276. } else {
  277. url = fmt.Sprintf("%s/api/paas/v4/models", baseURL)
  278. }
  279. case constant.ChannelTypeVolcEngine:
  280. if plan, ok := constant.ChannelSpecialBases[baseURL]; ok && plan.OpenAIBaseURL != "" {
  281. url = fmt.Sprintf("%s/v1/models", plan.OpenAIBaseURL)
  282. } else {
  283. url = fmt.Sprintf("%s/v1/models", baseURL)
  284. }
  285. case constant.ChannelTypeMoonshot:
  286. if plan, ok := constant.ChannelSpecialBases[baseURL]; ok && plan.OpenAIBaseURL != "" {
  287. url = fmt.Sprintf("%s/models", plan.OpenAIBaseURL)
  288. } else {
  289. url = fmt.Sprintf("%s/v1/models", baseURL)
  290. }
  291. default:
  292. url = fmt.Sprintf("%s/v1/models", baseURL)
  293. }
  294. // 获取用于请求的可用密钥(多密钥渠道优先使用启用状态的密钥)
  295. key, _, apiErr := channel.GetNextEnabledKey()
  296. if apiErr != nil {
  297. c.JSON(http.StatusOK, gin.H{
  298. "success": false,
  299. "message": fmt.Sprintf("获取渠道密钥失败: %s", apiErr.Error()),
  300. })
  301. return
  302. }
  303. key = strings.TrimSpace(key)
  304. headers, err := buildFetchModelsHeaders(channel, key)
  305. if err != nil {
  306. common.ApiError(c, err)
  307. return
  308. }
  309. body, err := GetResponseBody("GET", url, channel, headers)
  310. if err != nil {
  311. common.ApiError(c, err)
  312. return
  313. }
  314. var result OpenAIModelsResponse
  315. if err = json.Unmarshal(body, &result); err != nil {
  316. c.JSON(http.StatusOK, gin.H{
  317. "success": false,
  318. "message": fmt.Sprintf("解析响应失败: %s", err.Error()),
  319. })
  320. return
  321. }
  322. var ids []string
  323. for _, model := range result.Data {
  324. id := model.ID
  325. if channel.Type == constant.ChannelTypeGemini {
  326. id = strings.TrimPrefix(id, "models/")
  327. }
  328. ids = append(ids, id)
  329. }
  330. c.JSON(http.StatusOK, gin.H{
  331. "success": true,
  332. "message": "",
  333. "data": ids,
  334. })
  335. }
  336. func FixChannelsAbilities(c *gin.Context) {
  337. success, fails, err := model.FixAbility()
  338. if err != nil {
  339. common.ApiError(c, err)
  340. return
  341. }
  342. c.JSON(http.StatusOK, gin.H{
  343. "success": true,
  344. "message": "",
  345. "data": gin.H{
  346. "success": success,
  347. "fails": fails,
  348. },
  349. })
  350. }
  351. func SearchChannels(c *gin.Context) {
  352. keyword := c.Query("keyword")
  353. group := c.Query("group")
  354. modelKeyword := c.Query("model")
  355. statusParam := c.Query("status")
  356. statusFilter := parseStatusFilter(statusParam)
  357. idSort, _ := strconv.ParseBool(c.Query("id_sort"))
  358. enableTagMode, _ := strconv.ParseBool(c.Query("tag_mode"))
  359. channelData := make([]*model.Channel, 0)
  360. if enableTagMode {
  361. tags, err := model.SearchTags(keyword, group, modelKeyword, idSort)
  362. if err != nil {
  363. c.JSON(http.StatusOK, gin.H{
  364. "success": false,
  365. "message": err.Error(),
  366. })
  367. return
  368. }
  369. for _, tag := range tags {
  370. if tag != nil && *tag != "" {
  371. tagChannel, err := model.GetChannelsByTag(*tag, idSort, false)
  372. if err == nil {
  373. channelData = append(channelData, tagChannel...)
  374. }
  375. }
  376. }
  377. } else {
  378. channels, err := model.SearchChannels(keyword, group, modelKeyword, idSort)
  379. if err != nil {
  380. c.JSON(http.StatusOK, gin.H{
  381. "success": false,
  382. "message": err.Error(),
  383. })
  384. return
  385. }
  386. channelData = channels
  387. }
  388. if statusFilter == common.ChannelStatusEnabled || statusFilter == 0 {
  389. filtered := make([]*model.Channel, 0, len(channelData))
  390. for _, ch := range channelData {
  391. if statusFilter == common.ChannelStatusEnabled && ch.Status != common.ChannelStatusEnabled {
  392. continue
  393. }
  394. if statusFilter == 0 && ch.Status == common.ChannelStatusEnabled {
  395. continue
  396. }
  397. filtered = append(filtered, ch)
  398. }
  399. channelData = filtered
  400. }
  401. // calculate type counts for search results
  402. typeCounts := make(map[int64]int64)
  403. for _, channel := range channelData {
  404. typeCounts[int64(channel.Type)]++
  405. }
  406. typeParam := c.Query("type")
  407. typeFilter := -1
  408. if typeParam != "" {
  409. if tp, err := strconv.Atoi(typeParam); err == nil {
  410. typeFilter = tp
  411. }
  412. }
  413. if typeFilter >= 0 {
  414. filtered := make([]*model.Channel, 0, len(channelData))
  415. for _, ch := range channelData {
  416. if ch.Type == typeFilter {
  417. filtered = append(filtered, ch)
  418. }
  419. }
  420. channelData = filtered
  421. }
  422. page, _ := strconv.Atoi(c.DefaultQuery("p", "1"))
  423. pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
  424. if page < 1 {
  425. page = 1
  426. }
  427. if pageSize <= 0 {
  428. pageSize = 20
  429. }
  430. total := len(channelData)
  431. startIdx := (page - 1) * pageSize
  432. if startIdx > total {
  433. startIdx = total
  434. }
  435. endIdx := startIdx + pageSize
  436. if endIdx > total {
  437. endIdx = total
  438. }
  439. pagedData := channelData[startIdx:endIdx]
  440. for _, datum := range pagedData {
  441. clearChannelInfo(datum)
  442. }
  443. c.JSON(http.StatusOK, gin.H{
  444. "success": true,
  445. "message": "",
  446. "data": gin.H{
  447. "items": pagedData,
  448. "total": total,
  449. "type_counts": typeCounts,
  450. },
  451. })
  452. return
  453. }
  454. func GetChannel(c *gin.Context) {
  455. id, err := strconv.Atoi(c.Param("id"))
  456. if err != nil {
  457. common.ApiError(c, err)
  458. return
  459. }
  460. channel, err := model.GetChannelById(id, false)
  461. if err != nil {
  462. common.ApiError(c, err)
  463. return
  464. }
  465. if channel != nil {
  466. clearChannelInfo(channel)
  467. }
  468. c.JSON(http.StatusOK, gin.H{
  469. "success": true,
  470. "message": "",
  471. "data": channel,
  472. })
  473. return
  474. }
  475. // GetChannelKey 获取渠道密钥(需要通过安全验证中间件)
  476. // 此函数依赖 SecureVerificationRequired 中间件,确保用户已通过安全验证
  477. func GetChannelKey(c *gin.Context) {
  478. userId := c.GetInt("id")
  479. channelId, err := strconv.Atoi(c.Param("id"))
  480. if err != nil {
  481. common.ApiError(c, fmt.Errorf("渠道ID格式错误: %v", err))
  482. return
  483. }
  484. // 获取渠道信息(包含密钥)
  485. channel, err := model.GetChannelById(channelId, true)
  486. if err != nil {
  487. common.ApiError(c, fmt.Errorf("获取渠道信息失败: %v", err))
  488. return
  489. }
  490. if channel == nil {
  491. common.ApiError(c, fmt.Errorf("渠道不存在"))
  492. return
  493. }
  494. // 记录操作日志
  495. model.RecordLog(userId, model.LogTypeSystem, fmt.Sprintf("查看渠道密钥信息 (渠道ID: %d)", channelId))
  496. // 返回渠道密钥
  497. c.JSON(http.StatusOK, gin.H{
  498. "success": true,
  499. "message": "获取成功",
  500. "data": map[string]interface{}{
  501. "key": channel.Key,
  502. },
  503. })
  504. }
  505. // validateTwoFactorAuth 统一的2FA验证函数
  506. func validateTwoFactorAuth(twoFA *model.TwoFA, code string) bool {
  507. // 尝试验证TOTP
  508. if cleanCode, err := common.ValidateNumericCode(code); err == nil {
  509. if isValid, _ := twoFA.ValidateTOTPAndUpdateUsage(cleanCode); isValid {
  510. return true
  511. }
  512. }
  513. // 尝试验证备用码
  514. if isValid, err := twoFA.ValidateBackupCodeAndUpdateUsage(code); err == nil && isValid {
  515. return true
  516. }
  517. return false
  518. }
  519. // validateChannel 通用的渠道校验函数
  520. func validateChannel(channel *model.Channel, isAdd bool) error {
  521. // 校验 channel settings
  522. if err := channel.ValidateSettings(); err != nil {
  523. return fmt.Errorf("渠道额外设置[channel setting] 格式错误:%s", err.Error())
  524. }
  525. // 如果是添加操作,检查 channel 和 key 是否为空
  526. if isAdd {
  527. if channel == nil || channel.Key == "" {
  528. return fmt.Errorf("channel cannot be empty")
  529. }
  530. if strings.TrimSpace(channel.PublicName) == "" {
  531. return fmt.Errorf("public name cannot be empty")
  532. }
  533. // 检查模型名称长度是否超过 255
  534. for _, m := range channel.GetModels() {
  535. if len(m) > 255 {
  536. return fmt.Errorf("模型名称过长: %s", m)
  537. }
  538. }
  539. }
  540. // VertexAI 特殊校验
  541. if channel.Type == constant.ChannelTypeVertexAi {
  542. if channel.Other == "" {
  543. return fmt.Errorf("部署地区不能为空")
  544. }
  545. regionMap, err := common.StrToMap(channel.Other)
  546. if err != nil {
  547. return fmt.Errorf("部署地区必须是标准的Json格式,例如{\"default\": \"us-central1\", \"region2\": \"us-east1\"}")
  548. }
  549. if regionMap["default"] == nil {
  550. return fmt.Errorf("部署地区必须包含default字段")
  551. }
  552. }
  553. // Codex OAuth key validation (optional, only when JSON object is provided)
  554. if channel.Type == constant.ChannelTypeCodex {
  555. trimmedKey := strings.TrimSpace(channel.Key)
  556. if isAdd && trimmedKey == "" {
  557. return fmt.Errorf("Codex key cannot be empty")
  558. }
  559. if strings.HasPrefix(trimmedKey, "{") {
  560. if _, err := common.ParseCodexOAuthCredential(trimmedKey); err != nil {
  561. if errors.Is(err, common.ErrCodexOAuthCredentialInvalidJSON) {
  562. return fmt.Errorf("Codex key must be a valid JSON object")
  563. }
  564. if errors.Is(err, common.ErrCodexOAuthAccessTokenRequired) {
  565. return fmt.Errorf("Codex key JSON must include access_token")
  566. }
  567. if errors.Is(err, common.ErrCodexOAuthAccountIDRequired) {
  568. return fmt.Errorf("Codex key JSON must include account_id")
  569. }
  570. return fmt.Errorf("Codex key must be a valid JSON object")
  571. }
  572. }
  573. }
  574. return nil
  575. }
  576. func RefreshCodexChannelCredential(c *gin.Context) {
  577. channelId, err := strconv.Atoi(c.Param("id"))
  578. if err != nil {
  579. common.ApiError(c, fmt.Errorf("invalid channel id: %w", err))
  580. return
  581. }
  582. ctx, cancel := context.WithTimeout(c.Request.Context(), 10*time.Second)
  583. defer cancel()
  584. oauthKey, ch, err := service.RefreshCodexChannelCredential(ctx, channelId, service.CodexCredentialRefreshOptions{ResetCaches: true})
  585. if err != nil {
  586. if errors.Is(err, common.ErrCodexOAuthCredentialRequired) {
  587. c.JSON(http.StatusOK, gin.H{"success": false, "message": "当前凭证方式不支持刷新凭证"})
  588. return
  589. }
  590. common.SysError("failed to refresh codex channel credential: " + err.Error())
  591. c.JSON(http.StatusOK, gin.H{"success": false, "message": "刷新凭证失败,请稍后重试"})
  592. return
  593. }
  594. c.JSON(http.StatusOK, gin.H{
  595. "success": true,
  596. "message": "refreshed",
  597. "data": gin.H{
  598. "expires_at": oauthKey.Expired,
  599. "last_refresh": oauthKey.LastRefresh,
  600. "account_id": oauthKey.AccountID,
  601. "email": oauthKey.Email,
  602. "channel_id": ch.Id,
  603. "channel_type": ch.Type,
  604. "channel_name": ch.Name,
  605. },
  606. })
  607. }
  608. type AddChannelRequest struct {
  609. Mode string `json:"mode"`
  610. MultiKeyMode constant.MultiKeyMode `json:"multi_key_mode"`
  611. BatchAddSetKeyPrefix2Name bool `json:"batch_add_set_key_prefix_2_name"`
  612. Channel *model.Channel `json:"channel"`
  613. }
  614. func getVertexArrayKeys(keys string) ([]string, error) {
  615. if keys == "" {
  616. return nil, nil
  617. }
  618. var keyArray []interface{}
  619. err := common.Unmarshal([]byte(keys), &keyArray)
  620. if err != nil {
  621. return nil, fmt.Errorf("批量添加 Vertex AI 必须使用标准的JsonArray格式,例如[{key1}, {key2}...],请检查输入: %w", err)
  622. }
  623. cleanKeys := make([]string, 0, len(keyArray))
  624. for _, key := range keyArray {
  625. var keyStr string
  626. switch v := key.(type) {
  627. case string:
  628. keyStr = strings.TrimSpace(v)
  629. default:
  630. bytes, err := json.Marshal(v)
  631. if err != nil {
  632. return nil, fmt.Errorf("Vertex AI key JSON 编码失败: %w", err)
  633. }
  634. keyStr = string(bytes)
  635. }
  636. if keyStr != "" {
  637. cleanKeys = append(cleanKeys, keyStr)
  638. }
  639. }
  640. if len(cleanKeys) == 0 {
  641. return nil, fmt.Errorf("批量添加 Vertex AI 的 keys 不能为空")
  642. }
  643. return cleanKeys, nil
  644. }
  645. func AddChannel(c *gin.Context) {
  646. addChannelRequest := AddChannelRequest{}
  647. err := c.ShouldBindJSON(&addChannelRequest)
  648. if err != nil {
  649. common.ApiError(c, err)
  650. return
  651. }
  652. // 使用统一的校验函数
  653. if err := validateChannel(addChannelRequest.Channel, true); err != nil {
  654. c.JSON(http.StatusOK, gin.H{
  655. "success": false,
  656. "message": err.Error(),
  657. })
  658. return
  659. }
  660. addChannelRequest.Channel.CreatedTime = common.GetTimestamp()
  661. keys := make([]string, 0)
  662. switch addChannelRequest.Mode {
  663. case "multi_to_single":
  664. addChannelRequest.Channel.ChannelInfo.IsMultiKey = true
  665. addChannelRequest.Channel.ChannelInfo.MultiKeyMode = addChannelRequest.MultiKeyMode
  666. if addChannelRequest.Channel.Type == constant.ChannelTypeVertexAi && addChannelRequest.Channel.GetOtherSettings().VertexKeyType != dto.VertexKeyTypeAPIKey {
  667. array, err := getVertexArrayKeys(addChannelRequest.Channel.Key)
  668. if err != nil {
  669. c.JSON(http.StatusOK, gin.H{
  670. "success": false,
  671. "message": err.Error(),
  672. })
  673. return
  674. }
  675. addChannelRequest.Channel.ChannelInfo.MultiKeySize = len(array)
  676. addChannelRequest.Channel.Key = strings.Join(array, "\n")
  677. } else {
  678. cleanKeys := make([]string, 0)
  679. for _, key := range strings.Split(addChannelRequest.Channel.Key, "\n") {
  680. if key == "" {
  681. continue
  682. }
  683. key = strings.TrimSpace(key)
  684. cleanKeys = append(cleanKeys, key)
  685. }
  686. addChannelRequest.Channel.ChannelInfo.MultiKeySize = len(cleanKeys)
  687. addChannelRequest.Channel.Key = strings.Join(cleanKeys, "\n")
  688. }
  689. keys = []string{addChannelRequest.Channel.Key}
  690. case "batch":
  691. if addChannelRequest.Channel.Type == constant.ChannelTypeVertexAi && addChannelRequest.Channel.GetOtherSettings().VertexKeyType != dto.VertexKeyTypeAPIKey {
  692. // multi json
  693. keys, err = getVertexArrayKeys(addChannelRequest.Channel.Key)
  694. if err != nil {
  695. c.JSON(http.StatusOK, gin.H{
  696. "success": false,
  697. "message": err.Error(),
  698. })
  699. return
  700. }
  701. } else {
  702. keys = strings.Split(addChannelRequest.Channel.Key, "\n")
  703. }
  704. case "single":
  705. keys = []string{addChannelRequest.Channel.Key}
  706. default:
  707. c.JSON(http.StatusOK, gin.H{
  708. "success": false,
  709. "message": "不支持的添加模式",
  710. })
  711. return
  712. }
  713. channels := make([]model.Channel, 0, len(keys))
  714. for _, key := range keys {
  715. if key == "" {
  716. continue
  717. }
  718. localChannel := addChannelRequest.Channel
  719. localChannel.Key = key
  720. if addChannelRequest.BatchAddSetKeyPrefix2Name && len(keys) > 1 {
  721. keyPrefix := localChannel.Key
  722. if len(localChannel.Key) > 8 {
  723. keyPrefix = localChannel.Key[:8]
  724. }
  725. localChannel.Name = fmt.Sprintf("%s %s", localChannel.Name, keyPrefix)
  726. }
  727. channels = append(channels, *localChannel)
  728. }
  729. err = model.BatchInsertChannels(channels)
  730. if err != nil {
  731. common.ApiError(c, err)
  732. return
  733. }
  734. service.ResetProxyClientCache()
  735. c.JSON(http.StatusOK, gin.H{
  736. "success": true,
  737. "message": "",
  738. })
  739. return
  740. }
  741. func DeleteChannel(c *gin.Context) {
  742. id, _ := strconv.Atoi(c.Param("id"))
  743. channel := model.Channel{Id: id}
  744. err := channel.Delete()
  745. if err != nil {
  746. common.ApiError(c, err)
  747. return
  748. }
  749. model.InitChannelCache()
  750. c.JSON(http.StatusOK, gin.H{
  751. "success": true,
  752. "message": "",
  753. })
  754. return
  755. }
  756. func DeleteDisabledChannel(c *gin.Context) {
  757. rows, err := model.DeleteDisabledChannel()
  758. if err != nil {
  759. common.ApiError(c, err)
  760. return
  761. }
  762. model.InitChannelCache()
  763. c.JSON(http.StatusOK, gin.H{
  764. "success": true,
  765. "message": "",
  766. "data": rows,
  767. })
  768. return
  769. }
  770. type ChannelTag struct {
  771. Tag string `json:"tag"`
  772. NewTag *string `json:"new_tag"`
  773. Priority *int64 `json:"priority"`
  774. Weight *uint `json:"weight"`
  775. ModelMapping *string `json:"model_mapping"`
  776. Models *string `json:"models"`
  777. Groups *string `json:"groups"`
  778. ParamOverride *string `json:"param_override"`
  779. HeaderOverride *string `json:"header_override"`
  780. }
  781. func DisableTagChannels(c *gin.Context) {
  782. channelTag := ChannelTag{}
  783. err := c.ShouldBindJSON(&channelTag)
  784. if err != nil || channelTag.Tag == "" {
  785. c.JSON(http.StatusOK, gin.H{
  786. "success": false,
  787. "message": "参数错误",
  788. })
  789. return
  790. }
  791. err = model.DisableChannelByTag(channelTag.Tag)
  792. if err != nil {
  793. common.ApiError(c, err)
  794. return
  795. }
  796. model.InitChannelCache()
  797. c.JSON(http.StatusOK, gin.H{
  798. "success": true,
  799. "message": "",
  800. })
  801. return
  802. }
  803. func EnableTagChannels(c *gin.Context) {
  804. channelTag := ChannelTag{}
  805. err := c.ShouldBindJSON(&channelTag)
  806. if err != nil || channelTag.Tag == "" {
  807. c.JSON(http.StatusOK, gin.H{
  808. "success": false,
  809. "message": "参数错误",
  810. })
  811. return
  812. }
  813. err = model.EnableChannelByTag(channelTag.Tag)
  814. if err != nil {
  815. common.ApiError(c, err)
  816. return
  817. }
  818. model.InitChannelCache()
  819. c.JSON(http.StatusOK, gin.H{
  820. "success": true,
  821. "message": "",
  822. })
  823. return
  824. }
  825. func EditTagChannels(c *gin.Context) {
  826. channelTag := ChannelTag{}
  827. err := c.ShouldBindJSON(&channelTag)
  828. if err != nil {
  829. c.JSON(http.StatusOK, gin.H{
  830. "success": false,
  831. "message": "参数错误",
  832. })
  833. return
  834. }
  835. if channelTag.Tag == "" {
  836. c.JSON(http.StatusOK, gin.H{
  837. "success": false,
  838. "message": "tag不能为空",
  839. })
  840. return
  841. }
  842. if channelTag.ParamOverride != nil {
  843. trimmed := strings.TrimSpace(*channelTag.ParamOverride)
  844. if trimmed != "" && !json.Valid([]byte(trimmed)) {
  845. c.JSON(http.StatusOK, gin.H{
  846. "success": false,
  847. "message": "参数覆盖必须是合法的 JSON 格式",
  848. })
  849. return
  850. }
  851. channelTag.ParamOverride = common.GetPointer[string](trimmed)
  852. }
  853. if channelTag.HeaderOverride != nil {
  854. trimmed := strings.TrimSpace(*channelTag.HeaderOverride)
  855. if trimmed != "" && !json.Valid([]byte(trimmed)) {
  856. c.JSON(http.StatusOK, gin.H{
  857. "success": false,
  858. "message": "请求头覆盖必须是合法的 JSON 格式",
  859. })
  860. return
  861. }
  862. channelTag.HeaderOverride = common.GetPointer[string](trimmed)
  863. }
  864. err = model.EditChannelByTag(channelTag.Tag, channelTag.NewTag, channelTag.ModelMapping, channelTag.Models, channelTag.Groups, channelTag.Priority, channelTag.Weight, channelTag.ParamOverride, channelTag.HeaderOverride)
  865. if err != nil {
  866. common.ApiError(c, err)
  867. return
  868. }
  869. model.InitChannelCache()
  870. c.JSON(http.StatusOK, gin.H{
  871. "success": true,
  872. "message": "",
  873. })
  874. return
  875. }
  876. type ChannelBatch struct {
  877. Ids []int `json:"ids"`
  878. Tag *string `json:"tag"`
  879. }
  880. func DeleteChannelBatch(c *gin.Context) {
  881. channelBatch := ChannelBatch{}
  882. err := c.ShouldBindJSON(&channelBatch)
  883. if err != nil || len(channelBatch.Ids) == 0 {
  884. c.JSON(http.StatusOK, gin.H{
  885. "success": false,
  886. "message": "参数错误",
  887. })
  888. return
  889. }
  890. err = model.BatchDeleteChannels(channelBatch.Ids)
  891. if err != nil {
  892. common.ApiError(c, err)
  893. return
  894. }
  895. model.InitChannelCache()
  896. c.JSON(http.StatusOK, gin.H{
  897. "success": true,
  898. "message": "",
  899. "data": len(channelBatch.Ids),
  900. })
  901. return
  902. }
  903. type PatchChannel struct {
  904. model.Channel
  905. MultiKeyMode *string `json:"multi_key_mode"`
  906. KeyMode *string `json:"key_mode"` // 多key模式下密钥覆盖或者追加
  907. }
  908. func UpdateChannel(c *gin.Context) {
  909. channel := PatchChannel{}
  910. err := c.ShouldBindJSON(&channel)
  911. if err != nil {
  912. common.ApiError(c, err)
  913. return
  914. }
  915. // 使用统一的校验函数
  916. if err := validateChannel(&channel.Channel, false); err != nil {
  917. c.JSON(http.StatusOK, gin.H{
  918. "success": false,
  919. "message": err.Error(),
  920. })
  921. return
  922. }
  923. // Preserve existing ChannelInfo to ensure multi-key channels keep correct state even if the client does not send ChannelInfo in the request.
  924. originChannel, err := model.GetChannelById(channel.Id, true)
  925. if err != nil {
  926. c.JSON(http.StatusOK, gin.H{
  927. "success": false,
  928. "message": err.Error(),
  929. })
  930. return
  931. }
  932. // Always copy the original ChannelInfo so that fields like IsMultiKey and MultiKeySize are retained.
  933. channel.ChannelInfo = originChannel.ChannelInfo
  934. // If the request explicitly specifies a new MultiKeyMode, apply it on top of the original info.
  935. if channel.MultiKeyMode != nil && *channel.MultiKeyMode != "" {
  936. channel.ChannelInfo.MultiKeyMode = constant.MultiKeyMode(*channel.MultiKeyMode)
  937. }
  938. // 处理多key模式下的密钥追加/覆盖逻辑
  939. if channel.KeyMode != nil && channel.ChannelInfo.IsMultiKey {
  940. switch *channel.KeyMode {
  941. case "append":
  942. // 追加模式:将新密钥添加到现有密钥列表
  943. if originChannel.Key != "" {
  944. var newKeys []string
  945. var existingKeys []string
  946. // 解析现有密钥
  947. if strings.HasPrefix(strings.TrimSpace(originChannel.Key), "[") {
  948. // JSON数组格式
  949. var arr []json.RawMessage
  950. if err := json.Unmarshal([]byte(strings.TrimSpace(originChannel.Key)), &arr); err == nil {
  951. existingKeys = make([]string, len(arr))
  952. for i, v := range arr {
  953. existingKeys[i] = string(v)
  954. }
  955. }
  956. } else {
  957. // 换行分隔格式
  958. existingKeys = strings.Split(strings.Trim(originChannel.Key, "\n"), "\n")
  959. }
  960. // 处理 Vertex AI 的特殊情况
  961. if channel.Type == constant.ChannelTypeVertexAi && channel.GetOtherSettings().VertexKeyType != dto.VertexKeyTypeAPIKey {
  962. // 尝试解析新密钥为JSON数组
  963. if strings.HasPrefix(strings.TrimSpace(channel.Key), "[") {
  964. array, err := getVertexArrayKeys(channel.Key)
  965. if err != nil {
  966. c.JSON(http.StatusOK, gin.H{
  967. "success": false,
  968. "message": "追加密钥解析失败: " + err.Error(),
  969. })
  970. return
  971. }
  972. newKeys = array
  973. } else {
  974. // 单个JSON密钥
  975. newKeys = []string{channel.Key}
  976. }
  977. } else {
  978. // 普通渠道的处理
  979. inputKeys := strings.Split(channel.Key, "\n")
  980. for _, key := range inputKeys {
  981. key = strings.TrimSpace(key)
  982. if key != "" {
  983. newKeys = append(newKeys, key)
  984. }
  985. }
  986. }
  987. seen := make(map[string]struct{}, len(existingKeys)+len(newKeys))
  988. for _, key := range existingKeys {
  989. normalized := strings.TrimSpace(key)
  990. if normalized == "" {
  991. continue
  992. }
  993. seen[normalized] = struct{}{}
  994. }
  995. dedupedNewKeys := make([]string, 0, len(newKeys))
  996. for _, key := range newKeys {
  997. normalized := strings.TrimSpace(key)
  998. if normalized == "" {
  999. continue
  1000. }
  1001. if _, ok := seen[normalized]; ok {
  1002. continue
  1003. }
  1004. seen[normalized] = struct{}{}
  1005. dedupedNewKeys = append(dedupedNewKeys, normalized)
  1006. }
  1007. allKeys := append(existingKeys, dedupedNewKeys...)
  1008. channel.Key = strings.Join(allKeys, "\n")
  1009. }
  1010. case "replace":
  1011. // 覆盖模式:直接使用新密钥(默认行为,不需要特殊处理)
  1012. }
  1013. }
  1014. err = channel.Update()
  1015. if err != nil {
  1016. common.ApiError(c, err)
  1017. return
  1018. }
  1019. model.InitChannelCache()
  1020. service.ResetProxyClientCache()
  1021. channel.Key = ""
  1022. clearChannelInfo(&channel.Channel)
  1023. c.JSON(http.StatusOK, gin.H{
  1024. "success": true,
  1025. "message": "",
  1026. "data": channel,
  1027. })
  1028. return
  1029. }
  1030. func FetchModels(c *gin.Context) {
  1031. var req struct {
  1032. BaseURL string `json:"base_url"`
  1033. Type int `json:"type"`
  1034. Key string `json:"key"`
  1035. }
  1036. if err := c.ShouldBindJSON(&req); err != nil {
  1037. c.JSON(http.StatusBadRequest, gin.H{
  1038. "success": false,
  1039. "message": "Invalid request",
  1040. })
  1041. return
  1042. }
  1043. baseURL := req.BaseURL
  1044. if baseURL == "" {
  1045. baseURL = constant.ChannelBaseURLs[req.Type]
  1046. }
  1047. // remove line breaks and extra spaces.
  1048. key := strings.TrimSpace(req.Key)
  1049. key = strings.Split(key, "\n")[0]
  1050. if req.Type == constant.ChannelTypeOllama {
  1051. models, err := ollama.FetchOllamaModels(baseURL, key)
  1052. if err != nil {
  1053. c.JSON(http.StatusOK, gin.H{
  1054. "success": false,
  1055. "message": fmt.Sprintf("获取Ollama模型失败: %s", err.Error()),
  1056. })
  1057. return
  1058. }
  1059. names := make([]string, 0, len(models))
  1060. for _, modelInfo := range models {
  1061. names = append(names, modelInfo.Name)
  1062. }
  1063. c.JSON(http.StatusOK, gin.H{
  1064. "success": true,
  1065. "data": names,
  1066. })
  1067. return
  1068. }
  1069. if req.Type == constant.ChannelTypeGemini {
  1070. models, err := gemini.FetchGeminiModels(baseURL, key, "")
  1071. if err != nil {
  1072. c.JSON(http.StatusOK, gin.H{
  1073. "success": false,
  1074. "message": fmt.Sprintf("获取Gemini模型失败: %s", err.Error()),
  1075. })
  1076. return
  1077. }
  1078. c.JSON(http.StatusOK, gin.H{
  1079. "success": true,
  1080. "data": models,
  1081. })
  1082. return
  1083. }
  1084. client := &http.Client{}
  1085. url := fmt.Sprintf("%s/v1/models", baseURL)
  1086. request, err := http.NewRequest("GET", url, nil)
  1087. if err != nil {
  1088. c.JSON(http.StatusInternalServerError, gin.H{
  1089. "success": false,
  1090. "message": err.Error(),
  1091. })
  1092. return
  1093. }
  1094. request.Header.Set("Authorization", "Bearer "+key)
  1095. response, err := client.Do(request)
  1096. if err != nil {
  1097. c.JSON(http.StatusInternalServerError, gin.H{
  1098. "success": false,
  1099. "message": err.Error(),
  1100. })
  1101. return
  1102. }
  1103. //check status code
  1104. if response.StatusCode != http.StatusOK {
  1105. c.JSON(http.StatusInternalServerError, gin.H{
  1106. "success": false,
  1107. "message": "Failed to fetch models",
  1108. })
  1109. return
  1110. }
  1111. defer response.Body.Close()
  1112. var result struct {
  1113. Data []struct {
  1114. ID string `json:"id"`
  1115. } `json:"data"`
  1116. }
  1117. if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
  1118. c.JSON(http.StatusInternalServerError, gin.H{
  1119. "success": false,
  1120. "message": err.Error(),
  1121. })
  1122. return
  1123. }
  1124. var models []string
  1125. for _, model := range result.Data {
  1126. models = append(models, model.ID)
  1127. }
  1128. c.JSON(http.StatusOK, gin.H{
  1129. "success": true,
  1130. "data": models,
  1131. })
  1132. }
  1133. func BatchSetChannelTag(c *gin.Context) {
  1134. channelBatch := ChannelBatch{}
  1135. err := c.ShouldBindJSON(&channelBatch)
  1136. if err != nil || len(channelBatch.Ids) == 0 {
  1137. c.JSON(http.StatusOK, gin.H{
  1138. "success": false,
  1139. "message": "参数错误",
  1140. })
  1141. return
  1142. }
  1143. err = model.BatchSetChannelTag(channelBatch.Ids, channelBatch.Tag)
  1144. if err != nil {
  1145. common.ApiError(c, err)
  1146. return
  1147. }
  1148. model.InitChannelCache()
  1149. c.JSON(http.StatusOK, gin.H{
  1150. "success": true,
  1151. "message": "",
  1152. "data": len(channelBatch.Ids),
  1153. })
  1154. return
  1155. }
  1156. func GetTagModels(c *gin.Context) {
  1157. tag := c.Query("tag")
  1158. if tag == "" {
  1159. c.JSON(http.StatusBadRequest, gin.H{
  1160. "success": false,
  1161. "message": "tag不能为空",
  1162. })
  1163. return
  1164. }
  1165. channels, err := model.GetChannelsByTag(tag, false, false) // idSort=false, selectAll=false
  1166. if err != nil {
  1167. c.JSON(http.StatusInternalServerError, gin.H{
  1168. "success": false,
  1169. "message": err.Error(),
  1170. })
  1171. return
  1172. }
  1173. var longestModels string
  1174. maxLength := 0
  1175. // Find the longest models string among all channels with the given tag
  1176. for _, channel := range channels {
  1177. if channel.Models != "" {
  1178. currentModels := strings.Split(channel.Models, ",")
  1179. if len(currentModels) > maxLength {
  1180. maxLength = len(currentModels)
  1181. longestModels = channel.Models
  1182. }
  1183. }
  1184. }
  1185. c.JSON(http.StatusOK, gin.H{
  1186. "success": true,
  1187. "message": "",
  1188. "data": longestModels,
  1189. })
  1190. return
  1191. }
  1192. // CopyChannel handles cloning an existing channel with its key.
  1193. // POST /api/channel/copy/:id
  1194. // Optional query params:
  1195. //
  1196. // suffix - string appended to the original name (default "_复制")
  1197. // reset_balance - bool, when true will reset balance & used_quota to 0 (default true)
  1198. func CopyChannel(c *gin.Context) {
  1199. id, err := strconv.Atoi(c.Param("id"))
  1200. if err != nil {
  1201. c.JSON(http.StatusOK, gin.H{"success": false, "message": "invalid id"})
  1202. return
  1203. }
  1204. suffix := c.DefaultQuery("suffix", "_复制")
  1205. resetBalance := true
  1206. if rbStr := c.DefaultQuery("reset_balance", "true"); rbStr != "" {
  1207. if v, err := strconv.ParseBool(rbStr); err == nil {
  1208. resetBalance = v
  1209. }
  1210. }
  1211. // fetch original channel with key
  1212. origin, err := model.GetChannelById(id, true)
  1213. if err != nil {
  1214. common.SysError("failed to get channel by id: " + err.Error())
  1215. c.JSON(http.StatusOK, gin.H{"success": false, "message": "获取渠道信息失败,请稍后重试"})
  1216. return
  1217. }
  1218. // clone channel
  1219. clone := *origin // shallow copy is sufficient as we will overwrite primitives
  1220. clone.Id = 0 // let DB auto-generate
  1221. clone.CreatedTime = common.GetTimestamp()
  1222. clone.Name = origin.Name + suffix
  1223. clone.TestTime = 0
  1224. clone.ResponseTime = 0
  1225. if resetBalance {
  1226. clone.Balance = 0
  1227. clone.UsedQuota = 0
  1228. }
  1229. // insert
  1230. if err := model.BatchInsertChannels([]model.Channel{clone}); err != nil {
  1231. common.SysError("failed to clone channel: " + err.Error())
  1232. c.JSON(http.StatusOK, gin.H{"success": false, "message": "复制渠道失败,请稍后重试"})
  1233. return
  1234. }
  1235. model.InitChannelCache()
  1236. // success
  1237. c.JSON(http.StatusOK, gin.H{"success": true, "message": "", "data": gin.H{"id": clone.Id}})
  1238. }
  1239. // MultiKeyManageRequest represents the request for multi-key management operations
  1240. type MultiKeyManageRequest struct {
  1241. ChannelId int `json:"channel_id"`
  1242. Action string `json:"action"` // "disable_key", "enable_key", "delete_key", "delete_disabled_keys", "get_key_status"
  1243. KeyIndex *int `json:"key_index,omitempty"` // for disable_key, enable_key, and delete_key actions
  1244. Page int `json:"page,omitempty"` // for get_key_status pagination
  1245. PageSize int `json:"page_size,omitempty"` // for get_key_status pagination
  1246. Status *int `json:"status,omitempty"` // for get_key_status filtering: 1=enabled, 2=manual_disabled, 3=auto_disabled, nil=all
  1247. }
  1248. // MultiKeyStatusResponse represents the response for key status query
  1249. type MultiKeyStatusResponse struct {
  1250. Keys []KeyStatus `json:"keys"`
  1251. Total int `json:"total"`
  1252. Page int `json:"page"`
  1253. PageSize int `json:"page_size"`
  1254. TotalPages int `json:"total_pages"`
  1255. // Statistics
  1256. EnabledCount int `json:"enabled_count"`
  1257. ManualDisabledCount int `json:"manual_disabled_count"`
  1258. AutoDisabledCount int `json:"auto_disabled_count"`
  1259. }
  1260. type KeyStatus struct {
  1261. Index int `json:"index"`
  1262. Status int `json:"status"` // 1: enabled, 2: disabled
  1263. DisabledTime int64 `json:"disabled_time,omitempty"`
  1264. Reason string `json:"reason,omitempty"`
  1265. KeyPreview string `json:"key_preview"` // first 10 chars of key for identification
  1266. }
  1267. // ManageMultiKeys handles multi-key management operations
  1268. func ManageMultiKeys(c *gin.Context) {
  1269. request := MultiKeyManageRequest{}
  1270. err := c.ShouldBindJSON(&request)
  1271. if err != nil {
  1272. common.ApiError(c, err)
  1273. return
  1274. }
  1275. channel, err := model.GetChannelById(request.ChannelId, true)
  1276. if err != nil {
  1277. c.JSON(http.StatusOK, gin.H{
  1278. "success": false,
  1279. "message": "渠道不存在",
  1280. })
  1281. return
  1282. }
  1283. if !channel.ChannelInfo.IsMultiKey {
  1284. c.JSON(http.StatusOK, gin.H{
  1285. "success": false,
  1286. "message": "该渠道不是多密钥模式",
  1287. })
  1288. return
  1289. }
  1290. lock := model.GetChannelPollingLock(channel.Id)
  1291. lock.Lock()
  1292. defer lock.Unlock()
  1293. switch request.Action {
  1294. case "get_key_status":
  1295. keys := channel.GetKeys()
  1296. // Default pagination parameters
  1297. page := request.Page
  1298. pageSize := request.PageSize
  1299. if page <= 0 {
  1300. page = 1
  1301. }
  1302. if pageSize <= 0 {
  1303. pageSize = 50 // Default page size
  1304. }
  1305. // Statistics for all keys (unchanged by filtering)
  1306. var enabledCount, manualDisabledCount, autoDisabledCount int
  1307. // Build all key status data first
  1308. var allKeyStatusList []KeyStatus
  1309. for i, key := range keys {
  1310. status := 1 // default enabled
  1311. var disabledTime int64
  1312. var reason string
  1313. if channel.ChannelInfo.MultiKeyStatusList != nil {
  1314. if s, exists := channel.ChannelInfo.MultiKeyStatusList[i]; exists {
  1315. status = s
  1316. }
  1317. }
  1318. // Count for statistics (all keys)
  1319. switch status {
  1320. case 1:
  1321. enabledCount++
  1322. case 2:
  1323. manualDisabledCount++
  1324. case 3:
  1325. autoDisabledCount++
  1326. }
  1327. if status != 1 {
  1328. if channel.ChannelInfo.MultiKeyDisabledTime != nil {
  1329. disabledTime = channel.ChannelInfo.MultiKeyDisabledTime[i]
  1330. }
  1331. if channel.ChannelInfo.MultiKeyDisabledReason != nil {
  1332. reason = channel.ChannelInfo.MultiKeyDisabledReason[i]
  1333. }
  1334. }
  1335. // Create key preview (first 10 chars)
  1336. keyPreview := key
  1337. if len(key) > 10 {
  1338. keyPreview = key[:10] + "..."
  1339. }
  1340. allKeyStatusList = append(allKeyStatusList, KeyStatus{
  1341. Index: i,
  1342. Status: status,
  1343. DisabledTime: disabledTime,
  1344. Reason: reason,
  1345. KeyPreview: keyPreview,
  1346. })
  1347. }
  1348. // Apply status filter if specified
  1349. var filteredKeyStatusList []KeyStatus
  1350. if request.Status != nil {
  1351. for _, keyStatus := range allKeyStatusList {
  1352. if keyStatus.Status == *request.Status {
  1353. filteredKeyStatusList = append(filteredKeyStatusList, keyStatus)
  1354. }
  1355. }
  1356. } else {
  1357. filteredKeyStatusList = allKeyStatusList
  1358. }
  1359. // Calculate pagination based on filtered results
  1360. filteredTotal := len(filteredKeyStatusList)
  1361. totalPages := (filteredTotal + pageSize - 1) / pageSize
  1362. if totalPages == 0 {
  1363. totalPages = 1
  1364. }
  1365. if page > totalPages {
  1366. page = totalPages
  1367. }
  1368. // Calculate range for current page
  1369. start := (page - 1) * pageSize
  1370. end := start + pageSize
  1371. if end > filteredTotal {
  1372. end = filteredTotal
  1373. }
  1374. // Get the page data
  1375. var pageKeyStatusList []KeyStatus
  1376. if start < filteredTotal {
  1377. pageKeyStatusList = filteredKeyStatusList[start:end]
  1378. }
  1379. c.JSON(http.StatusOK, gin.H{
  1380. "success": true,
  1381. "message": "",
  1382. "data": MultiKeyStatusResponse{
  1383. Keys: pageKeyStatusList,
  1384. Total: filteredTotal, // Total of filtered results
  1385. Page: page,
  1386. PageSize: pageSize,
  1387. TotalPages: totalPages,
  1388. EnabledCount: enabledCount, // Overall statistics
  1389. ManualDisabledCount: manualDisabledCount, // Overall statistics
  1390. AutoDisabledCount: autoDisabledCount, // Overall statistics
  1391. },
  1392. })
  1393. return
  1394. case "disable_key":
  1395. if request.KeyIndex == nil {
  1396. c.JSON(http.StatusOK, gin.H{
  1397. "success": false,
  1398. "message": "未指定要禁用的密钥索引",
  1399. })
  1400. return
  1401. }
  1402. keyIndex := *request.KeyIndex
  1403. if keyIndex < 0 || keyIndex >= channel.ChannelInfo.MultiKeySize {
  1404. c.JSON(http.StatusOK, gin.H{
  1405. "success": false,
  1406. "message": "密钥索引超出范围",
  1407. })
  1408. return
  1409. }
  1410. if channel.ChannelInfo.MultiKeyStatusList == nil {
  1411. channel.ChannelInfo.MultiKeyStatusList = make(map[int]int)
  1412. }
  1413. if channel.ChannelInfo.MultiKeyDisabledTime == nil {
  1414. channel.ChannelInfo.MultiKeyDisabledTime = make(map[int]int64)
  1415. }
  1416. if channel.ChannelInfo.MultiKeyDisabledReason == nil {
  1417. channel.ChannelInfo.MultiKeyDisabledReason = make(map[int]string)
  1418. }
  1419. channel.ChannelInfo.MultiKeyStatusList[keyIndex] = 2 // disabled
  1420. err = channel.Update()
  1421. if err != nil {
  1422. common.ApiError(c, err)
  1423. return
  1424. }
  1425. model.InitChannelCache()
  1426. c.JSON(http.StatusOK, gin.H{
  1427. "success": true,
  1428. "message": "密钥已禁用",
  1429. })
  1430. return
  1431. case "enable_key":
  1432. if request.KeyIndex == nil {
  1433. c.JSON(http.StatusOK, gin.H{
  1434. "success": false,
  1435. "message": "未指定要启用的密钥索引",
  1436. })
  1437. return
  1438. }
  1439. keyIndex := *request.KeyIndex
  1440. if keyIndex < 0 || keyIndex >= channel.ChannelInfo.MultiKeySize {
  1441. c.JSON(http.StatusOK, gin.H{
  1442. "success": false,
  1443. "message": "密钥索引超出范围",
  1444. })
  1445. return
  1446. }
  1447. // 从状态列表中删除该密钥的记录,使其回到默认启用状态
  1448. if channel.ChannelInfo.MultiKeyStatusList != nil {
  1449. delete(channel.ChannelInfo.MultiKeyStatusList, keyIndex)
  1450. }
  1451. if channel.ChannelInfo.MultiKeyDisabledTime != nil {
  1452. delete(channel.ChannelInfo.MultiKeyDisabledTime, keyIndex)
  1453. }
  1454. if channel.ChannelInfo.MultiKeyDisabledReason != nil {
  1455. delete(channel.ChannelInfo.MultiKeyDisabledReason, keyIndex)
  1456. }
  1457. err = channel.Update()
  1458. if err != nil {
  1459. common.ApiError(c, err)
  1460. return
  1461. }
  1462. model.InitChannelCache()
  1463. c.JSON(http.StatusOK, gin.H{
  1464. "success": true,
  1465. "message": "密钥已启用",
  1466. })
  1467. return
  1468. case "enable_all_keys":
  1469. // 清空所有禁用状态,使所有密钥回到默认启用状态
  1470. var enabledCount int
  1471. if channel.ChannelInfo.MultiKeyStatusList != nil {
  1472. enabledCount = len(channel.ChannelInfo.MultiKeyStatusList)
  1473. }
  1474. channel.ChannelInfo.MultiKeyStatusList = make(map[int]int)
  1475. channel.ChannelInfo.MultiKeyDisabledTime = make(map[int]int64)
  1476. channel.ChannelInfo.MultiKeyDisabledReason = make(map[int]string)
  1477. err = channel.Update()
  1478. if err != nil {
  1479. common.ApiError(c, err)
  1480. return
  1481. }
  1482. model.InitChannelCache()
  1483. c.JSON(http.StatusOK, gin.H{
  1484. "success": true,
  1485. "message": fmt.Sprintf("已启用 %d 个密钥", enabledCount),
  1486. })
  1487. return
  1488. case "disable_all_keys":
  1489. // 禁用所有启用的密钥
  1490. if channel.ChannelInfo.MultiKeyStatusList == nil {
  1491. channel.ChannelInfo.MultiKeyStatusList = make(map[int]int)
  1492. }
  1493. if channel.ChannelInfo.MultiKeyDisabledTime == nil {
  1494. channel.ChannelInfo.MultiKeyDisabledTime = make(map[int]int64)
  1495. }
  1496. if channel.ChannelInfo.MultiKeyDisabledReason == nil {
  1497. channel.ChannelInfo.MultiKeyDisabledReason = make(map[int]string)
  1498. }
  1499. var disabledCount int
  1500. for i := 0; i < channel.ChannelInfo.MultiKeySize; i++ {
  1501. status := 1 // default enabled
  1502. if s, exists := channel.ChannelInfo.MultiKeyStatusList[i]; exists {
  1503. status = s
  1504. }
  1505. // 只禁用当前启用的密钥
  1506. if status == 1 {
  1507. channel.ChannelInfo.MultiKeyStatusList[i] = 2 // disabled
  1508. disabledCount++
  1509. }
  1510. }
  1511. if disabledCount == 0 {
  1512. c.JSON(http.StatusOK, gin.H{
  1513. "success": false,
  1514. "message": "没有可禁用的密钥",
  1515. })
  1516. return
  1517. }
  1518. err = channel.Update()
  1519. if err != nil {
  1520. common.ApiError(c, err)
  1521. return
  1522. }
  1523. model.InitChannelCache()
  1524. c.JSON(http.StatusOK, gin.H{
  1525. "success": true,
  1526. "message": fmt.Sprintf("已禁用 %d 个密钥", disabledCount),
  1527. })
  1528. return
  1529. case "delete_key":
  1530. if request.KeyIndex == nil {
  1531. c.JSON(http.StatusOK, gin.H{
  1532. "success": false,
  1533. "message": "未指定要删除的密钥索引",
  1534. })
  1535. return
  1536. }
  1537. keyIndex := *request.KeyIndex
  1538. if keyIndex < 0 || keyIndex >= channel.ChannelInfo.MultiKeySize {
  1539. c.JSON(http.StatusOK, gin.H{
  1540. "success": false,
  1541. "message": "密钥索引超出范围",
  1542. })
  1543. return
  1544. }
  1545. keys := channel.GetKeys()
  1546. var remainingKeys []string
  1547. var newStatusList = make(map[int]int)
  1548. var newDisabledTime = make(map[int]int64)
  1549. var newDisabledReason = make(map[int]string)
  1550. newIndex := 0
  1551. for i, key := range keys {
  1552. // 跳过要删除的密钥
  1553. if i == keyIndex {
  1554. continue
  1555. }
  1556. remainingKeys = append(remainingKeys, key)
  1557. // 保留其他密钥的状态信息,重新索引
  1558. if channel.ChannelInfo.MultiKeyStatusList != nil {
  1559. if status, exists := channel.ChannelInfo.MultiKeyStatusList[i]; exists && status != 1 {
  1560. newStatusList[newIndex] = status
  1561. }
  1562. }
  1563. if channel.ChannelInfo.MultiKeyDisabledTime != nil {
  1564. if t, exists := channel.ChannelInfo.MultiKeyDisabledTime[i]; exists {
  1565. newDisabledTime[newIndex] = t
  1566. }
  1567. }
  1568. if channel.ChannelInfo.MultiKeyDisabledReason != nil {
  1569. if r, exists := channel.ChannelInfo.MultiKeyDisabledReason[i]; exists {
  1570. newDisabledReason[newIndex] = r
  1571. }
  1572. }
  1573. newIndex++
  1574. }
  1575. if len(remainingKeys) == 0 {
  1576. c.JSON(http.StatusOK, gin.H{
  1577. "success": false,
  1578. "message": "不能删除最后一个密钥",
  1579. })
  1580. return
  1581. }
  1582. // Update channel with remaining keys
  1583. channel.Key = strings.Join(remainingKeys, "\n")
  1584. channel.ChannelInfo.MultiKeySize = len(remainingKeys)
  1585. channel.ChannelInfo.MultiKeyStatusList = newStatusList
  1586. channel.ChannelInfo.MultiKeyDisabledTime = newDisabledTime
  1587. channel.ChannelInfo.MultiKeyDisabledReason = newDisabledReason
  1588. err = channel.Update()
  1589. if err != nil {
  1590. common.ApiError(c, err)
  1591. return
  1592. }
  1593. model.InitChannelCache()
  1594. c.JSON(http.StatusOK, gin.H{
  1595. "success": true,
  1596. "message": "密钥已删除",
  1597. })
  1598. return
  1599. case "delete_disabled_keys":
  1600. keys := channel.GetKeys()
  1601. var remainingKeys []string
  1602. var deletedCount int
  1603. var newStatusList = make(map[int]int)
  1604. var newDisabledTime = make(map[int]int64)
  1605. var newDisabledReason = make(map[int]string)
  1606. newIndex := 0
  1607. for i, key := range keys {
  1608. status := 1 // default enabled
  1609. if channel.ChannelInfo.MultiKeyStatusList != nil {
  1610. if s, exists := channel.ChannelInfo.MultiKeyStatusList[i]; exists {
  1611. status = s
  1612. }
  1613. }
  1614. // 只删除自动禁用(status == 3)的密钥,保留启用(status == 1)和手动禁用(status == 2)的密钥
  1615. if status == 3 {
  1616. deletedCount++
  1617. } else {
  1618. remainingKeys = append(remainingKeys, key)
  1619. // 保留非自动禁用密钥的状态信息,重新索引
  1620. if status != 1 {
  1621. newStatusList[newIndex] = status
  1622. if channel.ChannelInfo.MultiKeyDisabledTime != nil {
  1623. if t, exists := channel.ChannelInfo.MultiKeyDisabledTime[i]; exists {
  1624. newDisabledTime[newIndex] = t
  1625. }
  1626. }
  1627. if channel.ChannelInfo.MultiKeyDisabledReason != nil {
  1628. if r, exists := channel.ChannelInfo.MultiKeyDisabledReason[i]; exists {
  1629. newDisabledReason[newIndex] = r
  1630. }
  1631. }
  1632. }
  1633. newIndex++
  1634. }
  1635. }
  1636. if deletedCount == 0 {
  1637. c.JSON(http.StatusOK, gin.H{
  1638. "success": false,
  1639. "message": "没有需要删除的自动禁用密钥",
  1640. })
  1641. return
  1642. }
  1643. // Update channel with remaining keys
  1644. channel.Key = strings.Join(remainingKeys, "\n")
  1645. channel.ChannelInfo.MultiKeySize = len(remainingKeys)
  1646. channel.ChannelInfo.MultiKeyStatusList = newStatusList
  1647. channel.ChannelInfo.MultiKeyDisabledTime = newDisabledTime
  1648. channel.ChannelInfo.MultiKeyDisabledReason = newDisabledReason
  1649. err = channel.Update()
  1650. if err != nil {
  1651. common.ApiError(c, err)
  1652. return
  1653. }
  1654. model.InitChannelCache()
  1655. c.JSON(http.StatusOK, gin.H{
  1656. "success": true,
  1657. "message": fmt.Sprintf("已删除 %d 个自动禁用的密钥", deletedCount),
  1658. "data": deletedCount,
  1659. })
  1660. return
  1661. default:
  1662. c.JSON(http.StatusOK, gin.H{
  1663. "success": false,
  1664. "message": "不支持的操作",
  1665. })
  1666. return
  1667. }
  1668. }
  1669. // OllamaPullModel 拉取 Ollama 模型
  1670. func OllamaPullModel(c *gin.Context) {
  1671. var req struct {
  1672. ChannelID int `json:"channel_id"`
  1673. ModelName string `json:"model_name"`
  1674. }
  1675. if err := c.ShouldBindJSON(&req); err != nil {
  1676. c.JSON(http.StatusBadRequest, gin.H{
  1677. "success": false,
  1678. "message": "Invalid request parameters",
  1679. })
  1680. return
  1681. }
  1682. if req.ChannelID == 0 || req.ModelName == "" {
  1683. c.JSON(http.StatusBadRequest, gin.H{
  1684. "success": false,
  1685. "message": "Channel ID and model name are required",
  1686. })
  1687. return
  1688. }
  1689. // 获取渠道信息
  1690. channel, err := model.GetChannelById(req.ChannelID, true)
  1691. if err != nil {
  1692. c.JSON(http.StatusNotFound, gin.H{
  1693. "success": false,
  1694. "message": "Channel not found",
  1695. })
  1696. return
  1697. }
  1698. // 检查是否是 Ollama 渠道
  1699. if channel.Type != constant.ChannelTypeOllama {
  1700. c.JSON(http.StatusBadRequest, gin.H{
  1701. "success": false,
  1702. "message": "This operation is only supported for Ollama channels",
  1703. })
  1704. return
  1705. }
  1706. baseURL := constant.ChannelBaseURLs[channel.Type]
  1707. if channel.GetBaseURL() != "" {
  1708. baseURL = channel.GetBaseURL()
  1709. }
  1710. key := strings.Split(channel.Key, "\n")[0]
  1711. err = ollama.PullOllamaModel(baseURL, key, req.ModelName)
  1712. if err != nil {
  1713. c.JSON(http.StatusInternalServerError, gin.H{
  1714. "success": false,
  1715. "message": fmt.Sprintf("Failed to pull model: %s", err.Error()),
  1716. })
  1717. return
  1718. }
  1719. c.JSON(http.StatusOK, gin.H{
  1720. "success": true,
  1721. "message": fmt.Sprintf("Model %s pulled successfully", req.ModelName),
  1722. })
  1723. }
  1724. // OllamaPullModelStream 流式拉取 Ollama 模型
  1725. func OllamaPullModelStream(c *gin.Context) {
  1726. var req struct {
  1727. ChannelID int `json:"channel_id"`
  1728. ModelName string `json:"model_name"`
  1729. }
  1730. if err := c.ShouldBindJSON(&req); err != nil {
  1731. c.JSON(http.StatusBadRequest, gin.H{
  1732. "success": false,
  1733. "message": "Invalid request parameters",
  1734. })
  1735. return
  1736. }
  1737. if req.ChannelID == 0 || req.ModelName == "" {
  1738. c.JSON(http.StatusBadRequest, gin.H{
  1739. "success": false,
  1740. "message": "Channel ID and model name are required",
  1741. })
  1742. return
  1743. }
  1744. // 获取渠道信息
  1745. channel, err := model.GetChannelById(req.ChannelID, true)
  1746. if err != nil {
  1747. c.JSON(http.StatusNotFound, gin.H{
  1748. "success": false,
  1749. "message": "Channel not found",
  1750. })
  1751. return
  1752. }
  1753. // 检查是否是 Ollama 渠道
  1754. if channel.Type != constant.ChannelTypeOllama {
  1755. c.JSON(http.StatusBadRequest, gin.H{
  1756. "success": false,
  1757. "message": "This operation is only supported for Ollama channels",
  1758. })
  1759. return
  1760. }
  1761. baseURL := constant.ChannelBaseURLs[channel.Type]
  1762. if channel.GetBaseURL() != "" {
  1763. baseURL = channel.GetBaseURL()
  1764. }
  1765. // 设置 SSE 头部
  1766. c.Header("Content-Type", "text/event-stream")
  1767. c.Header("Cache-Control", "no-cache")
  1768. c.Header("Connection", "keep-alive")
  1769. c.Header("Access-Control-Allow-Origin", "*")
  1770. key := strings.Split(channel.Key, "\n")[0]
  1771. // 创建进度回调函数
  1772. progressCallback := func(progress ollama.OllamaPullResponse) {
  1773. data, _ := json.Marshal(progress)
  1774. fmt.Fprintf(c.Writer, "data: %s\n\n", string(data))
  1775. c.Writer.Flush()
  1776. }
  1777. // 执行拉取
  1778. err = ollama.PullOllamaModelStream(baseURL, key, req.ModelName, progressCallback)
  1779. if err != nil {
  1780. errorData, _ := json.Marshal(gin.H{
  1781. "error": err.Error(),
  1782. })
  1783. fmt.Fprintf(c.Writer, "data: %s\n\n", string(errorData))
  1784. } else {
  1785. successData, _ := json.Marshal(gin.H{
  1786. "message": fmt.Sprintf("Model %s pulled successfully", req.ModelName),
  1787. })
  1788. fmt.Fprintf(c.Writer, "data: %s\n\n", string(successData))
  1789. }
  1790. // 发送结束标志
  1791. fmt.Fprintf(c.Writer, "data: [DONE]\n\n")
  1792. c.Writer.Flush()
  1793. }
  1794. // OllamaDeleteModel 删除 Ollama 模型
  1795. func OllamaDeleteModel(c *gin.Context) {
  1796. var req struct {
  1797. ChannelID int `json:"channel_id"`
  1798. ModelName string `json:"model_name"`
  1799. }
  1800. if err := c.ShouldBindJSON(&req); err != nil {
  1801. c.JSON(http.StatusBadRequest, gin.H{
  1802. "success": false,
  1803. "message": "Invalid request parameters",
  1804. })
  1805. return
  1806. }
  1807. if req.ChannelID == 0 || req.ModelName == "" {
  1808. c.JSON(http.StatusBadRequest, gin.H{
  1809. "success": false,
  1810. "message": "Channel ID and model name are required",
  1811. })
  1812. return
  1813. }
  1814. // 获取渠道信息
  1815. channel, err := model.GetChannelById(req.ChannelID, true)
  1816. if err != nil {
  1817. c.JSON(http.StatusNotFound, gin.H{
  1818. "success": false,
  1819. "message": "Channel not found",
  1820. })
  1821. return
  1822. }
  1823. // 检查是否是 Ollama 渠道
  1824. if channel.Type != constant.ChannelTypeOllama {
  1825. c.JSON(http.StatusBadRequest, gin.H{
  1826. "success": false,
  1827. "message": "This operation is only supported for Ollama channels",
  1828. })
  1829. return
  1830. }
  1831. baseURL := constant.ChannelBaseURLs[channel.Type]
  1832. if channel.GetBaseURL() != "" {
  1833. baseURL = channel.GetBaseURL()
  1834. }
  1835. key := strings.Split(channel.Key, "\n")[0]
  1836. err = ollama.DeleteOllamaModel(baseURL, key, req.ModelName)
  1837. if err != nil {
  1838. c.JSON(http.StatusInternalServerError, gin.H{
  1839. "success": false,
  1840. "message": fmt.Sprintf("Failed to delete model: %s", err.Error()),
  1841. })
  1842. return
  1843. }
  1844. c.JSON(http.StatusOK, gin.H{
  1845. "success": true,
  1846. "message": fmt.Sprintf("Model %s deleted successfully", req.ModelName),
  1847. })
  1848. }
  1849. // OllamaVersion 获取 Ollama 服务版本信息
  1850. func OllamaVersion(c *gin.Context) {
  1851. id, err := strconv.Atoi(c.Param("id"))
  1852. if err != nil {
  1853. c.JSON(http.StatusBadRequest, gin.H{
  1854. "success": false,
  1855. "message": "Invalid channel id",
  1856. })
  1857. return
  1858. }
  1859. channel, err := model.GetChannelById(id, true)
  1860. if err != nil {
  1861. c.JSON(http.StatusNotFound, gin.H{
  1862. "success": false,
  1863. "message": "Channel not found",
  1864. })
  1865. return
  1866. }
  1867. if channel.Type != constant.ChannelTypeOllama {
  1868. c.JSON(http.StatusBadRequest, gin.H{
  1869. "success": false,
  1870. "message": "This operation is only supported for Ollama channels",
  1871. })
  1872. return
  1873. }
  1874. baseURL := constant.ChannelBaseURLs[channel.Type]
  1875. if channel.GetBaseURL() != "" {
  1876. baseURL = channel.GetBaseURL()
  1877. }
  1878. key := strings.Split(channel.Key, "\n")[0]
  1879. version, err := ollama.FetchOllamaVersion(baseURL, key)
  1880. if err != nil {
  1881. c.JSON(http.StatusOK, gin.H{
  1882. "success": false,
  1883. "message": fmt.Sprintf("获取Ollama版本失败: %s", err.Error()),
  1884. })
  1885. return
  1886. }
  1887. c.JSON(http.StatusOK, gin.H{
  1888. "success": true,
  1889. "data": gin.H{
  1890. "version": version,
  1891. },
  1892. })
  1893. }
  1894. // GetUserChannelsForBinding 获取用户可用于绑定的渠道列表
  1895. // 只返回 id、name、type 和 remark,不包含敏感信息(如key)
  1896. func GetUserChannelsForBinding(c *gin.Context) {
  1897. channels, err := model.GetAllChannelsForBinding()
  1898. if err != nil {
  1899. common.ApiError(c, err)
  1900. return
  1901. }
  1902. // 构建返回结果
  1903. result := make([]gin.H, 0, len(channels))
  1904. for _, ch := range channels {
  1905. result = append(result, gin.H{
  1906. "id": ch.Id,
  1907. "name": ch.Name,
  1908. "public_name": ch.PublicName,
  1909. "type": ch.Type,
  1910. "remark": ch.Remark,
  1911. })
  1912. }
  1913. c.JSON(http.StatusOK, gin.H{
  1914. "success": true,
  1915. "message": "",
  1916. "data": result,
  1917. })
  1918. }