magic-agent 2.1.5 → 2.3.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 +103 -6
- package/client-ui/pc/app.css +3 -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 +21 -1
- package/client-ui/shared/conv/03-chat-message.js +43 -0
- 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/29-msg-variants.js +56 -3
- package/client-ui/shared/conv/README.md +6 -0
- package/client-ui/shared/conv/conv.css +16 -1
- package/client-ui/shared/conversation.css +42 -4
- package/client-ui/shared/conversation.js +655 -87
- package/client-ui/shared/transport-http.js +16 -0
- package/client-ui-mobile/index.html +401 -553
- package/client-ui-mobile/reqs.css +39 -0
- package/client-ui-mobile/reqs.js +507 -0
- package/cmd/magic-client/main.go +1 -1
- package/internal/cli/ask.go +6 -0
- package/internal/cli/root.go +5 -0
- package/internal/cli/session.go +16 -14
- 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/service.go +11 -1
- package/internal/client/suggest.go +131 -0
- package/internal/client/suggest_test.go +116 -0
- 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 +421 -0
- package/internal/suggest/suggest_test.go +218 -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 +5 -2
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/* ══════════ 需求页(「需求」tab 子页面)══════════
|
|
2
|
+
从 index.html 拆出:选择器只服务这一页(别的页面不引用)。
|
|
3
|
+
一级目录(空间 → 模块)、二级模块需求列表、组头「整理」按钮。
|
|
4
|
+
CSS 变量(--ink / --muted / --subtle / --border / --faint / --primary-action /
|
|
5
|
+
--warning / --tint-wait)仍由 index.html 的 :root 统一提供。 */
|
|
6
|
+
|
|
7
|
+
/* 空间规模(几模块 · 几条):紧跟在空间名后面,中性色 —— 不是状态,别用徽标色。
|
|
8
|
+
需求页专属:空间名改成自适应宽度,meta 才会贴在名字后面;meta 的 margin-right:auto
|
|
9
|
+
把「待整理」徽标和箭头推到行尾。 */
|
|
10
|
+
.wsrow__open--req .wsrow__l{flex:0 1 auto}
|
|
11
|
+
.wsrow__open--req .wsrow__meta{margin-right:auto}
|
|
12
|
+
.wsrow__meta{flex:0 1 auto;min-width:0;font-size:12px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
13
|
+
/* 每个空间自己的「整理」:需求页会同时列出多个空间,顶部那一枚只认当前会话的空间。
|
|
14
|
+
把入口放到组头,用户点哪个空间就初始化/增量哪个空间,不再猜「当前空间」是谁。 */
|
|
15
|
+
.wsrow__sync{flex:0 0 auto;height:32px;padding:0 10px;border-radius:var(--r-full);
|
|
16
|
+
font-size:12px;font-weight:500;color:var(--primary-action);background:var(--subtle);white-space:nowrap}
|
|
17
|
+
.wsrow__sync:active{opacity:.7}
|
|
18
|
+
.wsrow__sync:disabled{color:var(--muted);opacity:.6}
|
|
19
|
+
/* 「未初始化」与「待整理」同形不同色:前者是「还没按目录生成过基线」,
|
|
20
|
+
后者是「有对话等着归纳」,两种提示不能混成一个数字。 */
|
|
21
|
+
.wsrow__badge--init{color:var(--warning);background:var(--tint-wait)}
|
|
22
|
+
|
|
23
|
+
/* ══════════ 需求页:两级(空间→模块 → 需求列表)══════════
|
|
24
|
+
一级是**目录**:空间是行,模块缩进挂在空间下(树形),功能点一律不画在这里 ——
|
|
25
|
+
需求一多,一级就会被功能点撑成长列表,什么结构都看不出来。
|
|
26
|
+
二级才是内容:点某个模块进去,**只看这个模块**的需求列表。 */
|
|
27
|
+
.reqtree__mod{position:relative;display:flex;align-items:center;gap:8px;width:100%;
|
|
28
|
+
padding:9px 10px 9px 30px;background:none;border:0;text-align:left;color:var(--ink)}
|
|
29
|
+
.reqtree__mod:active{background:var(--subtle)}
|
|
30
|
+
/* 连接线:从空间名垂下来的细线 + 每个模块前的一小截横线 —— 归属关系靠线说话,不靠缩进猜 */
|
|
31
|
+
.reqtree__mod::before{content:"";position:absolute;left:14px;top:0;bottom:0;width:1px;background:var(--border)}
|
|
32
|
+
.reqtree__mod::after{content:"";position:absolute;left:14px;top:50%;width:10px;height:1px;background:var(--border)}
|
|
33
|
+
.reqtree__modname{flex:1;min-width:0;font-size:14px;font-weight:500;color:var(--ink);
|
|
34
|
+
overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
35
|
+
.reqtree__modcount{flex:0 0 auto;font-size:12px;color:var(--muted);font-variant-numeric:tabular-nums}
|
|
36
|
+
.reqtree__modgo{flex:0 0 auto;color:var(--faint);display:flex;align-items:center}
|
|
37
|
+
/* 二级视图的分组小节:只画当前模块,故这一节恒为高亮态 */
|
|
38
|
+
.reqdetail__sec--cur .sechead{color:var(--primary-action)}
|
|
39
|
+
.reqdetail__sec--cur .sechead__t{font-weight:800}
|
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
/* 需求页(移动端「需求」tab)—— 独立子页面模块。
|
|
2
|
+
*
|
|
3
|
+
* 为什么单独一个文件:这一页的 DOM、样式、渲染与「整理」动作原本全混在
|
|
4
|
+
* index.html 的主 IIFE 里(约 500 行),改需求页要在 5600 行的文件里翻半天。
|
|
5
|
+
* 现在 index 只留两个空容器(#reqs / #reqDetail)+ 一次 mount,本文件自包含:
|
|
6
|
+
* · 一级:空间目录(空间 → 模块,功能点不画);
|
|
7
|
+
* · 二级:点模块进去,**只看这个模块**的需求列表;
|
|
8
|
+
* · 「整理」:POST /desk/requirements/consolidate(全量 / 单空间 / 二级页单空间)。
|
|
9
|
+
*
|
|
10
|
+
* 与宿主(index.html)的边界:只通过 mount(host) 注入的能力交互 ——
|
|
11
|
+
* getBase / getToken(切环境后是活的,必须 getter,不能传快照)、
|
|
12
|
+
* getRootDirs / getCustomSpaces / getRunPref、isRealSpaceDir / topLevelSessions、
|
|
13
|
+
* hEl / elSpan / svgUse / toast / openReader / hideAll / setActive /
|
|
14
|
+
* showTabbar / hideTabbar。
|
|
15
|
+
* 数据形状见 CLIENT-CONTRACT §3.4.1:
|
|
16
|
+
* {ok, root, workspaces[{id,dir,name,pending,memory,
|
|
17
|
+
* modules:[{name,features:[{module,name,file,desc,points,conclusions,conclusion,
|
|
18
|
+
* sources,has_final,final_preview,updated_at}]}],feature_count,
|
|
19
|
+
* last_consolidated_at,last_error}]}
|
|
20
|
+
*/
|
|
21
|
+
(function () {
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
var H = null; // 宿主(index.html 注入)
|
|
25
|
+
var els = {}; // 需求页内部元素
|
|
26
|
+
var REQ_DATA = null; // 最近一次清单(重画用,避免每次点都重新拉)
|
|
27
|
+
var REQ_BUSY = false; // 「整理」进行中(防重复点击)
|
|
28
|
+
var REQ_BUSY_LABEL = '全部整理'; // 顶栏按钮的进行中文案(重渲染后要能恢复)
|
|
29
|
+
var REQ_OPEN = {}; // 工作区展开状态(key = 工作区 id),与会话列表的 WS_OPEN 同构
|
|
30
|
+
var REQ_ALIVE = null; // 仍有会话的空间路径集合;null = 拿不到会话清单(不筛)
|
|
31
|
+
var REQ_DETAIL = null; // 二级视图:{key: 工作区 key, mod: 模块名, ws: 工作区对象}
|
|
32
|
+
|
|
33
|
+
/* 一级 / 二级两个容器的内容都在这里渲染 —— index.html 里只留空壳。 */
|
|
34
|
+
var ROOT_HTML =
|
|
35
|
+
'<div class="history__top">' +
|
|
36
|
+
'<span class="history__title">需求</span>' +
|
|
37
|
+
'<button class="history__disconnect" id="btnReqSync" type="button">全部整理</button>' +
|
|
38
|
+
'</div>' +
|
|
39
|
+
'<div class="history__list" id="reqList"></div>';
|
|
40
|
+
var DETAIL_HTML =
|
|
41
|
+
'<div class="history__top">' +
|
|
42
|
+
'<button class="topbar__icon" id="btnReqDetailBack" aria-label="返回"><svg width="20" height="20"><use href="#i-back"/></svg></button>' +
|
|
43
|
+
'<span class="history__title" id="reqDetailTitle">需求</span>' +
|
|
44
|
+
'<button class="history__disconnect" id="btnReqDetailSync" type="button">整理</button>' +
|
|
45
|
+
'</div>' +
|
|
46
|
+
'<div class="history__list" id="reqDetailList"></div>';
|
|
47
|
+
|
|
48
|
+
/* 路径归一:只去尾部斜杠,不做 realpath —— 与 core 登记表里的 workspace 同一口径。 */
|
|
49
|
+
function reqNormDir(p) { return String(p || '').replace(/\/+$/, ''); }
|
|
50
|
+
/* 宿主能力的短名(函数体里沿用原 index 的写法,搬迁时不用到处加 H.) */
|
|
51
|
+
function hEl(tag, cls, text) { return H.hEl(tag, cls, text); }
|
|
52
|
+
function elSpan(cls, text) { return H.elSpan(cls, text); }
|
|
53
|
+
function svgUse(id, size) { return H.svgUse(id, size); }
|
|
54
|
+
function toast(msg) { return H.toast(msg); }
|
|
55
|
+
|
|
56
|
+
function reqFeatureRow(f) {
|
|
57
|
+
/* 一行 = 一个功能点,**照会话行的排布**做(图标 + 标题 / 副标题 / 右侧时间):
|
|
58
|
+
标题(功能点名称)→ 归纳结论 / 需求描述 → 「N 需求点 · M 结论 · 来源 K」。
|
|
59
|
+
为什么优先结论:这一页要回答的是「这个功能定了什么」,不是「记录了多少条」。 */
|
|
60
|
+
var b = hEl('button', 'srow srow--req');
|
|
61
|
+
b.type = 'button';
|
|
62
|
+
var ico = hEl('span', 'srow__ico srow__ico--idle');
|
|
63
|
+
ico.appendChild(svgUse('#i-doc', 20));
|
|
64
|
+
b.appendChild(ico);
|
|
65
|
+
var col = hEl('span', 'srow__col');
|
|
66
|
+
col.appendChild(hEl('span', 'srow__t', f.name || '(未命名)'));
|
|
67
|
+
/* 先结论,再一句话描述,最后才是定稿预览。定稿预览是 markdown(可能带 `**目标**:`),
|
|
68
|
+
直接塞进这一行会把星号原样显示出来;有 desc 时它更适合当副标题。 */
|
|
69
|
+
var sub = f.conclusion || f.desc || f.final_preview || '';
|
|
70
|
+
if (sub) col.appendChild(hEl('span', 'srow__s', sub));
|
|
71
|
+
var meta = [];
|
|
72
|
+
if (f.points) meta.push(f.points + ' 需求点');
|
|
73
|
+
if (f.conclusions) meta.push(f.conclusions + ' 结论');
|
|
74
|
+
if (f.sources) meta.push('来源 ' + f.sources);
|
|
75
|
+
if (!f.has_final) meta.push('未定稿');
|
|
76
|
+
if (meta.length) col.appendChild(hEl('span', 'srow__s', meta.join(' · ')));
|
|
77
|
+
b.appendChild(col);
|
|
78
|
+
if (f.updated_at) {
|
|
79
|
+
var d = new Date(Number(f.updated_at) || 0);
|
|
80
|
+
if (!isNaN(d.getTime())) {
|
|
81
|
+
b.appendChild(hEl('span', 'srow__time',
|
|
82
|
+
(d.getMonth() + 1) + '-' + ('0' + d.getDate()).slice(-2)));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
b.onclick = function () { H.openReader(f.file); };
|
|
86
|
+
return b;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* 一级:空间目录。空间是行,模块缩进挂在空间下(树形),**功能点一律不画在这里**。
|
|
90
|
+
一级只回答「这个空间有哪几块、每块多少条」—— 需求一多时画功能点会把目录撑成长列表,
|
|
91
|
+
结构反而看不见了(2026-10-22 用户定稿:两级,一级只显示空间和模块)。
|
|
92
|
+
idx = 排序后的位次(0 = 需求条数最多的那个)。 */
|
|
93
|
+
function reqWorkspaceBlock(w, idx) {
|
|
94
|
+
var wrap = hEl('div', '');
|
|
95
|
+
var head = hEl('div', 'wsrow__head');
|
|
96
|
+
var key = w.id || w.dir || w.name || '';
|
|
97
|
+
var count = Number(w.feature_count) || 0;
|
|
98
|
+
/* 默认只展开**第一个**(需求条数最多的那个),其余折叠(用户 2026-10-22):
|
|
99
|
+
空间一多,全展开就是一屏拉到底的长列表,什么结构都看不出来。
|
|
100
|
+
用户手动点过谁就按用户的来(REQ_OPEN 里有记录就不动它)。 */
|
|
101
|
+
var open = (REQ_OPEN[key] !== undefined) ? !!REQ_OPEN[key] : (idx === 0);
|
|
102
|
+
var tog = document.createElement('button');
|
|
103
|
+
tog.type = 'button'; tog.className = 'wsrow__open wsrow__open--req';
|
|
104
|
+
tog.appendChild(elSpan('wsrow__l', w.name || w.dir || w.id));
|
|
105
|
+
/* 模块数 + 功能点数:一行说清这个空间有多大,比只写空间名有用。
|
|
106
|
+
用**中性色**(不是 wsrow__badge 那枚 queued 紫)—— 那是「等待中」的语义色,
|
|
107
|
+
空间规模不是状态,套上会让人误以为这个空间在排队。 */
|
|
108
|
+
var mods = (w.modules || []).filter(function (m) { return (m.features || []).length; });
|
|
109
|
+
tog.appendChild(elSpan('wsrow__meta', mods.length + ' 模块 · ' + count + ' 条'));
|
|
110
|
+
/* 待整理条数做成徽标(与「等待中」同款):折叠起来也看得见这个空间在长新需求。 */
|
|
111
|
+
if (w.pending > 0) tog.appendChild(elSpan('wsrow__badge', w.pending + ' 待整理'));
|
|
112
|
+
/* 未初始化 = 还没按空间目录生成过基线。老 core 不带 initialized 字段(undefined),
|
|
113
|
+
此时不提示 —— 不能把「不知道」画成「未初始化」。 */
|
|
114
|
+
if (w.initialized === false) tog.appendChild(elSpan('wsrow__badge wsrow__badge--init', '未初始化'));
|
|
115
|
+
var chev = elSpan('wsrow__chev' + (open ? ' is-open' : ''));
|
|
116
|
+
chev.appendChild(svgUse('#i-chev-r', 16));
|
|
117
|
+
tog.appendChild(chev);
|
|
118
|
+
tog.onclick = function () { REQ_OPEN[key] = !open; renderRequirements(); };
|
|
119
|
+
head.appendChild(tog);
|
|
120
|
+
/* 组头右侧的「整理」:只作用于这个空间(初始化它 / 归纳它的新会话)。 */
|
|
121
|
+
var sync = document.createElement('button');
|
|
122
|
+
sync.type = 'button'; sync.className = 'wsrow__sync'; sync.textContent = '整理';
|
|
123
|
+
sync.setAttribute('data-ws', key);
|
|
124
|
+
sync.onclick = function (ev) {
|
|
125
|
+
if (ev && ev.stopPropagation) ev.stopPropagation();
|
|
126
|
+
consolidateRequirements(w);
|
|
127
|
+
};
|
|
128
|
+
head.appendChild(sync);
|
|
129
|
+
wrap.appendChild(head);
|
|
130
|
+
if (!open) return wrap;
|
|
131
|
+
|
|
132
|
+
if (w.last_error) {
|
|
133
|
+
var err = hEl('div', 'history__hint', '上次整理失败:' + w.last_error);
|
|
134
|
+
err.style.color = 'var(--warning)';
|
|
135
|
+
wrap.appendChild(err);
|
|
136
|
+
}
|
|
137
|
+
var shown = 0;
|
|
138
|
+
(w.modules || []).forEach(function (m) {
|
|
139
|
+
var fs = (m.features || []);
|
|
140
|
+
if (!fs.length) return;
|
|
141
|
+
shown += fs.length;
|
|
142
|
+
/* 模块 = 目录里的一行(缩进 + 连接线归属到空间),点它进二级看需求列表。
|
|
143
|
+
未归类(name 为空)的老数据没有模块名可写 —— 这种情况直接不画这一行,
|
|
144
|
+
功能点仍能在二级里看到(core 把空 module 排在最后)。 */
|
|
145
|
+
if (!m.name) return;
|
|
146
|
+
var row = document.createElement('button');
|
|
147
|
+
row.type = 'button'; row.className = 'reqtree__mod';
|
|
148
|
+
row.appendChild(elSpan('reqtree__modname', m.name));
|
|
149
|
+
row.appendChild(elSpan('reqtree__modcount', String(fs.length)));
|
|
150
|
+
var go = elSpan('reqtree__modgo');
|
|
151
|
+
go.appendChild(svgUse('#i-chev-r', 16));
|
|
152
|
+
row.appendChild(go);
|
|
153
|
+
row.onclick = function () { openReqDetail(w, m.name); };
|
|
154
|
+
wrap.appendChild(row);
|
|
155
|
+
});
|
|
156
|
+
/* 空间还在但还没有文档:别画成一片空白,给出这一组的下一步(整理)。 */
|
|
157
|
+
if (!shown) {
|
|
158
|
+
var tip = Number(w.pending) > 0
|
|
159
|
+
? '有 ' + w.pending + ' 条对话待整理'
|
|
160
|
+
: (w.initialized === false ? '还没有需求文档,点右侧「整理」按空间生成' : '还没有需求文档');
|
|
161
|
+
var hint = hEl('div', 'history__hint', tip);
|
|
162
|
+
hint.style.padding = '6px 12px 12px';
|
|
163
|
+
wrap.appendChild(hint);
|
|
164
|
+
}
|
|
165
|
+
return wrap;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* 二级:**只看当前模块**的需求列表(2026-10-22 用户定稿:点哪个模块进来就只看哪个模块,
|
|
169
|
+
别把同空间其他模块的需求也倒进来)。模块名是这一页唯一的筛子;找不到(刚被删 / 改名)
|
|
170
|
+
时如实说这个模块没有,不退化成整空间 —— 否则用户会以为点错了。 */
|
|
171
|
+
function reqDetailBlock(d) {
|
|
172
|
+
var w = d.ws;
|
|
173
|
+
var frag = document.createDocumentFragment();
|
|
174
|
+
(w.modules || []).forEach(function (m) {
|
|
175
|
+
if (m.name !== d.mod) return;
|
|
176
|
+
var fs = (m.features || []);
|
|
177
|
+
if (!fs.length) return;
|
|
178
|
+
var sec = hEl('div', 'reqdetail__sec reqdetail__sec--cur');
|
|
179
|
+
var mh = hEl('div', 'sechead');
|
|
180
|
+
mh.style.paddingLeft = '16px';
|
|
181
|
+
mh.appendChild(elSpan('sechead__t', m.name + ' (' + fs.length + ')'));
|
|
182
|
+
sec.appendChild(mh);
|
|
183
|
+
fs.forEach(function (f) { sec.appendChild(reqFeatureRow(f)); });
|
|
184
|
+
frag.appendChild(sec);
|
|
185
|
+
});
|
|
186
|
+
if (!frag.childNodes.length) {
|
|
187
|
+
frag.appendChild(hEl('div', 'history__hint', '这个模块还没有需求文档'));
|
|
188
|
+
}
|
|
189
|
+
return frag;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/* 进二级:记下是哪个空间的哪个模块,只画这个模块的需求。 */
|
|
193
|
+
function openReqDetail(w, mod) {
|
|
194
|
+
var key = w.id || w.dir || w.name || '';
|
|
195
|
+
REQ_DETAIL = { key: key, mod: mod, ws: w };
|
|
196
|
+
renderRequirements();
|
|
197
|
+
/* 空间在两次渲染之间被删掉了(会话全删 → 空间联动消失):
|
|
198
|
+
renderRequirements 会把 REQ_DETAIL 清空,这时别停在空白的二级页,退回目录。 */
|
|
199
|
+
if (!REQ_DETAIL) { H.showRoot(); return; }
|
|
200
|
+
H.showDetail();
|
|
201
|
+
if (els.detailTitle) els.detailTitle.textContent = (w.name || w.dir || w.id) + ' · ' + mod;
|
|
202
|
+
/* 页里只有这一个模块,进来从顶部看起(旧的「滚到当前模块」已无意义)。 */
|
|
203
|
+
if (els.detailList) els.detailList.scrollTop = 0;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/* 退二级回一级目录。 */
|
|
207
|
+
function closeReqDetail() {
|
|
208
|
+
REQ_DETAIL = null;
|
|
209
|
+
H.showRoot();
|
|
210
|
+
renderRequirements();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function renderRequirements() {
|
|
214
|
+
if (!els.list) return;
|
|
215
|
+
var data = REQ_DATA;
|
|
216
|
+
if (!data) return;
|
|
217
|
+
/* 空间清单与任务页对齐:**用与 collectSpaceKeys 同一套空间键过滤**
|
|
218
|
+
(用户 2026-10-22:「4个空间需求里显示的不一样」「会话中全部删除后需求中的空间还在
|
|
219
|
+
也要联动删除」)。没有文档的空间不再整块藏掉,而是留一个组头 + 「整理」按钮 ——
|
|
220
|
+
这样两页的空间数一致,也给了「这个空间还没生成需求」的入口。
|
|
221
|
+
拿不到会话/目录清单时(REQ_ALIVE=null)不筛。 */
|
|
222
|
+
var all = (data && data.workspaces) || [];
|
|
223
|
+
var list = all.filter(function (w) {
|
|
224
|
+
var dir = reqNormDir(w && w.dir);
|
|
225
|
+
/* 根目录 / 归一后是空串 —— 「!dir 就放行」的老写法会把它漏进来,
|
|
226
|
+
需求页于是出现一个叫「/」的假空间(用户 2026-10-22:「没有空间的不需要」)。
|
|
227
|
+
这里先卡一道:不是真空间目录的直接不画。 */
|
|
228
|
+
if (!H.isRealSpaceDir(dir)) return false;
|
|
229
|
+
return !REQ_ALIVE || !dir || !!REQ_ALIVE[dir];
|
|
230
|
+
});
|
|
231
|
+
/* 排序:**需求条数多的排前面**(用户 2026-10-22)。
|
|
232
|
+
core 给的顺序是「最后归纳时间倒序」,但那是给「最近在动的」用的;
|
|
233
|
+
需求页要回答的是「哪块需求最多」,条数多的才是用户要先看的那个。
|
|
234
|
+
条数一样时按名字排 —— 同一条数下顺序要稳定,不能每次刷新都在跳。 */
|
|
235
|
+
list.sort(function (a, b) {
|
|
236
|
+
var ca = Number(a && a.feature_count) || 0;
|
|
237
|
+
var cb = Number(b && b.feature_count) || 0;
|
|
238
|
+
if (ca !== cb) return cb - ca;
|
|
239
|
+
return String((a && a.name) || '').localeCompare(String((b && b.name) || ''));
|
|
240
|
+
});
|
|
241
|
+
/* 二级视图:只画这个模块的需求列表。空间可能已被会话删除联动掉
|
|
242
|
+
(list 里没有它)—— 那就退回一级目录,不要留一页空白。 */
|
|
243
|
+
if (REQ_DETAIL) {
|
|
244
|
+
var hit = null;
|
|
245
|
+
for (var i = 0; i < list.length; i++) {
|
|
246
|
+
if ((list[i].id || list[i].dir || list[i].name || '') === REQ_DETAIL.key) { hit = list[i]; break; }
|
|
247
|
+
}
|
|
248
|
+
if (!hit) { REQ_DETAIL = null; }
|
|
249
|
+
else {
|
|
250
|
+
REQ_DETAIL.ws = hit;
|
|
251
|
+
if (els.detailTitle) els.detailTitle.textContent = (hit.name || hit.dir || hit.id) + ' · ' + REQ_DETAIL.mod;
|
|
252
|
+
if (els.detailList) els.detailList.replaceChildren(reqDetailBlock(REQ_DETAIL));
|
|
253
|
+
if (REQ_BUSY) reqSetBusy(true, REQ_BUSY_LABEL);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
var frag = document.createDocumentFragment();
|
|
258
|
+
if (!list.length) {
|
|
259
|
+
/* 一个空间都没有(会话全删了 / 还没聊过)。 */
|
|
260
|
+
frag.appendChild(hEl('div', 'history__hint',
|
|
261
|
+
REQ_ALIVE
|
|
262
|
+
? '还没有任何空间。在任务里聊几句,这里会按空间整理出功能点文档。'
|
|
263
|
+
: '还没有任何需求。在任务里聊几句,这里会自动整理出功能点文档。'));
|
|
264
|
+
els.list.replaceChildren(frag);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
/* 工作区顺序:上面已按需求条数排好(多的在前)。默认只展开第一个 ——
|
|
268
|
+
见 reqWorkspaceBlock 的 open 判定。 */
|
|
269
|
+
list.forEach(function (w, i) { frag.appendChild(reqWorkspaceBlock(w, i)); });
|
|
270
|
+
els.list.replaceChildren(frag);
|
|
271
|
+
/* 整理进行中切走再回来:新 DOM 的按钮默认是可点的,这里按当前状态重新置忙。 */
|
|
272
|
+
if (REQ_BUSY) reqSetBusy(true, REQ_BUSY_LABEL);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function loadRequirements(quiet) {
|
|
276
|
+
if (!els.list) return;
|
|
277
|
+
if (!quiet && !REQ_DATA) els.list.innerHTML = '<div class="history__hint">读取中…</div>';
|
|
278
|
+
var hdr = H.getToken() ? { 'X-Magic-Token': H.getToken() } : {};
|
|
279
|
+
var pReq = fetch(H.getBase() + '/desk/requirements', { headers: hdr })
|
|
280
|
+
.then(function (r) {
|
|
281
|
+
if (r.status === 501) {
|
|
282
|
+
/* 老 core 没有这个能力位:**说清楚**,不要给一页空白
|
|
283
|
+
(空白看起来像「没有需求」,是最难查的一种)。 */
|
|
284
|
+
return r.json().then(function (e) { throw new Error((e && e.error) || '当前 core 不支持需求聚合'); });
|
|
285
|
+
}
|
|
286
|
+
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
287
|
+
return r.json();
|
|
288
|
+
});
|
|
289
|
+
/* 会话清单与空间目录:用来判断「这个空间在任务页还在不在」。
|
|
290
|
+
两者都允许失败 —— 拿不到就不筛空间(见 renderRequirements)。 */
|
|
291
|
+
var pSess = fetch(H.getBase() + '/desk/sessions', { headers: hdr })
|
|
292
|
+
.then(function (r) { return r.ok ? r.json() : null; }).catch(function () { return null; });
|
|
293
|
+
var pDirs = fetch(H.getBase() + '/desk/dirs', { headers: hdr })
|
|
294
|
+
.then(function (r) { return r.ok ? r.json() : null; }).catch(function () { return null; });
|
|
295
|
+
Promise.all([pReq, pSess, pDirs])
|
|
296
|
+
.then(function (a) {
|
|
297
|
+
var sess = a[1], dirs = (a[2] && a[2].dirs) || null;
|
|
298
|
+
if (Array.isArray(sess) || Array.isArray(dirs)) {
|
|
299
|
+
/* 与任务页**同一套空间键**(用户 2026-10-22:「4个空间需求里显示的不一样」
|
|
300
|
+
「会话中全部删除后需求中的空间还在 也要联动删除」「没有空间的不需要」):
|
|
301
|
+
① 根目录下的 magic-* ② 自定义空间 ③ **顶层会话**的工作目录。
|
|
302
|
+
⚠️ ③ 只能取**顶层**会话的 workspace:团队子代理的会话挂在父会话下面,
|
|
303
|
+
它们的工作目录是 /…/run-xxx/a1 这种临时目录 —— 任务页不把它们列成空间,
|
|
304
|
+
需求页照收就会多出一堆 a1/t1/tmp 假空间。
|
|
305
|
+
⚠️ 根目录 / 与 /tmp 这类系统目录不是空间,哪怕历史上有会话在那儿跑过。 */
|
|
306
|
+
var alive = {};
|
|
307
|
+
(H.getRootDirs() || []).forEach(function (d) { if (d && d.path) alive[reqNormDir(d.path)] = 1; });
|
|
308
|
+
(dirs || []).forEach(function (d) {
|
|
309
|
+
if (d && d.path && String(d.name || '').indexOf('magic-') === 0) alive[reqNormDir(d.path)] = 1;
|
|
310
|
+
});
|
|
311
|
+
(H.getCustomSpaces() || []).forEach(function (p) { if (p) alive[reqNormDir(p)] = 1; });
|
|
312
|
+
H.topLevelSessions(sess).forEach(function (s) {
|
|
313
|
+
if (s && s.workspace && H.isRealSpaceDir(s.workspace)) alive[reqNormDir(s.workspace)] = 1;
|
|
314
|
+
});
|
|
315
|
+
REQ_ALIVE = alive;
|
|
316
|
+
} else {
|
|
317
|
+
REQ_ALIVE = null; // 清单没拿到:不筛,宁可多显示
|
|
318
|
+
}
|
|
319
|
+
REQ_DATA = a[0] || { workspaces: [] };
|
|
320
|
+
renderRequirements();
|
|
321
|
+
})
|
|
322
|
+
.catch(function (e) {
|
|
323
|
+
if (quiet && REQ_DATA) return; // 后台刷新失败不覆盖已有画面
|
|
324
|
+
els.list.innerHTML = '';
|
|
325
|
+
els.list.appendChild(hEl('div', 'history__hint', '读取需求失败:' + ((e && e.message) || e)));
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/* 「整理」:手动触发一轮(POST /desk/requirements/consolidate)。
|
|
330
|
+
服务端按 state 判断走哪条路:
|
|
331
|
+
· 没初始化过 → 先按空间目录反推需求基线(首次可能跑几分钟,按钮要能等);
|
|
332
|
+
· 已初始化 → 只消费上次整理之后的新会话。
|
|
333
|
+
入口有两个:
|
|
334
|
+
· 每个空间组头的「整理」——只整理这个空间(用户点哪个就是哪个,不猜);
|
|
335
|
+
· 右上角「全部整理」——按空间逐个串行跑一遍,汇总结果(一键触发全部)。
|
|
336
|
+
两个入口都带「我的 → 默认引擎/模型」,服务端不再自动替换引擎。 */
|
|
337
|
+
function reqPostConsolidate(ws) {
|
|
338
|
+
var hdr = { 'Content-Type': 'application/json' };
|
|
339
|
+
if (H.getToken()) hdr['X-Magic-Token'] = H.getToken();
|
|
340
|
+
return fetch(H.getBase() + '/desk/requirements/consolidate', {
|
|
341
|
+
method: 'POST', headers: hdr,
|
|
342
|
+
body: JSON.stringify({
|
|
343
|
+
workspace: ws || '',
|
|
344
|
+
engine: H.getRunPref().engine || '',
|
|
345
|
+
model: (H.getRunPref().engine && H.getRunPref().model) || ''
|
|
346
|
+
})
|
|
347
|
+
})
|
|
348
|
+
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
|
|
349
|
+
.then(function (x) {
|
|
350
|
+
var d = x.d || {};
|
|
351
|
+
if (!x.ok) throw new Error(d.error || ('HTTP ' + (x.d && x.d.status)));
|
|
352
|
+
if (d.error) throw new Error(d.error);
|
|
353
|
+
return d;
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/* 统一置忙:顶部主按钮 + 所有空间组头的「整理」一起禁用。
|
|
358
|
+
label 只改顶部;组头统一显示「整理中…」/「整理」,不各自变文案。
|
|
359
|
+
二级页那枚「整理」也跟着禁用 —— 归纳期间它和组头一样不能重复点。 */
|
|
360
|
+
function reqSetBusy(on, label) {
|
|
361
|
+
if (on) { if (label !== undefined) REQ_BUSY_LABEL = label; }
|
|
362
|
+
else REQ_BUSY_LABEL = '全部整理';
|
|
363
|
+
var btn = els.sync;
|
|
364
|
+
if (btn) {
|
|
365
|
+
btn.disabled = !!on;
|
|
366
|
+
if (label !== undefined) btn.textContent = label;
|
|
367
|
+
}
|
|
368
|
+
var dbtn = els.detailSync;
|
|
369
|
+
if (dbtn) {
|
|
370
|
+
dbtn.disabled = !!on;
|
|
371
|
+
dbtn.textContent = on ? '整理中…' : '整理';
|
|
372
|
+
}
|
|
373
|
+
Array.prototype.forEach.call(document.querySelectorAll('.wsrow__sync'), function (b) {
|
|
374
|
+
b.disabled = !!on;
|
|
375
|
+
b.textContent = on ? '整理中…' : '整理';
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function consolidateRequirements(target) {
|
|
380
|
+
if (REQ_BUSY || !target) return;
|
|
381
|
+
var ws = target.dir || target.id || '';
|
|
382
|
+
if (!ws) return;
|
|
383
|
+
var firstTime = target.initialized === false;
|
|
384
|
+
REQ_BUSY = true;
|
|
385
|
+
reqSetBusy(true, firstTime ? '初始化中…' : '整理中…');
|
|
386
|
+
var targetKey = target.id || target.dir || '';
|
|
387
|
+
var groupBtn = null;
|
|
388
|
+
Array.prototype.forEach.call(document.querySelectorAll('.wsrow__sync'), function (b) {
|
|
389
|
+
if (!groupBtn && targetKey && b.getAttribute('data-ws') === targetKey) groupBtn = b;
|
|
390
|
+
});
|
|
391
|
+
/* 点的是哪个空间,就只让哪个组头显示「初始化中…」;其它组头保持「整理中…」。 */
|
|
392
|
+
if (groupBtn) groupBtn.textContent = firstTime ? '初始化中…' : '整理中…';
|
|
393
|
+
reqPostConsolidate(ws)
|
|
394
|
+
.then(function (d) {
|
|
395
|
+
if (d.bootstrap) {
|
|
396
|
+
if (d.created || d.updated) toast('已按空间目录生成需求:新建 ' + (d.created || 0) + ' · 更新 ' + (d.updated || 0));
|
|
397
|
+
else toast('已按空间目录初始化:暂未识别到功能点');
|
|
398
|
+
}
|
|
399
|
+
else if (d.noop) toast('没有新的对话需要整理');
|
|
400
|
+
else if (d.updated || d.created) toast('整理完成:更新 ' + (d.updated || 0) + ' · 新建 ' + (d.created || 0));
|
|
401
|
+
else toast('整理完成');
|
|
402
|
+
loadRequirements(true); // 后台刷新(不闪「读取中」)
|
|
403
|
+
})
|
|
404
|
+
.catch(function (e) { toast('整理失败:' + ((e && e.message) || e)); })
|
|
405
|
+
.then(function () {
|
|
406
|
+
REQ_BUSY = false;
|
|
407
|
+
reqSetBusy(false, '全部整理');
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/* 全部整理:按空间串行跑,避免多个初始化同时读目录把机器拖垮。
|
|
412
|
+
顺序 = 有待整理的 → 未初始化的 → 其它(最近动过的仍在前),先做便宜/该做的。
|
|
413
|
+
每个空间单独一次 HTTP(各自 10 分钟超时);某个失败不影响后面的。 */
|
|
414
|
+
function reqAllWorkspaces() {
|
|
415
|
+
var seen = {};
|
|
416
|
+
var list = ((REQ_DATA && REQ_DATA.workspaces) || []).filter(function (w) {
|
|
417
|
+
var key = w.dir || w.id || '';
|
|
418
|
+
if (!key || seen[key]) return false;
|
|
419
|
+
/* 会话已全部删除的空间不参与「全部整理」:否则又会把它重新生成出来。 */
|
|
420
|
+
var dir = reqNormDir(w.dir);
|
|
421
|
+
if (REQ_ALIVE && dir && !REQ_ALIVE[dir]) return false;
|
|
422
|
+
seen[key] = true;
|
|
423
|
+
return true;
|
|
424
|
+
});
|
|
425
|
+
function rank(w) {
|
|
426
|
+
if (Number(w.pending) > 0) return 0;
|
|
427
|
+
if (w.initialized === false) return 1;
|
|
428
|
+
return 2;
|
|
429
|
+
}
|
|
430
|
+
return list.sort(function (a, b) { return rank(a) - rank(b); });
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function consolidateAllRequirements() {
|
|
434
|
+
if (REQ_BUSY) return;
|
|
435
|
+
var list = reqAllWorkspaces();
|
|
436
|
+
if (!list.length) {
|
|
437
|
+
toast('还没有可整理的空间');
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
REQ_BUSY = true;
|
|
441
|
+
reqSetBusy(true, '整理中 0/' + list.length);
|
|
442
|
+
var i = 0, ok = 0, fail = 0, created = 0, updated = 0, firstErr = '';
|
|
443
|
+
function done() {
|
|
444
|
+
REQ_BUSY = false;
|
|
445
|
+
reqSetBusy(false, '全部整理');
|
|
446
|
+
loadRequirements(true);
|
|
447
|
+
if (fail) {
|
|
448
|
+
toast('全部整理完成:成功 ' + ok + ' · 失败 ' + fail + (firstErr ? '(' + firstErr + ')' : ''));
|
|
449
|
+
} else {
|
|
450
|
+
toast('全部整理完成:' + ok + ' 个空间 · 新建 ' + created + ' · 更新 ' + updated);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
function step() {
|
|
454
|
+
if (i >= list.length) { done(); return; }
|
|
455
|
+
var w = list[i++];
|
|
456
|
+
REQ_BUSY_LABEL = '整理中 ' + i + '/' + list.length;
|
|
457
|
+
var btn = els.sync;
|
|
458
|
+
if (btn) btn.textContent = REQ_BUSY_LABEL;
|
|
459
|
+
reqPostConsolidate(w.dir || w.id || '')
|
|
460
|
+
.then(function (d) {
|
|
461
|
+
ok++;
|
|
462
|
+
created += Number(d.created) || 0;
|
|
463
|
+
updated += Number(d.updated) || 0;
|
|
464
|
+
})
|
|
465
|
+
.catch(function (e) {
|
|
466
|
+
fail++;
|
|
467
|
+
if (!firstErr) firstErr = (e && e.message) || String(e);
|
|
468
|
+
})
|
|
469
|
+
.then(step);
|
|
470
|
+
}
|
|
471
|
+
step();
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/* 挂载:index.html 在 DOM 就绪后调用一次。
|
|
475
|
+
host 里的 getter 必须保持为函数 —— 切环境后 BASE/TOKEN/空间清单会变。 */
|
|
476
|
+
function mount(host) {
|
|
477
|
+
H = host;
|
|
478
|
+
var root = H.rootEl, detail = H.detailEl;
|
|
479
|
+
if (!root || !detail) return;
|
|
480
|
+
root.innerHTML = ROOT_HTML;
|
|
481
|
+
detail.innerHTML = DETAIL_HTML;
|
|
482
|
+
els.root = root;
|
|
483
|
+
els.detail = detail;
|
|
484
|
+
els.list = root.querySelector('#reqList');
|
|
485
|
+
els.detailList = detail.querySelector('#reqDetailList');
|
|
486
|
+
els.detailTitle = detail.querySelector('#reqDetailTitle');
|
|
487
|
+
els.sync = root.querySelector('#btnReqSync');
|
|
488
|
+
els.detailBack = detail.querySelector('#btnReqDetailBack');
|
|
489
|
+
els.detailSync = detail.querySelector('#btnReqDetailSync');
|
|
490
|
+
/* 两个动作:全部整理 / 返回一级;二级页的「整理」只作用于当前空间
|
|
491
|
+
(不是全部空间 —— 二级页的作用域就是这个空间,标成「全部整理」会让人以为还是全局动作)。 */
|
|
492
|
+
if (els.sync) els.sync.onclick = function () { consolidateAllRequirements(); };
|
|
493
|
+
if (els.detailBack) els.detailBack.onclick = function () { closeReqDetail(); };
|
|
494
|
+
if (els.detailSync) els.detailSync.onclick = function () {
|
|
495
|
+
if (!REQ_DETAIL || !REQ_DETAIL.ws) return;
|
|
496
|
+
consolidateRequirements(REQ_DETAIL.ws);
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/* 进入「需求」tab:每次进来都回到一级目录并拉一次清单(只读,不触发归纳)。 */
|
|
501
|
+
function show() {
|
|
502
|
+
REQ_DETAIL = null;
|
|
503
|
+
loadRequirements(false);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
window.MagicReqs = { mount: mount, show: show };
|
|
507
|
+
})();
|
package/cmd/magic-client/main.go
CHANGED
|
@@ -126,7 +126,7 @@ func main() {
|
|
|
126
126
|
if strings.TrimSpace(*uiDir) != "" || strings.TrimSpace(*mobileDir) != "" {
|
|
127
127
|
fmt.Fprintf(os.Stderr, "magic-client: 移动端界面(远程 HTML,改界面免重打 APK) http://127.0.0.1:%d/m/?token=%s\n", port, tok)
|
|
128
128
|
}
|
|
129
|
-
fmt.Fprintf(os.Stderr, "magic-client: 接口 /desk/{health,contract,engines,modules,sessions,profile,asks,ask,ask/{id}/control} + 业务转发 /api/*(都要 X-Magic-Token)\n")
|
|
129
|
+
fmt.Fprintf(os.Stderr, "magic-client: 接口 /desk/{health,contract,engines,modules,sessions,profile,suggest,asks,ask,ask/{id}/control} + 业务转发 /api/*(都要 X-Magic-Token)\n")
|
|
130
130
|
var lanAddrs []string
|
|
131
131
|
if *lan {
|
|
132
132
|
// 绑到局域网就把可用地址全打出来:一台机器常有多个网卡,猜错比多打两行麻烦。
|
package/internal/cli/ask.go
CHANGED
|
@@ -126,6 +126,11 @@ type askOptions struct {
|
|
|
126
126
|
titleForce bool // --title-force:已有覆盖也重新归纳
|
|
127
127
|
titleEngine string // --title-engine:归纳用哪个引擎(默认 llm)
|
|
128
128
|
|
|
129
|
+
// 下一轮建议(2026-10-23;见 internal/suggest)。
|
|
130
|
+
// 一轮结束后生成 0-3 条「用户下一句」的快捷回复;一次性旁路调用,不续接会话。
|
|
131
|
+
// 值 = 请求 JSON 的文件路径("-" = stdin)。
|
|
132
|
+
suggestInput string // --suggest-followups <file|->:{"user":"…","reply":"…"}
|
|
133
|
+
|
|
129
134
|
// 四档权限模型(见 internal/agent/permission.go)。仅 claude / codebuddy 接线。
|
|
130
135
|
permission string // 档位:manual | accept-edits | auto | full
|
|
131
136
|
sandboxExclude []string // 始终在沙箱外执行的命令(sandbox.excludedCommands)
|
|
@@ -209,6 +214,7 @@ func bindAskFlags(cmd *cobra.Command, opts *askOptions) {
|
|
|
209
214
|
f.StringVar(&opts.sessionTitle, "session-title", "", "读一条会话**已存**的标题(JSON:{id,title}),不调 LLM。没有覆盖时回落到 prompt_head。列表页取标题走这条,别逐行调 --summarize-title")
|
|
210
215
|
f.BoolVar(&opts.titleForce, "title-force", false, "配合 --summarize-title:已有标题也重新归纳(默认命中缓存就直接返回,省一次 LLM 调用)")
|
|
211
216
|
f.StringVar(&opts.titleEngine, "title-engine", "llm", "--summarize-title 用哪个引擎归纳(默认 llm:纯文本、不带工具)")
|
|
217
|
+
f.StringVar(&opts.suggestInput, "suggest-followups", "", "生成下一轮快捷回复:传请求 JSON 的文件路径(\"-\" = stdin):{\"user\":\"…\",\"reply\":\"…\"}。旁路调用,不续接当前会话;引擎走 -e(默认 llm)")
|
|
212
218
|
f.Uint64Var(&opts.logAfter, "after", 0, "配合 --session-log:增量续读,只回 seq > N 的事件(另附 count/after/lastSeq/snapshotRequired)")
|
|
213
219
|
f.Uint64Var(&opts.logBefore, "before", 0, "配合 --session-log:向上翻更早的历史,从文件尾跳过 N 条已画事件再取一页(0 = 最新一页;下一页把返回的 nextBefore 传回来。另附 count/before/nextBefore/firstSeq/more;与 --after 互斥)")
|
|
214
220
|
f.IntVar(&opts.logLimit, "limit", 200, "配合 --before:每页最多返回多少条事件(默认 200)")
|
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
|
}
|