@wendongfly/myhi 1.3.58 → 1.3.60

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
@@ -137,8 +137,9 @@
137
137
  #img-preview .img-remove:hover { background: #f85149; color: #fff; }
138
138
  #input-box.focused { border-color: #58a6ff; }
139
139
  #input-box.disabled { opacity: 0.5; pointer-events: none; }
140
- #cmd-input { display: block; width: 100%; background: transparent; color: #e6edf3; border: none; padding: 0.6rem 0.75rem 0.3rem; font-family: 'SF Mono', 'Cascadia Code', 'Consolas', monospace; font-size: 0.85rem; outline: none; resize: none; line-height: 1.4; max-height: 120px; overflow-y: auto; }
140
+ #cmd-input { display: block; width: 100%; background: transparent; color: #e6edf3; border: none; padding: 0.6rem 0.75rem 0.3rem; font-family: 'SF Mono', 'Cascadia Code', 'Consolas', monospace; font-size: 0.85rem; outline: none; resize: none; line-height: 1.5; max-height: 140px; overflow-y: auto; }
141
141
  #cmd-input::placeholder { color: #484f58; }
142
+ @media (pointer: coarse) { #cmd-input { font-size: 1rem; padding: 0.75rem 0.75rem 0.35rem; line-height: 1.55; } }
142
143
  #input-toolbar { display: flex; align-items: center; padding: 0.25rem 0.4rem 0.4rem; gap: 0.15rem; }
143
144
  #input-toolbar .spacer { flex: 1; }
144
145
  .tb-btn { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 8px; border: none; background: transparent; color: #8b949e; cursor: pointer; transition: all 0.15s; flex-shrink: 0; }
@@ -349,6 +350,7 @@
349
350
  <button class="sk sk-claude" onclick="doSlashCmd('/rename')">命名</button>
350
351
  <button class="sk sk-claude" onclick="openGitSheet()" style="color:#3fb950">提交</button>
351
352
  <button class="sk sk-claude" onclick="showMemory()">记忆</button>
353
+ <button class="sk sk-claude" onclick="openVaSheet()" style="color:#1f6feb">语音</button>
352
354
  <button class="sk sk-claude" onclick="openSkillSheet()" style="color:#9d5cf5">技能</button>
353
355
  <span id="sk-builtin-skills"></span>
354
356
  <span id="sk-custom-skills"></span>
@@ -568,6 +570,36 @@
568
570
  </div>
569
571
  </div>
570
572
 
573
+ <!-- 语音需求助手面板 -->
574
+ <div id="va-sheet" class="action-sheet">
575
+ <div class="action-sheet-backdrop" onclick="closeVaSheet()"></div>
576
+ <div class="action-sheet-box" style="max-height:90vh;display:flex;flex-direction:column">
577
+ <div class="action-sheet-title" style="display:flex;align-items:center;justify-content:space-between">
578
+ <span>语音需求助手</span>
579
+ <button onclick="vaClear()" style="background:none;border:none;color:#8b949e;font-size:0.78rem;cursor:pointer;padding:0.2rem 0.4rem">清除</button>
580
+ </div>
581
+ <div id="va-transcript" style="flex:1;overflow-y:auto;min-height:80px;max-height:28vh;padding:0.6rem 0.8rem;background:#0d1117;border:1px solid #30363d;border-radius:8px;margin:0 0.2rem 0.4rem;font-size:0.85rem;color:#c9d1d9;line-height:1.65;scrollbar-width:thin;white-space:pre-wrap">
582
+ <span style="color:#8b949e">按住大按钮说话,松手自动识别,多轮累积后整理成需求...</span>
583
+ </div>
584
+ <div id="va-result-wrap" style="display:none;padding:0 0.2rem 0.3rem">
585
+ <div style="font-size:0.72rem;color:#8b949e;margin-bottom:0.2rem">整理后的需求</div>
586
+ <textarea id="va-result" class="slash-inp" rows="6" style="resize:vertical;font-size:0.82rem;line-height:1.5;font-family:monospace"></textarea>
587
+ </div>
588
+ <button id="va-record-btn"
589
+ style="background:#1f6feb;color:#fff;font-size:1.1rem;font-weight:700;padding:1.1rem;margin:0 0.2rem 0.3rem;border:none;border-radius:12px;cursor:pointer;touch-action:none;user-select:none;-webkit-user-select:none">
590
+ 按住说话
591
+ </button>
592
+ <div id="va-status" style="text-align:center;font-size:0.78rem;color:#8b949e;min-height:1.3em;margin-bottom:0.2rem"></div>
593
+ <div style="display:flex;gap:0.4rem;padding:0 0.2rem">
594
+ <button id="va-organize-btn" class="action-sheet-cancel" onclick="vaOrganize()"
595
+ style="flex:1;background:#7c3aed;color:#fff;font-weight:600;margin-bottom:0">整理成需求</button>
596
+ <button id="va-send-btn" class="action-sheet-cancel" onclick="vaSend()"
597
+ style="flex:1;background:#1f6feb;color:#fff;font-weight:600;margin-bottom:0;display:none">发送到对话</button>
598
+ </div>
599
+ <button class="action-sheet-cancel" onclick="closeVaSheet()" style="margin-top:0.4rem">关闭</button>
600
+ </div>
601
+ </div>
602
+
571
603
  <div id="status-overlay">连接中...</div>
572
604
 
573
605
  <script type="module">
@@ -3019,11 +3051,14 @@
3019
3051
  addStatusMessage('语音上传失败: ' + e.message);
3020
3052
  } finally {
3021
3053
  _voicePlaceholder();
3022
- cmdInput.focus();
3054
+ if (!_isTouchDevice()) cmdInput.focus(); // 触屏不抢焦点,避免弹键盘
3023
3055
  }
3024
3056
  }
