mingdao-harness 0.1.60 → 0.1.62

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mingdao-harness",
3
- "version": "0.1.60",
3
+ "version": "0.1.62",
4
4
  "description": "MingDao Harness —— 开源智能体框架(Agent Harness)。零依赖、开箱即用,针对 DeepSeek-V4 系列优化,开放主流模型接入。",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -436,7 +436,8 @@ async function main() {
436
436
  if (opts.init) return;
437
437
  }
438
438
 
439
- let modelName = opts.model || cfg.model || 'deepseek-v4-flash';
439
+ // 模型回退链(向导允许跳过模型选择 cfg.model 可缺省):参数 > config > 该服务商首个预设模型 > flash
440
+ let modelName = opts.model || cfg.model || PROVIDERS[cfg.provider]?.models?.[0] || 'deepseek-v4-flash';
440
441
  const io = createIO();
441
442
 
442
443
  const pc0 = resolveProviderConfig(cfg, modelName);
@@ -610,6 +611,22 @@ async function main() {
610
611
  ].filter(Boolean));
611
612
  io.print(style('输入问题开始对话 · /help 查看命令 · Tab 补全 · Ctrl+C 中断生成\n', C.dim));
612
613
 
614
+ // WebUI 自动启动(首次运行 mingdao web 时可选开启,存 config.web.autoStart):
615
+ // 独立后台进程拉起,退出 TUI 后 WebUI 继续可用;关闭:mingdao web --no-autostart
616
+ if (cfg.web?.autoStart && !process.env.MINGDAO_NO_WEB_AUTOSTART) {
617
+ try {
618
+ const { spawn } = await import('node:child_process');
619
+ const { fileURLToPath } = await import('node:url');
620
+ const child = spawn(process.execPath, [fileURLToPath(import.meta.url), 'web'], {
621
+ detached: true,
622
+ stdio: 'ignore',
623
+ env: process.env,
624
+ });
625
+ child.unref();
626
+ io.print(style(`🌐 WebUI 后台启动中:http://127.0.0.1:${cfg.web?.port || 3820}(关闭自动启动:mingdao web --no-autostart)`, C.dim));
627
+ } catch {}
628
+ }
629
+
613
630
  let session = null;
