magic-agent 2.3.0 → 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.
- package/CLIENT-CONTRACT.md +78 -13
- package/README.md +10 -4
- package/client-ui/pc/app.css +14 -0
- package/client-ui/pc/js/93-rail.js +3 -1
- package/client-ui/shared/conv/03-chat-message.js +20 -3
- package/client-ui/shared/conv/07-stream-delegation.js +15 -0
- package/client-ui/shared/conv/27-conv-ledger.js +1 -1
- package/client-ui/shared/conv/conv.css +17 -0
- package/client-ui/shared/conversation.js +209 -39
- package/client-ui/shared/transport-http.js +193 -38
- package/client-ui-mobile/index.html +733 -107
- package/client-ui-mobile/reqs.js +42 -29
- 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 +6 -0
- package/internal/agent/llmengine_test.go +7 -0
- package/internal/agent/llmhttp.go +6 -0
- package/internal/agent/llmhttp_test.go +27 -0
- package/internal/agent/llmmodels.go +13 -0
- package/internal/agent/permission.go +5 -0
- package/internal/cli/ask.go +32 -17
- package/internal/cli/requirements.go +2 -0
- package/internal/cli/sessionlog.go +5 -0
- package/internal/cli/sessionlog_user_test.go +49 -0
- package/internal/client/cors_test.go +32 -0
- package/internal/client/jobs.go +1160 -0
- package/internal/client/jobs_test.go +369 -0
- package/internal/client/requirements.go +27 -7
- package/internal/client/requirements_test.go +51 -0
- package/internal/client/service.go +259 -142
- package/internal/client/service_test.go +78 -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/session.go +15 -1
- package/internal/session/workspace_lock.go +165 -0
- package/internal/session/workspace_lock_test.go +90 -0
- package/internal/suggest/suggest.go +101 -7
- package/internal/suggest/suggest_test.go +44 -0
- 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 +3 -2
package/client-ui-mobile/reqs.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* 现在 index 只留两个空容器(#reqs / #reqDetail)+ 一次 mount,本文件自包含:
|
|
6
6
|
* · 一级:空间目录(空间 → 模块,功能点不画);
|
|
7
7
|
* · 二级:点模块进去,**只看这个模块**的需求列表;
|
|
8
|
-
* · 「整理」:POST /desk/requirements/consolidate(全量 / 单空间 /
|
|
8
|
+
* · 「整理」:POST /desk/requirements/consolidate(全量 / 单空间 / 二级页单模块)。
|
|
9
9
|
*
|
|
10
10
|
* 与宿主(index.html)的边界:只通过 mount(host) 注入的能力交互 ——
|
|
11
11
|
* getBase / getToken(切环境后是活的,必须 getter,不能传快照)、
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
var REQ_DATA = null; // 最近一次清单(重画用,避免每次点都重新拉)
|
|
27
27
|
var REQ_BUSY = false; // 「整理」进行中(防重复点击)
|
|
28
28
|
var REQ_BUSY_LABEL = '全部整理'; // 顶栏按钮的进行中文案(重渲染后要能恢复)
|
|
29
|
+
var REQ_BUSY_KEY = ''; // 本次整理的组头(空间 key;空 = 全部整理,所有组头都置忙)
|
|
29
30
|
var REQ_OPEN = {}; // 工作区展开状态(key = 工作区 id),与会话列表的 WS_OPEN 同构
|
|
30
31
|
var REQ_ALIVE = null; // 仍有会话的空间路径集合;null = 拿不到会话清单(不筛)
|
|
31
32
|
var REQ_DETAIL = null; // 二级视图:{key: 工作区 key, mod: 模块名, ws: 工作区对象}
|
|
@@ -41,7 +42,7 @@
|
|
|
41
42
|
'<div class="history__top">' +
|
|
42
43
|
'<button class="topbar__icon" id="btnReqDetailBack" aria-label="返回"><svg width="20" height="20"><use href="#i-back"/></svg></button>' +
|
|
43
44
|
'<span class="history__title" id="reqDetailTitle">需求</span>' +
|
|
44
|
-
'<button class="history__disconnect" id="btnReqDetailSync" type="button">整理</button>' +
|
|
45
|
+
'<button class="history__disconnect" id="btnReqDetailSync" type="button" title="只整理这个模块">整理</button>' +
|
|
45
46
|
'</div>' +
|
|
46
47
|
'<div class="history__list" id="reqDetailList"></div>';
|
|
47
48
|
|
|
@@ -330,17 +331,19 @@
|
|
|
330
331
|
服务端按 state 判断走哪条路:
|
|
331
332
|
· 没初始化过 → 先按空间目录反推需求基线(首次可能跑几分钟,按钮要能等);
|
|
332
333
|
· 已初始化 → 只消费上次整理之后的新会话。
|
|
333
|
-
|
|
334
|
+
入口有三个:
|
|
334
335
|
· 每个空间组头的「整理」——只整理这个空间(用户点哪个就是哪个,不猜);
|
|
335
|
-
·
|
|
336
|
-
|
|
337
|
-
|
|
336
|
+
· 二级页右上角「整理」——只整理当前模块(body 带 module,不推进空间游标);
|
|
337
|
+
· 一级页右上角「全部整理」——按空间逐个串行跑一遍,汇总结果(一键触发全部)。
|
|
338
|
+
三个入口都带「我的 → 默认引擎/模型」,服务端不再自动替换引擎。 */
|
|
339
|
+
function reqPostConsolidate(ws, module) {
|
|
338
340
|
var hdr = { 'Content-Type': 'application/json' };
|
|
339
341
|
if (H.getToken()) hdr['X-Magic-Token'] = H.getToken();
|
|
340
342
|
return fetch(H.getBase() + '/desk/requirements/consolidate', {
|
|
341
343
|
method: 'POST', headers: hdr,
|
|
342
344
|
body: JSON.stringify({
|
|
343
345
|
workspace: ws || '',
|
|
346
|
+
module: module || '',
|
|
344
347
|
engine: H.getRunPref().engine || '',
|
|
345
348
|
model: (H.getRunPref().engine && H.getRunPref().model) || ''
|
|
346
349
|
})
|
|
@@ -354,10 +357,13 @@
|
|
|
354
357
|
});
|
|
355
358
|
}
|
|
356
359
|
|
|
357
|
-
/* 统一置忙:顶部主按钮 +
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
360
|
+
/* 统一置忙:顶部主按钮 + 所有空间组头的「整理」一起禁用(防重复点)。
|
|
361
|
+
只有**真正在整理的那个入口**显示「整理中…」:
|
|
362
|
+
· 点某个空间 / 某个模块的「整理」→ 只有这一组的文案变;
|
|
363
|
+
· 点「全部整理」→ 所有组头一起变(那才是全部在跑)。
|
|
364
|
+
其余按钮只置灰、保持「整理」—— 否则点一个模块会让人以为所有空间都在整理。 */
|
|
365
|
+
function reqSetBusy(on, label, busyKey) {
|
|
366
|
+
if (busyKey === undefined) busyKey = REQ_BUSY_KEY;
|
|
361
367
|
if (on) { if (label !== undefined) REQ_BUSY_LABEL = label; }
|
|
362
368
|
else REQ_BUSY_LABEL = '全部整理';
|
|
363
369
|
var btn = els.sync;
|
|
@@ -372,38 +378,43 @@
|
|
|
372
378
|
}
|
|
373
379
|
Array.prototype.forEach.call(document.querySelectorAll('.wsrow__sync'), function (b) {
|
|
374
380
|
b.disabled = !!on;
|
|
375
|
-
b.textContent =
|
|
381
|
+
if (!on) { b.textContent = '整理'; return; }
|
|
382
|
+
b.textContent = (!busyKey || b.getAttribute('data-ws') === busyKey) ? '整理中…' : '整理';
|
|
376
383
|
});
|
|
377
384
|
}
|
|
378
385
|
|
|
379
|
-
|
|
386
|
+
/* 整理一个空间(module 为空)或一个模块(module 非空)。
|
|
387
|
+
· 空间级:消费该空间游标之后的新会话,可能同时更新多个模块;
|
|
388
|
+
· 模块级:只更新这个模块,**不推进空间游标** —— 别的模块的需求仍留在
|
|
389
|
+
pending,等空间级「整理」/下一轮自动归纳处理,不会被这一下吞掉。
|
|
390
|
+
服务端按 state 判断走哪条路:没初始化过 → 先按空间目录反推基线;
|
|
391
|
+
模块级不会触发首次初始化(目录基线天然是空间级的)。 */
|
|
392
|
+
function consolidateRequirements(target, module) {
|
|
380
393
|
if (REQ_BUSY || !target) return;
|
|
381
394
|
var ws = target.dir || target.id || '';
|
|
382
395
|
if (!ws) return;
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
reqSetBusy(true, firstTime ? '初始化中…' : '整理中…');
|
|
396
|
+
module = String(module || '').trim();
|
|
397
|
+
var firstTime = target.initialized === false && !module;
|
|
386
398
|
var targetKey = target.id || target.dir || '';
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
/* 点的是哪个空间,就只让哪个组头显示「初始化中…」;其它组头保持「整理中…」。 */
|
|
392
|
-
if (groupBtn) groupBtn.textContent = firstTime ? '初始化中…' : '整理中…';
|
|
393
|
-
reqPostConsolidate(ws)
|
|
399
|
+
REQ_BUSY = true;
|
|
400
|
+
REQ_BUSY_KEY = targetKey;
|
|
401
|
+
reqSetBusy(true, firstTime ? '初始化中…' : '整理中…', targetKey);
|
|
402
|
+
reqPostConsolidate(ws, module)
|
|
394
403
|
.then(function (d) {
|
|
404
|
+
var prefix = module ? '模块「' + module + '」' : '';
|
|
395
405
|
if (d.bootstrap) {
|
|
396
406
|
if (d.created || d.updated) toast('已按空间目录生成需求:新建 ' + (d.created || 0) + ' · 更新 ' + (d.updated || 0));
|
|
397
407
|
else toast('已按空间目录初始化:暂未识别到功能点');
|
|
398
408
|
}
|
|
399
|
-
else if (d.noop) toast('没有新的对话需要整理');
|
|
400
|
-
else if (d.updated || d.created) toast('
|
|
401
|
-
else toast('整理完成');
|
|
409
|
+
else if (d.noop) toast(prefix ? prefix + '没有新的对话需要整理' : '没有新的对话需要整理');
|
|
410
|
+
else if (d.updated || d.created) toast((prefix || '整理完成') + ':更新 ' + (d.updated || 0) + ' · 新建 ' + (d.created || 0));
|
|
411
|
+
else toast(prefix ? prefix + '整理完成' : '整理完成');
|
|
402
412
|
loadRequirements(true); // 后台刷新(不闪「读取中」)
|
|
403
413
|
})
|
|
404
414
|
.catch(function (e) { toast('整理失败:' + ((e && e.message) || e)); })
|
|
405
415
|
.then(function () {
|
|
406
416
|
REQ_BUSY = false;
|
|
417
|
+
REQ_BUSY_KEY = '';
|
|
407
418
|
reqSetBusy(false, '全部整理');
|
|
408
419
|
});
|
|
409
420
|
}
|
|
@@ -438,10 +449,12 @@
|
|
|
438
449
|
return;
|
|
439
450
|
}
|
|
440
451
|
REQ_BUSY = true;
|
|
452
|
+
REQ_BUSY_KEY = ''; // 空 = 全部整理:所有组头一起显示「整理中…」
|
|
441
453
|
reqSetBusy(true, '整理中 0/' + list.length);
|
|
442
454
|
var i = 0, ok = 0, fail = 0, created = 0, updated = 0, firstErr = '';
|
|
443
455
|
function done() {
|
|
444
456
|
REQ_BUSY = false;
|
|
457
|
+
REQ_BUSY_KEY = '';
|
|
445
458
|
reqSetBusy(false, '全部整理');
|
|
446
459
|
loadRequirements(true);
|
|
447
460
|
if (fail) {
|
|
@@ -456,7 +469,7 @@
|
|
|
456
469
|
REQ_BUSY_LABEL = '整理中 ' + i + '/' + list.length;
|
|
457
470
|
var btn = els.sync;
|
|
458
471
|
if (btn) btn.textContent = REQ_BUSY_LABEL;
|
|
459
|
-
reqPostConsolidate(w.dir || w.id || '')
|
|
472
|
+
reqPostConsolidate(w.dir || w.id || '', '')
|
|
460
473
|
.then(function (d) {
|
|
461
474
|
ok++;
|
|
462
475
|
created += Number(d.created) || 0;
|
|
@@ -487,13 +500,13 @@
|
|
|
487
500
|
els.sync = root.querySelector('#btnReqSync');
|
|
488
501
|
els.detailBack = detail.querySelector('#btnReqDetailBack');
|
|
489
502
|
els.detailSync = detail.querySelector('#btnReqDetailSync');
|
|
490
|
-
/* 两个动作:全部整理 /
|
|
491
|
-
|
|
503
|
+
/* 两个动作:全部整理 / 返回一级;二级页的「整理」只作用于**当前模块**
|
|
504
|
+
(不是整空间、更不是全部空间 —— 用户点哪个模块,就只整理哪个模块)。 */
|
|
492
505
|
if (els.sync) els.sync.onclick = function () { consolidateAllRequirements(); };
|
|
493
506
|
if (els.detailBack) els.detailBack.onclick = function () { closeReqDetail(); };
|
|
494
507
|
if (els.detailSync) els.detailSync.onclick = function () {
|
|
495
508
|
if (!REQ_DETAIL || !REQ_DETAIL.ws) return;
|
|
496
|
-
consolidateRequirements(REQ_DETAIL.ws);
|
|
509
|
+
consolidateRequirements(REQ_DETAIL.ws, REQ_DETAIL.mod);
|
|
497
510
|
};
|
|
498
511
|
}
|
|
499
512
|
|
|
@@ -149,6 +149,13 @@ const (
|
|
|
149
149
|
FeatureSessionEvents = "feature.session.events"
|
|
150
150
|
// FeatureSessionControl `--stream --control`:stdin NDJSON 控制通道(打断 / 收工 / 回审批)。
|
|
151
151
|
FeatureSessionControl = "feature.session.control"
|
|
152
|
+
// FeatureSessionHosted `--hosted`:这一轮的用户提问由客户端「会话托管」自动发出。
|
|
153
|
+
//
|
|
154
|
+
// 为什么归 core:托管本身是**纯客户端策略**(执行仍走普通 ask),但「这句是托管的」
|
|
155
|
+
// 要写进会话历史,回放时才能把气泡画成「托管发送」而不是「我」—— 历史在 core 手里,
|
|
156
|
+
// 所以标识只能由 core 落。客户端据此决定要不要把 hosted 透传给 core;老 core 没有
|
|
157
|
+
// 这个位时插件忽略该字段(实时气泡仍由客户端自己标),不阻断发送。
|
|
158
|
+
FeatureSessionHosted = "feature.session.hosted"
|
|
152
159
|
// FeatureSessionTeam `--team`:团队模式(主引擎拆任务、子引擎分头执行、主引擎汇总)。
|
|
153
160
|
//
|
|
154
161
|
// 为什么归 core 而不是某个引擎:它是**会话层的一种运行模式**,任何能当主引擎的
|
|
@@ -164,5 +171,5 @@ const (
|
|
|
164
171
|
|
|
165
172
|
// CoreFeatures 返回 core 自身提供的通道能力(字典序,稳定)。
|
|
166
173
|
func CoreFeatures() []string {
|
|
167
|
-
return []string{FeatureRequirements, FeatureSessionControl, FeatureSessionEvents, FeatureSessionTeam}
|
|
174
|
+
return []string{FeatureRequirements, FeatureSessionControl, FeatureSessionEvents, FeatureSessionHosted, FeatureSessionTeam}
|
|
168
175
|
}
|
package/internal/agent/claude.go
CHANGED
|
@@ -238,9 +238,11 @@ func (e *ClaudeEngine) buildArgsBase(req Request) []string {
|
|
|
238
238
|
if m := stripModelPrefix(req.Model); m != "" {
|
|
239
239
|
args = append(args, "--model", m)
|
|
240
240
|
}
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
if
|
|
241
|
+
// 推理 / 思考:建议这类短任务显式关闭(--thinking disabled);
|
|
242
|
+
// 其余调用仍按 --effort 档位。两者互斥,关闭时不传 effort,避免又落回默认高档。
|
|
243
|
+
if req.NoThinking {
|
|
244
|
+
args = append(args, "--thinking", "disabled")
|
|
245
|
+
} else if lv := ReasoningLevel(req.Reasoning); lv != "" {
|
|
244
246
|
if lv == "minimal" {
|
|
245
247
|
lv = "low"
|
|
246
248
|
}
|
|
@@ -375,8 +375,12 @@ func (c codebuddyCore) buildArgsBase(req Request) []string {
|
|
|
375
375
|
args = append(args, "--model", c.model)
|
|
376
376
|
}
|
|
377
377
|
// 推理档位:codebuddy 的 --effort 认 minimal/low/medium/high/xhigh/max。
|
|
378
|
-
|
|
379
|
-
|
|
378
|
+
// NoThinking 走官方 settings alwaysThinkingEnabled=false(见 agentSettingsPayload),
|
|
379
|
+
// 不传 --effort off(该值不在 --effort 合法枚举里)。
|
|
380
|
+
if !req.NoThinking {
|
|
381
|
+
if lv := ReasoningLevel(req.Reasoning); lv != "" {
|
|
382
|
+
args = append(args, "--effort", lv)
|
|
383
|
+
}
|
|
380
384
|
}
|
|
381
385
|
// 统一参数矩阵:`--settings` 只接受一份载荷,故 MaxTokens 的 env 注入与
|
|
382
386
|
// 四档模型的 sandbox / autoMode / permissions 合并进同一个 JSON。
|
package/internal/agent/codex.go
CHANGED
|
@@ -540,8 +540,13 @@ func (e *CodexEngine) buildArgs(req Request, prompt, outFile, schemaFile string)
|
|
|
540
540
|
}
|
|
541
541
|
}
|
|
542
542
|
// 推理档位:`-c` 在 `exec` 与 `exec resume` 下都可用(同 model 覆写),
|
|
543
|
-
// 故不按子命令分流。codex 的取值:minimal/low/medium/high/xhigh/max。
|
|
544
|
-
|
|
543
|
+
// 故不按子命令分流。codex 的取值:none/minimal/low/medium/high/xhigh/max。
|
|
544
|
+
// 建议这类短任务用 none 明确关闭思考(不支持 none 的模型会在服务端报错,
|
|
545
|
+
// 由建议层降级到 llm,不会拖住用户会话)。
|
|
546
|
+
if req.NoThinking {
|
|
547
|
+
args = append(args, "-c", "model_reasoning_effort="+tomlBasicString("none"),
|
|
548
|
+
"-c", "model_reasoning_summary="+tomlBasicString("none"))
|
|
549
|
+
} else if lv := ReasoningLevel(req.Reasoning); lv != "" {
|
|
545
550
|
args = append(args, "-c", "model_reasoning_effort="+tomlBasicString(lv))
|
|
546
551
|
}
|
|
547
552
|
// SystemPrompt → -c developer_instructions=<TOML 字符串>
|
|
@@ -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 直连请求体传 reasoning_effort="none";CLI 委托路径传
|
|
163
|
+
// 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
|
|
|
@@ -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 {
|
|
@@ -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,19 @@ 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
|
+
|
|
79
92
|
// LLMModelsPath 返回 models.json 的路径($MAGIC_AGENT_MODELS 优先)。
|
|
80
93
|
func LLMModelsPath() string {
|
|
81
94
|
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
|
// 且从移动端发起的会话没人替它归纳。
|
|
@@ -171,6 +177,7 @@ type askOptions struct {
|
|
|
171
177
|
reqAll bool // --requirement-all:全量重放
|
|
172
178
|
reqEngine string // --requirement-engine(默认 llm)
|
|
173
179
|
reqModel string // --requirement-model
|
|
180
|
+
reqModule string // --requirement-module:只整理这个模块
|
|
174
181
|
reqTimeout time.Duration // --requirement-timeout
|
|
175
182
|
reqAuto bool // --requirement-auto:自动归纳,不触发首次初始化
|
|
176
183
|
}
|
|
@@ -224,6 +231,7 @@ func bindAskFlags(cmd *cobra.Command, opts *askOptions) {
|
|
|
224
231
|
f.BoolVar(&opts.keepAlive, "keep-alive", true, "常驻会话:首轮结束后不退出、等 --append 追加(需 --stream)。claude/codebuddy 默认开;dsh 默认关(显式传 --keep-alive 开启);其余引擎不支持")
|
|
225
232
|
f.StringVar(&opts.appendTo, "append", "", "向常驻会话追加一条消息:传 session_id 或 run_id,内容用 -p/位置参数给")
|
|
226
233
|
f.DurationVar(&opts.idle, "idle", 5*time.Minute, "常驻会话空闲收工时长(默认 5m;0 = 本轮结束就收工,追加窗口只在任务运行期间)")
|
|
234
|
+
f.BoolVar(&opts.hosted, "hosted", false, "仅标记:本轮提问由客户端「会话托管」自动发送(写进会话历史,执行语义不变)")
|
|
227
235
|
f.IntVar(&opts.maxTokens, "max-tokens", 0, "输出 token 上限(0=不指定;claude/codebuddy 经 --settings 注入,llm 透传,trae/dsh 忽略)")
|
|
228
236
|
f.Float64Var(&opts.temperature, "temperature", -1, "采样温度(-1=不指定;仅 llm 引擎透传,其余引擎忽略)")
|
|
229
237
|
f.StringVar(&opts.reasoning, "reasoning", "max", "推理/思考档位: minimal | low | medium | high | xhigh | max | off(默认 max;claude/codebuddy 走 --effort,codex 走 -c model_reasoning_effort;其余引擎无此参数,忽略)")
|
|
@@ -242,7 +250,9 @@ func bindAskFlags(cmd *cobra.Command, opts *askOptions) {
|
|
|
242
250
|
f.BoolVar(&opts.stream, "stream", false, "流式输出:正文/思考增量实时打到 stdout(text 模式思考走 stderr)")
|
|
243
251
|
f.BoolVar(&opts.events, "events", false, "配合 --stream:事件流带契约版本与行号(每行加 v / seq,并先发一行 ready),供客户端消费;老消费者不要开(形状与 --stream 不同)")
|
|
244
252
|
f.BoolVar(&opts.control, "control", false, "配合 --stream:从 stdin 读 NDJSON 控制命令(ping / interrupt / stop / answer),让调用方能打断长任务、回审批")
|
|
245
|
-
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")
|
|
246
256
|
bindTeamFlags(cmd, opts)
|
|
247
257
|
bindRequirementFlags(cmd, opts)
|
|
248
258
|
bindPermissionFlags(cmd, opts)
|
|
@@ -720,7 +730,7 @@ func runStreamAsk(cmd *cobra.Command, args []string, opts *askOptions) error {
|
|
|
720
730
|
}
|
|
721
731
|
// 附件挂在 user 事件上:引擎事件里没有附件信息(`image` 只对应助手产出),
|
|
722
732
|
// 只有落这里,历史回放才复原得出「那一轮带了哪张图」。
|
|
723
|
-
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})
|
|
724
734
|
}
|
|
725
735
|
}
|
|
726
736
|
|
|
@@ -1177,6 +1187,10 @@ func prepareAsk(cmd *cobra.Command, args []string, opts *askOptions) (agent.Engi
|
|
|
1177
1187
|
fmt.Fprintf(cmd.ErrOrStderr(),
|
|
1178
1188
|
"magic-agent: 提示:%s 引擎没有推理档位参数,--reasoning %s 已忽略\n", engine.Name(), reasoning)
|
|
1179
1189
|
}
|
|
1190
|
+
if opts.disableThinking && !agent.NoThinkingSupportOf(engine.Name()) {
|
|
1191
|
+
fmt.Fprintf(cmd.ErrOrStderr(),
|
|
1192
|
+
"magic-agent: 提示:%s 引擎没有已知的关闭思考参数,--disable-thinking 已忽略\n", engine.Name())
|
|
1193
|
+
}
|
|
1180
1194
|
toolsMode, err := parseToolsMode(opts.tools)
|
|
1181
1195
|
if err != nil {
|
|
1182
1196
|
return nil, "", agent.Request{}, err
|
|
@@ -1329,6 +1343,7 @@ func prepareAsk(cmd *cobra.Command, args []string, opts *askOptions) (agent.Engi
|
|
|
1329
1343
|
Continue: opts.continueF,
|
|
1330
1344
|
Workspace: workspace,
|
|
1331
1345
|
Reasoning: reasoning,
|
|
1346
|
+
NoThinking: opts.disableThinking,
|
|
1332
1347
|
Permission: tier,
|
|
1333
1348
|
PermissionOptions: agent.PermissionOptions{
|
|
1334
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
|
}
|
|
@@ -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 到
|