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 +1 -1
- package/src/cli.js +18 -1
- package/src/commands/skill.js +37 -3
- package/src/config.js +55 -25
- package/src/web/index.html +72 -8
- package/src/web/server.js +22 -1
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -436,7 +436,8 @@ async function main() {
|
|
|
436
436
|
if (opts.init) return;
|
|
437
437
|
}
|
|
438
438
|
|
|
439
|
-
|
|
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);
|
package/src/commands/skill.js
CHANGED
|
@@ -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 初始化向导', ['
|
|
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
|
|
60
|
+
let baseUrl = '';
|
|
61
61
|
if (provider === 'custom') {
|
|
62
|
-
|
|
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
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
98
|
+
// ③ 选择模型(允许暂时跳过——不写 config.model,进入后默认用该服务商首个模型,/model 可随时换)
|
|
99
|
+
let model = null;
|
|
84
100
|
if (provider === 'custom') {
|
|
85
|
-
|
|
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')}`,
|
package/src/web/index.html
CHANGED
|
@@ -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="
|
|
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
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
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=
|
|
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;
|