@wendongfly/myhi 1.3.204 → 1.3.205

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 CHANGED
@@ -406,7 +406,26 @@
406
406
  .fp-header h3 { flex: 1; font-size: 0.9rem; margin: 0; color: var(--text); }
407
407
  .fp-btn { background: var(--surface2); color: var(--text); border: 1px solid var(--border); border-radius: 6px; padding: 0.3rem 0.6rem; font-size: 0.75rem; cursor: pointer; }
408
408
  .fp-btn:active { background: var(--border); }
409
- .fp-path { padding: 0.4rem 0.75rem; font-size: 0.7rem; color: var(--muted); border-bottom: 1px solid var(--surface2); word-break: break-all; flex-shrink: 0; }
409
+ .fp-path { padding: 0.4rem 0.75rem; font-size: 0.74rem; color: var(--muted); border-bottom: 1px solid var(--surface2); word-break: break-all; flex-shrink: 0; line-height: 1.6; }
410
+ .fp-path .crumb { color: var(--blue); cursor: pointer; }
411
+ .fp-path .crumb:hover { text-decoration: underline; }
412
+ .fp-path .crumb.cur { color: var(--text); cursor: default; text-decoration: none; font-weight: 600; }
413
+ .fp-path .sep { margin: 0 0.25rem; color: var(--muted); }
414
+ .fp-x { padding: 0.25rem 0.55rem; }
415
+ #fp-tools { display: flex; gap: 0.4rem; align-items: center; padding: 0.35rem 0.75rem; border-bottom: 1px solid var(--surface2); flex-shrink: 0; }
416
+ #fp-sel-bar { display: none; align-items: center; gap: 0.3rem; padding: 0.45rem 0.6rem; background: var(--surface2); font-size: 0.76rem; border-top: 1px solid var(--border); flex-shrink: 0; }
417
+ #fp-sel-bar .fp-btn { padding: 0.3rem 0.45rem; white-space: nowrap; }
418
+ .fp-item .fp-more { color: var(--muted); padding: 0 0.45rem; font-size: 1rem; line-height: 1; letter-spacing: 1px; flex-shrink: 0; align-self: flex-start; border-radius: 6px; cursor: pointer; }
419
+ .fp-item .fp-more:hover { background: var(--surface2); color: var(--text); }
420
+ /* 行 / 工具 ⋯ 菜单:电脑上是贴着鼠标的小菜单,手机上是底部抽屉 */
421
+ #fp-menu-ov { position: fixed; inset: 0; z-index: 90; }
422
+ #fp-menu { position: fixed; z-index: 91; background: var(--surface); border: 1px solid var(--border); border-radius: 10px; box-shadow: 0 8px 24px rgba(0,0,0,0.3); padding: 0.3rem 0; min-width: 12rem; max-width: 18rem; }
423
+ #fp-menu.sheet { left: 0 !important; right: 0; bottom: 0; top: auto !important; max-width: none; border-radius: 16px 16px 0 0; padding-bottom: max(0.6rem, env(safe-area-inset-bottom)); }
424
+ #fp-menu .mh { padding: 0.4rem 0.9rem 0.3rem; font-size: 0.7rem; color: var(--muted); word-break: break-all; border-bottom: 1px solid var(--surface2); }
425
+ #fp-menu button { display: block; width: 100%; text-align: left; background: none; border: none; color: var(--text); font-size: 0.84rem; padding: 0.55rem 0.9rem; cursor: pointer; }
426
+ #fp-menu.sheet button { padding: 0.8rem 1.1rem; font-size: 0.9rem; }
427
+ #fp-menu button:hover { background: var(--surface2); }
428
+ #fp-menu button small { color: var(--muted); font-size: 0.7rem; margin-left: 0.3rem; }
410
429
  .fp-list { flex: 1; overflow-y: auto; padding: 0.25rem 0; }
411
430
  .fp-item { display: flex; align-items: center; padding: 0.5rem 0.75rem; gap: 0.5rem; cursor: pointer; font-size: 0.82rem; color: var(--text); }