3025
3057
 
3026
- // 桌面端:点击切换;触屏:按住录音,松手发送
3058
+ // 桌面端:点击切换;触屏:长按录音松手发送,短按提示
3059
+ const MIN_RECORD_MS = 400;
3060
+ let _voiceStartTime = 0;
3061
+
3027
3062
  _voiceBtn.addEventListener('click', (e) => {
3028
3063
  if (_isTouchDevice()) return; // 触屏由 pointer 事件处理
3029
3064
  _startVoice();
@@ -3031,17 +3066,186 @@
3031
3066
  _voiceBtn.addEventListener('pointerdown', (e) => {
3032
3067
  if (!_isTouchDevice()) return;
3033
3068
  e.preventDefault();
3069
+ cmdInput.blur(); // 防止键盘弹出
3070
+ _voiceStartTime = Date.now();
3034
3071
  _startVoice();
3035
3072
  });
3036
3073
  _voiceBtn.addEventListener('pointerup', (e) => {
3037
3074
  if (!_isTouchDevice() || !_voiceActive) return;
3038
3075
  e.preventDefault();
3076
+ const elapsed = Date.now() - _voiceStartTime;
3077
+ if (elapsed < MIN_RECORD_MS) {
3078
+ // 短按:取消录音,不上传,给提示
3079
+ _voiceActive = false;
3080
+ _voiceBtn.classList.remove('recording');
3081
+ _voicePlaceholder();
3082
+ try { _mediaRecorder?.stream?.getTracks().forEach(t => t.stop()); } catch {}
3083
+ try { if (_mediaRecorder?.state !== 'inactive') _mediaRecorder?.stop(); } catch {}
3084
+ _mediaRecorder = null;
3085
+ addStatusMessage('请长按 🎤 录音(按住说话,松手识别)');
3086
+ return;
3087
+ }
3039
3088
  _stopVoice();
3040
3089
  });
3041
3090
  _voiceBtn.addEventListener('pointercancel', () => { if (_voiceActive) _stopVoice(); });
3042
3091
 
3043
3092
  window.toggleVoice = _startVoice; // 保留 onclick 兼容
3044
3093
 
3094
+ // ── 语音需求助手 ──────────────────────────────────────────
3095
+ let _vaLines = [];
3096
+ let _vaMR = null;
3097
+ let _vaActive = false;
3098
+ let _vaStartTime2 = 0;
3099
+
3100
+ const _vaBtn = document.getElementById('va-record-btn');
3101
+ const _vaStatusEl = document.getElementById('va-status');
3102
+
3103
+ function _vaSetStatus(t) { _vaStatusEl.textContent = t || ''; }
3104
+
3105
+ function _vaRender() {
3106
+ const el = document.getElementById('va-transcript');
3107
+ if (_vaLines.length === 0) {
3108
+ el.innerHTML = '<span style="color:#8b949e">按住大按钮说话,松手自动识别,多轮累积后整理成需求...</span>';
3109
+ } else {
3110
+ el.textContent = _vaLines.map((t, i) => `[${i + 1}] ${t}`).join('\n\n');
3111
+ el.scrollTop = el.scrollHeight;
3112
+ }
3113
+ }
3114
+
3115
+ async function _vaStart() {
3116
+ if (_vaActive) return;
3117
+ if (!navigator.mediaDevices?.getUserMedia) {
3118
+ _vaSetStatus('浏览器不支持麦克风(需要 HTTPS)'); return;
3119
+ }
3120
+ let stream;
3121
+ try {
3122
+ stream = await navigator.mediaDevices.getUserMedia({ audio: true });
3123
+ } catch (e) {
3124
+ _vaSetStatus(e.name === 'NotAllowedError' ? '麦克风权限被拒绝,请在地址栏允许' : '麦克风失败: ' + e.message);
3125
+ return;
3126
+ }
3127
+ const chunks = [];
3128
+ _vaMR = new MediaRecorder(stream);
3129
+ _vaMR.ondataavailable = (e) => { if (e.data.size > 0) chunks.push(e.data); };
3130
+ _vaMR.onstop = async () => {
3131
+ stream.getTracks().forEach(t => t.stop());
3132
+ await _vaTranscribe(new Blob(chunks, { type: _vaMR.mimeType || 'audio/webm' }));
3133
+ };
3134
+ _vaMR.start();
3135
+ _vaActive = true;
3136
+ _vaBtn.textContent = '松手停止';
3137
+ _vaBtn.style.background = '#da3633';
3138
+ _vaSetStatus('录音中...');
3139
+ }
3140
+
3141
+ function _vaStop() {
3142
+ if (!_vaActive || !_vaMR) return;
3143
+ _vaActive = false;
3144
+ _vaBtn.textContent = '按住说话';
3145
+ _vaBtn.style.background = '#1f6feb';
3146
+ _vaSetStatus('识别中...');
3147
+ _vaMR.stop();
3148
+ }
3149
+
3150
+ async function _vaTranscribe(blob) {
3151
+ try {
3152
+ const form = new FormData();
3153
+ form.append('audio', blob, 'audio.webm');
3154
+ const resp = await fetch(`/api/voice-transcribe?sessionId=${SESSION_ID}`, { method: 'POST', body: form });
3155
+ const data = await resp.json();
3156
+ if (resp.status === 503) {
3157
+ _vaSetStatus('ASR 服务未配置');
3158
+ } else if (data.text) {
3159
+ _vaLines.push(data.text);
3160
+ _vaRender();
3161
+ _vaSetStatus(`第 ${_vaLines.length} 段已录入,可继续录或点"整理成需求"`);
3162
+ } else {
3163
+ _vaSetStatus('未识别到内容,请重试');
3164
+ }
3165
+ } catch (e) {
3166
+ _vaSetStatus('上传失败: ' + e.message);
3167
+ }
3168
+ }
3169
+
3170
+ window.openVaSheet = function() {
3171
+ document.getElementById('va-sheet').classList.add('active');
3172
+ _vaRender();
3173
+ };
3174
+ window.closeVaSheet = function() {
3175
+ document.getElementById('va-sheet').classList.remove('active');
3176
+ if (_vaActive) _vaStop();
3177
+ };
3178
+ window.vaClear = function() {
3179
+ _vaLines = [];
3180
+ document.getElementById('va-result-wrap').style.display = 'none';
3181
+ document.getElementById('va-send-btn').style.display = 'none';
3182
+ document.getElementById('va-organize-btn').style.display = '';
3183
+ _vaRender();
3184
+ _vaSetStatus('');
3185
+ };
3186
+ window.vaOrganize = async function() {
3187
+ if (_vaLines.length === 0) { _vaSetStatus('请先录入语音内容'); return; }
3188
+ const btn = document.getElementById('va-organize-btn');
3189
+ btn.textContent = '整理中...';
3190
+ btn.disabled = true;
3191
+ try {
3192
+ const resp = await fetch('/api/voice-assist/organize', {
3193
+ method: 'POST',
3194
+ headers: { 'Content-Type': 'application/json' },
3195
+ body: JSON.stringify({ transcript: _vaLines.join('\n'), sessionId: SESSION_ID }),
3196
+ });
3197
+ const data = await resp.json();
3198
+ if (data.requirement) {
3199
+ document.getElementById('va-result').value = data.requirement;
3200
+ document.getElementById('va-result-wrap').style.display = '';
3201
+ document.getElementById('va-send-btn').style.display = '';
3202
+ _vaSetStatus('整理完成,可编辑后点"发送到对话"');
3203
+ } else {
3204
+ _vaSetStatus('整理失败: ' + (data.error || '未知错误'));
3205
+ }
3206
+ } catch (e) {
3207
+ _vaSetStatus('请求失败: ' + e.message);
3208
+ } finally {
3209
+ btn.textContent = '整理成需求';
3210
+ btn.disabled = false;
3211
+ }
3212
+ };
3213
+ window.vaSend = function() {
3214
+ const text = document.getElementById('va-result').value.trim();
3215
+ if (!text) return;
3216
+ cmdInput.value = text;
3217
+ window.closeVaSheet();
3218
+ if (!_isTouchDevice()) cmdInput.focus();
3219
+ };
3220
+
3221
+ // 大录音按钮事件(长按逻辑与主录音按钮一致)
3222
+ _vaBtn.addEventListener('pointerdown', (e) => {
3223
+ e.preventDefault();
3224
+ _vaStartTime2 = Date.now();
3225
+ _vaStart();
3226
+ });
3227
+ _vaBtn.addEventListener('pointerup', (e) => {
3228
+ e.preventDefault();
3229
+ if (!_vaActive) return;
3230
+ const elapsed = Date.now() - _vaStartTime2;
3231
+ if (elapsed < MIN_RECORD_MS) {
3232
+ _vaActive = false;
3233
+ _vaBtn.textContent = '按住说话';
3234
+ _vaBtn.style.background = '#1f6feb';
3235
+ try { _vaMR?.stream?.getTracks().forEach(t => t.stop()); } catch {}
3236
+ try { if (_vaMR?.state !== 'inactive') _vaMR?.stop(); } catch {}
3237
+ _vaMR = null;
3238
+ _vaSetStatus('请按住按钮说话,松手识别');
3239
+ return;
3240
+ }
3241
+ _vaStop();
3242
+ });
3243
+ _vaBtn.addEventListener('pointercancel', () => { if (_vaActive) _vaStop(); });
3244
+ _vaBtn.addEventListener('click', (e) => {
3245
+ if (_isTouchDevice()) return; // 触屏由 pointer 处理
3246
+ if (_vaActive) { _vaStop(); } else { _vaStart(); }
3247
+ });
3248
+
3045
3249
  cmdInput.focus();
3046
3250
 
3047
3251
  // ── 版本更新检查 ──────────────────────────────