@wendongfly/myhi 1.3.49 → 1.3.52

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/bin/daemon.js CHANGED
@@ -17,6 +17,36 @@ const pidFile = join(configDir, 'daemon.pid');
17
17
  const serverEntry = join(__dirname, '../dist/index.js');
18
18
 
19
19
  mkdirSync(configDir, { recursive: true });
20
+
21
+ // 单例守卫:如果已有活 daemon,先 SIGTERM 它再启动。否则双 daemon 会让各自的
22
+ // server child 因互读 server.pid 而互杀,触发"连续崩溃 5 次"熔断(pc41 1.3.51 升级踩到)
23
+ (function killOldDaemon() {
24
+ let oldPid;
25
+ try { oldPid = parseInt(readFileSync(pidFile, 'utf8').trim(), 10); } catch { return; }
26
+ if (!oldPid || oldPid === process.pid) return;
27
+ let alive = false;
28
+ try { process.kill(oldPid, 0); alive = true; } catch {}
29
+ if (!alive) return;
30
+ process.stderr.write(`[daemon] 检测到已运行的 daemon (PID ${oldPid}),先停止...\n`);
31
+ try {
32
+ if (process.platform === 'win32') execSync(`taskkill /PID ${oldPid} /F /T`, { stdio: 'pipe', timeout: 5000 });
33
+ else process.kill(oldPid, 'SIGTERM');
34
+ } catch {}
35
+ // 等最多 5 秒
36
+ const deadline = Date.now() + 5000;
37
+ while (Date.now() < deadline) {
38
+ try { process.kill(oldPid, 0); } catch { return; }
39
+ const end = Date.now() + 200;
40
+ while (Date.now() < end) {} // busy-wait, 此处无事件循环可用
41
+ }
42
+ let stillAlive = false;
43
+ try { process.kill(oldPid, 0); stillAlive = true; } catch {}
44
+ if (stillAlive) {
45
+ process.stderr.write(`[daemon] 无法停止旧 daemon (PID ${oldPid}),本进程退出避免冲突\n`);
46
+ process.exit(1);
47
+ }
48
+ })();
49
+
20
50
  writeFileSync(pidFile, String(process.pid));
21
51
 
22
52
  let child = null;
@@ -35,7 +65,7 @@ function startServer() {
35
65
  log('启动 server 子进程...');
36
66
  child = fork(serverEntry, process.argv.slice(2), {
37
67
  stdio: ['ignore', 'inherit', 'inherit', 'ipc'],
38
- env: { ...process.env, MYHI_DAEMON: '1' },
68
+ env: { ...process.env, MYHI_DAEMON: '1', MYHI_DAEMON_PID: String(process.pid) },
39
69
  windowsHide: true,
40
70
  });
41
71
 
package/dist/chat.html CHANGED
@@ -2769,35 +2769,58 @@
2769
2769
  addStatusMessage(`会话统计: ${inputs} 条提问, ${assistants} 条回复, ${tools} 次工具调用`);
2770
2770
  };
2771
2771
 
2772
+ // ── 图片上传(拍照 / 选文件 / 剪贴板粘贴共用) ──────────
2773
+ // 压缩源 Blob → POST /upload → setPendingImage
2774
+ async function uploadImageBlob(srcBlob, filename) {
2775
+ const img = new Image(); const url = URL.createObjectURL(srcBlob);
2776
+ const blob = await new Promise((resolve, reject) => {
2777
+ img.onload = () => { URL.revokeObjectURL(url); let {width:w, height:h} = img;
2778
+ // 压缩策略:长边限制 1024px,JPEG 质量 0.7,适合截图分析
2779
+ const M = 1024;
2780
+ if(w>M||h>M){if(w>=h){h=Math.round(h*M/w);w=M}else{w=Math.round(w*M/h);h=M}}
2781
+ const c=document.createElement('canvas');c.width=w;c.height=h;c.getContext('2d').drawImage(img,0,0,w,h);
2782
+ c.toBlob(b=>b?resolve(b):reject(new Error('toBlob failed')),'image/jpeg',0.7); };
2783
+ img.onerror=reject; img.src=url; });
2784
+ const form = new FormData(); form.append('image', blob, filename || 'image.jpg');
2785
+ const res = await fetch(`/upload?sessionId=${SESSION_ID}`, { method: 'POST', body: form });
2786
+ const data = await res.json();
2787
+ if (!data.path) throw new Error(data.error || '上传失败');
2788
+ setPendingImage(data.path, URL.createObjectURL(blob));
2789
+ }
2790
+
2772
2791
  // ── 拍照上传 ──────────────────────────────────