412
431
  .fp-item:active { background: var(--surface2); }
@@ -419,12 +438,15 @@
419
438
  #fp-multi-btn.on { border-color: var(--purple); color: var(--purple); }
420
439
  .fp-item .fp-dl { color: var(--blue); font-size: 0.78rem; text-decoration: none; padding: 0.2rem 0.3rem; flex-shrink: 0; align-self: flex-start; }
421
440
  .fp-empty { text-align: center; color: var(--muted); padding: 2rem 0.75rem; font-size: 0.85rem; }
441
+ /* 拖拽上传区:平时不占位置,电脑上把文件拖进抽屉时才出现(手机没法拖拽) */
422
442
  .fp-drop-zone {
443
+ display: none;
423
444
  margin: 0.5rem 0.75rem; padding: 1.5rem; border: 2px dashed var(--border); border-radius: 8px;
424
445
  text-align: center; color: var(--muted); font-size: 0.8rem; flex-shrink: 0;
425
446
  transition: border-color 0.2s, background 0.2s;
426
447
  }
427
448
  .fp-drop-zone.dragover { border-color: var(--blue); background: rgba(88,166,255,0.08); }
449
+ #file-panel.dragging .fp-drop-zone { display: block; border-color: var(--blue); background: rgba(88,166,255,0.08); color: var(--blue); }
428
450
  .fp-progress { padding: 0.4rem 0.75rem; font-size: 0.75rem; color: var(--yellow); flex-shrink: 0; display: none; }
429
451
  .fp-sync-bar { display: flex; gap: 0.4rem; padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--surface2); flex-shrink: 0; align-items: center; flex-wrap: wrap; }
430
452
  .fp-sync-bar .fp-btn { font-size: 0.7rem; padding: 0.25rem 0.5rem; }
@@ -826,10 +848,8 @@
826
848
  <div id="file-panel-backdrop" onclick="closeFilePanel()"></div>
827
849
  <div id="file-panel">
828
850
  <div class="fp-header">
829
- <h3 id="fp-title">文件管理</h3>
830
- <button class="fp-btn" onclick="fpUploadClick()">上传</button>
831
- <button class="fp-btn" onclick="fpStartSync()">同步</button>
832
- <button class="fp-btn" onclick="closeFilePanel()">关闭</button>
851
+ <h3 id="fp-title">文件</h3>
852
+ <button class="fp-btn fp-x" onclick="closeFilePanel()" title="关闭">✕</button>
833
853
  </div>
834
854
  <div class="fp-sync-bar" id="fp-sync-bar" style="display:none">
835
855
  <span class="sync-dir" id="fp-sync-dir"></span>
@@ -837,26 +857,29 @@
837
857
  <button class="fp-btn" onclick="fpExitSync()">退出同步</button>
838
858
  </div>
839
859
  <div class="fp-path" id="fp-path"></div>
840
- <div id="fp-tools" style="display:flex;gap:0.4rem;align-items:center;padding:0.3rem 0.75rem;flex-wrap:wrap">
841
- <button class="fp-btn" onclick="fpOpenFolder()" title="在本机桌面打开该任务的文件夹——仅当浏览器与 myhi 服务在同一台机器时有效;远程访问不会有效果,请用下方列表浏览/下载">📂 打开当前文件夹(不支持远程)</button>
860
+ <!-- 工具栏只留常用的两个;同步 / 排序 / 本机打开文件夹收进 ⋯ -->
861
+ <div id="fp-tools">
862
+ <button class="fp-btn" onclick="fpUploadClick()" title="上传文件到当前目录(电脑上也可以直接把文件拖进来)">⬆ 上传</button>
863
+ <button class="fp-btn" id="fp-multi-btn" onclick="fpToggleMulti()" title="勾选多个文件 / 文件夹:打包下载,或一次引用给 AI">☑ 选择</button>
842
864
  <span style="flex:1"></span>
