mingdao-harness 0.2.0 → 0.2.1
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/QUALITY-REVIEW-0.1.68.md +3 -2
- package/package.json +8 -3
- package/src/web/app.js +25 -7
- package/src/web/index.html +4 -7
- package/src/web/routes/api.js +47 -1
|
@@ -87,8 +87,9 @@
|
|
|
87
87
|
- L1/L2/L3/L6/L11/L12/L13 顺手修
|
|
88
88
|
- 验收:94 组测试 + 38 组 bench 全绿、tsc 0 错误、三平台 CI 过
|
|
89
89
|
|
|
90
|
-
### Phase 2 —「结构治理」v0.2.0(≈4–6
|
|
91
|
-
|
|
90
|
+
### Phase 2 —「结构治理」v0.2.0(≈4–6 天)✅ 已实施(2026-08-29)
|
|
91
|
+
实施项:server.js 拆路由模块(src/web/routes/api.js,1299→579 行)、cli.js 命令分发显式化、SPA JS 外置 app.js + CSP 收紧、SSRF 目标校验、inflight 并发、会话写互斥、listen Promise + 桌面端口重试、sync-server 限流加固、API Key 即时生效、更新下载失败可见化。
|
|
92
|
+
(cli.js 的 REPL/worker 进一步拆分与 routes/api.js 按域细分留待 0.2.1 渐进完成。)
|
|
92
93
|
- H5 server.js 拆 `src/web/routes/*.js`(chat/sessions/config/sync/schedule/skills/workspace/fs-browse 等)+ 共享中间件
|
|
93
94
|
- M13 cli.js 命令分发重构 + REPL/worker 拆分 + 重复逻辑抽取
|
|
94
95
|
- M14 index.html JS 外置 `app.js` + CSP 收紧(去 unsafe-inline)+ 图标表/双拉取去重
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mingdao-harness",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "MingDao Harness —— 开源智能体框架(Agent Harness)。零依赖、开箱即用,针对 DeepSeek-V4 系列优化,开放主流模型接入。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -48,13 +48,18 @@
|
|
|
48
48
|
"license": "MIT",
|
|
49
49
|
"scripts": {
|
|
50
50
|
"start": "node src/cli.js",
|
|
51
|
-
"test": "node test/
|
|
51
|
+
"test": "node test/run-all.mjs",
|
|
52
52
|
"typecheck": "tsc -p tsconfig.json",
|
|
53
53
|
"prepublishOnly": "node test/smoke.js && node test/e2e-local.js && node test/e2e-schedule.js",
|
|
54
54
|
"desktop": "npm --prefix desktop start",
|
|
55
55
|
"desktop:dist": "node scripts/sync-versions.mjs && npm --prefix desktop run dist:dir",
|
|
56
56
|
"desktop:sync": "node scripts/sync-versions.mjs",
|
|
57
|
-
"bench": "node test/bench/bench-tokenizer.mjs && node test/bench/bench-routing.mjs && node test/bench/bench-compaction.mjs"
|
|
57
|
+
"bench": "node test/bench/bench-tokenizer.mjs && node test/bench/bench-routing.mjs && node test/bench/bench-compaction.mjs",
|
|
58
|
+
"coverage": "node test/run-all.mjs --coverage && node scripts/coverage-report.mjs",
|
|
59
|
+
"typecheck:strict": "node scripts/strict-ratchet.mjs",
|
|
60
|
+
"preversion": "node test/smoke.js && node test/e2e-local.js && npm run bench",
|
|
61
|
+
"version": "node scripts/bump-changelog.mjs",
|
|
62
|
+
"postversion": "git add package.json package-lock.json CHANGELOG.md 2>/dev/null; git commit -m \"chore: release v$npm_package_version\" --no-verify || true"
|
|
58
63
|
},
|
|
59
64
|
"devDependencies": {
|
|
60
65
|
"@types/node": "^26.2.0",
|
package/src/web/app.js
CHANGED
|
@@ -423,7 +423,7 @@ function renderWorkStatus(){
|
|
|
423
423
|
const secs=curWorkT0?Math.round((Date.now()-curWorkT0)/1000):0;
|
|
424
424
|
html='<span class="ws-busy"><span class="spinner"></span>⏳ '+curSteps+' 步 · '+Math.floor(secs/60)+' 分 '+Math.round(secs%60)+' 秒 · '+curTools+' 工具步</span>';
|
|
425
425
|
} else if(bgRunning>0){
|
|
426
|
-
html='<span class="ws-bg">🛠 '+bgRunning+'
|
|
426
|
+
html='<span class="ws-bg">🛠 '+bgRunning+' 个后台任务运行中(下载/定时任务等)— 点击查看,完成后会自动提示</span>';
|
|
427
427
|
}
|
|
428
428
|
el.style.display=html?'flex':'none';
|
|
429
429
|
el.innerHTML=html;
|
|
@@ -481,10 +481,8 @@ function openTraj(msg){
|
|
|
481
481
|
list.appendChild(div);
|
|
482
482
|
}
|
|
483
483
|
$('#trajPanel').style.display='flex';
|
|
484
|
-
$('#tasksPanel').style.display='none';
|
|
485
|
-
$('#subPanel').style.display='none';
|
|
484
|
+
$('#tasksPanel').style.display='none'; // 仅任务面板(右侧)与轨迹互斥;子代理面板可与轨迹同显
|
|
486
485
|
$('#trajRailBtn').classList.add('on');
|
|
487
|
-
$('#subRailBtn').classList.remove('on');
|
|
488
486
|
}
|
|
489
487
|
function resultText(r){
|
|
490
488
|
if(r==null||r===undefined) return '';
|
|
@@ -529,7 +527,7 @@ const toggleSubPanel=()=>{
|
|
|
529
527
|
const willShow=p.style.display==='none';
|
|
530
528
|
p.style.display=willShow?'flex':'none';
|
|
531
529
|
$('#subRailBtn').classList.toggle('on', willShow);
|
|
532
|
-
if(willShow){ $('#tasksPanel').style.display='none';
|
|
530
|
+
if(willShow){ $('#tasksPanel').style.display='none'; renderSubPanel(); } // 轨迹(左侧)可与子代理(右侧)同显
|
|
533
531
|
};
|
|
534
532
|
$('#subRailBtn').onclick=toggleSubPanel;
|
|
535
533
|
$('#sbClose').onclick=()=>{ $('#subPanel').style.display='none'; $('#subRailBtn').classList.remove('on'); };
|
|
@@ -538,10 +536,21 @@ function setBtn(){
|
|
|
538
536
|
sendBtn.textContent = generating ? '■ 停止' : '发送';
|
|
539
537
|
sendBtn.className = generating ? 'danger' : 'primary';
|
|
540
538
|
}
|
|
539
|
+
const lastBgStatus = new Map(); // 后台任务状态跟踪(完成/失败转换 → 聊天横幅反馈)
|
|
541
540
|
async function updateTasksPanel(){
|
|
542
541
|
const r=await fetch('/api/tasks').catch(()=>null); if(!r) return;
|
|
543
542
|
const j=await r.json(); const list=$('#tpList'); list.innerHTML='';
|
|
544
|
-
|
|
543
|
+
// 质检(等待状态静默):后台任务(worker/调度)并入计数与面板;状态转换时在聊天区弹可见横幅
|
|
544
|
+
bgRunning=(Number(j.running)||0)+(Number(j.bgRunning)||0);
|
|
545
|
+
for(const t of (j.background||[])){
|
|
546
|
+
const prev=lastBgStatus.get(t.id);
|
|
547
|
+
if(prev && prev.status==='running' && t.status!=='running'){
|
|
548
|
+
if(t.status==='done') renderBanner({text:'✅ 后台任务「'+esc(String(t.message||'').slice(0,30)||'任务')+'」已完成'+(t.durationMs!=null?'(用时 '+(t.durationMs/1000).toFixed(1)+' 秒)':'')});
|
|
549
|
+
else if(t.status==='failed') renderBanner({text:'✖ 后台任务「'+esc(String(t.message||'').slice(0,30)||'任务')+'」失败:'+esc(String(t.error||'').slice(0,60))});
|
|
550
|
+
}
|
|
551
|
+
lastBgStatus.set(t.id,{status:t.status});
|
|
552
|
+
}
|
|
553
|
+
renderWorkStatus();
|
|
545
554
|
$('#tpCount').textContent='('+j.running+' 运行中 / 上限 '+j.maxConcurrent+')';
|
|
546
555
|
// 审计(第二问无反应修复):generating 改为「本轮在途」本地状态,由 send/finally 维护,
|
|
547
556
|
// 不再由 /api/tasks 轮询推导——任务面板瞬时波动或一次轮询失败曾让按钮永久卡在停止态,
|
|
@@ -554,6 +563,14 @@ async function updateTasksPanel(){
|
|
|
554
563
|
if(t.status==='running'){ const b=document.createElement('button'); b.textContent='中断'; b.className='danger'; b.style.cssText='margin-left:auto;padding:2px 8px;font-size:11px'; b.onclick=()=>fetch('/api/abort',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({taskId:t.id})}).catch(()=>{}); div.querySelector('.tp-meta').appendChild(b); }
|
|
555
564
|
list.appendChild(div);
|
|
556
565
|
}
|
|
566
|
+
for(const t of (j.background||[])){
|
|
567
|
+
const div=document.createElement('div'); div.className='tp-item';
|
|
568
|
+
const dot = t.status==='running'?'tp-run':(t.status==='done'?'tp-done':'tp-bad');
|
|
569
|
+
const secs = t.status==='running'?'…':(t.durationMs!=null?' '+(t.durationMs/1000).toFixed(1)+'s':'');
|
|
570
|
+
const kindLabel = t.kind==='schedule'?'⏰ ':(t.kind==='worker'?'🛠 ':'');
|
|
571
|
+
div.innerHTML='<div class="tp-title">'+kindLabel+esc(t.message||t.id||'任务')+'</div><div class="tp-meta"><span class="tp-dot '+dot+'"></span>'+t.status+secs+'</div>';
|
|
572
|
+
list.appendChild(div);
|
|
573
|
+
}
|
|
557
574
|
}
|
|
558
575
|
setInterval(updateTasksPanel, 2000);
|
|
559
576
|
// 费用徽标:今日费用 / 缓存命中率 / 护栏(15s 刷新)
|
|
@@ -856,9 +873,10 @@ async function refreshModelsCfg(){
|
|
|
856
873
|
const kstate=c.keyState==='stored'?'<span style="color:var(--accent)">'+esc(c.keyMasked||'')+'</span>':'<span style="color:var(--err)">无Key</span>';
|
|
857
874
|
div.innerHTML='<span style="flex:1;min-width:0"><b>'+esc(c.name)+'</b> <span style="color:var(--faint)">'+esc(c.label)+' · '+esc(c.baseUrl)+'</span></span><span style="white-space:nowrap">'+kstate+'</span>';
|
|
858
875
|
const sk=document.createElement('button'); sk.textContent='设Key'; sk.style.cssText='padding:1px 8px;font-size:11px'; sk.onclick=async()=>{ const k=await uiPrompt('API Key('+c.name+'):', null, {hidden:true}); if(k===null) return; fetch('/api/models-config',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'setCustomKey',name:c.name,key:k})}).then(rr=>rr.json()).then(jj=>{ if(jj.ok) refreshModelsCfg(); else uiAlert(jj.error||'设置失败'); }); };
|
|
876
|
+
const test=document.createElement('button'); test.textContent='测试'; test.style.cssText='padding:1px 8px;font-size:11px'; test.onclick=async()=>{ test.disabled=true; test.textContent='测试中…'; try{ const rr=await fetch('/api/models-config',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'testCustom',name:c.name})}); const jj=await rr.json().catch(()=>({error:'请求失败'})); if(jj.ok){ await uiAlert('✅ 连接成功('+jj.latencyMs+'ms):'+esc(jj.reply||'空回复')); } else { await uiAlert('✖ 连接失败:'+esc(jj.error||'未知错误')); } } finally { test.disabled=false; test.textContent='测试'; } };
|
|
859
877
|
const ed=document.createElement('button'); ed.textContent='修改'; ed.style.cssText='padding:1px 8px;font-size:11px'; ed.onclick=async()=>{ const u=await uiPrompt('API 地址:', c.baseUrl); if(u===null) return; const l=await uiPrompt('标签:', c.label); if(l===null) return; fetch('/api/models-config',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'updateCustom',name:c.name,baseUrl:u,label:l})}).then(rr=>rr.json()).then(jj=>{ if(jj.ok){ refreshModelsCfg(); reloadModels(); } else uiAlert(jj.error||'修改失败'); }); };
|
|
860
878
|
const rm=document.createElement('button'); rm.textContent='删除'; rm.className='danger'; rm.style.cssText='padding:1px 8px;font-size:11px'; rm.onclick=async()=>{ if(!await uiConfirm('删除自定义模型 '+c.name+'?')) return; const rr=await fetch('/api/models-config',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'removeCustom',name:c.name})}); const jj=await rr.json().catch(()=>({error:'失败'})); if(jj.ok){ refreshModelsCfg(); reloadModels(); } else uiAlert(jj.error||'删除失败'); };
|
|
861
|
-
div.appendChild(sk); div.appendChild(ed); div.appendChild(rm); cl.appendChild(div);
|
|
879
|
+
div.appendChild(sk); div.appendChild(test); div.appendChild(ed); div.appendChild(rm); cl.appendChild(div);
|
|
862
880
|
}
|
|
863
881
|
$('#baseUrlOverride').value=j.baseUrlOverride||'';
|
|
864
882
|
}
|
package/src/web/index.html
CHANGED
|
@@ -43,11 +43,8 @@ header select{flex:none}
|
|
|
43
43
|
/* 输入框上方工作状态条(审计:长任务静默硬伤——实时显示执行步数/耗时与后台任务数)。
|
|
44
44
|
与输入框同宽对齐(复用 #composer 的居中容器),不再横贯整个聊天窗口 */
|
|
45
45
|
#workStatus{max-width:860px;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}
|
|
46
|
-
/*
|
|
47
|
-
#
|
|
48
|
-
#liveBar .spinner{margin-right:2px}
|
|
49
|
-
#liveBar .lb-phase{color:var(--accent2);font-weight:600;white-space:nowrap}
|
|
50
|
-
#liveBar .lb-stat{color:var(--dim);white-space:nowrap}
|
|
46
|
+
/* 状态条 busy 行:承载完整阶段信息(合并原顶部活动条),与输入框同列不遮挡聊天区 */
|
|
47
|
+
#workStatus .ws-phase{color:var(--accent2);font-weight:600;white-space:nowrap}
|
|
51
48
|
/* 左侧轨迹固定栏 + 右侧子代理面板(布局重构:轨迹在左、子代理在右) */
|
|
52
49
|
#trajRail{position:fixed;left:0;top:52px;bottom:0;width:42px;background:var(--bg2);border-right:1px solid var(--border);z-index:35;display:flex;flex-direction:column;align-items:center;padding-top:10px;gap:10px}
|
|
53
50
|
#trajRail .rail-btn{width:34px;height:34px;border-radius:10px;border:1px solid var(--border);background:var(--bg3);cursor:pointer;font-size:15px;display:flex;align-items:center;justify-content:center}
|
|
@@ -73,7 +70,7 @@ header select{flex:none}
|
|
|
73
70
|
.trajBtn{cursor:pointer;background:var(--bg3);border:1px solid var(--border);color:var(--dim);border-radius:20px;padding:2px 10px;font-size:11.5px}
|
|
74
71
|
.trajBtn:hover{color:var(--text);border-color:var(--accent2)}
|
|
75
72
|
/* 轨迹面板(与任务面板互斥展示):置于聊天窗口左侧(DSH 式侧栏) */
|
|
76
|
-
#trajPanel{position:fixed;left:42px;top:52px;bottom:0;width:
|
|
73
|
+
#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)}
|
|
77
74
|
.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}
|
|
78
75
|
.tj-head button{margin-left:auto}
|
|
79
76
|
#tjList{flex:1;overflow-y:auto;padding:8px}
|
|
@@ -240,7 +237,7 @@ footer{flex:none;border-top:1px solid var(--border);background:var(--bg2);paddin
|
|
|
240
237
|
<div class="tp-head">任务面板 <span id="tpCount"></span><button id="tpClose" style="float:right">✕</button></div>
|
|
241
238
|
<div id="tpList"></div>
|
|
242
239
|
</aside>
|
|
243
|
-
<main><div id="
|
|
240
|
+
<main><div id="chat"></div></main>
|
|
244
241
|
<footer>
|
|
245
242
|
<div id="workStatus" style="display:none"></div>
|
|
246
243
|
<input type="file" id="fileInput" accept="image/png,image/jpeg,image/gif,image/webp,.txt,.md,.json,.js,.py,.log,.csv,.html,.css" multiple style="display:none">
|
package/src/web/routes/api.js
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
reconcileSchedules,
|
|
40
40
|
} from '../../schedule.js';
|
|
41
41
|
import { enableAutostart, disableAutostart, autostartStatus } from '../../autostart.js';
|
|
42
|
+
import { listTasks } from '../../tasks.js';
|
|
42
43
|
import { currentWorkspace, workspaceForDir, listWorkspaces, addWorkspace, removeWorkspace, renameWorkspace, setWorkspaceDir, workspacePath, touchWorkspace, getSessionWorkspace, setSessionWorkspace, removeSessionWorkspace, moveSessionWorkspace } from '../../workspace.js';
|
|
43
44
|
import { loadMemory, writeMemory, dedupeMemory } from '../../memory.js';
|
|
44
45
|
import { recordUsage, listCacheStats, summarizeCacheStats, costBreakdown } from '../../cachestats.js';
|
|
@@ -330,6 +331,22 @@ export function createApiDispatch(deps) {
|
|
|
330
331
|
saveConfig(cfg);
|
|
331
332
|
return json(res, 200, { ok: true, name, model: state.modelName });
|
|
332
333
|
}
|
|
334
|
+
// 质检(自定义模型连通性):发起一次最小对话验证 baseUrl+Key 可用
|
|
335
|
+
if (action === 'testCustom') {
|
|
336
|
+
const name = String(body.name || '').trim();
|
|
337
|
+
const cm = (cfg.customModels || {})[name];
|
|
338
|
+
if (!cm) return json(res, 400, { error: `自定义模型 ${name} 不存在` });
|
|
339
|
+
const pc = resolveProviderConfig(cfg, name);
|
|
340
|
+
if (!pc.apiKey) return json(res, 400, { error: '该模型未设置 API Key,请先「设Key」再测试' });
|
|
341
|
+
const t0 = Date.now();
|
|
342
|
+
try {
|
|
343
|
+
const tp = await createProvider(cfg, name);
|
|
344
|
+
const r = await tp.chat({ model: name, messages: [{ role: 'user', content: 'ping' }], tools: [], temperature: 0, maxTokens: 8 });
|
|
345
|
+
return json(res, 200, { ok: true, name, latencyMs: Date.now() - t0, reply: String(r.text || r.reasoning || '').slice(0, 60) || '(空回复,但连接成功)' });
|
|
346
|
+
} catch (e) {
|
|
347
|
+
return json(res, 200, { ok: false, name, latencyMs: Date.now() - t0, error: String(e?.message || e) });
|
|
348
|
+
}
|
|
349
|
+
}
|
|
333
350
|
if (action === 'setCustomKey') {
|
|
334
351
|
const name = String(body.name || '').trim();
|
|
335
352
|
if (!(cfg.customModels || {})[name]) return json(res, 400, { error: `自定义模型 ${name} 不存在` });
|
|
@@ -420,7 +437,36 @@ export function createApiDispatch(deps) {
|
|
|
420
437
|
durationMs: t.status === 'running' ? Date.now() - t.startedAt : t.durationMs,
|
|
421
438
|
session: t.session ? path.basename(t.session.file) : null,
|
|
422
439
|
}));
|
|
423
|
-
|
|
440
|
+
// 质检(等待状态静默):后台 worker 任务与运行中的调度任务并入面板与状态条计数——
|
|
441
|
+
// 此前 /api/tasks 只含聊天 SSE 任务,后台下载/长任务期间界面完全无感知
|
|
442
|
+
let bgRunning = 0;
|
|
443
|
+
const background = [];
|
|
444
|
+
try {
|
|
445
|
+
for (const t of listTasks(home)) {
|
|
446
|
+
if (t.status === 'running') bgRunning += 1;
|
|
447
|
+
background.push({
|
|
448
|
+
id: t.id,
|
|
449
|
+
kind: 'worker',
|
|
450
|
+
status: t.status,
|
|
451
|
+
message: t.message || t.question || '',
|
|
452
|
+
startedAt: t.startedAt,
|
|
453
|
+
durationMs: t.status === 'running' ? Date.now() - t.startedAt : t.durationMs,
|
|
454
|
+
error: t.error || '',
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
for (const sch of listSchedules(home)) {
|
|
458
|
+
if (sch.status === 'running') bgRunning += 1;
|
|
459
|
+
background.push({
|
|
460
|
+
id: sch.id,
|
|
461
|
+
kind: 'schedule',
|
|
462
|
+
status: sch.status,
|
|
463
|
+
message: sch.question || sch.id || '',
|
|
464
|
+
startedAt: sch.createdAt || null,
|
|
465
|
+
durationMs: sch.status === 'running' ? Date.now() - (sch.lastRunAt || Date.now()) : null,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
} catch {}
|
|
469
|
+
json(res, 200, { ok: true, running, bgRunning, maxConcurrent: MAX_CONCURRENT, tasks: list, background });
|
|
424
470
|
return;
|
|
425
471
|
}
|
|
426
472
|
if (req.method === 'POST' && p === '/api/chat') {
|