magic-agent 2.3.2 → 2.3.9

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.
Files changed (33) hide show
  1. package/CLIENT-CONTRACT.md +30 -4
  2. package/README.md +1 -1
  3. package/client-ui/shared/conv/07-stream-delegation.js +15 -0
  4. package/client-ui/shared/conversation.js +39 -0
  5. package/client-ui/shared/transport-http.js +10 -0
  6. package/client-ui-mobile/index.html +368 -44
  7. package/internal/agent/engine.go +2 -2
  8. package/internal/agent/llmengine.go +0 -8
  9. package/internal/agent/llmengine_test.go +0 -17
  10. package/internal/agent/llmmodels.go +0 -35
  11. package/internal/client/cors_test.go +32 -0
  12. package/internal/client/jobs.go +1160 -0
  13. package/internal/client/jobs_test.go +369 -0
  14. package/internal/client/service.go +235 -140
  15. package/internal/client/service_test.go +43 -0
  16. package/internal/session/session.go +15 -1
  17. package/internal/session/workspace_lock.go +165 -0
  18. package/internal/session/workspace_lock_test.go +90 -0
  19. package/internal/suggest/suggest.go +8 -25
  20. package/internal/suggest/suggest_test.go +0 -6
  21. package/npm/apk/magic-agent-mobile-release.apk +0 -0
  22. package/npm/apk/version.json +3 -3
  23. package/npm/dist/darwin-arm64/magic-agent +0 -0
  24. package/npm/dist/darwin-arm64/magic-client +0 -0
  25. package/npm/dist/darwin-x64/magic-agent +0 -0
  26. package/npm/dist/darwin-x64/magic-client +0 -0
  27. package/npm/dist/linux-arm64/magic-agent +0 -0
  28. package/npm/dist/linux-arm64/magic-client +0 -0
  29. package/npm/dist/linux-x64/magic-agent +0 -0
  30. package/npm/dist/linux-x64/magic-client +0 -0
  31. package/npm/dist/win32-x64/magic-agent.exe +0 -0
  32. package/npm/dist/win32-x64/magic-client.exe +0 -0
  33. package/package.json +1 -1
@@ -159,8 +159,8 @@ type Request struct {
159
159
  // codex -c model_reasoning_effort="none" + model_reasoning_summary="none"
160
160
  // codebuddy settings alwaysThinkingEnabled=false(官方 settings / SDK thinking:disabled)
161
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)
162
+ // llm 直连请求体传 reasoning_effort="none";CLI 委托路径传
163
+ // llm 官方 -o reasoning_effort none。不自动换模型。
164
164
  // 其余引擎没有已知开关;调用方(建议生成)仍会标记,失败后再降级 llm。
165
165
  NoThinking bool
166
166
 
@@ -213,11 +213,6 @@ func (e *LLMEngine) ListModels(ctx context.Context) ([]string, error) {
213
213
  // --json 输出同 `llm logs --json` 的结构(数组,每项含 response /
214
214
  // input_tokens / output_tokens),解析失败时退回纯文本路径,不引入新的失败点。
215
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
- }
221
216
  // 配置文件模型(models.json):命中 id → 直连端点(见 llmhttp.go)。
222
217
  // 为什么必须按 id:id 唯一,而真实模型名会重复(MiniMax-M3 与 minimax-nothink
223
218
  // 真实名相同,只差 extraBody)—— 用名字做参数就无法区分。
@@ -328,9 +323,6 @@ func parseLLMJSONEntry(raw string) (llmJSONEntry, bool) {
328
323
  // Stream 实现 Streamer:llm prompt 默认流式,逐行读纯文本 stdout,
329
324
  // thinkSplitter 把标签块路由到 thinking 通道。
330
325
  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
- }
334
326
  // 配置文件模型:命中 id → 直连 SSE(见 llmhttp.go / completeConfigured 上方注释)
335
327
  if m, ok := ResolveLLMConfigModel(req.Model); ok {
336
328
  return e.streamConfigured(ctx, m, req, onEvent)
@@ -556,20 +556,3 @@ func TestLLMDetectMissingCli(t *testing.T) {
556
556
  t.Errorf("note = %q, want the missing path", note)
557
557
  }
558
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
- }
@@ -89,41 +89,6 @@ func LLMModelDisablesThinking(m LLMConfigModel) bool {
89
89
  return strings.EqualFold(strings.TrimSpace(fmt.Sprint(th["type"])), "disabled")
90
90
  }
91
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
-
127
92
  // LLMModelsPath 返回 models.json 的路径($MAGIC_AGENT_MODELS 优先)。
128
93
  func LLMModelsPath() string {
129
94
  if p := strings.TrimSpace(os.Getenv("MAGIC_AGENT_MODELS")); p != "" {
@@ -97,6 +97,38 @@ func TestCORSForLocalOrigins(t *testing.T) {
97
97
  })
98
98
  }
99
99
 
100
+ /* 写方法(DELETE / PUT)必须出现在预检的 Allow-Methods 里(2026-10-23)。
101
+ *
102
+ * 为什么单独钉:容器里的页面源是 https://localhost,接口在局域网 IP,天生跨源;
103
+ * 删除会话(DELETE)、重命名 / 置顶同步(PUT)都带自定义头 X-Magic-Token ⇒ 必发预检。
104
+ * 预检里没点名的方法,浏览器会判定「未授权」而**不发出真请求** —— 用户侧就是
105
+ * 「确认删除了,但什么都没发生」。上面那条测试只协商 GET,所以一直没照到这里。
106
+ */
107
+ func TestCORSPreflightAllowsWriteMethods(t *testing.T) {
108
+ svc := newTestService(t, fakeCore(t, goodContract))
109
+ srv := httptest.NewServer(svc.Handler())
110
+ defer srv.Close()
111
+
112
+ for _, method := range []string{"DELETE", "PUT", "POST"} {
113
+ req, err := http.NewRequest(http.MethodOptions, srv.URL+"/desk/session/x", nil)
114
+ if err != nil {
115
+ t.Fatal(err)
116
+ }
117
+ req.Header.Set("Origin", "https://localhost")
118
+ req.Header.Set("Access-Control-Request-Method", method)
119
+ req.Header.Set("Access-Control-Request-Headers", "x-magic-token")
120
+ res, err := http.DefaultClient.Do(req)
121
+ if err != nil {
122
+ t.Fatal(err)
123
+ }
124
+ res.Body.Close()
125
+ allowed := res.Header.Get("Access-Control-Allow-Methods")
126
+ if !containsFold(allowed, method) {
127
+ t.Fatalf("预检必须允许 %s,得到 Allow-Methods=%q(浏览器会拦掉真请求)", method, allowed)
128
+ }
129
+ }
130
+ }
131
+
100
132
  func containsFold(s, sub string) bool {
101
133
  return len(s) >= len(sub) && (indexFold(s, sub) >= 0)
102
134
  }