843
- <button class="fp-btn" id="fp-multi-btn" onclick="fpToggleMulti()" title="勾选多个文件 / 文件夹,打包成一个 zip 下载">☑ 多选</button>
844
- <button class="fp-btn" id="fp-sort-btn" onclick="fpToggleSort()" title="切换排序">排序: 名称</button>
865
+ <button class="fp-btn" onclick="fpToolsMenu(event)" title="更多:同步到本地、排序、在本机打开文件夹">⋯</button>
845
866
  </div>
846
- <div id="fp-sel-bar" style="display:none;align-items:center;gap:0.4rem;padding:0.35rem 0.75rem;background:var(--surface2);font-size:0.78rem;flex-wrap:wrap">
847
- <span id="fp-sel-count" style="color:var(--muted)">已选 0 项</span>
867
+ <div class="fp-list" id="fp-list"></div>
868
+ <!-- 选择模式的操作栏放底部:手指够得着 -->
869
+ <div id="fp-sel-bar">
870
+ <span id="fp-sel-count" style="color:var(--muted);white-space:nowrap">已选0</span>
848
871
  <span style="flex:1"></span>
849
872
  <button class="fp-btn" onclick="fpSelAll()">全选</button>
873
+ <button class="fp-btn" id="fp-sel-ref" onclick="fpReferSelected()" disabled title="把选中的文件路径一起插进消息框,让 AI 读这些文件">@ 引用</button>
850
874
  <button class="fp-btn fp-btn-primary" id="fp-sel-dl" onclick="fpZipSelected()" disabled>⬇ 打包下载</button>
851
- <button class="fp-btn" onclick="fpToggleMulti(false)">取消</button>
875
+ <button class="fp-btn" onclick="fpToggleMulti(false)" title="取消选择">✕</button>
852
876
  </div>
853
- <div class="fp-list" id="fp-list"></div>
854
877
  <div class="fp-diff-actions" id="fp-diff-actions" style="display:none">
855
878
  <button class="fp-btn fp-btn-primary" onclick="fpSyncToLocal()">下载到本地</button>
856
879
  <button class="fp-btn fp-btn-warn" onclick="fpSyncToServer()">上传到服务器</button>
857
880
  </div>
858
881
  <div class="fp-progress" id="fp-progress"></div>
859
- <div class="fp-drop-zone" id="fp-drop">拖拽文件到此处上传,或点击上方按钮</div>
882
+ <div class="fp-drop-zone" id="fp-drop">松开鼠标,上传到当前目录</div>
860
883
  <input type="file" id="fp-file-input" multiple style="display:none">
861
884
  </div>
862
885
 
@@ -4134,7 +4157,7 @@
4134
4157
  const r = await fetch(`/api/files/${SESSION_ID}?path=${encodeURIComponent(fpCurrentPath)}`);
4135
4158
  const data = await r.json();
4136
4159
  if (!data.ok) { list.innerHTML = `<div class="fp-empty">${escHtml(data.error)}</div>`; return; }
4137
- document.getElementById('fp-path').textContent = data.cwd + (data.path ? '/' + data.path : '');
4160
+ fpRenderCrumbs(data.cwd, data.path || '');
4138
4161
  if (!data.entries.length && data.parent == null) {
4139
4162
  list.innerHTML = '<div class="fp-empty">空目录</div>';
4140
4163
  return;
@@ -4156,19 +4179,15 @@
4156
4179
  html += `<div class="fp-item${on ? ' sel' : ''}" onclick="fpToggleSel('${escAttr(rel)}')"><input type="checkbox" class="fp-chk" ${on ? 'checked' : ''} tabindex="-1" style="pointer-events:none"><span class="fp-icon">${e.isDir ? '📁' : '📄'}</span><span class="fp-name" title="${escAttr(rel)}">${escHtml(e.name)}</span>${e.isDir ? '' : `<span class="fp-size">${fpFormatSize(e.size)}</span>`}</div>`;
4157
4180
  continue;
4158
4181
  }
