diff --git a/CHANGELOG.md b/CHANGELOG.md index ebf1a1b..0d19fb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [1.4.1] - 2026-08-18 +### 修复 +- **恢复 Cmd/Ctrl+Enter 纯批注直接发送(issue #17)**:v1.3.18 为修 issue #10 把修饰键 Enter 全部排除,导致空草稿时 Cmd/Ctrl+Enter 无法直接发送纯批注。现仅在草稿为空时接管 Cmd/Ctrl+Enter,并在 capture 阶段主动 `submit('queue')`,避免 composer 的 accelerated 路径在「运行中 + 有排队消息」时误走 steerQueue 而把批注块留在输入框;草稿已有文字时仍交由 composer 处理,保留宿主的 Queue / Steer 策略。 + ## [1.4.0] - 2026-08-18 ### 新功能 - **语言跟随 DSH(issue #11)**:接入 `@deepseek-ai/dsh-client-locale` 的 `locale` 服务(`ctx.locale.getSnapshot().active` + `subscribe()`),UI 文案与批注协议块按 `zh`/`en` 双语生成与实时切换;气泡隐藏手术与反解析同时兼容 `提问:`/`Ask:`、`批注:`/`Note: ` 及「问题:」老格式,历史消息跨语言切换可解析;回复芯片保持语言中立的 `Annotation N`;locale 服务缺失时回退 zh,行为与旧版一致。 diff --git a/client.js b/client.js index 50c76f3..eb6fcb8 100644 --- a/client.js +++ b/client.js @@ -994,13 +994,33 @@ window.__ModuleLoader__.load({ // 批注清单 + 用户输入的问题。用户始终看不到文本被塞进去。 // IME 铁律(v1.3.10 修了 nativeEvent.keyCode;v1.3.11 补 compositionend // 后 Enter keyCode=13 的时序洞):合成期 / 上屏确认 Enter 绝不能 setDraft。 - // 修饰键守卫(v1.3.18):只拦裸 Enter——Shift+Enter 换行、Ctrl/Meta+Enter - // 官方 accelerated 提交、以及浏览器默认换行路径都不应触发拼稿。 - if (e.key === 'Enter' && !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey + // 修饰键守卫(v1.3.18 修 issue #10):Shift+Enter 换行、Alt+Enter 默认路径 + // 不触发拼稿;裸 Enter 继续处理带文字草稿,Cmd/Ctrl+Enter 只接管空草稿 + // 的纯批注。已有文字时交回 composer,保留宿主的 Queue / Steer 策略。 + // 纯批注的 Cmd/Ctrl+Enter 需要在这里直接提交:composer 的 accelerated 路径在 + // 「运行中 + 有排队消息」时会走 steerQueue,而不是发送当前草稿;我们在 + // capture 阶段 setDraft 后 stopPropagation,主动 submit('queue'),保证 + // 纯批注能直接发出,同时不会把批注块明文留在输入框。 + if (e.key === 'Enter' && !e.shiftKey && !e.altKey && ui.quotes.length > 0 && !isImeKeyBlocked(e)) { var ta = e.target if (ta instanceof HTMLTextAreaElement && ta.closest && ta.closest('[data-composer-card]') !== null) { - attachAndSend() + var attached = attachAndSend(e) + if (attached && (e.ctrlKey || e.metaKey)) { + e.preventDefault() + e.stopPropagation() + var current = sessions.list.getSnapshot().current + if (current !== undefined) { + var scoped = sessions.scope(current) + if (scoped !== undefined) { + try { + ctx.conversation.input.for(scoped).submit('queue') + } catch (err) { + console.warn('[annotation] Cmd/Ctrl+Enter 直接提交失败:', err) + } + } + } + } } } } @@ -1295,25 +1315,33 @@ window.__ModuleLoader__.load({ + '\n\n' + t('block.format', { n: n }) + '\n\n' + t('block.marker') } - /** 提交前把批注块拼进 composer 草稿(随回车一起发送)。 */ - function attachAndSend() { + function shouldAttachForEnter(e, draft) { + return !(e.ctrlKey || e.metaKey) || draft.trim() === '' + } + + /** 提交前把批注块拼进 composer 草稿(随回车一起发送)。 + * 返回 true 表示批注块已在草稿中(本次刚拼入,或之前已拼入未发送)。 */ + function attachAndSend(e) { var current = sessions.list.getSnapshot().current - if (current === undefined) return + if (current === undefined) return false try { var scoped = sessions.scope(current) - if (scoped === undefined) return + if (scoped === undefined) return false var shell = ctx.conversation.input.for(scoped) var st = shell.state.getSnapshot() var draft = st.draft || '' + if (!shouldAttachForEnter(e, draft)) return false // 草稿已含批注块(上次追加未发送)→ 不重复追加(zh/en 双哨兵)。 - if (hasAnnotationBlock(draft)) return + if (hasAnnotationBlock(draft)) return true var block = buildBlock() shell.setDraft(block + '\n' + draft) annotationAttached = true console.log('[annotation] 批注块已拼入草稿,回车将随消息发送(' + ui.quotes.length + ' 条)') + return true } catch (err) { console.warn('[annotation] 批注拼稿失败:', err) showToast(t('toast.attachFail') + (err && err.message ? err.message : err)) + return false } } diff --git a/package.json b/package.json index 1f8e8c6..2d17e63 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@omdsh-dev/dsh-annotation", - "version": "1.4.0", + "version": "1.4.1", "type": "module", "description": "DSH Web selection-annotation plugin: select assistant text, annotate (optional), and press Enter to send the annotation block with your message — the model replies to each annotation by number, with hoverable chips in the reply.", "main": "lib/index.js", @@ -26,7 +26,8 @@ ], "scripts": { "build": "node -e \"require('node:fs').rmSync('lib', { recursive: true, force: true })\" && tsc -p tsconfig.json", - "check": "npm run build && node --check lib/index.js && node --check client.js", + "test": "node --test test/*.test.mjs", + "check": "npm run build && node --check lib/index.js && node --check client.js && npm test", "prepack": "npm run check" }, "publishConfig": { diff --git a/test/enter-policy.test.mjs b/test/enter-policy.test.mjs new file mode 100644 index 0000000..6b2078d --- /dev/null +++ b/test/enter-policy.test.mjs @@ -0,0 +1,15 @@ +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import test from 'node:test' + +const source = readFileSync(new URL('../client.js', import.meta.url), 'utf8') +const match = source.match(/function shouldAttachForEnter\(e, draft\) \{[\s\S]*?\n \}/) +assert.ok(match, 'client.js should define shouldAttachForEnter') +const shouldAttachForEnter = Function(`return (${match[0]})`)() + +test('快捷回车只接管空草稿的纯批注', () => { + assert.equal(shouldAttachForEnter({ ctrlKey: true, metaKey: false }, ''), true) + assert.equal(shouldAttachForEnter({ ctrlKey: false, metaKey: true }, ' '), true) + assert.equal(shouldAttachForEnter({ ctrlKey: true, metaKey: false }, '用户问题'), false) + assert.equal(shouldAttachForEnter({ ctrlKey: false, metaKey: false }, '用户问题'), true) +})