dsh-plugin-sprig 0.0.0-stage → 1.0.1
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 +41 -2
- package/client.js +210 -0
- package/cordis.patch.yml +4 -0
- package/docs/DETAIL.md +18 -0
- package/icon.svg +4 -0
- package/index.js +2 -0
- package/locale/en.json +7 -0
- package/locale/zh.json +7 -0
- package/package.json +51 -4
package/README.md
CHANGED
|
@@ -1,3 +1,42 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-plugin-sprig
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
DSH client plugin that turns selected conversation text into a [Sprig](https://github.com/thatwzzh/sprig) explanation card.
|
|
4
|
+
|
|
5
|
+
Select text in a DSH conversation → the native quote bar becomes **Copy / Quote / Explain with Sprig**.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
dsh plugin add dsh-plugin-sprig
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or from DSH settings → **Plugins → Add plugin** and paste `dsh-plugin-sprig`.
|
|
14
|
+
|
|
15
|
+
Then make sure **Sprig is running** and **cross-app selection is enabled** in Sprig's settings, and refresh the DSH page (or start a new session).
|
|
16
|
+
|
|
17
|
+
## What it does
|
|
18
|
+
|
|
19
|
+
- Reuses DSH's own `quote` surface (same key, lower priority) instead of adding a second right-click menu that duplicates the native Copy.
|
|
20
|
+
- Sends at most **one real user/assistant message either side** of the selection — `<system-reminder>`, `available_skills`, and context nodes are filtered out, capped at 1800 characters.
|
|
21
|
+
- Delivers the payload to a **running Sprig** over the local bridge on `127.0.0.1:53917`. Sprig creates the card and explains.
|
|
22
|
+
- Sprig carries the source with the explanation, so a selection inside a DSH answer still knows which conversation it came from.
|
|
23
|
+
|
|
24
|
+
## Requirements
|
|
25
|
+
|
|
26
|
+
- Sprig running on the same machine, with **cross-app selection** enabled.
|
|
27
|
+
- DSH **Web UI**. The terminal/CLI does not work — there is no browser surface to hook into.
|
|
28
|
+
|
|
29
|
+
## Limitations
|
|
30
|
+
|
|
31
|
+
- The plugin hooks DSH's internal quote surface. A major DSH upgrade may change that surface and require an update here.
|
|
32
|
+
- The bridge listens on loopback only, has no token, and can only create explanation cards. It never reads local data.
|
|
33
|
+
- Request logs go to `/tmp/sprig-bridge.log`.
|
|
34
|
+
|
|
35
|
+
## Links
|
|
36
|
+
|
|
37
|
+
- Repo: [thatwzzh/sprig](https://github.com/thatwzzh/sprig) · plugin source at [`dsh-plugins/sprig`](https://github.com/thatwzzh/sprig/tree/main/dsh-plugins/sprig)
|
|
38
|
+
- Sprig site: [sprig-site.app.workbuddy.host](https://sprig-site.app.workbuddy.host/?sharecode=HVF3SOVDGG)
|
|
39
|
+
|
|
40
|
+
## License
|
|
41
|
+
|
|
42
|
+
MIT
|
package/client.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSH Client 插件:把「复制 / 引用 / 用知枝解释」合并到 DSH 原有的选词操作条。
|
|
3
|
+
*
|
|
4
|
+
* 做法:复用 message-quotes 的 selection 交互(由它 request.open() 打开 quote 表面),
|
|
5
|
+
* 但把 key='quote' 的表面替换成同时包含三个操作的组件,避免右键时和原生 Copy 菜单叠在一起。
|
|
6
|
+
*/
|
|
7
|
+
window.__ModuleLoader__.load({
|
|
8
|
+
id: '@local/sprig',
|
|
9
|
+
factory(require) {
|
|
10
|
+
const React = require('react')
|
|
11
|
+
const h = React.createElement
|
|
12
|
+
const BRIDGE = 'http://127.0.0.1:53917/v1/explain'
|
|
13
|
+
|
|
14
|
+
// 只取用户/助手真实对话;system-reminder、技能清单、注入上下文不参与解释。
|
|
15
|
+
const SYSTEM_REMINDER = /<system-reminder>[\s\S]*?<\/system-reminder>/gi
|
|
16
|
+
const AVAILABLE_SKILLS = /<available_skills>[\s\S]*?<\/available_skills>/gi
|
|
17
|
+
|
|
18
|
+
function cleanText(raw) {
|
|
19
|
+
return String(raw || '')
|
|
20
|
+
.replace(SYSTEM_REMINDER, '')
|
|
21
|
+
.replace(AVAILABLE_SKILLS, '')
|
|
22
|
+
.trim()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function nodeText(node) {
|
|
26
|
+
if (!node) return ''
|
|
27
|
+
if (node.kind === 'user' || node.kind === 'steering') {
|
|
28
|
+
return cleanText((node.content || [])
|
|
29
|
+
.filter(block => block && block.type === 'text' && typeof block.text === 'string')
|
|
30
|
+
.map(block => block.text)
|
|
31
|
+
.join('\n'))
|
|
32
|
+
}
|
|
33
|
+
if (node.kind === 'assistant') {
|
|
34
|
+
return cleanText((node.blocks || [])
|
|
35
|
+
.filter(block => block && block.kind === 'text' && typeof block.text === 'string')
|
|
36
|
+
.map(block => block.text)
|
|
37
|
+
.join('\n'))
|
|
38
|
+
}
|
|
39
|
+
// context(系统注入)、tool、command 等节点不是对话上下文
|
|
40
|
+
return ''
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function roleLabel(node) {
|
|
44
|
+
if (!node) return '消息'
|
|
45
|
+
if (node.kind === 'assistant') return '助手'
|
|
46
|
+
return '用户'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function excerptAround(text, selection, limit) {
|
|
50
|
+
if (text.length <= limit) return text
|
|
51
|
+
const index = text.indexOf(selection)
|
|
52
|
+
if (index < 0) return text.slice(0, limit) + '…'
|
|
53
|
+
const before = Math.max(0, Math.floor((limit - selection.length) / 2))
|
|
54
|
+
const start = Math.max(0, index - before)
|
|
55
|
+
const head = start > 0 ? '…' : ''
|
|
56
|
+
const tail = start + limit < text.length ? '…' : ''
|
|
57
|
+
return head + text.slice(start, start + limit) + tail
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function buildContext(nodes, selection) {
|
|
61
|
+
const items = (nodes || [])
|
|
62
|
+
.filter(node => node && (node.kind === 'user' || node.kind === 'steering' || node.kind === 'assistant'))
|
|
63
|
+
.map(node => ({ node, text: nodeText(node) }))
|
|
64
|
+
.filter(item => item.text.length > 0)
|
|
65
|
+
|
|
66
|
+
if (items.length === 0) return ''
|
|
67
|
+
const matched = items.findIndex(item => item.text.includes(selection))
|
|
68
|
+
const center = matched >= 0 ? matched : items.length - 1
|
|
69
|
+
const indexes = []
|
|
70
|
+
if (center - 1 >= 0) indexes.push(center - 1)
|
|
71
|
+
indexes.push(center)
|
|
72
|
+
if (center + 1 < items.length) indexes.push(center + 1)
|
|
73
|
+
|
|
74
|
+
const blocks = []
|
|
75
|
+
let remaining = 1800
|
|
76
|
+
for (const index of indexes) {
|
|
77
|
+
if (remaining < 80) break
|
|
78
|
+
const item = items[index]
|
|
79
|
+
const selected = index === center
|
|
80
|
+
const budget = Math.min(selected ? 1000 : 400, remaining - 20)
|
|
81
|
+
const text = excerptAround(item.text, selection, budget)
|
|
82
|
+
const block = `【${roleLabel(item.node)}】\n${text}`
|
|
83
|
+
if (block.length <= remaining) {
|
|
84
|
+
blocks.push(block)
|
|
85
|
+
remaining -= block.length
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return blocks.join('\n\n').slice(0, 1800)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const baseButton = {
|
|
92
|
+
height: 26,
|
|
93
|
+
padding: '0 11px',
|
|
94
|
+
borderRadius: 7,
|
|
95
|
+
border: '1px solid var(--dsw-alias-border, rgba(127,127,127,.35))',
|
|
96
|
+
background: 'var(--dsw-alias-bg-elevated, #fff)',
|
|
97
|
+
color: 'var(--dsw-alias-text-primary, #111)',
|
|
98
|
+
font: '12px -apple-system, system-ui, sans-serif',
|
|
99
|
+
cursor: 'pointer',
|
|
100
|
+
whiteSpace: 'nowrap',
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function SelectionActions({ dismiss, readText, clearSelection, inputActions, useSession, useChat, sessionId }) {
|
|
104
|
+
const enabled = useSession(s => !s.removed && (s.subagent === null || s.subagent.address.mode === 'continuable'))
|
|
105
|
+
const nodes = useChat(snapshot => (snapshot && snapshot.legacy ? snapshot.legacy.nodes : []))
|
|
106
|
+
const [busy, setBusy] = React.useState(false)
|
|
107
|
+
const [message, setMessage] = React.useState('')
|
|
108
|
+
|
|
109
|
+
React.useEffect(() => {
|
|
110
|
+
if (!enabled) dismiss()
|
|
111
|
+
}, [enabled, dismiss])
|
|
112
|
+
if (!enabled) return null
|
|
113
|
+
|
|
114
|
+
const currentText = () => {
|
|
115
|
+
const text = readText()
|
|
116
|
+
if (text === null || !text.trim()) {
|
|
117
|
+
dismiss()
|
|
118
|
+
return null
|
|
119
|
+
}
|
|
120
|
+
return text
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const copy = async () => {
|
|
124
|
+
const text = currentText()
|
|
125
|
+
if (text === null) return
|
|
126
|
+
try {
|
|
127
|
+
await navigator.clipboard.writeText(text)
|
|
128
|
+
clearSelection()
|
|
129
|
+
dismiss()
|
|
130
|
+
} catch (error) {
|
|
131
|
+
setMessage('复制失败')
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const quote = () => {
|
|
136
|
+
const text = currentText()
|
|
137
|
+
if (text === null) return
|
|
138
|
+
if (!inputActions.insertQuote(text)) {
|
|
139
|
+
setMessage('当前会话不能引用')
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
clearSelection()
|
|
143
|
+
dismiss()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const explain = async () => {
|
|
147
|
+
const selection = currentText()
|
|
148
|
+
if (selection === null) return
|
|
149
|
+
setBusy(true)
|
|
150
|
+
setMessage('')
|
|
151
|
+
try {
|
|
152
|
+
const response = await fetch(BRIDGE, {
|
|
153
|
+
method: 'POST',
|
|
154
|
+
headers: { 'Content-Type': 'application/json' },
|
|
155
|
+
body: JSON.stringify({
|
|
156
|
+
selection,
|
|
157
|
+
context: buildContext(nodes, selection),
|
|
158
|
+
title: 'DSH 对话',
|
|
159
|
+
appName: 'DeepSeek Harness',
|
|
160
|
+
sessionID: String(sessionId || ''),
|
|
161
|
+
messageRole: 'assistant',
|
|
162
|
+
}),
|
|
163
|
+
})
|
|
164
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
165
|
+
clearSelection()
|
|
166
|
+
dismiss()
|
|
167
|
+
} catch (error) {
|
|
168
|
+
setMessage('知枝未运行,或本机 bridge 被占用;先打开知枝再试')
|
|
169
|
+
setBusy(false)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return h('div', { style: { display: 'flex', alignItems: 'center', gap: 8 } },
|
|
174
|
+
h('button', {
|
|
175
|
+
type: 'button',
|
|
176
|
+
style: baseButton,
|
|
177
|
+
onPointerDown: event => event.preventDefault(),
|
|
178
|
+
onClick: copy,
|
|
179
|
+
}, '复制'),
|
|
180
|
+
h('button', {
|
|
181
|
+
type: 'button',
|
|
182
|
+
style: baseButton,
|
|
183
|
+
onPointerDown: event => event.preventDefault(),
|
|
184
|
+
onClick: quote,
|
|
185
|
+
}, '引用'),
|
|
186
|
+
h('button', {
|
|
187
|
+
type: 'button',
|
|
188
|
+
style: { ...baseButton, fontWeight: 500 },
|
|
189
|
+
disabled: busy,
|
|
190
|
+
onPointerDown: event => event.preventDefault(),
|
|
191
|
+
onClick: explain,
|
|
192
|
+
}, busy ? '正在发送…' : '用知枝解释'),
|
|
193
|
+
message ? h('span', { style: { fontSize: 11, opacity: 0.7, maxWidth: 260 } }, message) : null)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
inject: ['slots'],
|
|
198
|
+
apply(ctx) {
|
|
199
|
+
// key 复用 quote:不新增表面,直接替换为三合一的选词操作条。
|
|
200
|
+
ctx.slots.inject('conversation.chat.text-interaction', () => ctx.slots.register({
|
|
201
|
+
name: 'conversation.chat.text-interaction',
|
|
202
|
+
key: 'quote',
|
|
203
|
+
// message-quotes 已以 priority 0 占了 quote;同一 key 必须用更低的 priority
|
|
204
|
+
// 才能覆盖(slot 规则:数值越低越优先渲染)。
|
|
205
|
+
priority: -10,
|
|
206
|
+
}, SelectionActions))
|
|
207
|
+
},
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
})
|
package/cordis.patch.yml
ADDED
package/docs/DETAIL.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# 知枝选词解释
|
|
2
|
+
|
|
3
|
+
在 DSH 对话里选中文字后,原来的「引用」操作条会变成 **复制 / 引用 / 用知枝解释** 三个按钮,不再额外弹一个和原生 Copy 重叠的右键菜单。
|
|
4
|
+
|
|
5
|
+
- 插件只读取选中文字,以及选中消息前后各最多一条**真实用户/助手消息**;系统注入的 `<system-reminder>`、技能清单、context 节点会被过滤,上下文总长 ≤1800 字。
|
|
6
|
+
- 通过本机 `127.0.0.1:53917` 的 bridge 发送给 **正在运行的知枝**,由知枝建卡并流式解释。
|
|
7
|
+
- 不会把内容上传到第三方;上下文只在本机 DSH 和知枝之间传递。
|
|
8
|
+
|
|
9
|
+
## 前置条件
|
|
10
|
+
|
|
11
|
+
1. 知枝已启动(菜单栏有图标)。
|
|
12
|
+
2. 知枝设置里启用了「启用跨应用划词」。
|
|
13
|
+
3. 选中 DSH 中已展示的用户内容或已完成的 Assistant 文本;工具、注入上下文、流式输出不参与选区交互。
|
|
14
|
+
|
|
15
|
+
## 限制
|
|
16
|
+
|
|
17
|
+
- 这是 DSH Client 插件,只出现在 Web UI;终端/CLI 不生效。
|
|
18
|
+
- 知枝退出后按钮会提示「知枝未运行」,重试即可。
|
package/icon.svg
ADDED
package/index.js
ADDED
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,53 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-sprig",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "DSH client plugin: send selected conversation text to Sprig and get a source-aware explanation card.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/thatwzzh/sprig.git",
|
|
10
|
+
"directory": "dsh-plugins/sprig"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/thatwzzh/sprig/tree/main/dsh-plugins/sprig",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/thatwzzh/sprig/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"dsh",
|
|
18
|
+
"dsh-plugin",
|
|
19
|
+
"sprig",
|
|
20
|
+
"selection",
|
|
21
|
+
"explain",
|
|
22
|
+
"macos"
|
|
23
|
+
],
|
|
24
|
+
"exports": {
|
|
25
|
+
".": "./index.js",
|
|
26
|
+
"./client": "./client.js",
|
|
27
|
+
"./locale/*.json": "./locale/*.json",
|
|
28
|
+
"./icon": "./icon.svg"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"index.js",
|
|
32
|
+
"client.js",
|
|
33
|
+
"cordis.patch.yml",
|
|
34
|
+
"locale/*.json",
|
|
35
|
+
"docs",
|
|
36
|
+
"icon.svg",
|
|
37
|
+
"README.md"
|
|
38
|
+
],
|
|
39
|
+
"dsh": {
|
|
40
|
+
"bundle": {
|
|
41
|
+
"patch": "./cordis.patch.yml"
|
|
42
|
+
},
|
|
43
|
+
"client": {
|
|
44
|
+
"platform": "web",
|
|
45
|
+
"immediately": true,
|
|
46
|
+
"inject": [
|
|
47
|
+
"@deepseek-ai/dsh-client-ui-conversation",
|
|
48
|
+
"@deepseek-ai/dsh-client-ui-chat",
|
|
49
|
+
"@deepseek-ai/dsh-message-quotes"
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|