4159
- if (e.isDir) {
4160
- html += `<div class="fp-item" onclick="fpNav('${escHtml(rel)}')"><span class="fp-icon">📁</span><span class="fp-name" title="${escAttr(rel)}">${escHtml(e.name)}</span><span class="fp-dl" onclick="event.stopPropagation();fpRefer('${escAttr(rel)}/')" title="引用:把目录路径插入消息框">@</span><span class="fp-dl" onclick="event.stopPropagation();fpZip(['${escAttr(rel)}'])" title="打包下载这个文件夹(zip)">⬇</span></div>`;
4161
- } else {
4162
- const editable = isEditableFile(e.name);
4163
- html += `<div class="fp-item" ${editable ? `style="cursor:pointer" onclick="openFileEditor('${escAttr(rel)}')" title="点击在线编辑"` : ''}>
4164
- <span class="fp-icon">${editable ? '📝' : '📄'}</span>
4165
- <span class="fp-name" title="${escAttr(rel)}">${escHtml(e.name)}</span>
4166
- <span class="fp-size">${fpFormatSize(e.size)}</span>
4167
- <span class="fp-dl" onclick="event.stopPropagation();fpRefer('${escAttr(rel)}')" title="引用:把文件路径插入消息框,让 AI 读这个文件">@</span>
4168
- ${editable ? `<span class="fp-dl" onclick="event.stopPropagation();openFileEditor('${escAttr(rel)}')" title="在线编辑">✏️</span>` : ''}
4169
- <a class="fp-dl" href="/api/files/${SESSION_ID}/download?path=${encodeURIComponent(rel)}" download title="下载" onclick="event.stopPropagation()">⬇</a>
4170
- </div>`;
4171
- }
4182
+ // 点整行 = 打开(文件夹进入 / 文件预览或编辑);其余操作都在行尾 ⋯(电脑上右键也行)
4183
+ const kind = e.isDir ? 'd' : isEditableFile(e.name) ? 'e' : 'f';
4184
+ const a = `'${escAttr(rel)}','${kind}'`;
4185
+ html += `<div class="fp-item" onclick="fpOpen(${a})" oncontextmenu="fpItemMenu(event,${a})" title="${kind === 'f' ? '点击查看可用操作' : '点击打开'}">
4186
+ <span class="fp-icon">${e.isDir ? '📁' : kind === 'e' ? '📝' : '📄'}</span>
4187
+ <span class="fp-name">${escHtml(e.name)}</span>
4188
+ ${e.isDir ? '' : `<span class="fp-size">${fpFormatSize(e.size)}</span>`}
4189
+ <span class="fp-more" onclick="event.stopPropagation();fpItemMenu(event,${a})" title="更多操作">⋯</span>
4190
+ </div>`;
4172
4191
  }
4173
4192
  list.innerHTML = html;
4174
4193
  } catch (e) {
@@ -4186,8 +4205,9 @@
4186
4205
  bar.style.display = fpSel ? 'flex' : 'none';
4187
4206
  document.getElementById('fp-multi-btn').classList.toggle('on', !!fpSel);
4188
4207
  if (!fpSel) return;
4189
- document.getElementById('fp-sel-count').textContent = `已选 ${fpSel.size} 项`;
4208
+ document.getElementById('fp-sel-count').textContent = `已选${fpSel.size}`;
4190
4209
  document.getElementById('fp-sel-dl').disabled = !fpSel.size;
4210
+ document.getElementById('fp-sel-ref').disabled = !fpSel.size;
4191
4211
  }
4192
4212
  window.fpToggleMulti = function(on) {
4193
4213
  fpSel = (on ?? !fpSel) ? new Set() : null;
@@ -4205,6 +4225,86 @@
4205
4225
  fpSyncSelBar(); fpLoadFiles();
4206
4226
  };
4207
4227
  window.fpZipSelected = function() { if (fpSel?.size) fpZip([...fpSel]); };