614
631
  if (opts.resume) {
615
632
  const list = listSessions(home).slice(0, 10);
@@ -2,10 +2,21 @@
2
2
  import { listSkills, tamperedSkillNames } from '../skills.js';
3
3
  import { libraryList, searchLibrary, installSkill, uninstallSkill, reinstallSkill, trustSkill } from '../skill-lib.js';
4
4
  import { searchRegistry } from '../skill-registry.js';
5
- import { loadConfig } from '../config.js';
6
- import { ensureHome } from '../config.js';
5
+ import { loadConfig, saveConfig, ensureHome } from '../config.js';
7
6
  import { searchSessions, relativeTime } from '../session.js';
8
7
  import { runWebServer } from '../web/server.js';
8
+ import readline from 'node:readline';
9
+
10
+ // 首次运行询问「是否自动后台启动 WebUI」(交互终端才问;管道/脚本下默认否)
11
+ function askAutoStart() {
12
+ return new Promise((resolve) => {
13
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
14
+ rl.question('是否在每次运行 mingdao 时自动后台启动 WebUI(免敲命令)?[y/N] ', (a) => {
15
+ rl.close();
16
+ resolve(/^y(es)?$/i.test(String(a).trim()));
17
+ });
18
+ });
19
+ }
9
20
 
10
21
  export async function handleSkill(cmd, args) {
11
22
  const sub = args[0] || 'list';
@@ -102,10 +113,13 @@ export async function handleSkill(cmd, args) {
102
113
  return true;
103
114
  }
104
115
 
105
- // WebUI:mingdao web [端口] [--auth-token <令牌>](评估 P3-1:参数结构不合法的按提问处理)
116
+ // WebUI:mingdao web [端口] [--auth-token <令牌>] [--autostart|--no-autostart](评估 P3-1:参数结构不合法的按提问处理)
117
+ // --autostart/--no-autostart:写 config.web.autoStart——开启后每次运行 mingdao 会自动后台拉起 WebUI;
118
+ // 首次交互运行且未设置时,询问一次「下次是否自动启动」。
106
119
  export async function handleWeb(cmd, args) {
107
120
  let portIndex = -1;
108
121
  let tokenSeen = false;
122
+ let autoChoice; // undefined=未指定;true/false=显式指定
109
123
  for (let i = 0; i < args.length; i++) {
110
124
  const a = args[i];
111
125
  if (a === '--auth-token') {
@@ -119,6 +133,11 @@ export async function handleWeb(cmd, args) {
119
133
  tokenSeen = true;
120
134
  continue;
121
135
  }
136
+ if (a === '--autostart' || a === '--no-autostart') {
137
+ if (autoChoice !== undefined) return false;
138
+ autoChoice = a === '--autostart';
139
+ continue;
140
+ }
122
141
  if (/^\d+$/.test(a) && portIndex === -1) {
123
142
  portIndex = i; // 审计 P2-11:记录端口实际位置,而非默认读首参
124
143
  continue;
@@ -126,6 +145,21 @@ export async function handleWeb(cmd, args) {
126
145
  return false;
127
146
  }
128
147
  const cfg0 = loadConfig();
148
+ // 自动启动开关(先落盘再起服务,服务失败也保留用户选择)
149
+ if (autoChoice !== undefined) {
150
+ const c = cfg0 ? { ...cfg0, web: { ...(cfg0.web || {}), autoStart: autoChoice } } : { web: { autoStart: autoChoice } };
151
+ saveConfig(c);
152
+ console.log(
153
+ autoChoice
154
+ ? '✓ 已开启 WebUI 自动启动:以后直接运行 mingdao 即可后台拉起(关闭:mingdao web --no-autostart)'
155
+ : '✓ 已关闭 WebUI 自动启动'
156
+ );
157
+ } else if (cfg0?.web?.autoStart === undefined && process.stdin.isTTY) {
158
+ const ans = await askAutoStart();
159
+ const c = { ...cfg0, web: { ...(cfg0.web || {}), autoStart: ans } };
160
+ saveConfig(c);
161
+ if (ans) console.log('✓ 已开启:以后直接运行 mingdao 即可自动后台启动 WebUI(关闭:mingdao web --no-autostart)');
162
+ }
129
163
  const portArg = portIndex !== -1 ? Number(args[portIndex]) : NaN;
130
164
  const port = Number.isFinite(portArg) && portArg > 0 ? portArg : cfg0?.web?.port || 3820;
131
165
  const host = cfg0?.web?.host || '127.0.0.1';
package/src/config.js CHANGED
@@ -47,7 +47,7 @@ export function effectiveApiKey(cfg, providerName) {
47
47
  }
48
48
 
49
49
  export async function runWizard(io) {
50
- io.box('MingDao 初始化向导', ['三步完成:选服务商/模型 → 填 API Key 选权限模式']);
50
+ io.box('MingDao Harness 初始化向导', [' 选服务商 填 API Key(自动验证)→ 选模型(可跳过)']);
51
51
  io.print('');
52
52
 
53
53
  const providerKeys = Object.keys(PROVIDERS);
@@ -57,33 +57,63 @@ export async function runWizard(io) {
57
57
  );
58
58
  const pp = PROVIDERS[provider];
59
59
 
60
- let model;
60
+ let baseUrl = '';
61
61
  if (provider === 'custom') {
62
- model = await io.ask('② 模型名(例如 deepseek-v4-flash):');
63
- } else {
64
- const options = pp.models.map((m) => {
65
- const preset = modelPreset(m);
66
- return { value: m, label: preset ? `${m} — ${preset.label}` : m };
67
- });
68
- options.push({ value: '__custom__', label: '自定义模型名(手动输入)' });
69
- const choice = await io.choose('② 选择模型:', options);
70
- model = choice === '__custom__' ? await io.ask('模型名:') : choice;
62
+ baseUrl = await io.ask(`API 地址(回车默认 ${pp.baseUrl}):`);
71
63
  }
72
-
73
- const envDetected = pp.envKey && process.env[pp.envKey] ? `(检测到环境变量 ${pp.envKey},回车直接使用)` : '';
74
- const apiKey = await io.ask(`③ API Key${envDetected}:`, { hidden: true });
75
-
76
- if (apiKey) {
77
- setStoredKey(provider, apiKey);
78
- io.print(`✓ API Key 已保存到独立凭证库(权限 600),不会写入 config.json。`);
79
- } else {
80
- io.print(`✓ 未输入 API Key:将使用环境变量 ${pp.envKey || 'MINGDAO_API_KEY'}。`);
64
+ const provisional = { provider, baseUrl: baseUrl || pp.baseUrl };
65
+ const hasEnvKey = () => Boolean((pp.envKey && process.env[pp.envKey]) || process.env.MINGDAO_API_KEY);
66
+
67
+ // ② 选定服务商后立即输入该服务商的 API Key,随后调用 /models 验证有效性
68
+ let apiKey = '';
69
+ let models = null; // 验证通过的线上模型名单
70
+ let verifyError = null;
71
+ for (let tries = 0; tries < 3; tries++) {
72
+ const envDetected = hasEnvKey() ? '(检测到环境变量,回车直接使用)' : '';
73
+ const input = await io.ask(`② ${provider} 的 API Key${envDetected}:`, { hidden: true });
74
+ apiKey = String(input || '').trim();
75
+ if (apiKey) setStoredKey(provider, apiKey);
76
+ if (!apiKey && !hasEnvKey()) {
77
+ const skipped = await io.confirm(' 未输入 API Key,跳过密钥设置?');
78
+ if (!skipped) continue;
79
+ break;
80
+ }
81
+ io.print(' 正在验证 API Key 有效性(调用 /models)…');
82
+ const { fetchProviderModels } = await import('./model-discovery.js');
83
+ const r = await fetchProviderModels(provisional, provider, { force: true });
84
+ if (r.models?.length) {
85
+ models = r.models;
86
+ break;
87
+ }
88
+ verifyError = r.error;
89
+ const retry = await io.confirm(`⚠ 验证失败:${r.error}(可能是密钥错误,或该网关不支持 /models 接口)。重新输入密钥?`);
90
+ if (retry) continue;
91
+ break; // 用户选择继续(稍后自行验证)
81
92
  }
93
+ if (apiKey) io.print('✓ API Key 已保存到独立凭证库(权限 600),不会写入 config.json。');
94
+ else io.print('✓ 未输入 API Key:将使用环境变量(稍后可用 mingdao key set 补齐)。');
95
+ if (models) io.print(`✓ API Key 验证通过:该服务商线上可用模型 ${models.length} 个。`);
96
+ else if (verifyError) io.print('(已跳过验证,稍后可在 WebUI 设置面板「刷新模型」处再次校验)');
82
97
 
83
- let baseUrl = '';
98
+ // 选择模型(允许暂时跳过——不写 config.model,进入后默认用该服务商首个模型,/model 可随时换)
99
+ let model = null;
84
100
  if (provider === 'custom') {
85
- baseUrl = await io.ask(`API 地址(回车默认 ${pp.baseUrl}):`);
101
+ const m = await io.ask('③ 模型名(可留空跳过,稍后 /model 再选):');
102
+ model = m.trim() || null;
103
+ } else {
104
+ const options = [{ value: '__skip__', label: '暂时跳过(稍后 /model 再选)' }];
105
+ for (const m of models || pp.models) {
106
+ const preset = modelPreset(m);
107
+ options.push({ value: m, label: preset ? `${m} — ${preset.label}` : m });
108
+ }
109
+ options.push({ value: '__custom__', label: '自定义模型名(手动输入)' });
110
+ const choice = await io.choose('③ 选择模型:', options);
111
+ if (choice === '__skip__') model = null;
112
+ else if (choice === '__custom__') model = (await io.ask('模型名:')).trim() || null;
113
+ else model = choice;
86
114
  }
115
+ if (model) io.print(`✓ 已选择模型:${model}`);
116
+ else io.print('✓ 已跳过模型选择:进入后自动使用该服务商默认模型,输入 /model 可随时切换。');
87
117
 
88
118
  const perm = await io.choose('权限模式(写文件 / 执行命令时):', [
89
119
  { value: 'ask', label: 'ask — 每次询问(推荐,最安全)' },
@@ -106,7 +136,7 @@ export async function runWizard(io) {
106
136
  if (routeChoice === 'on') routing = { enabled: true, planner: 'deepseek-v4-pro', executor: 'deepseek-v4-flash' };
107
137
  }
108
138
 
109
- const preset = modelPreset(model);
139
+ const preset = model ? modelPreset(model) : null;
110
140
  const defaultBudget = preset?.budgetTokens ?? 128000;
111
141
  const budgetInput = await io.ask(`上下文预算 tokens(回车默认 ${defaultBudget}):`);
112
142
  const contextBudget = Number(budgetInput) > 0 ? Number(budgetInput) : defaultBudget;
@@ -114,18 +144,18 @@ export async function runWizard(io) {
114
144
  // 注意:config.json 不含任何密钥(可安全分享/提交),密钥只存 credentials.json。
115
145
  const cfg = {
116
146
  provider,
117
- model,
118
147
  baseUrl: baseUrl || pp.baseUrl,
119
148
  permission: perm,
120
149
  sandbox,
121
150
  contextBudget,
122
151
  };
152
+ if (model) cfg.model = model;
123
153
  if (routing) cfg.routing = routing;
124
154
  saveConfig(cfg);
125
155
  io.print('');
126
156
  io.box('配置完成 ✓', [
127
157
  `服务商 ${provider}(${pp.label})`,
128
- `模型 ${model}`,
158
+ model ? `模型 ${model}` : '模型 (暂未选择,进入后 /model 随时切换)',
129
159
  `权限 ${perm} · 沙箱 ${sandbox}`,
130
160
  `路由 ${routing ? '自动(pro⇄flash)' : '关闭'}`,
131
161
  `密钥 ${apiKey ? '凭证库 ' + maskKey(apiKey) : '环境变量 ' + (pp.envKey || 'MINGDAO_API_KEY')}`,
@@ -119,6 +119,8 @@ footer{flex:none;border-top:1px solid var(--border);background:var(--bg2);paddin
119
119
  .cfg-row select,.cfg-row input[type=number]{background:var(--bg3);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:6px 10px;font-size:13px}
120
120
  .cfg-hint{font-size:12px;color:var(--faint);margin-top:4px}
121
121
  .modal .row{display:flex;gap:10px;justify-content:flex-end}
122
+ .dir-entry{padding:6px 10px;border-radius:6px;cursor:pointer;font-size:13.5px;word-break:break-all}
123
+ .dir-entry:hover{background:var(--bg3)}
122
124
  .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}
123
125
  @keyframes spin{to{transform:rotate(360deg)}}
124
126
  </style>
@@ -188,7 +190,7 @@ footer{flex:none;border-top:1px solid var(--border);background:var(--bg2);paddin
188
190
  <h4 style="margin:4px 0;color:var(--accent2)">云同步(跨设备会话同步,服务器端运行 mingdao sync-server)</h4>
189
191
  <div id="syncStatus" style="font-size:12.5px;color:var(--dim);padding:2px"></div>
190
192
  <div class="cfg-row" style="display:flex;gap:6px;flex-wrap:wrap">
191
- <input id="syncUrl" placeholder="服务器地址 https://…" style="flex:1.4;min-width:150px;background:var(--bg3);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:6px 10px;font-size:13px">
193
+ <input id="syncUrl" placeholder="默认 https://session.mingdao.ai/" style="flex:1.4;min-width:150px;background:var(--bg3);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:6px 10px;font-size:13px">
192
194
  <input id="syncUser" placeholder="用户名" style="flex:1;min-width:90px;background:var(--bg3);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:6px 10px;font-size:13px">
193
195
  </div>
194
196
  <div class="cfg-row" style="display:flex;gap:6px;flex-wrap:wrap">
@@ -269,6 +271,19 @@ footer{flex:none;border-top:1px solid var(--border);background:var(--bg2);paddin
269
271
  <div class="row"><button id="cfgCancel">取消</button><button class="primary" id="cfgSave">保存</button></div>
270
272
  </div>
271
273
  </div>
274
+ <!-- 目录选择器(新建工作空间 / 改目录:浏览服务器磁盘目录树) -->
275
+ <div id="dirModal" class="modal-mask" style="display:none">
276
+ <div class="modal" style="width:min(520px,92vw);display:flex;flex-direction:column;max-height:78vh">
277
+ <h4 style="margin:0 0 6px;color:var(--cyan)">选择目录(点目录进入,点「使用此目录」确认)</h4>
278
+ <div id="dirPath" style="font-size:12px;color:var(--dim);word-break:break-all;margin-bottom:8px;background:var(--bg3);border:1px solid var(--border);border-radius:8px;padding:6px 10px"></div>
279
+ <div id="dirList" style="flex:1;overflow-y:auto;border:1px solid var(--border);border-radius:8px;padding:6px;min-height:220px"></div>
280
+ <div class="row" style="margin-top:12px">
281
+ <button id="dirPickNone" class="ghost">不指定(当前目录)</button>
282
+ <button id="dirPickCancel" class="ghost">取消</button>
283
+ <button id="dirPickOk" class="primary">使用此目录</button>
284
+ </div>
285
+ </div>
286
+ </div>
272
287
  <script>
273
288
  // 访问令牌(P1-3):地址带 ?token= 时记入 sessionStorage 并从地址栏移除(防截图/历史外泄),
274
289
  // 之后所有同源请求统一附加 X-MingDao-Token 头;无令牌时行为与旧版一致。
@@ -291,6 +306,53 @@ if (AUTH_TOKEN) {
291
306
  };
292
307
  }
293
308
  const $ = (s) => document.querySelector(s);
309
+ // —— 目录选择器(新建工作空间 / 改目录):浏览服务器磁盘目录树 ——
310
+ let pickerCb = null, pickerDir = '/';
311
+ function loadDirList(){
312
+ $('#dirPath').textContent = pickerDir;
313
+ const box = $('#dirList');
314
+ fetch('/api/fs-browse?dir=' + encodeURIComponent(pickerDir), { cache: 'no-store' })
315
+ .then((r) => r.json())
316
+ .then((j) => {
317
+ box.innerHTML = '';
318
+ if (!j.ok) {
319
+ const e = document.createElement('div'); e.className = 'dir-entry'; e.style.color = 'var(--err)';
320
+ e.textContent = j.error || '无法读取该目录'; box.appendChild(e); return;
321
+ }
322
+ if (j.parent != null) {
323
+ const up = document.createElement('div'); up.className = 'dir-entry'; up.textContent = '⬆ …(上级目录)';
324
+ up.onclick = () => { pickerDir = j.parent; loadDirList(); };
325
+ box.appendChild(up);
326
+ }
327
+ if (!(j.entries || []).length) {
328
+ const e = document.createElement('div'); e.className = 'dir-entry'; e.style.color = 'var(--faint)';
329
+ e.textContent = '(无子目录)'; box.appendChild(e);
330
+ }
331
+ for (const en of j.entries || []) {
332
+ const d = document.createElement('div'); d.className = 'dir-entry'; d.textContent = '📁 ' + en.name;
333
+ d.onclick = () => { pickerDir = en.path; loadDirList(); };
334
+ box.appendChild(d);
335
+ }
336
+ })
337
+ .catch(() => {
338
+ box.innerHTML = '<div class="dir-entry" style="color:var(--err)">请求失败(请重试)</div>';
339
+ });
340
+ }
341
+ function openDirPicker(startDir, cb){
342
+ pickerCb = cb;
343
+ if (startDir) { pickerDir = startDir; loadDirList(); }
344
+ else {
345
+ // 无指定起点:从服务器当前工作目录开始浏览
346
+ fetch('/api/workspaces', { cache: 'no-store' }).then((r) => r.json()).then((j) => {
347
+ pickerDir = (j && j.cwd) || '/';
348
+ loadDirList();
349
+ }).catch(() => { pickerDir = '/'; loadDirList(); });
350
+ }
351
+ $('#dirModal').style.display = 'flex';
352
+ }
353
+ $('#dirPickOk').onclick = () => { const cb = pickerCb; pickerCb = null; $('#dirModal').style.display = 'none'; if (cb) cb(pickerDir); };
354
+ $('#dirPickNone').onclick = () => { const cb = pickerCb; pickerCb = null; $('#dirModal').style.display = 'none'; if (cb) cb(null); };
355
+ $('#dirPickCancel').onclick = () => { pickerCb = null; $('#dirModal').style.display = 'none'; };
294
356
  const chatEl = $('#chat'), input = $('#input'), sendBtn = $('#sendBtn');
295
357
  let generating = false; // 生成中:发送按钮复用为停止按钮
296
358
  let currentSession = null, thinking = null;
@@ -637,11 +699,13 @@ $('#wsSel').addEventListener('change', async e=>{
637
699
  const v=e.target.value;
638
700
  if(v==='__add__'){
639
701
  const name=prompt('新工作空间名称:'); if(!name){ refreshWsSel(); return; }
640
- const dir=prompt('目录路径(留空 = 当前目录,目录不存在会自动创建):');
641
- const r=await fetch('/api/workspaces',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'add',name,dir:dir||undefined})});
642
- const j=await r.json().catch(()=>({error:'请求失败'}));
643
- if(j.ok) renderBanner({text:'✓ 已登记工作空间 '+j.name+' → '+j.dir+'(目录已自动创建)'}); else alert(j.error||'创建失败');
644
- refreshWsSel(); reloadModels();
702
+ openDirPicker(null, (dir)=>{
703
+ fetch('/api/workspaces',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'add',name,dir:dir||undefined})})
704
+ .then((r)=>r.json()).then((j)=>{
705
+ if(j.ok) renderBanner({text:'✓ 已登记工作空间 '+j.name+' → '+j.dir+'(目录已自动创建)'}); else alert(j.error||'创建失败');
706
+ refreshWsSel(); reloadModels();
707
+ });
708
+ });
645
709
  return;
646
710
  }
647
711
  if(v==='__manage__'){
@@ -698,7 +762,7 @@ async function refreshWorkspaces(){
698
762
  for(const w of j.workspaces){
699
763
  const div=document.createElement('div'); div.style.cssText='display:flex;align-items:center;gap:6px;padding:5px 0;border-bottom:1px solid var(--border);font-size:12px';
700
764
  div.innerHTML='<span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+esc(w.name)+(w.name===j.current?' <span style="color:var(--accent)">●当前</span>':'')+'</span><span style="flex:1.4;color:var(--faint);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;direction:rtl;text-align:left">'+esc(w.dir||'')+'</span>';
701
- const ed=document.createElement('button'); ed.textContent='改目录'; ed.style.cssText='padding:1px 8px;font-size:11px'; ed.onclick=async()=>{ const d=prompt('新目录:', w.dir); if(!d) return; const rr=await fetch('/api/workspaces',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'set',name:w.name,dir:d})}); const jj=await rr.json().catch(()=>({error:'失败'})); if(jj.ok){ refreshWorkspaces(); refreshWsSel(); reloadModels(); } else alert(jj.error||'修改失败'); };
765
+ const ed=document.createElement('button'); ed.textContent='改目录'; ed.style.cssText='padding:1px 8px;font-size:11px'; ed.onclick=()=>{ openDirPicker(w.dir||null, async (d)=>{ if(d==null) return; const rr=await fetch('/api/workspaces',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'set',name:w.name,dir:d})}); const jj=await rr.json().catch(()=>({error:'失败'})); if(jj.ok){ refreshWorkspaces(); refreshWsSel(); reloadModels(); } else alert(jj.error||'修改失败'); }); };
702
766
  const rn=document.createElement('button'); rn.textContent='重命名'; rn.style.cssText='padding:1px 8px;font-size:11px'; rn.onclick=async()=>{ const t=prompt('新名称:', w.name); if(!t) return; const rr=await fetch('/api/workspaces',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'rename',name:w.name,newName:t})}); const jj=await rr.json().catch(()=>({error:'失败'})); if(jj.ok){ refreshWorkspaces(); refreshWsSel(); } else alert(jj.error||'重命名失败'); };
703
767
  const rm=document.createElement('button'); rm.textContent='删除'; rm.className='danger'; rm.style.cssText='padding:1px 8px;font-size:11px'; rm.onclick=async()=>{ if(!confirm('删除工作空间 '+w.name+'?')) return; await fetch('/api/workspaces',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'remove',name:w.name})}); refreshWorkspaces(); refreshWsSel(); reloadModels(); };
704
768
  div.appendChild(ed); div.appendChild(rn); div.appendChild(rm); list.appendChild(div);
@@ -862,7 +926,7 @@ $('#baseUrlSave').onclick=async ()=>{
862
926
  async function refreshSyncUI(){
863
927
  const r=await fetch('/api/sync',{cache:'no-store'}).catch(()=>null); if(!r) return;
864
928
  const j=await r.json();
865
- $('#syncUrl').value=j.url||''; $('#syncUser').value=j.username||''; $('#syncDevice').value=j.deviceName||''; $('#syncAutoChk').checked=Boolean(j.auto);
929
+ $('#syncUrl').value=j.url||'https://session.mingdao.ai/'; $('#syncUser').value=j.username||''; $('#syncDevice').value=j.deviceName||''; $('#syncAutoChk').checked=Boolean(j.auto);
866
930
  const st=$('#syncStatus');
867
931
  if(!j.configured) st.innerHTML='<span style="color:var(--faint)">未配置:填服务器地址并登录(服务器端运行 mingdao sync-server)</span>';
868
932
  else if(!j.loggedIn) st.innerHTML='<span style="color:var(--warn)">已配置 '+esc(j.url)+' · 未登录(输入密码登录)</span>';
package/src/web/server.js CHANGED
@@ -892,7 +892,7 @@ export async function runWebServer({ host = '127.0.0.1', port = 3820, authToken
892
892
  }
893
893
  }
894
894
  if (req.method === 'GET' && p === '/api/workspaces') {
895
- json(res, 200, { ok: true, workspaces: listWorkspaces(), current: currentWorkspace(workingDir)?.name || null });
895
+ json(res, 200, { ok: true, workspaces: listWorkspaces(), current: currentWorkspace(workingDir)?.name || null, cwd: workingDir });
896
896
  return;
897
897
  }
898
898
  if (req.method === 'POST' && p === '/api/workspaces') {
@@ -944,6 +944,27 @@ export async function runWebServer({ host = '127.0.0.1', port = 3820, authToken
944
944
  }
945
945
  return json(res, 400, { error: '未知操作:add|rename|set|remove' });
946
946
  }
947
+ // 目录浏览器(「新建工作空间」选择电脑磁盘目录用):本机运行时即用户电脑的目录树;
948
+ // 只列子目录(不含隐藏目录),供前端逐级导航选择
949
+ if (req.method === 'GET' && p === '/api/fs-browse') {
950
+ let dir = String(url.searchParams.get('dir') || '').trim();
951
+ if (!path.isAbsolute(dir)) return json(res, 400, { error: '需要绝对路径' });
952
+ try {
953
+ const st = fs.statSync(dir);
954
+ if (!st.isDirectory()) return json(res, 400, { error: '不是目录' });
955
+ const entries = fs
956
+ .readdirSync(dir, { withFileTypes: true })
957
+ .filter((e) => e.isDirectory() && !e.name.startsWith('.'))
958
+ .map((e) => ({ name: e.name, path: path.join(dir, e.name) }))
959
+ .sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'))
960
+ .slice(0, 300);
961
+ const parent = path.dirname(dir);
962
+ json(res, 200, { ok: true, path: dir, parent: parent === dir ? null : parent, entries });
963
+ } catch (err) {
964
+ json(res, 400, { error: String(err?.message || err) });
965
+ }
966
+ return;
967
+ }
947
968
  if (req.method === 'GET' && p === '/api/memory') {
948
969
  json(res, 200, { ok: true, content: loadMemory() });
949
970
  return;