@tianyulinfeng1/dsh-plugin-deepseek-chat 0.0.0-stage → 1.0.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.en.md +138 -0
- package/README.md +156 -2
- package/cordis.patch.yml +6 -0
- package/icon.svg +11 -0
- package/lib/client.js +549 -0
- package/lib/index.js +9 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +60 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 sweet560
|
|
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.en.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# dsh-plugin-deepseek-chat
|
|
2
|
+
|
|
3
|
+
English | [中文](https://github.com/sweet560/dsh-plugin-deepseek-chat/blob/main/README.md)
|
|
4
|
+
|
|
5
|
+
Adds a **DeepSeek Chat** entry to the **DeepSeek Harness (DSH) desktop** sidebar. Clicking it opens `chat.deepseek.com` full-width in the main area and remembers the last conversation.
|
|
6
|
+
|
|
7
|
+
> Repository: [GitHub](https://github.com/sweet560/dsh-plugin-deepseek-chat) | [Gitee mirror](https://gitee.com/wtt520wyl/dsh-plugin-deepseek-chat)
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- Sidebar icon entry, placed between **Plugins** and **Scheduled tasks**.
|
|
14
|
+
- Full-width embedded `https://chat.deepseek.com/` (Electron `<webview>` through the desktop guest-lease bridge `window.dshDesktop.browser`).
|
|
15
|
+
- **Last-conversation memory**: switching panels/sessions or restarting the app reopens the conversation you left, not a new chat.
|
|
16
|
+
- Auth pages never overwrite that memory; a dead conversation falls back to the site root.
|
|
17
|
+
- **Crash containment**: if the embedded page's process dies, is destroyed, or never starts, the panel shows a Retry card — the app window no longer goes blank (fixed in v0.4.0).
|
|
18
|
+
- Inert on the plain Web surface (`dsh web`): the site sends `frame-ancestors 'none'`, so an iframe cannot carry it, and the guest bridge only exists in the desktop shell.
|
|
19
|
+
|
|
20
|
+
## Requirements
|
|
21
|
+
|
|
22
|
+
| Item | Requirement |
|
|
23
|
+
|---|---|
|
|
24
|
+
| DSH | Desktop `0.2.1-rc.2` or newer |
|
|
25
|
+
| Platform | Windows / macOS / Linux desktop shell |
|
|
26
|
+
| git | Only for the GitHub install route (pnpm fetches the repository) |
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
npm package: **`@tianyulinfeng1/dsh-plugin-deepseek-chat`** (the GitHub repository is still named `dsh-plugin-deepseek-chat`).
|
|
31
|
+
|
|
32
|
+
### Option 1 — npm (recommended)
|
|
33
|
+
|
|
34
|
+
```powershell
|
|
35
|
+
dsh plugin --profile desktop add @tianyulinfeng1/dsh-plugin-deepseek-chat
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
You can also enter the package name `@tianyulinfeng1/dsh-plugin-deepseek-chat` in the Plugins page.
|
|
39
|
+
|
|
40
|
+
### Option 2 — Plugins page (GitHub)
|
|
41
|
+
|
|
42
|
+
1. Open DSH desktop and click **Plugins** in the sidebar.
|
|
43
|
+
2. Click **Install a third-party plugin** above the list.
|
|
44
|
+
3. Enter:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
github:sweet560/dsh-plugin-deepseek-chat
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`https://github.com/sweet560/dsh-plugin-deepseek-chat` works too.
|
|
51
|
+
4. Click **Install** and wait for pnpm to finish.
|
|
52
|
+
5. The plugin is enabled automatically; **DeepSeek Chat** appears in the sidebar.
|
|
53
|
+
|
|
54
|
+
### Option 3 — CLI (GitHub)
|
|
55
|
+
|
|
56
|
+
```powershell
|
|
57
|
+
dsh plugin --profile desktop add github:sweet560/dsh-plugin-deepseek-chat
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Then open **Plugins → Installed plugins** and make sure **DeepSeek Chat** is switched on (the CLI only installs the package).
|
|
61
|
+
|
|
62
|
+
### Option 4 — local checkout (development)
|
|
63
|
+
|
|
64
|
+
```powershell
|
|
65
|
+
dsh plugin --profile desktop add file:"<absolute path to this repository>"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Usage
|
|
69
|
+
|
|
70
|
+
- The first visit shows the chat.deepseek.com sign-in page; sign in once.
|
|
71
|
+
- Within one app run, switching panels or sessions keeps you signed in.
|
|
72
|
+
- **Sign in again after restarting DSH**: DSH currently gives embedded browsers an in-memory partition (`dsh-sidebar-browser-<uuid>`, no `persist:` prefix), so cookies are dropped when the process exits. This plugin does not modify DSH's installation files, so it cannot bypass that. To sign in once and stay signed in, run the bundled patch script **[docs/patch-dsh-persistent-partition.cjs](https://github.com/sweet560/dsh-plugin-deepseek-chat/blob/main/docs/patch-dsh-persistent-partition.cjs)** (rc.3 ASAR-integrity aware, supports `--dry-run`, automatic backup and full verification); see **[docs/login-persistence.md](https://github.com/sweet560/dsh-plugin-deepseek-chat/blob/main/docs/login-persistence.md)**.
|
|
73
|
+
- The conversation URL is stored in the app's localStorage (`dsh-plugin-deepseek-chat:last-url`), so the last conversation is restored after a restart (while signed in and while it still exists).
|
|
74
|
+
|
|
75
|
+
## Persistent login (optional)
|
|
76
|
+
|
|
77
|
+
DSH hands embedded browsers an **in-memory partition** (`dsh-sidebar-browser-${randomUUID()}`, no `persist:` prefix), so every app restart drops the cookies. To sign in once and stay signed in, replace that partition with a fixed `persist:` one inside `resources/app.asar` — the two statements are the same length, so the archive can be patched byte-for-byte in place.
|
|
78
|
+
|
|
79
|
+
Cause, script, verification, rollback and per-platform paths:
|
|
80
|
+
|
|
81
|
+
**→ [docs/login-persistence.md](https://github.com/sweet560/dsh-plugin-deepseek-chat/blob/main/docs/login-persistence.md)**
|
|
82
|
+
|
|
83
|
+
Bundled patch script (Node 18+, no dependencies):
|
|
84
|
+
|
|
85
|
+
```powershell
|
|
86
|
+
# Patch (creates a timestamped backup first)
|
|
87
|
+
node docs/patch-dsh-persistent-partition.cjs "F:\DeepSeek Harness\resources\app.asar"
|
|
88
|
+
|
|
89
|
+
# Dry run: check without writing
|
|
90
|
+
node docs/patch-dsh-persistent-partition.cjs "F:\DeepSeek Harness\resources\app.asar" --dry-run
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The script understands the per-file ASAR `integrity` metadata shipped by `0.2.1-rc.3` (it recomputes and syncs `lib/main.js`'s SHA256), is idempotent (already patched returns 0), creates an automatic backup, verifies every block hash before and after writing, and supports `--dry-run`.
|
|
94
|
+
|
|
95
|
+
Note: this modifies the installed application and is outside DSH's support; a DSH update overwrites `app.asar` and the patch must be re-applied (the `Partitions` data survives). Back up first and proceed at your own risk.
|
|
96
|
+
|
|
97
|
+
## Known limitations
|
|
98
|
+
|
|
99
|
+
- chat.deepseek.com detects the Electron embedding and occasionally shows a "usage environment is abnormal" notice. That is site-side behaviour, unrelated to this plugin.
|
|
100
|
+
- Desktop only; `dsh web` in a browser shows no entry.
|
|
101
|
+
- If the site changes its conversation URL scheme (currently `/a/chat/s/<id>`), the memory falls back to the site root.
|
|
102
|
+
|
|
103
|
+
## Crash containment (v0.4.0)
|
|
104
|
+
|
|
105
|
+
Earlier versions could leave the whole app a blank window a second or two after opening the panel, with no crash dialog and no `crash-*.log`.
|
|
106
|
+
|
|
107
|
+
Root cause: React runs effect cleanups after it has removed the DOM, and Electron's `<webview>` throws from `getURL()` once detached. DSH treats any uncaught plugin error as `slot entry crashed in 'root'` and unmounts the root slot — the entire UI.
|
|
108
|
+
|
|
109
|
+
The current implementation guards every guest call behind an `isConnected` check and `try/catch`, reads the URL before removing the element, wraps every event handler, puts a React error boundary around the panel, listens for `render-process-gone` / `destroyed` / `did-fail-load`, and has a 15-second start watchdog.
|
|
110
|
+
|
|
111
|
+
## Development
|
|
112
|
+
|
|
113
|
+
```powershell
|
|
114
|
+
git clone https://github.com/sweet560/dsh-plugin-deepseek-chat.git
|
|
115
|
+
cd dsh-plugin-deepseek-chat
|
|
116
|
+
node test/check-client.mjs
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`test/check-client.mjs` loads `lib/client.js` in a `node:vm` sandbox with fake React/DOM/bridge objects and checks registration, conversation memory, lease release, and crash containment.
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
lib/index.js host half (empty; it only gives the client bundle a Loader entry)
|
|
123
|
+
lib/client.js browser half: entry, panel, webview lifecycle, crash containment
|
|
124
|
+
cordis.patch.yml bundle patch that inserts this plugin's Loader row
|
|
125
|
+
locale/{zh,en}.json Plugins-page title/description
|
|
126
|
+
icon.svg Plugins-page icon
|
|
127
|
+
test/check-client.mjs offline self-check
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Uninstall
|
|
131
|
+
|
|
132
|
+
```powershell
|
|
133
|
+
dsh plugin --profile desktop remove dsh-plugin-deepseek-chat
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## License
|
|
137
|
+
|
|
138
|
+
[MIT](LICENSE)
|
package/README.md
CHANGED
|
@@ -1,3 +1,157 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-plugin-deepseek-chat
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[English](https://github.com/sweet560/dsh-plugin-deepseek-chat/blob/main/README.en.md) | 中文
|
|
4
|
+
|
|
5
|
+
在 **DeepSeek Harness(DSH)桌面版**的左侧栏加入 **DeepSeek 聊天** 入口,点开后 `chat.deepseek.com` 直接占满主内容区,并记住上次所在的会话。
|
|
6
|
+
|
|
7
|
+
> 仓库:[GitHub](https://github.com/sweet560/dsh-plugin-deepseek-chat) | [Gitee 镜像](https://gitee.com/wtt520wyl/dsh-plugin-deepseek-chat)
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
## 功能
|
|
12
|
+
|
|
13
|
+
- 左侧栏图标入口,排在「插件」和「自动化任务」之间。
|
|
14
|
+
- 主内容区全宽内嵌 `https://chat.deepseek.com/`(Electron `<webview>`,走桌面壳的 guest 租约桥 `window.dshDesktop.browser`)。
|
|
15
|
+
- **记住上次会话**:切到别的面板、切换会话、重启 App 后再进来,直接回到上次那条对话,而不是新对话。
|
|
16
|
+
- 登录/鉴权页不会覆盖记忆;记住的会话失效或打不开时自动回退到首页。
|
|
17
|
+
- **崩溃隔离**:内嵌页进程退出、被销毁或启动超时,只在面板里显示「重试」,不会再出现整个 App 变空白窗口的问题(v0.4.0 修复,见下文)。
|
|
18
|
+
- 纯 Web 表层(`dsh web` 在浏览器里打开)自动不注册——该站点 `frame-ancestors 'none'`,iframe 嵌不了,guest 租约桥也只有桌面壳提供。
|
|
19
|
+
|
|
20
|
+
## 环境要求
|
|
21
|
+
|
|
22
|
+
| 项 | 要求 |
|
|
23
|
+
|---|---|
|
|
24
|
+
| DSH | 桌面版 `0.2.1-rc.2` 或更新 |
|
|
25
|
+
| 平台 | Windows / macOS / Linux 桌面壳 |
|
|
26
|
+
| git | 仅「GitHub 安装」方式需要(pnpm 通过 git 拉取仓库) |
|
|
27
|
+
|
|
28
|
+
## 安装
|
|
29
|
+
|
|
30
|
+
npm 包名:**`@tianyulinfeng1/dsh-plugin-deepseek-chat`**(GitHub 仓库名仍是 `dsh-plugin-deepseek-chat`)。
|
|
31
|
+
|
|
32
|
+
### 方式一:npm(推荐)
|
|
33
|
+
|
|
34
|
+
```powershell
|
|
35
|
+
dsh plugin --profile desktop add @tianyulinfeng1/dsh-plugin-deepseek-chat
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
插件页「安装第三方插件」里也可以直接填包名 `@tianyulinfeng1/dsh-plugin-deepseek-chat`。
|
|
39
|
+
|
|
40
|
+
### 方式二:插件页安装(GitHub)
|
|
41
|
+
|
|
42
|
+
1. 打开 DSH 桌面版,左侧栏点 **插件**。
|
|
43
|
+
2. 点列表上方的 **安装第三方插件**。
|
|
44
|
+
3. 在「包名或地址」里填入:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
github:sweet560/dsh-plugin-deepseek-chat
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
也可以填 `https://github.com/sweet560/dsh-plugin-deepseek-chat`。
|
|
51
|
+
4. 点 **安装**,等 pnpm 跑完(首次会拉取仓库并安装依赖)。
|
|
52
|
+
5. 安装完成后插件自动启用,左侧栏出现 **DeepSeek 聊天**。
|
|
53
|
+
|
|
54
|
+
### 方式三:命令行(GitHub)
|
|
55
|
+
|
|
56
|
+
```powershell
|
|
57
|
+
dsh plugin --profile desktop add github:sweet560/dsh-plugin-deepseek-chat
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
装完打开 **插件 → 已安装插件**,确认「DeepSeek 聊天」处于启用状态(命令行只负责装依赖,启用状态以插件页为准)。
|
|
61
|
+
|
|
62
|
+
### 方式四:本地目录(二次开发)
|
|
63
|
+
|
|
64
|
+
```powershell
|
|
65
|
+
dsh plugin --profile desktop add file:"<本仓库绝对路径>"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## 使用
|
|
69
|
+
|
|
70
|
+
- 第一次打开会停在 chat.deepseek.com 的登录页,登录一次即可。
|
|
71
|
+
- 本次 App 运行期间,切换面板/会话不需要重新登录。
|
|
72
|
+
- **重启 DSH 后需要重新登录一次**:DSH 目前给内嵌浏览器的是内存分区(`dsh-sidebar-browser-<uuid>`,没有 `persist:` 前缀),进程退出即清空 cookie。本插件不修改 DSH 的安装文件,所以不会绕过这一限制;想做到「登录一次、长期免登录」,可直接运行仓库自带的补丁脚本 **[docs/patch-dsh-persistent-partition.cjs](https://github.com/sweet560/dsh-plugin-deepseek-chat/blob/main/docs/patch-dsh-persistent-partition.cjs)**(已适配 `0.2.1-rc.3` 的 ASAR integrity,支持 `--dry-run`、自动备份与全量校验),说明见 **[docs/login-persistence.md](https://github.com/sweet560/dsh-plugin-deepseek-chat/blob/main/docs/login-persistence.md)**。
|
|
73
|
+
- 会话链接记在 App 的 localStorage(key `dsh-plugin-deepseek-chat:last-url`),重启后仍会打开上次的会话(前提是已登录且该会话仍存在)。
|
|
74
|
+
|
|
75
|
+
## 登录态持久化(可选,免登录)
|
|
76
|
+
|
|
77
|
+
DSH 目前给内嵌浏览器分配的是**内存分区**(`dsh-sidebar-browser-${randomUUID()}`,无 `persist:` 前缀),所以每次重启 App 都会清空 cookie。想做到「登录一次、以后不再登录」,需要把桌面主进程里的那行分区名改成固定且带 `persist:` 前缀的值——两条语句**字节数相同**,可以直接在 `resources/app.asar` 里原地替换。
|
|
78
|
+
|
|
79
|
+
完整原因、脚本、验证步骤、回滚方式和各平台路径都整理在这里:
|
|
80
|
+
|
|
81
|
+
**→ [docs/login-persistence.md](https://github.com/sweet560/dsh-plugin-deepseek-chat/blob/main/docs/login-persistence.md)**
|
|
82
|
+
|
|
83
|
+
补丁脚本(仓库自带,Node 18+,零依赖):
|
|
84
|
+
|
|
85
|
+
```powershell
|
|
86
|
+
# 打补丁(自动备份)
|
|
87
|
+
node docs/patch-dsh-persistent-partition.cjs "F:\DeepSeek Harness\resources\app.asar"
|
|
88
|
+
|
|
89
|
+
# 只检查、不写盘
|
|
90
|
+
node docs/patch-dsh-persistent-partition.cjs "F:\DeepSeek Harness\resources\app.asar" --dry-run
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
脚本已适配 `0.2.1-rc.3` 的逐文件 ASAR `integrity`(自动重算并同步 `lib/main.js` 的 SHA256),支持幂等重跑(已打补丁直接返回 0)、自动备份、写入前后全量校验,以及 `--dry-run` 预演。
|
|
94
|
+
|
|
95
|
+
注意:这属于对已安装应用的修改,不在 DSH 支持范围内;DSH 升级会覆盖 `app.asar`,需要重新打补丁(`Partitions` 里的登录数据不会丢)。请先备份,风险自负。
|
|
96
|
+
|
|
97
|
+
## 已知限制
|
|
98
|
+
|
|
99
|
+
- chat.deepseek.com 会检测 Electron 内嵌环境,偶尔弹出「使用环境异常…建议使用官方产品」的提示;这是站点侧行为,与本插件无关。
|
|
100
|
+
- 仅桌面版有效;`dsh web` 在浏览器里打开时不显示入口。
|
|
101
|
+
- 站点若调整会话 URL 规则(当前识别 `/a/chat/s/<id>` 形式),「记住会话」可能失效并回退到首页。
|
|
102
|
+
|
|
103
|
+
## 崩溃隔离(v0.4.0)
|
|
104
|
+
|
|
105
|
+
早期版本出现过:点开面板、聊天页正常显示 1~2 秒后**整个 App 变成空白窗口**(只剩原生菜单和窗口按钮),没有崩溃对话框也没有 `crash-*.log`。
|
|
106
|
+
|
|
107
|
+
根因是 React 卸载面板时**先移除 DOM、后执行 effect 清理**,而 Electron 的 `<webview>` 一旦脱离文档,`getURL()` 就会抛错;DSH 把插件里任何未捕获异常都当作 `slot entry crashed in 'root'`,直接卸载 `root` slot —— 整个 App UI 因此消失。
|
|
108
|
+
|
|
109
|
+
现在的实现:
|
|
110
|
+
|
|
111
|
+
- 所有 guest 调用先判断 `element.isConnected` 再 `try/catch`,清理阶段「先读 URL、再移除元素」;
|
|
112
|
+
- 每个事件处理器都包 `try/catch`,异常只落在面板自己的状态里;
|
|
113
|
+
- 面板外包一层 React 错误边界;
|
|
114
|
+
- 监听 `render-process-gone` / `destroyed` / `did-fail-load`,guest 出问题时面板内显示「重试」;
|
|
115
|
+
- 15 秒启动看门狗,guest 一直不 `dom-ready` 就转重试卡片。
|
|
116
|
+
|
|
117
|
+
## 开发
|
|
118
|
+
|
|
119
|
+
```powershell
|
|
120
|
+
git clone https://github.com/sweet560/dsh-plugin-deepseek-chat.git
|
|
121
|
+
cd dsh-plugin-deepseek-chat
|
|
122
|
+
node test/check-client.mjs
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`test/check-client.mjs` 是离线自检:在 `node:vm` 里加载 `lib/client.js`,用假的 React/DOM/租约桥验证注册、会话记忆、租约释放和崩溃隔离(DOM 先移除再清理不抛错、guest 崩溃不逃逸、反复开关不漏租约)。
|
|
126
|
+
|
|
127
|
+
目录:
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
lib/index.js 宿主半侧(空实现,只为让 client bundle 有 Loader 入口)
|
|
131
|
+
lib/client.js 浏览器半侧:入口、面板、webview 生命周期、崩溃隔离
|
|
132
|
+
cordis.patch.yml bundle 补丁:插入本插件的 Loader 行
|
|
133
|
+
locale/zh.json 插件页显示名称/描述(中文)
|
|
134
|
+
locale/en.json 插件页显示名称/描述(英文)
|
|
135
|
+
icon.svg 插件页图标
|
|
136
|
+
test/check-client.mjs 离线自检
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## 卸载
|
|
140
|
+
|
|
141
|
+
```powershell
|
|
142
|
+
dsh plugin --profile desktop remove dsh-plugin-deepseek-chat
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## 作者
|
|
146
|
+
|
|
147
|
+
- **作者**:伍悦匿匿
|
|
148
|
+
- **联系方式**:437830488@qq.com
|
|
149
|
+
- **GitHub**:https://github.com/sweet560
|
|
150
|
+
- **博客园**:https://www.cnblogs.com/wuyuenini/
|
|
151
|
+
- **CSDN**:https://blog.csdn.net/weixin_42076389
|
|
152
|
+
- **Gitee**:https://gitee.com/wtt520wyl
|
|
153
|
+
- **Gitee 镜像仓库**:https://gitee.com/wtt520wyl/dsh-plugin-deepseek-chat
|
|
154
|
+
|
|
155
|
+
## 许可证
|
|
156
|
+
|
|
157
|
+
[MIT](LICENSE)
|
package/cordis.patch.yml
ADDED
package/icon.svg
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M8.25 10.5c0-1.52 1.23-2.75 2.75-2.75h14c1.52 0 2.75 1.23 2.75 2.75v10.5c0 1.52-1.23 2.75-2.75 2.75h-9.1l-4.55 3.8c-.78.65-1.95.09-1.95-.93V10.5Z" stroke="url(#chat-bubble)" stroke-width="2.4" stroke-linejoin="round"/>
|
|
3
|
+
<path d="M13.4 13.6h9.2M13.4 18h5.6" stroke="#658EFF" stroke-width="2.2" stroke-linecap="round"/>
|
|
4
|
+
<defs>
|
|
5
|
+
<linearGradient id="chat-bubble" x1="7" y1="8" x2="29" y2="28" gradientUnits="userSpaceOnUse">
|
|
6
|
+
<stop stop-color="#41E1AC"/>
|
|
7
|
+
<stop offset="0.65" stop-color="#658EFF"/>
|
|
8
|
+
<stop offset="1" stop-color="#41E1AC"/>
|
|
9
|
+
</linearGradient>
|
|
10
|
+
</defs>
|
|
11
|
+
</svg>
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser half of dsh-plugin-deepseek-chat.
|
|
3
|
+
*
|
|
4
|
+
* Registers one global left-sidebar panel — the same mechanism the Plugins and
|
|
5
|
+
* Scheduled tasks entries use:
|
|
6
|
+
* `sidebar.panellist` (list) contributes the icon row, and the root `main`
|
|
7
|
+
* keyed slot with the same key renders the panel page.
|
|
8
|
+
*
|
|
9
|
+
* The page hosts chat.deepseek.com in an Electron `<webview>`. The site sends
|
|
10
|
+
* `content-security-policy: frame-ancestors 'none'`, so an iframe cannot carry
|
|
11
|
+
* it; the desktop shell exposes `globalThis.dshDesktop.browser`, the same guest
|
|
12
|
+
* lease bridge the built-in sidebar browser uses: acquire a lease → create a
|
|
13
|
+
* webview named by the lease with its approved partition → load the URL.
|
|
14
|
+
*
|
|
15
|
+
* Failure containment is the hard rule of this file. DSH treats any uncaught
|
|
16
|
+
* error thrown by a plugin as a crash of its slot entry, and a crash inside the
|
|
17
|
+
* `root` slot unmounts the entire application UI (the native frame stays, every
|
|
18
|
+
* panel disappears). Electron throws from webview methods such as `getURL()`
|
|
19
|
+
* and `loadURL()` as soon as the element leaves the document, and React runs
|
|
20
|
+
* effect cleanups after the DOM removal — so every guest call here is attached-
|
|
21
|
+
* checked and wrapped, every event handler catches, and a React error boundary
|
|
22
|
+
* keeps a render failure inside this panel. A dead guest is reported in the
|
|
23
|
+
* panel with a Retry button; it never escapes to the shell.
|
|
24
|
+
*
|
|
25
|
+
* Leaving the panel unmounts this page and releases the guest, so the page
|
|
26
|
+
* remembers the last conversation URL and reopens there instead of starting a
|
|
27
|
+
* new chat every time.
|
|
28
|
+
*/
|
|
29
|
+
window.__ModuleLoader__.load({
|
|
30
|
+
id: "dsh-plugin-deepseek-chat",
|
|
31
|
+
factory: (require) => {
|
|
32
|
+
var module = { exports: {} };
|
|
33
|
+
var exports = module.exports;
|
|
34
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
35
|
+
const react = require("react");
|
|
36
|
+
|
|
37
|
+
const PANEL_ID = "deepseek-chat";
|
|
38
|
+
const CHAT_URL = "https://chat.deepseek.com/";
|
|
39
|
+
/**
|
|
40
|
+
* Storage identity for the guest partition. The main process keeps one
|
|
41
|
+
* partition per identity for the lifetime of the process, so switching
|
|
42
|
+
* panels or sessions keeps the page state and its login.
|
|
43
|
+
*/
|
|
44
|
+
const PARTITION_KEY = "panel:deepseek-chat";
|
|
45
|
+
|
|
46
|
+
const english = typeof navigator !== "undefined" && /^en\b/i.test(navigator.language ?? "");
|
|
47
|
+
const COPY = english ? {
|
|
48
|
+
label: "DeepSeek Chat",
|
|
49
|
+
loading: "Loading DeepSeek Chat…",
|
|
50
|
+
unsupported: "The embedded chat needs the desktop app.",
|
|
51
|
+
failed: "The chat page could not be opened",
|
|
52
|
+
crashed: "The embedded chat stopped responding",
|
|
53
|
+
timeout: "The embedded chat did not start",
|
|
54
|
+
retry: "Retry"
|
|
55
|
+
} : {
|
|
56
|
+
label: "DeepSeek 聊天",
|
|
57
|
+
loading: "正在加载 DeepSeek 聊天…",
|
|
58
|
+
unsupported: "内嵌聊天窗口需要桌面版 App。",
|
|
59
|
+
failed: "聊天页面打开失败",
|
|
60
|
+
crashed: "内嵌聊天进程已退出",
|
|
61
|
+
timeout: "内嵌聊天页面没有启动",
|
|
62
|
+
retry: "重试"
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/** Where the last visited conversation URL is remembered; the shell keeps this storage across restarts. */
|
|
66
|
+
const STORAGE_KEY = "dsh-plugin-deepseek-chat:last-url";
|
|
67
|
+
/** Paths that are not a conversation: a brand-new chat, or an auth page. */
|
|
68
|
+
const NON_CONVERSATION_PATHS = new Set(["/", "/sign_in", "/login", "/signup", "/auth"]);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Parse a chat.deepseek.com page URL.
|
|
72
|
+
* @param value - candidate URL.
|
|
73
|
+
* @returns the parsed URL for the site origin, or undefined.
|
|
74
|
+
*/
|
|
75
|
+
function parseChatUrl(value) {
|
|
76
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 4096) return void 0;
|
|
77
|
+
try {
|
|
78
|
+
const url = new URL(value);
|
|
79
|
+
if (url.protocol !== "https:" || url.hostname !== "chat.deepseek.com") return void 0;
|
|
80
|
+
if (url.username !== "" || url.password !== "") return void 0;
|
|
81
|
+
return url;
|
|
82
|
+
} catch {
|
|
83
|
+
return void 0;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Whether a URL deep enough to be a conversation page.
|
|
89
|
+
* @param url - parsed chat URL, possibly undefined.
|
|
90
|
+
* @returns true for a conversation, false for the new-chat root or an auth page.
|
|
91
|
+
*/
|
|
92
|
+
function isConversation(url) {
|
|
93
|
+
if (url === void 0) return false;
|
|
94
|
+
const path = url.pathname.replace(/\/+$/u, "") || "/";
|
|
95
|
+
return !NON_CONVERSATION_PATHS.has(path);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Read the conversation remembered from the last visit.
|
|
100
|
+
* @returns the URL to open; the site root when nothing usable is remembered.
|
|
101
|
+
*/
|
|
102
|
+
function rememberedTarget() {
|
|
103
|
+
try {
|
|
104
|
+
const saved = parseChatUrl(window.localStorage.getItem(STORAGE_KEY));
|
|
105
|
+
return isConversation(saved) ? saved.toString() : CHAT_URL;
|
|
106
|
+
} catch {
|
|
107
|
+
return CHAT_URL;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Remember where the guest currently is.
|
|
113
|
+
*
|
|
114
|
+
* A brand-new chat clears the memory so the next visit starts clean. An auth
|
|
115
|
+
* page never overwrites it, so a login detour does not lose the conversation.
|
|
116
|
+
* @param raw - current page URL reported by the guest.
|
|
117
|
+
*/
|
|
118
|
+
function rememberTarget(raw) {
|
|
119
|
+
const url = parseChatUrl(raw);
|
|
120
|
+
if (url === void 0) return;
|
|
121
|
+
try {
|
|
122
|
+
const path = url.pathname.replace(/\/+$/u, "") || "/";
|
|
123
|
+
if (isConversation(url)) window.localStorage.setItem(STORAGE_KEY, url.toString());
|
|
124
|
+
else if (path === "/") window.localStorage.removeItem(STORAGE_KEY);
|
|
125
|
+
} catch {
|
|
126
|
+
/* storage unavailable: the panel still works, it just forgets */
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Whether the guest element is still in the document. Electron's webview
|
|
132
|
+
* methods throw once it is detached, so this gates every guest call.
|
|
133
|
+
* @param element - webview element.
|
|
134
|
+
* @returns true while the element can still host a guest.
|
|
135
|
+
*/
|
|
136
|
+
function attached(element) {
|
|
137
|
+
try {
|
|
138
|
+
return element.isConnected === true;
|
|
139
|
+
} catch {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Guest URL, or undefined when the element cannot report one. This is the
|
|
146
|
+
* call that used to escape: React cleans up effects after removing the DOM,
|
|
147
|
+
* and Electron then throws from `getURL()`.
|
|
148
|
+
* @param element - webview element.
|
|
149
|
+
* @returns the URL string, or undefined.
|
|
150
|
+
*/
|
|
151
|
+
function readUrl(element) {
|
|
152
|
+
if (!attached(element)) return void 0;
|
|
153
|
+
try {
|
|
154
|
+
const url = element.getURL();
|
|
155
|
+
return typeof url === "string" && url.length > 0 ? url : void 0;
|
|
156
|
+
} catch {
|
|
157
|
+
return void 0;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Navigate the guest without ever throwing.
|
|
163
|
+
* @param element - webview element.
|
|
164
|
+
* @param url - target URL.
|
|
165
|
+
* @returns false when the guest is gone, so the caller can report it.
|
|
166
|
+
*/
|
|
167
|
+
function navigate(element, url) {
|
|
168
|
+
if (!attached(element)) return false;
|
|
169
|
+
try {
|
|
170
|
+
const pending = element.loadURL(url);
|
|
171
|
+
if (pending !== void 0 && pending !== null && typeof pending.catch === "function") pending.catch(() => {});
|
|
172
|
+
return true;
|
|
173
|
+
} catch {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Panel-list glyph, drawn on currentColor so the sidebar's active state owns
|
|
180
|
+
* the ink. Matches the sidebar's 16/18px icon seat.
|
|
181
|
+
* @param props - size passed by the sidebar panel list.
|
|
182
|
+
* @returns the glyph element.
|
|
183
|
+
*/
|
|
184
|
+
function ChatGlyph({ size = 18 }) {
|
|
185
|
+
return react.createElement("svg", {
|
|
186
|
+
width: size,
|
|
187
|
+
height: size,
|
|
188
|
+
viewBox: "0 0 24 24",
|
|
189
|
+
fill: "none",
|
|
190
|
+
"aria-hidden": "true",
|
|
191
|
+
focusable: "false"
|
|
192
|
+
}, react.createElement("path", {
|
|
193
|
+
d: "M4.5 5.75C4.5 4.784 5.284 4 6.25 4h11.5c.966 0 1.75.784 1.75 1.75v8.5c0 .966-.784 1.75-1.75 1.75H9.56l-3.34 2.784c-.497.414-1.22.06-1.22-.586V5.75Z",
|
|
194
|
+
stroke: "currentColor",
|
|
195
|
+
strokeWidth: 1.3,
|
|
196
|
+
strokeLinejoin: "round"
|
|
197
|
+
}), react.createElement("path", {
|
|
198
|
+
d: "M8.5 9.25h7M8.5 12.25h4.5",
|
|
199
|
+
stroke: "currentColor",
|
|
200
|
+
strokeWidth: 1.3,
|
|
201
|
+
strokeLinecap: "round"
|
|
202
|
+
}));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const styles = {
|
|
206
|
+
root: {
|
|
207
|
+
display: "flex",
|
|
208
|
+
flexDirection: "column",
|
|
209
|
+
height: "100%",
|
|
210
|
+
minHeight: "0",
|
|
211
|
+
background: "var(--dsw-alias-bg-base)",
|
|
212
|
+
color: "var(--dsw-alias-label-primary)"
|
|
213
|
+
},
|
|
214
|
+
host: {
|
|
215
|
+
position: "relative",
|
|
216
|
+
flex: "1",
|
|
217
|
+
minHeight: "0",
|
|
218
|
+
display: "flex"
|
|
219
|
+
},
|
|
220
|
+
guest: {
|
|
221
|
+
width: "100%",
|
|
222
|
+
height: "100%",
|
|
223
|
+
border: "0",
|
|
224
|
+
background: "transparent",
|
|
225
|
+
display: "flex"
|
|
226
|
+
},
|
|
227
|
+
notice: {
|
|
228
|
+
position: "absolute",
|
|
229
|
+
inset: "0",
|
|
230
|
+
display: "flex",
|
|
231
|
+
flexDirection: "column",
|
|
232
|
+
alignItems: "center",
|
|
233
|
+
justifyContent: "center",
|
|
234
|
+
gap: "10px",
|
|
235
|
+
padding: "24px",
|
|
236
|
+
textAlign: "center",
|
|
237
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
238
|
+
font: "var(--dsw-font-xxs-12)",
|
|
239
|
+
background: "var(--dsw-alias-bg-base)"
|
|
240
|
+
},
|
|
241
|
+
button: {
|
|
242
|
+
color: "var(--dsw-alias-label-primary)",
|
|
243
|
+
background: "var(--dsw-alias-bg-layer-1)",
|
|
244
|
+
border: ".5px solid var(--dsw-alias-border-l3)",
|
|
245
|
+
borderRadius: "var(--dsw-radius-sm)",
|
|
246
|
+
padding: "4px 12px",
|
|
247
|
+
cursor: "pointer",
|
|
248
|
+
font: "var(--dsw-font-xxs-12)"
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Keeps a rendering failure inside this panel. DSH has no boundary of its own
|
|
254
|
+
* around slot entries: an escaping error unmounts the root slot, which is the
|
|
255
|
+
* whole application UI.
|
|
256
|
+
*/
|
|
257
|
+
class PanelBoundary extends react.Component {
|
|
258
|
+
constructor(props) {
|
|
259
|
+
super(props);
|
|
260
|
+
this.state = { failure: void 0 };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
static getDerivedStateFromError(error) {
|
|
264
|
+
return { failure: String((error && error.message) || error) };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
componentDidCatch(error) {
|
|
268
|
+
try {
|
|
269
|
+
console.error("dsh-plugin-deepseek-chat: panel render failed", error);
|
|
270
|
+
} catch {
|
|
271
|
+
/* console unavailable */
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
render() {
|
|
276
|
+
if (this.state.failure !== void 0) {
|
|
277
|
+
return react.createElement("div", { style: styles.root },
|
|
278
|
+
react.createElement("div", { style: styles.notice },
|
|
279
|
+
react.createElement("div", null, `${COPY.failed}: ${this.state.failure}`)));
|
|
280
|
+
}
|
|
281
|
+
return this.props.children ?? null;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* The global chat panel: a guest webview filling the main column.
|
|
287
|
+
*
|
|
288
|
+
* The guest is created only after the lease resolves, and it is mounted with
|
|
289
|
+
* `name`/`partition`/`src` already set, because the main process inspects
|
|
290
|
+
* exactly that bootstrap src when it approves the attachment. The first load
|
|
291
|
+
* goes to the remembered conversation; navigation events keep that memory
|
|
292
|
+
* current. A dead guest (crash, destroy, failed start) turns into a retry card
|
|
293
|
+
* inside the panel; cleanup releases the lease and never calls into a detached
|
|
294
|
+
* element.
|
|
295
|
+
* @returns the panel element tree.
|
|
296
|
+
*/
|
|
297
|
+
function ChatPanel() {
|
|
298
|
+
const hostRef = react.useRef(null);
|
|
299
|
+
const lastUrlRef = react.useRef(void 0);
|
|
300
|
+
const [failure, setFailure] = react.useState(void 0);
|
|
301
|
+
const [loading, setLoading] = react.useState(true);
|
|
302
|
+
const [attempt, setAttempt] = react.useState(0);
|
|
303
|
+
|
|
304
|
+
const retry = react.useCallback(() => {
|
|
305
|
+
try {
|
|
306
|
+
setFailure(void 0);
|
|
307
|
+
setLoading(true);
|
|
308
|
+
setAttempt((value) => value + 1);
|
|
309
|
+
} catch {
|
|
310
|
+
/* the panel is gone */
|
|
311
|
+
}
|
|
312
|
+
}, []);
|
|
313
|
+
|
|
314
|
+
react.useEffect(() => {
|
|
315
|
+
const host = hostRef.current;
|
|
316
|
+
const bridge = globalThis.dshDesktop?.browser;
|
|
317
|
+
if (host === null) return;
|
|
318
|
+
if (bridge === void 0) {
|
|
319
|
+
try {
|
|
320
|
+
setFailure(COPY.unsupported);
|
|
321
|
+
setLoading(false);
|
|
322
|
+
} catch {
|
|
323
|
+
/* the panel is gone */
|
|
324
|
+
}
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
let disposed = false;
|
|
329
|
+
let unsubscribeOpen;
|
|
330
|
+
let lease;
|
|
331
|
+
let watchdog;
|
|
332
|
+
const element = document.createElement("webview");
|
|
333
|
+
const initialTarget = rememberedTarget();
|
|
334
|
+
let restoring = initialTarget !== CHAT_URL;
|
|
335
|
+
|
|
336
|
+
Object.assign(element.style, styles.guest);
|
|
337
|
+
element.setAttribute("allowpopups", "");
|
|
338
|
+
|
|
339
|
+
/** Report a guest failure inside the panel; never throw. */
|
|
340
|
+
const fail = (error) => {
|
|
341
|
+
if (disposed) return;
|
|
342
|
+
try {
|
|
343
|
+
setLoading(false);
|
|
344
|
+
setFailure(String((error && error.message) || error));
|
|
345
|
+
} catch {
|
|
346
|
+
/* the panel is gone */
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
/** Remember the last document the guest reported. */
|
|
351
|
+
const remember = (raw) => {
|
|
352
|
+
if (raw === void 0) return;
|
|
353
|
+
lastUrlRef.current = raw;
|
|
354
|
+
rememberTarget(raw);
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
const onDomReady = () => {
|
|
358
|
+
if (disposed) return;
|
|
359
|
+
if (watchdog !== void 0) {
|
|
360
|
+
clearTimeout(watchdog);
|
|
361
|
+
watchdog = void 0;
|
|
362
|
+
}
|
|
363
|
+
if (!navigate(element, initialTarget)) fail(new Error(COPY.crashed));
|
|
364
|
+
else remember(initialTarget);
|
|
365
|
+
};
|
|
366
|
+
const onStopLoading = () => {
|
|
367
|
+
if (disposed) return;
|
|
368
|
+
const raw = readUrl(element);
|
|
369
|
+
if (raw === void 0) return;
|
|
370
|
+
remember(raw);
|
|
371
|
+
restoring = false;
|
|
372
|
+
try {
|
|
373
|
+
setLoading(false);
|
|
374
|
+
} catch {
|
|
375
|
+
/* the panel is gone */
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
const onNavigate = () => {
|
|
379
|
+
if (disposed) return;
|
|
380
|
+
remember(readUrl(element));
|
|
381
|
+
};
|
|
382
|
+
const onNavigateInPage = (event) => {
|
|
383
|
+
if (disposed || event?.isMainFrame !== true) return;
|
|
384
|
+
onNavigate();
|
|
385
|
+
};
|
|
386
|
+
const onFailLoad = (event) => {
|
|
387
|
+
if (disposed || event?.isMainFrame !== true) return;
|
|
388
|
+
// -3 is ERR_ABORTED: a superseded navigation, not a failure.
|
|
389
|
+
if (event.errorCode === -3) return;
|
|
390
|
+
if (restoring) {
|
|
391
|
+
// The remembered conversation is gone or unreachable; start clean.
|
|
392
|
+
restoring = false;
|
|
393
|
+
if (!navigate(element, CHAT_URL)) fail(new Error(COPY.crashed));
|
|
394
|
+
else remember(CHAT_URL);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
fail(`${event.errorDescription ?? event.errorCode}`);
|
|
398
|
+
};
|
|
399
|
+
/** The guest process exited or the guest was torn down underneath us. */
|
|
400
|
+
const onGuestGone = () => {
|
|
401
|
+
if (disposed) return;
|
|
402
|
+
fail(new Error(COPY.crashed));
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const listeners = [
|
|
406
|
+
["dom-ready", onDomReady],
|
|
407
|
+
["did-stop-loading", onStopLoading],
|
|
408
|
+
["did-navigate", onNavigate],
|
|
409
|
+
["did-navigate-in-page", onNavigateInPage],
|
|
410
|
+
["did-fail-load", onFailLoad],
|
|
411
|
+
["render-process-gone", onGuestGone],
|
|
412
|
+
["destroyed", onGuestGone]
|
|
413
|
+
];
|
|
414
|
+
|
|
415
|
+
try {
|
|
416
|
+
for (const [name, handler] of listeners) {
|
|
417
|
+
element.addEventListener(name, handler, name === "dom-ready" ? { once: true } : void 0);
|
|
418
|
+
}
|
|
419
|
+
} catch (error) {
|
|
420
|
+
fail(error);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const start = async () => {
|
|
424
|
+
try {
|
|
425
|
+
const reservation = await bridge.acquire(PARTITION_KEY);
|
|
426
|
+
if (disposed) {
|
|
427
|
+
try {
|
|
428
|
+
Promise.resolve(bridge.release(reservation.lease)).catch(() => {});
|
|
429
|
+
} catch {
|
|
430
|
+
/* lease already gone */
|
|
431
|
+
}
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
lease = reservation.lease;
|
|
435
|
+
element.setAttribute("name", reservation.lease);
|
|
436
|
+
element.setAttribute("partition", reservation.partition);
|
|
437
|
+
element.setAttribute("src", `about:blank#${reservation.lease}`);
|
|
438
|
+
try {
|
|
439
|
+
unsubscribeOpen = bridge.onOpenRequested(reservation.lease, (url) => {
|
|
440
|
+
if (!disposed && typeof url === "string") navigate(element, url);
|
|
441
|
+
});
|
|
442
|
+
} catch {
|
|
443
|
+
/* popup bridge unavailable: the guest still works */
|
|
444
|
+
}
|
|
445
|
+
host.append(element);
|
|
446
|
+
watchdog = setTimeout(() => {
|
|
447
|
+
watchdog = void 0;
|
|
448
|
+
if (disposed) return;
|
|
449
|
+
if (readUrl(element) === void 0) fail(new Error(COPY.timeout));
|
|
450
|
+
}, 15000);
|
|
451
|
+
} catch (error) {
|
|
452
|
+
fail(error);
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
void start();
|
|
456
|
+
|
|
457
|
+
return () => {
|
|
458
|
+
disposed = true;
|
|
459
|
+
// Read before removal: React cleans up after taking the DOM away, and
|
|
460
|
+
// Electron's getURL() throws on a detached webview.
|
|
461
|
+
remember(readUrl(element) ?? lastUrlRef.current);
|
|
462
|
+
if (watchdog !== void 0) {
|
|
463
|
+
clearTimeout(watchdog);
|
|
464
|
+
watchdog = void 0;
|
|
465
|
+
}
|
|
466
|
+
try {
|
|
467
|
+
if (typeof unsubscribeOpen === "function") unsubscribeOpen();
|
|
468
|
+
} catch {
|
|
469
|
+
/* already detached */
|
|
470
|
+
}
|
|
471
|
+
for (const [name, handler] of listeners) {
|
|
472
|
+
try {
|
|
473
|
+
element.removeEventListener(name, handler);
|
|
474
|
+
} catch {
|
|
475
|
+
/* already detached */
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
try {
|
|
479
|
+
element.remove();
|
|
480
|
+
} catch {
|
|
481
|
+
/* already detached */
|
|
482
|
+
}
|
|
483
|
+
const released = lease;
|
|
484
|
+
lease = void 0;
|
|
485
|
+
if (released !== void 0) {
|
|
486
|
+
try {
|
|
487
|
+
Promise.resolve(bridge.release(released)).catch(() => {});
|
|
488
|
+
} catch {
|
|
489
|
+
/* lease already gone */
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
}, [attempt]);
|
|
494
|
+
|
|
495
|
+
return react.createElement("div", { style: styles.root },
|
|
496
|
+
react.createElement("div", { style: styles.host, ref: hostRef },
|
|
497
|
+
failure === void 0 && loading
|
|
498
|
+
? react.createElement("div", { style: styles.notice }, COPY.loading)
|
|
499
|
+
: null,
|
|
500
|
+
failure === void 0
|
|
501
|
+
? null
|
|
502
|
+
: react.createElement("div", { style: styles.notice },
|
|
503
|
+
react.createElement("div", null, `${COPY.failed}: ${failure}`),
|
|
504
|
+
react.createElement("button", { type: "button", style: styles.button, onClick: retry }, COPY.retry))));
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/** The registered panel: the boundary owns every rendering failure below it. */
|
|
508
|
+
function ChatPanelShell() {
|
|
509
|
+
return react.createElement(PanelBoundary, null, react.createElement(ChatPanel));
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/** Client service this plugin reads; the loader waits for the slot registry. */
|
|
513
|
+
const inject = ["slots"];
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Register the left-sidebar entry and its main panel.
|
|
517
|
+
* @param ctx - client plugin context.
|
|
518
|
+
*/
|
|
519
|
+
function apply(ctx) {
|
|
520
|
+
try {
|
|
521
|
+
// The panel needs the desktop guest bridge; on the Web surface an iframe
|
|
522
|
+
// cannot carry the site, so the plugin stays inert there.
|
|
523
|
+
if (globalThis.dshDesktop?.browser === void 0) return;
|
|
524
|
+
|
|
525
|
+
ctx.slots.inject("main", () => ctx.slots.register({
|
|
526
|
+
name: "main",
|
|
527
|
+
key: PANEL_ID
|
|
528
|
+
}, ChatPanelShell));
|
|
529
|
+
|
|
530
|
+
ctx.slots.inject("sidebar.panellist", () => ctx.slots.register({
|
|
531
|
+
name: "sidebar.panellist",
|
|
532
|
+
id: PANEL_ID,
|
|
533
|
+
order: 5,
|
|
534
|
+
label: () => COPY.label
|
|
535
|
+
}, ChatGlyph));
|
|
536
|
+
} catch (error) {
|
|
537
|
+
try {
|
|
538
|
+
console.error("dsh-plugin-deepseek-chat: registration failed", error);
|
|
539
|
+
} catch {
|
|
540
|
+
/* console unavailable */
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
exports.apply = apply;
|
|
546
|
+
exports.inject = inject;
|
|
547
|
+
return module.exports;
|
|
548
|
+
}
|
|
549
|
+
});
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host half of dsh-plugin-deepseek-chat.
|
|
3
|
+
*
|
|
4
|
+
* The feature is browser-surface only: the right Sidebar guide card and the
|
|
5
|
+
* redirect into the built-in Browser tab both live in ./client.js. This half
|
|
6
|
+
* exists because the package is mounted as an ordinary Loader entry, and a
|
|
7
|
+
* client bundle is discovered through its entry.
|
|
8
|
+
*/
|
|
9
|
+
export function apply() {}
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,62 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tianyulinfeng1/dsh-plugin-deepseek-chat",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "DeepSeek Chat (chat.deepseek.com) as a DSH sidebar panel: full main-area chat, last-conversation memory, and crash containment.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"icon": "./icon.svg",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./lib/index.js",
|
|
10
|
+
"./client": "./lib/client.js",
|
|
11
|
+
"./locale/*.json": "./locale/*.json",
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"lib",
|
|
16
|
+
"locale",
|
|
17
|
+
"icon.svg",
|
|
18
|
+
"cordis.patch.yml",
|
|
19
|
+
"README.md",
|
|
20
|
+
"README.en.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"test": "node test/check-client.mjs"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"deepseek-harness",
|
|
28
|
+
"dsh-plugin",
|
|
29
|
+
"deepseek",
|
|
30
|
+
"chat",
|
|
31
|
+
"webview"
|
|
32
|
+
],
|
|
33
|
+
"author": "sweet560",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/sweet560/dsh-plugin-deepseek-chat.git"
|
|
41
|
+
},
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/sweet560/dsh-plugin-deepseek-chat/issues"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://github.com/sweet560/dsh-plugin-deepseek-chat#readme",
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=20",
|
|
48
|
+
"dsh": ">=0.2.1-rc.2"
|
|
49
|
+
},
|
|
50
|
+
"dsh": {
|
|
51
|
+
"manifestVersion": 1,
|
|
52
|
+
"bundle": {
|
|
53
|
+
"patch": "./cordis.patch.yml"
|
|
54
|
+
},
|
|
55
|
+
"client": {
|
|
56
|
+
"platform": "web",
|
|
57
|
+
"inject": [
|
|
58
|
+
"@deepseek-ai/dsh-client-ui-sidebar"
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|