@chentyit/dsh-chatbot-panel 0.0.0-stage → 0.2.0
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/LICENSE +21 -0
- package/README.md +174 -2
- package/client.js +516 -0
- package/cordis.patch.yml +24 -0
- package/icon.svg +15 -0
- package/index.js +718 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +59 -4
- package/pure.js +466 -0
package/client.js
ADDED
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser half of the ChatBot capability panel bundle.
|
|
3
|
+
*
|
|
4
|
+
* Renders a split capsule immediately right of the permission chip in the
|
|
5
|
+
* composer tool row: the left segment toggles the session's web capability and
|
|
6
|
+
* the right segment opens an upward panel listing the capabilities plus this
|
|
7
|
+
* session's usage and estimated cost. Capability state and usage are Host
|
|
8
|
+
* projections; this file only reads them and dispatches `/chatbot ...` lines.
|
|
9
|
+
*/
|
|
10
|
+
window.__ModuleLoader__.load({
|
|
11
|
+
id: '@chentyit/dsh-chatbot-panel',
|
|
12
|
+
factory(require) {
|
|
13
|
+
const React = require('react')
|
|
14
|
+
const { createPortal } = require('react-dom')
|
|
15
|
+
const h = React.createElement
|
|
16
|
+
|
|
17
|
+
/** Locale namespace for the capsule copy and the registration's `t`. */
|
|
18
|
+
const NS = 'chatbot-panel'
|
|
19
|
+
/** Official platform usage page; the Host projection carries the same link. */
|
|
20
|
+
const USAGE_URL = 'https://platform.deepseek.com/usage'
|
|
21
|
+
/** Panel width in CSS pixels, used to keep it inside the viewport. */
|
|
22
|
+
const PANEL_WIDTH = 328
|
|
23
|
+
|
|
24
|
+
const zh = {
|
|
25
|
+
label: 'ChatBot',
|
|
26
|
+
openPanel: '展开 ChatBot 能力',
|
|
27
|
+
panelTitle: 'ChatBot 能力',
|
|
28
|
+
scope: '本会话',
|
|
29
|
+
groupWeb: '联网工具',
|
|
30
|
+
web: '联网搜索',
|
|
31
|
+
webHint: '¥5 / 千次 · 附图提问即搜图',
|
|
32
|
+
deep: '深度思考搜索',
|
|
33
|
+
deepHint: '多步推理 · 支持以图搜图 · 更耗 token',
|
|
34
|
+
deepNeedsWeb: '先开启联网搜索',
|
|
35
|
+
searchNote: '以图搜图只读本轮图片;搜图与深搜需 DeepSeek 账号或 API key。',
|
|
36
|
+
pending: '应用中…',
|
|
37
|
+
usageTitle: '本会话用量',
|
|
38
|
+
search: '搜索',
|
|
39
|
+
fetch: '抓取',
|
|
40
|
+
imageUsage: '搜图',
|
|
41
|
+
billed: '次计费请求',
|
|
42
|
+
imagesCount: '张图',
|
|
43
|
+
deepUsage: '深搜',
|
|
44
|
+
failed: '失败',
|
|
45
|
+
times: '次',
|
|
46
|
+
sources: '个来源',
|
|
47
|
+
toolCalls: '次工具调用',
|
|
48
|
+
input: '输入',
|
|
49
|
+
output: '输出',
|
|
50
|
+
feeTitle: '费用估算',
|
|
51
|
+
searchFee: '搜索费',
|
|
52
|
+
fetchFee: '抓取费',
|
|
53
|
+
imageToolFee: '搜图工具费',
|
|
54
|
+
imageTokenFee: '搜图 token 费',
|
|
55
|
+
deepToolFee: '深搜工具费',
|
|
56
|
+
deepTokenFee: '深搜 token 费',
|
|
57
|
+
totalFee: '合计',
|
|
58
|
+
fetchFree: '匿名抓取不计费',
|
|
59
|
+
feeNote: '按官方价估算,实际以平台账单为准',
|
|
60
|
+
usageLink: '查看官方用量 ↗',
|
|
61
|
+
active: '已开启',
|
|
62
|
+
inactive: '未开启',
|
|
63
|
+
errorUnknown: '命令未生效',
|
|
64
|
+
errorPrefix: '操作失败:',
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const en = {
|
|
68
|
+
label: 'ChatBot',
|
|
69
|
+
openPanel: 'Open ChatBot capabilities',
|
|
70
|
+
panelTitle: 'ChatBot capabilities',
|
|
71
|
+
scope: 'this session',
|
|
72
|
+
groupWeb: 'Web tools',
|
|
73
|
+
web: 'Web search',
|
|
74
|
+
webHint: '¥5 / 1,000 · attach an image and ask',
|
|
75
|
+
deep: 'Deep thinking search',
|
|
76
|
+
deepHint: 'Multi-step · image search · token-heavy',
|
|
77
|
+
deepNeedsWeb: 'Turn on web search first',
|
|
78
|
+
searchNote: 'Image search reads only this turn\'s images; image and deep search need a DeepSeek sign-in or API key.',
|
|
79
|
+
pending: 'Applying…',
|
|
80
|
+
usageTitle: 'Session usage',
|
|
81
|
+
search: 'Search',
|
|
82
|
+
fetch: 'Fetch',
|
|
83
|
+
imageUsage: 'Image search',
|
|
84
|
+
billed: 'billed requests',
|
|
85
|
+
imagesCount: 'images',
|
|
86
|
+
deepUsage: 'Deep',
|
|
87
|
+
failed: 'Failed',
|
|
88
|
+
times: '',
|
|
89
|
+
sources: 'sources',
|
|
90
|
+
toolCalls: 'tool calls',
|
|
91
|
+
input: 'in',
|
|
92
|
+
output: 'out',
|
|
93
|
+
feeTitle: 'Estimated cost',
|
|
94
|
+
searchFee: 'Search',
|
|
95
|
+
fetchFee: 'Fetch',
|
|
96
|
+
imageToolFee: 'Image tools',
|
|
97
|
+
imageTokenFee: 'Image tokens',
|
|
98
|
+
deepToolFee: 'Deep tools',
|
|
99
|
+
deepTokenFee: 'Deep tokens',
|
|
100
|
+
totalFee: 'Total',
|
|
101
|
+
fetchFree: 'Anonymous fetch is free',
|
|
102
|
+
feeNote: 'Estimated from official prices; the platform bill is authoritative',
|
|
103
|
+
usageLink: 'Open platform usage ↗',
|
|
104
|
+
active: 'on',
|
|
105
|
+
inactive: 'off',
|
|
106
|
+
errorUnknown: 'The command did not apply',
|
|
107
|
+
errorPrefix: 'Failed: ',
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const CSS = [
|
|
111
|
+
/* Chip chrome copied from the permission trigger: transparent, 28px,
|
|
112
|
+
label-secondary, hover-only tint, small glyph and chevron. No border,
|
|
113
|
+
no fill, so it reads as another composer tool next to 「工作区内修改」. */
|
|
114
|
+
'.cbp-capsule{display:inline-flex;align-items:center;height:28px;border-radius:var(--dsw-radius-sm);background:transparent;color:var(--dsw-alias-label-secondary);font-family:var(--dsw-font-family,inherit);font-size:13px;line-height:20px;font-weight:500;vertical-align:middle}',
|
|
115
|
+
'.cbp-main{display:inline-flex;align-items:center;gap:4px;min-width:0;max-width:220px;height:28px;box-sizing:border-box;padding:0 4px 0 8px;border:none;border-radius:var(--dsw-radius-sm);outline:none;background:transparent;color:inherit;font:inherit;cursor:pointer}',
|
|
116
|
+
/* The chip is one control: clicking anywhere on it opens or closes the
|
|
117
|
+
capability panel. Hover tints the whole chip, arrow included. */
|
|
118
|
+
'@media (hover: hover) {',
|
|
119
|
+
'.cbp-capsule:hover{background:var(--dsw-alias-interactive-bg-hover)}',
|
|
120
|
+
'}',
|
|
121
|
+
'.cbp-main:focus-visible{box-shadow:0 0 0 2px var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary))}',
|
|
122
|
+
'.cbp-icon{display:inline-flex;flex:0 0 auto;color:var(--dsw-alias-label-caption);transition:color .12s ease}',
|
|
123
|
+
'.cbp-icon svg{display:block;width:14px;height:14px}',
|
|
124
|
+
'.cbp-capsule[data-on="true"] .cbp-icon{color:var(--dsw-alias-state-success-primary)}',
|
|
125
|
+
'.cbp-capsule[data-on="true"] .cbp-label{color:var(--dsw-alias-label-primary)}',
|
|
126
|
+
'.cbp-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
127
|
+
'.cbp-arrow{display:inline-flex;flex:0 0 auto;margin-left:1px;color:var(--dsw-alias-label-caption)}',
|
|
128
|
+
'.cbp-arrow svg{display:block;width:12px;height:12px;transition:transform 120ms ease}',
|
|
129
|
+
'.cbp-main[data-open="true"] .cbp-arrow svg{transform:rotate(180deg)}',
|
|
130
|
+
'.cbp-panel{position:fixed;z-index:1200;box-sizing:border-box;display:flex;flex-direction:column;padding:6px;border-radius:var(--dsw-radius-lg);background:var(--dsw-specific-menu,var(--dsw-alias-bg-layer-1,#fff));backdrop-filter:var(--dsw-menu-backdrop-filter,none);box-shadow:var(--dsw-elevation-prominent,0 12px 32px rgba(0,0,0,.18));color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family,inherit);overflow-y:auto;overscroll-behavior:contain}',
|
|
131
|
+
'.cbp-head{display:flex;align-items:baseline;justify-content:space-between;gap:8px;padding:6px 8px 2px}',
|
|
132
|
+
'.cbp-head-title{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary)}',
|
|
133
|
+
'.cbp-head-scope{font-size:11px;color:var(--dsw-alias-label-caption)}',
|
|
134
|
+
'.cbp-group{padding:8px 8px 2px;font-size:11px;font-weight:600;letter-spacing:.03em;color:var(--dsw-alias-label-tertiary)}',
|
|
135
|
+
'.cbp-group-note{padding:4px 8px 2px}',
|
|
136
|
+
'.cbp-row{display:flex;align-items:center;gap:10px;width:100%;box-sizing:border-box;padding:7px 8px;border:0;border-radius:var(--dsw-radius-md,8px);background:transparent;color:var(--dsw-alias-label-primary);font-family:inherit;text-align:left;cursor:pointer}',
|
|
137
|
+
'.cbp-row:hover:not([aria-disabled="true"]){background:var(--dsw-alias-interactive-bg-hover)}',
|
|
138
|
+
'.cbp-row:focus-visible{outline:var(--dsw-focus-ring-width,2px) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:-1px}',
|
|
139
|
+
'.cbp-row[aria-disabled="true"]{cursor:default;opacity:.55}',
|
|
140
|
+
'.cbp-row-sub{padding-left:22px}',
|
|
141
|
+
'.cbp-row-text{display:flex;flex-direction:column;gap:1px;min-width:0;flex:1}',
|
|
142
|
+
'.cbp-row-label{font-size:13px;line-height:18px}',
|
|
143
|
+
'.cbp-row-hint{font-size:11px;line-height:15px;color:var(--dsw-alias-label-secondary)}',
|
|
144
|
+
'.cbp-check{display:inline-flex;align-items:center;justify-content:center;flex:none;width:18px;height:18px;border-radius:var(--dsw-radius-sm,6px);border:1.5px solid var(--dsw-alias-border-l2,currentColor);box-sizing:border-box;color:transparent}',
|
|
145
|
+
'.cbp-check[data-checked="true"]{border-color:transparent;background:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-label-primary-inverted)}',
|
|
146
|
+
'.cbp-check svg{display:block;width:12px;height:12px}',
|
|
147
|
+
'.cbp-sep{height:1px;margin:6px 6px;background:var(--dsw-alias-border-l1)}',
|
|
148
|
+
'.cbp-usage{display:flex;flex-direction:column;gap:2px;padding:2px 8px 4px;font-size:11px;line-height:17px;color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums}',
|
|
149
|
+
'.cbp-usage-title{font-size:11px;font-weight:600;letter-spacing:.03em;color:var(--dsw-alias-label-tertiary);padding:0 0 2px}',
|
|
150
|
+
'.cbp-usage-line{display:flex;justify-content:space-between;gap:10px}',
|
|
151
|
+
'.cbp-usage-line b{font-weight:600;color:var(--dsw-alias-label-primary)}',
|
|
152
|
+
'.cbp-fee{border-top:1px dashed var(--dsw-alias-border-l1);margin-top:4px;padding-top:4px}',
|
|
153
|
+
'.cbp-total{display:flex;justify-content:space-between;gap:10px;font-size:12px;color:var(--dsw-alias-label-primary);font-weight:600}',
|
|
154
|
+
'.cbp-note{font-size:10px;line-height:15px;color:var(--dsw-alias-label-caption)}',
|
|
155
|
+
'.cbp-link{font-size:11px;line-height:17px;color:var(--dsw-alias-link);text-decoration:none}',
|
|
156
|
+
'.cbp-link:hover{text-decoration:underline}',
|
|
157
|
+
'.cbp-error{margin:4px 6px 2px;padding:5px 8px;border-radius:var(--dsw-radius-sm,6px);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 12%,transparent);color:var(--dsw-alias-state-error-primary);font-size:11px;line-height:15px}',
|
|
158
|
+
].join('')
|
|
159
|
+
|
|
160
|
+
/** The capsule owns its stylesheet, removed with the plugin's activation. */
|
|
161
|
+
function insertStyles() {
|
|
162
|
+
if (typeof document === 'undefined') return () => {}
|
|
163
|
+
const tag = document.createElement('style')
|
|
164
|
+
tag.setAttribute('data-plugin', '@chentyit/dsh-chatbot-panel')
|
|
165
|
+
tag.textContent = CSS
|
|
166
|
+
document.head.appendChild(tag)
|
|
167
|
+
return () => { tag.remove() }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Format a yuan amount for the detail rows. */
|
|
171
|
+
function yuan(value) {
|
|
172
|
+
const amount = typeof value === 'number' && Number.isFinite(value) ? value : 0
|
|
173
|
+
return `¥${amount.toFixed(4)}`
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Format a token count compactly. */
|
|
177
|
+
function tokens(value) {
|
|
178
|
+
const amount = typeof value === 'number' && Number.isFinite(value) ? value : 0
|
|
179
|
+
return amount >= 1000 ? `${(amount / 1000).toFixed(1)}k` : String(amount)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Join a count and its unit without a trailing space when the unit is empty. */
|
|
183
|
+
function count(value, unit) {
|
|
184
|
+
const amount = typeof value === 'number' && Number.isFinite(value) ? value : 0
|
|
185
|
+
return typeof unit === 'string' && unit.length > 0 ? `${amount} ${unit}` : String(amount)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** A small check mark drawn in the theme's inverted label color. */
|
|
189
|
+
function CheckGlyph() {
|
|
190
|
+
return h('svg', { viewBox: '0 0 12 12', 'aria-hidden': true },
|
|
191
|
+
h('path', {
|
|
192
|
+
d: 'M2.2 6.3 4.6 8.7 9.8 3.4',
|
|
193
|
+
fill: 'none',
|
|
194
|
+
stroke: 'currentColor',
|
|
195
|
+
strokeWidth: '1.8',
|
|
196
|
+
strokeLinecap: 'round',
|
|
197
|
+
strokeLinejoin: 'round',
|
|
198
|
+
}))
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Chevron pointing up while the panel is closed, down while it is open. */
|
|
202
|
+
/** A small bot-head mark (antenna, head, eyes) matching the 14px chip glyphs. */
|
|
203
|
+
function BotGlyph() {
|
|
204
|
+
// Optically centered against the 13px label: text ink sits ~0.35px above
|
|
205
|
+
// the flex-line center, so the glyph's ink bounds (outer top 1.17 … outer
|
|
206
|
+
// bottom 12.12, stroke included) center on y≈6.65 instead of 7. Measured
|
|
207
|
+
// from a rendered chip screenshot, this lands the icon level with the text.
|
|
208
|
+
return h('svg', { viewBox: '0 0 14 14', 'aria-hidden': true },
|
|
209
|
+
h('path', {
|
|
210
|
+
d: 'M7 3.67v1.2',
|
|
211
|
+
fill: 'none',
|
|
212
|
+
stroke: 'currentColor',
|
|
213
|
+
strokeWidth: '1.2',
|
|
214
|
+
strokeLinecap: 'round',
|
|
215
|
+
}),
|
|
216
|
+
h('circle', { cx: 7, cy: 2.72, r: 0.95, fill: 'currentColor' }),
|
|
217
|
+
h('rect', {
|
|
218
|
+
x: 2.1,
|
|
219
|
+
y: 4.92,
|
|
220
|
+
width: 9.8,
|
|
221
|
+
height: 6.6,
|
|
222
|
+
rx: 2.2,
|
|
223
|
+
fill: 'none',
|
|
224
|
+
stroke: 'currentColor',
|
|
225
|
+
strokeWidth: '1.2',
|
|
226
|
+
}),
|
|
227
|
+
h('circle', { cx: 5.3, cy: 7.27, r: 0.85, fill: 'currentColor' }),
|
|
228
|
+
h('circle', { cx: 8.7, cy: 7.27, r: 0.85, fill: 'currentColor' }),
|
|
229
|
+
h('path', {
|
|
230
|
+
d: 'M5.6 9.37h2.8',
|
|
231
|
+
fill: 'none',
|
|
232
|
+
stroke: 'currentColor',
|
|
233
|
+
strokeWidth: '1.1',
|
|
234
|
+
strokeLinecap: 'round',
|
|
235
|
+
}))
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Chevron pointing down; the open state rotates it in CSS. */
|
|
239
|
+
function ChevronGlyph() {
|
|
240
|
+
return h('svg', { viewBox: '0 0 12 12', 'aria-hidden': true },
|
|
241
|
+
h('path', {
|
|
242
|
+
d: 'M2.4 4.4 6 8 9.6 4.4',
|
|
243
|
+
fill: 'none',
|
|
244
|
+
stroke: 'currentColor',
|
|
245
|
+
strokeWidth: '1.6',
|
|
246
|
+
strokeLinecap: 'round',
|
|
247
|
+
strokeLinejoin: 'round',
|
|
248
|
+
}))
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** One multi-select capability row. */
|
|
252
|
+
function CapabilityRow(props) {
|
|
253
|
+
const { t, checked, label, hint, sub, disabled, onToggle } = props
|
|
254
|
+
return h('button', {
|
|
255
|
+
type: 'button',
|
|
256
|
+
role: 'checkbox',
|
|
257
|
+
'aria-checked': checked ? 'true' : 'false',
|
|
258
|
+
'aria-disabled': disabled ? 'true' : undefined,
|
|
259
|
+
className: sub === true ? 'cbp-row cbp-row-sub' : 'cbp-row',
|
|
260
|
+
onClick: disabled ? undefined : onToggle,
|
|
261
|
+
},
|
|
262
|
+
h('span', { className: 'cbp-row-text' },
|
|
263
|
+
h('span', { className: 'cbp-row-label' }, label),
|
|
264
|
+
typeof hint === 'string' && hint.length > 0 ? h('span', { className: 'cbp-row-hint' }, hint) : null),
|
|
265
|
+
h('span', { className: 'cbp-check', 'data-checked': checked ? 'true' : 'false', 'aria-hidden': true },
|
|
266
|
+
checked ? h(CheckGlyph) : null))
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Detailed usage and estimate block. */
|
|
270
|
+
function UsageBlock(props) {
|
|
271
|
+
const { t, usage } = props
|
|
272
|
+
if (usage === undefined || usage === null) {
|
|
273
|
+
return h('div', { className: 'cbp-usage' },
|
|
274
|
+
h('div', { className: 'cbp-usage-title' }, t('usageTitle')),
|
|
275
|
+
h('div', { className: 'cbp-usage-line' }, h('span', null, t('search')), h('b', null, count(0, t('times')))))
|
|
276
|
+
}
|
|
277
|
+
const search = usage.search || { calls: 0, queries: 0, sources: 0, failed: 0, failedQueries: 0 }
|
|
278
|
+
const fetch = usage.fetch || { calls: 0, failed: 0 }
|
|
279
|
+
const image = usage.image || { calls: 0, failed: 0, images: 0, results: 0, imageResults: 0, toolSearch: 0, toolFetch: 0, imageSearch: 0, inputTokens: 0, outputTokens: 0 }
|
|
280
|
+
const deep = usage.deep || { calls: 0, failed: 0, toolSearch: 0, toolFetch: 0, imageSearch: 0, inputTokens: 0, outputTokens: 0, sources: 0 }
|
|
281
|
+
const fees = usage.fees || { search: 0, fetch: 0, image: 0, imageTools: 0, imageTokens: 0, deepTools: 0, deepTokens: 0, total: 0 }
|
|
282
|
+
const deepTools = (deep.toolSearch || 0) + (deep.toolFetch || 0) + (deep.imageSearch || 0)
|
|
283
|
+
const imageBilled = (image.toolSearch || 0) + (image.toolFetch || 0) + (image.imageSearch || 0)
|
|
284
|
+
const showImage = image.calls > 0 || image.failed > 0 || imageBilled > 0
|
|
285
|
+
const failures = (search.failed || 0) + (fetch.failed || 0) + (image.failed || 0) + (deep.failed || 0)
|
|
286
|
+
const lines = []
|
|
287
|
+
lines.push(h('div', { key: 'title', className: 'cbp-usage-title' }, t('usageTitle')))
|
|
288
|
+
lines.push(h('div', { key: 'search', className: 'cbp-usage-line' },
|
|
289
|
+
h('span', null, t('search')),
|
|
290
|
+
h('span', null,
|
|
291
|
+
h('b', null, count(search.calls, t('billed'))),
|
|
292
|
+
` · ${count(search.sources, t('sources'))}`)))
|
|
293
|
+
if (showImage) {
|
|
294
|
+
lines.push(h('div', { key: 'image', className: 'cbp-usage-line' },
|
|
295
|
+
h('span', null, t('imageUsage')),
|
|
296
|
+
h('span', null,
|
|
297
|
+
h('b', null, count(image.calls, t('times'))),
|
|
298
|
+
` · ${count(image.images, t('imagesCount'))} · ${count(imageBilled, t('billed'))}`)))
|
|
299
|
+
}
|
|
300
|
+
lines.push(h('div', { key: 'fetch', className: 'cbp-usage-line' },
|
|
301
|
+
h('span', null, t('fetch')),
|
|
302
|
+
h('span', null,
|
|
303
|
+
h('b', null, count(fetch.calls, t('times'))),
|
|
304
|
+
usage.countFetchCost === true ? '' : ` · ${t('fetchFree')}`)))
|
|
305
|
+
lines.push(h('div', { key: 'deep', className: 'cbp-usage-line' },
|
|
306
|
+
h('span', null, t('deepUsage')),
|
|
307
|
+
h('span', null,
|
|
308
|
+
h('b', null, count(deep.calls, t('times'))),
|
|
309
|
+
` · ${count(deepTools, t('toolCalls'))} · ${tokens(deep.inputTokens)}/${tokens(deep.outputTokens)} tok`)))
|
|
310
|
+
if (failures > 0) {
|
|
311
|
+
lines.push(h('div', { key: 'failed', className: 'cbp-usage-line' },
|
|
312
|
+
h('span', null, t('failed')),
|
|
313
|
+
h('span', null, count(failures, t('times')))))
|
|
314
|
+
}
|
|
315
|
+
lines.push(h('div', { key: 'sep', className: 'cbp-sep' }))
|
|
316
|
+
lines.push(h('div', { key: 'fee-title', className: 'cbp-usage-title' }, t('feeTitle')))
|
|
317
|
+
lines.push(h('div', { key: 'fee-search', className: 'cbp-usage-line' },
|
|
318
|
+
h('span', null, t('searchFee')), h('span', null, yuan(fees.search))))
|
|
319
|
+
if (showImage) {
|
|
320
|
+
lines.push(h('div', { key: 'fee-image-tools', className: 'cbp-usage-line' },
|
|
321
|
+
h('span', null, t('imageToolFee')), h('span', null, yuan(fees.imageTools))))
|
|
322
|
+
lines.push(h('div', { key: 'fee-image-tokens', className: 'cbp-usage-line' },
|
|
323
|
+
h('span', null, t('imageTokenFee')), h('span', null, yuan(fees.imageTokens))))
|
|
324
|
+
}
|
|
325
|
+
lines.push(h('div', { key: 'fee-fetch', className: 'cbp-usage-line' },
|
|
326
|
+
h('span', null, t('fetchFee')), h('span', null, yuan(fees.fetch))))
|
|
327
|
+
lines.push(h('div', { key: 'fee-deep-tools', className: 'cbp-usage-line' },
|
|
328
|
+
h('span', null, t('deepToolFee')), h('span', null, yuan(fees.deepTools))))
|
|
329
|
+
lines.push(h('div', { key: 'fee-deep-tokens', className: 'cbp-usage-line' },
|
|
330
|
+
h('span', null, t('deepTokenFee')), h('span', null, yuan(fees.deepTokens))))
|
|
331
|
+
lines.push(h('div', { key: 'total', className: 'cbp-total' },
|
|
332
|
+
h('span', null, t('totalFee')), h('span', null, `≈ ${yuan(fees.total)}`)))
|
|
333
|
+
lines.push(h('div', { key: 'note', className: 'cbp-note' }, t('feeNote')))
|
|
334
|
+
lines.push(h('a', {
|
|
335
|
+
key: 'link',
|
|
336
|
+
className: 'cbp-link',
|
|
337
|
+
href: typeof usage.usageUrl === 'string' && usage.usageUrl.length > 0 ? usage.usageUrl : USAGE_URL,
|
|
338
|
+
target: '_blank',
|
|
339
|
+
rel: 'noopener noreferrer',
|
|
340
|
+
}, t('usageLink')))
|
|
341
|
+
return h('div', { className: 'cbp-usage' }, ...lines)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** The composer control: split capsule plus the upward panel. */
|
|
345
|
+
function ChatBotControl(props) {
|
|
346
|
+
const t = typeof props.t === 'function' ? props.t : key => key
|
|
347
|
+
const caps = props.useProjection('chatbotCapabilities')
|
|
348
|
+
const usage = props.useProjection('chatbotUsage')
|
|
349
|
+
const web = caps !== undefined && caps !== null && caps.web === true
|
|
350
|
+
const deep = caps !== undefined && caps !== null && caps.deep === true
|
|
351
|
+
const pending = caps !== undefined && caps !== null && caps.pending === true
|
|
352
|
+
const [open, setOpen] = React.useState(false)
|
|
353
|
+
const [busy, setBusy] = React.useState(false)
|
|
354
|
+
const [error, setError] = React.useState(null)
|
|
355
|
+
const [position, setPosition] = React.useState(null)
|
|
356
|
+
const wrapRef = React.useRef(null)
|
|
357
|
+
const panelRef = React.useRef(null)
|
|
358
|
+
|
|
359
|
+
const submit = typeof props.submit === 'function' ? props.submit : async () => undefined
|
|
360
|
+
const sessionId = props.sessionId
|
|
361
|
+
const run = React.useCallback((line) => {
|
|
362
|
+
setBusy(true)
|
|
363
|
+
setError(null)
|
|
364
|
+
Promise.resolve()
|
|
365
|
+
.then(() => submit(sessionId, line))
|
|
366
|
+
.then((result) => {
|
|
367
|
+
if (result === undefined || result === null) { setError(t('errorUnknown')); return }
|
|
368
|
+
if (result.ok !== true) {
|
|
369
|
+
const message = result.error && typeof result.error.message === 'string' ? result.error.message : t('errorUnknown')
|
|
370
|
+
setError(message)
|
|
371
|
+
return
|
|
372
|
+
}
|
|
373
|
+
const value = result.value
|
|
374
|
+
if (value === undefined) { setError(t('errorUnknown')); return }
|
|
375
|
+
if (value !== null && typeof value === 'object' && value.kind === 'error') {
|
|
376
|
+
setError(typeof value.text === 'string' ? value.text : t('errorUnknown'))
|
|
377
|
+
}
|
|
378
|
+
})
|
|
379
|
+
.catch((failure) => { setError(`${t('errorPrefix')}${String(failure)}`) })
|
|
380
|
+
.finally(() => { setBusy(false) })
|
|
381
|
+
}, [submit, sessionId, t])
|
|
382
|
+
|
|
383
|
+
const place = React.useCallback(() => {
|
|
384
|
+
const element = wrapRef.current
|
|
385
|
+
if (element === null || typeof window === 'undefined') return
|
|
386
|
+
const rect = element.getBoundingClientRect()
|
|
387
|
+
const width = Math.min(PANEL_WIDTH, Math.max(160, window.innerWidth - 16))
|
|
388
|
+
const left = Math.min(Math.max(8, rect.left), Math.max(8, window.innerWidth - width - 8))
|
|
389
|
+
const bottom = Math.max(8, window.innerHeight - rect.top + 8)
|
|
390
|
+
setPosition({ left, bottom, width, maxHeight: Math.max(120, window.innerHeight - bottom - 8) })
|
|
391
|
+
}, [])
|
|
392
|
+
|
|
393
|
+
React.useLayoutEffect(() => {
|
|
394
|
+
if (!open) return undefined
|
|
395
|
+
place()
|
|
396
|
+
const onResize = () => { place() }
|
|
397
|
+
window.addEventListener('resize', onResize)
|
|
398
|
+
return () => { window.removeEventListener('resize', onResize) }
|
|
399
|
+
}, [open, place])
|
|
400
|
+
|
|
401
|
+
React.useEffect(() => {
|
|
402
|
+
if (!open || panelRef.current === null) return undefined
|
|
403
|
+
const first = panelRef.current.querySelector('button[role="checkbox"]:not([aria-disabled="true"])')
|
|
404
|
+
if (first !== null && typeof first.focus === 'function') first.focus({ preventScroll: true })
|
|
405
|
+
return undefined
|
|
406
|
+
}, [open])
|
|
407
|
+
|
|
408
|
+
React.useEffect(() => {
|
|
409
|
+
if (!open) return undefined
|
|
410
|
+
const onPointerDown = (event) => {
|
|
411
|
+
if (wrapRef.current !== null && wrapRef.current.contains(event.target)) return
|
|
412
|
+
if (panelRef.current !== null && panelRef.current.contains(event.target)) return
|
|
413
|
+
setOpen(false)
|
|
414
|
+
}
|
|
415
|
+
const onKeyDown = (event) => {
|
|
416
|
+
if (event.key !== 'Escape') return
|
|
417
|
+
setOpen(false)
|
|
418
|
+
const trigger = wrapRef.current === null ? null : wrapRef.current.querySelector('button')
|
|
419
|
+
if (trigger !== null && typeof trigger.focus === 'function') trigger.focus()
|
|
420
|
+
}
|
|
421
|
+
document.addEventListener('pointerdown', onPointerDown, true)
|
|
422
|
+
document.addEventListener('keydown', onKeyDown, true)
|
|
423
|
+
return () => {
|
|
424
|
+
document.removeEventListener('pointerdown', onPointerDown, true)
|
|
425
|
+
document.removeEventListener('keydown', onKeyDown, true)
|
|
426
|
+
}
|
|
427
|
+
}, [open])
|
|
428
|
+
|
|
429
|
+
const disabled = busy || pending
|
|
430
|
+
|
|
431
|
+
const active = web || deep
|
|
432
|
+
|
|
433
|
+
const capsule = h('span', {
|
|
434
|
+
ref: wrapRef,
|
|
435
|
+
className: 'cbp-capsule',
|
|
436
|
+
'data-on': active ? 'true' : 'false',
|
|
437
|
+
},
|
|
438
|
+
h('button', {
|
|
439
|
+
type: 'button',
|
|
440
|
+
className: 'cbp-main',
|
|
441
|
+
'data-open': open ? 'true' : 'false',
|
|
442
|
+
'aria-haspopup': 'dialog',
|
|
443
|
+
'aria-expanded': open ? 'true' : 'false',
|
|
444
|
+
'aria-label': `${t('openPanel')} · ${active ? t('active') : t('inactive')}`,
|
|
445
|
+
title: t('openPanel'),
|
|
446
|
+
onClick: () => { setOpen(value => !value) },
|
|
447
|
+
},
|
|
448
|
+
h('span', { className: 'cbp-icon', 'aria-hidden': true }, h(BotGlyph)),
|
|
449
|
+
h('span', { className: 'cbp-label' }, t('label')),
|
|
450
|
+
h('span', { className: 'cbp-arrow', 'aria-hidden': true }, h(ChevronGlyph))))
|
|
451
|
+
|
|
452
|
+
let panel = null
|
|
453
|
+
if (open && typeof document !== 'undefined') {
|
|
454
|
+
panel = createPortal(h('div', {
|
|
455
|
+
ref: panelRef,
|
|
456
|
+
className: 'cbp-panel',
|
|
457
|
+
role: 'dialog',
|
|
458
|
+
'aria-label': t('panelTitle'),
|
|
459
|
+
style: {
|
|
460
|
+
left: position === null ? 0 : `${position.left}px`,
|
|
461
|
+
bottom: position === null ? 12 : `${position.bottom}px`,
|
|
462
|
+
width: position === null ? 'min(328px, calc(100vw - 16px))' : `${position.width}px`,
|
|
463
|
+
maxHeight: position === null ? 'calc(100vh - 24px)' : `${position.maxHeight}px`,
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
h('div', { className: 'cbp-head' },
|
|
467
|
+
h('span', { className: 'cbp-head-title' }, t('panelTitle')),
|
|
468
|
+
h('span', { className: 'cbp-head-scope' }, t('scope'))),
|
|
469
|
+
h('div', { className: 'cbp-group' }, t('groupWeb')),
|
|
470
|
+
h(CapabilityRow, {
|
|
471
|
+
t,
|
|
472
|
+
checked: web,
|
|
473
|
+
label: t('web'),
|
|
474
|
+
hint: t('webHint'),
|
|
475
|
+
disabled,
|
|
476
|
+
onToggle: () => { run(web ? 'web off' : 'web on') },
|
|
477
|
+
}),
|
|
478
|
+
h(CapabilityRow, {
|
|
479
|
+
t,
|
|
480
|
+
checked: deep,
|
|
481
|
+
label: t('deep'),
|
|
482
|
+
hint: web ? t('deepHint') : t('deepNeedsWeb'),
|
|
483
|
+
sub: true,
|
|
484
|
+
disabled: disabled || !web,
|
|
485
|
+
onToggle: () => { run(deep ? 'deep off' : 'deep on') },
|
|
486
|
+
}),
|
|
487
|
+
h('div', { className: 'cbp-note cbp-group-note' }, t('searchNote')),
|
|
488
|
+
h('div', { className: 'cbp-sep' }),
|
|
489
|
+
h(UsageBlock, { t, usage }),
|
|
490
|
+
pending || busy ? h('div', { className: 'cbp-note' }, t('pending')) : null,
|
|
491
|
+
error === null ? null : h('div', { className: 'cbp-error', role: 'alert' }, error)),
|
|
492
|
+
document.body)
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return h(React.Fragment, null, capsule, panel)
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return {
|
|
499
|
+
inject: ['slots', 'locale', 'remote', 'remote.commands'],
|
|
500
|
+
|
|
501
|
+
apply(ctx) {
|
|
502
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'chatbot-panel: dictionaries')
|
|
503
|
+
ctx.effect(insertStyles, 'chatbot-panel: styles')
|
|
504
|
+
ctx.slots.inject('conversation.input.left', () => ctx.slots.register({
|
|
505
|
+
name: 'conversation.input.left',
|
|
506
|
+
id: 'chatbot-panel',
|
|
507
|
+
order: 0,
|
|
508
|
+
locale: NS,
|
|
509
|
+
inject: () => ({
|
|
510
|
+
submit: (sessionId, line) => ctx.remote.commands.execute(sessionId, `/chatbot ${line}`, []),
|
|
511
|
+
}),
|
|
512
|
+
}, ChatBotControl))
|
|
513
|
+
},
|
|
514
|
+
}
|
|
515
|
+
},
|
|
516
|
+
})
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Profile patch for the ChatBot capability panel bundle: one Host row whose
|
|
2
|
+
# package also carries the browser half (`dsh.client` in package.json).
|
|
3
|
+
- insert:
|
|
4
|
+
- id: chatbot-panel
|
|
5
|
+
name: '@chentyit/dsh-chatbot-panel'
|
|
6
|
+
config:
|
|
7
|
+
# New sessions start with every capability off; the panel turns them on.
|
|
8
|
+
manageBuiltinWebTools: true
|
|
9
|
+
# Official web-tool price: 5 CNY per 1,000 successful requests.
|
|
10
|
+
pricePerThousand: 5
|
|
11
|
+
# Anonymous HTTP fetch is free; enable only when the fetch provider is
|
|
12
|
+
# the DeepSeek Fetch API.
|
|
13
|
+
countFetchCost: false
|
|
14
|
+
# Deep and image search share this endpoint, model and credential.
|
|
15
|
+
deepSearch:
|
|
16
|
+
baseURL: https://api.deepseek.com/anthropic/v1
|
|
17
|
+
apiKeyEnv: DEEPSEEK_API_KEY
|
|
18
|
+
model: deepseek-flash
|
|
19
|
+
maxTokens: 4096
|
|
20
|
+
timeoutMs: 180000
|
|
21
|
+
tokenPrice:
|
|
22
|
+
# deepseek-flash off-peak prices; peak hours are double.
|
|
23
|
+
inputPerMillion: 1
|
|
24
|
+
outputPerMillion: 4
|
package/icon.svg
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="ChatBot capability panel">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="cbp-bg" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
+
<stop offset="0" stop-color="#101319"/>
|
|
5
|
+
<stop offset="1" stop-color="#2a3342"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<rect x="2" y="2" width="60" height="60" rx="16" fill="url(#cbp-bg)"/>
|
|
9
|
+
<path d="M14 22.5c0-3 2.4-5.5 5.4-5.5h25.2c3 0 5.4 2.5 5.4 5.5v14c0 3-2.4 5.5-5.4 5.5H29.6l-8.4 7.2c-.8.7-2 .1-2-1V42h-.8c-2.4 0-4.4-2-4.4-4.5z" fill="#f7f8fb"/>
|
|
10
|
+
<circle cx="24" cy="29.5" r="2.6" fill="#101319"/>
|
|
11
|
+
<circle cx="32" cy="29.5" r="2.6" fill="#101319"/>
|
|
12
|
+
<circle cx="40" cy="29.5" r="2.6" fill="#101319"/>
|
|
13
|
+
<circle cx="47" cy="47" r="9" fill="#4b8bf5"/>
|
|
14
|
+
<path d="M43 47h8M47 43v8" stroke="#fff" stroke-width="2.2" stroke-linecap="round"/>
|
|
15
|
+
</svg>
|