2773
2792
  window.openCamera = async function() {
2774
2793
  const input = document.createElement('input'); input.type = 'file'; input.accept = 'image/*';
2775
2794
  input.onchange = async () => {
2776
2795
  const file = input.files?.[0]; if (!file) return;
2777
2796
  const btn = document.getElementById('btn-photo'); btn.classList.add('active'); btn.disabled = true;
2778
- try {
2779
- const img = new Image(); const url = URL.createObjectURL(file);
2780
- const blob = await new Promise((resolve, reject) => {
2781
- img.onload = () => { URL.revokeObjectURL(url); let {width:w, height:h} = img;
2782
- // 压缩策略:长边限制 1024px,JPEG 质量 0.7,适合截图分析
2783
- const M = 1024;
2784
- if(w>M||h>M){if(w>=h){h=Math.round(h*M/w);w=M}else{w=Math.round(w*M/h);h=M}}
2785
- const c=document.createElement('canvas');c.width=w;c.height=h;c.getContext('2d').drawImage(img,0,0,w,h);
2786
- c.toBlob(b=>b?resolve(b):reject(new Error('toBlob failed')),'image/jpeg',0.7); };
2787
- img.onerror=reject; img.src=url; });
2788
- const form = new FormData(); form.append('image', blob, 'photo.jpg');
2789
- const res = await fetch(`/upload?sessionId=${SESSION_ID}`, { method: 'POST', body: form }); const data = await res.json();
2790
- if (data.path) {
2791
- // 暂存图片,等用户输入文字后一起发送
2792
- const thumbUrl = URL.createObjectURL(blob);
2793
- setPendingImage(data.path, thumbUrl);
2794
- }
2795
- else addStatusMessage('上传失败: ' + (data.error || '未知'));
2796
- } catch (e) { addStatusMessage('出错: ' + e.message); }
2797
+ try { await uploadImageBlob(file, 'photo.jpg'); }
2798
+ catch (e) { addStatusMessage('出错: ' + e.message); }
2797
2799
  finally { btn.classList.remove('active'); btn.disabled = false; }
2798
2800
  }; input.click();
2799
2801
  };
2800
2802
 
2803
+ // ── 剪贴板粘贴图片(桌面端 Ctrl+V)──────────────
2804
+ // document 级监听:截图后切到浏览器直接 Ctrl+V,无需先聚焦输入框
2805
+ document.addEventListener('paste', async (e) => {
2806
+ const items = e.clipboardData?.items;
2807
+ if (!items) return;
2808
+ for (const item of items) {
2809
+ if (item.kind === 'file' && item.type.startsWith('image/')) {
2810
+ const blob = item.getAsFile();
2811
+ if (!blob) continue;
2812
+ e.preventDefault(); // 阻止浏览器把图片当文本塞进 textarea
2813
+ addStatusMessage('粘贴图片上传中...');
2814
+ try {
2815
+ await uploadImageBlob(blob, 'paste.png');
2816
+ document.getElementById('cmd-input')?.focus();
2817
+ } catch (err) { addStatusMessage('上传失败: ' + err.message); }
2818
+ return; // 只处理第一张
2819
+ }
2820
+ }
2821
+ // 剪贴板里没有图片 → 不拦截,文本粘贴走默认行为
2822
+ });
2823
+
2801
2824
  // ── 语音输入 ──────────────────────────────────
2802
2825
  let recognition = null;
2803
2826
  let isRecording = false;