magic-agent 1.0.0 → 1.0.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 +298 -0
- package/README.md +35 -0
- package/client-ui/assets/OFL-Caveat.txt +93 -0
- package/client-ui/assets/caveat.woff2 +0 -0
- package/client-ui/index.html +246 -0
- package/client-ui/pc/app.css +5110 -0
- package/client-ui/pc/assets/damiao-mascot.png +0 -0
- package/client-ui/pc/assets/icon.png +0 -0
- package/client-ui/pc/index.html +154 -0
- package/client-ui/pc/js/00-base.js +52 -0
- package/client-ui/pc/js/03-chat-message.js +880 -0
- package/client-ui/pc/js/04-task-list.js +430 -0
- package/client-ui/pc/js/05-task-list-render.js +812 -0
- package/client-ui/pc/js/06-ask-answer.js +204 -0
- package/client-ui/pc/js/07-chat-stream-render.js +493 -0
- package/client-ui/pc/js/08-ask-event-map.js +171 -0
- package/client-ui/pc/js/09-stream-apply.js +367 -0
- package/client-ui/pc/js/10-markdown.js +430 -0
- package/client-ui/pc/js/18-askbox.js +262 -0
- package/client-ui/pc/js/19-attachments.js +679 -0
- package/client-ui/pc/js/29-msg-variants.js +462 -0
- package/client-ui/pc/js/90-glue.js +212 -0
- package/client-ui/pc/js/91-workbench-stubs.js +134 -0
- package/client-ui/pc/js/92-composer.js +406 -0
- package/client-ui/pc/js/93-rail.js +362 -0
- package/client-ui/pc/vendor/highlight.min.js +1262 -0
- package/client-ui/pc/vendor/marked.min.js +6 -0
- package/client-ui/pc/vendor/purify.min.js +3 -0
- package/client-ui/shared/conversation.css +256 -0
- package/client-ui/shared/conversation.js +1225 -0
- package/client-ui/shared/transport-http.js +123 -0
- package/client-ui-mobile/assets/OFL-Caveat.txt +93 -0
- package/client-ui-mobile/assets/caveat.woff2 +0 -0
- package/client-ui-mobile/index.html +173 -0
- package/npm/bin/magic-agent.js +43 -0
- package/npm/dist/darwin-arm64/magic-agent +0 -0
- package/npm/dist/darwin-x64/magic-agent +0 -0
- package/npm/dist/linux-arm64/magic-agent +0 -0
- package/npm/dist/linux-x64/magic-agent +0 -0
- package/npm/dist/win32-x64/magic-agent.exe +0 -0
- package/package.json +4 -1
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
/* 撰写区交互层:把「+ / 引擎 / 模型 / 权限」四只菜单接上,并绑定发送主链。
|
|
2
|
+
*
|
|
3
|
+
* ── 为什么不是直接搬 magic-test 的 bindHomeEntry ──
|
|
4
|
+
* 上游那 1100 行把四只菜单、左下角工作目录切换器、需求派发拦截、键盘导航、
|
|
5
|
+
* Floating UI 定位全缠在一个闭包里,且依赖整个工作台。这一版只保「对话撰写区」这一层:
|
|
6
|
+
* · 标记 = 18-askbox.js 现画的(一字未改)
|
|
7
|
+
* · 菜单内容 = qaPermMenuBuild()(18)/ askAddMenuBuild()(19)现成的构造器,直接用
|
|
8
|
+
* · 弹出层 = 这里自己写的一份(position:fixed,贴着按钮翻上/翻下)
|
|
9
|
+
* 上游的 Floating UI 精确定位、完整键盘导航、工作目录切换器 —— **未移植**(见增强点清单)。
|
|
10
|
+
*
|
|
11
|
+
* ── 命名沿用上游 ──
|
|
12
|
+
* 函数名(qaPickerSync / qaModelPickSync / qaEngHintSync / qaMenuPlace / qaCloseOne …)
|
|
13
|
+
* 与上游一致,因为别的分片(05 的任务行尾菜单、07 的发送链)会按名字引用它们。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/* ---------- 引擎行元素:弹出层与内容构造器要用的公共小件 ---------- */
|
|
17
|
+
|
|
18
|
+
/* 与上游 09-ask-event.js 一致的「支持流式」图标(只标正向,不支持什么都不画) */
|
|
19
|
+
const QA_ICON_STREAM = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M3 7h9M3 12h6M3 17h9"/><rect x="15.6" y="5" width="2.8" height="14" rx="1.4" fill="currentColor" stroke="none"/></svg>';
|
|
20
|
+
const qaStreamTag = (x)=>{
|
|
21
|
+
if(!x || x.streaming !== true) return "";
|
|
22
|
+
return `<span class="stm" data-streaming="true" role="img" aria-label="支持流式" title="支持 --stream:回答逐字流式返回">${QA_ICON_STREAM}</span>`;
|
|
23
|
+
};
|
|
24
|
+
/** 对每个已挂载的实例做一件事(实例清单唯一来源 = ASK_INSTANCES)。原样搬自上游。 */
|
|
25
|
+
const qaAskEach = (suffix, fn)=>{ ASK_INSTANCES.forEach(p=>{ const el = $("#" + p + suffix); if(el) fn(el, p); }); };
|
|
26
|
+
|
|
27
|
+
/* ---------- 弹出层(自研的极简版) ---------- */
|
|
28
|
+
|
|
29
|
+
let qaMenuBound = false;
|
|
30
|
+
|
|
31
|
+
/** 关掉一只菜单:隐藏 + 复位按钮的打开态。 */
|
|
32
|
+
function qaCloseOne(m){
|
|
33
|
+
if(!m || m.hidden) return;
|
|
34
|
+
m.hidden = true;
|
|
35
|
+
m.classList.remove("dock-up","dock-down");
|
|
36
|
+
const btn = m._qaBtn;
|
|
37
|
+
if(btn){ btn.classList.remove("open"); btn.setAttribute("aria-expanded","false"); }
|
|
38
|
+
m._qaBtn = null;
|
|
39
|
+
}
|
|
40
|
+
/** 关掉全部(除 except)。清单 = 登记在册的四只组件菜单。 */
|
|
41
|
+
function qaMenusClose(except){
|
|
42
|
+
ASK_INSTANCES.forEach(p=>["-add-menu","-engine-menu","-model-menu","-perm-menu"].forEach(s=>{
|
|
43
|
+
const m = $("#" + p + s);
|
|
44
|
+
if(m && m !== except) qaCloseOne(m);
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
/** 定位:贴着触发按钮。下方放不下就翻到上方;左右夹持在视口内。
|
|
48
|
+
* ⚠️ 用 `position:fixed` + 视口坐标(不是 absolute + offsetParent)—— 宿主布局一变,
|
|
49
|
+
* absolute 的包含块就不再是 .home-entry,会整体错位。 */
|
|
50
|
+
function qaMenuPlace(btn, menu){
|
|
51
|
+
if(!btn || !menu || menu.hidden) return;
|
|
52
|
+
const r = btn.getBoundingClientRect();
|
|
53
|
+
const w = menu.offsetWidth, h = menu.offsetHeight;
|
|
54
|
+
menu.style.position = "fixed";
|
|
55
|
+
menu.style.right = "auto"; menu.style.bottom = "auto";
|
|
56
|
+
menu.style.left = Math.max(8, Math.min(Math.round(r.left), window.innerWidth - w - 8)) + "px";
|
|
57
|
+
const below = Math.round(r.bottom) + 4;
|
|
58
|
+
const up = (below + h > window.innerHeight - 8) && (r.top - h - 4 > 8);
|
|
59
|
+
menu.style.top = Math.round(up ? r.top - h - 4 : below) + "px";
|
|
60
|
+
menu.classList.toggle("dock-up", up);
|
|
61
|
+
menu.classList.toggle("dock-down", !up);
|
|
62
|
+
}
|
|
63
|
+
/** 清单异步变长后重排(模型清单可能几十项)。 */
|
|
64
|
+
function qaMenuReplace(){ ASK_INSTANCES.forEach(p=>["-add-menu","-engine-menu","-model-menu","-perm-menu"].forEach(s=>{
|
|
65
|
+
const m = $("#" + p + s);
|
|
66
|
+
if(m && !m.hidden && m._qaBtn) qaMenuPlace(m._qaBtn, m);
|
|
67
|
+
})); }
|
|
68
|
+
function qaMenuToggle(btn, menu, p){
|
|
69
|
+
if(!menu) return;
|
|
70
|
+
const willOpen = menu.hidden;
|
|
71
|
+
qaMenusClose(menu);
|
|
72
|
+
if(!willOpen){ qaCloseOne(menu); return; }
|
|
73
|
+
menu.dataset.qaInstance = p;
|
|
74
|
+
menu._qaBtn = btn;
|
|
75
|
+
menu.hidden = false;
|
|
76
|
+
btn.classList.add("open");
|
|
77
|
+
btn.setAttribute("aria-expanded","true");
|
|
78
|
+
qaMenuPlace(btn, menu);
|
|
79
|
+
/* 打开时把键盘高亮落到当前选中项 */
|
|
80
|
+
const cur = menu.querySelector(".ag-model-opt.on") || menu.querySelector(".ag-model-opt");
|
|
81
|
+
menu.querySelectorAll(".ag-model-opt").forEach(o=>o.classList.remove("kb"));
|
|
82
|
+
if(cur){ cur.classList.add("kb"); cur.scrollIntoView({ block:"nearest" }); }
|
|
83
|
+
}
|
|
84
|
+
/* 供别的函数作用域复用(05 的任务行尾菜单走这两个钩子,与上游同款) */
|
|
85
|
+
window.__qaMenuClose = qaCloseOne;
|
|
86
|
+
window.__qaMenuPlace = qaMenuPlace;
|
|
87
|
+
window.__qaMenusClose = qaMenusClose;
|
|
88
|
+
window.__qaMenuReplace = qaMenuReplace;
|
|
89
|
+
|
|
90
|
+
/* ---------- 键盘导航(↑↓ 移动 / Enter 选中 / Esc 关闭) ---------- */
|
|
91
|
+
function qaKbMove(menu, dir){
|
|
92
|
+
const opts = [...menu.querySelectorAll(".ag-model-opt")];
|
|
93
|
+
if(!opts.length) return -1;
|
|
94
|
+
let i = opts.findIndex(o=>o.classList.contains("kb"));
|
|
95
|
+
opts.forEach(o=>o.classList.remove("kb"));
|
|
96
|
+
i = i < 0 ? 0 : (i + dir + opts.length) % opts.length;
|
|
97
|
+
opts[i].classList.add("kb");
|
|
98
|
+
opts[i].scrollIntoView({ block:"nearest" });
|
|
99
|
+
return i;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* ---------- 菜单内容构造 ---------- */
|
|
103
|
+
|
|
104
|
+
/** 引擎菜单:清单来自 `magic-agent --engines`(经 desk.engines 桥)。原样搬自上游。 */
|
|
105
|
+
function qaEngMenuBuild(){
|
|
106
|
+
const html = '<span class="ag-menu-cap">引擎</span>' + qaEngines.map(x=>
|
|
107
|
+
`<button class="ag-model-opt${x.engine===qaEngine?" on":""}" data-engine="${escHtml(x.engine)}" type="button" role="option" aria-selected="${x.engine===qaEngine}" title="${x.ok ? escHtml(x.engine) : escHtml(x.engine) + "(本机 CLI 不可用)"}">
|
|
108
|
+
<span class="eng-opt${x.ok?"":" off"}"><span class="st"></span><span class="nm">${escHtml(x.engine)}</span>${qaStreamTag(x)}</span>
|
|
109
|
+
</button>`).join("");
|
|
110
|
+
qaAskEach("-engine-menu", m=>{ m.innerHTML = html; });
|
|
111
|
+
qaMenuReplace();
|
|
112
|
+
}
|
|
113
|
+
/** 模型菜单:首项「自动」(按配置链的链首),其后是该引擎探测到的模型。原样搬自上游。 */
|
|
114
|
+
function qaMdlMenuBuild(){
|
|
115
|
+
const auto = qaModelAuto();
|
|
116
|
+
const e = qaEngines.find(x=>x.engine === qaEngine) || null;
|
|
117
|
+
const labels = (e && e.modelLabels) || {};
|
|
118
|
+
const credits = (e && e.modelCredits) || {};
|
|
119
|
+
const crOf = (id)=>{ const v = String(credits[id] || "").trim(); return v ? "x" + v : ""; };
|
|
120
|
+
const ids = (e && Array.isArray(e.models) ? e.models : []).map(m=>String(m||"")).filter(Boolean);
|
|
121
|
+
const autoName = auto ? qaModelName(auto, qaEngine) : "引擎默认";
|
|
122
|
+
const autoCr = auto ? crOf(auto) : "";
|
|
123
|
+
const opts = [{ id:"", name:"自动", note: autoCr ? autoName + " · " + autoCr : autoName }]
|
|
124
|
+
.concat(ids.map(id=>{
|
|
125
|
+
const bits = [];
|
|
126
|
+
if(auto === id) bits.push("链首");
|
|
127
|
+
const cr = crOf(id);
|
|
128
|
+
if(cr) bits.push(cr);
|
|
129
|
+
return { id, name: labels[id] || id, note: bits.join(" · ") };
|
|
130
|
+
}));
|
|
131
|
+
const html = '<span class="ag-menu-cap">模型</span>' + opts.map(m=>
|
|
132
|
+
`<button class="ag-model-opt" data-model="${escHtml(m.id||"")}" type="button" role="option" aria-selected="false" title="${escHtml(m.id ? ("指定模型 · 下发给 -m " + m.id) : ("自动 · 按设置里「" + qaEngine + "」的模型链(链首 " + autoName + ")"))}">
|
|
133
|
+
<span class="mdl-opt"><span class="nm">${escHtml(m.name)}</span>${m.note ? `<span class="dm">${escHtml(m.note)}</span>` : ""}</span>
|
|
134
|
+
</button>`).join("");
|
|
135
|
+
qaAskEach("-model-menu", m=>{ m.innerHTML = html; });
|
|
136
|
+
qaModelPickSync();
|
|
137
|
+
qaMenuReplace();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* ---------- 三处同步出口(与上游同名同义) ---------- */
|
|
141
|
+
|
|
142
|
+
/** 某实例「当前选中了什么模型」(`""` = 自动)。首页看全局选择、任务对话看这条任务。 */
|
|
143
|
+
function qaModelPickOf(p){
|
|
144
|
+
if(p === "home2") return String(qaModelPick || "");
|
|
145
|
+
const t = Array.isArray(qaTasks) ? (qaTasks.find(x=>x.id === qaActiveId) || null) : null;
|
|
146
|
+
return (t && t.model) ? String(t.model) : String(qaModelPick || "");
|
|
147
|
+
}
|
|
148
|
+
/** 模型下拉按钮文案 + 菜单选中态 —— **唯一**同步出口(两个实例各算一次)。原样搬自上游。 */
|
|
149
|
+
function qaModelPickSync(){
|
|
150
|
+
const auto = qaModelAuto();
|
|
151
|
+
qaAskEach("-model", (b, p)=>{
|
|
152
|
+
const picked = qaModelPickOf(p);
|
|
153
|
+
const show = (typeof qaModelFor === "function") ? qaModelFor(p, qaActiveId) : picked;
|
|
154
|
+
const t = b.querySelector(".txt");
|
|
155
|
+
if(t) t.textContent = show ? qaModelName(show, qaEngine) : "引擎默认";
|
|
156
|
+
b.dataset.model = picked;
|
|
157
|
+
b.title = picked
|
|
158
|
+
? `本任务用「${qaModelName(picked, qaEngine)}」(下发给 -m ${picked})· 点开可改,或切回自动`
|
|
159
|
+
: `自动 · 按设置里「${qaEngine}」的模型链(链首 ${auto ? qaModelName(auto, qaEngine) : "—"})`;
|
|
160
|
+
});
|
|
161
|
+
qaAskEach("-model-menu", (m, p)=>{
|
|
162
|
+
const picked = qaModelPickOf(p);
|
|
163
|
+
m.querySelectorAll(".ag-model-opt").forEach(x=>{
|
|
164
|
+
const on = (x.dataset.model||"") === picked;
|
|
165
|
+
x.classList.toggle("on", on);
|
|
166
|
+
x.setAttribute("aria-selected", String(on));
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
window.qaModelPickSync = qaModelPickSync;
|
|
171
|
+
|
|
172
|
+
/** 引擎按钮文案 + 菜单选中态 + 引擎提示行 + 模型下拉 + 权限胶囊 —— **唯一**同步出口。
|
|
173
|
+
* 上游还在这里管「对话进行中禁用引擎」的 title,本版没做那层锁(见增强点)。 */
|
|
174
|
+
function qaPickerSync(){
|
|
175
|
+
qaAskEach("-engine", b=>{ const t = b.querySelector(".txt"); if(t) t.textContent = qaEngine; });
|
|
176
|
+
qaAskEach("-engine-menu", m=>{ m.querySelectorAll(".ag-model-opt").forEach(x=>{
|
|
177
|
+
const on = x.dataset.engine === qaEngine;
|
|
178
|
+
x.classList.toggle("on", on);
|
|
179
|
+
x.setAttribute("aria-selected", String(on));
|
|
180
|
+
}); });
|
|
181
|
+
qaEngHintSync();
|
|
182
|
+
qaModelPickSync();
|
|
183
|
+
if(typeof qaPermSync === "function") qaPermSync();
|
|
184
|
+
}
|
|
185
|
+
window.qaPickerSync = qaPickerSync;
|
|
186
|
+
|
|
187
|
+
/* ---------- 引擎清单取数 ---------- */
|
|
188
|
+
|
|
189
|
+
/* 引擎清单取数。`qaEnginesReal` 声明在 91-workbench-stubs.js(04 的 qaPermSupported 要读它)。 */
|
|
190
|
+
/** 取引擎清单并重建两只菜单。返回 Promise<boolean>(true = 拿到真实清单)。 */
|
|
191
|
+
function qaEnginesFetch(fresh){
|
|
192
|
+
return window.desk.engines(fresh ? { fresh:true } : undefined)
|
|
193
|
+
.then(r=> (r && r.ok && Array.isArray(r.engines) && r.engines.length) ? r.engines : null)
|
|
194
|
+
.catch(()=>null)
|
|
195
|
+
.then(list=>{
|
|
196
|
+
if(!list) return false;
|
|
197
|
+
qaEngines = list;
|
|
198
|
+
qaEnginesReal = true;
|
|
199
|
+
if(!qaEngines.some(x=>x.engine === qaEngine)){ // 记住的引擎已不在清单 → 回首个可用
|
|
200
|
+
qaEngine = qaEngines[0].engine;
|
|
201
|
+
try{ localStorage.setItem("qaEngine", qaEngine); }catch(e){}
|
|
202
|
+
}
|
|
203
|
+
/* 引擎定下来就重取该引擎的模型链(「自动」档用的就是链首)并重建模型菜单。 */
|
|
204
|
+
qaModelChainLoad(qaEngine, true);
|
|
205
|
+
qaEngMenuBuild();
|
|
206
|
+
qaMdlMenuBuild();
|
|
207
|
+
/* ⚠️ 权限菜单也要重建:底部那行「该引擎不接受权限档位」是按**当前引擎**拼的
|
|
208
|
+
(qaPermNoteHtml),只在 qaPermMenuBuild 里现填。清单是异步到的 —— 不重建的话,
|
|
209
|
+
首帧(还拿着兜底表、不发这行)那次构建会一直留在 DOM 里,引擎换成不支持档位的
|
|
210
|
+
那个也看不到说明。上游把清单更早拿到手,所以没这一处。 */
|
|
211
|
+
qaPermMenuBuild();
|
|
212
|
+
qaPickerSync();
|
|
213
|
+
return true;
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
window.__qaEnginesRefresh = ()=>qaEnginesFetch(true);
|
|
217
|
+
|
|
218
|
+
/* ---------- 交互绑定 ---------- */
|
|
219
|
+
|
|
220
|
+
/** 引擎被选中:写状态 + 落 localStorage + 重取链 + 全部重刷。 */
|
|
221
|
+
function qaEnginePick(name){
|
|
222
|
+
if(!name || name === qaEngine) return;
|
|
223
|
+
qaEngine = name;
|
|
224
|
+
try{ localStorage.setItem("qaEngine", qaEngine); }catch(e){}
|
|
225
|
+
qaModelChainLoad(qaEngine, true);
|
|
226
|
+
qaEngMenuBuild();
|
|
227
|
+
qaMdlMenuBuild();
|
|
228
|
+
qaPermMenuBuild(); // 同上:说明行按当前引擎拼,换引擎必须重建
|
|
229
|
+
qaPickerSync();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** 撰写区总装:挂好四只菜单的开关与点选,绑发送主链。`askBoxMountAll` 之后调一次。 */
|
|
233
|
+
function qaComposerBind(){
|
|
234
|
+
ASK_INSTANCES.forEach(p=>{
|
|
235
|
+
const box = $("#" + p + "-box");
|
|
236
|
+
if(!box) return; // 该实例没挂(如只有 #ask-home)
|
|
237
|
+
const pairs = [
|
|
238
|
+
["-plus", "-add-menu", "add"],
|
|
239
|
+
["-access", "-perm-menu", "perm"],
|
|
240
|
+
["-engine", "-engine-menu", "engine"],
|
|
241
|
+
["-model", "-model-menu", "model"],
|
|
242
|
+
];
|
|
243
|
+
pairs.forEach(([bs, ms, kind])=>{
|
|
244
|
+
const btn = $("#" + p + bs), menu = $("#" + p + ms);
|
|
245
|
+
if(!btn || !menu) return;
|
|
246
|
+
btn.addEventListener("click", (e)=>{ e.stopPropagation(); qaMenuToggle(btn, menu, p); });
|
|
247
|
+
/* 菜单项点选:菜单内容会被 innerHTML 重建,所以委托在菜单自己身上。 */
|
|
248
|
+
menu.addEventListener("click", (e)=>{
|
|
249
|
+
const o = e.target.closest(".ag-model-opt"); if(!o) return;
|
|
250
|
+
e.stopPropagation();
|
|
251
|
+
if(kind === "add"){ askAddPick(p, o); return; } // 19:挂/摘胶囊(内部自己关菜单)
|
|
252
|
+
if(kind === "perm"){ qaPermSet(o.dataset.perm); qaCloseOne(menu); return; }
|
|
253
|
+
if(kind === "engine"){ qaEnginePick(o.dataset.engine); qaCloseOne(menu); return; }
|
|
254
|
+
/* 模型:写回唯一状态位(首页 = qaModelPick,任务对话 = 这条任务的 model) */
|
|
255
|
+
qaModelSet(o.dataset.model, p, qaActiveId);
|
|
256
|
+
qaModelPickSync();
|
|
257
|
+
qaCloseOne(menu);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
/* 胶囊行上的 × —— 摘掉这枚胶囊 */
|
|
261
|
+
const refs = $("#" + p + "-refs");
|
|
262
|
+
if(refs) refs.addEventListener("click", (e)=>{
|
|
263
|
+
const x = e.target.closest(".rx"); if(!x) return;
|
|
264
|
+
e.stopPropagation();
|
|
265
|
+
askChipDrop(p, x.dataset.kind);
|
|
266
|
+
});
|
|
267
|
+
/* 点菜单外 / Esc 关闭(composedPath:菜单项点选会重绘,closest 判不出「点在菜单外」) */
|
|
268
|
+
if(!qaMenuBound){
|
|
269
|
+
document.addEventListener("click", (e)=>{
|
|
270
|
+
const path = e.composedPath();
|
|
271
|
+
if(path.some(n=>n && n.classList && n.classList.contains("ag-menu"))) return;
|
|
272
|
+
qaMenusClose(null);
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
if(!qaMenuBound){
|
|
277
|
+
qaMenuBound = true;
|
|
278
|
+
document.addEventListener("keydown", (e)=>{
|
|
279
|
+
/* Esc:关菜单并回焦输入框 */
|
|
280
|
+
if(e.key === "Escape"){ qaMenusClose(null); const i = $("#home-input"); if(i) i.focus(); return; }
|
|
281
|
+
const open = [...document.querySelectorAll(".ag-menu")].find(m=>!m.hidden);
|
|
282
|
+
if(!open) return;
|
|
283
|
+
if(e.key === "ArrowDown" || e.key === "ArrowUp"){ e.preventDefault(); qaKbMove(open, e.key === "ArrowDown" ? 1 : -1); return; }
|
|
284
|
+
if(e.key === "Enter"){
|
|
285
|
+
const cur = open.querySelector(".ag-model-opt.kb");
|
|
286
|
+
if(cur){ e.preventDefault(); cur.click(); }
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
/* 滚动 / 缩放后重排(自研版没有 autoUpdate,退化成一次性重算) */
|
|
290
|
+
window.addEventListener("resize", ()=>qaMenuReplace());
|
|
291
|
+
window.addEventListener("scroll", ()=>qaMenuReplace(), true);
|
|
292
|
+
}
|
|
293
|
+
bindComposerSend();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** 发送主链:与上游 bindHomeAsk 的「提交 / 回车 / 停止」逐条对齐。
|
|
297
|
+
* ⚠️ 上游还订阅 desk.onAskEvent 把事件喂给渲染管线 —— 那一层在 90-glue.js 的 boot 里挂。 */
|
|
298
|
+
function bindComposerSend(){
|
|
299
|
+
const input = $("#home-input"), sendBtn = $("#home-send"), box = $("#home-box");
|
|
300
|
+
if(!input || !sendBtn){ throw new Error("输入框标记没注入(#home-input / #home-send 找不到)"); }
|
|
301
|
+
const submit = ()=>qaSend(input.value, { followup: true, from: "home" });
|
|
302
|
+
sendBtn.addEventListener("click", async ()=>{
|
|
303
|
+
/* 同一枚钮两态(形态由 qaSendBtnSync 按「输入框有没有内容」落类):运行中 + 空输入 = 停止 */
|
|
304
|
+
if(box && box.classList.contains("is-busy") && !input.value.trim()){ await qaStopActive(); return; }
|
|
305
|
+
await submit();
|
|
306
|
+
});
|
|
307
|
+
input.addEventListener("keydown", (e)=>{
|
|
308
|
+
if(qaImeComposing(e)) return; // 输入法合成中,那一下回车归输入法
|
|
309
|
+
if(e.key === "Enter" && !e.shiftKey){ e.preventDefault(); submit(); }
|
|
310
|
+
});
|
|
311
|
+
input.addEventListener("input", ()=>qaSendBtnSync("home"));
|
|
312
|
+
qaSendBtnSync("home"); // 首帧也对一次
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/* ---------- 流式事件 → 画面 ---------- */
|
|
316
|
+
|
|
317
|
+
/** 订阅一轮里推进来的事件,按 rid 路由到**它那一轮**的 run,再交给 qaStreamApply。
|
|
318
|
+
* 这是「事件 → 画面」的唯一入口 —— 逐字搬自上游 bindHomeAsk 的 handle(只去掉工作台
|
|
319
|
+
* 无关的越权分支)。上游主进程会把同一轮事件**批量合并**成 { rid, batch:[...] },
|
|
320
|
+
* 这里两种形态都收(我们的 desk 适配是逐条推,但别假设宿主一定不批量)。 */
|
|
321
|
+
function qaStreamBind(){
|
|
322
|
+
if(qaUnsub || !(window.desk && typeof window.desk.onAskEvent === "function")) return;
|
|
323
|
+
const handle = (ev)=>{
|
|
324
|
+
const run = qaRuns.get(ev.rid); if(!run) return; // 过期 / 未知会话事件丢弃
|
|
325
|
+
const h = run.h; if(!h) return;
|
|
326
|
+
/* 一轮刚开跑 → 让左栏尽快出现这条会话(新会话此时还没 session_id,收尾时会再刷一次) */
|
|
327
|
+
if (ev.type === "start" && typeof qaRailRefreshSoon === "function") qaRailRefreshSoon();
|
|
328
|
+
const visible = (run.taskId === qaActiveId) && !run.holder; // 这一轮此刻在屏幕上吗
|
|
329
|
+
qaPaintBg = !visible;
|
|
330
|
+
try{
|
|
331
|
+
qaStreamApply(ev, {
|
|
332
|
+
h, tools: run.tools, agents: run.agents,
|
|
333
|
+
rid: run.rid, taskId: run.taskId,
|
|
334
|
+
setMs: (ms)=>{ run.ms = ms; },
|
|
335
|
+
/* 消息台账:主会话正文逐段累加(收尾时落进快照用) */
|
|
336
|
+
onText: (t)=>{ run.msgText = (run.msgText || "") + String(t || ""); },
|
|
337
|
+
/* 过程步骤:思考块 / 工具卡攒着 */
|
|
338
|
+
onStep: (st)=>{
|
|
339
|
+
if(!st) return;
|
|
340
|
+
if(st.type === "tool_result"){
|
|
341
|
+
for(let i = run.logSteps.length - 1; i >= 0; i--){
|
|
342
|
+
const s = run.logSteps[i];
|
|
343
|
+
if(s.type === "tool" && String(s.tid) === String(st.tid)){
|
|
344
|
+
s.result = String(st.text || ""); s.ok = st.ok !== false;
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
run.logSteps.push({ type: "tool", name: "结果回执", args: "", result: String(st.text || ""), ok: st.ok !== false });
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
run.logSteps.push(st);
|
|
352
|
+
},
|
|
353
|
+
onSession: (sid)=>{ qaSessionBindFor(run.taskId, sid); },
|
|
354
|
+
onError: (e)=>{
|
|
355
|
+
/* 显示哪一句:**优先根因**(reason),退回完整错误链(error)——理由同上游 */
|
|
356
|
+
qaRenderDone(false, "调用失败 · " + String(e.reason || e.error || "未知错误").slice(0, 80), qaRunHost(run));
|
|
357
|
+
qaTaskMarkSidLostFor(run.taskId);
|
|
358
|
+
qaSaveSnapshotFor(run.taskId, "error", { holder: run.holder });
|
|
359
|
+
qaRuns.delete(run.rid);
|
|
360
|
+
qaRefreshRunning();
|
|
361
|
+
qaTaskRender();
|
|
362
|
+
qaAskFlush(run.taskId);
|
|
363
|
+
qaMsgFlush(run.taskId);
|
|
364
|
+
/* 左栏跟着刷新:这一轮的会话(含刚拿到 session_id 的新会话)该出现在列表里了 */
|
|
365
|
+
if (typeof qaRailRefreshSoon === "function") qaRailRefreshSoon();
|
|
366
|
+
},
|
|
367
|
+
onDone: (e)=>{
|
|
368
|
+
const ms = run.ms;
|
|
369
|
+
qaRenderDone(!e.stopped, e.stopped ? "已停止" : "已完成" + (ms != null ? ` · ${(ms/1000).toFixed(1)}s` : ""), qaRunHost(run));
|
|
370
|
+
run.ms = null;
|
|
371
|
+
qaTaskMarkSidLostFor(run.taskId);
|
|
372
|
+
qaSaveSnapshotFor(run.taskId, "done", { holder: run.holder });
|
|
373
|
+
qaRuns.delete(run.rid);
|
|
374
|
+
qaRefreshRunning();
|
|
375
|
+
qaTaskRender();
|
|
376
|
+
qaAskFlush(run.taskId);
|
|
377
|
+
qaMsgFlush(run.taskId);
|
|
378
|
+
/* 左栏跟着刷新:这一轮的会话(含刚拿到 session_id 的新会话)该出现在列表里了 */
|
|
379
|
+
if (typeof qaRailRefreshSoon === "function") qaRailRefreshSoon();
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
} finally { qaPaintBg = false; }
|
|
383
|
+
if(visible) qaScrollIntoView();
|
|
384
|
+
};
|
|
385
|
+
qaUnsub = window.desk.onAskEvent((p)=>{
|
|
386
|
+
if(p && Array.isArray(p.batch)){ p.batch.forEach(handle); return; }
|
|
387
|
+
handle(p);
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** 启动总装(由 90-glue.js 的 boot 调用,在 askBoxMountAll 之后)。 */
|
|
392
|
+
function qaComposerInit(){
|
|
393
|
+
qaPermMenuBuild();
|
|
394
|
+
qaPermSync();
|
|
395
|
+
qaEngMenuBuild();
|
|
396
|
+
qaMdlMenuBuild();
|
|
397
|
+
qaComposerBind();
|
|
398
|
+
qaStreamBind();
|
|
399
|
+
/* ⚠️ 画布上的**事件委托**必须挂(上游在 17-bind.js 的 bind() 里挂,本版没有那个文件):
|
|
400
|
+
工具卡 / 思考条的展开收起、提问卡的选项点选与「其他」确认全靠它 ——
|
|
401
|
+
漏了它的表现是「卡片画出来了但点不动」,而且不报错(绑定函数自己只是加监听)。
|
|
402
|
+
`bindNavTasks()` 是左栏那一套(本版没有左栏),刻意不调。 */
|
|
403
|
+
if(typeof bindQaStreamDelegation === "function") bindQaStreamDelegation();
|
|
404
|
+
/* 引擎清单异步取回后再重刷一次(拿不到就保持兜底清单,界面不开天窗) */
|
|
405
|
+
qaEnginesFetch(false);
|
|
406
|
+
}
|