mingdao-harness 0.2.6 → 0.2.8
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/docs/CONFIG.md +5 -5
- package/docs/QA-REPORT.md +2 -0
- package/package.json +2 -2
- package/skills/release-checklist/SKILL.md +5 -4
- package/src/agent.js +52 -1
- package/src/cli.js +18 -3
- package/src/commands/repl.js +8 -5
- package/src/log-writer.js +5 -2
- package/src/pricing.js +10 -1
- package/src/prompts.js +1 -1
- package/src/schedule.js +3 -1
- package/src/session-index.js +130 -41
- package/src/tools/fs-tools.js +2 -1
- package/src/tools/index.js +4 -4
- package/src/web/app.js +79 -84
- package/src/web/attachments.js +3 -4
- package/src/web/constants.js +9 -0
- package/src/web/index.html +23 -9
- package/src/web/routes/api.js +9 -7
- package/src/web/routes/domains/config.js +20 -2
- package/src/web/server.js +9 -4
- package/src/web/util.js +201 -0
package/src/web/app.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { $, esc, highlight, renderMarkdown, scrollBottom, expandableBody, resultText, truncText, fmtDur, fmtTok, fmtT } from './util.js';
|
|
2
|
+
import { MAX_ATTACHMENTS, MAX_IMAGE_BYTES, MAX_TEXT_BYTES } from './constants.js';
|
|
3
|
+
|
|
1
4
|
// 访问令牌(P1-3):地址带 ?token= 时记入 sessionStorage 并从地址栏移除(防截图/历史外泄),
|
|
2
5
|
// 之后所有同源请求统一附加 X-MingDao-Token 头;无令牌时行为与旧版一致。
|
|
3
6
|
const AUTH_TOKEN = (() => {
|
|
@@ -18,7 +21,52 @@ if (AUTH_TOKEN) {
|
|
|
18
21
|
return rawFetch(input, init);
|
|
19
22
|
};
|
|
20
23
|
}
|
|
21
|
-
|
|
24
|
+
|
|
25
|
+
// —— 自绘悬浮气泡 tooltip(替换原生 title:出现快、样式与暗色主题一致) ——
|
|
26
|
+
let tipEl = null;
|
|
27
|
+
function ensureTip() {
|
|
28
|
+
if (!tipEl) {
|
|
29
|
+
tipEl = document.createElement('div');
|
|
30
|
+
tipEl.className = 'mdtip';
|
|
31
|
+
document.body.appendChild(tipEl);
|
|
32
|
+
}
|
|
33
|
+
return tipEl;
|
|
34
|
+
}
|
|
35
|
+
function showTip(text, anchor) {
|
|
36
|
+
if (!text || !anchor) return;
|
|
37
|
+
const t = ensureTip();
|
|
38
|
+
t.textContent = text;
|
|
39
|
+
t.style.display = 'block';
|
|
40
|
+
const r = anchor.getBoundingClientRect();
|
|
41
|
+
const w = t.offsetWidth, h = t.offsetHeight;
|
|
42
|
+
let left = r.left + r.width / 2 - w / 2;
|
|
43
|
+
left = Math.max(8, Math.min(left, window.innerWidth - w - 8));
|
|
44
|
+
let top = r.top - h - 8;
|
|
45
|
+
if (top < 8) top = r.bottom + 8;
|
|
46
|
+
t.style.left = left + 'px';
|
|
47
|
+
t.style.top = top + 'px';
|
|
48
|
+
}
|
|
49
|
+
function hideTip() { if (tipEl) tipEl.style.display = 'none'; }
|
|
50
|
+
function attachTip(el, text) {
|
|
51
|
+
if (!el || !text) return;
|
|
52
|
+
el.removeAttribute('title'); // 去掉原生 title,避免双提示
|
|
53
|
+
const get = typeof text === 'function' ? text : () => text;
|
|
54
|
+
el.addEventListener('mouseenter', () => showTip(get(), el));
|
|
55
|
+
el.addEventListener('mouseleave', hideTip);
|
|
56
|
+
el.addEventListener('focus', () => showTip(get(), el));
|
|
57
|
+
el.addEventListener('blur', hideTip);
|
|
58
|
+
el.addEventListener('mousedown', hideTip); // 打开下拉/点击时隐藏,避免遮挡
|
|
59
|
+
}
|
|
60
|
+
window.addEventListener('scroll', hideTip, true);
|
|
61
|
+
|
|
62
|
+
// 输入区下拉 / 附件按钮:把原生 title 换成自绘气泡(文本取自原 title,一次性读取后移除)
|
|
63
|
+
function initTips() {
|
|
64
|
+
const perm = $('#permSel'); if (perm) attachTip(perm, perm.getAttribute('title'));
|
|
65
|
+
const reas = $('#reasoningSel'); if (reas) attachTip(reas, reas.getAttribute('title'));
|
|
66
|
+
const model = $('#modelSel'); if (model) attachTip(model, () => { const o = model.options[model.selectedIndex]; return (o && o.title) ? o.title : '切换模型'; });
|
|
67
|
+
const at = $('#attachBtn'); if (at) attachTip(at, at.getAttribute('title'));
|
|
68
|
+
}
|
|
69
|
+
initTips();
|
|
22
70
|
|
|
23
71
|
// —— 弹窗三件套(Electron 不实现 window.prompt/confirm/alert:prompt 恒返回 null、confirm 恒 false、
|
|
24
72
|
// alert 静默无反应——桌面版「⚙ 设置里点设Key 没反应」即由此而来)。统一替换为应用内模态框。
|
|
@@ -101,6 +149,7 @@ $('#dirPickNone').onclick = () => { const cb = pickerCb; pickerCb = null; $('#di
|
|
|
101
149
|
$('#dirPickCancel').onclick = () => { pickerCb = null; $('#dirModal').style.display = 'none'; };
|
|
102
150
|
const chatEl = $('#chat'), input = $('#input'), sendBtn = $('#sendBtn');
|
|
103
151
|
let activeAiMsg=null, bgRunning=0, curSteps=0, curWorkT0=0, curTools=0, curTasks=0; // 本轮进度(活动条/状态条/轨迹共用)
|
|
152
|
+
let bgTasks=[]; // 后台任务列表快照(chip tooltip 详情用,updateTasksPanel 每 2s 刷新)
|
|
104
153
|
let curPhase='模型推理中'; // 阶段语义(服务端 progress 事件下发)
|
|
105
154
|
const sessionSubs=[]; // 本会话全部子代理(task 工具)条目:{seq, question, result, msg}
|
|
106
155
|
// 回到底部悬浮按钮:滚动容器为 main;上滚超过 300px 时出现,点击平滑回底
|
|
@@ -124,61 +173,7 @@ chatEl.addEventListener('click', (e) => {
|
|
|
124
173
|
let generating = false; // 生成中:发送按钮复用为停止按钮
|
|
125
174
|
let currentSession = null, thinking = null;
|
|
126
175
|
|
|
127
|
-
function esc(s){return String(s??'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''')}
|
|
128
176
|
|
|
129
|
-
function highlight(code, lang){
|
|
130
|
-
const kws = new Set('const let var function return if else for while import from export class new await async try catch throw null undefined true false def elif raise with as lambda None True False self echo exit if then else fi done local function'.split(' '));
|
|
131
|
-
const re = /(\/\/[^\n]*|#[^\n]*|"(?:\\.|[^"\\])*"|'[^']*'|`[^`]*`|\b\d[\d._]*\b|[A-Za-z_$][\w$]*)/g;
|
|
132
|
-
return esc(code).replace(re, (tok)=>{
|
|
133
|
-
if(tok.startsWith('//')||tok.startsWith('#')) return '<span class="hl-c">'+tok+'</span>';
|
|
134
|
-
if(/^["'`]/.test(tok)) return '<span class="hl-s">'+tok+'</span>';
|
|
135
|
-
if(/^\d/.test(tok)) return '<span class="hl-n">'+tok+'</span>';
|
|
136
|
-
if(kws.has(tok)) return '<span class="hl-kw">'+tok+'</span>';
|
|
137
|
-
return tok;
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
function renderMarkdown(text){
|
|
141
|
-
// 块级解析(美化:加粗标题 / 段落 / 真列表 / 引用 / 分割线,参照 DeepSeek-Harness 会话排版)
|
|
142
|
-
const lines = String(text).split('\n');
|
|
143
|
-
let out = '', code = null, codeLang = '';
|
|
144
|
-
const flushCode = () => { if (code !== null) { out += '<div class="codeblock"><div class="cb-banner"><span class="cb-lang"><span class="cb-dot"></span>' + (esc(codeLang) || 'code') + '</span><span class="cb-copy" style="cursor:pointer">复制</span></div><pre><code class="lang-' + esc(codeLang) + '">' + highlight(code.join('\n'), codeLang) + '</code></pre></div>'; code = null; } };
|
|
145
|
-
let para = [];
|
|
146
|
-
let list = null; // {type:'ul'|'ol', items:[]}
|
|
147
|
-
const flushPara = () => { if (para.length) { out += '<p>' + para.join('<br>') + '</p>'; para = []; } };
|
|
148
|
-
const flushList = () => { if (list) { out += '<' + list.type + '>' + list.items.map((i) => '<li>' + i + '</li>').join('') + '</' + list.type + '>'; list = null; } };
|
|
149
|
-
// 行内元素(审计 P1-3:内容与 codeLang 均经 esc 转义防 XSS)
|
|
150
|
-
const inline = (l) => {
|
|
151
|
-
let s = esc(l);
|
|
152
|
-
s = s.replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
153
|
-
.replace(/\*\*([^*]+)\*\*/g, '<b>$1</b>')
|
|
154
|
-
.replace(/\*([^*\s][^*]*)\*/g, '<i>$1</i>')
|
|
155
|
-
.replace(/\[([^\]]+)\]\((https?:\/\/[^\s'"<>)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
|
|
156
|
-
return s;
|
|
157
|
-
};
|
|
158
|
-
for (const line of lines) {
|
|
159
|
-
const t = line.trim();
|
|
160
|
-
if (t.startsWith('```')) {
|
|
161
|
-
if (code === null) { flushList(); flushPara(); code = []; codeLang = (t.slice(3).trim().split(/\s+/)[0] || ''); }
|
|
162
|
-
else flushCode();
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
if (code !== null) { code.push(line); continue; }
|
|
166
|
-
if (t === '') { flushList(); flushPara(); continue; }
|
|
167
|
-
const h = t.match(/^(#{1,6})\s+(.*)$/);
|
|
168
|
-
if (h) { flushList(); flushPara(); const level = Math.min(h[1].length, 4); out += '<h' + level + '>' + inline(h[2]) + '</h' + level + '>'; continue; }
|
|
169
|
-
const ul = t.match(/^([-*+])\s+(.*)$/);
|
|
170
|
-
if (ul) { flushPara(); if (!list || list.type !== 'ul') { flushList(); list = { type: 'ul', items: [] }; } list.items.push(inline(ul[2])); continue; }
|
|
171
|
-
const ol = t.match(/^(\d+)[.)]\s+(.*)$/);
|
|
172
|
-
if (ol) { flushPara(); if (!list || list.type !== 'ol') { flushList(); list = { type: 'ol', items: [] }; } list.items.push(inline(ol[2])); continue; }
|
|
173
|
-
if (/^>\s?/.test(t)) { flushList(); flushPara(); out += '<blockquote>' + inline(t.replace(/^>\s?/, '')) + '</blockquote>'; continue; }
|
|
174
|
-
if (/^(-{3,}|\*{3,})$/.test(t)) { flushList(); flushPara(); out += '<hr>'; continue; }
|
|
175
|
-
flushList();
|
|
176
|
-
para.push(inline(t));
|
|
177
|
-
}
|
|
178
|
-
flushCode(); flushList(); flushPara();
|
|
179
|
-
return out;
|
|
180
|
-
}
|
|
181
|
-
function scrollBottom(){ const sc=document.querySelector('main'); if(sc) sc.scrollTop = sc.scrollHeight; }
|
|
182
177
|
|
|
183
178
|
function newAiMsg(){ const el=document.createElement('div'); el.className='msg-ai'; chatEl.appendChild(el); scrollBottom(); return el; }
|
|
184
179
|
function aiContent(msg){ const c=msg.querySelector('.content'); return c || (()=>{const d=document.createElement('div');d.className='content';msg.appendChild(d);return d;})(); }
|
|
@@ -186,15 +181,6 @@ function addReasoning(msg){ const d=document.createElement('details'); d.classNa
|
|
|
186
181
|
|
|
187
182
|
function addUser(text){ const el=document.createElement('div'); el.className='msg-user'; el.innerHTML='<div class="bubble">'+esc(text)+'</div>'; chatEl.appendChild(el); scrollBottom(); }
|
|
188
183
|
|
|
189
|
-
function expandableBody(previewHtml, fullHtml){
|
|
190
|
-
const b=document.createElement('div'); b.className='body';
|
|
191
|
-
const prev=document.createElement('div'); const full=document.createElement('div');
|
|
192
|
-
prev.innerHTML=previewHtml; full.innerHTML=fullHtml; full.style.display='none';
|
|
193
|
-
const btn=document.createElement('button'); btn.textContent='展开全文'; btn.style.cssText='padding:1px 8px;font-size:11px;margin-top:4px';
|
|
194
|
-
btn.onclick=()=>{ const open=full.style.display!=='none'; full.style.display=open?'none':'block'; btn.textContent=open?'展开全文':'收起'; scrollBottom(); };
|
|
195
|
-
b.appendChild(prev); b.appendChild(full); b.appendChild(btn);
|
|
196
|
-
return b;
|
|
197
|
-
}
|
|
198
184
|
const runningTools = new Map();
|
|
199
185
|
function renderToolStartEvent(ev){
|
|
200
186
|
const card=document.createElement('div'); card.className='tool';
|
|
@@ -301,13 +287,13 @@ let attachments=[];
|
|
|
301
287
|
$('#attachBtn').onclick=()=>{ $('#fileInput').click(); };
|
|
302
288
|
$('#fileInput').addEventListener('change', async e=>{
|
|
303
289
|
for(const f of e.target.files||[]){
|
|
304
|
-
if(attachments.length>=
|
|
290
|
+
if(attachments.length>=MAX_ATTACHMENTS){ uiAlert('最多 '+MAX_ATTACHMENTS+' 个附件'); break; }
|
|
305
291
|
if(f.type.startsWith('image/')){
|
|
306
|
-
if(f.size>
|
|
292
|
+
if(f.size>MAX_IMAGE_BYTES){ uiAlert(f.name+' 超过 5MB'); continue; }
|
|
307
293
|
const dataUrl=await new Promise(r=>{ const fr=new FileReader(); fr.onload=()=>r(fr.result); fr.readAsDataURL(f); });
|
|
308
294
|
attachments.push({type:'image',name:f.name,dataUrl});
|
|
309
295
|
}else if(f.type.startsWith('text/')||/\.(txt|md|json|js|py|log|csv|html|css)$/i.test(f.name)){
|
|
310
|
-
if(f.size>
|
|
296
|
+
if(f.size>MAX_TEXT_BYTES){ uiAlert(f.name+' 超过 200KB'); continue; }
|
|
311
297
|
const content=await new Promise(r=>{ const fr=new FileReader(); fr.onload=()=>r(fr.result); fr.readAsText(f); });
|
|
312
298
|
attachments.push({type:'text',name:f.name,content});
|
|
313
299
|
}else{
|
|
@@ -419,15 +405,20 @@ function renderWorkStatus(){
|
|
|
419
405
|
renderLiveBar();
|
|
420
406
|
const el=$('#workStatus'); if(!el) return;
|
|
421
407
|
let html='';
|
|
408
|
+
let tip='';
|
|
422
409
|
if(generating){
|
|
423
410
|
const secs=curWorkT0?Math.round((Date.now()-curWorkT0)/1000):0;
|
|
424
|
-
html='<span class="ws-busy"><span class="spinner"></span>⏳ '+curSteps+' 步 · '+Math.floor(secs/60)+' 分 '+Math.round(secs%60)+' 秒 · '+curTools+'
|
|
411
|
+
html='<span class="ws-busy"><span class="spinner"></span><span class="ws-phase">'+esc(curPhase)+'</span><span>⏳ 第 '+curSteps+' 步 · '+Math.floor(secs/60)+' 分 '+Math.round(secs%60)+' 秒 · '+curTools+' 工具步'+(curTasks>0?' · '+curTasks+' 个子代理':'')+'</span></span>';
|
|
425
412
|
} else if(bgRunning>0){
|
|
426
|
-
|
|
413
|
+
// 后台任务 chip(非顶部,位于输入框上方):计数 + 最新任务 + 悬浮详情 tooltip,点击打开详情面板
|
|
414
|
+
const running=bgTasks.filter((/** @type {any} */ t)=>t.status==='running');
|
|
415
|
+
const latest=running.length?running[running.length-1]:null;
|
|
416
|
+
tip=bgTasks.map((/** @type {any} */ t)=>(t.kind==='schedule'?'⏰':'🛠')+' '+String(t.message||t.id||'').slice(0,40)+' · '+t.status).join('\n');
|
|
417
|
+
html='<span class="ws-bg">🛠 '+bgRunning+' 个后台任务运行中'+(latest?' · '+esc(String(latest.message||'').slice(0,22)):'')+' — 点击查看详情</span>';
|
|
427
418
|
}
|
|
428
419
|
el.style.display=html?'flex':'none';
|
|
429
420
|
el.innerHTML=html;
|
|
430
|
-
const bg=el.querySelector('.ws-bg'); if(bg) bg.onclick=()=>{ const tp=$('#tasksPanel'); tp.style.display = tp.style.display==='none'?'flex':'none'; if(tp.style.display==='flex'){ $('#trajPanel').style.display='none'; $('#subPanel').style.display='none'; updateTasksPanel(); } syncPanelLayout(); };
|
|
421
|
+
const bg=el.querySelector('.ws-bg'); if(bg){ bg.onclick=()=>{ const tp=$('#tasksPanel'); tp.style.display = tp.style.display==='none'?'flex':'none'; if(tp.style.display==='flex'){ $('#trajPanel').style.display='none'; $('#subPanel').style.display='none'; updateTasksPanel(); } syncPanelLayout(); }; attachTip(bg, tip); }
|
|
431
422
|
}
|
|
432
423
|
// 顶部常驻活动条(静默完美解决层):生成期间始终可见,滚动不影响
|
|
433
424
|
function renderLiveBar(){
|
|
@@ -493,14 +484,6 @@ function syncPanelLayout(){
|
|
|
493
484
|
document.body.classList.toggle('sub-open', open('subPanel'));
|
|
494
485
|
document.body.classList.toggle('tasks-open', open('tasksPanel'));
|
|
495
486
|
}
|
|
496
|
-
function resultText(r){
|
|
497
|
-
if(r==null||r===undefined) return '';
|
|
498
|
-
if(typeof r==='string') return r;
|
|
499
|
-
if(r.output) return String(r.output);
|
|
500
|
-
if(r.stdout||r.stderr) return String(r.stdout||'')+String(r.stderr||'');
|
|
501
|
-
return JSON.stringify(r);
|
|
502
|
-
}
|
|
503
|
-
function truncText(t,n){ t=String(t||''); return t.length>n?t.slice(0,n)+'\n…(截断,共 '+t.length+' 字)':t; }
|
|
504
487
|
$('#tjClose').onclick=()=>{ $('#trajPanel').style.display='none'; $('#trajRailBtn').classList.remove('on'); syncPanelLayout(); };
|
|
505
488
|
$('#trajRailBtn').onclick=()=>{
|
|
506
489
|
const p=$('#trajPanel');
|
|
@@ -553,6 +536,7 @@ async function updateTasksPanel(){
|
|
|
553
536
|
const j=await r.json(); const list=$('#tpList'); list.innerHTML='';
|
|
554
537
|
// 质检(等待状态静默):后台任务(worker/调度)并入计数与面板;状态转换时在聊天区弹可见横幅
|
|
555
538
|
bgRunning=(Number(j.running)||0)+(Number(j.bgRunning)||0);
|
|
539
|
+
bgTasks=j.background||[];
|
|
556
540
|
for(const t of (j.background||[])){
|
|
557
541
|
const prev=lastBgStatus.get(t.id);
|
|
558
542
|
if(prev && prev.status==='running' && t.status!=='running'){
|
|
@@ -591,13 +575,11 @@ async function refreshCostBadge(){
|
|
|
591
575
|
const bd=j.breakdown||{}; const gd=j.guard||null;
|
|
592
576
|
let t='今日 ≈¥'+(bd.today||0).toFixed(4);
|
|
593
577
|
if(bd.rate!=null) t+=' · 命中 '+(bd.rate*100).toFixed(0)+'%';
|
|
594
|
-
if(gd&&gd.limit>0) t+=' · 护栏 '+(gd.cost/gd.limit*100).toFixed(0)+'%';
|
|
578
|
+
if(gd&&gd.limit>0&&gd.cost!=null) t+=' · 护栏 '+(gd.cost/gd.limit*100).toFixed(0)+'%';
|
|
595
579
|
$('#costBadge').textContent=t;
|
|
596
580
|
}
|
|
597
581
|
refreshCostBadge(); setInterval(refreshCostBadge, 15000);
|
|
598
582
|
// 底部状态栏:轮次/步数/LLM 与工具时长/首 token 平均/吞吐/缓存命中/输入输出 tokens
|
|
599
|
-
function fmtDur(ms){ if(!ms) return '0s'; const sec=Math.round(ms/1000); const m=Math.floor(sec/60); return m>0 ? m+'m'+String(sec%60).padStart(2,'0')+'s' : sec+'s'; }
|
|
600
|
-
function fmtTok(n){ if(n>=1e9) return (n/1e9).toFixed(1)+'B'; if(n>=1e6) return (n/1e6).toFixed(0)+'M'; if(n>=1e3) return (n/1e3).toFixed(1)+'K'; return String(n); }
|
|
601
583
|
async function refreshStatusBar(){
|
|
602
584
|
const r=await fetch('/api/cache-stats',{cache:'no-store'}).catch(()=>null); if(!r) return;
|
|
603
585
|
const j=await r.json().catch(()=>null); if(!j) return;
|
|
@@ -614,6 +596,18 @@ $('#tpClose').onclick=()=>{ $('#tasksPanel').style.display='none'; syncPanelLayo
|
|
|
614
596
|
async function refreshSessions(q){ const u=q?'/api/sessions?q='+encodeURIComponent(q):'/api/sessions'; const r=await fetch(u).catch(()=>null); if(!r)return; const j=await r.json(); const sel=$('#sessions'); sel.innerHTML='<option value="">历史会话</option>'; for(const s of j.sessions){ const o=document.createElement('option'); o.value=s.file; o.textContent=s.label; sel.appendChild(o); } if(currentSession&&!q) sel.value=currentSession; }
|
|
615
597
|
let searchTimer=null;
|
|
616
598
|
$('#sessionSearch').addEventListener('input',e=>{ clearTimeout(searchTimer); searchTimer=setTimeout(()=>refreshSessions(e.target.value.trim()),300); });
|
|
599
|
+
// 思考模式 / 推理等级(v0.2.8):与模型选择并列在输入区,按当前模型独立。
|
|
600
|
+
// 仅 reasoning 模型显示该下拉(关/低/高/最高);切换模型时随 /api/state 的 reasoning 字段刷新。
|
|
601
|
+
function applyReasoningUI(reasoning){
|
|
602
|
+
const sel=$('#reasoningSel');
|
|
603
|
+
if(!sel) return;
|
|
604
|
+
const supported = Boolean(reasoning && reasoning.supported);
|
|
605
|
+
sel.style.display = supported ? '' : 'none';
|
|
606
|
+
if(!supported) return;
|
|
607
|
+
const effort = reasoning && reasoning.effort ? reasoning.effort : 'high';
|
|
608
|
+
sel.value = ['off','low','high','max'].includes(effort) ? effort : 'high';
|
|
609
|
+
}
|
|
610
|
+
$('#reasoningSel').onchange=()=>{ applyConfig({reasoningEffort:$('#reasoningSel').value}); };
|
|
617
611
|
async function init(){
|
|
618
612
|
try{
|
|
619
613
|
const r=await fetch('/api/state',{cache:'no-store'}); const j=await r.json();
|
|
@@ -622,7 +616,7 @@ async function init(){
|
|
|
622
616
|
for(const m of (j.models||[])){ const g=m.providerLabel||'其他'; if(!groups[g]){groups[g]=[];order.push(g);} groups[g].push(m); }
|
|
623
617
|
for(const g of order){
|
|
624
618
|
const og=document.createElement('optgroup'); og.label=g;
|
|
625
|
-
for(const m of groups[g]){ const o=document.createElement('option'); o.value=m.name; o.textContent=m.label; if(m.name===j.model) o.selected=true; og.appendChild(o); }
|
|
619
|
+
for(const m of groups[g]){ const o=document.createElement('option'); o.value=m.name; o.textContent=m.name; o.title=m.label; if(m.name===j.model) o.selected=true; og.appendChild(o); }
|
|
626
620
|
ms.appendChild(og);
|
|
627
621
|
}
|
|
628
622
|
if(!(j.models||[]).length){ const o=document.createElement('option'); o.value=j.model; o.textContent=j.model; ms.appendChild(o); }
|
|
@@ -633,6 +627,7 @@ async function init(){
|
|
|
633
627
|
$('#budgetInput').value=j.contextBudget||128000;
|
|
634
628
|
$('#autoStartChk').checked=Boolean(j.autostart);
|
|
635
629
|
$('#notifyChk').checked=j.notify!==false;
|
|
630
|
+
applyReasoningUI(j.reasoning);
|
|
636
631
|
const env = ('路由'+(j.routing?'开':'关')+' · 沙箱'+((j.sandbox&&j.sandbox!=='off')?(j.sandboxSupported?j.sandbox:'降级'):'off'));
|
|
637
632
|
$('#envBadge').textContent=env; $('#envBadge').style.display='';
|
|
638
633
|
// 首次使用引导:无 API Key 时明确提示去设置(桌面版自动初始化后必走这里)
|
|
@@ -773,7 +768,6 @@ $('#memDedupe').onclick=async ()=>{
|
|
|
773
768
|
if(j.ok){ memFlash('✓ 去重完成,移除 '+j.removed+' 行', true); loadMemoryUI(); } else memFlash('✖ '+(j.error||'去重失败'), false);
|
|
774
769
|
};
|
|
775
770
|
// —— 缓存命中率仪表盘 ——
|
|
776
|
-
function fmtT(n){ n=Number(n||0); return n>=1000?(n/1000).toFixed(1)+'k':String(n); }
|
|
777
771
|
// 省钱 B3:费用二级分账面板(模型/工具 Top5 + 近 14 天折线,零依赖 SVG)
|
|
778
772
|
function renderCostBreakdown(bd){
|
|
779
773
|
const models=$('#costTopModels'); models.innerHTML='';
|
|
@@ -898,12 +892,13 @@ async function reloadModels(){
|
|
|
898
892
|
for(const m of (j.models||[])){ const g=m.providerLabel||'其他'; if(!groups[g]){groups[g]=[];order.push(g);} groups[g].push(m); }
|
|
899
893
|
for(const g of order){
|
|
900
894
|
const og=document.createElement('optgroup'); og.label=g;
|
|
901
|
-
for(const m of groups[g]){ const o=document.createElement('option'); o.value=m.name; o.textContent=m.label; if(m.name===j.model) o.selected=true; og.appendChild(o); }
|
|
895
|
+
for(const m of groups[g]){ const o=document.createElement('option'); o.value=m.name; o.textContent=m.name; o.title=m.label; if(m.name===j.model) o.selected=true; og.appendChild(o); }
|
|
902
896
|
ms.appendChild(og);
|
|
903
897
|
}
|
|
904
898
|
if(!(j.models||[]).length){ const o=document.createElement('option'); o.value=j.model; o.textContent=j.model; ms.appendChild(o); }
|
|
905
899
|
const env=('路由'+(j.routing?'开':'关')+' · 沙箱'+((j.sandbox&&j.sandbox!=='off')?(j.sandboxSupported?j.sandbox:'降级'):'off'));
|
|
906
900
|
$('#envBadge').textContent=env; $('#envBadge').style.display='';
|
|
901
|
+
applyReasoningUI(j.reasoning);
|
|
907
902
|
}catch(e){}
|
|
908
903
|
}
|
|
909
904
|
async function refreshModelsCfg(){
|
package/src/web/attachments.js
CHANGED
|
@@ -4,12 +4,11 @@
|
|
|
4
4
|
// - 文本文件:直接拼接进消息文本(≤200KB),并留文件名标注
|
|
5
5
|
// - 返回 { content(模型消息内容:字符串或图文数组), persistText(落盘文本), error }
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
const MAX_TEXT = 200 * 1024;
|
|
7
|
+
import { MAX_ATTACHMENTS, MAX_IMAGE_DATAURL, MAX_TEXT_BYTES } from './constants.js';
|
|
9
8
|
|
|
10
9
|
export function buildUserContent(/** @type {any} */ message, /** @type {any} */ attachments, /** @type {any} */ visionSupported) {
|
|
11
10
|
const text = String(message ?? '').trim();
|
|
12
|
-
const list = Array.isArray(attachments) ? attachments.slice(0,
|
|
11
|
+
const list = Array.isArray(attachments) ? attachments.slice(0, MAX_ATTACHMENTS) : [];
|
|
13
12
|
const imageParts = [];
|
|
14
13
|
const persistParts = [];
|
|
15
14
|
let finalText = text;
|
|
@@ -31,7 +30,7 @@ export function buildUserContent(/** @type {any} */ message, /** @type {any} */
|
|
|
31
30
|
} else if (a.type === 'text') {
|
|
32
31
|
const content = String(a.content ?? '');
|
|
33
32
|
if (!content.trim()) continue;
|
|
34
|
-
if (content.length >
|
|
33
|
+
if (content.length > MAX_TEXT_BYTES) {
|
|
35
34
|
return { error: `文本文件过大:${a.name || '未命名'}(≤200KB)` };
|
|
36
35
|
}
|
|
37
36
|
finalText += `${finalText ? '\n\n' : ''}[文件 ${a.name || '未命名'}]\n${content}`;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// 前端 + 服务端共享常量(零依赖纯 ESM,浏览器与 Node 均可 import)。
|
|
2
|
+
// 单源化(v0.2.8 B2):附件上限 / 并发上限等数值只在此定义,
|
|
3
|
+
// 防 app.js(客户端预检)/ attachments.js(服务端校验)/ fs-tools.js(读取上限)漂移。
|
|
4
|
+
export const MAX_CONCURRENT = 8; // 并发任务上限(server 与任务面板共用)
|
|
5
|
+
export const MAX_ATTACHMENTS = 4; // 单次最多附件数
|
|
6
|
+
export const MAX_IMAGE_BYTES = 5 * 1024 * 1024; // 单张图片原图上限(5MB)
|
|
7
|
+
export const MAX_IMAGE_DATAURL = 7 * 1024 * 1024; // base64 膨胀 1.33 倍后的 dataURL 上限(≈5MB 原图)
|
|
8
|
+
export const MAX_TEXT_BYTES = 200 * 1024; // 文本附件上限(200KB)
|
|
9
|
+
export const MAX_FILE_READ_BYTES = 5 * 1024 * 1024; // read/edit 工具单文件读取上限(独立语义,非附件上限)
|
package/src/web/index.html
CHANGED
|
@@ -62,7 +62,8 @@ header select{flex:none}
|
|
|
62
62
|
#tasksPanel{position:fixed;right:0;top:52px;bottom:0;width:280px;background:var(--bg2);border-left:1px solid var(--border);z-index:40;display:flex;flex-direction:column}
|
|
63
63
|
/* 输入框上方工作状态条(审计:长任务静默硬伤——实时显示执行步数/耗时与后台任务数)。
|
|
64
64
|
与输入框同宽对齐(复用 #composer 的居中容器),不再横贯整个聊天窗口 */
|
|
65
|
-
#workStatus{max-width:
|
|
65
|
+
#workStatus{width:fit-content;max-width:100%;margin:0 auto 4px;display:flex;align-items:center;gap:10px;padding:6px 14px;background:var(--panel);border:1px solid var(--border);border-radius:12px;font-size:12.5px;color:var(--dim);flex:none;overflow:hidden}
|
|
66
|
+
#workStatus .ws-busy{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
66
67
|
/* 状态条 busy 行:承载完整阶段信息(合并原顶部活动条),与输入框同列不遮挡聊天区 */
|
|
67
68
|
#workStatus .ws-phase{color:var(--accent2);font-weight:600;white-space:nowrap}
|
|
68
69
|
/* 左侧轨迹固定栏 + 右侧子代理面板(布局重构:轨迹在左、子代理在右) */
|
|
@@ -93,6 +94,7 @@ header select{flex:none}
|
|
|
93
94
|
#trajPanel{position:fixed;left:42px;top:52px;bottom:0;width:252px;background:var(--bg2);border-right:1px solid var(--border);z-index:40;display:flex;flex-direction:column;box-shadow:4px 0 24px rgba(0,0,0,.35)}
|
|
94
95
|
.tj-head{padding:12px 14px;font-size:14px;font-weight:600;border-bottom:1px solid var(--border);color:var(--accent2);display:flex;align-items:center;gap:8px}
|
|
95
96
|
.tj-head button{margin-left:auto}
|
|
97
|
+
#tjClose,#sbClose{font-size:12px;white-space:nowrap;padding:3px 10px}
|
|
96
98
|
#tjList{flex:1;overflow-y:auto;padding:8px}
|
|
97
99
|
.tj-turn{color:var(--accent2);font-size:12px;font-weight:600;margin:8px 2px 4px}
|
|
98
100
|
.tj-item{padding:6px 10px;border:1px solid var(--border);border-radius:8px;margin-bottom:6px;font-size:12.5px;background:var(--bg3);cursor:pointer}
|
|
@@ -188,10 +190,14 @@ main{flex:1;overflow-y:auto;padding:18px 0 12px;min-width:0}
|
|
|
188
190
|
.errline{color:var(--err);background:rgba(229,83,75,.08);border:1px solid rgba(229,83,75,.3);border-radius:10px;padding:10px 14px;font-size:13.5px}
|
|
189
191
|
footer{flex:none;border-top:1px solid var(--border);background:var(--bg2);padding:12px 18px}
|
|
190
192
|
#composer{max-width:860px;margin:0 auto;display:flex;gap:10px;align-items:flex-end}
|
|
191
|
-
#
|
|
193
|
+
#composer #permSel{max-width:96px;min-width:64px;flex:none;padding:8px 8px}
|
|
194
|
+
#composer #reasoningSel{max-width:64px;min-width:48px;flex:none;padding:8px 6px;font-size:12px}
|
|
195
|
+
#composer #modelSel{max-width:150px;min-width:88px;flex:none;padding:8px 8px;font-size:12px}
|
|
196
|
+
#input{flex:1;background:var(--bg3);border:1px solid var(--border);border-radius:12px;color:var(--text);padding:10px 14px;font:14px/1.6 inherit;resize:none;min-height:44px;max-height:200px;min-width:0}
|
|
192
197
|
#input:focus{outline:none;border-color:var(--accent2)}
|
|
193
198
|
#hint{font-size:11.5px;color:var(--faint);margin-top:6px;max-width:860px;margin-left:auto;margin-right:auto}
|
|
194
199
|
#statusBar{font-size:11.5px;color:var(--faint);margin-top:2px;max-width:860px;margin-left:auto;margin-right:auto;font-variant-numeric:tabular-nums;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
200
|
+
@media (max-width:560px){footer{padding:10px 8px}#composer{gap:6px}#composer #permSel{min-width:52px;padding:8px 4px}#composer #reasoningSel{min-width:44px;padding:8px 4px}#composer #modelSel{min-width:72px;padding:8px 4px}#attachBtn{padding:10px 9px}}
|
|
195
201
|
.modal-mask{position:fixed;inset:0;background:rgba(0,0,0,.55);display:flex;align-items:center;justify-content:center;z-index:50}
|
|
196
202
|
.modal{background:var(--panel);border:1px solid var(--border);border-radius:14px;width:min(520px,92vw);padding:18px;max-height:88vh;overflow-y:auto}
|
|
197
203
|
/* 设置多级菜单(审计:一拉到底改为左侧分组导航 + 右侧面板) */
|
|
@@ -220,17 +226,13 @@ footer{flex:none;border-top:1px solid var(--border);background:var(--bg2);paddin
|
|
|
220
226
|
.spinner{display:inline-block;width:14px;height:14px;border:2px solid var(--accent);border-top-color:transparent;border-radius:50%;animation:spin .8s linear infinite;vertical-align:-2px}
|
|
221
227
|
#scrollBottomBtn{position:fixed;right:26px;bottom:118px;width:40px;height:40px;border-radius:50%;background:linear-gradient(135deg,var(--accent),var(--accent2));color:#06121a;font-size:20px;font-weight:800;border:none;cursor:pointer;box-shadow:0 4px 16px rgba(0,0,0,.45);z-index:30;display:none;align-items:center;justify-content:center}
|
|
222
228
|
@keyframes spin{to{transform:rotate(360deg)}}
|
|
229
|
+
/* 自绘悬浮气泡 tooltip(替换原生 title):暗色悬浮层 + 阴影,跟随锚点居中,越界自动回弹 */
|
|
230
|
+
.mdtip{position:fixed;z-index:1000;max-width:320px;background:var(--bg3);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:7px 11px;font-size:12px;line-height:1.6;box-shadow:0 8px 24px rgba(0,0,0,.55);pointer-events:none;white-space:pre-line;word-break:break-word;display:none}
|
|
223
231
|
</style>
|
|
224
232
|
</head>
|
|
225
233
|
<body>
|
|
226
234
|
<header>
|
|
227
235
|
<div class="logo">MingDao <span>Harness</span></div>
|
|
228
|
-
<select id="modelSel" title="切换模型"></select>
|
|
229
|
-
<select id="permSel" title="权限模式">
|
|
230
|
-
<option value="ask">权限 ask</option>
|
|
231
|
-
<option value="auto">权限 auto</option>
|
|
232
|
-
<option value="readonly">权限 readonly</option>
|
|
233
|
-
</select>
|
|
234
236
|
<span id="cfgMsg" class="badge" style="display:none"></span>
|
|
235
237
|
<button id="cfgBtn" title="设置">⚙</button>
|
|
236
238
|
<select id="wsSel" title="工作空间(切换 / 新建,目录自动创建)"></select>
|
|
@@ -265,6 +267,18 @@ footer{flex:none;border-top:1px solid var(--border);background:var(--bg2);paddin
|
|
|
265
267
|
<div id="composer">
|
|
266
268
|
<button id="attachBtn" title="上传图片或文本文件(图片需视觉模型,如 deepseek-v4-flash-vision-exp)">📎</button>
|
|
267
269
|
<textarea id="input" rows="1" placeholder="输入任务…(Enter 发送,Shift+Enter 换行)"></textarea>
|
|
270
|
+
<select id="permSel" title="权限模式:询问=写文件/执行命令前先确认(推荐) · 自动=全部直接放行 · 只读=禁止修改与执行">
|
|
271
|
+
<option value="ask">询问</option>
|
|
272
|
+
<option value="auto">自动</option>
|
|
273
|
+
<option value="readonly">只读</option>
|
|
274
|
+
</select>
|
|
275
|
+
<select id="modelSel" title="切换模型"></select>
|
|
276
|
+
<select id="reasoningSel" title="思考模式(推理等级):关=不推理省 token · 低 · 高(默认)· 最高=最强推理" style="display:none">
|
|
277
|
+
<option value="off">关</option>
|
|
278
|
+
<option value="low">低</option>
|
|
279
|
+
<option value="high">高</option>
|
|
280
|
+
<option value="max">最高</option>
|
|
281
|
+
</select>
|
|
268
282
|
<button id="sendBtn" class="primary">发送</button>
|
|
269
283
|
</div>
|
|
270
284
|
<div id="hint"><span id="hintText">工具执行与权限确认会实时展示;生成中可点「中断」停止。</span><label style="margin-left:12px;cursor:pointer;user-select:none"><input type="checkbox" id="journalChk" style="vertical-align:-2px;margin:0 4px 0 0">📌 带上文(最近会话日志,默认关=新会话全新开始)</label></div>
|
|
@@ -441,6 +455,6 @@ footer{flex:none;border-top:1px solid var(--border);background:var(--bg2);paddin
|
|
|
441
455
|
</div>
|
|
442
456
|
</div>
|
|
443
457
|
</div>
|
|
444
|
-
<script src="/app.js"></script>
|
|
458
|
+
<script type="module" src="/app.js"></script>
|
|
445
459
|
</body>
|
|
446
460
|
</html>
|
package/src/web/routes/api.js
CHANGED
|
@@ -31,7 +31,7 @@ export function createApiDispatch(deps) {
|
|
|
31
31
|
// 访问控制(P1-3/P1-4):token 校验覆盖数据与操作接口;壳页面与 PWA 静态资源公开
|
|
32
32
|
// (壳不含任何数据,SPA 需要先加载才能读取 ?token=);Host 白名单覆盖一切请求
|
|
33
33
|
const isStaticAsset =
|
|
34
|
-
p === '/' || p === '/index.html' || p === '/app.js' || p === '/favicon.ico' || p === '/icon.svg' || p === '/icon-192.png' || p === '/icon-512.png' || p === '/manifest.webmanifest' || p === '/sw.js';
|
|
34
|
+
p === '/' || p === '/index.html' || p === '/app.js' || p === '/util.js' || p === '/constants.js' || p === '/favicon.ico' || p === '/icon.svg' || p === '/icon-192.png' || p === '/icon-512.png' || p === '/manifest.webmanifest' || p === '/sw.js';
|
|
35
35
|
if (authEnabled && !isStaticAsset && !tokenMatches(requestToken(req, url))) {
|
|
36
36
|
return json(res, 401, { error: '未授权:缺少或无效的访问令牌(地址需带 ?token=…,或请求头携带 X-MingDao-Token)' });
|
|
37
37
|
}
|
|
@@ -60,15 +60,17 @@ export function createApiDispatch(deps) {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
// —— 静态壳资源(公开;不经过域分发) ——
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
// 质检 Q2/S5:SPA 外部 JS——从磁盘读取(与 index.html 同目录)。
|
|
64
|
+
// v0.2.8 C2:app.js 拆分为 ES Modules(app.js / util.js / constants.js),统一从此处伺服。
|
|
65
|
+
const WEB_JS_FILES = new Set(['app.js', 'util.js', 'constants.js']);
|
|
66
|
+
if (method === 'GET' && WEB_JS_FILES.has(p.slice(1))) {
|
|
65
67
|
try {
|
|
66
|
-
const js = fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), '..',
|
|
68
|
+
const js = fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), '..', p.slice(1)), 'utf8');
|
|
67
69
|
res.writeHead(200, { 'Content-Type': 'text/javascript; charset=utf-8', 'Cache-Control': 'no-cache' });
|
|
68
70
|
res.end(js);
|
|
69
71
|
} catch {
|
|
70
72
|
res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
71
|
-
res.end(
|
|
73
|
+
res.end(p.slice(1) + ' 加载失败');
|
|
72
74
|
}
|
|
73
75
|
return;
|
|
74
76
|
}
|
|
@@ -124,8 +126,8 @@ export function createApiDispatch(deps) {
|
|
|
124
126
|
}
|
|
125
127
|
if (method === 'GET' && p === '/sw.js') {
|
|
126
128
|
res.writeHead(200, { 'Content-Type': 'application/javascript; charset=utf-8' });
|
|
127
|
-
// 缓存键去掉 query(?token= 不落缓存);mingdao-
|
|
128
|
-
res.end(`self.addEventListener('install',()=>self.skipWaiting());self.addEventListener('activate',e=>e.waitUntil(caches.keys().then(ks=>Promise.all(ks.filter(k=>k!=='mingdao-
|
|
129
|
+
// 缓存键去掉 query(?token= 不落缓存);mingdao-v5:随 token 认证版本升版本号,强制替换旧 SW 缓存
|
|
130
|
+
res.end(`self.addEventListener('install',()=>self.skipWaiting());self.addEventListener('activate',e=>e.waitUntil(caches.keys().then(ks=>Promise.all(ks.filter(k=>k!=='mingdao-v5').map(k=>caches.delete(k)))).then(()=>clients.claim())));self.addEventListener('fetch',e=>{if(e.request.method==='GET'&&new URL(e.request.url).origin===location.origin&&!e.request.url.includes('/api/')){const u=new URL(e.request.url);u.search='';e.respondWith(fetch(e.request).then(r=>{const c=r.clone();caches.open('mingdao-v5').then(cache=>cache.put(u.toString(),c));return r;}).catch(()=>caches.match(u.toString()).then(m=>m||caches.match('/'))));}});`);
|
|
129
131
|
return;
|
|
130
132
|
}
|
|
131
133
|
|
|
@@ -6,7 +6,7 @@ import { loadConfig, saveConfig } from '../../../config.js';
|
|
|
6
6
|
import { setStoredKey, removeStoredKey, getStoredKey, maskKey } from '../../../credentials.js';
|
|
7
7
|
import { availableModels, fetchProviderModels, providerHasKey } from '../../../model-discovery.js';
|
|
8
8
|
import { createProvider, resolveProviderConfig } from '../../../providers/index.js';
|
|
9
|
-
import { MODELS, PROVIDERS } from '../../../models.js';
|
|
9
|
+
import { MODELS, PROVIDERS, modelPreset } from '../../../models.js';
|
|
10
10
|
import { detectSandbox } from '../../../tools/bash.js';
|
|
11
11
|
import { enableAutostart, disableAutostart, autostartStatus } from '../../../autostart.js';
|
|
12
12
|
import { PRICE_DATA_AS_OF } from '../../../pricing.js';
|
|
@@ -29,6 +29,14 @@ export async function handle({ req, res, method, p, url }, deps, shared) {
|
|
|
29
29
|
.map((s) => ({ file: s.name, mtime: s.mtime, label: `${relativeTime(s.mtime)} · ${sessionPreview(s.file)}` }));
|
|
30
30
|
// 模型列表:只列已设置 Key 的服务商,名称以 /models 接口线上名单为准(预设仅回退与补价)
|
|
31
31
|
const models = await availableModels(cfg, state.modelName);
|
|
32
|
+
// 思考模式 / 推理等级(v0.2.8):当前模型是否支持 reasoning、当前档位与可选档位。
|
|
33
|
+
// 按模型独立:reasoningByModel[当前模型] 覆盖 > 全局 reasoningEffort(旧配置兼容)> 模型预设默认。
|
|
34
|
+
const rp = modelPreset(state.modelName);
|
|
35
|
+
const reasoning = {
|
|
36
|
+
supported: Boolean(rp?.supportsReasoning),
|
|
37
|
+
effort: cfg.reasoningByModel?.[state.modelName] ?? cfg.reasoningEffort ?? rp?.reasoningEffort?.default ?? (rp?.supportsReasoning ? 'high' : 'off'),
|
|
38
|
+
options: rp?.reasoningEffort?.options ?? ['low', 'high', 'max'],
|
|
39
|
+
};
|
|
32
40
|
json(res, 200, {
|
|
33
41
|
ok: true,
|
|
34
42
|
model: state.modelName,
|
|
@@ -40,6 +48,7 @@ export async function handle({ req, res, method, p, url }, deps, shared) {
|
|
|
40
48
|
sandbox: cfg.sandbox || 'off',
|
|
41
49
|
sandboxSupported: detectSandbox() !== 'none',
|
|
42
50
|
routing: cfg.routing?.enabled ? cfg.routing : null,
|
|
51
|
+
reasoning,
|
|
43
52
|
contextBudget: cfg.contextBudget || 128000,
|
|
44
53
|
pricingAsOf: PRICE_DATA_AS_OF,
|
|
45
54
|
autostart: autostartStatus(),
|
|
@@ -103,6 +112,15 @@ export async function handle({ req, res, method, p, url }, deps, shared) {
|
|
|
103
112
|
next.contextBudget = n;
|
|
104
113
|
cfg.contextBudget = n;
|
|
105
114
|
}
|
|
115
|
+
if (body.reasoningEffort !== undefined) {
|
|
116
|
+
const re = String(body.reasoningEffort);
|
|
117
|
+
if (!['off', 'low', 'high', 'max'].includes(re)) {
|
|
118
|
+
return json(res, 400, { error: '思考强度必须是 off / low / high / max' });
|
|
119
|
+
}
|
|
120
|
+
// 按模型独立:写入当前模型的覆盖档位(不污染其他模型)
|
|
121
|
+
cfg.reasoningByModel = cfg.reasoningByModel || {};
|
|
122
|
+
cfg.reasoningByModel[state.modelName] = re;
|
|
123
|
+
}
|
|
106
124
|
let autostartChanged = false;
|
|
107
125
|
if (body.autostart !== undefined) {
|
|
108
126
|
autostartChanged = true;
|
|
@@ -121,7 +139,7 @@ export async function handle({ req, res, method, p, url }, deps, shared) {
|
|
|
121
139
|
cfg.model = next.model;
|
|
122
140
|
cfg.permission = next.permission;
|
|
123
141
|
saveConfig(cfg);
|
|
124
|
-
json(res, 200, { ok: true, model: state.modelName, permission: cfg.permission, sandbox: cfg.sandbox, routing: cfg.routing?.enabled, contextBudget: cfg.contextBudget, autostart: autostartChanged ? autostartStatus() : undefined, notify: cfg.notify !== false });
|
|
142
|
+
json(res, 200, { ok: true, model: state.modelName, permission: cfg.permission, sandbox: cfg.sandbox, routing: cfg.routing?.enabled, contextBudget: cfg.contextBudget, reasoningEffort: cfg.reasoningByModel?.[state.modelName] ?? cfg.reasoningEffort, autostart: autostartChanged ? autostartStatus() : undefined, notify: cfg.notify !== false });
|
|
125
143
|
return true;
|
|
126
144
|
}
|
|
127
145
|
|
package/src/web/server.js
CHANGED
|
@@ -25,6 +25,7 @@ import { createProvider, resolveProviderConfig, helperProvider } from '../provid
|
|
|
25
25
|
import { MODELS, modelPreset, PROVIDERS } from '../models.js';
|
|
26
26
|
import { routeTask, routingConfig } from '../routing.js';
|
|
27
27
|
import { buildUserContent } from './attachments.js';
|
|
28
|
+
import { MAX_CONCURRENT } from './constants.js';
|
|
28
29
|
import { createAgent } from '../agent.js';
|
|
29
30
|
import { createPermission } from '../permissions.js';
|
|
30
31
|
import { buildSystemPrompt } from '../prompts.js';
|
|
@@ -195,11 +196,16 @@ export async function runWebServer({ host = '127.0.0.1', port = 3820, authToken
|
|
|
195
196
|
/** @type {any} */
|
|
196
197
|
let mcpManager = null;
|
|
197
198
|
if (cfg.mcpServers && Object.keys(cfg.mcpServers).length) {
|
|
199
|
+
// 超时后输家 promise 仍在跑:迟到就绪的 manager 立即 stop,防 detached 子进程成孤儿(自查 #2)
|
|
200
|
+
const mcpStartP = startMcpServers(cfg.mcpServers, workingDir).catch(() => null);
|
|
198
201
|
mcpManager = await Promise.race([
|
|
199
|
-
|
|
202
|
+
mcpStartP,
|
|
200
203
|
new Promise((/** @type {any} */ r) => setTimeout(() => r(null), 6000)),
|
|
201
204
|
]);
|
|
202
|
-
if (!mcpManager)
|
|
205
|
+
if (!mcpManager) {
|
|
206
|
+
console.error('[MingDao] ⚠ MCP 连接超时(6s):本会话不注入 MCP 工具(重启 mingdao web 可重试)');
|
|
207
|
+
mcpStartP.then((/** @type {any} */ m) => { if (m) m.stop(); });
|
|
208
|
+
}
|
|
203
209
|
}
|
|
204
210
|
const mcpFacade = {
|
|
205
211
|
toolSchemas: () => (mcpManager ? mcpManager.toolSchemas() : []),
|
|
@@ -212,7 +218,6 @@ export async function runWebServer({ host = '127.0.0.1', port = 3820, authToken
|
|
|
212
218
|
};
|
|
213
219
|
|
|
214
220
|
// 任务注册表:支持多会话并行——每个任务独立的 SSE 流、权限确认、中断控制
|
|
215
|
-
const MAX_CONCURRENT = 8;
|
|
216
221
|
let inflight = 0; // 质检 S2:在途聊天请求计数(与请求生命周期绑定,防 readBody 期间并发超限)
|
|
217
222
|
const tasks = new Map(); // taskId -> { res, send, abortHandler, pendingAsk, session, startedAt, status, message, durationMs }
|
|
218
223
|
let taskSeq = 0;
|
|
@@ -514,7 +519,7 @@ export async function runWebServer({ host = '127.0.0.1', port = 3820, authToken
|
|
|
514
519
|
durationMs: r.durationMs,
|
|
515
520
|
truncated: r.truncated,
|
|
516
521
|
aborted: r.aborted,
|
|
517
|
-
note: r.note || (r.text ? '' :
|
|
522
|
+
note: r.note || (r.text ? '' : `(任务已执行 ${io.stats().toolCount} 步工具操作${io.stats().deliverables.length ? `、交付 ${io.stats().deliverables.length} 个文件` : ''},自动收尾总结未能生成——可追问「总结一下刚才的工作」)`),
|
|
518
523
|
stats: io.stats(),
|
|
519
524
|
session: path.basename(session.file),
|
|
520
525
|
});
|