mingdao-harness 0.3.0 → 0.3.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.
@@ -31,20 +31,23 @@ body{margin:0;background:var(--bg);color:var(--text);font:15px/1.65 -apple-syste
31
31
  body.traj-open{padding-left:294px}
32
32
  body.sub-open{padding-right:320px}
33
33
  body.tasks-open{padding-right:280px}
34
+ body.dash-open{padding-right:400px}
34
35
  @media (max-width:1200px){
35
36
  #trajPanel{width:210px}body.traj-open{padding-left:252px}
36
37
  #subPanel{width:270px}body.sub-open{padding-right:270px}
37
38
  #tasksPanel{width:230px}body.tasks-open{padding-right:230px}
39
+ #dashPanel{width:360px}body.dash-open{padding-right:360px}
38
40
  }
39
41
  @media (max-width:860px){
40
42
  #trajPanel{width:160px}body.traj-open{padding-left:202px}
41
43
  #subPanel{width:190px}body.sub-open{padding-right:190px}
42
44
  #tasksPanel{width:190px}body.tasks-open{padding-right:190px}
45
+ #dashPanel{width:min(340px,94vw)}body.dash-open{padding-right:min(340px,94vw)}
43
46
  }
44
47
  @media (max-width:560px){
45
- body.traj-open,body.sub-open,body.tasks-open{padding:0}
48
+ body.traj-open,body.sub-open,body.tasks-open,body.dash-open{padding:0}
46
49
  #trajPanel{width:calc(100vw - 42px);left:42px}
47
- #subPanel,#tasksPanel{width:100vw}
50
+ #subPanel,#tasksPanel,#dashPanel{width:100vw}
48
51
  }
49
52
  header{min-height:52px;display:flex;align-items:center;gap:10px;padding:8px 12px;border-bottom:1px solid var(--border);background:var(--bg2);flex:none;flex-wrap:nowrap;white-space:nowrap;overflow-x:auto}
50
53
  header .logo{font-weight:700;font-size:16px;color:var(--accent);letter-spacing:.5px;flex:none}
@@ -229,6 +232,36 @@ footer{flex:none;border-top:1px solid var(--border);background:var(--bg2);paddin
229
232
  @keyframes spin{to{transform:rotate(360deg)}}
230
233
  /* 自绘悬浮气泡 tooltip(替换原生 title):暗色悬浮层 + 阴影,跟随锚点居中,越界自动回弹 */
231
234
  .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}
235
+ /* —— 省钱仪表盘(v0.3.1 顶部入口,真·仪表盘) —— */
236
+ #dashPanel{position:fixed;right:0;top:52px;bottom:0;width:400px;max-width:94vw;background:var(--bg2);border-left:1px solid var(--border);z-index:45;display:flex;flex-direction:column;box-shadow:-8px 0 32px rgba(0,0,0,.4)}
237
+ .dash-head{padding:12px 14px;font-size:14px;font-weight:700;color:var(--accent);border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px}
238
+ .dash-head .dash-sub{font-size:11.5px;color:var(--faint);font-weight:400}
239
+ .dash-head button{margin-left:auto;padding:2px 8px;font-size:12px}
240
+ .dash-body{flex:1;overflow-y:auto;padding:14px}
241
+ .dash-kpi{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:14px}
242
+ .kpi{background:var(--bg3);border:1px solid var(--border);border-radius:10px;padding:10px 12px;display:flex;flex-direction:column;gap:2px}
243
+ .kpi-l{font-size:11px;color:var(--faint)}
244
+ .kpi-v{font-size:17px;font-weight:700;font-variant-numeric:tabular-nums}
245
+ .dash-gauge-wrap{position:relative;width:120px;margin:4px auto 14px;text-align:center}
246
+ .gauge-label{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:0}
247
+ .gauge-label b{font-size:20px;color:var(--accent2)}
248
+ .gauge-label span{font-size:10.5px;color:var(--faint)}
249
+ .dash-sec{font-size:11.5px;color:var(--faint);margin:12px 0 6px;border-top:1px dashed var(--border);padding-top:10px}
250
+ .dash-sec:first-of-type{border-top:none;padding-top:0}
251
+ .dash-chart{background:var(--bg3);border:1px solid var(--border);border-radius:10px;padding:8px}
252
+ .dash-bars{display:flex;flex-direction:column;gap:6px}
253
+ .dbar{background:var(--bg3);border:1px solid var(--border);border-radius:8px;padding:6px 10px}
254
+ .dbar .t{display:flex;justify-content:space-between;font-size:11.5px;margin-bottom:4px}
255
+ .dbar .t .n{color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:60%}
256
+ .dbar .t .v{color:var(--dim);font-variant-numeric:tabular-nums}
257
+ .dbar .track{height:5px;border-radius:3px;background:var(--bg2);overflow:hidden}
258
+ .dbar .fill{height:100%;border-radius:3px;background:linear-gradient(90deg,var(--accent2),var(--accent))}
259
+ .dash-recent{display:flex;flex-direction:column;gap:5px;font-size:11.5px}
260
+ .dr{display:flex;justify-content:space-between;background:var(--bg3);border:1px solid var(--border);border-radius:8px;padding:5px 10px}
261
+ .dr .m{color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:50%}
262
+ .dr .p{color:var(--accent2);font-variant-numeric:tabular-nums}
263
+ .dr .c{color:var(--faint);font-variant-numeric:tabular-nums}
264
+ .badge-btn:hover{border-color:var(--accent2);color:var(--accent2)}
232
265
  </style>
233
266
  </head>
234
267
  <body>
@@ -237,7 +270,7 @@ footer{flex:none;border-top:1px solid var(--border);background:var(--bg2);paddin
237
270
  <span id="cfgMsg" class="badge" style="display:none"></span>
238
271
  <button id="cfgBtn" title="设置">⚙</button>
239
272
  <select id="wsSel" title="工作空间(切换 / 新建,目录自动创建)"></select>
240
- <span class="badge" id="envBadge"></span><span class="badge" id="costBadge" title="今日费用 / 缓存命中率(每 15s 刷新)"></span>
273
+ <span class="badge" id="envBadge"></span><span class="badge badge-btn" id="costBadge" title="省钱仪表盘(点击展开)" style="cursor:pointer">📊 —</span>
241
274
  <div class="spacer"></div>
242
275
  <input id="sessionSearch" placeholder="搜索会话…" title="关键词检索历史会话">
243
276
  <select id="sessions" title="历史会话"><option value="">历史会话</option></select>
@@ -260,6 +293,29 @@ footer{flex:none;border-top:1px solid var(--border);background:var(--bg2);paddin
260
293
  <div class="tp-head">任务面板 <span id="tpCount"></span><button id="tpClose" style="float:right">✕</button></div>
261
294
  <div id="tpList"></div>
262
295
  </aside>
296
+ <aside id="dashPanel" style="display:none">
297
+ <div class="dash-head">📊 省钱仪表盘 <span class="dash-sub">费用 · 命中 · 节省</span><button id="dashClose">✕</button></div>
298
+ <div class="dash-body">
299
+ <div class="dash-kpi">
300
+ <div class="kpi"><span class="kpi-l">今日费用</span><span class="kpi-v" id="kpiToday">—</span></div>
301
+ <div class="kpi"><span class="kpi-l">缓存命中</span><span class="kpi-v" id="kpiRate">—</span></div>
302
+ <div class="kpi"><span class="kpi-l">已节省</span><span class="kpi-v" style="color:var(--accent)" id="kpiSaved">—</span></div>
303
+ <div class="kpi"><span class="kpi-l">轮次/步数</span><span class="kpi-v" id="kpiTurns">—</span></div>
304
+ </div>
305
+ <div class="dash-gauge-wrap">
306
+ <svg id="rateGauge" viewBox="0 0 120 120" width="120" height="120"></svg>
307
+ <div class="gauge-label"><b id="rateGaugeNum">—</b><span>命中率</span></div>
308
+ </div>
309
+ <div class="dash-sec">近 14 天费用趋势(¥)</div>
310
+ <div id="trendChart" class="dash-chart"></div>
311
+ <div class="dash-sec">模型费用 Top</div>
312
+ <div id="dashModels" class="dash-bars"></div>
313
+ <div class="dash-sec">工具耗时 Top</div>
314
+ <div id="dashTools" class="dash-bars"></div>
315
+ <div class="dash-sec">最近缓存命中</div>
316
+ <div id="dashRecent" class="dash-recent"></div>
317
+ </div>
318
+ </aside>
263
319
  <main><div id="chat"></div></main>
264
320
  <footer>
265
321
  <div id="workStatus" style="display:none"></div>
@@ -304,7 +360,7 @@ footer{flex:none;border-top:1px solid var(--border);background:var(--bg2);paddin
304
360
  <button class="cfg-nav-btn" data-panel="schedule">⏰ 调度队列</button>
305
361
  <button class="cfg-nav-btn" data-panel="workspace">📁 工作空间</button>
306
362
  <button class="cfg-nav-btn" data-panel="memory">🧠 长期记忆</button>
307
- <button class="cfg-nav-btn" data-panel="cache">📊 缓存仪表盘</button>
363
+
308
364
  <button class="cfg-nav-btn" data-panel="mcp">🔌 MCP 生态</button>
309
365
  <button class="cfg-nav-btn" data-panel="skills">🧩 技能库</button>
310
366
  </nav>
@@ -322,6 +378,11 @@ footer{flex:none;border-top:1px solid var(--border);background:var(--bg2);paddin
322
378
  <input id="cmUrl" placeholder="API 地址 https://…/v1" style="flex:1.4;min-width:170px;background:var(--bg3);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:6px 10px;font-size:13px">
323
379
  <input id="cmKey" type="password" placeholder="API Key(可选)" style="flex:1;min-width:120px;background:var(--bg3);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:6px 10px;font-size:13px">
324
380
  </div>
381
+ <div class="cfg-row" style="display:flex;gap:6px;flex-wrap:wrap">
382
+ <input id="cmCtx" type="number" placeholder="上下文窗口 tokens(本地模型建议填,如 131072)" style="flex:1.6;min-width:180px;background:var(--bg3);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:6px 10px;font-size:13px">
383
+ <input id="cmMaxOut" type="number" placeholder="最大输出 tokens(可选,如 8192)" style="flex:1;min-width:120px;background:var(--bg3);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:6px 10px;font-size:13px">
384
+ </div>
385
+ <div class="cfg-hint">本地部署模型(127.0.0.1/localhost):填「上下文窗口」后预算自动收紧到窗口 75% 以内并留输出余量,长任务不再撑爆窗口。</div>
325
386
  <div class="row" style="justify-content:flex-start"><button id="cmAdd">+ 添加自定义模型</button></div>
326
387
  <div class="cfg-row" style="display:flex;gap:6px"><input id="baseUrlOverride" placeholder="API 地址覆盖(可选,覆盖当前服务商地址)" style="flex:1;background:var(--bg3);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:6px 10px;font-size:13px"><button id="baseUrlSave">保存</button></div>
327
388
  </section>
@@ -362,6 +423,12 @@ footer{flex:none;border-top:1px solid var(--border);background:var(--bg2);paddin
362
423
  </div>
363
424
  <div class="cfg-row"><label><input type="checkbox" id="routeChk"> 自动模型路由(规划→pro / 执行→flash)</label></div>
364
425
  <div class="cfg-row"><label>上下文预算 tokens <input type="number" id="budgetInput" min="1000" step="1000"></label></div>
426
+ <div class="cfg-row" style="display:flex;gap:6px;flex-wrap:wrap;align-items:flex-end">
427
+ <label style="font-size:12px">首 token 等待(秒)<input type="number" id="toFirstToken" min="0" step="10" placeholder="自动"></label>
428
+ <label style="font-size:12px">流式空闲(秒)<input type="number" id="toStreamIdle" min="0" step="10" placeholder="自动"></label>
429
+ <label style="font-size:12px">总量(秒)<input type="number" id="toTotal" min="0" step="10" placeholder="自动"></label>
430
+ </div>
431
+ <div class="cfg-hint">留空 = 自适应:本地模型自动给更长首 token 等待(慢 prefill 不被误杀)。仅在默认值仍不够/过松时手填。</div>
365
432
  <div class="cfg-row"><label><input type="checkbox" id="autoStartChk"> 开机自启(登录后自动启动服务器)</label></div>
366
433
  <div class="cfg-row"><label><input type="checkbox" id="notifyChk"> 后台任务完成/失败时桌面通知</label></div>
367
434
  </section>
@@ -402,24 +469,7 @@ footer{flex:none;border-top:1px solid var(--border);background:var(--bg2);paddin
402
469
  <textarea id="memArea" rows="5" placeholder="例如:所有文档用中文;提交信息用约定式提交…" style="width:100%;background:var(--bg3);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:6px 10px;font-size:12.5px;resize:vertical"></textarea>
403
470
  <div class="row" style="justify-content:flex-start"><button id="memSave">保存记忆</button><button id="memDedupe">去重合并</button><span id="memMsg" style="font-size:12px;color:var(--faint)"></span></div>
404
471
  </section>
405
- <section class="cfg-panel" data-panel="cache">
406
- <hr style="border:none;border-top:1px solid var(--border);margin:14px 0 8px">
407
- <h4 style="margin:4px 0;color:var(--accent2)">缓存命中率仪表盘(最近 10 次)</h4>
408
- <div id="cacheSummary" style="font-size:12.5px;color:var(--dim);padding:2px"></div>
409
- <div id="cacheRecent" style="max-height:130px;overflow-y:auto"></div>
410
- <h4 style="margin:10px 0 4px;color:var(--accent2)">费用二级分账(模型 / 工具 Top5 · 近 14 天)</h4>
411
- <div style="display:flex;gap:10px;flex-wrap:wrap">
412
- <div style="flex:1;min-width:170px">
413
- <div style="font-size:11.5px;color:var(--faint)">模型 Top5(费用)</div>
414
- <div id="costTopModels" style="font-size:12px;color:var(--dim)"></div>
415
- </div>
416
- <div style="flex:1;min-width:170px">
417
- <div style="font-size:11.5px;color:var(--faint)">工具 Top5(耗时)</div>
418
- <div id="costTopTools" style="font-size:12px;color:var(--dim)"></div>
419
- </div>
420
- </div>
421
- <div id="costLine" style="margin-top:8px"></div>
422
- </section>
472
+
423
473
  <section class="cfg-panel" data-panel="mcp">
424
474
  <hr style="border:none;border-top:1px solid var(--border);margin:14px 0 8px">
425
475
  <h4 style="margin:4px 0;color:var(--accent2)">MCP 生态预设(一键接入,重启生效)</h4>
@@ -50,6 +50,7 @@ export async function handle({ req, res, method, p, url }, deps, shared) {
50
50
  routing: cfg.routing?.enabled ? cfg.routing : null,
51
51
  reasoning,
52
52
  contextBudget: cfg.contextBudget || 128000,
53
+ timeout: cfg.timeout || null, // v0.3.2:分层超时(firstTokenMs/streamIdleMs/totalMs)
53
54
  pricingAsOf: PRICE_DATA_AS_OF,
54
55
  autostart: autostartStatus(),
55
56
  notify: cfg.notify !== false,
@@ -112,6 +113,17 @@ export async function handle({ req, res, method, p, url }, deps, shared) {
112
113
  next.contextBudget = n;
113
114
  cfg.contextBudget = n;
114
115
  }
116
+ // v0.3.2 超时分层:firstTokenMs(首 token 等待)/ streamIdleMs(流式空闲)/ totalMs(总量)
117
+ if (body.timeout !== undefined && body.timeout && typeof body.timeout === 'object') {
118
+ cfg.timeout = cfg.timeout || {};
119
+ for (const k of ['firstTokenMs', 'streamIdleMs', 'totalMs']) {
120
+ const v = Number(body.timeout[k]);
121
+ if (Number.isFinite(v) && v > 0) cfg.timeout[k] = Math.round(v);
122
+ else if (body.timeout[k] === null || body.timeout[k] === '') delete cfg.timeout[k];
123
+ }
124
+ // 超时在 createProvider 时按 cfg 固化进实例,改后清缓存让新值即时生效(否则要重启)
125
+ providerCache.clear();
126
+ }
115
127
  if (body.reasoningEffort !== undefined) {
116
128
  const re = String(body.reasoningEffort);
117
129
  if (!['off', 'low', 'high', 'max'].includes(re)) {
@@ -165,6 +177,8 @@ export async function handle({ req, res, method, p, url }, deps, shared) {
165
177
  baseUrl: cm.baseUrl || '',
166
178
  envKey: cm.envKey || null,
167
179
  vision: Boolean(cm.vision),
180
+ contextWindow: Number(cm.contextWindow) > 0 ? Number(cm.contextWindow) : null,
181
+ maxOutputTokens: Number(cm.maxOutputTokens) > 0 ? Number(cm.maxOutputTokens) : null,
168
182
  keyState: stored ? 'stored' : 'none',
169
183
  keyMasked: stored ? maskKey(stored) : null,
170
184
  };
@@ -232,14 +246,23 @@ export async function handle({ req, res, method, p, url }, deps, shared) {
232
246
  return json(res, 400, { error: `自定义模型 ${name} 已存在(可修改)` });
233
247
  }
234
248
  cfg.customModels = cfg.customModels || {};
249
+ // 修改时保留未提交字段(tokenizer/contextWindow/maxOutputTokens/vision)——
250
+ // 前端「修改」只发 baseUrl+label,整体替换会丢这些声明,导致本地模型窗口回退兜底
251
+ const prev = action === 'updateCustom' ? (cfg.customModels[name] || {}) : {};
235
252
  cfg.customModels[name] = {
236
253
  label,
237
254
  baseUrl,
238
- envKey: String(body.envKey || '').trim() || undefined,
239
- vision: body.vision === true || body.vision === 'on' ? true : undefined,
255
+ envKey: String(body.envKey || '').trim() || prev.envKey || undefined,
256
+ vision: body.vision === true || body.vision === 'on' ? true : prev.vision === true ? true : undefined,
257
+ ...(prev.tokenizer ? { tokenizer: prev.tokenizer } : {}),
258
+ // v0.3.2 本地模型自适应:显式声明上下文窗口/最大输出,预算据此推导(不撑爆窗口)
259
+ ...(Number(body.contextWindow) > 0 ? { contextWindow: Math.round(Number(body.contextWindow)) } : (prev.contextWindow ? { contextWindow: prev.contextWindow } : {})),
260
+ ...(Number(body.maxOutputTokens) > 0 ? { maxOutputTokens: Math.round(Number(body.maxOutputTokens)) } : (prev.maxOutputTokens ? { maxOutputTokens: prev.maxOutputTokens } : {})),
240
261
  };
241
262
  if (String(body.key || '').trim()) setStoredKey(`custom:${name}`, String(body.key).trim());
242
263
  saveConfig(cfg);
264
+ // baseUrl 改动影响 isLocal 判定 → 超时档位变化;清缓存让新超时/新端点即时生效
265
+ providerCache.clear();
243
266
  return json(res, 200, { ok: true, name });
244
267
  }
245
268
  if (action === 'removeCustom') {
@@ -252,6 +275,7 @@ export async function handle({ req, res, method, p, url }, deps, shared) {
252
275
  cfg.model = state.modelName;
253
276
  }
254
277
  saveConfig(cfg);
278
+ providerCache.clear();
255
279
  return json(res, 200, { ok: true, name, model: state.modelName });
256
280
  }
257
281
  // 质检(自定义模型连通性):发起一次最小对话验证 baseUrl+Key 可用
package/src/web/server.js CHANGED
@@ -29,8 +29,8 @@ import { MAX_CONCURRENT } from './constants.js';
29
29
  import { createAgent } from '../agent.js';
30
30
  import { createPermission } from '../permissions.js';
31
31
  import { buildSystemPrompt } from '../prompts.js';
32
- import { loadProjectMemory, extractAndAppendProjectMemory } from '../memory.js';
33
- import { saveTaskState, clearTaskState, loadTaskState, resumePrompt } from '../task-state.js';
32
+ import { loadProjectMemory, loadProjectMemoryEntries, retrieveRelevant, extractAndAppendProjectMemory } from '../memory.js';
33
+ import { saveTaskStateMerge, clearTaskState, loadTaskState, resumePrompt } from '../task-state.js';
34
34
  import { createWebIO } from './web-io.js';
35
35
  import { startMcpServers } from '../mcp.js';
36
36
  import {
@@ -397,7 +397,11 @@ export async function runWebServer({ host = '127.0.0.1', port = 3820, authToken
397
397
  // 每轮结束提取的新条目只写文件、不回灌当前会话。
398
398
  let projectMemorySnapshot = sessionMemoryCache.get(sessionName);
399
399
  if (projectMemorySnapshot === undefined) {
400
- projectMemorySnapshot = loadProjectMemory(taskDir);
400
+ // v0.3.1 语义检索:用本轮任务(新会话即首条消息)取「相关记忆条目」,而非全量截 4K——
401
+ // 换任务只注入相关记忆、省 token;快照保证同一会话前缀稳定。
402
+ const allEntries = loadProjectMemoryEntries(taskDir);
403
+ const query = String(built.persistText || '');
404
+ projectMemorySnapshot = query ? retrieveRelevant(allEntries, query, 6).join('\n') : loadProjectMemory(taskDir);
401
405
  sessionMemoryCache.set(sessionName, projectMemorySnapshot);
402
406
  if (sessionMemoryCache.size > 200) {
403
407
  const oldest = sessionMemoryCache.keys().next().value;
@@ -527,7 +531,7 @@ export async function runWebServer({ host = '127.0.0.1', port = 3820, authToken
527
531
  // v0.3.0 P0-2:任务检查点——跑满步数(capHit)或中断(aborted)时落盘供续跑,正常完成清除
528
532
  const finalSessionName = path.basename(session.file);
529
533
  if (r.capHit || r.aborted) {
530
- saveTaskState(finalSessionName, {
534
+ saveTaskStateMerge(finalSessionName, {
531
535
  goal: built.persistText,
532
536
  progress: r.text || '',
533
537
  artifacts: io.stats().deliverables,
@@ -547,7 +551,9 @@ export async function runWebServer({ host = '127.0.0.1', port = 3820, authToken
547
551
  const { messageTokens } = await import('../context.js');
548
552
  const counter = makeTokenCounter(runModel);
549
553
  const used = messages.reduce((/** @type {any} */ sum, /** @type {any} */ m) => sum + messageTokens(m, counter), 0);
550
- budgetInfo = { used, total: cfg.contextBudget || modelPreset(runModel)?.budgetTokens || 128000 };
554
+ // v0.3.2:展示用 total 与 Agent 实际预算同源(safeBudget 按窗口推导),避免「显示 128k 实际 98k」的误导
555
+ const { resolveModelCaps, safeBudget } = await import('../model-caps.js');
556
+ budgetInfo = { used, total: safeBudget(cfg, resolveModelCaps(cfg, runModel)) };
551
557
  } catch {}
552
558
  send({
553
559
  type: 'done',