magic-agent 2.1.5 → 2.3.2
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 +147 -11
- package/README.md +10 -4
- package/client-ui/pc/app.css +17 -0
- package/client-ui/pc/index.html +5 -0
- package/client-ui/pc/js/05-task-list-render.js +28 -4
- package/client-ui/pc/js/07-chat-stream-render.js +19 -8
- package/client-ui/pc/js/93-rail.js +24 -2
- package/client-ui/shared/conv/03-chat-message.js +63 -3
- package/client-ui/shared/conv/06-ask-answer.js +30 -7
- package/client-ui/shared/conv/09-stream-apply.js +16 -0
- package/client-ui/shared/conv/19-attachments.js +75 -11
- package/client-ui/shared/conv/27-conv-ledger.js +1 -1
- package/client-ui/shared/conv/29-msg-variants.js +56 -3
- package/client-ui/shared/conv/README.md +6 -0
- package/client-ui/shared/conv/conv.css +33 -1
- package/client-ui/shared/conversation.css +42 -4
- package/client-ui/shared/conversation.js +801 -102
- package/client-ui/shared/transport-http.js +199 -38
- package/client-ui-mobile/index.html +731 -581
- package/client-ui-mobile/reqs.css +39 -0
- package/client-ui-mobile/reqs.js +520 -0
- package/cmd/magic-client/main.go +1 -1
- package/internal/agent/capability.go +8 -1
- package/internal/agent/claude.go +5 -3
- package/internal/agent/codebuddy.go +6 -2
- package/internal/agent/codex.go +7 -2
- package/internal/agent/dsh_sdk.go +8 -0
- package/internal/agent/dsh_sdk_test.go +17 -0
- package/internal/agent/engine.go +20 -0
- package/internal/agent/engine_test.go +52 -0
- package/internal/agent/llmengine.go +14 -0
- package/internal/agent/llmengine_test.go +24 -0
- package/internal/agent/llmhttp.go +6 -0
- package/internal/agent/llmhttp_test.go +27 -0
- package/internal/agent/llmmodels.go +48 -0
- package/internal/agent/permission.go +5 -0
- package/internal/cli/ask.go +38 -17
- package/internal/cli/requirements.go +2 -0
- package/internal/cli/root.go +5 -0
- package/internal/cli/session.go +16 -14
- package/internal/cli/sessionlog.go +5 -0
- package/internal/cli/sessionlog_user_test.go +49 -0
- package/internal/cli/suggest.go +98 -0
- package/internal/cli/suggest_test.go +54 -0
- package/internal/cli/team.go +111 -24
- package/internal/client/requirements.go +27 -7
- package/internal/client/requirements_test.go +51 -0
- package/internal/client/service.go +35 -3
- package/internal/client/service_test.go +35 -0
- package/internal/client/suggest.go +131 -0
- package/internal/client/suggest_test.go +116 -0
- package/internal/requirements/consolidate.go +86 -24
- package/internal/requirements/requirements.go +9 -0
- package/internal/requirements/requirements_test.go +129 -0
- package/internal/requirements/store.go +11 -3
- package/internal/session/delete.go +58 -11
- package/internal/session/delete_test.go +140 -0
- package/internal/session/session.go +25 -0
- package/internal/session/session_mark_test.go +62 -0
- package/internal/suggest/suggest.go +532 -0
- package/internal/suggest/suggest_test.go +268 -0
- package/internal/team/board.go +107 -15
- package/internal/team/execute.go +1 -1
- package/internal/team/team.go +28 -19
- package/internal/team/team_test.go +68 -15
- package/npm/apk/magic-agent-mobile-release.apk +0 -0
- package/npm/apk/version.json +3 -3
- 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 +6 -2
|
@@ -167,6 +167,11 @@ type dshSDKInitParams struct {
|
|
|
167
167
|
Provider string `json:"provider"`
|
|
168
168
|
Model string `json:"model"`
|
|
169
169
|
MaxTokens int `json:"maxTokens,omitempty"`
|
|
170
|
+
// ReasoningEffort 为空 = 沿用模型默认;NoThinking 时传官方值 "off"。
|
|
171
|
+
// 官方依据:dsh-sdk-protocol InitializeParams.reasoningEffort;
|
|
172
|
+
// dsh-llm-deepseek README:"off" 序列化为 thinking.type=disabled。
|
|
173
|
+
// pi-ai 路由需在 profile 声明 reasoningEfforts.off(本机 sdk patch 已加)。
|
|
174
|
+
ReasoningEffort string `json:"reasoningEffort,omitempty"`
|
|
170
175
|
}
|
|
171
176
|
|
|
172
177
|
// dshSDKTextBlock = TextBlock(ContentBlock 的 text 成员)。
|
|
@@ -410,6 +415,9 @@ func dshSDKTurn(ctx context.Context, bin, workspace string, req Request, prompt
|
|
|
410
415
|
|
|
411
416
|
// ① initialize:握手失败 = SDK 通道不可用(旧版 dsh 没有该 profile 时会走到这里)。
|
|
412
417
|
initParams := dshSDKInitParams{Cwd: workspace, Provider: provider, Model: model, MaxTokens: req.MaxTokens}
|
|
418
|
+
if req.NoThinking {
|
|
419
|
+
initParams.ReasoningEffort = "off"
|
|
420
|
+
}
|
|
413
421
|
initID, err := send("initialize", initParams)
|
|
414
422
|
if err != nil {
|
|
415
423
|
return dshSDKTurnResult{}, fmt.Errorf("%w: 写 initialize: %v", errDshSDKUnavailable, err)
|
|
@@ -261,6 +261,23 @@ func TestDshSDKIgnoresOtherSessions(t *testing.T) {
|
|
|
261
261
|
}
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
+
// NoThinking:官方 InitializeParams.reasoningEffort=off,不能只靠隐藏事件。
|
|
265
|
+
func TestDshSDKNoThinkingSendsOff(t *testing.T) {
|
|
266
|
+
dshSDKHome(t, "modelverse", "deepseek-v4.1-flash")
|
|
267
|
+
dir := t.TempDir()
|
|
268
|
+
bin, _, stdinLog := dshSDKStub(t, dir, dshSDKStubOpts{})
|
|
269
|
+
|
|
270
|
+
if _, err := (&DshEngine{BinPath: bin}).Complete(context.Background(), Request{
|
|
271
|
+
Messages: []Message{{Role: "user", Content: "hi"}},
|
|
272
|
+
NoThinking: true,
|
|
273
|
+
}); err != nil {
|
|
274
|
+
t.Fatalf("Complete: %v", err)
|
|
275
|
+
}
|
|
276
|
+
if got := readFileString(t, stdinLog); !strings.Contains(got, `"reasoningEffort":"off"`) {
|
|
277
|
+
t.Errorf("initialize 应带 reasoningEffort=off: %s", got)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
264
281
|
// initialize 被服务端明确拒绝(模型没配)→ **真失败**,不静默回退 headless。
|
|
265
282
|
func TestDshSDKInitializeRejectedIsFatal(t *testing.T) {
|
|
266
283
|
dshSDKHome(t, "modelverse", "no-such-model")
|
package/internal/agent/engine.go
CHANGED
|
@@ -154,6 +154,16 @@ type Request struct {
|
|
|
154
154
|
// trae/llm/openclaw/dsh/arkclaw 无对应参数。
|
|
155
155
|
Reasoning string
|
|
156
156
|
|
|
157
|
+
// NoThinking 请求引擎关闭模型思考:
|
|
158
|
+
// claude --thinking disabled(官方)
|
|
159
|
+
// codex -c model_reasoning_effort="none" + model_reasoning_summary="none"
|
|
160
|
+
// codebuddy settings alwaysThinkingEnabled=false(官方 settings / SDK thinking:disabled)
|
|
161
|
+
// dsh InitializeParams.reasoningEffort="off"(官方 SDK;dsh-llm-deepseek 会发 thinking.type=disabled)
|
|
162
|
+
// llm 选同线 thinking.disabled 变体;没有变体时传 reasoning_effort="none"
|
|
163
|
+
// (CLI 委托路径用 llm 的官方 -o reasoning_effort none)
|
|
164
|
+
// 其余引擎没有已知开关;调用方(建议生成)仍会标记,失败后再降级 llm。
|
|
165
|
+
NoThinking bool
|
|
166
|
+
|
|
157
167
|
// JSONSchema 是可选的结构化输出约束。
|
|
158
168
|
//
|
|
159
169
|
// 各引擎行为:
|
|
@@ -274,6 +284,16 @@ type Response struct {
|
|
|
274
284
|
Attempts int
|
|
275
285
|
}
|
|
276
286
|
|
|
287
|
+
// NoThinkingSupportOf 引擎是否有已知的关闭思考手段。
|
|
288
|
+
// 只列官方文档/参数确认过的;未知引擎返回 false,调用方应提示而不是假装关闭。
|
|
289
|
+
func NoThinkingSupportOf(name string) bool {
|
|
290
|
+
switch name {
|
|
291
|
+
case "claude", "codex", "codebuddy", "codebuddy-ai", "dsh", "llm":
|
|
292
|
+
return true
|
|
293
|
+
}
|
|
294
|
+
return false
|
|
295
|
+
}
|
|
296
|
+
|
|
277
297
|
// Engine 是一个 agent CLI 后端的统一抽象。
|
|
278
298
|
type Engine interface {
|
|
279
299
|
// Name 引擎标识("claude" / "codebuddy" / "trae")。
|
|
@@ -190,6 +190,42 @@ func TestReasoningEffortArgs(t *testing.T) {
|
|
|
190
190
|
t.Errorf("codex model_reasoning_effort 未透传: %v", codexArgs)
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
/* NoThinking(建议生成):每个有原生开关的引擎都要真的关闭,不能只是 minimal。 */
|
|
194
|
+
claudeNo := (&ClaudeEngine{}).buildArgsBase(Request{NoThinking: true, Reasoning: "max"})
|
|
195
|
+
if !hasPair(claudeNo, "--thinking", "disabled") || hasPair(claudeNo, "--effort", "max") {
|
|
196
|
+
t.Errorf("claude NoThinking 应 --thinking disabled 且不传 effort: %v", claudeNo)
|
|
197
|
+
}
|
|
198
|
+
// codebuddy:NoThinking 走官方 settings alwaysThinkingEnabled=false,
|
|
199
|
+
// 不能再传 --effort xhigh(那会把思考档位又打开)。
|
|
200
|
+
cbNo := (&CodeBuddyEngine{}).buildArgsBase(Request{NoThinking: true, Reasoning: "xhigh"})
|
|
201
|
+
if hasPair(cbNo, "--effort", "off") || hasPair(cbNo, "--effort", "xhigh") {
|
|
202
|
+
t.Errorf("codebuddy NoThinking 不应传 --effort: %v", cbNo)
|
|
203
|
+
}
|
|
204
|
+
codexNo := (&CodexEngine{}).buildArgs(Request{NoThinking: true, Reasoning: "max"}, "hi", "", "")
|
|
205
|
+
if !hasPair(codexNo, "-c", `model_reasoning_effort="none"`) ||
|
|
206
|
+
!hasPair(codexNo, "-c", `model_reasoning_summary="none"`) ||
|
|
207
|
+
hasPair(codexNo, "-c", `model_reasoning_effort="max"`) {
|
|
208
|
+
t.Errorf("codex NoThinking 应 none 且不传 max: %v", codexNo)
|
|
209
|
+
}
|
|
210
|
+
// dsh 的 launcher/headless 没有 reasoning 开关:NoThinking 对它保持现状,
|
|
211
|
+
// 不能塞 --effort(实测启动器报 "--profile <name> is required")。
|
|
212
|
+
dshNo := dshHeadlessArgs("headless", "", "hi", false)
|
|
213
|
+
if len(dshNo) < 3 || dshNo[0] != "--profile" || dshNo[1] != "headless" {
|
|
214
|
+
t.Errorf("dsh 参数不应被 NoThinking 改坏: %v", dshNo)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// 官方关闭思考能力表:只列有依据的;trae/openclaw/arkclaw 不假装支持。
|
|
218
|
+
for _, e := range []string{"claude", "codex", "codebuddy", "dsh", "llm"} {
|
|
219
|
+
if !NoThinkingSupportOf(e) {
|
|
220
|
+
t.Errorf("NoThinkingSupportOf(%s) 应为 true", e)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
for _, e := range []string{"trae", "openclaw", "arkclaw"} {
|
|
224
|
+
if NoThinkingSupportOf(e) {
|
|
225
|
+
t.Errorf("NoThinkingSupportOf(%s) 应为 false", e)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
193
229
|
// off/none/disabled 一律归一为「不传」。
|
|
194
230
|
for _, v := range []string{"off", "none", "disabled", " OFF "} {
|
|
195
231
|
if lv := ReasoningLevel(v); lv != "" {
|
|
@@ -893,6 +929,22 @@ func TestCodeBuddyMaxTokensSettings(t *testing.T) {
|
|
|
893
929
|
}
|
|
894
930
|
}
|
|
895
931
|
|
|
932
|
+
// TestCodeBuddyNoThinkingSettings 官方 settings 键:NoThinking 时 alwaysThinkingEnabled=false。
|
|
933
|
+
func TestCodeBuddyNoThinkingSettings(t *testing.T) {
|
|
934
|
+
dir := t.TempDir()
|
|
935
|
+
w, log := argsCaptureCLI(t, dir, "codebuddy", `echo '{"type":"result","result":"ok"}'`)
|
|
936
|
+
if _, err := w.codebuddy().Complete(context.Background(), Request{
|
|
937
|
+
Messages: []Message{{Role: "user", Content: "hi"}},
|
|
938
|
+
NoThinking: true,
|
|
939
|
+
}); err != nil {
|
|
940
|
+
t.Fatalf("Complete: %v", err)
|
|
941
|
+
}
|
|
942
|
+
data, _ := os.ReadFile(log)
|
|
943
|
+
if !strings.Contains(string(data), `"alwaysThinkingEnabled":false`) {
|
|
944
|
+
t.Errorf("codebuddy 应写官方 settings alwaysThinkingEnabled=false: %q", data)
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
896
948
|
// TestNoSettingsWhenMaxTokensUnset MaxTokens 未设置时不注入 --settings。
|
|
897
949
|
func TestNoSettingsWhenMaxTokensUnset(t *testing.T) {
|
|
898
950
|
cases := []struct {
|
|
@@ -145,6 +145,12 @@ func (e *LLMEngine) buildArgs(req Request, prompt string) []string {
|
|
|
145
145
|
if req.Temperature != nil {
|
|
146
146
|
args = append(args, "-o", "temperature", formatTemperature(*req.Temperature))
|
|
147
147
|
}
|
|
148
|
+
if req.NoThinking {
|
|
149
|
+
// simonw/llm 的官方 model option(llm.datasette.io/en/stable/usage.html:
|
|
150
|
+
// -o/--option key value,OpenAI 系模型暴露 reasoning_effort)。支持该 option
|
|
151
|
+
// 的插件会把它发到端点;不支持时直接报错 —— 显式 --disable-thinking 的预期结果。
|
|
152
|
+
args = append(args, "-o", "reasoning_effort", "none")
|
|
153
|
+
}
|
|
148
154
|
return append(args, appendAttachmentSection(prompt, nonImage))
|
|
149
155
|
}
|
|
150
156
|
|
|
@@ -207,6 +213,11 @@ func (e *LLMEngine) ListModels(ctx context.Context) ([]string, error) {
|
|
|
207
213
|
// --json 输出同 `llm logs --json` 的结构(数组,每项含 response /
|
|
208
214
|
// input_tokens / output_tokens),解析失败时退回纯文本路径,不引入新的失败点。
|
|
209
215
|
func (e *LLMEngine) Complete(ctx context.Context, req Request) (Response, error) {
|
|
216
|
+
// --disable-thinking:优先换成同一条线上的 no-think 变体(MiniMax-M3.1-Flash
|
|
217
|
+
// 这类模型对 thinking.disabled 直接 400,不能靠请求体硬塞)。
|
|
218
|
+
if req.NoThinking {
|
|
219
|
+
req.Model = NoThinkLLMModel(req.Model)
|
|
220
|
+
}
|
|
210
221
|
// 配置文件模型(models.json):命中 id → 直连端点(见 llmhttp.go)。
|
|
211
222
|
// 为什么必须按 id:id 唯一,而真实模型名会重复(MiniMax-M3 与 minimax-nothink
|
|
212
223
|
// 真实名相同,只差 extraBody)—— 用名字做参数就无法区分。
|
|
@@ -317,6 +328,9 @@ func parseLLMJSONEntry(raw string) (llmJSONEntry, bool) {
|
|
|
317
328
|
// Stream 实现 Streamer:llm prompt 默认流式,逐行读纯文本 stdout,
|
|
318
329
|
// thinkSplitter 把标签块路由到 thinking 通道。
|
|
319
330
|
func (e *LLMEngine) Stream(ctx context.Context, req Request, onEvent func(StreamEvent)) (StreamResult, error) {
|
|
331
|
+
if req.NoThinking {
|
|
332
|
+
req.Model = NoThinkLLMModel(req.Model)
|
|
333
|
+
}
|
|
320
334
|
// 配置文件模型:命中 id → 直连 SSE(见 llmhttp.go / completeConfigured 上方注释)
|
|
321
335
|
if m, ok := ResolveLLMConfigModel(req.Model); ok {
|
|
322
336
|
return e.streamConfigured(ctx, m, req, onEvent)
|
|
@@ -104,6 +104,13 @@ func TestLLMBuildArgsModelOptions(t *testing.T) {
|
|
|
104
104
|
t.Errorf("buildArgs = %q, want %q (温度 0 必须透传)", got, want)
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
// NoThinking:llm 官方 model option;只在 CLI 委托路径出现
|
|
108
|
+
got = e.buildArgs(Request{NoThinking: true}, "hi")
|
|
109
|
+
want = []string{"prompt", "-o", "reasoning_effort", "none", "hi"}
|
|
110
|
+
if strings.Join(got, "\x00") != strings.Join(want, "\x00") {
|
|
111
|
+
t.Errorf("buildArgs = %q, want %q", got, want)
|
|
112
|
+
}
|
|
113
|
+
|
|
107
114
|
// 都不给
|
|
108
115
|
got = e.buildArgs(Request{Model: "minimax-m3"}, "hi")
|
|
109
116
|
for _, a := range got {
|
|
@@ -549,3 +556,20 @@ func TestLLMDetectMissingCli(t *testing.T) {
|
|
|
549
556
|
t.Errorf("note = %q, want the missing path", note)
|
|
550
557
|
}
|
|
551
558
|
}
|
|
559
|
+
|
|
560
|
+
func TestNoThinkLLMModelPicksAnyVariant(t *testing.T) {
|
|
561
|
+
dir := t.TempDir()
|
|
562
|
+
p := filepath.Join(dir, "models.json")
|
|
563
|
+
raw := `[
|
|
564
|
+
{"id":"MiniMax-M3.1-Flash","model":"MiniMax-M3.1-Flash","url":"http://x/v1","apiKey":"k"},
|
|
565
|
+
{"id":"minimax-nothink","model":"MiniMax-M3","url":"http://x/v1","apiKey":"k",
|
|
566
|
+
"extraBody":{"thinking":{"type":"disabled"}}}
|
|
567
|
+
]`
|
|
568
|
+
if err := os.WriteFile(p, []byte(raw), 0o600); err != nil {
|
|
569
|
+
t.Fatal(err)
|
|
570
|
+
}
|
|
571
|
+
t.Setenv("MAGIC_AGENT_MODELS", p)
|
|
572
|
+
if got := NoThinkLLMModel("MiniMax-M3.1-Flash"); got != "minimax-nothink" {
|
|
573
|
+
t.Fatalf("NoThinkLLMModel = %q, want minimax-nothink", got)
|
|
574
|
+
}
|
|
575
|
+
}
|
|
@@ -92,6 +92,12 @@ func buildConfiguredBody(m LLMConfigModel, req Request, stream bool) (map[string
|
|
|
92
92
|
} else if m.Temperature != nil {
|
|
93
93
|
body["temperature"] = *m.Temperature
|
|
94
94
|
}
|
|
95
|
+
if req.NoThinking && !LLMModelDisablesThinking(m) {
|
|
96
|
+
// 没有同线 no-think 变体时,用 llm/OpenAI 兼容端点认的官方参数。
|
|
97
|
+
// 若端点不支持该参数会返回 400 —— 这是显式 --disable-thinking 的预期失败,
|
|
98
|
+
// 不静默退回「假装关闭」。
|
|
99
|
+
body["reasoning_effort"] = "none"
|
|
100
|
+
}
|
|
95
101
|
return body, nil
|
|
96
102
|
}
|
|
97
103
|
|
|
@@ -274,6 +274,33 @@ func TestUnknownModelFallsBackToCLI(t *testing.T) {
|
|
|
274
274
|
}
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
+
func TestBuildConfiguredBodyNoThinking(t *testing.T) {
|
|
278
|
+
m := LLMConfigModel{ID: "x", Model: "x", URL: "http://x/v1"}
|
|
279
|
+
body, err := buildConfiguredBody(m, Request{
|
|
280
|
+
Messages: []Message{{Role: "user", Content: "hi"}},
|
|
281
|
+
NoThinking: true,
|
|
282
|
+
}, false)
|
|
283
|
+
if err != nil {
|
|
284
|
+
t.Fatal(err)
|
|
285
|
+
}
|
|
286
|
+
if body["reasoning_effort"] != "none" {
|
|
287
|
+
t.Errorf("没有 no-think 变体时应传 reasoning_effort=none: %#v", body["reasoning_effort"])
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
noThink := LLMConfigModel{ID: "x-nothink", Model: "x", URL: "http://x/v1",
|
|
291
|
+
ExtraBody: map[string]any{"thinking": map[string]any{"type": "disabled"}}}
|
|
292
|
+
body, err = buildConfiguredBody(noThink, Request{
|
|
293
|
+
Messages: []Message{{Role: "user", Content: "hi"}},
|
|
294
|
+
NoThinking: true,
|
|
295
|
+
}, false)
|
|
296
|
+
if err != nil {
|
|
297
|
+
t.Fatal(err)
|
|
298
|
+
}
|
|
299
|
+
if _, ok := body["reasoning_effort"]; ok {
|
|
300
|
+
t.Errorf("已有 thinking.disabled 时不应再塞 reasoning_effort: %#v", body)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
277
304
|
func TestExpandEnvKeep(t *testing.T) {
|
|
278
305
|
t.Setenv("LLM_TEST_KEY", "secret")
|
|
279
306
|
cases := map[string]string{
|
|
@@ -76,6 +76,54 @@ func (m LLMConfigModel) Endpoint() string {
|
|
|
76
76
|
return strings.TrimRight(u, "/") + "/chat/completions"
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
// LLMModelDisablesThinking 判断 models.json 条目是否声明了不思考变体
|
|
80
|
+
// (extraBody.thinking.type == "disabled")。
|
|
81
|
+
func LLMModelDisablesThinking(m LLMConfigModel) bool {
|
|
82
|
+
if strings.TrimSpace(m.ID) == "" || m.ExtraBody == nil {
|
|
83
|
+
return false
|
|
84
|
+
}
|
|
85
|
+
th, ok := m.ExtraBody["thinking"].(map[string]any)
|
|
86
|
+
if !ok {
|
|
87
|
+
return false
|
|
88
|
+
}
|
|
89
|
+
return strings.EqualFold(strings.TrimSpace(fmt.Sprint(th["type"])), "disabled")
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// NoThinkLLMModel 给 --disable-thinking 选 no-think 变体:
|
|
93
|
+
// 用户已选 no-think → 原样;否则先找 WireModel 相同、声明 thinking.disabled 的条目,
|
|
94
|
+
// 再退到任意 no-think 条目(M3.1-Flash 只能换模型才能关思考)。都没有则保留原模型
|
|
95
|
+
// (由调用方决定是否再传 reasoning_effort=none)。
|
|
96
|
+
func NoThinkLLMModel(selected string) string {
|
|
97
|
+
m, ok := ResolveLLMConfigModel(selected)
|
|
98
|
+
if !ok {
|
|
99
|
+
return strings.TrimSpace(selected)
|
|
100
|
+
}
|
|
101
|
+
if LLMModelDisablesThinking(m) {
|
|
102
|
+
return m.ID
|
|
103
|
+
}
|
|
104
|
+
list, err := LoadLLMConfigModels()
|
|
105
|
+
if err != nil {
|
|
106
|
+
return m.ID
|
|
107
|
+
}
|
|
108
|
+
// 先找同一条线上的变体(不换真实模型),再退到任意 no-think 变体 ——
|
|
109
|
+
// 显式 --disable-thinking 的目标是「不思考」,MiniMax-M3.1-Flash 这类模型
|
|
110
|
+
// 对 thinking.disabled / reasoning_effort=none 直接 400,只能换到 no-think 条目。
|
|
111
|
+
for _, alt := range list {
|
|
112
|
+
if alt.ID == m.ID || alt.WireModel() != m.WireModel() {
|
|
113
|
+
continue
|
|
114
|
+
}
|
|
115
|
+
if LLMModelDisablesThinking(alt) {
|
|
116
|
+
return alt.ID
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
for _, alt := range list {
|
|
120
|
+
if alt.ID != m.ID && LLMModelDisablesThinking(alt) {
|
|
121
|
+
return alt.ID
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return m.ID
|
|
125
|
+
}
|
|
126
|
+
|
|
79
127
|
// LLMModelsPath 返回 models.json 的路径($MAGIC_AGENT_MODELS 优先)。
|
|
80
128
|
func LLMModelsPath() string {
|
|
81
129
|
if p := strings.TrimSpace(os.Getenv("MAGIC_AGENT_MODELS")); p != "" {
|
|
@@ -383,6 +383,11 @@ func agentSettingsPayload(req Request, engine string) (string, bool) {
|
|
|
383
383
|
if env := maxTokensEnv(req.MaxTokens); env != nil {
|
|
384
384
|
settings["env"] = env
|
|
385
385
|
}
|
|
386
|
+
// CodeBuddy 官方 settings 键:始终开启思考。NoThinking 时显式关掉;
|
|
387
|
+
// 这是 codebuddy CLI 能对模型请求施加的官方开关(SDK 另有 thinking.type=disabled)。
|
|
388
|
+
if req.NoThinking {
|
|
389
|
+
settings["alwaysThinkingEnabled"] = false
|
|
390
|
+
}
|
|
386
391
|
|
|
387
392
|
// 沙箱 / autoMode / 权限规则只在真会调用工具时注入。
|
|
388
393
|
if !toolsIsOff(req) {
|
package/internal/cli/ask.go
CHANGED
|
@@ -62,21 +62,22 @@ type askOptions struct {
|
|
|
62
62
|
login string // 拉起某引擎自己的交互式登录会话(--login <engine>)
|
|
63
63
|
// repairModels 还原被反向同步写坏的桌面端 models.json(--repair-models <engine>):
|
|
64
64
|
// 2026-09-30 Windows 事故的收尾手段,见 internal/agent/custom_models.go 的事故还原一节。
|
|
65
|
-
repairModels
|
|
66
|
-
events
|
|
67
|
-
control
|
|
68
|
-
jsonOut
|
|
69
|
-
stream
|
|
70
|
-
noThinking
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
65
|
+
repairModels string
|
|
66
|
+
events bool // --stream 的事件流带契约版本与行号(--events;客户端消费用)
|
|
67
|
+
control bool // 从 stdin 读 NDJSON 控制命令(--control;打断 / 收工 / 回审批)
|
|
68
|
+
jsonOut bool
|
|
69
|
+
stream bool
|
|
70
|
+
noThinking bool // 只隐藏思考事件(--no-thinking,历史语义)
|
|
71
|
+
disableThinking bool // 请求模型真正关闭思考(--disable-thinking / --no-think)
|
|
72
|
+
maxTokens int
|
|
73
|
+
temperature float64
|
|
74
|
+
tempSet bool
|
|
75
|
+
reasoning string // 推理档位(--reasoning;默认 max)
|
|
76
|
+
jsonSchema string // 内联 JSON Schema(仅 llm 引擎走 --schema)
|
|
77
|
+
session string // 会话续接 id(空 = 新会话,默认)
|
|
78
|
+
continueF bool // 续接最近一次会话(-c/--continue;不需要 id)
|
|
79
|
+
workspace string // 工作目录(-w/--workspace;空 = 用调用方 cwd)
|
|
80
|
+
stop string // 停止指定会话/运行(--stop <session_id|run_id>)
|
|
80
81
|
// deleteSession 删除指定会话/运行(--delete-session <session_id|run_id>):
|
|
81
82
|
// 登记记录 + 事件日志 + 标题覆盖一起删。与插件的 DELETE /desk/session/{id} 同一份实现
|
|
82
83
|
// (见 internal/session/delete.go)——「一端删了、另一端还在」的根因就是删除没落在
|
|
@@ -118,6 +119,11 @@ type askOptions struct {
|
|
|
118
119
|
appendTo string // 向常驻会话追加消息(--append <session_id|run_id>)
|
|
119
120
|
idle time.Duration // 常驻会话空闲收工时长(--idle,仅 --keep-alive 有效)
|
|
120
121
|
|
|
122
|
+
// hosted 本轮提问由客户端「会话托管」自动发送(--hosted,2026-10-23)。
|
|
123
|
+
// 只写进会话历史的 user 事件做标识,不改变提示词、引擎或权限语义 ——
|
|
124
|
+
// 执行路径与用户亲手发的完全一样,客户端回放时据此画成「托管发送」。
|
|
125
|
+
hosted bool
|
|
126
|
+
|
|
121
127
|
// 会话标题(2026-10-15;见 internal/session/title.go)。
|
|
122
128
|
// 归纳口径收进 core 后,客户端不再各自实现 —— 否则 PC 与移动端标题不一致,
|
|
123
129
|
// 且从移动端发起的会话没人替它归纳。
|
|
@@ -126,6 +132,11 @@ type askOptions struct {
|
|
|
126
132
|
titleForce bool // --title-force:已有覆盖也重新归纳
|
|
127
133
|
titleEngine string // --title-engine:归纳用哪个引擎(默认 llm)
|
|
128
134
|
|
|
135
|
+
// 下一轮建议(2026-10-23;见 internal/suggest)。
|
|
136
|
+
// 一轮结束后生成 0-3 条「用户下一句」的快捷回复;一次性旁路调用,不续接会话。
|
|
137
|
+
// 值 = 请求 JSON 的文件路径("-" = stdin)。
|
|
138
|
+
suggestInput string // --suggest-followups <file|->:{"user":"…","reply":"…"}
|
|
139
|
+
|
|
129
140
|
// 四档权限模型(见 internal/agent/permission.go)。仅 claude / codebuddy 接线。
|
|
130
141
|
permission string // 档位:manual | accept-edits | auto | full
|
|
131
142
|
sandboxExclude []string // 始终在沙箱外执行的命令(sandbox.excludedCommands)
|
|
@@ -166,6 +177,7 @@ type askOptions struct {
|
|
|
166
177
|
reqAll bool // --requirement-all:全量重放
|
|
167
178
|
reqEngine string // --requirement-engine(默认 llm)
|
|
168
179
|
reqModel string // --requirement-model
|
|
180
|
+
reqModule string // --requirement-module:只整理这个模块
|
|
169
181
|
reqTimeout time.Duration // --requirement-timeout
|
|
170
182
|
reqAuto bool // --requirement-auto:自动归纳,不触发首次初始化
|
|
171
183
|
}
|
|
@@ -209,6 +221,7 @@ func bindAskFlags(cmd *cobra.Command, opts *askOptions) {
|
|
|
209
221
|
f.StringVar(&opts.sessionTitle, "session-title", "", "读一条会话**已存**的标题(JSON:{id,title}),不调 LLM。没有覆盖时回落到 prompt_head。列表页取标题走这条,别逐行调 --summarize-title")
|
|
210
222
|
f.BoolVar(&opts.titleForce, "title-force", false, "配合 --summarize-title:已有标题也重新归纳(默认命中缓存就直接返回,省一次 LLM 调用)")
|
|
211
223
|
f.StringVar(&opts.titleEngine, "title-engine", "llm", "--summarize-title 用哪个引擎归纳(默认 llm:纯文本、不带工具)")
|
|
224
|
+
f.StringVar(&opts.suggestInput, "suggest-followups", "", "生成下一轮快捷回复:传请求 JSON 的文件路径(\"-\" = stdin):{\"user\":\"…\",\"reply\":\"…\"}。旁路调用,不续接当前会话;引擎走 -e(默认 llm)")
|
|
212
225
|
f.Uint64Var(&opts.logAfter, "after", 0, "配合 --session-log:增量续读,只回 seq > N 的事件(另附 count/after/lastSeq/snapshotRequired)")
|
|
213
226
|
f.Uint64Var(&opts.logBefore, "before", 0, "配合 --session-log:向上翻更早的历史,从文件尾跳过 N 条已画事件再取一页(0 = 最新一页;下一页把返回的 nextBefore 传回来。另附 count/before/nextBefore/firstSeq/more;与 --after 互斥)")
|
|
214
227
|
f.IntVar(&opts.logLimit, "limit", 200, "配合 --before:每页最多返回多少条事件(默认 200)")
|
|
@@ -218,6 +231,7 @@ func bindAskFlags(cmd *cobra.Command, opts *askOptions) {
|
|
|
218
231
|
f.BoolVar(&opts.keepAlive, "keep-alive", true, "常驻会话:首轮结束后不退出、等 --append 追加(需 --stream)。claude/codebuddy 默认开;dsh 默认关(显式传 --keep-alive 开启);其余引擎不支持")
|
|
219
232
|
f.StringVar(&opts.appendTo, "append", "", "向常驻会话追加一条消息:传 session_id 或 run_id,内容用 -p/位置参数给")
|
|
220
233
|
f.DurationVar(&opts.idle, "idle", 5*time.Minute, "常驻会话空闲收工时长(默认 5m;0 = 本轮结束就收工,追加窗口只在任务运行期间)")
|
|
234
|
+
f.BoolVar(&opts.hosted, "hosted", false, "仅标记:本轮提问由客户端「会话托管」自动发送(写进会话历史,执行语义不变)")
|
|
221
235
|
f.IntVar(&opts.maxTokens, "max-tokens", 0, "输出 token 上限(0=不指定;claude/codebuddy 经 --settings 注入,llm 透传,trae/dsh 忽略)")
|
|
222
236
|
f.Float64Var(&opts.temperature, "temperature", -1, "采样温度(-1=不指定;仅 llm 引擎透传,其余引擎忽略)")
|
|
223
237
|
f.StringVar(&opts.reasoning, "reasoning", "max", "推理/思考档位: minimal | low | medium | high | xhigh | max | off(默认 max;claude/codebuddy 走 --effort,codex 走 -c model_reasoning_effort;其余引擎无此参数,忽略)")
|
|
@@ -236,7 +250,9 @@ func bindAskFlags(cmd *cobra.Command, opts *askOptions) {
|
|
|
236
250
|
f.BoolVar(&opts.stream, "stream", false, "流式输出:正文/思考增量实时打到 stdout(text 模式思考走 stderr)")
|
|
237
251
|
f.BoolVar(&opts.events, "events", false, "配合 --stream:事件流带契约版本与行号(每行加 v / seq,并先发一行 ready),供客户端消费;老消费者不要开(形状与 --stream 不同)")
|
|
238
252
|
f.BoolVar(&opts.control, "control", false, "配合 --stream:从 stdin 读 NDJSON 控制命令(ping / interrupt / stop / answer),让调用方能打断长任务、回审批")
|
|
239
|
-
f.BoolVar(&opts.noThinking, "no-thinking", false, "
|
|
253
|
+
f.BoolVar(&opts.noThinking, "no-thinking", false, "流式模式下不转发思考过程增量(只隐藏,不关闭模型思考)")
|
|
254
|
+
f.BoolVar(&opts.disableThinking, "disable-thinking", false, "请求引擎关闭模型思考(claude/codex/dsh/codebuddy/llm 各自用官方开关)")
|
|
255
|
+
f.BoolVar(&opts.disableThinking, "no-think", false, "同 --disable-thinking")
|
|
240
256
|
bindTeamFlags(cmd, opts)
|
|
241
257
|
bindRequirementFlags(cmd, opts)
|
|
242
258
|
bindPermissionFlags(cmd, opts)
|
|
@@ -714,7 +730,7 @@ func runStreamAsk(cmd *cobra.Command, args []string, opts *askOptions) error {
|
|
|
714
730
|
}
|
|
715
731
|
// 附件挂在 user 事件上:引擎事件里没有附件信息(`image` 只对应助手产出),
|
|
716
732
|
// 只有落这里,历史回放才复原得出「那一轮带了哪张图」。
|
|
717
|
-
sessionLog.push(sessionEvent{Kind: "user", Text: m.Content, Attachments: req.Attachments})
|
|
733
|
+
sessionLog.push(sessionEvent{Kind: "user", Text: m.Content, Attachments: req.Attachments, Hosted: opts.hosted})
|
|
718
734
|
}
|
|
719
735
|
}
|
|
720
736
|
|
|
@@ -1171,6 +1187,10 @@ func prepareAsk(cmd *cobra.Command, args []string, opts *askOptions) (agent.Engi
|
|
|
1171
1187
|
fmt.Fprintf(cmd.ErrOrStderr(),
|
|
1172
1188
|
"magic-agent: 提示:%s 引擎没有推理档位参数,--reasoning %s 已忽略\n", engine.Name(), reasoning)
|
|
1173
1189
|
}
|
|
1190
|
+
if opts.disableThinking && !agent.NoThinkingSupportOf(engine.Name()) {
|
|
1191
|
+
fmt.Fprintf(cmd.ErrOrStderr(),
|
|
1192
|
+
"magic-agent: 提示:%s 引擎没有已知的关闭思考参数,--disable-thinking 已忽略\n", engine.Name())
|
|
1193
|
+
}
|
|
1174
1194
|
toolsMode, err := parseToolsMode(opts.tools)
|
|
1175
1195
|
if err != nil {
|
|
1176
1196
|
return nil, "", agent.Request{}, err
|
|
@@ -1323,6 +1343,7 @@ func prepareAsk(cmd *cobra.Command, args []string, opts *askOptions) (agent.Engi
|
|
|
1323
1343
|
Continue: opts.continueF,
|
|
1324
1344
|
Workspace: workspace,
|
|
1325
1345
|
Reasoning: reasoning,
|
|
1346
|
+
NoThinking: opts.disableThinking,
|
|
1326
1347
|
Permission: tier,
|
|
1327
1348
|
PermissionOptions: agent.PermissionOptions{
|
|
1328
1349
|
ExcludedCommands: opts.sandboxExclude,
|
|
@@ -86,6 +86,7 @@ func runConsolidateRequirements(cmd *cobra.Command, opts *askOptions) error {
|
|
|
86
86
|
All: opts.reqAll,
|
|
87
87
|
Engine: opts.reqEngine,
|
|
88
88
|
Model: opts.reqModel,
|
|
89
|
+
Module: opts.reqModule,
|
|
89
90
|
Timeout: timeout,
|
|
90
91
|
Auto: opts.reqAuto,
|
|
91
92
|
})
|
|
@@ -114,6 +115,7 @@ func bindRequirementFlags(cmd *cobra.Command, opts *askOptions) {
|
|
|
114
115
|
f.BoolVar(&opts.reqAll, "requirement-all", false, "配合 --consolidate-requirements:全量重放历史记忆(回填 / 滚动校正)。仍走增量合并,只增不减,不会推翻已有内容")
|
|
115
116
|
f.StringVar(&opts.reqEngine, "requirement-engine", "", "--consolidate-requirements 用哪个引擎(空 = 自动:首次初始化挑能读目录的引擎,增量归纳用 llm);客户端应传自己的默认引擎")
|
|
116
117
|
f.StringVar(&opts.reqModel, "requirement-model", "", "配合 --consolidate-requirements:增量归纳用的模型(空 = 引擎默认;首次目录初始化始终用初始化引擎的默认模型)")
|
|
118
|
+
f.StringVar(&opts.reqModule, "requirement-module", "", "配合 --consolidate-requirements:只整理这个模块(空 = 整个空间)。不推进空间游标,不触发首次目录初始化")
|
|
117
119
|
f.DurationVar(&opts.reqTimeout, "requirement-timeout", requirements.DefaultTimeout, "配合 --consolidate-requirements:单次模型调用超时(默认归纳 4m / 首次目录初始化 9m)")
|
|
118
120
|
f.BoolVar(&opts.reqAuto, "requirement-auto", false, "配合 --consolidate-requirements:自动归纳(不触发首次按目录初始化;插件轮末 / 每日兜底用)")
|
|
119
121
|
}
|
package/internal/cli/root.go
CHANGED
|
@@ -368,6 +368,8 @@ codebuddy-gateway 引擎(CodeBuddy Code HTTP 网关,走 webhook + SSE;凭
|
|
|
368
368
|
一端删了处处都看不到。与插件的 DELETE /desk/session/{id} 是同一份实现
|
|
369
369
|
(internal/session/delete.go),两条路可互换。
|
|
370
370
|
magic-agent --delete-session <session_id> # 删掉这条会话(含历史)
|
|
371
|
+
团队主会话还会沿父指针级联删掉挂在它下面的全部子任务会话(JSON 里
|
|
372
|
+
child_sessions_removed 是连带删掉的子会话条数);只删子会话不会带走主会话。
|
|
371
373
|
幂等:id 不认识 / 文件本来就不在 → 退出码 0,JSON 里 records_removed=0。
|
|
372
374
|
⚠️ 是**物理删除**:删完历史就回放不了了。客户端把它当成「删除」的唯一实现,
|
|
373
375
|
只在本地藏起来(记墓碑)的话,别的客户端照样列着(2026-10-19 用户报的
|
|
@@ -477,6 +479,9 @@ codebuddy-gateway 引擎(CodeBuddy Code HTTP 网关,走 webhook + SSE;凭
|
|
|
477
479
|
return runSummarizeTitle(cmd, opts)
|
|
478
480
|
case flagChanged(cmd, "session-title"):
|
|
479
481
|
return runSessionTitle(cmd, opts)
|
|
482
|
+
case flagChanged(cmd, "suggest-followups"):
|
|
483
|
+
// 值可以是 "-"(stdin),所以用「传过没有」判,而不是「值非空」。
|
|
484
|
+
return runSuggestFollowups(cmd, opts)
|
|
480
485
|
case flagChanged(cmd, "title-force"):
|
|
481
486
|
// --title-force 只在 --summarize-title 下成立:单给它必然是调用方误解了
|
|
482
487
|
// (想要「强制重新归纳」却没给 id)。明说,不静默忽略。
|
package/internal/cli/session.go
CHANGED
|
@@ -646,24 +646,26 @@ func runDeleteSession(cmd *cobra.Command, opts *askOptions) error {
|
|
|
646
646
|
}
|
|
647
647
|
if opts.output == "text" {
|
|
648
648
|
_, _ = fmt.Fprintf(cmd.OutOrStdout(),
|
|
649
|
-
"magic-agent: 已删除会话 %s(登记记录 %d 条 / 历史 %d 份 / 标题 %d 条 / 状态 %d 条)\n",
|
|
650
|
-
res.ID, res.RecordsRemoved, res.LogsRemoved, res.TitlesRemoved, res.StatusesRemoved)
|
|
649
|
+
"magic-agent: 已删除会话 %s(登记记录 %d 条 / 历史 %d 份 / 标题 %d 条 / 状态 %d 条 / 子会话 %d 条)\n",
|
|
650
|
+
res.ID, res.RecordsRemoved, res.LogsRemoved, res.TitlesRemoved, res.StatusesRemoved, res.ChildSessionsRemoved)
|
|
651
651
|
return nil
|
|
652
652
|
}
|
|
653
653
|
out := struct {
|
|
654
|
-
Type
|
|
655
|
-
ID
|
|
656
|
-
RecordsRemoved
|
|
657
|
-
LogsRemoved
|
|
658
|
-
TitlesRemoved
|
|
659
|
-
StatusesRemoved
|
|
654
|
+
Type string `json:"type"`
|
|
655
|
+
ID string `json:"id"`
|
|
656
|
+
RecordsRemoved int `json:"records_removed"`
|
|
657
|
+
LogsRemoved int `json:"logs_removed"`
|
|
658
|
+
TitlesRemoved int `json:"titles_removed"`
|
|
659
|
+
StatusesRemoved int `json:"statuses_removed"`
|
|
660
|
+
ChildSessionsRemoved int `json:"child_sessions_removed"`
|
|
660
661
|
}{
|
|
661
|
-
Type:
|
|
662
|
-
ID:
|
|
663
|
-
RecordsRemoved:
|
|
664
|
-
LogsRemoved:
|
|
665
|
-
TitlesRemoved:
|
|
666
|
-
StatusesRemoved:
|
|
662
|
+
Type: "delete",
|
|
663
|
+
ID: res.ID,
|
|
664
|
+
RecordsRemoved: res.RecordsRemoved,
|
|
665
|
+
LogsRemoved: res.LogsRemoved,
|
|
666
|
+
TitlesRemoved: res.TitlesRemoved,
|
|
667
|
+
StatusesRemoved: res.StatusesRemoved,
|
|
668
|
+
ChildSessionsRemoved: res.ChildSessionsRemoved,
|
|
667
669
|
}
|
|
668
670
|
return writeJSONLine(cmd.OutOrStdout(), out)
|
|
669
671
|
}
|
|
@@ -122,6 +122,11 @@ type sessionEvent struct {
|
|
|
122
122
|
// (`image` 事件只对应助手**产出**),所以只有落在这里,历史回放才复原得出
|
|
123
123
|
// 「那一轮用了哪张图」—— 移动端据此在历史气泡里画出缩略图。
|
|
124
124
|
Attachments []agent.Attachment `json:"attachments,omitempty"`
|
|
125
|
+
// Hosted 这条用户提问由客户端的「会话托管」自动发出(仅 kind:"user" 有)。
|
|
126
|
+
// 2026-10-23 只增字段:用户要求「托管发送的要标识 显示时区别于“我”发送」。
|
|
127
|
+
// 它只影响历史回放的画法(机器人头像 + 「托管发送」小标),不参与执行;
|
|
128
|
+
// 老日志没有这个字段 = 用户亲手发的,回放与以前一致。
|
|
129
|
+
Hosted bool `json:"hosted,omitempty"`
|
|
125
130
|
}
|
|
126
131
|
|
|
127
132
|
// sessionWriter 每条流式调用一个实例;开着就一直 append 到
|
|
@@ -85,6 +85,9 @@ func TestStreamAskPersistsUserMessage(t *testing.T) {
|
|
|
85
85
|
if evs[0].SessionID != sid {
|
|
86
86
|
t.Fatalf("首条应带上 session_id(pending 刷盘时回填),实得 %q", evs[0].SessionID)
|
|
87
87
|
}
|
|
88
|
+
if evs[0].Hosted {
|
|
89
|
+
t.Fatal("普通提问不该带 hosted —— 只有客户端显式传 --hosted 的托管自动发送才标")
|
|
90
|
+
}
|
|
88
91
|
|
|
89
92
|
// ② 引擎事件也都在,且排在 user 之后。
|
|
90
93
|
sawText := false
|
|
@@ -122,6 +125,52 @@ func TestStreamAskPersistsUserMessage(t *testing.T) {
|
|
|
122
125
|
}
|
|
123
126
|
}
|
|
124
127
|
|
|
128
|
+
// TestStreamAskPersistsHostedUserMessage `--hosted` 只给 user 事件加一个标识
|
|
129
|
+
// (2026-10-23 用户:「是托管发送的要标识 显示时区别于“我”发送」)。
|
|
130
|
+
//
|
|
131
|
+
// 标识必须落在**会话历史**里:客户端只画实时那条不够 —— 重开会话时气泡是从
|
|
132
|
+
// kind:"user" 回放出来的,历史没这个字段就又会变回「我」。
|
|
133
|
+
func TestStreamAskPersistsHostedUserMessage(t *testing.T) {
|
|
134
|
+
dir := t.TempDir()
|
|
135
|
+
prev := SessionLogDirOverride
|
|
136
|
+
SessionLogDirOverride = dir
|
|
137
|
+
defer func() { SessionLogDirOverride = prev }()
|
|
138
|
+
|
|
139
|
+
const sid = "sess-hosted-log"
|
|
140
|
+
const prompt = "继续下一步"
|
|
141
|
+
registerFake(&userEchoStreamEngine{name: "fake-hosted-log", sessionID: sid})
|
|
142
|
+
|
|
143
|
+
if _, _, err := runAskCmd(t, "", "-e", "fake-hosted-log", "--stream", "--save-history", "--hosted", prompt); err != nil {
|
|
144
|
+
t.Fatalf("--stream --hosted: %v", err)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
evs, err := ReadSessionLog(dir, sid)
|
|
148
|
+
if err != nil {
|
|
149
|
+
t.Fatalf("读回会话日志失败:%v", err)
|
|
150
|
+
}
|
|
151
|
+
if len(evs) == 0 || evs[0].Kind != "user" {
|
|
152
|
+
t.Fatalf("首条应是 user,实得:%s", dumpKinds(evs))
|
|
153
|
+
}
|
|
154
|
+
if !evs[0].Hosted {
|
|
155
|
+
t.Fatal("--hosted 没有写进 user 事件(历史回放会退回「我」发送)")
|
|
156
|
+
}
|
|
157
|
+
// 正文不能被标识污染:发给引擎 / 回放的都是原句。
|
|
158
|
+
if evs[0].Text != prompt {
|
|
159
|
+
t.Fatalf("user 文本被改动:期望 %q,实得 %q", prompt, evs[0].Text)
|
|
160
|
+
}
|
|
161
|
+
raw := readLines(t, filepath.Join(dir, sid+".jsonl"))
|
|
162
|
+
if len(raw) == 0 {
|
|
163
|
+
t.Fatal("落盘文件是空的")
|
|
164
|
+
}
|
|
165
|
+
var first map[string]any
|
|
166
|
+
if err := json.Unmarshal([]byte(raw[0]), &first); err != nil {
|
|
167
|
+
t.Fatal(err)
|
|
168
|
+
}
|
|
169
|
+
if first["hosted"] != true {
|
|
170
|
+
t.Fatalf("落盘 JSON 应带 hosted:true(只增字段),实得 %v", first)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
125
174
|
// TestStreamAskNoArchiveByDefault **默认什么都不存**(用户 2026-10-12:
|
|
126
175
|
// 「加一个参数 默认不存档」+「会话登记也不需要」)。
|
|
127
176
|
//
|