@chentyit/dsh-chatbot-panel 0.0.0-stage → 0.1.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 +150 -2
- package/client.js +468 -0
- package/cordis.patch.yml +23 -0
- package/icon.svg +15 -0
- package/index.js +651 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +56 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 chentyit
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,151 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ChatBot capability panel
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A **DeepSeek Harness** bundle that turns the composer's web capabilities into an explicit,
|
|
4
|
+
per-session choice, and keeps the usage honest.
|
|
5
|
+
|
|
6
|
+
It adds a small **ChatBot** chip right of the composer's permission chip («Workspace Write»).
|
|
7
|
+
Clicking the chip opens an upward panel; enabling a capability there starts it for the current
|
|
8
|
+
session. While a capability is off, its tools are not "politely denied" — they leave the model's
|
|
9
|
+
tool catalog entirely.
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **Per-session capability switch.** Click the chip to open/close the panel. The chip itself never
|
|
14
|
+
toggles a capability; enabling anything in the panel marks it active (tinted bot icon).
|
|
15
|
+
- **Web search** — exposes the built-in `web_search` / `web_fetch` tools only while enabled.
|
|
16
|
+
- **Deep thinking search** — adds `chatbot_deep_search`, which runs DeepSeek's server-side
|
|
17
|
+
`deepseek_web_search` tool with thinking enabled: the search model plans several queries,
|
|
18
|
+
searches, opens and reads relevant pages over multiple steps, then answers with sources.
|
|
19
|
+
- **Default off.** A fresh session starts with no web tools declared; turning a capability off
|
|
20
|
+
restores that state.
|
|
21
|
+
- **Durable state.** The switch is stored in the session log through the `/chatbot` command
|
|
22
|
+
lifecycle and read back from a session projection, so it survives page reloads, resume and forks.
|
|
23
|
+
- **Usage and estimated cost.** The panel footer shows search calls / queries / sources, fetch
|
|
24
|
+
calls, deep-search calls and tool calls, deep-search tokens and failures, per-category fees and
|
|
25
|
+
the estimated total, with a link to the DeepSeek platform usage page.
|
|
26
|
+
- **Theme-native, borderless.** The chip uses the host theme tokens and matches the permission
|
|
27
|
+
chip's 28 px text-chip look; no border, no fill, hover tints the whole chip.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
Requires DeepSeek Harness `0.2.2-rc.3` or another compatible `0.2.x` runtime (`engines.dsh`).
|
|
32
|
+
|
|
33
|
+
After the package is published to a registry:
|
|
34
|
+
|
|
35
|
+
1. **Settings → Plugins → install** and enter `@chentyit/dsh-chatbot-panel@0.1.0`, or
|
|
36
|
+
2. use the plugin manager tool: `install_bundle` with target `@chentyit/dsh-chatbot-panel@0.1.0`.
|
|
37
|
+
|
|
38
|
+
From a local checkout (development):
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# a local directory is linked into the profile; edits need a bundle toggle or restart
|
|
42
|
+
plugin_manager install_bundle target=/path/to/dsh-chatbot-panel
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The Web page reloads the client half after activation; if the Plugins page asks for a restart,
|
|
46
|
+
restart the app. Uninstall from Settings → Plugins (or remove the bundle).
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
1. Open a session. The **ChatBot** chip sits right of the permission chip.
|
|
51
|
+
2. Click it; the panel opens upward. Tick **Web search** (and optionally **Deep thinking search**).
|
|
52
|
+
3. Ask the model a question that needs current information — it now has the matching tools.
|
|
53
|
+
4. Untick everything, and the tools disappear from the model's catalog again.
|
|
54
|
+
5. The panel footer reports this session's usage and estimated cost.
|
|
55
|
+
|
|
56
|
+
You can also drive the same state with the slash command:
|
|
57
|
+
|
|
58
|
+
```text
|
|
59
|
+
/chatbot web on | web off
|
|
60
|
+
/chatbot deep on | deep off
|
|
61
|
+
/chatbot reset
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Usage and billing
|
|
65
|
+
|
|
66
|
+
The official price list bills web search and page fetch separately from model tokens:
|
|
67
|
+
**CNY 5 per 1,000 successful requests** (≈ ¥0.005 per call; failed or empty results are not
|
|
68
|
+
billed).
|
|
69
|
+
|
|
70
|
+
| Row in the panel | What is counted |
|
|
71
|
+
|---|---|
|
|
72
|
+
| Search | one successful `web_search` tool call fans out one request per query, so a successful call counts `queries.length` billed searches |
|
|
73
|
+
| Fetch | successful `web_fetch` calls; anonymous HTTP fetch is free, so no fee is added unless the fetch provider is the DeepSeek Fetch API (`countFetchCost: true`) |
|
|
74
|
+
| Deep | deep-search tool calls plus the billed `tool_search` / `tool_fetch` / `tool_image_search` counters from the provider and the auxiliary model's input/output tokens |
|
|
75
|
+
| Failed | calls that ended in an error; a failed multi-query search may already have billed some requests |
|
|
76
|
+
|
|
77
|
+
The displayed amount is an **estimate**: the API returns counts, not money, so the panel multiplies
|
|
78
|
+
counts by the configured unit prices. The DeepSeek platform usage page remains authoritative, and
|
|
79
|
+
the panel links to it.
|
|
80
|
+
|
|
81
|
+
## Configuration
|
|
82
|
+
|
|
83
|
+
Override the row's `config` in the profile patch (`~/.dsh/profiles/<profile>/cordis.patch.yml`):
|
|
84
|
+
|
|
85
|
+
```yaml
|
|
86
|
+
- id: chatbot-panel
|
|
87
|
+
name: '@chentyit/dsh-chatbot-panel'
|
|
88
|
+
config:
|
|
89
|
+
manageBuiltinWebTools: true # false = the panel no longer hides/enables built-in web tools
|
|
90
|
+
pricePerThousand: 5 # CNY per 1,000 successful web-tool requests (estimate only)
|
|
91
|
+
countFetchCost: false # true only when the fetch provider is the DeepSeek Fetch API
|
|
92
|
+
deepSearch:
|
|
93
|
+
baseURL: https://api.deepseek.com/anthropic/v1
|
|
94
|
+
apiKeyEnv: DEEPSEEK_API_KEY
|
|
95
|
+
model: deepseek-flash
|
|
96
|
+
maxTokens: 4096
|
|
97
|
+
timeoutMs: 180000
|
|
98
|
+
tokenPrice: # auxiliary deep-search turn, deepseek-flash idle prices
|
|
99
|
+
inputPerMillion: 1
|
|
100
|
+
outputPerMillion: 4
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Deep search authenticates with the signed-in DeepSeek account when the session runs on the
|
|
104
|
+
account route (`x-dsh-auth-token`), otherwise with the API key named by `deepSearch.apiKeyEnv`
|
|
105
|
+
(`$DSH_HOME/.env` or the process environment). Without either it fails with a clear message.
|
|
106
|
+
|
|
107
|
+
## Limitations
|
|
108
|
+
|
|
109
|
+
- Deep search needs a DeepSeek account sign-in or an API key.
|
|
110
|
+
- Toggling a capability changes the request's tool catalog, so that session's DeepSeek prompt
|
|
111
|
+
cache is invalidated once.
|
|
112
|
+
- With plan mode active, the plan chip sits between the permission chip and this chip (official
|
|
113
|
+
single-slot order).
|
|
114
|
+
- Usage is folded from the session log; billing counts from failed multi-query searches are not
|
|
115
|
+
recoverable, so they are reported as failures only.
|
|
116
|
+
- Requires a DeepSeek model route that serves the official Messages API; other providers keep
|
|
117
|
+
their own web tools untouched.
|
|
118
|
+
|
|
119
|
+
## Development
|
|
120
|
+
|
|
121
|
+
The package is plain JavaScript — no build step, no runtime dependencies:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
npm run check # syntax + metadata validation
|
|
125
|
+
npm pack --dry-run # inspect the published file list
|
|
126
|
+
npm publish --access public
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## License
|
|
130
|
+
|
|
131
|
+
MIT. Copyright (c) 2026 chentyit.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## 中文说明
|
|
136
|
+
|
|
137
|
+
**DeepSeek Harness 插件(bundle)**:把 composer 里的联网能力变成按会话的显式开关,并如实记录用量。
|
|
138
|
+
|
|
139
|
+
在输入框「工作区内修改」权限按钮右侧加一个 **ChatBot** 小按钮(无边框、无底色,与权限 chip 同款视觉)。
|
|
140
|
+
点击按钮展开向上弹出的能力面板;在面板里勾选能力,才对本会话生效。**关闭时不是"拒绝调用",而是把对应工具从模型的工具目录里彻底移除。**
|
|
141
|
+
|
|
142
|
+
- **联网搜索**:仅在开启时向模型暴露内置 `web_search` / `web_fetch`。
|
|
143
|
+
- **深度思考搜索**:额外提供 `chatbot_deep_search`,走 DeepSeek 服务端 `deepseek_web_search`(thinking 开启)多步搜索、打开网页后带来源作答。
|
|
144
|
+
- **默认关闭**;开关按会话写入会话日志,刷新、恢复、分叉后仍有效。
|
|
145
|
+
- **用量与费用**:面板底部按官方口径记录搜索次数/查询条数/来源数、抓取次数、深搜次数与工具调用、深搜 token、失败次数、各项费用与估算合计,并链接开放平台用量页。
|
|
146
|
+
|
|
147
|
+
安装:`Settings → Plugins` 输入 `@chentyit/dsh-chatbot-panel@0.1.0`,或用 `plugin_manager` 的 `install_bundle`。
|
|
148
|
+
|
|
149
|
+
计费口径:联网搜索/抓取官方价 **5 元 / 1000 次成功请求**(失败或零结果不计费);一次 `web_search` 按查询词数拆成多次请求,成功计 `queries.length` 次。面板金额是**估算**(API 只返回次数),以平台账单为准。
|
|
150
|
+
|
|
151
|
+
配置项与限制见上方英文段落(`manageBuiltinWebTools` / `pricePerThousand` / `countFetchCost` / `deepSearch` / `tokenPrice`)。
|
package/client.js
ADDED
|
@@ -0,0 +1,468 @@
|
|
|
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
|
+
pending: '应用中…',
|
|
36
|
+
usageTitle: '本会话用量',
|
|
37
|
+
search: '搜索',
|
|
38
|
+
fetch: '抓取',
|
|
39
|
+
deepUsage: '深搜',
|
|
40
|
+
failed: '失败',
|
|
41
|
+
times: '次',
|
|
42
|
+
queries: '条查询',
|
|
43
|
+
sources: '个来源',
|
|
44
|
+
toolCalls: '次工具调用',
|
|
45
|
+
input: '输入',
|
|
46
|
+
output: '输出',
|
|
47
|
+
feeTitle: '费用估算',
|
|
48
|
+
searchFee: '搜索费',
|
|
49
|
+
fetchFee: '抓取费',
|
|
50
|
+
deepToolFee: '深搜工具费',
|
|
51
|
+
deepTokenFee: '深搜 token 费',
|
|
52
|
+
totalFee: '合计',
|
|
53
|
+
fetchFree: '匿名抓取不计费',
|
|
54
|
+
feeNote: '按官方价估算,实际以平台账单为准',
|
|
55
|
+
usageLink: '查看官方用量 ↗',
|
|
56
|
+
errorUnknown: '命令未生效',
|
|
57
|
+
errorPrefix: '操作失败:',
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const en = {
|
|
61
|
+
label: 'ChatBot',
|
|
62
|
+
openPanel: 'Open ChatBot capabilities',
|
|
63
|
+
panelTitle: 'ChatBot capabilities',
|
|
64
|
+
scope: 'this session',
|
|
65
|
+
groupWeb: 'Web tools',
|
|
66
|
+
web: 'Web search',
|
|
67
|
+
webHint: 'Billed per successful query, ¥5 / 1,000',
|
|
68
|
+
deep: 'Deep thinking search',
|
|
69
|
+
deepHint: 'Searches while reasoning; slower and token-heavier',
|
|
70
|
+
deepNeedsWeb: 'Turn on web search first',
|
|
71
|
+
pending: 'Applying…',
|
|
72
|
+
usageTitle: 'Session usage',
|
|
73
|
+
search: 'Search',
|
|
74
|
+
fetch: 'Fetch',
|
|
75
|
+
deepUsage: 'Deep',
|
|
76
|
+
failed: 'Failed',
|
|
77
|
+
times: '',
|
|
78
|
+
queries: 'queries',
|
|
79
|
+
sources: 'sources',
|
|
80
|
+
toolCalls: 'tool calls',
|
|
81
|
+
input: 'in',
|
|
82
|
+
output: 'out',
|
|
83
|
+
feeTitle: 'Estimated cost',
|
|
84
|
+
searchFee: 'Search',
|
|
85
|
+
fetchFee: 'Fetch',
|
|
86
|
+
deepToolFee: 'Deep tools',
|
|
87
|
+
deepTokenFee: 'Deep tokens',
|
|
88
|
+
totalFee: 'Total',
|
|
89
|
+
fetchFree: 'Anonymous fetch is free',
|
|
90
|
+
feeNote: 'Estimated from official prices; the platform bill is authoritative',
|
|
91
|
+
usageLink: 'Open platform usage ↗',
|
|
92
|
+
errorUnknown: 'The command did not apply',
|
|
93
|
+
errorPrefix: 'Failed: ',
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const CSS = [
|
|
97
|
+
/* Chip chrome copied from the permission trigger: transparent, 28px,
|
|
98
|
+
label-secondary, hover-only tint, small glyph and chevron. No border,
|
|
99
|
+
no fill, so it reads as another composer tool next to 「工作区内修改」. */
|
|
100
|
+
'.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}',
|
|
101
|
+
'.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}',
|
|
102
|
+
/* The chip is one control: clicking anywhere on it opens or closes the
|
|
103
|
+
capability panel. Hover tints the whole chip, arrow included. */
|
|
104
|
+
'@media (hover: hover) {',
|
|
105
|
+
'.cbp-capsule:hover{background:var(--dsw-alias-interactive-bg-hover)}',
|
|
106
|
+
'}',
|
|
107
|
+
'.cbp-main:focus-visible{box-shadow:0 0 0 2px var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary))}',
|
|
108
|
+
'.cbp-icon{display:inline-flex;flex:0 0 auto;color:var(--dsw-alias-label-caption);transition:color .12s ease}',
|
|
109
|
+
'.cbp-icon svg{display:block;width:14px;height:14px}',
|
|
110
|
+
'.cbp-capsule[data-on="true"] .cbp-icon{color:var(--dsw-alias-state-success-primary)}',
|
|
111
|
+
'.cbp-capsule[data-on="true"] .cbp-label{color:var(--dsw-alias-label-primary)}',
|
|
112
|
+
'.cbp-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
113
|
+
'.cbp-arrow{display:inline-flex;flex:0 0 auto;margin-left:1px;color:var(--dsw-alias-label-caption)}',
|
|
114
|
+
'.cbp-arrow svg{display:block;width:12px;height:12px;transition:transform 120ms ease}',
|
|
115
|
+
'.cbp-main[data-open="true"] .cbp-arrow svg{transform:rotate(180deg)}',
|
|
116
|
+
'.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)}',
|
|
117
|
+
'.cbp-head{display:flex;align-items:baseline;justify-content:space-between;gap:8px;padding:6px 8px 2px}',
|
|
118
|
+
'.cbp-head-title{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary)}',
|
|
119
|
+
'.cbp-head-scope{font-size:11px;color:var(--dsw-alias-label-caption)}',
|
|
120
|
+
'.cbp-group{padding:8px 8px 2px;font-size:11px;font-weight:600;letter-spacing:.03em;color:var(--dsw-alias-label-tertiary)}',
|
|
121
|
+
'.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}',
|
|
122
|
+
'.cbp-row:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}',
|
|
123
|
+
'.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}',
|
|
124
|
+
'.cbp-row:disabled{cursor:default;opacity:.55}',
|
|
125
|
+
'.cbp-row-sub{padding-left:22px}',
|
|
126
|
+
'.cbp-row-text{display:flex;flex-direction:column;gap:1px;min-width:0;flex:1}',
|
|
127
|
+
'.cbp-row-label{font-size:13px;line-height:18px}',
|
|
128
|
+
'.cbp-row-hint{font-size:11px;line-height:15px;color:var(--dsw-alias-label-secondary)}',
|
|
129
|
+
'.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}',
|
|
130
|
+
'.cbp-check[data-checked="true"]{border-color:transparent;background:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-label-primary-inverted)}',
|
|
131
|
+
'.cbp-check svg{display:block;width:12px;height:12px}',
|
|
132
|
+
'.cbp-sep{height:1px;margin:6px 6px;background:var(--dsw-alias-border-l1)}',
|
|
133
|
+
'.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}',
|
|
134
|
+
'.cbp-usage-title{font-size:11px;font-weight:600;letter-spacing:.03em;color:var(--dsw-alias-label-tertiary);padding:0 0 2px}',
|
|
135
|
+
'.cbp-usage-line{display:flex;justify-content:space-between;gap:10px}',
|
|
136
|
+
'.cbp-usage-line b{font-weight:600;color:var(--dsw-alias-label-primary)}',
|
|
137
|
+
'.cbp-fee{border-top:1px dashed var(--dsw-alias-border-l1);margin-top:4px;padding-top:4px}',
|
|
138
|
+
'.cbp-total{display:flex;justify-content:space-between;gap:10px;font-size:12px;color:var(--dsw-alias-label-primary);font-weight:600}',
|
|
139
|
+
'.cbp-note{font-size:10px;line-height:15px;color:var(--dsw-alias-label-caption)}',
|
|
140
|
+
'.cbp-link{font-size:11px;line-height:17px;color:var(--dsw-alias-link);text-decoration:none}',
|
|
141
|
+
'.cbp-link:hover{text-decoration:underline}',
|
|
142
|
+
'.cbp-error{margin:4px 6px 2px;padding:5px 8px;border-radius:var(--dsw-radius-sm,6px);background:var(--dsw-alias-state-error-tertiary,rgba(220,38,38,.12));color:var(--dsw-alias-state-error-primary,#dc2626);font-size:11px;line-height:15px}',
|
|
143
|
+
].join('')
|
|
144
|
+
|
|
145
|
+
/** The capsule owns its stylesheet, removed with the plugin's activation. */
|
|
146
|
+
function insertStyles() {
|
|
147
|
+
if (typeof document === 'undefined') return () => {}
|
|
148
|
+
const tag = document.createElement('style')
|
|
149
|
+
tag.setAttribute('data-plugin', '@chentyit/dsh-chatbot-panel')
|
|
150
|
+
tag.textContent = CSS
|
|
151
|
+
document.head.appendChild(tag)
|
|
152
|
+
return () => { tag.remove() }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Format a yuan amount for the detail rows. */
|
|
156
|
+
function yuan(value) {
|
|
157
|
+
const amount = typeof value === 'number' && Number.isFinite(value) ? value : 0
|
|
158
|
+
return `¥${amount.toFixed(4)}`
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Format a token count compactly. */
|
|
162
|
+
function tokens(value) {
|
|
163
|
+
const amount = typeof value === 'number' && Number.isFinite(value) ? value : 0
|
|
164
|
+
return amount >= 1000 ? `${(amount / 1000).toFixed(1)}k` : String(amount)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** A small check mark drawn in the theme's inverted label color. */
|
|
168
|
+
function CheckGlyph() {
|
|
169
|
+
return h('svg', { viewBox: '0 0 12 12', 'aria-hidden': true },
|
|
170
|
+
h('path', {
|
|
171
|
+
d: 'M2.2 6.3 4.6 8.7 9.8 3.4',
|
|
172
|
+
fill: 'none',
|
|
173
|
+
stroke: 'currentColor',
|
|
174
|
+
strokeWidth: '1.8',
|
|
175
|
+
strokeLinecap: 'round',
|
|
176
|
+
strokeLinejoin: 'round',
|
|
177
|
+
}))
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Chevron pointing up while the panel is closed, down while it is open. */
|
|
181
|
+
/** A small bot-head mark (antenna, head, eyes) matching the 14px chip glyphs. */
|
|
182
|
+
function BotGlyph() {
|
|
183
|
+
// Optically centered against the 13px label: text ink sits ~0.35px above
|
|
184
|
+
// the flex-line center, so the glyph's ink bounds (outer top 1.17 … outer
|
|
185
|
+
// bottom 12.12, stroke included) center on y≈6.65 instead of 7. Measured
|
|
186
|
+
// from a rendered chip screenshot, this lands the icon level with the text.
|
|
187
|
+
return h('svg', { viewBox: '0 0 14 14', 'aria-hidden': true },
|
|
188
|
+
h('path', {
|
|
189
|
+
d: 'M7 3.67v1.2',
|
|
190
|
+
fill: 'none',
|
|
191
|
+
stroke: 'currentColor',
|
|
192
|
+
strokeWidth: '1.2',
|
|
193
|
+
strokeLinecap: 'round',
|
|
194
|
+
}),
|
|
195
|
+
h('circle', { cx: 7, cy: 2.72, r: 0.95, fill: 'currentColor' }),
|
|
196
|
+
h('rect', {
|
|
197
|
+
x: 2.1,
|
|
198
|
+
y: 4.92,
|
|
199
|
+
width: 9.8,
|
|
200
|
+
height: 6.6,
|
|
201
|
+
rx: 2.2,
|
|
202
|
+
fill: 'none',
|
|
203
|
+
stroke: 'currentColor',
|
|
204
|
+
strokeWidth: '1.2',
|
|
205
|
+
}),
|
|
206
|
+
h('circle', { cx: 5.3, cy: 7.27, r: 0.85, fill: 'currentColor' }),
|
|
207
|
+
h('circle', { cx: 8.7, cy: 7.27, r: 0.85, fill: 'currentColor' }),
|
|
208
|
+
h('path', {
|
|
209
|
+
d: 'M5.6 9.37h2.8',
|
|
210
|
+
fill: 'none',
|
|
211
|
+
stroke: 'currentColor',
|
|
212
|
+
strokeWidth: '1.1',
|
|
213
|
+
strokeLinecap: 'round',
|
|
214
|
+
}))
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Chevron pointing down; the open state rotates it in CSS. */
|
|
218
|
+
function ChevronGlyph() {
|
|
219
|
+
return h('svg', { viewBox: '0 0 12 12', 'aria-hidden': true },
|
|
220
|
+
h('path', {
|
|
221
|
+
d: 'M2.4 4.4 6 8 9.6 4.4',
|
|
222
|
+
fill: 'none',
|
|
223
|
+
stroke: 'currentColor',
|
|
224
|
+
strokeWidth: '1.6',
|
|
225
|
+
strokeLinecap: 'round',
|
|
226
|
+
strokeLinejoin: 'round',
|
|
227
|
+
}))
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** One multi-select capability row. */
|
|
231
|
+
function CapabilityRow(props) {
|
|
232
|
+
const { t, checked, label, hint, sub, disabled, onToggle } = props
|
|
233
|
+
return h('button', {
|
|
234
|
+
type: 'button',
|
|
235
|
+
role: 'menuitemcheckbox',
|
|
236
|
+
'aria-checked': checked ? 'true' : 'false',
|
|
237
|
+
disabled,
|
|
238
|
+
className: sub === true ? 'cbp-row cbp-row-sub' : 'cbp-row',
|
|
239
|
+
onClick: onToggle,
|
|
240
|
+
},
|
|
241
|
+
h('span', { className: 'cbp-row-text' },
|
|
242
|
+
h('span', { className: 'cbp-row-label' }, label),
|
|
243
|
+
typeof hint === 'string' && hint.length > 0 ? h('span', { className: 'cbp-row-hint' }, hint) : null),
|
|
244
|
+
h('span', { className: 'cbp-check', 'data-checked': checked ? 'true' : 'false', 'aria-hidden': true },
|
|
245
|
+
checked ? h(CheckGlyph) : null))
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Detailed usage and estimate block. */
|
|
249
|
+
function UsageBlock(props) {
|
|
250
|
+
const { t, usage } = props
|
|
251
|
+
if (usage === undefined || usage === null) {
|
|
252
|
+
return h('div', { className: 'cbp-usage' },
|
|
253
|
+
h('div', { className: 'cbp-usage-title' }, t('usageTitle')),
|
|
254
|
+
h('div', { className: 'cbp-usage-line' }, h('span', null, t('search')), h('b', null, `0 ${t('times')}`)))
|
|
255
|
+
}
|
|
256
|
+
const search = usage.search || { calls: 0, queries: 0, sources: 0, failed: 0, failedQueries: 0 }
|
|
257
|
+
const fetch = usage.fetch || { calls: 0, failed: 0 }
|
|
258
|
+
const deep = usage.deep || { calls: 0, failed: 0, toolSearch: 0, toolFetch: 0, imageSearch: 0, inputTokens: 0, outputTokens: 0, sources: 0 }
|
|
259
|
+
const fees = usage.fees || { search: 0, fetch: 0, deepTools: 0, deepTokens: 0, total: 0 }
|
|
260
|
+
const deepTools = (deep.toolSearch || 0) + (deep.toolFetch || 0) + (deep.imageSearch || 0)
|
|
261
|
+
const failures = (search.failed || 0) + (fetch.failed || 0) + (deep.failed || 0)
|
|
262
|
+
const lines = []
|
|
263
|
+
lines.push(h('div', { key: 'title', className: 'cbp-usage-title' }, t('usageTitle')))
|
|
264
|
+
lines.push(h('div', { key: 'search', className: 'cbp-usage-line' },
|
|
265
|
+
h('span', null, t('search')),
|
|
266
|
+
h('span', null,
|
|
267
|
+
h('b', null, `${search.calls} ${t('times')}`),
|
|
268
|
+
` · ${search.queries} ${t('queries')} · ${search.sources} ${t('sources')}`)))
|
|
269
|
+
lines.push(h('div', { key: 'fetch', className: 'cbp-usage-line' },
|
|
270
|
+
h('span', null, t('fetch')),
|
|
271
|
+
h('span', null,
|
|
272
|
+
h('b', null, `${fetch.calls} ${t('times')}`),
|
|
273
|
+
usage.countFetchCost === true ? '' : ` · ${t('fetchFree')}`)))
|
|
274
|
+
lines.push(h('div', { key: 'deep', className: 'cbp-usage-line' },
|
|
275
|
+
h('span', null, t('deepUsage')),
|
|
276
|
+
h('span', null,
|
|
277
|
+
h('b', null, `${deep.calls} ${t('times')}`),
|
|
278
|
+
` · ${deepTools} ${t('toolCalls')} · ${tokens(deep.inputTokens)}/${tokens(deep.outputTokens)} tok`)))
|
|
279
|
+
if (failures > 0) {
|
|
280
|
+
lines.push(h('div', { key: 'failed', className: 'cbp-usage-line' },
|
|
281
|
+
h('span', null, t('failed')),
|
|
282
|
+
h('span', null, `${failures} ${t('times')}`)))
|
|
283
|
+
}
|
|
284
|
+
lines.push(h('div', { key: 'sep', className: 'cbp-sep' }))
|
|
285
|
+
lines.push(h('div', { key: 'fee-title', className: 'cbp-usage-title' }, t('feeTitle')))
|
|
286
|
+
lines.push(h('div', { key: 'fee-search', className: 'cbp-usage-line' },
|
|
287
|
+
h('span', null, t('searchFee')), h('span', null, yuan(fees.search))))
|
|
288
|
+
lines.push(h('div', { key: 'fee-fetch', className: 'cbp-usage-line' },
|
|
289
|
+
h('span', null, t('fetchFee')), h('span', null, yuan(fees.fetch))))
|
|
290
|
+
lines.push(h('div', { key: 'fee-deep-tools', className: 'cbp-usage-line' },
|
|
291
|
+
h('span', null, t('deepToolFee')), h('span', null, yuan(fees.deepTools))))
|
|
292
|
+
lines.push(h('div', { key: 'fee-deep-tokens', className: 'cbp-usage-line' },
|
|
293
|
+
h('span', null, t('deepTokenFee')), h('span', null, yuan(fees.deepTokens))))
|
|
294
|
+
lines.push(h('div', { key: 'total', className: 'cbp-total' },
|
|
295
|
+
h('span', null, t('totalFee')), h('span', null, `≈ ${yuan(fees.total)}`)))
|
|
296
|
+
lines.push(h('div', { key: 'note', className: 'cbp-note' }, t('feeNote')))
|
|
297
|
+
lines.push(h('a', {
|
|
298
|
+
key: 'link',
|
|
299
|
+
className: 'cbp-link',
|
|
300
|
+
href: typeof usage.usageUrl === 'string' && usage.usageUrl.length > 0 ? usage.usageUrl : USAGE_URL,
|
|
301
|
+
target: '_blank',
|
|
302
|
+
rel: 'noopener noreferrer',
|
|
303
|
+
}, t('usageLink')))
|
|
304
|
+
return h('div', { className: 'cbp-usage' }, ...lines)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** The composer control: split capsule plus the upward panel. */
|
|
308
|
+
function ChatBotControl(props) {
|
|
309
|
+
const t = typeof props.t === 'function' ? props.t : key => key
|
|
310
|
+
const caps = props.useProjection('chatbotCapabilities')
|
|
311
|
+
const usage = props.useProjection('chatbotUsage')
|
|
312
|
+
const web = caps !== undefined && caps !== null && caps.web === true
|
|
313
|
+
const deep = caps !== undefined && caps !== null && caps.deep === true
|
|
314
|
+
const pending = caps !== undefined && caps !== null && caps.pending === true
|
|
315
|
+
const [open, setOpen] = React.useState(false)
|
|
316
|
+
const [busy, setBusy] = React.useState(false)
|
|
317
|
+
const [error, setError] = React.useState(null)
|
|
318
|
+
const [position, setPosition] = React.useState(null)
|
|
319
|
+
const wrapRef = React.useRef(null)
|
|
320
|
+
const panelRef = React.useRef(null)
|
|
321
|
+
|
|
322
|
+
const submit = typeof props.submit === 'function' ? props.submit : async () => undefined
|
|
323
|
+
const sessionId = props.sessionId
|
|
324
|
+
const run = React.useCallback((line) => {
|
|
325
|
+
setBusy(true)
|
|
326
|
+
setError(null)
|
|
327
|
+
Promise.resolve(submit(sessionId, line))
|
|
328
|
+
.then((result) => {
|
|
329
|
+
if (result === undefined || result === null) { setError(t('errorUnknown')); return }
|
|
330
|
+
if (result.ok !== true) {
|
|
331
|
+
const message = result.error && typeof result.error.message === 'string' ? result.error.message : t('errorUnknown')
|
|
332
|
+
setError(message)
|
|
333
|
+
return
|
|
334
|
+
}
|
|
335
|
+
const value = result.value
|
|
336
|
+
if (value === undefined) { setError(t('errorUnknown')); return }
|
|
337
|
+
if (value !== null && typeof value === 'object' && value.kind === 'error') {
|
|
338
|
+
setError(typeof value.text === 'string' ? value.text : t('errorUnknown'))
|
|
339
|
+
}
|
|
340
|
+
})
|
|
341
|
+
.catch((failure) => { setError(`${t('errorPrefix')}${String(failure)}`) })
|
|
342
|
+
.finally(() => { setBusy(false) })
|
|
343
|
+
}, [submit, sessionId, t])
|
|
344
|
+
|
|
345
|
+
const place = React.useCallback(() => {
|
|
346
|
+
const element = wrapRef.current
|
|
347
|
+
if (element === null || typeof window === 'undefined') return
|
|
348
|
+
const rect = element.getBoundingClientRect()
|
|
349
|
+
const left = Math.min(Math.max(8, rect.left), Math.max(8, window.innerWidth - PANEL_WIDTH - 8))
|
|
350
|
+
const bottom = Math.max(8, window.innerHeight - rect.top + 8)
|
|
351
|
+
setPosition({ left, bottom })
|
|
352
|
+
}, [])
|
|
353
|
+
|
|
354
|
+
React.useLayoutEffect(() => {
|
|
355
|
+
if (!open) return undefined
|
|
356
|
+
place()
|
|
357
|
+
const onResize = () => { place() }
|
|
358
|
+
window.addEventListener('resize', onResize)
|
|
359
|
+
return () => { window.removeEventListener('resize', onResize) }
|
|
360
|
+
}, [open, place])
|
|
361
|
+
|
|
362
|
+
React.useEffect(() => {
|
|
363
|
+
if (!open) return undefined
|
|
364
|
+
const onPointerDown = (event) => {
|
|
365
|
+
if (wrapRef.current !== null && wrapRef.current.contains(event.target)) return
|
|
366
|
+
if (panelRef.current !== null && panelRef.current.contains(event.target)) return
|
|
367
|
+
setOpen(false)
|
|
368
|
+
}
|
|
369
|
+
const onKeyDown = (event) => {
|
|
370
|
+
if (event.key !== 'Escape') return
|
|
371
|
+
setOpen(false)
|
|
372
|
+
const trigger = wrapRef.current === null ? null : wrapRef.current.querySelector('button')
|
|
373
|
+
if (trigger !== null && typeof trigger.focus === 'function') trigger.focus()
|
|
374
|
+
}
|
|
375
|
+
document.addEventListener('pointerdown', onPointerDown, true)
|
|
376
|
+
document.addEventListener('keydown', onKeyDown, true)
|
|
377
|
+
return () => {
|
|
378
|
+
document.removeEventListener('pointerdown', onPointerDown, true)
|
|
379
|
+
document.removeEventListener('keydown', onKeyDown, true)
|
|
380
|
+
}
|
|
381
|
+
}, [open])
|
|
382
|
+
|
|
383
|
+
const disabled = busy || pending
|
|
384
|
+
|
|
385
|
+
const active = web || deep
|
|
386
|
+
|
|
387
|
+
const capsule = h('span', {
|
|
388
|
+
ref: wrapRef,
|
|
389
|
+
className: 'cbp-capsule',
|
|
390
|
+
'data-on': active ? 'true' : 'false',
|
|
391
|
+
},
|
|
392
|
+
h('button', {
|
|
393
|
+
type: 'button',
|
|
394
|
+
className: 'cbp-main',
|
|
395
|
+
'data-open': open ? 'true' : 'false',
|
|
396
|
+
'aria-haspopup': 'menu',
|
|
397
|
+
'aria-expanded': open ? 'true' : 'false',
|
|
398
|
+
'aria-label': t('openPanel'),
|
|
399
|
+
title: t('openPanel'),
|
|
400
|
+
onClick: () => { setOpen(value => !value) },
|
|
401
|
+
},
|
|
402
|
+
h('span', { className: 'cbp-icon', 'aria-hidden': true }, h(BotGlyph)),
|
|
403
|
+
h('span', { className: 'cbp-label' }, t('label')),
|
|
404
|
+
h('span', { className: 'cbp-arrow', 'aria-hidden': true }, h(ChevronGlyph))))
|
|
405
|
+
|
|
406
|
+
let panel = null
|
|
407
|
+
if (open && typeof document !== 'undefined') {
|
|
408
|
+
panel = createPortal(h('div', {
|
|
409
|
+
ref: panelRef,
|
|
410
|
+
className: 'cbp-panel',
|
|
411
|
+
role: 'menu',
|
|
412
|
+
'aria-label': t('panelTitle'),
|
|
413
|
+
style: {
|
|
414
|
+
left: position === null ? 0 : `${position.left}px`,
|
|
415
|
+
bottom: position === null ? 12 : `${position.bottom}px`,
|
|
416
|
+
width: `${PANEL_WIDTH}px`,
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
h('div', { className: 'cbp-head' },
|
|
420
|
+
h('span', { className: 'cbp-head-title' }, t('panelTitle')),
|
|
421
|
+
h('span', { className: 'cbp-head-scope' }, t('scope'))),
|
|
422
|
+
h('div', { className: 'cbp-group' }, t('groupWeb')),
|
|
423
|
+
h(CapabilityRow, {
|
|
424
|
+
t,
|
|
425
|
+
checked: web,
|
|
426
|
+
label: t('web'),
|
|
427
|
+
hint: t('webHint'),
|
|
428
|
+
disabled,
|
|
429
|
+
onToggle: () => { run(web ? 'web off' : 'web on') },
|
|
430
|
+
}),
|
|
431
|
+
h(CapabilityRow, {
|
|
432
|
+
t,
|
|
433
|
+
checked: deep,
|
|
434
|
+
label: t('deep'),
|
|
435
|
+
hint: web ? t('deepHint') : t('deepNeedsWeb'),
|
|
436
|
+
sub: true,
|
|
437
|
+
disabled: disabled || !web,
|
|
438
|
+
onToggle: () => { run(deep ? 'deep off' : 'deep on') },
|
|
439
|
+
}),
|
|
440
|
+
h('div', { className: 'cbp-sep' }),
|
|
441
|
+
h(UsageBlock, { t, usage }),
|
|
442
|
+
pending ? h('div', { className: 'cbp-note' }, t('pending')) : null,
|
|
443
|
+
error === null ? null : h('div', { className: 'cbp-error', role: 'alert' }, error)),
|
|
444
|
+
document.body)
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return h(React.Fragment, null, capsule, panel)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return {
|
|
451
|
+
inject: ['slots', 'locale', 'remote', 'remote.commands'],
|
|
452
|
+
|
|
453
|
+
apply(ctx) {
|
|
454
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'chatbot-panel: dictionaries')
|
|
455
|
+
ctx.effect(insertStyles, 'chatbot-panel: styles')
|
|
456
|
+
ctx.slots.inject('conversation.input.left', () => ctx.slots.register({
|
|
457
|
+
name: 'conversation.input.left',
|
|
458
|
+
id: 'chatbot-panel',
|
|
459
|
+
order: 0,
|
|
460
|
+
locale: NS,
|
|
461
|
+
inject: () => ({
|
|
462
|
+
submit: (sessionId, line) => ctx.remote.commands.execute(sessionId, `/chatbot ${line}`, []),
|
|
463
|
+
}),
|
|
464
|
+
}, ChatBotControl))
|
|
465
|
+
},
|
|
466
|
+
}
|
|
467
|
+
},
|
|
468
|
+
})
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
deepSearch:
|
|
15
|
+
baseURL: https://api.deepseek.com/anthropic/v1
|
|
16
|
+
apiKeyEnv: DEEPSEEK_API_KEY
|
|
17
|
+
model: deepseek-flash
|
|
18
|
+
maxTokens: 4096
|
|
19
|
+
timeoutMs: 180000
|
|
20
|
+
tokenPrice:
|
|
21
|
+
# deepseek-flash off-peak prices; peak hours are double.
|
|
22
|
+
inputPerMillion: 1
|
|
23
|
+
outputPerMillion: 4
|