@wendongfly/myhi 1.3.198 → 1.3.200
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/dist/chat.html +86 -11
- package/dist/index.html +132 -0
- package/dist/index.js +1 -1
- package/dist/index.min.js +172 -170
- package/package.json +1 -1
package/dist/chat.html
CHANGED
|
@@ -93,6 +93,12 @@
|
|
|
93
93
|
html.m #ctx-chip:not(.hi) { display: none !important; }
|
|
94
94
|
html.m #sk-builtin-skills, html.m #sk-custom-skills { display: none !important; }
|
|
95
95
|
html.m #top-bar { transition: margin-top 0.2s ease; }
|
|
96
|
+
/* 安全区只在「添加到主屏幕」全屏运行时才需要;普通浏览器标签页上面已有地址栏,
|
|
97
|
+
部分国产浏览器仍报状态栏高度 → 顶栏上方白空一行,这里不留 */
|
|
98
|
+
@media (display-mode: browser) {
|
|
99
|
+
#top-bar { padding-top: 0.5rem; }
|
|
100
|
+
#m-toast { top: 3.2rem; }
|
|
101
|
+
}
|
|
96
102
|
#m-more { position: relative; font-size: 1.1rem; letter-spacing: 1px; }
|
|
97
103
|
#m-more.dot::after { content: ''; position: absolute; top: 6px; right: 5px; width: 7px; height: 7px; border-radius: 50%; background: var(--red); }
|
|
98
104
|
#m-ro { font-size: 0.68rem; color: var(--muted); border: 1px solid var(--border); border-radius: 10px; padding: 0.1rem 0.45rem; white-space: nowrap; flex-shrink: 0; cursor: pointer; }
|
|
@@ -444,7 +450,10 @@
|
|
|
444
450
|
#fedit-ta:focus { border-color: var(--purple); }
|
|
445
451
|
/* 预览复用记忆面板的 .md 渲染样式,只解开它自带的 max-height/隐藏 */
|
|
446
452
|
#fedit-preview { display: none; max-height: none; flex: 1; min-height: 0; border: 1px solid var(--border); border-radius: 8px; background: var(--bg); }
|
|
453
|
+
#fedit-preview.html { padding: 0; overflow: hidden; background: #fff; }
|
|
454
|
+
#fedit-preview.html iframe { width: 100%; height: 100%; border: 0; display: block; background: #fff; }
|
|
447
455
|
.fedit-tab { flex-shrink: 0; }
|
|
456
|
+
#fedit-sheet .fp-btn { white-space: nowrap; }
|
|
448
457
|
.fedit-tab.on { border-color: var(--purple); background: var(--purple); color: #fff; }
|
|
449
458
|
.fp-btn-warn { background: var(--yellow2); border-color: #d2992280; }
|
|
450
459
|
.fp-btn-warn:active { background: var(--yellow2); }
|
|
@@ -1171,10 +1180,13 @@
|
|
|
1171
1180
|
<div class="action-sheet-backdrop" onclick="closeFileEditor()"></div>
|
|
1172
1181
|
<div class="action-sheet-box" style="height:92vh;max-height:92vh;display:flex;flex-direction:column">
|
|
1173
1182
|
<div class="action-sheet-title" style="display:flex;align-items:center;gap:0.5rem">
|
|
1174
|
-
<span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"
|
|
1183
|
+
<span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"><span id="fedit-icon">✏️</span> <span id="fedit-title">文件</span></span>
|
|
1175
1184
|
<span id="fedit-state" style="font-size:0.64rem;color:var(--muted);flex-shrink:0;font-weight:400"></span>
|
|
1176
1185
|
</div>
|
|
1177
|
-
<div
|
|
1186
|
+
<div style="display:flex;align-items:flex-start;gap:0.4rem;padding:0 0.2rem 0.4rem">
|
|
1187
|
+
<div id="fedit-path" style="flex:1;min-width:0;font-size:0.68rem;color:var(--muted);word-break:break-all;font-family:'SF Mono',Consolas,monospace" title="文件所在位置"></div>
|
|
1188
|
+
<button class="fp-btn" onclick="feditCopyPath(this)" style="flex-shrink:0;font-size:0.66rem;padding:0.15rem 0.45rem" title="复制完整路径">📋 复制路径</button>
|
|
1189
|
+
</div>
|
|
1178
1190
|
<div style="flex:1;min-height:0;display:flex;flex-direction:column;gap:0.4rem;padding:0 0.2rem">
|
|
1179
1191
|
<div id="fedit-loading" style="flex:1;display:flex;align-items:center;justify-content:center;color:var(--muted);font-size:0.82rem">加载中...</div>
|
|
1180
1192
|
<textarea id="fedit-ta" spellcheck="false"></textarea>
|
|
@@ -1183,6 +1195,7 @@
|
|
|
1183
1195
|
<div style="display:flex;gap:0.4rem;padding:0.5rem 0.2rem 0;align-items:center">
|
|
1184
1196
|
<button class="fp-btn fedit-tab" id="fedit-tab-edit" onclick="feditSetMode('edit')">✏️ 编辑</button>
|
|
1185
1197
|
<button class="fp-btn fedit-tab" id="fedit-tab-preview" onclick="feditSetMode('preview')" style="display:none">👁 预览</button>
|
|
1198
|
+
<button class="fp-btn" id="fedit-newtab" onclick="feditOpenTab()" style="display:none" title="在浏览器新标签页里全屏查看(已保存的版本)">↗ 全屏</button>
|
|
1186
1199
|
<span style="flex:1"></span>
|
|
1187
1200
|
<button class="fp-btn" onclick="feditReload()" title="放弃本地修改,重新读磁盘最新内容">🔄 重读</button>
|
|
1188
1201
|
<button class="fp-btn" onclick="closeFileEditor()">关闭</button>
|
|
@@ -1770,6 +1783,7 @@
|
|
|
1770
1783
|
if (h.start > last) frag.appendChild(document.createTextNode(s.slice(last, h.start)));
|
|
1771
1784
|
const a = document.createElement('span');
|
|
1772
1785
|
a.className = 'file-link';
|
|
1786
|
+
a.title = /\.(html?|md|markdown)$/i.test(h.text) ? '点击查看(可切到编辑)' : '点击查看 / 编辑';
|
|
1773
1787
|
a.textContent = h.text;
|
|
1774
1788
|
a.dataset.path = h.text;
|
|
1775
1789
|
frag.appendChild(a);
|
|
@@ -4241,14 +4255,19 @@
|
|
|
4241
4255
|
const sheet = document.getElementById('fedit-sheet');
|
|
4242
4256
|
sheet.classList.add('open');
|
|
4243
4257
|
document.getElementById('fedit-title').textContent = rel.split('/').pop();
|
|
4244
|
-
document.getElementById('fedit-path').textContent = rel;
|
|
4258
|
+
document.getElementById('fedit-path').textContent = feditAbs(rel);
|
|
4245
4259
|
document.getElementById('fedit-loading').style.display = 'flex';
|
|
4246
4260
|
document.getElementById('fedit-loading').textContent = '加载中...';
|
|
4247
4261
|
document.getElementById('fedit-ta').style.display = 'none';
|
|
4248
4262
|
document.getElementById('fedit-preview').style.display = 'none';
|
|
4249
4263
|
feditSetState('');
|
|
4250
4264
|
try {
|
|
4251
|
-
|
|
4265
|
+
let r = await fetch(`/api/files/${SESSION_ID}/content?path=${encodeURIComponent(rel)}`);
|
|
4266
|
+
// AI 常只写文件名(不带目录):根目录没有就在任务目录里按名字找,唯一命中就打开它
|
|
4267
|
+
if (r.status === 404 && !rel.includes('/')) {
|
|
4268
|
+
const hit = await feditFindByName(rel);
|
|
4269
|
+
if (hit) { r = await fetch(`/api/files/${SESSION_ID}/content?path=${encodeURIComponent(hit)}`); document.getElementById('fedit-path').textContent = feditAbs(hit); }
|
|
4270
|
+
}
|
|
4252
4271
|
const d = await r.json().catch(() => ({}));
|
|
4253
4272
|
if (!r.ok || !d.ok) {
|
|
4254
4273
|
const map = { 404: '文件不存在(可能还没生成,或路径不对)', 403: '路径越界(不在会话目录内)', 413: '文件超过 2MB,请用下载' };
|
|
@@ -4256,26 +4275,82 @@
|
|
|
4256
4275
|
sheet.classList.remove('open');
|
|
4257
4276
|
return;
|
|
4258
4277
|
}
|
|
4259
|
-
|
|
4278
|
+
const html = /\.html?$/i.test(d.name || d.path || '');
|
|
4279
|
+
_fedit = { path: d.path, name: d.name, mtime: d.mtime, md: !!d.md, html, mode: 'edit', loaded: d.content, dirty: false };
|
|
4280
|
+
document.getElementById('fedit-path').textContent = feditAbs(d.path);
|
|
4260
4281
|
document.getElementById('fedit-ta').value = d.content;
|
|
4261
4282
|
document.getElementById('fedit-loading').style.display = 'none';
|
|
4262
|
-
document.getElementById('fedit-tab-preview').style.display = _fedit.md ? '' : 'none';
|
|
4263
|
-
|
|
4283
|
+
document.getElementById('fedit-tab-preview').style.display = (_fedit.md || html) ? '' : 'none';
|
|
4284
|
+
document.getElementById('fedit-newtab').style.display = html ? '' : 'none';
|
|
4285
|
+
// 文档类(md / html)默认「看」,要改再点 ✏️ 编辑
|
|
4286
|
+
feditSetMode(_fedit.md || html ? 'preview' : 'edit');
|
|
4264
4287
|
} catch (e) {
|
|
4265
4288
|
addStatusMessage('打开出错: ' + e.message);
|
|
4266
4289
|
sheet.classList.remove('open');
|
|
4267
4290
|
}
|
|
4268
4291
|
};
|
|
4269
4292
|
|
|
4293
|
+
// 完整路径(任务目录 + 相对路径),方便知道文件在哪、复制给别的工具
|
|
4294
|
+
function feditAbs(rel) {
|
|
4295
|
+
rel = String(rel || '');
|
|
4296
|
+
if (/^[A-Za-z]:[\\/]/.test(rel) || rel.startsWith('/')) return rel;
|
|
4297
|
+
const cwd = String(currentSession?.cwd || '');
|
|
4298
|
+
if (!cwd) return rel;
|
|
4299
|
+
const sep = cwd.includes('\\') ? '\\' : '/';
|
|
4300
|
+
return cwd.replace(/[\\/]+$/, '') + sep + rel.split('/').join(sep);
|
|
4301
|
+
}
|
|
4302
|
+
window.feditCopyPath = function(btn) {
|
|
4303
|
+
const t = document.getElementById('fedit-path').textContent;
|
|
4304
|
+
const done = () => { const o = btn.textContent; btn.textContent = '✓ 已复制'; setTimeout(() => { btn.textContent = o; }, 1200); };
|
|
4305
|
+
(navigator.clipboard?.writeText(t) || Promise.reject()).then(done, () => { const x = document.createElement('textarea'); x.value = t; document.body.appendChild(x); x.select(); try { document.execCommand('copy'); done(); } catch {} x.remove(); });
|
|
4306
|
+
};
|
|
4307
|
+
async function feditFindByName(name) {
|
|
4308
|
+
try {
|
|
4309
|
+
const d = await (await fetch(`/api/files/${SESSION_ID}/search?q=${encodeURIComponent(name)}`)).json();
|
|
4310
|
+
const hits = (d.files || []).filter(f => !f.dir && f.name.split('/').pop() === name);
|
|
4311
|
+
return hits.length === 1 ? hits[0].name : null;
|
|
4312
|
+
} catch { return null; }
|
|
4313
|
+
}
|
|
4314
|
+
// HTML 预览令牌(见 server /view/:token):同一任务复用,过期前换新
|
|
4315
|
+
let _viewTok = null;
|
|
4316
|
+
async function feditViewBase() {
|
|
4317
|
+
if (!_viewTok || _viewTok.exp < Date.now()) {
|
|
4318
|
+
const d = await (await fetch(`/api/files/${SESSION_ID}/view-token`, { method: 'POST' })).json();
|
|
4319
|
+
if (!d.ok) throw new Error(d.error || '预览不可用');
|
|
4320
|
+
_viewTok = { token: d.token, exp: Date.now() + 3600_000 };
|
|
4321
|
+
}
|
|
4322
|
+
return '/view/' + _viewTok.token + '/';
|
|
4323
|
+
}
|
|
4324
|
+
const feditEncPath = (p) => String(p).split('/').map(encodeURIComponent).join('/');
|
|
4325
|
+
window.feditOpenTab = async function() {
|
|
4326
|
+
if (!_fedit) return;
|
|
4327
|
+
try { window.open((await feditViewBase()) + feditEncPath(_fedit.path), '_blank'); } catch (e) { addStatusMessage('打开失败: ' + e.message); }
|
|
4328
|
+
};
|
|
4329
|
+
// HTML 渲染:沙箱 iframe(不同源,页面脚本碰不到 myhi);内容用编辑框里的(没保存的修改也能看),
|
|
4330
|
+
// 注入 <base> 让图片 / CSS 等相对路径资源走只读预览链接
|
|
4331
|
+
async function feditRenderHtml(pv, src) {
|
|
4332
|
+
pv.innerHTML = '';
|
|
4333
|
+
let base;
|
|
4334
|
+
try { base = (await feditViewBase()) + feditEncPath(_fedit.path.split('/').slice(0, -1).join('/')) + (_fedit.path.includes('/') ? '/' : ''); }
|
|
4335
|
+
catch (e) { pv.textContent = '预览不可用:' + e.message; return; }
|
|
4336
|
+
const f = document.createElement('iframe');
|
|
4337
|
+
f.setAttribute('sandbox', 'allow-scripts allow-popups allow-forms allow-modals');
|
|
4338
|
+
const tag = '<base href="' + location.origin + base + '">';
|
|
4339
|
+
f.srcdoc = /<head[^>]*>/i.test(src) ? src.replace(/<head[^>]*>/i, (m) => m + tag) : tag + src;
|
|
4340
|
+
pv.appendChild(f);
|
|
4341
|
+
}
|
|
4342
|
+
|
|
4270
4343
|
window.feditSetMode = function(mode) {
|
|
4271
4344
|
if (!_fedit) return;
|
|
4272
|
-
if (mode === 'preview' && !_fedit.md) mode = 'edit';
|
|
4345
|
+
if (mode === 'preview' && !_fedit.md && !_fedit.html) mode = 'edit';
|
|
4273
4346
|
_fedit.mode = mode;
|
|
4274
4347
|
const ta = document.getElementById('fedit-ta');
|
|
4275
4348
|
const pv = document.getElementById('fedit-preview');
|
|
4276
4349
|
ta.style.display = mode === 'edit' ? 'block' : 'none';
|
|
4277
4350
|
pv.style.display = mode === 'preview' ? 'block' : 'none';
|
|
4278
|
-
|
|
4351
|
+
pv.classList.toggle('html', mode === 'preview' && !!_fedit.html);
|
|
4352
|
+
document.getElementById('fedit-icon').textContent = mode === 'preview' ? '📄' : '✏️';
|
|
4353
|
+
if (mode === 'preview') { if (_fedit.html) feditRenderHtml(pv, ta.value); else pv.innerHTML = renderMarkdown(ta.value); }
|
|
4279
4354
|
document.getElementById('fedit-tab-edit').classList.toggle('on', mode === 'edit');
|
|
4280
4355
|
document.getElementById('fedit-tab-preview').classList.toggle('on', mode === 'preview');
|
|
4281
4356
|
};
|
|
@@ -7717,7 +7792,7 @@
|
|
|
7717
7792
|
try {
|
|
7718
7793
|
const resp = await fetch('/api/claude/update', { method: 'POST' });
|
|
7719
7794
|
const data = await resp.json();
|
|
7720
|
-
if (!data.ok) { addStatusMessage('claude 升级失败:' + (data.error || '未知')); b.textContent = '⬆ claude 失败'; b.style.color = 'var(--red)'; return; }
|
|
7795
|
+
if (!data.ok) { addStatusMessage('claude 升级失败:' + (data.error || '未知')); alert('Claude 升级没有开始:\n' + (data.error || ('HTTP ' + resp.status))); b.textContent = '⬆ claude 失败'; b.style.color = 'var(--red)'; return; }
|
|
7721
7796
|
addStatusMessage(data.message || 'claude 升级中...');
|
|
7722
7797
|
const poll = setInterval(async () => {
|
|
7723
7798
|
try {
|
|
@@ -7726,7 +7801,7 @@
|
|
|
7726
7801
|
if (!s.running) {
|
|
7727
7802
|
clearInterval(poll);
|
|
7728
7803
|
if (s.ok) { addStatusMessage('Claude Code 已升级到 ' + s.version); b.style.display = 'none'; }
|
|
7729
|
-
else { addStatusMessage('claude 升级失败:\n' + (s.error || '未知')); b.textContent = '⬆ claude
|
|
7804
|
+
else { addStatusMessage('claude 升级失败:\n' + (s.error || '未知')); alert('Claude 升级失败:\n' + (s.error || '未知') + '\n\n可以再点一次重试;仍失败把这段发给管理员'); b.textContent = '⬆ claude 失败(点重试)'; b.style.color = 'var(--red)'; }
|
|
7730
7805
|
}
|
|
7731
7806
|
} catch {}
|
|
7732
7807
|
}, 4000);
|
package/dist/index.html
CHANGED
|
@@ -83,6 +83,28 @@
|
|
|
83
83
|
z-index: 10;
|
|
84
84
|
border-bottom: 1px solid var(--border);
|
|
85
85
|
}
|
|
86
|
+
/* ── 任务迁移面板(导出 / 导入)── */
|
|
87
|
+
#mig-ov { position: fixed; inset: 0; z-index: 40; background: rgba(0,0,0,0.55); display: flex; align-items: flex-end; justify-content: center; }
|
|
88
|
+
#mig-box { background: var(--surface); color: var(--text); width: 100%; max-width: 560px; max-height: 90vh; overflow-y: auto; border-radius: 16px 16px 0 0; padding: 1rem 1.1rem max(1rem, env(safe-area-inset-bottom)); font-size: 0.85rem; line-height: 1.6; }
|
|
89
|
+
@media (min-width: 700px) { #mig-ov { align-items: center; } #mig-box { border-radius: 16px; } }
|
|
90
|
+
#mig-box h3 { font-size: 1.02rem; margin: 0 0 0.5rem; display: flex; justify-content: space-between; align-items: center; }
|
|
91
|
+
#mig-box h3 button { background: none; border: none; color: var(--muted); font-size: 1.1rem; cursor: pointer; }
|
|
92
|
+
#mig-box .mig-intro { color: var(--muted); margin-bottom: 0.7rem; }
|
|
93
|
+
#mig-box .mig-card { background: var(--bg); border: 1px solid var(--border); border-radius: 10px; padding: 0.6rem 0.75rem; margin-bottom: 0.6rem; }
|
|
94
|
+
#mig-box .mig-card b { color: var(--text); }
|
|
95
|
+
#mig-box .mig-row { display: flex; gap: 0.5rem; padding: 0.25rem 0; }
|
|
96
|
+
#mig-box .mig-row > span:first-child { flex-shrink: 0; }
|
|
97
|
+
#mig-box small, #mig-box .mig-dim { color: var(--muted); font-size: 0.76rem; }
|
|
98
|
+
#mig-box ol { margin: 0.2rem 0 0 1.2rem; padding: 0; }
|
|
99
|
+
#mig-box .mig-warn { color: #d29922; }
|
|
100
|
+
#mig-box .mig-btn { width: 100%; border: none; border-radius: 10px; padding: 0.75rem; font-size: 0.9rem; font-weight: 700; cursor: pointer; background: var(--accent); color: #fff; margin-top: 0.3rem; }
|
|
101
|
+
#mig-box .mig-btn:disabled { opacity: 0.5; cursor: default; }
|
|
102
|
+
#mig-box .mig-btn2 { width: 100%; border: 1px solid var(--border); border-radius: 10px; padding: 0.6rem; font-size: 0.84rem; cursor: pointer; background: var(--bg); color: var(--muted); margin-top: 0.45rem; }
|
|
103
|
+
#mig-box input[type=text] { width: 100%; box-sizing: border-box; background: var(--bg); color: var(--text); border: 1px solid var(--border); border-radius: 8px; padding: 0.5rem 0.6rem; font-size: 0.82rem; font-family: 'SF Mono', Consolas, monospace; }
|
|
104
|
+
#mig-box .mig-bar { height: 6px; background: var(--border); border-radius: 3px; overflow: hidden; margin: 0.4rem 0; }
|
|
105
|
+
#mig-box .mig-bar > div { height: 100%; width: 0; background: var(--accent); transition: width 0.2s; }
|
|
106
|
+
/* 普通浏览器标签页不留状态栏安全区(部分手机浏览器带地址栏仍报安全区 → 顶部白空一行) */
|
|
107
|
+
@media (display-mode: browser) { #header { padding-top: 0.45rem; } }
|
|
86
108
|
.logo {
|
|
87
109
|
font-size: 1.35rem;
|
|
88
110
|
font-weight: 700;
|
|
@@ -1001,6 +1023,7 @@
|
|
|
1001
1023
|
<div id="recent-dirs" style="display:flex;flex-wrap:wrap;gap:0.35rem;margin-top:0.4rem"></div>
|
|
1002
1024
|
</div>
|
|
1003
1025
|
<button id="btn-create" onclick="createSession()">创建</button>
|
|
1026
|
+
<button onclick="closeSheet();openMigrateImport()" style="width:100%;background:var(--bg);color:var(--accent);border:1.5px solid var(--accent);border-radius:10px;padding:0.7rem;font-size:0.85rem;font-weight:600;cursor:pointer;margin-top:0.5rem">📦 从迁移包导入<span style="font-weight:400;font-size:0.75rem;opacity:0.8">(从另一台 myhi 带过来的任务)</span></button>
|
|
1004
1027
|
<button id="btn-import" onclick="openImportSheet()" style="width:100%;background:var(--bg);color:var(--accent);border:1.5px solid var(--accent);border-radius:10px;padding:0.7rem;font-size:0.85rem;font-weight:600;cursor:pointer;margin-top:0.5rem">导入已有 Claude 会话</button>
|
|
1005
1028
|
</div>
|
|
1006
1029
|
|
|
@@ -1876,6 +1899,114 @@
|
|
|
1876
1899
|
|
|
1877
1900
|
// ── Navigation ──────────────────────────────────────────────
|
|
1878
1901
|
// 电脑端工作台:在右侧打开;手机/窄屏:进会话页
|
|
1902
|
+
// ── 任务迁移:导出(任务 ⋯ 菜单)/ 导入(新建任务面板)──────────────────
|
|
1903
|
+
// 场景:公司 myhi → 家里 myhi,两边网络不通也没有 GitLab —— 下载一个 .myhipack 文件带过去导入
|
|
1904
|
+
const fmtSize = (n) => n >= 1024 ** 3 ? (n / 1024 ** 3).toFixed(1) + ' GB' : n >= 1024 ** 2 ? (n / 1024 ** 2).toFixed(1) + ' MB' : Math.max(1, Math.round(n / 1024)) + ' KB';
|
|
1905
|
+
function migSheet(html) {
|
|
1906
|
+
document.getElementById('mig-ov')?.remove();
|
|
1907
|
+
const ov = document.createElement('div'); ov.id = 'mig-ov';
|
|
1908
|
+
ov.innerHTML = '<div id="mig-box">' + html + '</div>';
|
|
1909
|
+
ov.addEventListener('click', (e) => { if (e.target === ov || e.target.closest('[data-mig-close]')) ov.remove(); });
|
|
1910
|
+
document.body.appendChild(ov);
|
|
1911
|
+
return ov.firstChild;
|
|
1912
|
+
}
|
|
1913
|
+
const MIG_SKIP_TIP = { '.git': '提交历史(需要的话用 git push / pull 同步)', node_modules: 'npm 依赖(到新电脑执行 npm install)', '.venv': 'Python 虚拟环境(到新电脑重新建)', venv: 'Python 虚拟环境(到新电脑重新建)', upload: '聊天里上传的截图' };
|
|
1914
|
+
|
|
1915
|
+
window.openMigrateExport = async function(id) {
|
|
1916
|
+
const s = _allSessions.find(x => x.id === id) || {};
|
|
1917
|
+
const box = migSheet(`<h3>📦 迁移到另一台 myhi<button data-mig-close>✕</button></h3>
|
|
1918
|
+
<div class="mig-intro">把「${esc(s.title || '')}」打包成<b>一个文件</b>,带到另一台电脑的 myhi 接着做(比如公司 → 家里)。两台电脑<b>不需要联网互通</b>,也不需要 GitLab。</div>
|
|
1919
|
+
<div class="mig-card" id="mig-what"><span class="mig-dim">正在统计要打包的内容…</span></div>
|
|
1920
|
+
<div class="mig-card"><b>怎么用(3 步)</b><ol>
|
|
1921
|
+
<li>点下面「下载迁移包」,得到一个 <code>.myhipack</code> 文件</li>
|
|
1922
|
+
<li>用微信文件传输助手 / U 盘 / 网盘,把它传到另一台电脑</li>
|
|
1923
|
+
<li>在那台电脑打开 myhi 首页 → <b>+ 新建任务</b> → <b>📦 从迁移包导入</b>,选这个文件</li></ol></div>
|
|
1924
|
+
<div class="mig-dim" style="margin-bottom:0.5rem">🔒 迁移包里是项目原文件(和对话记录),请像保管项目代码一样保管,别发到公开群里。</div>
|
|
1925
|
+
<button class="mig-btn" id="mig-dl" disabled>⬇ 下载迁移包</button>
|
|
1926
|
+
<button class="mig-btn2" data-mig-close>取消</button>`);
|
|
1927
|
+
let p;
|
|
1928
|
+
try { p = await (await fetch('/api/migrate/' + encodeURIComponent(id) + '/preview')).json(); } catch (e) { p = { ok: false, error: e.message }; }
|
|
1929
|
+
const what = box.querySelector('#mig-what');
|
|
1930
|
+
if (!p.ok) { what.innerHTML = '<span class="mig-warn">统计失败:' + esc(p.error || '') + '</span>'; return; }
|
|
1931
|
+
const skipped = (p.skipped || []).map(d => `<div class="mig-dim"> · <code>${esc(d)}</code> ${esc(MIG_SKIP_TIP[d] || '可重新生成的目录')}</div>`).join('');
|
|
1932
|
+
what.innerHTML = `<b>包里会有</b>
|
|
1933
|
+
<div class="mig-row"><span>📁</span><div>项目文件 <b>${p.files}</b> 个,共 <b>${fmtSize(p.bytes)}</b><br><small>${esc(p.cwd)}</small>${p.truncated ? '<div class="mig-warn">文件太多,只打包了前 20 万个</div>' : ''}</div></div>
|
|
1934
|
+
${skipped ? `<div class="mig-row"><span>⏭</span><div>不打包(可重新生成,到新电脑再装):${skipped}</div></div>` : ''}
|
|
1935
|
+
<div class="mig-row"><span>💬</span><label style="cursor:pointer"><input type="checkbox" id="mig-conv" ${p.hasConv ? 'checked' : 'disabled'}> 带上 AI 对话记录${p.hasConv ? `(${fmtSize(p.convBytes)})` : ''}<br><small>${p.hasConv ? '带上后在新电脑上可以<b>接着聊</b>,AI 记得之前做过什么;对话里的项目路径会自动改成新位置。项目记忆也一起带走。' : '这个任务还没有对话记录,只带项目文件。'}</small></label></div>
|
|
1936
|
+
${p.guard ? `<div class="mig-row"><span>🛡</span><div>隐私护栏配置(${p.guard.enabled ? '已开启' : '未开启'},主体词典 ${p.guard.entities} 个)和假名映射表一起带走,新电脑上继续脱敏</div></div>` : ''}
|
|
1937
|
+
${p.secrets.length ? `<div class="mig-row"><span>🔑</span><div><b>密钥不会带走</b>(${p.secrets.map(esc).join('、')})<br><small>为了安全,密钥只留在这台电脑。到新电脑后在任务会话页「🔑 密钥」里重新录入。</small></div></div>` : ''}
|
|
1938
|
+
${p.busy ? '<div class="mig-row mig-warn"><span>⚠️</span><div>AI 正在执行,建议等这一轮结束再导出,否则最后一轮对话可能不完整。</div></div>' : ''}`;
|
|
1939
|
+
const btn = box.querySelector('#mig-dl'); btn.disabled = false;
|
|
1940
|
+
btn.onclick = () => {
|
|
1941
|
+
const conv = box.querySelector('#mig-conv')?.checked ? '1' : '0';
|
|
1942
|
+
const a = document.createElement('a'); a.href = '/api/migrate/' + encodeURIComponent(id) + '/export?conv=' + conv; a.download = '';
|
|
1943
|
+
document.body.appendChild(a); a.click(); a.remove();
|
|
1944
|
+
btn.textContent = '✓ 已开始下载'; btn.disabled = true;
|
|
1945
|
+
btn.insertAdjacentHTML('afterend', '<div class="mig-dim" style="margin-top:0.4rem">文件大时浏览器会在后台下载,留意下载栏 / 通知。下载完成后按上面第 2、3 步操作。</div>');
|
|
1946
|
+
};
|
|
1947
|
+
};
|
|
1948
|
+
|
|
1949
|
+
window.openMigrateImport = function() {
|
|
1950
|
+
const box = migSheet(`<h3>📦 从迁移包导入<button data-mig-close>✕</button></h3>
|
|
1951
|
+
<div class="mig-intro">把另一台 myhi 导出的任务(<code>.myhipack</code> 文件)导入到这台电脑,导入后会生成一个新任务,项目文件和 AI 对话都在,可以接着做。</div>
|
|
1952
|
+
<div class="mig-card"><b>还没有迁移包?</b><div class="mig-dim">在原来那台电脑的 myhi 首页,点任务右边的 <b>⋯</b> → <b>📦 迁移到另一台 myhi</b> → 下载迁移包,再用微信 / U 盘 / 网盘传到这台电脑。</div></div>
|
|
1953
|
+
<div class="mig-card" id="mig-step1"><b>① 选择迁移包</b><div style="margin-top:0.4rem"><input type="file" id="mig-file" accept="${'.myhipack'},application/gzip"></div>
|
|
1954
|
+
<div id="mig-up" style="display:none"><div class="mig-bar"><div></div></div><span class="mig-dim" id="mig-up-t">上传中…</span></div></div>
|
|
1955
|
+
<div id="mig-step2"></div>
|
|
1956
|
+
<button class="mig-btn2" data-mig-close>取消</button>`);
|
|
1957
|
+
const step2 = box.querySelector('#mig-step2');
|
|
1958
|
+
box.querySelector('#mig-file').onchange = (ev) => {
|
|
1959
|
+
const f = ev.target.files[0]; if (!f) return;
|
|
1960
|
+
const up = box.querySelector('#mig-up'), bar = up.querySelector('.mig-bar > div'), t = box.querySelector('#mig-up-t');
|
|
1961
|
+
up.style.display = ''; step2.innerHTML = '';
|
|
1962
|
+
const fd = new FormData(); fd.append('pack', f);
|
|
1963
|
+
const xhr = new XMLHttpRequest(); xhr.open('POST', '/api/migrate/upload');
|
|
1964
|
+
xhr.upload.onprogress = (e) => { if (e.lengthComputable) { bar.style.width = (e.loaded / e.total * 100) + '%'; t.textContent = `上传中… ${fmtSize(e.loaded)} / ${fmtSize(e.total)}`; } };
|
|
1965
|
+
xhr.onload = () => {
|
|
1966
|
+
let r; try { r = JSON.parse(xhr.responseText); } catch { r = { ok: false, error: '上传失败(' + xhr.status + ')' }; }
|
|
1967
|
+
if (!r.ok) { t.innerHTML = '<span class="mig-warn">' + esc(r.error) + '</span>'; return; }
|
|
1968
|
+
bar.style.width = '100%'; t.textContent = '已上传,请确认下面的信息';
|
|
1969
|
+
showMigrateConfirm(step2, r);
|
|
1970
|
+
};
|
|
1971
|
+
xhr.onerror = () => { t.innerHTML = '<span class="mig-warn">上传失败,网络中断了,请重试</span>'; };
|
|
1972
|
+
xhr.send(fd);
|
|
1973
|
+
};
|
|
1974
|
+
};
|
|
1975
|
+
function showMigrateConfirm(el, r) {
|
|
1976
|
+
const m = r.manifest;
|
|
1977
|
+
const proj = String(m.cwd || '').replace(/[\\/]+$/, '').split(/[\\/]/).pop() || m.title;
|
|
1978
|
+
let target = defaultTaskDir(proj);
|
|
1979
|
+
if (r.userDir && !target) target = r.userDir.replace(/[\\/]+$/, '') + (r.userDir.includes('\\') ? '\\' : '/') + sanitizeTaskName(proj);
|
|
1980
|
+
const skipped = (m.skipped || []).map(d => `<code>${esc(d)}</code>`).join('、');
|
|
1981
|
+
el.innerHTML = `<div class="mig-card"><b>② 确认迁移包内容</b>
|
|
1982
|
+
<div class="mig-row"><span>📝</span><div>任务:<b>${esc(m.title || '')}</b><br><small>来自「${esc(m.from || '另一台 myhi')}」,${esc(new Date(m.exportedAt).toLocaleString())} 导出</small></div></div>
|
|
1983
|
+
<div class="mig-row"><span>📁</span><div>项目文件 ${m.files} 个,共 ${fmtSize(m.bytes || 0)}<br><small>原位置:${esc(m.cwd || '')}</small></div></div>
|
|
1984
|
+
<div class="mig-row"><span>💬</span><div>${m.conv ? 'AI 对话记录:有 —— 导入后可以<b>接着聊</b>' : 'AI 对话记录:没带 —— 导入后是一个新对话(项目文件都在)'}</div></div>
|
|
1985
|
+
${skipped ? `<div class="mig-row"><span>⏭</span><div>原电脑上没打包:${skipped}<br><small>依赖 / 构建产物需要在这台电脑重新安装或生成(例如 npm install);可以导入后直接让 AI 帮你装。</small></div></div>` : ''}
|
|
1986
|
+
${m.guard ? `<div class="mig-row"><span>🛡</span><div>隐私护栏配置一起导入(主体词典 ${m.guard.entities} 个)</div></div>` : ''}
|
|
1987
|
+
${(m.secrets || []).length ? `<div class="mig-row mig-warn"><span>🔑</span><div>需要重新录入的密钥:${m.secrets.map(esc).join('、')}<br><small>导入后在任务会话页「🔑 密钥」里填。</small></div></div>` : ''}
|
|
1988
|
+
</div>
|
|
1989
|
+
<div class="mig-card"><b>③ 放到这台电脑的哪个目录</b>
|
|
1990
|
+
<div style="margin:0.35rem 0"><input type="text" id="mig-cwd" value="${esc(target)}" placeholder="例如 D:\\projects\\${esc(proj)} 或 /home/me/${esc(proj)}" spellcheck="false"></div>
|
|
1991
|
+
<div class="mig-dim">填完整路径。目录不存在会自动创建;<b>已存在且有文件</b>会拒绝导入,避免覆盖。对话里的路径会自动改成这个目录。</div></div>
|
|
1992
|
+
<button class="mig-btn" id="mig-go">导入为新任务</button><div id="mig-res"></div>`;
|
|
1993
|
+
el.querySelector('#mig-go').onclick = async (ev) => {
|
|
1994
|
+
const btn = ev.target, res = el.querySelector('#mig-res');
|
|
1995
|
+
const cwd = el.querySelector('#mig-cwd').value.trim();
|
|
1996
|
+
if (!cwd) { res.innerHTML = '<div class="mig-warn">请填写目标目录</div>'; return; }
|
|
1997
|
+
btn.disabled = true; btn.textContent = '导入中…(文件多时要等一会儿)'; res.innerHTML = '';
|
|
1998
|
+
let d;
|
|
1999
|
+
try { d = await (await fetch('/api/migrate/import', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: r.token, cwd }) })).json(); }
|
|
2000
|
+
catch (e) { d = { ok: false, error: e.message }; }
|
|
2001
|
+
if (!d.ok) { btn.disabled = false; btn.textContent = '导入为新任务'; res.innerHTML = '<div class="mig-warn" style="margin-top:0.4rem">' + esc(d.error) + '</div>'; return; }
|
|
2002
|
+
btn.textContent = '✓ 导入完成';
|
|
2003
|
+
res.innerHTML = `<div style="margin-top:0.5rem">任务已导入到 <code>${esc(cwd)}</code>。${d.conv ? '打开后直接发消息就能接着聊。' : ''}${(d.secrets || []).length ? `<div class="mig-warn">别忘了在会话页「🔑 密钥」里重新录入:${d.secrets.map(esc).join('、')}</div>` : ''}</div>
|
|
2004
|
+
<button class="mig-btn" id="mig-open">打开这个任务</button>`;
|
|
2005
|
+
res.querySelector('#mig-open').onclick = () => { document.getElementById('mig-ov')?.remove(); openSession(d.id); };
|
|
2006
|
+
try { socket.emit('list'); } catch {}
|
|
2007
|
+
};
|
|
2008
|
+
}
|
|
2009
|
+
|
|
1879
2010
|
function openSession(id) {
|
|
1880
2011
|
if (wsOn()) return wsOpen(id);
|
|
1881
2012
|
window.location.href = `/terminal/${id}`;
|
|
@@ -2383,6 +2514,7 @@
|
|
|
2383
2514
|
['✏️ 重命名', () => inlineRename(id)],
|
|
2384
2515
|
['🏷 设置标签', () => openTagEditor(id, anchor || { x: e.clientX, y: e.clientY })],
|
|
2385
2516
|
['↗ 在新窗口打开', () => window.open('/terminal/' + encodeURIComponent(id), '_blank')],
|
|
2517
|
+
['📦 迁移到另一台 myhi', () => openMigrateExport(id)],
|
|
2386
2518
|
['🗑 终止会话', () => showKill(e, id, title), true],
|
|
2387
2519
|
];
|
|
2388
2520
|
if (matchMedia('(pointer: fine)').matches) {
|