4228
+ // 选中的一次引用给 AI:路径用空格连起来插进消息框
4229
+ window.fpReferSelected = function() {
4230
+ if (!fpSel?.size) return;
4231
+ const list = [...fpSel];
4232
+ fpToggleMulti(false);
4233
+ fpRefer(list.join(' '));
4234
+ };
4235
+
4236
+ // 面包屑:项目名 › 子目录 › …,点哪级回哪级
4237
+ function fpRenderCrumbs(cwd, path) {
4238
+ const el = document.getElementById('fp-path');
4239
+ const root = String(cwd || '').replace(/[\\/]+$/, '').split(/[\\/]/).pop() || '项目';
4240
+ const segs = path ? path.split('/') : [];
4241
+ const parts = [{ name: root, rel: '' }, ...segs.map((n, i) => ({ name: n, rel: segs.slice(0, i + 1).join('/') }))];
4242
+ el.innerHTML = parts.map((p, i) => i === parts.length - 1
4243
+ ? `<span class="crumb cur">${escHtml(p.name)}</span>`
4244
+ : `<span class="crumb" onclick="fpNav('${escAttr(p.rel)}')">${escHtml(p.name)}</span><span class="sep">›</span>`).join('');
4245
+ el.title = String(cwd || '') + (path ? '/' + path : '');
4246
+ }
4247
+
4248
+ // 点整行:文件夹进入;能在线看 / 改的打开(html / md 默认预览);其他文件(图片、压缩包…)弹出操作菜单
4249
+ window.fpOpen = function(rel, kind) {
4250
+ if (fpSel) return fpToggleSel(rel);
4251
+ if (kind === 'd') return fpNav(rel);
4252
+ if (kind === 'e') return openFileEditor(rel);
4253
+ fpItemMenu(null, rel, kind);
4254
+ };
4255
+ window.fpCopyPath = function(rel) {
4256
+ const t = typeof feditAbs === 'function' ? feditAbs(rel) : rel;
4257
+ const ok = () => addStatusMessage('已复制路径:' + t);
4258
+ (navigator.clipboard?.writeText(t) || Promise.reject()).then(ok, () => {
4259
+ const x = document.createElement('textarea'); x.value = t; document.body.appendChild(x); x.select();
4260
+ try { document.execCommand('copy'); ok(); } catch {} x.remove();
4261
+ });
4262
+ };
4263
+ function fpDownload(rel) {
4264
+ const a = document.createElement('a');
4265
+ a.href = `/api/files/${SESSION_ID}/download?path=${encodeURIComponent(rel)}`; a.download = '';
4266
+ document.body.appendChild(a); a.click(); a.remove();
4267
+ }
4268
+
4269
+ // 通用小菜单:电脑(鼠标)贴着点击位置弹,手机从底部弹
4270
+ function fpMenu(ev, title, items) {
4271
+ document.getElementById('fp-menu-ov')?.remove(); document.getElementById('fp-menu')?.remove();
4272
+ const ov = document.createElement('div'); ov.id = 'fp-menu-ov';
4273
+ const m = document.createElement('div'); m.id = 'fp-menu';
4274
+ const sheet = !ev || !matchMedia('(pointer: fine)').matches;
4275
+ if (sheet) { m.classList.add('sheet'); ov.style.background = 'rgba(0,0,0,0.45)'; }
4276
+ m.innerHTML = (title ? `<div class="mh">${escHtml(title)}</div>` : '') +
4277
+ items.map((it, i) => `<button data-i="${i}">${it[0]}${it[2] ? `<small>${escHtml(it[2])}</small>` : ''}</button>`).join('');
4278
+ const close = () => { ov.remove(); m.remove(); };
4279
+ ov.onclick = close; ov.oncontextmenu = (e) => { e.preventDefault(); close(); };
4280
+ m.onclick = (e) => { const b = e.target.closest('button[data-i]'); if (!b) return; close(); items[+b.dataset.i][1](); };
4281
+ document.body.appendChild(ov); document.body.appendChild(m);
4282
+ if (!sheet) {
4283
+ const r = m.getBoundingClientRect();
4284
+ m.style.left = Math.max(8, Math.min(ev.clientX, innerWidth - r.width - 8)) + 'px';
4285
+ m.style.top = Math.max(8, Math.min(ev.clientY, innerHeight - r.height - 8)) + 'px';
4286
+ }
4287
+ }
4288
+ window.fpItemMenu = function(ev, rel, kind) {
4289
+ if (ev) { ev.preventDefault(); ev.stopPropagation(); }
4290
+ if (fpSel) return;
4291
+ const items = [];
4292
+ if (kind === 'd') items.push(['📂 打开', () => fpNav(rel)], ['⬇ 打包下载', () => fpZip([rel]), 'zip']);
4293
+ else {
4294
+ if (kind === 'e') items.push([/\.(html?|md|markdown)$/i.test(rel) ? '👁 查看 / 编辑' : '✏️ 编辑', () => openFileEditor(rel)]);
4295
+ items.push(['⬇ 下载', () => fpDownload(rel)]);
4296
+ }
4297
+ items.push(['@ 引用到消息', () => fpRefer(kind === 'd' ? rel + '/' : rel), '让 AI 读它'], ['📋 复制路径', () => fpCopyPath(rel)]);
4298
+ fpMenu(ev, rel, items);
4299
+ };
4300
+ window.fpToolsMenu = function(ev) {
4301
+ const items = [
4302
+ ['🔄 同步到本地文件夹', () => fpStartSync(), 'Chrome / Edge'],
4303
+ [fpSort === 'recent' ? '🔤 按名称排序' : '🕘 按最近修改排序', () => fpToggleSort()],
4304
+ ];
4305
+ if (!document.body.classList.contains('no-desktop')) items.push(['📂 在这台电脑上打开文件夹', () => fpOpenFolder(), '远程访问无效']);
4306
+ fpMenu(ev, '', items);
4307
+ };
4208
4308
  window.fpZip = async function(paths) {
4209
4309
  const prog = document.getElementById('fp-progress');
4210
4310
  prog.textContent = '正在统计要打包的文件…'; prog.style.display = 'block';
@@ -4285,13 +4385,16 @@
4285
4385
  setTimeout(() => { progress.style.display = 'none'; }, 3000);
4286
4386
  }
