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
package/internal/cli/control.go
CHANGED
|
@@ -25,12 +25,17 @@ import (
|
|
|
25
25
|
* {"op":"answer","text":"..."} → 把答案作为后续 user 消息追进常驻会话
|
|
26
26
|
* → {"type":"answered"};通道不可用 →
|
|
27
27
|
* {"type":"control_error","error":"..."}
|
|
28
|
+
* {"op":"ask_answer","request_id":"perm_…","allowed":true|false,"message":"…","tool_name":"…"}
|
|
29
|
+
* → **原地**回应引擎的 control_request(计划审批 /
|
|
30
|
+
* 工具授权),走子进程 stdin 写一行 control_response
|
|
31
|
+
* → {"type":"ask_answered",…};无落点(引擎不支持 /
|
|
32
|
+
* 本轮已结束 / stdin 已关)→ control_error
|
|
28
33
|
* 其它 → {"type":"control_error","op":"..."}
|
|
29
34
|
*
|
|
30
|
-
* ⚠️ answer
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
35
|
+
* ⚠️ answer 与 ask_answer 是**两件事**,别合并:
|
|
36
|
+
* · answer 是「下一轮用户消息」(headless 下模型提问后 CLI 会自行拒绝,答案只能这样补);
|
|
37
|
+
* · ask_answer 是「这一轮正阻塞等着的裁决」(ExitPlanMode 计划审批、manual 档的工具授权),
|
|
38
|
+
* 必须写回 CLI 的 control_response —— 当 user 消息发进去它不会读,整轮继续挂着。
|
|
34
39
|
*/
|
|
35
40
|
|
|
36
41
|
// controlCommand 一条控制命令。
|
|
@@ -38,6 +43,14 @@ type controlCommand struct {
|
|
|
38
43
|
Op string `json:"op"`
|
|
39
44
|
Reason string `json:"reason,omitempty"`
|
|
40
45
|
Text string `json:"text,omitempty"`
|
|
46
|
+
// RequestID 引擎 control_request 的 id(ask_answer 必填;原样带回 control_response)。
|
|
47
|
+
RequestID string `json:"request_id,omitempty"`
|
|
48
|
+
// Allowed 裁决结果(ask_answer 必填;nil = 没给 → 明确报错,不猜)。
|
|
49
|
+
Allowed *bool `json:"allowed,omitempty"`
|
|
50
|
+
// Message 拒绝理由 / 想回给引擎的话。
|
|
51
|
+
Message string `json:"message,omitempty"`
|
|
52
|
+
// ToolName 待裁决的工具名(只用于回执与排障,编码时不参与)。
|
|
53
|
+
ToolName string `json:"tool_name,omitempty"`
|
|
41
54
|
}
|
|
42
55
|
|
|
43
56
|
// controlHooks 控制命令的落地动作。
|
|
@@ -48,6 +61,9 @@ type controlHooks struct {
|
|
|
48
61
|
stop func()
|
|
49
62
|
// answer 把答案追进常驻会话;返回 false 表示通道此刻不可用。
|
|
50
63
|
answer func(text string) bool
|
|
64
|
+
// answerAsk 原地回应一条 control_request;返回 false 表示写不回去
|
|
65
|
+
//(引擎不支持 / stdin 已关 / 这一轮已结束),调用方据此回 control_error。
|
|
66
|
+
answerAsk func(requestID string, allowed bool, message, toolName string) bool
|
|
51
67
|
}
|
|
52
68
|
|
|
53
69
|
// controlSession 一次控制通道的生命周期。
|
|
@@ -129,10 +145,35 @@ func (s *controlSession) dispatch(c controlCommand) {
|
|
|
129
145
|
}
|
|
130
146
|
_ = s.sink.notice("answered", nil)
|
|
131
147
|
|
|
148
|
+
case "ask_answer":
|
|
149
|
+
if strings.TrimSpace(c.RequestID) == "" {
|
|
150
|
+
_ = s.sink.notice("control_error", map[string]any{
|
|
151
|
+
"op": "ask_answer", "error": "ask_answer 需要 request_id(引擎 control_request 的 id)",
|
|
152
|
+
})
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
if c.Allowed == nil {
|
|
156
|
+
_ = s.sink.notice("control_error", map[string]any{
|
|
157
|
+
"op": "ask_answer", "error": "ask_answer 需要 allowed(true = 允许,false = 拒绝)",
|
|
158
|
+
})
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
if s.hooks.answerAsk == nil || !s.hooks.answerAsk(c.RequestID, *c.Allowed, c.Message, c.ToolName) {
|
|
162
|
+
_ = s.sink.notice("control_error", map[string]any{
|
|
163
|
+
"op": "ask_answer",
|
|
164
|
+
"error": "这一轮没法原地回审批:引擎不支持控制应答,或 stdin 已关(本轮已结束)。" +
|
|
165
|
+
"请改用 answer(会把答复作为下一轮用户消息发进会话)",
|
|
166
|
+
})
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
_ = s.sink.notice("ask_answered", map[string]any{
|
|
170
|
+
"request_id": c.RequestID, "allowed": *c.Allowed, "tool_name": c.ToolName,
|
|
171
|
+
})
|
|
172
|
+
|
|
132
173
|
default:
|
|
133
174
|
_ = s.sink.notice("control_error", map[string]any{
|
|
134
175
|
"op": c.Op,
|
|
135
|
-
"error": "未知 op:支持 ping / interrupt / stop / answer",
|
|
176
|
+
"error": "未知 op:支持 ping / interrupt / stop / answer / ask_answer",
|
|
136
177
|
})
|
|
137
178
|
}
|
|
138
179
|
}
|
|
@@ -129,6 +129,71 @@ func TestControlAnswer(t *testing.T) {
|
|
|
129
129
|
})
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
// TestControlAskAnswer 原地回审批(ask_answer):参数校验、成败回执,以及
|
|
133
|
+
// 它和 answer(下一轮用户消息)是两条不同的路 —— 别把审批偷偷降级成追加。
|
|
134
|
+
func TestControlAskAnswer(t *testing.T) {
|
|
135
|
+
t.Run("缺 request_id", func(t *testing.T) {
|
|
136
|
+
w := newLineWriter()
|
|
137
|
+
ctl := startControlReader(strings.NewReader(`{"op":"ask_answer","allowed":true}`+"\n"),
|
|
138
|
+
newEventSink(w, "codebuddy", true), controlHooks{})
|
|
139
|
+
defer ctl.Close()
|
|
140
|
+
if ln := w.wait(t, 1)[0]; !strings.Contains(ln, "control_error") || !strings.Contains(ln, "request_id") {
|
|
141
|
+
t.Errorf("应回带 request_id 的 control_error,实际 %s", ln)
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
t.Run("缺 allowed", func(t *testing.T) {
|
|
146
|
+
w := newLineWriter()
|
|
147
|
+
ctl := startControlReader(strings.NewReader(`{"op":"ask_answer","request_id":"perm_1"}`+"\n"),
|
|
148
|
+
newEventSink(w, "codebuddy", true), controlHooks{})
|
|
149
|
+
defer ctl.Close()
|
|
150
|
+
if ln := w.wait(t, 1)[0]; !strings.Contains(ln, "control_error") || !strings.Contains(ln, "allowed") {
|
|
151
|
+
t.Errorf("应回带 allowed 的 control_error,实际 %s", ln)
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
t.Run("写不回去", func(t *testing.T) {
|
|
156
|
+
w := newLineWriter()
|
|
157
|
+
ctl := startControlReader(strings.NewReader(`{"op":"ask_answer","request_id":"perm_1","allowed":true}`+"\n"),
|
|
158
|
+
newEventSink(w, "codebuddy", true), controlHooks{})
|
|
159
|
+
defer ctl.Close()
|
|
160
|
+
if ln := w.wait(t, 1)[0]; !strings.Contains(ln, "control_error") {
|
|
161
|
+
t.Errorf("没有 answerAsk 时应回 control_error,实际 %s", ln)
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
t.Run("可用", func(t *testing.T) {
|
|
166
|
+
w := newLineWriter()
|
|
167
|
+
var gotReq atomic.Value
|
|
168
|
+
var gotAllowed atomic.Bool
|
|
169
|
+
var gotMsg atomic.Value
|
|
170
|
+
ctl := startControlReader(strings.NewReader(
|
|
171
|
+
`{"op":"ask_answer","request_id":"perm_9","allowed":false,"message":"先改改","tool_name":"ExitPlanMode"}`+"\n"),
|
|
172
|
+
newEventSink(w, "codebuddy", true), controlHooks{
|
|
173
|
+
answerAsk: func(requestID string, allowed bool, message, toolName string) bool {
|
|
174
|
+
gotReq.Store(requestID + "|" + toolName)
|
|
175
|
+
gotAllowed.Store(allowed)
|
|
176
|
+
gotMsg.Store(message)
|
|
177
|
+
return true
|
|
178
|
+
},
|
|
179
|
+
})
|
|
180
|
+
defer ctl.Close()
|
|
181
|
+
ln := w.wait(t, 1)[0]
|
|
182
|
+
if !strings.Contains(ln, `"type":"ask_answered"`) || !strings.Contains(ln, `"request_id":"perm_9"`) {
|
|
183
|
+
t.Errorf("应回 ask_answered 并带回 request_id,实际 %s", ln)
|
|
184
|
+
}
|
|
185
|
+
if v, _ := gotReq.Load().(string); v != "perm_9|ExitPlanMode" {
|
|
186
|
+
t.Errorf("hook 收到的 request_id/tool_name = %q", v)
|
|
187
|
+
}
|
|
188
|
+
if gotAllowed.Load() {
|
|
189
|
+
t.Error("allowed 应为 false")
|
|
190
|
+
}
|
|
191
|
+
if v, _ := gotMsg.Load().(string); v != "先改改" {
|
|
192
|
+
t.Errorf("message = %q want 先改改", v)
|
|
193
|
+
}
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
|
|
132
197
|
// TestControlInterruptAndStop interrupt 不立即回应答(应答在本轮真的结束时发),
|
|
133
198
|
// stop 立即回;两个标志位都要落。
|
|
134
199
|
func TestControlInterruptAndStop(t *testing.T) {
|
package/internal/cli/events.go
CHANGED
|
@@ -202,6 +202,9 @@ func streamEventPayload(ev agent.StreamEvent) any {
|
|
|
202
202
|
session_id)。omitempty:非团队事件不带它,老消费者字段集不变。 */
|
|
203
203
|
SubSessionID string `json:"sub_session_id,omitempty"`
|
|
204
204
|
Ask *agent.AskRequest `json:"ask,omitempty"`
|
|
205
|
+
/* 主备降级(2026-10-26,仅 type=fallback):from/to 两端 + 是否换了引擎 + 原因。
|
|
206
|
+
omitempty:其它事件不带它,老消费者字段集不变。 */
|
|
207
|
+
Fallback *agent.FallbackEvent `json:"fallback,omitempty"`
|
|
205
208
|
/* 状态收敛四元组(2026-10-02,对齐 agents-anywhere 的 timeline item)。
|
|
206
209
|
消费方按 (item_id, item_revision) 覆盖式 upsert 即可:
|
|
207
210
|
同一 item 的多个 partial 帧 + 最后一个 final 帧,
|
|
@@ -228,6 +231,7 @@ func streamEventPayload(ev agent.StreamEvent) any {
|
|
|
228
231
|
SessionID: ev.SessionID,
|
|
229
232
|
SubSessionID: ev.SubSessionID,
|
|
230
233
|
Ask: ev.Ask,
|
|
234
|
+
Fallback: ev.Fallback,
|
|
231
235
|
ItemID: ev.ItemID,
|
|
232
236
|
ItemRevision: ev.ItemRevision,
|
|
233
237
|
Status: string(ev.Status),
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
package cli
|
|
2
|
+
|
|
3
|
+
// quotas.go - `--quotas`:只查「账号额度 / 模型套餐额度 / 每日签到」的轻量命令。
|
|
4
|
+
//
|
|
5
|
+
// 为什么单独一条:`--engines` 为了拿模型清单要真的启动各引擎的 CLI(秒级),客户端
|
|
6
|
+
// 因此把它整份缓存起来;但额度是会变的(用一次就掉、换个 key 就是另一个账号)。
|
|
7
|
+
// 缓存整份清单会让额度跟着一起冻住 —— 用户看到「额度不刷新」。
|
|
8
|
+
//
|
|
9
|
+
// 这条命令**不探测引擎、不启动任何 CLI**:只读本机票据 / models.json,再发一两条
|
|
10
|
+
// 只读 HTTP 请求(codebuddy 系 2 条、llm 的 MiniMax 各 key 1 条),秒级返回。
|
|
11
|
+
// 客户端把它合并进缓存清单,就能做到「模型清单用缓存、额度每次现查」。
|
|
12
|
+
//
|
|
13
|
+
// 输出:`[{engine, credit_left?, credit_suffix?, model_quota?, checkin_today?, checkin_credit?}]`
|
|
14
|
+
// (字段与 --engines 同名同义;查不到的字段省略,调用方据此保留旧值)。
|
|
15
|
+
|
|
16
|
+
import (
|
|
17
|
+
"context"
|
|
18
|
+
"sync"
|
|
19
|
+
"time"
|
|
20
|
+
|
|
21
|
+
"github.com/darren/magic-agent/internal/agent"
|
|
22
|
+
"github.com/spf13/cobra"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
// quotaRow `--quotas` 的一行;字段语义与 --engines 对应字段完全一致。
|
|
26
|
+
type quotaRow struct {
|
|
27
|
+
Engine string `json:"engine"`
|
|
28
|
+
CreditLeft string `json:"credit_left,omitempty"`
|
|
29
|
+
CreditSuffix string `json:"credit_suffix,omitempty"`
|
|
30
|
+
ModelQuota map[string]agent.QuotaWindows `json:"model_quota,omitempty"`
|
|
31
|
+
CheckinToday *bool `json:"checkin_today,omitempty"`
|
|
32
|
+
CheckinCredit string `json:"checkin_credit,omitempty"`
|
|
33
|
+
// LoggedIn 该账号槽位是否已登录(只读票据文件,无网络)。登录 / 登出后
|
|
34
|
+
// 下一次打开选择器就能看到行数变化,不用等引擎清单缓存过期。
|
|
35
|
+
LoggedIn *bool `json:"logged_in,omitempty"`
|
|
36
|
+
// Account 登录的账号昵称(票据里的 account.nickname;未登录 / 无昵称时省略)。
|
|
37
|
+
Account string `json:"account,omitempty"`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// quotaProbeTimeout 整条命令的上限:额度查询本身各自有 8s 超时,这里再兜一层,
|
|
41
|
+
// 保证客户端不会因为一个挂住的账单接口把选择器拖到几十秒。
|
|
42
|
+
const quotaProbeTimeout = 6 * time.Second
|
|
43
|
+
|
|
44
|
+
// runQuotas 处理 `--quotas`。
|
|
45
|
+
func runQuotas(cmd *cobra.Command) error {
|
|
46
|
+
ctx, cancel := context.WithTimeout(cmd.Context(), quotaProbeTimeout)
|
|
47
|
+
defer cancel()
|
|
48
|
+
|
|
49
|
+
engines := agent.Engines()
|
|
50
|
+
rows := make([]quotaRow, 0, len(engines))
|
|
51
|
+
var mu sync.Mutex
|
|
52
|
+
var wg sync.WaitGroup
|
|
53
|
+
for _, e := range engines {
|
|
54
|
+
cl, hasCredit := e.(agent.CreditLister)
|
|
55
|
+
mq, hasQuota := e.(agent.ModelQuotaLister)
|
|
56
|
+
ck, hasCheckin := e.(agent.Checkiner)
|
|
57
|
+
if !hasCredit && !hasQuota && !hasCheckin {
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
unit := ""
|
|
61
|
+
if u, ok := e.(agent.CreditUnitLister); ok {
|
|
62
|
+
unit = u.CreditUnit()
|
|
63
|
+
}
|
|
64
|
+
i := len(rows)
|
|
65
|
+
rows = append(rows, quotaRow{Engine: e.Name()})
|
|
66
|
+
// 登录态只读票据文件,同步拿(不发网络);未登录的槽位客户端据此隐藏。
|
|
67
|
+
if lc, okl := e.(agent.LoginChecker); okl {
|
|
68
|
+
loggedIn := lc.LoggedIn()
|
|
69
|
+
rows[i].LoggedIn = &loggedIn
|
|
70
|
+
}
|
|
71
|
+
if al, okl := e.(agent.AccountLister); okl {
|
|
72
|
+
rows[i].Account = al.AccountName()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if hasCredit {
|
|
76
|
+
wg.Add(1)
|
|
77
|
+
go func(i int, l agent.CreditLister) {
|
|
78
|
+
defer wg.Done()
|
|
79
|
+
left := l.CreditLeft(ctx)
|
|
80
|
+
if left == "" {
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
mu.Lock()
|
|
84
|
+
rows[i].CreditLeft = left
|
|
85
|
+
if unit != "" {
|
|
86
|
+
rows[i].CreditSuffix = unit
|
|
87
|
+
}
|
|
88
|
+
mu.Unlock()
|
|
89
|
+
}(i, cl)
|
|
90
|
+
}
|
|
91
|
+
if hasQuota {
|
|
92
|
+
wg.Add(1)
|
|
93
|
+
go func(i int, l agent.ModelQuotaLister) {
|
|
94
|
+
defer wg.Done()
|
|
95
|
+
q := l.ModelQuota(ctx)
|
|
96
|
+
if len(q) == 0 {
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
mu.Lock()
|
|
100
|
+
rows[i].ModelQuota = q
|
|
101
|
+
if unit != "" {
|
|
102
|
+
rows[i].CreditSuffix = unit
|
|
103
|
+
}
|
|
104
|
+
mu.Unlock()
|
|
105
|
+
}(i, mq)
|
|
106
|
+
}
|
|
107
|
+
if hasCheckin {
|
|
108
|
+
wg.Add(1)
|
|
109
|
+
go func(i int, c agent.Checkiner) {
|
|
110
|
+
defer wg.Done()
|
|
111
|
+
info, known := c.CheckinStatus(ctx)
|
|
112
|
+
if !known {
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
checked := info.CheckedToday
|
|
116
|
+
mu.Lock()
|
|
117
|
+
rows[i].CheckinToday = &checked
|
|
118
|
+
rows[i].CheckinCredit = info.DailyCredit
|
|
119
|
+
mu.Unlock()
|
|
120
|
+
}(i, ck)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
wg.Wait()
|
|
124
|
+
return printJSON(cmd.OutOrStdout(), rows)
|
|
125
|
+
}
|
|
@@ -22,6 +22,7 @@ package cli
|
|
|
22
22
|
|
|
23
23
|
import (
|
|
24
24
|
"fmt"
|
|
25
|
+
"io"
|
|
25
26
|
"strings"
|
|
26
27
|
|
|
27
28
|
"github.com/spf13/cobra"
|
|
@@ -69,6 +70,23 @@ func runRequirements(cmd *cobra.Command, opts *askOptions) error {
|
|
|
69
70
|
return printJSON(cmd.OutOrStdout(), out)
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
// runRequirementsGraph 会话↔需求关系图谱:`--requirements-graph all|<ws_id|目录>`。
|
|
74
|
+
//
|
|
75
|
+
// 只读(与 --requirements 同族,客户端每刷一次都调,绝不能触发 LLM)。
|
|
76
|
+
// 聚合 relations.jsonl + 文档目录 → {ok,root,workspaces:[{id,name,dir,nodes,edges}]}。
|
|
77
|
+
func runRequirementsGraph(cmd *cobra.Command, opts *askOptions) error {
|
|
78
|
+
where := strings.TrimSpace(opts.reqGraph)
|
|
79
|
+
if where == "*" {
|
|
80
|
+
where = "all"
|
|
81
|
+
}
|
|
82
|
+
b, err := requirements.BuildGraph(where)
|
|
83
|
+
if err != nil {
|
|
84
|
+
return fmt.Errorf("--requirements-graph %s: %w", where, err)
|
|
85
|
+
}
|
|
86
|
+
_, err = cmd.OutOrStdout().Write(append(b, '\n'))
|
|
87
|
+
return err
|
|
88
|
+
}
|
|
89
|
+
|
|
72
90
|
// runConsolidateRequirements 立即归纳一轮:`--consolidate-requirements <ws_id|目录|.>`。
|
|
73
91
|
//
|
|
74
92
|
// `.` = 当前目录(与 `-w` 的默认语义一致:用户站在哪个项目里,整理的就是哪个项目)。
|
|
@@ -103,6 +121,35 @@ func runConsolidateRequirements(cmd *cobra.Command, opts *askOptions) error {
|
|
|
103
121
|
return printJSON(cmd.OutOrStdout(), res)
|
|
104
122
|
}
|
|
105
123
|
|
|
124
|
+
// runRequirementShot 给一个功能点追加一张「页面完成情况」截图:
|
|
125
|
+
// `--requirement-shot [<ws|dir>]` + `--requirement-name` / `--requirement-caption`。
|
|
126
|
+
//
|
|
127
|
+
// PNG 内容走 **stdin**(不走命令行参数):图片动辄几百 KB,放进 argv 会撞 Windows
|
|
128
|
+
// 命令行长度上限,也会把二进制搅进进程列表。stdin 的好处是调用方可以管线接。
|
|
129
|
+
func runRequirementShot(cmd *cobra.Command, opts *askOptions) error {
|
|
130
|
+
where := strings.TrimSpace(opts.reqShot)
|
|
131
|
+
if where == "." {
|
|
132
|
+
where = ""
|
|
133
|
+
}
|
|
134
|
+
name := strings.TrimSpace(opts.reqShotName)
|
|
135
|
+
if name == "" {
|
|
136
|
+
return &usageError{fmt.Errorf("--requirement-shot 需要 --requirement-name <功能点名>")}
|
|
137
|
+
}
|
|
138
|
+
png, err := io.ReadAll(io.LimitReader(cmd.InOrStdin(), requirements.MaxShotBytes+1))
|
|
139
|
+
if err != nil {
|
|
140
|
+
return fmt.Errorf("--requirement-shot: 读取 stdin 失败: %w", err)
|
|
141
|
+
}
|
|
142
|
+
res, err := requirements.AttachShot(where, opts.reqModule, name, opts.reqShotCap, png)
|
|
143
|
+
if err != nil && !requirements.IsBusy(err) {
|
|
144
|
+
if res.Error == "" {
|
|
145
|
+
res.Error = err.Error()
|
|
146
|
+
}
|
|
147
|
+
_ = printJSON(cmd.OutOrStdout(), res)
|
|
148
|
+
return fmt.Errorf("--requirement-shot %s: %s", where, err)
|
|
149
|
+
}
|
|
150
|
+
return printJSON(cmd.OutOrStdout(), res)
|
|
151
|
+
}
|
|
152
|
+
|
|
106
153
|
// bindRequirementFlags 注册需求聚合的 flags。
|
|
107
154
|
//
|
|
108
155
|
// --requirements / --consolidate-requirements 的值都可以留空(空串是合法输入:
|
|
@@ -110,6 +157,7 @@ func runConsolidateRequirements(cmd *cobra.Command, opts *askOptions) error {
|
|
|
110
157
|
func bindRequirementFlags(cmd *cobra.Command, opts *askOptions) {
|
|
111
158
|
f := cmd.PersistentFlags()
|
|
112
159
|
f.StringVar(&opts.requirements, "requirements", "", "列出需求清单(工作区 → 模块 → 功能点,JSON:{ok,root,workspaces})。值留空 = 列全部;给 ws id 或目录 = 只列那一个。只读,不调模型")
|
|
160
|
+
f.StringVar(&opts.reqGraph, "requirements-graph", "", "会话↔需求关系图谱(JSON:{ok,root,workspaces:[{nodes,edges}]},nodes 带 type=session|feature)。值留空 = 全部;只读,不调模型")
|
|
113
161
|
f.StringVar(&opts.consolidateReqs, "consolidate-requirements", "", "立即把会话记忆归纳成需求文档(会调模型)。值留空 = 当前目录;给 ws id 或目录 = 归纳那一个")
|
|
114
162
|
f.BoolVar(&opts.reqForce, "requirement-force", false, "配合 --consolidate-requirements:没有未归纳的记忆也跑一轮(手动「立即整理」)")
|
|
115
163
|
f.BoolVar(&opts.reqAll, "requirement-all", false, "配合 --consolidate-requirements:全量重放历史记忆(回填 / 滚动校正)。仍走增量合并,只增不减,不会推翻已有内容")
|
|
@@ -118,4 +166,7 @@ func bindRequirementFlags(cmd *cobra.Command, opts *askOptions) {
|
|
|
118
166
|
f.StringVar(&opts.reqModule, "requirement-module", "", "配合 --consolidate-requirements:只整理这个模块(空 = 整个空间)。不推进空间游标,不触发首次目录初始化")
|
|
119
167
|
f.DurationVar(&opts.reqTimeout, "requirement-timeout", requirements.DefaultTimeout, "配合 --consolidate-requirements:单次模型调用超时(默认归纳 4m / 首次目录初始化 9m)")
|
|
120
168
|
f.BoolVar(&opts.reqAuto, "requirement-auto", false, "配合 --consolidate-requirements:自动归纳(不触发首次按目录初始化;插件轮末 / 每日兜底用)")
|
|
169
|
+
f.StringVar(&opts.reqShot, "requirement-shot", "", "给功能点追加一张「页面完成情况」截图(PNG 走 stdin)。值留空 = 当前目录;给 ws id 或目录 = 那个空间")
|
|
170
|
+
f.StringVar(&opts.reqShotName, "requirement-name", "", "配合 --requirement-shot:功能点名(= 文档名)")
|
|
171
|
+
f.StringVar(&opts.reqShotCap, "requirement-caption", "", "配合 --requirement-shot:截图的标注说明")
|
|
121
172
|
}
|
package/internal/cli/root.go
CHANGED
|
@@ -33,6 +33,7 @@ package cli
|
|
|
33
33
|
// --temperature <t> 采样温度(仅 llm 引擎透传 -o temperature,其余忽略)
|
|
34
34
|
// --engines 列出引擎、可用性与支持的模型(JSON 数组;--json 兼容保留)
|
|
35
35
|
// --no-models 配合 --engines:跳过模型探测(只列引擎,快)
|
|
36
|
+
// --quotas 只查各引擎账号的额度与每日签到(不探测引擎/不启动 CLI,秒级)
|
|
36
37
|
// --stop <id> 停止指定会话/运行(session_id 或 run_id),杀掉它的引擎进程组
|
|
37
38
|
// --delete-session <id> 删除一条会话:登记记录 + 事件历史 + 标题覆盖一起删,**全机生效**
|
|
38
39
|
// (各端读同一份登记表,一端删了处处都看不到)。幂等
|
|
@@ -185,7 +186,7 @@ llm 引擎(包装 simonw/LLM CLI,模型/密钥由它自管):
|
|
|
185
186
|
每个可用引擎的模型清单都从它自己的权威入口现取现算,CLI 升级/新注册模型后自动跟随:
|
|
186
187
|
claude ~/.claude/settings.json(顶层 model + env 里 ANTHROPIC_*MODEL[*_NAME])
|
|
187
188
|
codebuddy codebuddy --help 里 --model 自带的 "Currently supported: (...)" 清单
|
|
188
|
-
trae
|
|
189
|
+
trae traex models --json
|
|
189
190
|
llm llm models
|
|
190
191
|
codex codex debug models
|
|
191
192
|
openclaw openclaw models list --json
|
|
@@ -202,7 +203,7 @@ llm 引擎(包装 simonw/LLM CLI,模型/密钥由它自管):
|
|
|
202
203
|
codex npm install -g @openai/codex
|
|
203
204
|
openclaw npm install -g openclaw@latest
|
|
204
205
|
dsh npm i -g @deepseek-ai/dsh
|
|
205
|
-
trae sh -c "$(curl -
|
|
206
|
+
trae sh -c "$(curl -fsSL https://trae.cn/trae-cli/install_v2.sh)" # Windows: irm https://trae.cn/trae-cli/install_v2.ps1 | iex
|
|
206
207
|
llm python3 -m venv ~/.llm-venv && ~/.llm-venv/bin/pip install llm
|
|
207
208
|
命令一律取自各 CLI 的官方安装方式;**没有可执行安装路径的引擎不给该字段**
|
|
208
209
|
(codebuddy / codebuddy-ai 是桌面端 GUI 应用,arkclaw 靠配置文件),
|
|
@@ -468,6 +469,9 @@ codebuddy-gateway 引擎(CodeBuddy Code HTTP 网关,走 webhook + SSE;凭
|
|
|
468
469
|
case flagChanged(cmd, "repair-models"):
|
|
469
470
|
// 同上:`--repair-models ""` 要走参数校验,而不是掉进「提问」分支报 empty prompt。
|
|
470
471
|
return runRepairModels(cmd, opts)
|
|
472
|
+
case flagChanged(cmd, "checkin"):
|
|
473
|
+
// 同上:`--checkin ""` 要走参数校验。
|
|
474
|
+
return runCheckin(cmd, opts)
|
|
471
475
|
case opts.listSessions:
|
|
472
476
|
return runSessions(cmd, opts)
|
|
473
477
|
case flagChanged(cmd, "session-log"):
|
|
@@ -500,9 +504,15 @@ codebuddy-gateway 引擎(CodeBuddy Code HTTP 网关,走 webhook + SSE;凭
|
|
|
500
504
|
case flagChanged(cmd, "requirements"):
|
|
501
505
|
// 读清单:值留空 = 列全部工作区(`--requirements ""` 也是合法调用)。
|
|
502
506
|
return runRequirements(cmd, opts)
|
|
507
|
+
case flagChanged(cmd, "requirements-graph"):
|
|
508
|
+
// 读图谱:值留空 = 全部工作区。只读,不调模型。
|
|
509
|
+
return runRequirementsGraph(cmd, opts)
|
|
503
510
|
case flagChanged(cmd, "consolidate-requirements"):
|
|
504
511
|
// 写:跑一轮归纳。值留空 = 当前目录。
|
|
505
512
|
return runConsolidateRequirements(cmd, opts)
|
|
513
|
+
case flagChanged(cmd, "requirement-shot"):
|
|
514
|
+
// 写:给功能点追加一张完成情况截图(PNG 走 stdin)。
|
|
515
|
+
return runRequirementShot(cmd, opts)
|
|
506
516
|
case flagChanged(cmd, "requirement-force"), flagChanged(cmd, "requirement-all"),
|
|
507
517
|
flagChanged(cmd, "requirement-engine"), flagChanged(cmd, "requirement-model"),
|
|
508
518
|
flagChanged(cmd, "requirement-timeout"), flagChanged(cmd, "requirement-auto"):
|
|
@@ -510,6 +520,10 @@ codebuddy-gateway 引擎(CodeBuddy Code HTTP 网关,走 webhook + SSE;凭
|
|
|
510
520
|
// 静默忽略就等于「传了参数什么都没发生」。
|
|
511
521
|
return &usageError{fmt.Errorf("--requirement-force / -all / -engine / -model / -timeout / -auto " +
|
|
512
522
|
"只在 --consolidate-requirements 下成立:请配合它使用")}
|
|
523
|
+
case flagChanged(cmd, "requirement-name"), flagChanged(cmd, "requirement-caption"):
|
|
524
|
+
// 这两个是 --requirement-shot 的修饰项(截图落在哪个功能点、标注写什么)。
|
|
525
|
+
return &usageError{fmt.Errorf("--requirement-name / --requirement-caption " +
|
|
526
|
+
"只在 --requirement-shot 下成立:请配合它使用")}
|
|
513
527
|
case flagChanged(cmd, "stop"):
|
|
514
528
|
// 用「传过没有」而不是「值非空」判断:`--stop ""` 也要走参数校验
|
|
515
529
|
//(exit 2 提示要 id),而不是掉进「提问」分支报 empty prompt。
|
|
@@ -523,6 +537,8 @@ codebuddy-gateway 引擎(CodeBuddy Code HTTP 网关,走 webhook + SSE;凭
|
|
|
523
537
|
return runContract(cmd, opts)
|
|
524
538
|
case opts.engines:
|
|
525
539
|
return runEngines(cmd, opts)
|
|
540
|
+
case opts.quotas:
|
|
541
|
+
return runQuotas(cmd)
|
|
526
542
|
case opts.team:
|
|
527
543
|
// 团队模式必须排在 --stream 之前:它自己就是流式路径的一种运行模式
|
|
528
544
|
//(同一套 ready / seq / result 收尾),但内部编排完全不同。
|
|
@@ -767,7 +783,7 @@ func flagChanged(cmd *cobra.Command, name string) bool {
|
|
|
767
783
|
// 实测只有「受理帧 + 整段正文」,见 internal/agent/arkclaw_stream.go
|
|
768
784
|
//
|
|
769
785
|
// 模型**一律现取现算、不硬编码**:各引擎用自己 CLI/配置文件里的权威入口
|
|
770
|
-
// (
|
|
786
|
+
// (traex models / llm models / codex debug models / openclaw models list /
|
|
771
787
|
// codebuddy --help / claude settings.json)。探测要真的启动 CLI(只读子命令),
|
|
772
788
|
// 故并发执行并套 agent.DefaultModelProbeTimeout;--no-models 可整段跳过(只列引擎,快)。
|
|
773
789
|
// --json 为兼容旧调用保留(本函数不再区分,恒为 JSON)。
|
|
@@ -823,6 +839,40 @@ func emitEngineRows(cmd *cobra.Command, opts *askOptions, envelope bool) error {
|
|
|
823
839
|
// 仅实现了 agent.ModelCreditLister 的引擎给出 —— 目前 codebuddy / codebuddy-ai)。
|
|
824
840
|
// 客户端不给倍率的模型(custom-local)不出现在 map 里。
|
|
825
841
|
ModelCredits map[string]string `json:"model_credits,omitempty"`
|
|
842
|
+
// ModelVendors 各模型的供应商展示名(model → 供应商,如 "Volcengine");
|
|
843
|
+
// 与 models 同形同链,键已剥 `custom-local:` 前缀。只用于**选择器的第二行**,
|
|
844
|
+
// 不参与 -m / 落库。仅实现 agent.ModelVendorLister 的引擎给出(目前 codebuddy 系),
|
|
845
|
+
// 其余引擎省略该字段(新增字段,不升 contractVersion)。
|
|
846
|
+
ModelVendors map[string]string `json:"model_vendors,omitempty"`
|
|
847
|
+
// CreditLeft 账号当前**剩余积分**(decimal 字符串,如 "5939.2";单位 credits)。
|
|
848
|
+
// 仅实现了 agent.CreditLister 的引擎给出(codebuddy / codebuddy-ai),
|
|
849
|
+
// 各自读自己票据(codebuddy-ai 读隔离目录),查不到就省略该字段。
|
|
850
|
+
// 新增(2026-10-24,只增字段不升 contractVersion)。
|
|
851
|
+
CreditLeft string `json:"credit_left,omitempty"`
|
|
852
|
+
// CreditSuffix CreditLeft / ModelQuota 的单位后缀(缺省「积分」)。llm 的
|
|
853
|
+
// MiniMax 套餐额度是百分比,给 "%" —— 客户端据此显示「5h 98% · 周 53%」。
|
|
854
|
+
// 新增(2026-10-25,只增字段不升 contractVersion)。
|
|
855
|
+
CreditSuffix string `json:"credit_suffix,omitempty"`
|
|
856
|
+
// ModelQuota 按**模型**给出的套餐额度快照(model id → {interval_left, weekly_left},
|
|
857
|
+
// 两个窗口的剩余百分比,单位见 credit_suffix)。仅实现了 agent.ModelQuotaLister
|
|
858
|
+
// 的引擎给出 —— 目前是 llm(MiniMax 套餐;只挂在走 MiniMax 端点的模型上)。
|
|
859
|
+
// 新增(2026-10-25,只增字段不升 contractVersion)。
|
|
860
|
+
ModelQuota map[string]agent.QuotaWindows `json:"model_quota,omitempty"`
|
|
861
|
+
// CheckinToday 今天是否已领每日积分(true / false);拿不到状态时**省略**该字段
|
|
862
|
+
// (nil 指针),客户端据此决定要不要画「领取」按钮。
|
|
863
|
+
// 新增(2026-10-24,只增字段不升 contractVersion)。
|
|
864
|
+
CheckinToday *bool `json:"checkin_today,omitempty"`
|
|
865
|
+
// CheckinCredit 今日可领积分(字符串,如 "100";活动没开 / 拿不到时省略)。
|
|
866
|
+
CheckinCredit string `json:"checkin_credit,omitempty"`
|
|
867
|
+
// LoggedIn 该账号槽位是否已登录(仅实现 agent.LoginChecker 的多账号引擎给出:
|
|
868
|
+
// codebuddy / codebuddy-ai)。客户端据此**只显示已登录的账号** ——
|
|
869
|
+
// 「登录了几个账号就显示几个」,而不是固定两行。
|
|
870
|
+
// 新增(2026-10-25,只增字段不升 contractVersion);老 core 没有该字段时客户端按「显示」处理。
|
|
871
|
+
LoggedIn *bool `json:"logged_in,omitempty"`
|
|
872
|
+
// Account 该账号槽位登录的**账号昵称**(如 "Darren" / "玲";仅 codebuddy 系给出,
|
|
873
|
+
// 来自票据里的 account.nickname)。选择器把它画在引擎名下面,一眼看出这行是哪个号。
|
|
874
|
+
// 新增(2026-10-25,只增字段不升 contractVersion)。
|
|
875
|
+
Account string `json:"account,omitempty"`
|
|
826
876
|
// Workspace 该引擎「指定工作目录」的落地方式(machine-readable,供调用方决定是否下发):
|
|
827
877
|
// flag:-C 原生 flag(codex);cwd 子进程 cwd(claude/codebuddy/trae);
|
|
828
878
|
// none 无落地通道(llm/arkclaw/openclaw/codebuddy-gateway,参数会被忽略)
|
|
@@ -896,6 +946,16 @@ func emitEngineRows(cmd *cobra.Command, opts *askOptions, envelope bool) error {
|
|
|
896
946
|
if ok {
|
|
897
947
|
r.Bin, r.Note = note, ""
|
|
898
948
|
}
|
|
949
|
+
/* 多账号引擎(codebuddy 系):这个槽位有没有登录。没登录的槽位客户端不显示,
|
|
950
|
+
所以「登录了几个账号就显示几个」;探测本身不依赖它(模型清单照旧给)。 */
|
|
951
|
+
if lc, okl := e.(agent.LoginChecker); okl {
|
|
952
|
+
loggedIn := lc.LoggedIn()
|
|
953
|
+
r.LoggedIn = &loggedIn
|
|
954
|
+
}
|
|
955
|
+
/* 账号昵称:选择器里两个 codebuddy 槽位只差引擎名,标上「这行是哪个号」。 */
|
|
956
|
+
if al, okl := e.(agent.AccountLister); okl {
|
|
957
|
+
r.Account = al.AccountName()
|
|
958
|
+
}
|
|
899
959
|
/* 安装 / 升级命令:**可用时也给**(同一条命令重跑一遍 = 升级到最新版)。
|
|
900
960
|
没有可执行安装路径的引擎留空 —— 见 InstallCommandOf 的说明,不编命令。 */
|
|
901
961
|
r.Install = agent.InstallCommandOf(e.Name())
|
|
@@ -914,6 +974,51 @@ func emitEngineRows(cmd *cobra.Command, opts *askOptions, envelope bool) error {
|
|
|
914
974
|
rows[i].Version = agent.EngineVersionOf(cmd.Context(), bin)
|
|
915
975
|
}(i, r.Bin)
|
|
916
976
|
}
|
|
977
|
+
/* 账号剩余额度(codebuddy 系积分 / llm 的 MiniMax 套餐百分比):只读本机票据
|
|
978
|
+
或 models.json + 一次只读请求。与模型探测并发跑;查不到就不给该字段。 */
|
|
979
|
+
if cl, okc := e.(agent.CreditLister); okc {
|
|
980
|
+
eng := e
|
|
981
|
+
wg.Add(1)
|
|
982
|
+
go func(i int, l agent.CreditLister) {
|
|
983
|
+
defer wg.Done()
|
|
984
|
+
left := l.CreditLeft(cmd.Context())
|
|
985
|
+
rows[i].CreditLeft = left
|
|
986
|
+
if left != "" {
|
|
987
|
+
if u, oku := eng.(agent.CreditUnitLister); oku {
|
|
988
|
+
rows[i].CreditSuffix = u.CreditUnit()
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
}(i, cl)
|
|
992
|
+
}
|
|
993
|
+
/* 模型级套餐额度(llm 的 MiniMax 模型):同一账号一份快照,挂到每个走该端点的模型。 */
|
|
994
|
+
if mq, okm := e.(agent.ModelQuotaLister); okm {
|
|
995
|
+
eng := e
|
|
996
|
+
wg.Add(1)
|
|
997
|
+
go func(i int, l agent.ModelQuotaLister) {
|
|
998
|
+
defer wg.Done()
|
|
999
|
+
q := l.ModelQuota(cmd.Context())
|
|
1000
|
+
if len(q) == 0 {
|
|
1001
|
+
return
|
|
1002
|
+
}
|
|
1003
|
+
rows[i].ModelQuota = q
|
|
1004
|
+
if u, oku := eng.(agent.CreditUnitLister); oku {
|
|
1005
|
+
rows[i].CreditSuffix = u.CreditUnit()
|
|
1006
|
+
}
|
|
1007
|
+
}(i, mq)
|
|
1008
|
+
}
|
|
1009
|
+
/* 每日签到状态(同上,仅 codebuddy 系):今天是否已领 + 可领多少,
|
|
1010
|
+
客户端据此画「领取」按钮;拿不到就省略两个字段。 */
|
|
1011
|
+
if ck, okc := e.(agent.Checkiner); okc {
|
|
1012
|
+
wg.Add(1)
|
|
1013
|
+
go func(i int, c agent.Checkiner) {
|
|
1014
|
+
defer wg.Done()
|
|
1015
|
+
if info, known := c.CheckinStatus(cmd.Context()); known {
|
|
1016
|
+
claimed := info.CheckedToday
|
|
1017
|
+
rows[i].CheckinToday = &claimed
|
|
1018
|
+
rows[i].CheckinCredit = info.DailyCredit
|
|
1019
|
+
}
|
|
1020
|
+
}(i, ck)
|
|
1021
|
+
}
|
|
917
1022
|
if !ok {
|
|
918
1023
|
continue
|
|
919
1024
|
}
|
|
@@ -923,7 +1028,7 @@ func emitEngineRows(cmd *cobra.Command, opts *askOptions, envelope bool) error {
|
|
|
923
1028
|
continue
|
|
924
1029
|
}
|
|
925
1030
|
wg.Add(1)
|
|
926
|
-
go func(i int, name string, l agent.ModelLister) {
|
|
1031
|
+
go func(i int, name string, l agent.ModelLister, eng agent.Engine) {
|
|
927
1032
|
defer wg.Done()
|
|
928
1033
|
ctx, cancel := context.WithTimeout(cmd.Context(), agent.DefaultModelProbeTimeout)
|
|
929
1034
|
defer cancel()
|
|
@@ -946,7 +1051,19 @@ func emitEngineRows(cmd *cobra.Command, opts *askOptions, envelope bool) error {
|
|
|
946
1051
|
rows[i].ModelCredits = credits
|
|
947
1052
|
}
|
|
948
1053
|
}
|
|
949
|
-
|
|
1054
|
+
/* 供应商展示名(选择器第二行):与 models 同形,键同样剥前缀。
|
|
1055
|
+
拿不到就整个省略 —— 界面据此决定画不画第二行(dsh 的第二行是路由,
|
|
1056
|
+
走的是另一条「id 里自带 route/」的路径,与此无关)。 */
|
|
1057
|
+
if vl := agent.ModelVendorListOf(eng); vl != nil {
|
|
1058
|
+
vendors := vl.ModelVendors(ctx)
|
|
1059
|
+
if name == "codebuddy" || name == "codebuddy-ai" {
|
|
1060
|
+
vendors = agent.StripCustomLocalPrefixCredits(vendors)
|
|
1061
|
+
}
|
|
1062
|
+
if len(vendors) > 0 {
|
|
1063
|
+
rows[i].ModelVendors = vendors
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}(i, e.Name(), lister, e)
|
|
950
1067
|
}
|
|
951
1068
|
wg.Wait()
|
|
952
1069
|
if !envelope {
|
package/internal/cli/session.go
CHANGED
|
@@ -397,6 +397,12 @@ func runSessions(cmd *cobra.Command, opts *askOptions) error {
|
|
|
397
397
|
if title == "" {
|
|
398
398
|
title = r.PromptHead
|
|
399
399
|
}
|
|
400
|
+
/* 排队占位(服务端作业队列预登记,state=queued)还没有会话日志,归纳必然失败。
|
|
401
|
+
这里直接报「已定稿」,客户端就不会每轮列表刷新都去打一次 --summarize-title。
|
|
402
|
+
真开跑后 core 那条记录照常走归纳。 */
|
|
403
|
+
if r.State == session.StateQueued {
|
|
404
|
+
summarized = true
|
|
405
|
+
}
|
|
400
406
|
/* 统一状态:一条会话一个值(各轮的行给出同一个),决定客户端画「进行中 / 已完成」。
|
|
401
407
|
没有条目(改造前的老记录)才回落这一轮的 state。alive 也**按它算** ——
|
|
402
408
|
否则一行里 status=idle、alive=true 自相矛盾,客户端又会分叉。 */
|
|
@@ -425,21 +431,12 @@ func runSessions(cmd *cobra.Command, opts *askOptions) error {
|
|
|
425
431
|
TitleSummarized: summarized,
|
|
426
432
|
StartedAt: r.StartedAt.Format("2006-01-02 15:04:05"),
|
|
427
433
|
UpdatedAt: r.UpdatedAt.Format("2006-01-02 15:04:05"),
|
|
428
|
-
HasLog:
|
|
434
|
+
HasLog: session.HasSessionLogIn(logDir, r.SessionID, r.RunID),
|
|
429
435
|
})
|
|
430
436
|
}
|
|
431
437
|
return printJSON(cmd.OutOrStdout(), rows)
|
|
432
438
|
}
|
|
433
439
|
|
|
434
|
-
// hasSessionLog 这条记录有没有可回放的事件日志。
|
|
435
|
-
//
|
|
436
|
-
// 日志文件名可能是 **session_id**(终名),也可能是 **run_id** —— 引擎还没吐 id 时
|
|
437
|
-
// core 先按 run_id 落盘(见 sessionlog.go 的 setRunID / renameTo),所以两个名字都试一次;
|
|
438
|
-
// 决议逻辑(含 run_id → session_id 的翻译)在 session.HasSessionLog 里,读接口共用同一份。
|
|
439
|
-
func hasSessionLog(dir string, rec session.Record) bool {
|
|
440
|
-
return session.HasSessionLog(dir, rec.SessionID) || session.HasSessionLog(dir, rec.RunID)
|
|
441
|
-
}
|
|
442
|
-
|
|
443
440
|
// runSessionLog 读一段会话的**事件历史**:`--session-log <id> [--after <seq>]`。
|
|
444
441
|
//
|
|
445
442
|
// 为什么这条命令必须存在:会话的「查询」原先只有 HTTP 插件能做(`/desk/session/{id}/messages`),
|
package/internal/cli/suggest.go
CHANGED
|
@@ -45,11 +45,13 @@ func runSuggestFollowups(cmd *cobra.Command, opts *askOptions) error {
|
|
|
45
45
|
model = opts.model
|
|
46
46
|
}
|
|
47
47
|
res, err := suggest.Generate(cmd.Context(), suggest.Request{
|
|
48
|
-
User:
|
|
49
|
-
Reply:
|
|
50
|
-
Engine:
|
|
51
|
-
Model:
|
|
52
|
-
|
|
48
|
+
User: body.User,
|
|
49
|
+
Reply: body.Reply,
|
|
50
|
+
Engine: engine,
|
|
51
|
+
Model: model,
|
|
52
|
+
FallbackEngine: strings.TrimSpace(opts.suggestFallbackEngine),
|
|
53
|
+
FallbackModel: strings.TrimSpace(opts.suggestFallbackModel),
|
|
54
|
+
Timeout: suggest.DefaultTimeout,
|
|
53
55
|
})
|
|
54
56
|
if err != nil {
|
|
55
57
|
return fmt.Errorf("--suggest-followups: 生成失败: %w", err)
|