magic-agent 2.4.2 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLIENT-CONTRACT.md +290 -30
- package/README.md +15 -14
- package/client-ui/pc/app.css +32 -0
- package/client-ui/pc/js/04-task-list.js +5 -1
- package/client-ui/pc/js/07-chat-stream-render.js +3 -3
- package/client-ui/pc/js/90-glue.js +37 -2
- package/client-ui/pc/js/92-composer.js +19 -4
- package/client-ui/pc/js/93-rail.js +16 -5
- package/client-ui/shared/conv/02-media-kinds.js +2 -2
- package/client-ui/shared/conv/03-chat-message.js +241 -23
- package/client-ui/shared/conv/06-ask-answer.js +75 -1
- package/client-ui/shared/conv/09-stream-apply.js +63 -7
- package/client-ui/shared/conv/19-attachments.js +157 -6
- package/client-ui/shared/conv/27-conv-ledger.js +5 -2
- package/client-ui/shared/conv/29-msg-variants.js +81 -0
- package/client-ui/shared/conv/conv.css +79 -19
- package/client-ui/shared/conv/dom-map.md +1 -0
- package/client-ui/shared/conversation.css +13 -2
- package/client-ui/shared/conversation.js +336 -50
- package/client-ui/shared/transport-http.js +14 -0
- package/client-ui-mobile/index.html +2932 -333
- package/client-ui-mobile/reqs.css +102 -0
- package/client-ui-mobile/reqs.js +485 -70
- package/client-ui-mobile/reqshot.js +505 -0
- package/client-ui-mobile/scripts.css +21 -0
- package/client-ui-mobile/scripts.js +539 -0
- package/client-ui-mobile/vendor/cose-base.js +3214 -0
- package/client-ui-mobile/vendor/cytoscape-fcose.js +1549 -0
- package/client-ui-mobile/vendor/cytoscape.min.js +32 -0
- package/client-ui-mobile/vendor/layout-base.js +5230 -0
- package/cmd/magic-client/browsectl.go +199 -0
- package/cmd/magic-client/clientctl.go +33 -9
- package/cmd/magic-client/main.go +57 -26
- package/internal/agent/ask.go +45 -0
- package/internal/agent/ask_test.go +39 -0
- package/internal/agent/attachments_test.go +6 -2
- package/internal/agent/capability.go +7 -1
- package/internal/agent/claude.go +20 -15
- package/internal/agent/codebuddy.go +125 -34
- package/internal/agent/codebuddy_account_test.go +54 -0
- package/internal/agent/codebuddy_stream_test.go +103 -0
- package/internal/agent/credits.go +713 -0
- package/internal/agent/credits_minimax_test.go +97 -0
- package/internal/agent/credits_test.go +211 -0
- package/internal/agent/custom_models.go +155 -0
- package/internal/agent/custom_models_test.go +125 -0
- package/internal/agent/dsh.go +123 -49
- package/internal/agent/dsh_resume.go +13 -2
- package/internal/agent/dsh_test.go +108 -6
- package/internal/agent/engine.go +37 -1
- package/internal/agent/engine_base.go +21 -17
- package/internal/agent/engine_base_test.go +2 -2
- package/internal/agent/engine_test.go +47 -86
- package/internal/agent/fallback.go +313 -0
- package/internal/agent/fallback_test.go +340 -0
- package/internal/agent/launch.go +43 -1
- package/internal/agent/llmhttp.go +46 -4
- package/internal/agent/llmhttp_test.go +157 -0
- package/internal/agent/models.go +53 -1
- package/internal/agent/models_test.go +20 -5
- package/internal/agent/pidalive_windows_test.go +40 -0
- package/internal/agent/runcmd.go +26 -0
- package/internal/agent/runcmd_windows.go +36 -5
- package/internal/agent/stream.go +136 -6
- package/internal/agent/stream_test.go +25 -38
- package/internal/agent/stream_timeline_test.go +2 -37
- package/internal/agent/streamjson.go +108 -6
- package/internal/agent/tools_suffix_test.go +6 -3
- package/internal/agent/trae.go +50 -243
- package/internal/agent/trae_exec.go +433 -0
- package/internal/agent/trae_exec_test.go +181 -0
- package/internal/agent/wraplaunch_test.go +113 -0
- package/internal/cli/ask.go +160 -37
- package/internal/cli/ask_test.go +1 -1
- package/internal/cli/checkin.go +77 -0
- package/internal/cli/clientcmd.go +3 -2
- package/internal/cli/control.go +46 -5
- package/internal/cli/control_test.go +65 -0
- package/internal/cli/events.go +4 -0
- package/internal/cli/quotas.go +125 -0
- package/internal/cli/requirements.go +51 -0
- package/internal/cli/root.go +122 -5
- package/internal/cli/session.go +7 -10
- package/internal/cli/suggest.go +7 -5
- package/internal/cli/team.go +37 -9
- package/internal/cli/team_test.go +39 -0
- package/internal/client/ab.go +372 -0
- package/internal/client/ab_test.go +287 -0
- package/internal/client/browse.go +84 -36
- package/internal/client/browse_test.go +107 -9
- package/internal/client/browseinput.go +481 -0
- package/internal/client/browseinput_test.go +216 -0
- package/internal/client/corecmd.go +23 -0
- package/internal/client/corecmd_other.go +8 -0
- package/internal/client/corecmd_windows.go +24 -0
- package/internal/client/daemon.go +90 -17
- package/internal/client/daemon_test.go +33 -0
- package/internal/client/engines_checkin_test.go +57 -0
- package/internal/client/fallback_flags_test.go +64 -0
- package/internal/client/jobs.go +294 -21
- package/internal/client/jobs_test.go +299 -2
- package/internal/client/knowledge.go +854 -0
- package/internal/client/knowledge_test.go +510 -0
- package/internal/client/lan.go +193 -7
- package/internal/client/lan_test.go +110 -0
- package/internal/client/quotas_merge_test.go +107 -0
- package/internal/client/requirements.go +198 -1
- package/internal/client/requirements_shots_test.go +200 -0
- package/internal/client/restart.go +134 -0
- package/internal/client/restart_test.go +151 -0
- package/internal/client/scripts.go +1047 -0
- package/internal/client/scripts_decode_other.go +27 -0
- package/internal/client/scripts_decode_test.go +125 -0
- package/internal/client/scripts_decode_windows.go +206 -0
- package/internal/client/scripts_test.go +726 -0
- package/internal/client/service.go +322 -18
- package/internal/client/service_test.go +51 -0
- package/internal/client/suggest.go +105 -2
- package/internal/client/suggest_test.go +86 -0
- package/internal/client/team_flags.go +25 -11
- package/internal/client/team_flags_test.go +39 -0
- package/internal/requirements/bootstrap.go +16 -0
- package/internal/requirements/consolidate.go +100 -9
- package/internal/requirements/doc.go +154 -3
- package/internal/requirements/graph.go +138 -0
- package/internal/requirements/memory.go +7 -0
- package/internal/requirements/modules.go +312 -0
- package/internal/requirements/modules_test.go +373 -0
- package/internal/requirements/relations.go +155 -0
- package/internal/requirements/relations_test.go +231 -0
- package/internal/requirements/requirements.go +10 -0
- package/internal/requirements/shots.go +418 -0
- package/internal/requirements/shots_test.go +387 -0
- package/internal/session/session.go +92 -0
- package/internal/session/sessionlog.go +23 -0
- package/internal/session/sessionlog_read_test.go +39 -0
- package/internal/suggest/suggest.go +23 -1
- package/internal/team/event.go +3 -1
- package/internal/team/execute.go +21 -6
- package/internal/team/fallback_route_test.go +80 -0
- package/internal/team/planner.go +15 -3
- package/internal/team/route.go +89 -17
- package/internal/team/team.go +18 -4
- package/npm/apk/magic-agent-mobile-release.apk +0 -0
- package/npm/apk/version.json +1 -5
- package/npm/dist/darwin-arm64/magic-agent +0 -0
- package/npm/dist/darwin-arm64/magic-client +0 -0
- package/npm/dist/darwin-x64/magic-agent +0 -0
- package/npm/dist/darwin-x64/magic-client +0 -0
- package/npm/dist/linux-arm64/magic-agent +0 -0
- package/npm/dist/linux-arm64/magic-client +0 -0
- package/npm/dist/linux-x64/magic-agent +0 -0
- package/npm/dist/linux-x64/magic-client +0 -0
- package/npm/dist/win32-x64/magic-agent.exe +0 -0
- package/npm/dist/win32-x64/magic-client.exe +0 -0
- package/package.json +8 -2
|
@@ -0,0 +1,713 @@
|
|
|
1
|
+
package agent
|
|
2
|
+
|
|
3
|
+
// credits.go - codebuddy 系账号的「剩余积分」查询(`--engines` 的 credit_left 字段)。
|
|
4
|
+
//
|
|
5
|
+
// 剩余积分只存在于账单接口(CLI 本地缓存里只有各模型倍率,没有余额):
|
|
6
|
+
//
|
|
7
|
+
// POST https://<auth.domain>/billing/meter/get-user-resource-summary
|
|
8
|
+
// Authorization: Bearer <auth.accessToken>
|
|
9
|
+
// X-User-Id: <account.uid> X-Domain: <auth.domain> Accept-Language: zh
|
|
10
|
+
//
|
|
11
|
+
// 返回 data.TotalCredits(credits,小数)或 data.Packages[].CycleRemainCapacity。
|
|
12
|
+
// 企业账号走 /billing/meter/get-enterprise-user-usage(X-Enterprise-Id),
|
|
13
|
+
// 返回 limit_num / used_num(limit_num=-1 表示不限量,此时不给数字)。
|
|
14
|
+
//
|
|
15
|
+
// 票据就在本机 auth/<id>.info;桌面 App 那几份(workbuddy-desktop*.info)的
|
|
16
|
+
// token 是 $wbEncrypted(受管密钥),独立进程读不了 —— 解析时自然跳过。
|
|
17
|
+
//
|
|
18
|
+
// 约束:只读票据 + 只发这一条只读请求;任何失败都返回 "",--engines 因此不带该字段,
|
|
19
|
+
// 绝不影响引擎列表本身。
|
|
20
|
+
|
|
21
|
+
import (
|
|
22
|
+
"context"
|
|
23
|
+
"encoding/json"
|
|
24
|
+
"fmt"
|
|
25
|
+
"io"
|
|
26
|
+
"net/http"
|
|
27
|
+
"os"
|
|
28
|
+
"path/filepath"
|
|
29
|
+
"runtime"
|
|
30
|
+
"sort"
|
|
31
|
+
"strconv"
|
|
32
|
+
"strings"
|
|
33
|
+
"time"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
// CreditLister 能报告账号当前剩余积分(仅 codebuddy / codebuddy-ai 实现)。
|
|
37
|
+
//
|
|
38
|
+
// 返回十进制字符串(如 "5939.2",已去掉多余的 0);未登录 / 无票据 / 查询失败
|
|
39
|
+
// 返回空串 —— 调用方据此省略字段,而不是显示 0。
|
|
40
|
+
type CreditLister interface {
|
|
41
|
+
CreditLeft(ctx context.Context) string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// LoginChecker 多账号引擎的**槽位**是否已登录(`--engines` 的 logged_in 字段)。
|
|
45
|
+
//
|
|
46
|
+
// codebuddy / codebuddy-ai 是两个独立账号槽位(各自票据目录),没登录的那个
|
|
47
|
+
// 不该出现在引擎选择器里 —— 「登录了几个账号就显示几个」,而不是固定两行。
|
|
48
|
+
// 判定只看票据文件是否存在且可读(App 的 $wbEncrypted 票据不算),不发网络请求。
|
|
49
|
+
type LoginChecker interface {
|
|
50
|
+
LoggedIn() bool
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// LoggedIn 实现 LoginChecker:codebuddy 主账号。
|
|
54
|
+
func (e *CodeBuddyEngine) LoggedIn() bool {
|
|
55
|
+
_, ok := readCodebuddyTicket(codebuddyAuthDir(userHomeDir()), codebuddyAuthID)
|
|
56
|
+
return ok
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// LoggedIn 实现 LoginChecker:codebuddy-ai 的隔离账号。
|
|
60
|
+
func (e *CodeBuddyAIEngine) LoggedIn() bool {
|
|
61
|
+
_, ok := readCodebuddyTicket(codebuddyAuthDir(codebuddyAIDir()), codebuddyAIAuthID)
|
|
62
|
+
return ok
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// AccountLister 多账号引擎的**账号昵称**(`--engines` / `--quotas` 的 account 字段)。
|
|
66
|
+
//
|
|
67
|
+
// 两个 codebuddy 槽位在选择器里只差一个引擎名,用户看不出「这行是哪个号」——
|
|
68
|
+
// 昵称来自票据里的 account.nickname(登录时写进去的),只读文件、不发网络。
|
|
69
|
+
// 未登录 / 票据里没有昵称时返回空串(调用方省略该字段)。
|
|
70
|
+
type AccountLister interface {
|
|
71
|
+
AccountName() string
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// AccountName 实现 AccountLister:codebuddy 主账号。
|
|
75
|
+
func (e *CodeBuddyEngine) AccountName() string {
|
|
76
|
+
t, ok := readCodebuddyTicket(codebuddyAuthDir(userHomeDir()), codebuddyAuthID)
|
|
77
|
+
if !ok {
|
|
78
|
+
return ""
|
|
79
|
+
}
|
|
80
|
+
return t.Nickname
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// AccountName 实现 AccountLister:codebuddy-ai 的隔离账号。
|
|
84
|
+
func (e *CodeBuddyAIEngine) AccountName() string {
|
|
85
|
+
t, ok := readCodebuddyTicket(codebuddyAuthDir(codebuddyAIDir()), codebuddyAIAuthID)
|
|
86
|
+
if !ok {
|
|
87
|
+
return ""
|
|
88
|
+
}
|
|
89
|
+
return t.Nickname
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// CreditLeft 实现 CreditLister:codebuddy 主账号(真实 homedir + ~/.codebuddy)。
|
|
93
|
+
func (e *CodeBuddyEngine) CreditLeft(ctx context.Context) string {
|
|
94
|
+
return codebuddyCreditLeft(ctx, userHomeDir(), codebuddyAuthID)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// CreditLeft 实现 CreditLister:codebuddy-ai 的隔离账号(homedir = ~/.codebuddy-ai)。
|
|
98
|
+
func (e *CodeBuddyAIEngine) CreditLeft(ctx context.Context) string {
|
|
99
|
+
return codebuddyCreditLeft(ctx, codebuddyAIDir(), codebuddyAIAuthID)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// userHomeDir 当前用户家目录(与 CLI 的 os.homedir() 同源:Windows 读 USERPROFILE)。
|
|
103
|
+
func userHomeDir() string {
|
|
104
|
+
home, err := os.UserHomeDir()
|
|
105
|
+
if err != nil || home == "" {
|
|
106
|
+
home = os.Getenv("HOME")
|
|
107
|
+
}
|
|
108
|
+
return home
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// codebuddyAuthDir 票据目录。与 CLI 的 FilePathService 同构:
|
|
112
|
+
//
|
|
113
|
+
// <home>/AppData/Local/CodeBuddyExtension/Data/Public/auth (win32)
|
|
114
|
+
// <home>/Library/Application Support/CodeBuddyExtension/.../auth (darwin)
|
|
115
|
+
// <home>/.local/share/CodeBuddyExtension/Data/Public/auth (linux)
|
|
116
|
+
//
|
|
117
|
+
// codebuddy-ai 传自己的隔离 home(~/.codebuddy-ai),因此票据天然与主引擎分开。
|
|
118
|
+
func codebuddyAuthDir(home string) string {
|
|
119
|
+
switch runtime.GOOS {
|
|
120
|
+
case "windows":
|
|
121
|
+
return filepath.Join(home, "AppData", "Local", "CodeBuddyExtension", "Data", "Public", "auth")
|
|
122
|
+
case "darwin":
|
|
123
|
+
return filepath.Join(home, "Library", "Application Support", "CodeBuddyExtension", "Data", "Public", "auth")
|
|
124
|
+
default:
|
|
125
|
+
return filepath.Join(home, ".local", "share", "CodeBuddyExtension", "Data", "Public", "auth")
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// codebuddyTicket 一张可用的 CLI 票据(只取查余额 / 展示账号需要的字段)。
|
|
130
|
+
type codebuddyTicket struct {
|
|
131
|
+
UID string
|
|
132
|
+
Nickname string
|
|
133
|
+
EnterpriseID string
|
|
134
|
+
Token string
|
|
135
|
+
Domain string
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// baseProductAuthID CLI 包内 product.json 的 authentication.id —— 新版 CLI 有时会把
|
|
139
|
+
// 票据写到这个名字上(见 codebuddy.go 的账号隔离说明),故作为第二候选。
|
|
140
|
+
const baseProductAuthID = "Tencent-Cloud.coding-copilot"
|
|
141
|
+
|
|
142
|
+
// readCodebuddyTicket 在 auth 目录里挑一张可读的票据。
|
|
143
|
+
// 顺序:本引擎 id → base product id → 其余 *.info 按 mtime 新到旧(跳过 $wbEncrypted)。
|
|
144
|
+
func readCodebuddyTicket(authDir, authID string) (codebuddyTicket, bool) {
|
|
145
|
+
preferred := []string{
|
|
146
|
+
filepath.Join(authDir, authID+".info"),
|
|
147
|
+
filepath.Join(authDir, baseProductAuthID+".info"),
|
|
148
|
+
}
|
|
149
|
+
seen := map[string]bool{}
|
|
150
|
+
for _, p := range preferred {
|
|
151
|
+
seen[p] = true
|
|
152
|
+
}
|
|
153
|
+
type cand struct {
|
|
154
|
+
path string
|
|
155
|
+
mod time.Time
|
|
156
|
+
}
|
|
157
|
+
var rest []cand
|
|
158
|
+
entries, err := os.ReadDir(authDir)
|
|
159
|
+
if err != nil {
|
|
160
|
+
return codebuddyTicket{}, false
|
|
161
|
+
}
|
|
162
|
+
for _, e := range entries {
|
|
163
|
+
if e.IsDir() || !strings.HasSuffix(e.Name(), ".info") {
|
|
164
|
+
continue
|
|
165
|
+
}
|
|
166
|
+
p := filepath.Join(authDir, e.Name())
|
|
167
|
+
if seen[p] {
|
|
168
|
+
continue
|
|
169
|
+
}
|
|
170
|
+
if st, statErr := e.Info(); statErr == nil {
|
|
171
|
+
rest = append(rest, cand{p, st.ModTime()})
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
sort.Slice(rest, func(i, j int) bool { return rest[i].mod.After(rest[j].mod) })
|
|
175
|
+
|
|
176
|
+
for _, p := range preferred {
|
|
177
|
+
if t, ok := parseCodebuddyTicket(p); ok {
|
|
178
|
+
return t, true
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
for _, c := range rest {
|
|
182
|
+
if t, ok := parseCodebuddyTicket(c.path); ok {
|
|
183
|
+
return t, true
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return codebuddyTicket{}, false
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// parseCodebuddyTicket 解析一张票据;App 的加密 token(对象形态)直接判不可用。
|
|
190
|
+
func parseCodebuddyTicket(path string) (codebuddyTicket, bool) {
|
|
191
|
+
raw, err := os.ReadFile(path)
|
|
192
|
+
if err != nil {
|
|
193
|
+
return codebuddyTicket{}, false
|
|
194
|
+
}
|
|
195
|
+
var doc struct {
|
|
196
|
+
Account struct {
|
|
197
|
+
UID string `json:"uid"`
|
|
198
|
+
Nickname string `json:"nickname"`
|
|
199
|
+
EnterpriseID string `json:"enterpriseId"`
|
|
200
|
+
} `json:"account"`
|
|
201
|
+
Auth struct {
|
|
202
|
+
AccessToken json.RawMessage `json:"accessToken"`
|
|
203
|
+
Domain string `json:"domain"`
|
|
204
|
+
} `json:"auth"`
|
|
205
|
+
}
|
|
206
|
+
if json.Unmarshal(raw, &doc) != nil {
|
|
207
|
+
return codebuddyTicket{}, false
|
|
208
|
+
}
|
|
209
|
+
var tok string
|
|
210
|
+
// $wbEncrypted 是对象;独立 CLI 解不开,跳过。
|
|
211
|
+
if len(doc.Auth.AccessToken) == 0 || json.Unmarshal(doc.Auth.AccessToken, &tok) != nil || tok == "" {
|
|
212
|
+
return codebuddyTicket{}, false
|
|
213
|
+
}
|
|
214
|
+
if doc.Auth.Domain == "" {
|
|
215
|
+
return codebuddyTicket{}, false
|
|
216
|
+
}
|
|
217
|
+
return codebuddyTicket{
|
|
218
|
+
UID: doc.Account.UID,
|
|
219
|
+
Nickname: strings.TrimSpace(doc.Account.Nickname),
|
|
220
|
+
EnterpriseID: doc.Account.EnterpriseID,
|
|
221
|
+
Token: tok,
|
|
222
|
+
Domain: doc.Auth.Domain,
|
|
223
|
+
}, true
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// codebuddyCreditLeft 查一张票据对应的剩余积分;失败返回 ""。
|
|
227
|
+
func codebuddyCreditLeft(ctx context.Context, home, authID string) string {
|
|
228
|
+
t, ok := readCodebuddyTicket(codebuddyAuthDir(home), authID)
|
|
229
|
+
if !ok {
|
|
230
|
+
return ""
|
|
231
|
+
}
|
|
232
|
+
ctx, cancel := context.WithTimeout(ctx, 8*time.Second)
|
|
233
|
+
defer cancel()
|
|
234
|
+
if t.EnterpriseID != "" {
|
|
235
|
+
return fetchEnterpriseCreditLeft(ctx, t)
|
|
236
|
+
}
|
|
237
|
+
return fetchPersonalCreditLeft(ctx, t)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// codebuddyBillingPost 发一次只读账单请求,返回响应体;非 200 / 网络错误返回 false。
|
|
241
|
+
func codebuddyBillingPost(ctx context.Context, t codebuddyTicket, path string, extraHeaders map[string]string) ([]byte, bool) {
|
|
242
|
+
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
|
|
243
|
+
"https://"+t.Domain+path, strings.NewReader("{}"))
|
|
244
|
+
if err != nil {
|
|
245
|
+
return nil, false
|
|
246
|
+
}
|
|
247
|
+
req.Header.Set("Content-Type", "application/json")
|
|
248
|
+
req.Header.Set("Accept-Language", "zh")
|
|
249
|
+
req.Header.Set("Authorization", "Bearer "+t.Token)
|
|
250
|
+
req.Header.Set("X-Domain", t.Domain)
|
|
251
|
+
if t.UID != "" {
|
|
252
|
+
req.Header.Set("X-User-Id", t.UID)
|
|
253
|
+
}
|
|
254
|
+
for k, v := range extraHeaders {
|
|
255
|
+
req.Header.Set(k, v)
|
|
256
|
+
}
|
|
257
|
+
client := &http.Client{Timeout: 8 * time.Second}
|
|
258
|
+
resp, err := client.Do(req)
|
|
259
|
+
if err != nil {
|
|
260
|
+
return nil, false
|
|
261
|
+
}
|
|
262
|
+
defer resp.Body.Close()
|
|
263
|
+
/* 不按 HTTP 状态码直接判失败:领取接口在「今天已领」这类业务失败时会返回
|
|
264
|
+
非 200 + JSON(code/msg)—— 那份正文才是给用户看的原因。解析交给调用方。 */
|
|
265
|
+
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
|
266
|
+
if err != nil || len(body) == 0 {
|
|
267
|
+
return nil, false
|
|
268
|
+
}
|
|
269
|
+
return body, true
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// fetchPersonalCreditLeft 个人账号:优先 data.TotalCredits,缺失时累加各包剩余。
|
|
273
|
+
func fetchPersonalCreditLeft(ctx context.Context, t codebuddyTicket) string {
|
|
274
|
+
body, ok := codebuddyBillingPost(ctx, t, "/billing/meter/get-user-resource-summary", nil)
|
|
275
|
+
if !ok {
|
|
276
|
+
return ""
|
|
277
|
+
}
|
|
278
|
+
return personalCreditLeftFromJSON(body)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// personalCreditLeftFromJSON 解析 get-user-resource-summary 的响应(抽出来便于单测)。
|
|
282
|
+
func personalCreditLeftFromJSON(body []byte) string {
|
|
283
|
+
var doc struct {
|
|
284
|
+
Code int `json:"code"`
|
|
285
|
+
Data struct {
|
|
286
|
+
TotalCredits json.RawMessage `json:"TotalCredits"`
|
|
287
|
+
Packages []struct {
|
|
288
|
+
CycleRemainCapacity json.RawMessage `json:"CycleRemainCapacity"`
|
|
289
|
+
} `json:"Packages"`
|
|
290
|
+
} `json:"data"`
|
|
291
|
+
}
|
|
292
|
+
if json.Unmarshal(body, &doc) != nil || doc.Code != 0 {
|
|
293
|
+
return ""
|
|
294
|
+
}
|
|
295
|
+
if v, ok := creditFloat(doc.Data.TotalCredits); ok {
|
|
296
|
+
return formatCredit(v)
|
|
297
|
+
}
|
|
298
|
+
var sum float64
|
|
299
|
+
found := false
|
|
300
|
+
for _, p := range doc.Data.Packages {
|
|
301
|
+
if v, ok := creditFloat(p.CycleRemainCapacity); ok {
|
|
302
|
+
sum += v
|
|
303
|
+
found = true
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if !found {
|
|
307
|
+
return ""
|
|
308
|
+
}
|
|
309
|
+
return formatCredit(sum)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// fetchEnterpriseCreditLeft 企业账号:limit_num - used_num(-1 = 不限量,不给数字)。
|
|
313
|
+
func fetchEnterpriseCreditLeft(ctx context.Context, t codebuddyTicket) string {
|
|
314
|
+
body, ok := codebuddyBillingPost(ctx, t, "/billing/meter/get-enterprise-user-usage",
|
|
315
|
+
map[string]string{"X-Enterprise-Id": t.EnterpriseID})
|
|
316
|
+
if !ok {
|
|
317
|
+
return ""
|
|
318
|
+
}
|
|
319
|
+
return enterpriseCreditLeftFromJSON(body)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// enterpriseCreditLeftFromJSON 解析 get-enterprise-user-usage 的响应。
|
|
323
|
+
func enterpriseCreditLeftFromJSON(body []byte) string {
|
|
324
|
+
var doc struct {
|
|
325
|
+
Code int `json:"code"`
|
|
326
|
+
Data struct {
|
|
327
|
+
LimitNum json.RawMessage `json:"limit_num"`
|
|
328
|
+
UsedNum json.RawMessage `json:"used_num"`
|
|
329
|
+
} `json:"data"`
|
|
330
|
+
}
|
|
331
|
+
if json.Unmarshal(body, &doc) != nil || doc.Code != 0 {
|
|
332
|
+
return ""
|
|
333
|
+
}
|
|
334
|
+
limit, ok := creditFloat(doc.Data.LimitNum)
|
|
335
|
+
if !ok || limit < 0 {
|
|
336
|
+
return ""
|
|
337
|
+
}
|
|
338
|
+
used, _ := creditFloat(doc.Data.UsedNum)
|
|
339
|
+
if left := limit - used; left > 0 {
|
|
340
|
+
return formatCredit(left)
|
|
341
|
+
}
|
|
342
|
+
return "0"
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// creditFloat 解析「数字或字符串形式的数字」(接口里两种形态都出现过)。
|
|
346
|
+
func creditFloat(raw json.RawMessage) (float64, bool) {
|
|
347
|
+
if len(raw) == 0 {
|
|
348
|
+
return 0, false
|
|
349
|
+
}
|
|
350
|
+
s := string(raw)
|
|
351
|
+
var quoted string
|
|
352
|
+
if json.Unmarshal(raw, "ed) == nil {
|
|
353
|
+
s = quoted
|
|
354
|
+
}
|
|
355
|
+
s = strings.TrimSpace(s)
|
|
356
|
+
if s == "" {
|
|
357
|
+
return 0, false
|
|
358
|
+
}
|
|
359
|
+
f, err := strconv.ParseFloat(s, 64)
|
|
360
|
+
if err != nil {
|
|
361
|
+
return 0, false
|
|
362
|
+
}
|
|
363
|
+
return f, true
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// formatCredit 归一化成最多两位小数、去掉多余的 0(5939.20000022 → "5939.2")。
|
|
367
|
+
func formatCredit(f float64) string {
|
|
368
|
+
s := strconv.FormatFloat(f, 'f', 2, 64)
|
|
369
|
+
s = strings.TrimRight(s, "0")
|
|
370
|
+
s = strings.TrimRight(s, ".")
|
|
371
|
+
if s == "" || s == "-0" {
|
|
372
|
+
return "0"
|
|
373
|
+
}
|
|
374
|
+
return s
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ── 每日积分签到 ──────────────────────────────────────────────
|
|
378
|
+
//
|
|
379
|
+
// 状态:POST https://<domain>/billing/meter/checkin-activity-status
|
|
380
|
+
// 领取:POST https://<domain>/billing/meter/daily-checkin
|
|
381
|
+
//
|
|
382
|
+
// ⚠️ 别用旧的 /billing/meter/checkin-status:实测返回 active=false 全 0,
|
|
383
|
+
// 活动接口是 checkin-activity-status(字段 today_checked_in / daily_credit / streak_days)。
|
|
384
|
+
|
|
385
|
+
// CheckinInfo 每日签到状态 / 领取结果。
|
|
386
|
+
type CheckinInfo struct {
|
|
387
|
+
// CheckedToday 今天是否已领。
|
|
388
|
+
CheckedToday bool
|
|
389
|
+
// DailyCredit 今日可领积分(十进制字符串,可能为空)。
|
|
390
|
+
DailyCredit string
|
|
391
|
+
// Credit 本次实际到账积分(仅领取成功时有)。
|
|
392
|
+
Credit string
|
|
393
|
+
// StreakDays 连续签到天数。
|
|
394
|
+
StreakDays int
|
|
395
|
+
// Message 业务失败原因(如「今天已经领过了」)。
|
|
396
|
+
Message string
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Checkiner 每日积分签到(仅 codebuddy / codebuddy-ai 实现)。
|
|
400
|
+
type Checkiner interface {
|
|
401
|
+
// CheckinStatus 返回签到状态;第二个返回值 false = 拿不到(未登录 / 活动未开 / 网络失败)。
|
|
402
|
+
CheckinStatus(ctx context.Context) (CheckinInfo, bool)
|
|
403
|
+
// ClaimCheckin 领取每日积分;业务失败返回 error(Message 里是服务端原因)。
|
|
404
|
+
ClaimCheckin(ctx context.Context) (CheckinInfo, error)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// CheckinStatus 实现 Checkiner:codebuddy 主账号。
|
|
408
|
+
func (e *CodeBuddyEngine) CheckinStatus(ctx context.Context) (CheckinInfo, bool) {
|
|
409
|
+
return codebuddyCheckinStatus(ctx, userHomeDir(), codebuddyAuthID)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// CheckinStatus 实现 Checkiner:codebuddy-ai 的隔离账号。
|
|
413
|
+
func (e *CodeBuddyAIEngine) CheckinStatus(ctx context.Context) (CheckinInfo, bool) {
|
|
414
|
+
return codebuddyCheckinStatus(ctx, codebuddyAIDir(), codebuddyAIAuthID)
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ClaimCheckin 实现 Checkiner:codebuddy 主账号。
|
|
418
|
+
func (e *CodeBuddyEngine) ClaimCheckin(ctx context.Context) (CheckinInfo, error) {
|
|
419
|
+
return codebuddyClaimCheckin(ctx, userHomeDir(), codebuddyAuthID)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ClaimCheckin 实现 Checkiner:codebuddy-ai 的隔离账号。
|
|
423
|
+
func (e *CodeBuddyAIEngine) ClaimCheckin(ctx context.Context) (CheckinInfo, error) {
|
|
424
|
+
return codebuddyClaimCheckin(ctx, codebuddyAIDir(), codebuddyAIAuthID)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// codebuddyCheckinStatus 查签到状态;拿不到返回 false(--engines 因此省略 checkin_today)。
|
|
428
|
+
func codebuddyCheckinStatus(ctx context.Context, home, authID string) (CheckinInfo, bool) {
|
|
429
|
+
t, ok := readCodebuddyTicket(codebuddyAuthDir(home), authID)
|
|
430
|
+
if !ok {
|
|
431
|
+
return CheckinInfo{}, false
|
|
432
|
+
}
|
|
433
|
+
ctx, cancel := context.WithTimeout(ctx, 8*time.Second)
|
|
434
|
+
defer cancel()
|
|
435
|
+
body, ok := codebuddyBillingPost(ctx, t, "/billing/meter/checkin-activity-status", nil)
|
|
436
|
+
if !ok {
|
|
437
|
+
return CheckinInfo{}, false
|
|
438
|
+
}
|
|
439
|
+
return checkinStatusFromJSON(body)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// checkinStatusFromJSON 解析 checkin-activity-status(抽出来便于单测)。
|
|
443
|
+
func checkinStatusFromJSON(body []byte) (CheckinInfo, bool) {
|
|
444
|
+
var doc struct {
|
|
445
|
+
Code int `json:"code"`
|
|
446
|
+
Data struct {
|
|
447
|
+
Active bool `json:"active"`
|
|
448
|
+
TodayCheckedIn bool `json:"today_checked_in"`
|
|
449
|
+
DailyCredit json.RawMessage `json:"daily_credit"`
|
|
450
|
+
StreakDays int `json:"streak_days"`
|
|
451
|
+
} `json:"data"`
|
|
452
|
+
}
|
|
453
|
+
if json.Unmarshal(body, &doc) != nil || doc.Code != 0 || !doc.Data.Active {
|
|
454
|
+
return CheckinInfo{}, false
|
|
455
|
+
}
|
|
456
|
+
daily := ""
|
|
457
|
+
if v, ok := creditFloat(doc.Data.DailyCredit); ok {
|
|
458
|
+
daily = formatCredit(v)
|
|
459
|
+
}
|
|
460
|
+
return CheckinInfo{
|
|
461
|
+
CheckedToday: doc.Data.TodayCheckedIn,
|
|
462
|
+
DailyCredit: daily,
|
|
463
|
+
StreakDays: doc.Data.StreakDays,
|
|
464
|
+
}, true
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// codebuddyClaimCheckin 领取每日积分(重复领取会返回业务错误,不会重复加)。
|
|
468
|
+
//
|
|
469
|
+
// 无论成功还是业务失败(今天已签),都**回查一次状态**:领取接口的失败响应不带
|
|
470
|
+
// 「今天是否已领」,不回查的话客户端领完一次(或发现已领)后仍会显示「领取」按钮。
|
|
471
|
+
func codebuddyClaimCheckin(ctx context.Context, home, authID string) (CheckinInfo, error) {
|
|
472
|
+
t, ok := readCodebuddyTicket(codebuddyAuthDir(home), authID)
|
|
473
|
+
if !ok {
|
|
474
|
+
return CheckinInfo{}, fmt.Errorf("没有可用的登录票据(先 magic-agent --login %s)", authID)
|
|
475
|
+
}
|
|
476
|
+
ctx, cancel := context.WithTimeout(ctx, 12*time.Second)
|
|
477
|
+
defer cancel()
|
|
478
|
+
body, ok := codebuddyBillingPost(ctx, t, "/billing/meter/daily-checkin", nil)
|
|
479
|
+
if !ok {
|
|
480
|
+
return CheckinInfo{}, fmt.Errorf("领取请求失败(网络或服务端错误)")
|
|
481
|
+
}
|
|
482
|
+
info, claimErr := claimCheckinFromJSON(body)
|
|
483
|
+
if st, ok := codebuddyCheckinStatus(ctx, home, authID); ok {
|
|
484
|
+
st.Credit = info.Credit
|
|
485
|
+
if info.Message != "" {
|
|
486
|
+
st.Message = info.Message
|
|
487
|
+
}
|
|
488
|
+
return st, claimErr
|
|
489
|
+
}
|
|
490
|
+
return info, claimErr
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// claimCheckinFromJSON 解析 daily-checkin 的响应。成功时 credit/streak_days 可能在
|
|
494
|
+
// 顶层或 data 里,两处都认。
|
|
495
|
+
func claimCheckinFromJSON(body []byte) (CheckinInfo, error) {
|
|
496
|
+
var doc struct {
|
|
497
|
+
Code int `json:"code"`
|
|
498
|
+
Msg string `json:"msg"`
|
|
499
|
+
Credit json.RawMessage `json:"credit"`
|
|
500
|
+
StreakDays int `json:"streak_days"`
|
|
501
|
+
Data struct {
|
|
502
|
+
Credit json.RawMessage `json:"credit"`
|
|
503
|
+
StreakDays int `json:"streak_days"`
|
|
504
|
+
TodayCheckedIn bool `json:"today_checked_in"`
|
|
505
|
+
} `json:"data"`
|
|
506
|
+
}
|
|
507
|
+
if json.Unmarshal(body, &doc) != nil {
|
|
508
|
+
return CheckinInfo{}, fmt.Errorf("领取响应解析失败")
|
|
509
|
+
}
|
|
510
|
+
if doc.Code != 0 {
|
|
511
|
+
msg := strings.TrimSpace(doc.Msg)
|
|
512
|
+
if msg == "" {
|
|
513
|
+
msg = fmt.Sprintf("领取失败(code=%d)", doc.Code)
|
|
514
|
+
}
|
|
515
|
+
return CheckinInfo{Message: msg}, fmt.Errorf("%s", msg)
|
|
516
|
+
}
|
|
517
|
+
raw := doc.Credit
|
|
518
|
+
if len(raw) == 0 {
|
|
519
|
+
raw = doc.Data.Credit
|
|
520
|
+
}
|
|
521
|
+
credit := ""
|
|
522
|
+
if v, ok := creditFloat(raw); ok {
|
|
523
|
+
credit = formatCredit(v)
|
|
524
|
+
}
|
|
525
|
+
streak := doc.StreakDays
|
|
526
|
+
if streak == 0 {
|
|
527
|
+
streak = doc.Data.StreakDays
|
|
528
|
+
}
|
|
529
|
+
return CheckinInfo{
|
|
530
|
+
CheckedToday: true,
|
|
531
|
+
Credit: credit,
|
|
532
|
+
StreakDays: streak,
|
|
533
|
+
}, nil
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// ── llm 引擎:MiniMax 套餐(Token Plan / Coding Plan)剩余额度 ──────────────
|
|
537
|
+
//
|
|
538
|
+
// 额度在套餐用量接口(只读,Bearer <API Key>):
|
|
539
|
+
//
|
|
540
|
+
// GET https://www.minimaxi.com/v1/api/openplatform/coding_plan/remains
|
|
541
|
+
// GET https://www.minimaxi.com/v1/token_plan/remains
|
|
542
|
+
//
|
|
543
|
+
// 返回 model_remains[],取 model_name=general 的两个窗口:
|
|
544
|
+
// current_interval_remaining_percent 5 小时窗口剩余百分比
|
|
545
|
+
// current_weekly_remaining_percent 本周剩余百分比
|
|
546
|
+
//(同一份票据两个接口当前返回一致,先试 coding_plan 再退 token_plan)。
|
|
547
|
+
// key 从 ~/.magic-agent/models.json 里带 minimax 域名的条目取(sk-cp-… 是 Coding Plan)。
|
|
548
|
+
//
|
|
549
|
+
// 额度按**模型**挂(ModelQuota),不挂引擎行:llm 下还有火山等别的供应商模型。
|
|
550
|
+
// 任何失败都不给该字段,不影响引擎列表。
|
|
551
|
+
|
|
552
|
+
// CreditUnitLister 可选接口:说明 CreditLeft / ModelQuota 的单位(缺省按「积分」展示)。
|
|
553
|
+
// MiniMax 套餐给的是百分比,客户端据此把「剩余 53」显示成「剩余 53%」。
|
|
554
|
+
type CreditUnitLister interface {
|
|
555
|
+
CreditUnit() string
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// QuotaWindows 套餐在「5 小时窗口 / 本周」两个维度的剩余百分比(已格式化的数字串,
|
|
559
|
+
// 如 "98" / "53")。空串 = 该窗口没有数据。
|
|
560
|
+
type QuotaWindows struct {
|
|
561
|
+
IntervalLeft string `json:"interval_left,omitempty"`
|
|
562
|
+
WeeklyLeft string `json:"weekly_left,omitempty"`
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// ModelQuotaLister 可选:按**模型**报告套餐额度快照(model id → 两个窗口的剩余百分比)。
|
|
566
|
+
// llm 用它把 MiniMax 套餐额度挂到真正走 MiniMax 端点的模型上 —— 不挂在引擎行,
|
|
567
|
+
// 因为同一个 llm 引擎下还有别的供应商模型(ark-glm 等),引擎级额度会张冠李戴。
|
|
568
|
+
type ModelQuotaLister interface {
|
|
569
|
+
ModelQuota(ctx context.Context) map[string]QuotaWindows
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// ModelQuota 实现 ModelQuotaLister:models.json 里 URL 指向 minimax 的模型**各用各的
|
|
573
|
+
// API Key** 查额度 —— 不同 key 是不同账号/套餐,额度当然不同;只有共用同一把 key
|
|
574
|
+
// 的模型才共享同一份额度。
|
|
575
|
+
func (e *LLMEngine) ModelQuota(ctx context.Context) map[string]QuotaWindows {
|
|
576
|
+
list, err := LoadLLMConfigModels()
|
|
577
|
+
if err != nil {
|
|
578
|
+
return nil
|
|
579
|
+
}
|
|
580
|
+
return modelQuotaMap(ctx, list, minimaxQuota)
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// CreditUnit 实现 CreditUnitLister:MiniMax 额度是百分比,不是积分。
|
|
584
|
+
func (e *LLMEngine) CreditUnit() string { return "%" }
|
|
585
|
+
|
|
586
|
+
// CLIModelQuotas 给 codebuddy 系用:按它自己 models.json 里**自定义模型**的
|
|
587
|
+
// (端点, key) 查 MiniMax 套餐额度,映射到展示 id(有别名用别名,与清单同形)。
|
|
588
|
+
//
|
|
589
|
+
// 为什么挂到 codebuddy:用户在 models.json 里接的 MiniMax 自定义端点(custom-local:*)
|
|
590
|
+
// 与 llm 下的是同一类 token plan,额度也应该在选择器里显示;官方 minimax-* 走平台
|
|
591
|
+
// 账号、没有用户侧的 key,查不到自然不显示。
|
|
592
|
+
func CLIModelQuotas(ctx context.Context, configDir string) map[string]QuotaWindows {
|
|
593
|
+
customs := CLICustomModels(configDir)
|
|
594
|
+
if len(customs) == 0 {
|
|
595
|
+
return nil
|
|
596
|
+
}
|
|
597
|
+
list := make([]LLMConfigModel, 0, len(customs))
|
|
598
|
+
for _, m := range customs {
|
|
599
|
+
if m.URL == "" || m.APIKey == "" {
|
|
600
|
+
continue
|
|
601
|
+
}
|
|
602
|
+
list = append(list, LLMConfigModel{ID: m.ID, URL: m.URL, APIKey: m.APIKey})
|
|
603
|
+
}
|
|
604
|
+
if len(list) == 0 {
|
|
605
|
+
return nil
|
|
606
|
+
}
|
|
607
|
+
return modelQuotaMap(ctx, list, minimaxQuota)
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// modelQuotaMap 按 (站点, key) 去重查额度,再映射回各自的模型 id。
|
|
611
|
+
// 同一把 key 只查一次;查不到的模型不给字段(不是复用别人的额度)。
|
|
612
|
+
func modelQuotaMap(ctx context.Context, list []LLMConfigModel,
|
|
613
|
+
query func(context.Context, string, string) (QuotaWindows, bool)) map[string]QuotaWindows {
|
|
614
|
+
type res struct {
|
|
615
|
+
q QuotaWindows
|
|
616
|
+
ok bool
|
|
617
|
+
}
|
|
618
|
+
cache := make(map[string]res)
|
|
619
|
+
out := make(map[string]QuotaWindows)
|
|
620
|
+
for _, m := range list {
|
|
621
|
+
host := minimaxHost(m.URL)
|
|
622
|
+
id := strings.TrimSpace(m.ID)
|
|
623
|
+
key := strings.TrimSpace(m.APIKey)
|
|
624
|
+
if host == "" || id == "" || key == "" {
|
|
625
|
+
continue
|
|
626
|
+
}
|
|
627
|
+
ck := host + "\x00" + key
|
|
628
|
+
r, hit := cache[ck]
|
|
629
|
+
if !hit {
|
|
630
|
+
r.q, r.ok = query(ctx, host, key)
|
|
631
|
+
cache[ck] = r
|
|
632
|
+
}
|
|
633
|
+
if !r.ok || (r.q.IntervalLeft == "" && r.q.WeeklyLeft == "") {
|
|
634
|
+
continue
|
|
635
|
+
}
|
|
636
|
+
out[id] = r.q
|
|
637
|
+
}
|
|
638
|
+
if len(out) == 0 {
|
|
639
|
+
return nil
|
|
640
|
+
}
|
|
641
|
+
return out
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// minimaxHost 返回该 URL 对应套餐用量接口的站点根;不是 MiniMax 端点返回 ""。
|
|
645
|
+
func minimaxHost(rawURL string) string {
|
|
646
|
+
u := strings.ToLower(strings.TrimSpace(rawURL))
|
|
647
|
+
switch {
|
|
648
|
+
case strings.Contains(u, "minimax.io"):
|
|
649
|
+
return "https://www.minimax.io"
|
|
650
|
+
case strings.Contains(u, "minimaxi.com"):
|
|
651
|
+
return "https://www.minimaxi.com"
|
|
652
|
+
}
|
|
653
|
+
return ""
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// minimaxQuota 依次试 coding_plan / token_plan 两个用量接口。
|
|
657
|
+
func minimaxQuota(ctx context.Context, host, key string) (QuotaWindows, bool) {
|
|
658
|
+
ctx, cancel := context.WithTimeout(ctx, 8*time.Second)
|
|
659
|
+
defer cancel()
|
|
660
|
+
for _, p := range []string{
|
|
661
|
+
"/v1/api/openplatform/coding_plan/remains",
|
|
662
|
+
"/v1/token_plan/remains",
|
|
663
|
+
} {
|
|
664
|
+
if q, ok := minimaxQuotaAt(ctx, host+p, key); ok {
|
|
665
|
+
return q, true
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return QuotaWindows{}, false
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// minimaxQuotaAt 查一次套餐用量,取 general 模型的「5 小时 / 本周」剩余百分比。
|
|
672
|
+
func minimaxQuotaAt(ctx context.Context, url, key string) (QuotaWindows, bool) {
|
|
673
|
+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
674
|
+
if err != nil {
|
|
675
|
+
return QuotaWindows{}, false
|
|
676
|
+
}
|
|
677
|
+
req.Header.Set("Authorization", "Bearer "+key)
|
|
678
|
+
req.Header.Set("Accept", "application/json")
|
|
679
|
+
resp, err := http.DefaultClient.Do(req)
|
|
680
|
+
if err != nil {
|
|
681
|
+
return QuotaWindows{}, false
|
|
682
|
+
}
|
|
683
|
+
defer resp.Body.Close()
|
|
684
|
+
if resp.StatusCode != http.StatusOK {
|
|
685
|
+
return QuotaWindows{}, false
|
|
686
|
+
}
|
|
687
|
+
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
|
688
|
+
if err != nil {
|
|
689
|
+
return QuotaWindows{}, false
|
|
690
|
+
}
|
|
691
|
+
var doc struct {
|
|
692
|
+
ModelRemains []struct {
|
|
693
|
+
ModelName string `json:"model_name"`
|
|
694
|
+
IntervalPct float64 `json:"current_interval_remaining_percent"`
|
|
695
|
+
WeeklyPercent float64 `json:"current_weekly_remaining_percent"`
|
|
696
|
+
} `json:"model_remains"`
|
|
697
|
+
BaseResp struct {
|
|
698
|
+
StatusCode int `json:"status_code"`
|
|
699
|
+
} `json:"base_resp"`
|
|
700
|
+
}
|
|
701
|
+
if json.Unmarshal(body, &doc) != nil || doc.BaseResp.StatusCode != 0 {
|
|
702
|
+
return QuotaWindows{}, false
|
|
703
|
+
}
|
|
704
|
+
for _, m := range doc.ModelRemains {
|
|
705
|
+
if m.ModelName == "general" {
|
|
706
|
+
return QuotaWindows{
|
|
707
|
+
IntervalLeft: formatCredit(m.IntervalPct),
|
|
708
|
+
WeeklyLeft: formatCredit(m.WeeklyPercent),
|
|
709
|
+
}, true
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return QuotaWindows{}, false
|
|
713
|
+
}
|