4287
4387
 
4288
- // 拖拽上传
4289
- const fpDrop = document.getElementById('fp-drop');
4290
- fpDrop.addEventListener('dragover', (e) => { e.preventDefault(); fpDrop.classList.add('dragover'); });
4291
- fpDrop.addEventListener('dragleave', () => fpDrop.classList.remove('dragover'));
4292
- fpDrop.addEventListener('drop', (e) => {
4293
- e.preventDefault();
4294
- fpDrop.classList.remove('dragover');
4388
+ // 拖拽上传:把文件拖到抽屉任意位置,出现提示区,松开即上传到当前目录
4389
+ const fpPanel = document.getElementById('file-panel');
4390
+ let fpDragDepth = 0;
4391
+ const isFileDrag = (e) => [...(e.dataTransfer?.types || [])].includes('Files');
4392
+ fpPanel.addEventListener('dragenter', (e) => { if (!isFileDrag(e) || syncMode) return; e.preventDefault(); fpDragDepth++; fpPanel.classList.add('dragging'); });
4393
+ fpPanel.addEventListener('dragover', (e) => { if (isFileDrag(e) && !syncMode) e.preventDefault(); });
4394
+ fpPanel.addEventListener('dragleave', () => { if (--fpDragDepth <= 0) { fpDragDepth = 0; fpPanel.classList.remove('dragging'); } });
4395
+ fpPanel.addEventListener('drop', (e) => {
4396
+ if (!isFileDrag(e) || syncMode) return;
4397
+ e.preventDefault(); fpDragDepth = 0; fpPanel.classList.remove('dragging');
4295
4398
  if (e.dataTransfer.files.length) fpUploadFiles(e.dataTransfer.files);
4296
4399
  });
4297
4400
 
package/dist/chat.html.gz CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wendongfly/myhi",
3
- "version": "1.3.204",
3
+ "version": "1.3.205",
4
4
  "description": "Web-based terminal sharing with chat UI — control your terminal from phone via LAN/Tailscale",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",