dsh-edit-turn 0.5.7 → 0.5.8
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/README.md +10 -0
- package/lib/client.js +113 -4
- package/lib/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -277,6 +277,16 @@ ln -s /path/to/dsh-install/node_modules ./node_modules
|
|
|
277
277
|
|
|
278
278
|
### 更新日志
|
|
279
279
|
|
|
280
|
+
**0.5.8** —— 复制交给**平台写入器这一层**兜底:不管按下走哪条路,落到剪贴板的都是改写后的文本。
|
|
281
|
+
|
|
282
|
+
- **0.5.7 没修好**:那一版把"回答按下"从按钮节点挪到了文档级捕获监听(按行匹配,节点被重画也不影响),但实测仍然复制出原文。说明这次按下**根本没有经过本插件能拦到的按钮**——具体是哪个环节,从外部看不出来(没有可用的浏览器来读真实 DOM)。
|
|
283
|
+
|
|
284
|
+
- **所以换到更低的层**:`navigator.clipboard.writeText` 是**所有**复制都要经过的唯一关口——宿主自己的 `writeClipboard` 原语内部就是调它。本插件把这一层包起来:写入的文本如果正是某条被改写消息的**原文**(按行登记,空白归一化后比较),就换成改写后的文本;其余写入一字不动。
|
|
285
|
+
|
|
286
|
+
- **登记表放在页级**(`globalThis` 上的一个 Symbol),不是模块变量:实时重载会生成新的 bundle,而**旧 bundle 装的包装器还在写入路径上**(包装本身是幂等的)。两份 Map 会让包装器读到一个没人填的登记表。
|
|
287
|
+
|
|
288
|
+
- 新增测试:宿主**自己**写它那份原文(不经过本插件的按钮接管)时,落地的是改写后的文本;与改写无关的复制原样通过。测试台补了 `navigator.clipboard` 与 `childNodes`。
|
|
289
|
+
|
|
280
290
|
**0.5.7** —— 改写过的行,复制按钮被宿主重画之后仍然交出改写后的文本。
|
|
281
291
|
|
|
282
292
|
- **问题**:编辑过一条消息(提示词走官方 surface 替换),界面上显示的是改写后的文本,但点那一排的**复制**按钮,粘出来的是**改写前**的原文。
|
package/lib/client.js
CHANGED
|
@@ -45,7 +45,7 @@ window.__ModuleLoader__.load({
|
|
|
45
45
|
const RERUN_PREFIX = '/dsh-rerun-turn'
|
|
46
46
|
|
|
47
47
|
/** Keep in sync with package.json and lib/index.js. */
|
|
48
|
-
const PLUGIN_VERSION = '0.5.
|
|
48
|
+
const PLUGIN_VERSION = '0.5.8'
|
|
49
49
|
|
|
50
50
|
// --- copy -----------------------------------------------------------------
|
|
51
51
|
|
|
@@ -1937,6 +1937,10 @@ window.__ModuleLoader__.load({
|
|
|
1937
1937
|
const COPY_TIMER = 'dshetCopyTimer'
|
|
1938
1938
|
/** The host draws its own check for a second; this is the same second. */
|
|
1939
1939
|
const COPY_FEEDBACK_MS = 1000
|
|
1940
|
+
/** 标记:这一页的剪贴板写入已经被本半部包过一次(重载会复用同一个 page)。 */
|
|
1941
|
+
const CLIPBOARD_WRAPPED = Symbol.for('dshetClipboardWrapped')
|
|
1942
|
+
/** 页级登记表:重载后新 bundle 与旧包装器共用同一份。 */
|
|
1943
|
+
const COPY_REGISTRY = Symbol.for('dshetCopyRegistry')
|
|
1940
1944
|
/**
|
|
1941
1945
|
* Row key -> the text that row's copy button must hand over.
|
|
1942
1946
|
*
|
|
@@ -1947,7 +1951,25 @@ window.__ModuleLoader__.load({
|
|
|
1947
1951
|
* the button it drew goes away with that node (the button it draws next is a
|
|
1948
1952
|
* different one).
|
|
1949
1953
|
*/
|
|
1950
|
-
|
|
1954
|
+
/**
|
|
1955
|
+
* Row key -> the text that row's copy must hand over, and the wording the host
|
|
1956
|
+
* would hand over instead.
|
|
1957
|
+
*
|
|
1958
|
+
* Both live on the PAGE rather than in this module: a live reload builds a new
|
|
1959
|
+
* bundle with new maps, and the clipboard wrapper the previous bundle installed
|
|
1960
|
+
* is still the one in the write path (wrapping is idempotent, by design). Two
|
|
1961
|
+
* maps would leave that wrapper reading a registry nobody fills.
|
|
1962
|
+
*/
|
|
1963
|
+
const copyRegistry = (() => {
|
|
1964
|
+
const page = typeof globalThis !== 'undefined' ? globalThis : {}
|
|
1965
|
+
const existing = page[COPY_REGISTRY]
|
|
1966
|
+
if (existing !== undefined && existing !== null && existing.text instanceof Map) return existing
|
|
1967
|
+
const created = { text: new Map(), host: new Map() }
|
|
1968
|
+
page[COPY_REGISTRY] = created
|
|
1969
|
+
return created
|
|
1970
|
+
})()
|
|
1971
|
+
const rowCopyText = copyRegistry.text
|
|
1972
|
+
const rowHostText = copyRegistry.host
|
|
1951
1973
|
|
|
1952
1974
|
/**
|
|
1953
1975
|
* A button another plugin put in this bar.
|
|
@@ -2011,13 +2033,93 @@ window.__ModuleLoader__.load({
|
|
|
2011
2033
|
* host's own clipboard writer the button keeps the host's behaviour too: a
|
|
2012
2034
|
* takeover that cannot copy is worse than none.
|
|
2013
2035
|
*/
|
|
2036
|
+
/**
|
|
2037
|
+
* The wording the host's own copy button would hand over for this row.
|
|
2038
|
+
*
|
|
2039
|
+
* It is the message the row was drawn for - the pre-edit text - and it is read
|
|
2040
|
+
* off the row itself, because that is where it still is: this plugin hides the
|
|
2041
|
+
* row's own bubble and stands its revision in front of it.
|
|
2042
|
+
*
|
|
2043
|
+
* @param {Element} row - the row.
|
|
2044
|
+
* @returns {string|null} the host's text, or null when it cannot be read.
|
|
2045
|
+
*/
|
|
2046
|
+
function hostCopyTextOf(row) {
|
|
2047
|
+
try {
|
|
2048
|
+
const bubble = ownRevisionBubble(row)
|
|
2049
|
+
let text = ''
|
|
2050
|
+
const children = row.childNodes
|
|
2051
|
+
if (children === undefined || children === null) return null
|
|
2052
|
+
for (const child of children) {
|
|
2053
|
+
if (bubble !== null && child === bubble) continue
|
|
2054
|
+
if (typeof child.textContent === 'string') text += child.textContent
|
|
2055
|
+
}
|
|
2056
|
+
const normalized = text.replace(/\s+/g, ' ').trim()
|
|
2057
|
+
return normalized === '' ? null : normalized
|
|
2058
|
+
} catch {
|
|
2059
|
+
return null
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
/**
|
|
2064
|
+
* The wording that must actually land on the clipboard.
|
|
2065
|
+
*
|
|
2066
|
+
* The host's copy button copies the message its row was drawn for, so on a row
|
|
2067
|
+
* this plugin rewrote it hands over the pre-edit text. The press itself is
|
|
2068
|
+
* answered earlier when this half can find the button, but the action bar is the
|
|
2069
|
+
* host's own React and it may draw a button this half never marked - so the last
|
|
2070
|
+
* say belongs to the write itself, which no button and no handler can bypass.
|
|
2071
|
+
*
|
|
2072
|
+
* @param {string} text - what the platform is about to write.
|
|
2073
|
+
* @returns {string|null} the text to write instead, or null to leave it alone.
|
|
2074
|
+
*/
|
|
2075
|
+
function rewriteCopiedText(text) {
|
|
2076
|
+
const wanted = text.replace(/\s+/g, ' ').trim()
|
|
2077
|
+
if (wanted === '') return null
|
|
2078
|
+
for (const [key, edited] of rowCopyText) {
|
|
2079
|
+
if (typeof edited !== 'string') continue
|
|
2080
|
+
if (edited.replace(/\s+/g, ' ').trim() === wanted) return null
|
|
2081
|
+
const hostText = rowHostText.get(key)
|
|
2082
|
+
if (typeof hostText === 'string' && (hostText === wanted || hostText.includes(wanted))) return edited
|
|
2083
|
+
}
|
|
2084
|
+
return null
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
/**
|
|
2088
|
+
* Wrap the platform's clipboard writer, once per page.
|
|
2089
|
+
*
|
|
2090
|
+
* `navigator.clipboard.writeText` is the single choke point every copy goes
|
|
2091
|
+
* through - the host's own `writeClipboard` primitive calls it, and so does a
|
|
2092
|
+
* browser's native copy of a selection.
|
|
2093
|
+
*
|
|
2094
|
+
* @returns {() => void} undo.
|
|
2095
|
+
*/
|
|
2096
|
+
function installClipboardRewrite() {
|
|
2097
|
+
const clipboard = typeof navigator !== 'undefined' ? navigator.clipboard : undefined
|
|
2098
|
+
if (clipboard === undefined || clipboard === null || typeof clipboard.writeText !== 'function') return () => {}
|
|
2099
|
+
if (clipboard.writeText[CLIPBOARD_WRAPPED] === true) return () => {}
|
|
2100
|
+
const native = clipboard.writeText.bind(clipboard)
|
|
2101
|
+
const wrapped = (value) => {
|
|
2102
|
+
const rewritten = rewriteCopiedText(String(value ?? ''))
|
|
2103
|
+
return rewritten === null ? native(value) : native(rewritten)
|
|
2104
|
+
}
|
|
2105
|
+
wrapped[CLIPBOARD_WRAPPED] = true
|
|
2106
|
+
clipboard.writeText = wrapped
|
|
2107
|
+
return () => {
|
|
2108
|
+
if (clipboard.writeText === wrapped) clipboard.writeText = native
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2014
2112
|
function takeOverRowCopy(row, text, t) {
|
|
2015
2113
|
// Registered before the button lookup on purpose: the handover has to work
|
|
2016
2114
|
// even when this pass cannot find the button to mark (the next one may, and
|
|
2017
2115
|
// the document-level press answers from this table either way).
|
|
2018
2116
|
{
|
|
2019
2117
|
const key = row.getAttribute('data-chat-flow-key')
|
|
2020
|
-
if (typeof key === 'string' && key !== '')
|
|
2118
|
+
if (typeof key === 'string' && key !== '') {
|
|
2119
|
+
rowCopyText.set(key, text)
|
|
2120
|
+
const hostText = hostCopyTextOf(row)
|
|
2121
|
+
if (hostText !== null) rowHostText.set(key, hostText)
|
|
2122
|
+
}
|
|
2021
2123
|
}
|
|
2022
2124
|
const button = nativeCopyButton(row, t)
|
|
2023
2125
|
if (button === null) return
|
|
@@ -2148,7 +2250,10 @@ window.__ModuleLoader__.load({
|
|
|
2148
2250
|
/** The copy handover of a row, wherever the last pass left it. */
|
|
2149
2251
|
function releaseRowCopy(row) {
|
|
2150
2252
|
const key = row.getAttribute('data-chat-flow-key')
|
|
2151
|
-
if (typeof key === 'string' && key !== '')
|
|
2253
|
+
if (typeof key === 'string' && key !== '') {
|
|
2254
|
+
rowCopyText.delete(key)
|
|
2255
|
+
rowHostText.delete(key)
|
|
2256
|
+
}
|
|
2152
2257
|
const button = nativeCopyButton(row)
|
|
2153
2258
|
if (button !== null) releaseCopyButton(button)
|
|
2154
2259
|
}
|
|
@@ -4793,6 +4898,10 @@ window.__ModuleLoader__.load({
|
|
|
4793
4898
|
controller.load()
|
|
4794
4899
|
}, [controller])
|
|
4795
4900
|
|
|
4901
|
+
// The last say on what lands: whatever route the press takes, the write
|
|
4902
|
+
// itself goes through here.
|
|
4903
|
+
react.useEffect(() => installClipboardRewrite(), [])
|
|
4904
|
+
|
|
4796
4905
|
// Pressing outside the editor is the one signal that means "the user
|
|
4797
4906
|
// left". Blur cannot be that signal: a host re-render replaces the node
|
|
4798
4907
|
// (blur without a related target) and a save temporarily disables it -
|
package/lib/index.js
CHANGED
|
@@ -78,7 +78,7 @@ export const PLUGIN_ID = 'dsh-edit-turn'
|
|
|
78
78
|
export const name = PLUGIN_ID
|
|
79
79
|
|
|
80
80
|
/** Keep in sync with package.json and lib/client.js. */
|
|
81
|
-
export const PLUGIN_VERSION = '0.5.
|
|
81
|
+
export const PLUGIN_VERSION = '0.5.8'
|
|
82
82
|
|
|
83
83
|
// A ring of the last few requests, answering "did the browser half even ask, and
|
|
84
84
|
// what did it get?" without needing its console. Read-only diagnostics: the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-edit-turn",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.8",
|
|
4
4
|
"description": "Edit any message in DeepSeek Harness in place: a prompt is replaced where it stands; a model reply block (answer text or reasoning) is patched through a plugin record and a message projection, so the turn stays where it is, later turns are untouched and the log grows by one small record. Falls back to the replay splice on hosts without the projection API. The append-only session log is never rewritten.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|