@jaychang1989/dsh-webchat 0.4.1 → 0.5.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/NOTICE +18 -17
- package/README.en.md +80 -0
- package/README.md +42 -45
- package/cordis.patch.yml +3 -5
- package/lib/client.js +256 -53
- package/lib/index.js +50 -10
- package/package.json +3 -3
- package/README.zh.md +0 -83
package/NOTICE
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
dsh-webchat
|
|
2
2
|
Copyright 2026 jaychang1989
|
|
3
3
|
|
|
4
|
-
This
|
|
4
|
+
This package is a fork of dsh-web-chat
|
|
5
5
|
(https://github.com/xmuwenxiang/dsh-web-chat), Copyright the dsh-web-chat
|
|
6
|
-
authors, licensed under the Apache License, Version 2.0.
|
|
6
|
+
authors, licensed under the Apache License, Version 2.0. A copy of that license
|
|
7
|
+
is in LICENSE.
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
Reused from that project, and the reason this notice exists:
|
|
9
10
|
|
|
10
|
-
* the
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
(`sessionPersistence.open` / `create` + handle `read` / `append` / `flush`)
|
|
16
|
-
instead of the retired service-level `load` / `append` pair.
|
|
11
|
+
* the plugin scaffolding — the dual-half packaging (a host half plus a
|
|
12
|
+
`dsh.client` browser half), the `cordis.patch.yml` bundle row, and the
|
|
13
|
+
`window.__ModuleLoader__.load` form of the emitted client bundle;
|
|
14
|
+
* the approach to injecting a sidebar entry row at the DOM level, with its
|
|
15
|
+
self-healing MutationObservers, and the base styling that came with it.
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
chat.deepseek.com web app in a window instead of driving the page and
|
|
20
|
-
re-rendering the conversation. The custom chat panel, the local transcript
|
|
21
|
-
store, the five `webchat_*` agent tools, the transfer/distillation code and
|
|
22
|
-
thirteen of the fifteen HTTP routes were removed, along with the
|
|
23
|
-
`playwright-core` runtime dependency and the React-based client build.
|
|
17
|
+
Written here:
|
|
24
18
|
|
|
25
|
-
|
|
19
|
+
* the launcher behaviour — opening the official chat.deepseek.com page in a
|
|
20
|
+
window, the three window strategies and their ordering;
|
|
21
|
+
* the entry-as-action interaction (the entry itself opens the page, with the
|
|
22
|
+
busy state and the result toast);
|
|
23
|
+
* the host routes, the tests and this documentation.
|
|
24
|
+
|
|
25
|
+
The package no longer drives the page, stores transcripts, exposes agent tools
|
|
26
|
+
or reimplements a chat interface.
|
package/README.en.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# dsh-webchat
|
|
2
|
+
|
|
3
|
+
[简体中文](./README.md) | **English**
|
|
4
|
+
|
|
5
|
+
Opens the official [chat.deepseek.com](https://chat.deepseek.com) web app inside [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) — **click the sidebar entry and the page renders in the DSH window itself**, filling the center column like the Automation Tasks page does.
|
|
6
|
+
|
|
7
|
+
No chat UI is reimplemented: model picker, deep think, smart search, history and attachments are all the official ones.
|
|
8
|
+
|
|
9
|
+
## How it can render inside the window
|
|
10
|
+
|
|
11
|
+
The obvious routes really are closed:
|
|
12
|
+
|
|
13
|
+
| Route | Result |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| `<iframe>` | the site answers `Content-Security-Policy: frame-ancestors 'none'`, so the browser refuses |
|
|
16
|
+
| A plain `<webview>` | the desktop build disables the tag and blocks `will-attach-webview` |
|
|
17
|
+
|
|
18
|
+
But the desktop shell keeps a door open for **approved browser guests**: the renderer asks `globalThis.dshDesktop.browser` for a lease, and the shell approves exactly the `<webview>` whose `src` is `about:blank#<lease>` and whose `partition` matches — everything else stays blocked. This is the mechanism the built-in side-card browser uses; this plugin takes the same channel.
|
|
19
|
+
|
|
20
|
+
So the page is **not an iframe** — it is a native guest owned by the host, which is why `frame-ancestors` does not apply to it. It also means the desktop app is what makes it possible.
|
|
21
|
+
|
|
22
|
+
Where that bridge is absent (a plain `dsh web` profile) the plugin **falls back** to opening a window, so the entry always does something.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
dsh plugin --profile desktop add @jaychang1989/dsh-webchat
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Or straight from this repository:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
dsh plugin --profile desktop add github:jaychang1989/dsh-webchat
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
> The npm package is scoped because the unscoped `dsh-webchat` name is taken. The scope does not change the plugin's identity: the entry id stays `webchat`.
|
|
37
|
+
|
|
38
|
+
**Restart the desktop app after installing.**
|
|
39
|
+
|
|
40
|
+
## Use
|
|
41
|
+
|
|
42
|
+
1. Click the "DeepSeek 网页 / DeepSeek Web" entry — the page loads in the center column; there is no second click.
|
|
43
|
+
2. Sign in to DeepSeek once; that holds for the rest of the run.
|
|
44
|
+
|
|
45
|
+
Click again to collapse the panel. The guest stays mounted while the panel is closed, so reopening neither reloads the page nor drops the session. Clicking a session or workspace row in the sidebar hands the center column back to the conversation.
|
|
46
|
+
|
|
47
|
+
## Requirements
|
|
48
|
+
|
|
49
|
+
- DeepSeek Harness **0.2.0-rc.1 or a newer 0.2.x**
|
|
50
|
+
- Node.js >= 22
|
|
51
|
+
- The desktop app: only it provides the native guest bridge; plain `dsh web` uses the window fallback
|
|
52
|
+
|
|
53
|
+
## Known limitations
|
|
54
|
+
|
|
55
|
+
- **Restarting the desktop app means signing in to DeepSeek again.** The host hands out a **process-lifetime** guest partition (a fresh random name per run, with no `persist:` prefix), so the session is not written to disk. That is the host's mechanism, not a choice this plugin makes.
|
|
56
|
+
- The DeepSeek web front end has its own rate limiting and sign-in flow; the plugin only hosts it and does not mediate its requests.
|
|
57
|
+
- Links the page opens are handled inside the guest; the plugin adds no navigation policy of its own.
|
|
58
|
+
|
|
59
|
+
## Troubleshooting
|
|
60
|
+
|
|
61
|
+
| Symptom | Cause / fix |
|
|
62
|
+
| --- | --- |
|
|
63
|
+
| The entry is missing | Check that `@jaychang1989/dsh-webchat` is in the profile's `dsh.profile.bundles`, then restart the desktop app |
|
|
64
|
+
| The center column says "载入失败:…" | The host refused the guest (the bridge threw). The text carries the host's reason |
|
|
65
|
+
| Clicking the entry opened a browser window instead | This renderer had no `dshDesktop.browser` (a plain web profile, for instance), so the fallback ran |
|
|
66
|
+
| It asks for a login again after a restart | See the limitations above — it is the host's partition behaviour |
|
|
67
|
+
|
|
68
|
+
## Development and tests
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
node --test
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Fourteen cases, and the browser half really executes against a DOM stand-in: in a desktop environment it checks that the entry mounts, that clicking reserves a lease, that the webview is built the way the host requires (`about:blank#<lease>`), that navigation happens on `dom-ready`, and that closing and reopening reuses the same guest; without a bridge it checks the window fallback and its reporting.
|
|
75
|
+
|
|
76
|
+
There is no build step — `lib/index.js` and `lib/client.js` are the hand-written runtime, and the package has **zero runtime dependencies**. The guest mechanism is written up in [MAINTAINING.md](./MAINTAINING.md) (Chinese).
|
|
77
|
+
|
|
78
|
+
## Origin and license
|
|
79
|
+
|
|
80
|
+
[Apache-2.0](./LICENSE). This package started as a fork of [xmuwenxiang/dsh-web-chat](https://github.com/xmuwenxiang/dsh-web-chat): it reuses that project's plugin scaffolding (the dual-half packaging, the `cordis.patch.yml` row, the `window.__ModuleLoader__.load` bundle form) and its approach to injecting a sidebar entry, while the current behaviour — the in-window guest panel, the window fallback chain, the entry-as-action — was written here. See [NOTICE](./NOTICE) for the copyright and license statements.
|
package/README.md
CHANGED
|
@@ -1,83 +1,80 @@
|
|
|
1
1
|
# dsh-webchat
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**简体中文** | [English](./README.en.md)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
在 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 里打开 [chat.deepseek.com](https://chat.deepseek.com) 官方网页版——**点侧边栏入口,页面就直接显示在 DSH 窗口里**,占满中栏,和「自动化任务」那些页面一样。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
不重新实现聊天界面:模型选择、深度思考、智能搜索、历史记录、附件上传,全都还是官方那一套。
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## 它凭什么能显示在窗口内
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
- the desktop build runs both windows with **`webviewTag: false`** and blocks `will-attach-webview`, so `<webview>` is unavailable too.
|
|
11
|
+
iframe 和 `<webview>` 常规路径确实都被封死了:
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
| 方式 | 结果 |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| `<iframe>` | 站点返回 `Content-Security-Policy: frame-ancestors 'none'`,浏览器拒绝渲染 |
|
|
16
|
+
| 直接写 `<webview>` | 桌面端默认关闭该标签,且 `will-attach-webview` 一律拦截 |
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
但桌面端为**受信任的浏览器访客**留了通道:渲染进程向 `globalThis.dshDesktop.browser` 申请一个租约,宿主只放行 `src="about:blank#<lease>"` 且 `partition` 匹配的那一个 `<webview>`,其余照旧拦截。DSH 自带的侧栏浏览器用的就是这套机制,本插件走同一条通道。
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
2. **`app-window-shell`** — a chromeless Edge/Chrome window (`--app=`) with its own `--user-data-dir` (`~/.dsh/dsh-webchat/app-window`), so its login stays separate from your everyday browser. Used when the host refuses (1).
|
|
22
|
-
3. **`system-browser`** — handed to the OS default browser, so the button always does something.
|
|
20
|
+
所以这个页面**不是嵌在 iframe 里**,而是宿主的原生访客——`frame-ancestors` 管不到它。这也解释了为什么它必须由 DSH 桌面端承载。
|
|
23
21
|
|
|
24
|
-
|
|
22
|
+
在没有该桥接的环境(纯 `dsh web`,非桌面端)会**自动降级**为打开一个窗口,保证入口永远有反应。
|
|
25
23
|
|
|
26
|
-
##
|
|
24
|
+
## 安装
|
|
27
25
|
|
|
28
26
|
```bash
|
|
29
27
|
dsh plugin --profile desktop add @jaychang1989/dsh-webchat
|
|
30
28
|
```
|
|
31
29
|
|
|
32
|
-
|
|
30
|
+
或直接从仓库安装:
|
|
33
31
|
|
|
34
32
|
```bash
|
|
35
33
|
dsh plugin --profile desktop add github:jaychang1989/dsh-webchat
|
|
36
34
|
```
|
|
37
35
|
|
|
38
|
-
>
|
|
36
|
+
> npm 包名带 scope,因为不带 scope 的 `dsh-webchat` 已被占用。scope 不影响插件身份:entry id 仍是 `webchat`。
|
|
39
37
|
|
|
40
|
-
|
|
38
|
+
安装后**重启桌面端**。
|
|
41
39
|
|
|
42
|
-
##
|
|
40
|
+
## 使用
|
|
43
41
|
|
|
44
|
-
1.
|
|
45
|
-
2.
|
|
42
|
+
1. 点侧边栏的「DeepSeek 网页」入口 —— 页面直接在中栏载入,不需要第二次点击;
|
|
43
|
+
2. 在里面登录一次 DeepSeek,当前这次运行内一直有效。
|
|
46
44
|
|
|
47
|
-
|
|
45
|
+
再点一次入口收起面板;面板关闭后访客保持挂载,重新打开不会重新加载、也不会掉登录。点侧边栏的会话/工作区行会自动把中栏还给对话。
|
|
48
46
|
|
|
49
|
-
##
|
|
47
|
+
## 环境要求
|
|
50
48
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
| Five agent tools (`webchat_status/send/recover/import/transfer`) | All removed |
|
|
55
|
-
| "Transfer to Harness", "recover from web", "import", "export" | All removed |
|
|
56
|
-
| Local transcript store at `~/.dsh/dsh-webchat/transcripts.json` | Removed (only the window profile directory remains) |
|
|
57
|
-
| Fifteen `/api/dsh-webchat/*` routes | Two: `state` and `open` |
|
|
58
|
-
| Depended on `playwright-core`; the client half needed React and a bundler | **No runtime dependencies**; plain-DOM client, no build step |
|
|
59
|
-
| ~3800 lines of build output | ~700 lines of hand-written JS |
|
|
49
|
+
- DeepSeek Harness **0.2.0-rc.1 或更新的 0.2.x**
|
|
50
|
+
- Node.js >= 22
|
|
51
|
+
- 桌面端(DSH Desktop):只有它提供原生访客桥接;纯 `dsh web` 会走窗口降级
|
|
60
52
|
|
|
61
|
-
##
|
|
53
|
+
## 已知限制
|
|
62
54
|
|
|
63
|
-
- DeepSeek
|
|
64
|
-
-
|
|
65
|
-
-
|
|
55
|
+
- **重启桌面端后需要重新登录 DeepSeek。** 宿主给访客分配的是**进程内**分区(每次运行随机命名、不带 `persist:`),登录态不落盘。这是宿主的机制,插件无法改变。
|
|
56
|
+
- 官方网页端有自己的风控与登录流程,插件只负责把它承载起来,不介入其请求。
|
|
57
|
+
- 页面里指向外部的链接在访客内处理;插件不追加自己的导航策略。
|
|
66
58
|
|
|
67
|
-
##
|
|
59
|
+
## 排查
|
|
68
60
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
61
|
+
| 现象 | 原因 / 处理 |
|
|
62
|
+
| --- | --- |
|
|
63
|
+
| 入口没出现 | 确认 profile 的 `dsh.profile.bundles` 里有 `@jaychang1989/dsh-webchat`,然后重启桌面端 |
|
|
64
|
+
| 中栏提示「载入失败:…」 | 宿主拒绝了访客(桥接返回异常)。文本里带着宿主给的原因 |
|
|
65
|
+
| 点了入口却弹出一个浏览器窗口 | 说明当前渲染进程拿不到 `dshDesktop.browser`(例如在纯 web 环境),插件走了降级路径 |
|
|
66
|
+
| 重启后要求重新登录 | 见上面的「已知限制」,属于宿主分区机制 |
|
|
72
67
|
|
|
73
|
-
##
|
|
68
|
+
## 开发与测试
|
|
74
69
|
|
|
75
70
|
```bash
|
|
76
|
-
node --test
|
|
71
|
+
node --test
|
|
77
72
|
```
|
|
78
73
|
|
|
79
|
-
|
|
74
|
+
14 个用例,其中浏览器半区在 DOM 桩里**真实执行**:桌面端环境下验证入口挂载、点击后申请租约、按宿主约定生成 `about:blank#<lease>` 的 webview、`dom-ready` 后导航到目标地址、关闭重开复用同一个访客;无桥接环境下验证降级为请求宿主开窗并如实提示结果。
|
|
75
|
+
|
|
76
|
+
没有构建步骤——`lib/index.js` 与 `lib/client.js` 就是手写的运行时代码,包内**零运行时依赖**。访客机制的来龙去脉见 [MAINTAINING.md](./MAINTAINING.md)。
|
|
80
77
|
|
|
81
|
-
##
|
|
78
|
+
## 来源与许可
|
|
82
79
|
|
|
83
|
-
[Apache-2.0](./LICENSE)
|
|
80
|
+
[Apache-2.0](./LICENSE)。本包最初由 [xmuwenxiang/dsh-web-chat](https://github.com/xmuwenxiang/dsh-web-chat) 分叉而来:沿用其插件骨架(双半区打包、`cordis.patch.yml` 行、浏览器 bundle 的 `window.__ModuleLoader__.load` 形式)与侧边栏入口的 DOM 注入思路;当前功能——中栏访客面板、窗口降级链、入口即动作——都是在这里写的。版权与许可声明见 [NOTICE](./NOTICE)。
|
package/cordis.patch.yml
CHANGED
|
@@ -13,11 +13,9 @@
|
|
|
13
13
|
# manifest name verbatim (scoped names included) — dsh-client-modules keys every
|
|
14
14
|
# browser row by it, and lib/client.js registers its factory under the same id.
|
|
15
15
|
# `id` is the profile entry id; dsh >= 0.1.7 also uses it as the settings form
|
|
16
|
-
# namespace, so keep it stable.
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# `id: webchat`. Do not keep both installed in one profile — a duplicate entry
|
|
20
|
-
# id is rejected at boot.
|
|
16
|
+
# namespace, so keep it stable. It must be unique per profile: a second package
|
|
17
|
+
# inserting the same `webchat` id cannot be installed alongside this one — the
|
|
18
|
+
# duplicate is rejected at boot.
|
|
21
19
|
- insert:
|
|
22
20
|
- id: webchat
|
|
23
21
|
name: '@jaychang1989/dsh-webchat'
|
package/lib/client.js
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-webchat — browser half.
|
|
3
3
|
*
|
|
4
|
-
* One sidebar entry
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* (`frame-ancestors 'none'`) and this build has `webviewTag: false`, so the
|
|
8
|
-
* official page can only live in a window of its own.
|
|
4
|
+
* One sidebar entry that opens the official chat.deepseek.com web app **inside
|
|
5
|
+
* the DSH window**, in the center column, exactly like the other full-page
|
|
6
|
+
* views (Automation Tasks and friends).
|
|
9
7
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* It can do that because the desktop shell lends out native browser guests: the
|
|
9
|
+
* renderer asks `globalThis.dshDesktop.browser` for a reservation and then
|
|
10
|
+
* attaches a `<webview>` whose `src` is `about:blank#<lease>` and whose
|
|
11
|
+
* `partition` matches — the shell approves exactly that element and assigns the
|
|
12
|
+
* real web preferences, so the page is not subject to `frame-ancestors` the way
|
|
13
|
+
* an iframe would be. This is the same mechanism the built-in side-card browser
|
|
14
|
+
* uses; see MAINTAINING.md.
|
|
15
|
+
*
|
|
16
|
+
* Where that bridge is absent (a plain `dsh web` profile, not the desktop app)
|
|
17
|
+
* the entry falls back to asking the host half to open the page in a window.
|
|
18
|
+
*
|
|
19
|
+
* Everything here is plain DOM — no React, no build step.
|
|
13
20
|
*/
|
|
14
21
|
window.__ModuleLoader__.load({
|
|
15
22
|
id: "@jaychang1989/dsh-webchat",
|
|
@@ -17,11 +24,26 @@ window.__ModuleLoader__.load({
|
|
|
17
24
|
var module = { exports: {} }
|
|
18
25
|
var exports = module.exports
|
|
19
26
|
|
|
20
|
-
/**
|
|
21
|
-
var
|
|
27
|
+
/** Host route used only by the window fallback. */
|
|
28
|
+
var OPEN_ROUTE = '/api/dsh-webchat/open'
|
|
29
|
+
/** The page the panel shows. */
|
|
30
|
+
var PAGE_URL = 'https://chat.deepseek.com/'
|
|
31
|
+
/**
|
|
32
|
+
* Storage identity for the guest partition. The shell keys one
|
|
33
|
+
* process-lifetime partition per identity, so this string is what makes
|
|
34
|
+
* the DeepSeek login survive panel close/reopen within one run.
|
|
35
|
+
*/
|
|
36
|
+
var STORAGE_IDENTITY = 'dsh-webchat'
|
|
37
|
+
|
|
22
38
|
var ENTRY_ATTR = 'data-dsh-webchat-entry'
|
|
39
|
+
var VIEW_ATTR = 'data-dsh-webchat-view'
|
|
40
|
+
var ACTIVE_ATTR = 'data-dsh-webchat-active'
|
|
41
|
+
var OTHER_ACTIVE_ATTRS = ['data-dsh-taskboard-active', 'data-dsh-ssh-active']
|
|
42
|
+
var ACTIVATE_EVENT = 'dsh-panel-activate'
|
|
43
|
+
var PANEL_NAME = 'webchat'
|
|
23
44
|
var STYLE_ID = 'dsh-webchat-style'
|
|
24
45
|
var TOAST_ATTR = 'data-dsh-webchat-toast'
|
|
46
|
+
var FRAME_CLASS = 'dsh-wc-frame'
|
|
25
47
|
|
|
26
48
|
var ZH = typeof navigator === 'object' && typeof navigator.language === 'string'
|
|
27
49
|
&& navigator.language.toLowerCase().indexOf('zh') === 0
|
|
@@ -30,25 +52,33 @@ window.__ModuleLoader__.load({
|
|
|
30
52
|
var T = ZH
|
|
31
53
|
? {
|
|
32
54
|
label: 'DeepSeek 网页',
|
|
33
|
-
tooltip: '
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
55
|
+
tooltip: '在 Harness 内打开 chat.deepseek.com',
|
|
56
|
+
failed: '载入失败',
|
|
57
|
+
fallbackOpened: '已打开 DeepSeek 网页',
|
|
58
|
+
fallbackHanded: '已交给系统默认浏览器打开',
|
|
59
|
+
fallbackFailed: '打开失败',
|
|
38
60
|
}
|
|
39
61
|
: {
|
|
40
62
|
label: 'DeepSeek Web',
|
|
41
|
-
tooltip: 'Open chat.deepseek.com',
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
63
|
+
tooltip: 'Open chat.deepseek.com inside Harness',
|
|
64
|
+
failed: 'Could not load it',
|
|
65
|
+
fallbackOpened: 'DeepSeek Web is open',
|
|
66
|
+
fallbackHanded: 'Handed to the system default browser',
|
|
67
|
+
fallbackFailed: 'Could not open',
|
|
46
68
|
}
|
|
47
69
|
|
|
48
|
-
/** Entry
|
|
70
|
+
/** Entry, center-column takeover and guest styling. */
|
|
49
71
|
var CSS = [
|
|
72
|
+
"[data-pane='conversation'],[class*='centerCol']{position:relative}",
|
|
73
|
+
'[' + VIEW_ATTR + ']{position:absolute;inset:0;display:none;z-index:60;background:var(--dsw-alias-bg-base)}',
|
|
74
|
+
'html[' + ACTIVE_ATTR + ']:not([data-dsh-taskboard-active]):not([data-dsh-ssh-active]) [' + VIEW_ATTR + ']{display:flex}',
|
|
75
|
+
"html[" + ACTIVE_ATTR + "]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-active]) [data-pane='conversation'] > :not([" + VIEW_ATTR + ']),',
|
|
76
|
+
'html[' + ACTIVE_ATTR + "]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-active]) [class*='centerCol'] > :not([" + VIEW_ATTR + ']){display:none !important}',
|
|
77
|
+
'.' + FRAME_CLASS + '{-webkit-app-region:no-drag;border:0;flex:auto;width:100%;min-width:0;height:100%;min-height:0;display:flex}',
|
|
78
|
+
'.dsh-wc-message{margin:auto;max-width:460px;padding:22px;text-align:center;font-size:13px;line-height:1.65;color:var(--dsw-alias-label-secondary);font-family:var(--dsw-font-family)}',
|
|
50
79
|
'[' + ENTRY_ATTR + ']{display:flex;align-items:center;gap:8px;width:100%;height:32px;padding:0 12px;background:transparent;border:none;border-radius:8px;color:var(--dsw-alias-label-secondary);cursor:pointer;font-size:13px;white-space:nowrap}',
|
|
51
80
|
'[' + ENTRY_ATTR + ']:hover{background:var(--dsw-specific-sidebar-nav-item-hover);color:var(--dsw-alias-label-primary)}',
|
|
81
|
+
'[' + ENTRY_ATTR + '][data-active]{background:var(--dsw-specific-sidebar-nav-item-active);color:var(--dsw-alias-label-primary);font-weight:600}',
|
|
52
82
|
'[' + ENTRY_ATTR + '][disabled]{opacity:.55;cursor:progress}',
|
|
53
83
|
'[' + ENTRY_ATTR + '] [data-part=icon]{display:inline-flex;align-items:center;justify-content:center;flex:none}',
|
|
54
84
|
'[' + ENTRY_ATTR + '] [data-part=label]{overflow:hidden;text-overflow:ellipsis}',
|
|
@@ -58,6 +88,16 @@ window.__ModuleLoader__.load({
|
|
|
58
88
|
'[' + TOAST_ATTR + '][data-state=bad]{border-color:var(--dsw-alias-state-danger,#ef4444);color:var(--dsw-alias-state-danger,#ef4444)}',
|
|
59
89
|
].join('\n')
|
|
60
90
|
|
|
91
|
+
/**
|
|
92
|
+
* The desktop shell's browser bridge, when this renderer is the desktop
|
|
93
|
+
* app's primary frame. A plain web profile exposes only a protocol
|
|
94
|
+
* version, so the window fallback takes over.
|
|
95
|
+
*/
|
|
96
|
+
const carrier = globalThis.dshDesktop
|
|
97
|
+
const guests = carrier !== null && typeof carrier === 'object' && carrier.protocolVersion === 1
|
|
98
|
+
? carrier.browser
|
|
99
|
+
: undefined
|
|
100
|
+
|
|
61
101
|
/** Inject the stylesheet once. */
|
|
62
102
|
function ensureStyle() {
|
|
63
103
|
if (document.getElementById(STYLE_ID) !== null) return
|
|
@@ -67,7 +107,7 @@ window.__ModuleLoader__.load({
|
|
|
67
107
|
document.head.appendChild(tag)
|
|
68
108
|
}
|
|
69
109
|
|
|
70
|
-
/** Show one transient message
|
|
110
|
+
/** Show one transient message in the corner. */
|
|
71
111
|
function toast(text, state) {
|
|
72
112
|
var previous = document.querySelector('[' + TOAST_ATTR + ']')
|
|
73
113
|
if (previous !== null) previous.remove()
|
|
@@ -81,15 +121,39 @@ window.__ModuleLoader__.load({
|
|
|
81
121
|
}, state === 'bad' ? 8000 : 3000)
|
|
82
122
|
}
|
|
83
123
|
|
|
84
|
-
/**
|
|
85
|
-
async function
|
|
86
|
-
const response = await fetch(
|
|
124
|
+
/** Ask the host half to open the page in a window (the non-desktop path). */
|
|
125
|
+
async function openInWindow() {
|
|
126
|
+
const response = await fetch(OPEN_ROUTE, { method: 'POST', headers: { accept: 'application/json' } })
|
|
87
127
|
let payload = {}
|
|
88
128
|
try { payload = await response.json() } catch (error) { payload = {} }
|
|
89
129
|
if (!response.ok && payload.error === undefined) payload.error = 'HTTP ' + response.status
|
|
90
130
|
return payload
|
|
91
131
|
}
|
|
92
132
|
|
|
133
|
+
/** Tiny observable for the panel's open state. */
|
|
134
|
+
function createController() {
|
|
135
|
+
var open = false
|
|
136
|
+
var listeners = new Set()
|
|
137
|
+
var notify = function () {
|
|
138
|
+
listeners.forEach(function (listener) {
|
|
139
|
+
try {
|
|
140
|
+
listener()
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.warn('[dsh-webchat] listener failed:', error)
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
isOpen: function () { return open },
|
|
148
|
+
subscribe: function (listener) {
|
|
149
|
+
listeners.add(listener)
|
|
150
|
+
return function () { listeners.delete(listener) }
|
|
151
|
+
},
|
|
152
|
+
toggle: function () { open = !open; notify() },
|
|
153
|
+
close: function () { if (!open) return; open = false; notify() },
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
93
157
|
/** The sidebar shell root, or undefined while it is not mounted. */
|
|
94
158
|
function sidebarRoot() {
|
|
95
159
|
var column = document.querySelector('[data-pane="sidebar"], [class*="sidebarCol"]')
|
|
@@ -110,7 +174,7 @@ window.__ModuleLoader__.load({
|
|
|
110
174
|
}
|
|
111
175
|
|
|
112
176
|
/** Build the entry row. */
|
|
113
|
-
function createEntry() {
|
|
177
|
+
function createEntry(onClick) {
|
|
114
178
|
var entry = document.createElement('button')
|
|
115
179
|
entry.type = 'button'
|
|
116
180
|
entry.setAttribute(ENTRY_ATTR, '')
|
|
@@ -126,26 +190,7 @@ window.__ModuleLoader__.load({
|
|
|
126
190
|
label.textContent = T.label
|
|
127
191
|
entry.appendChild(icon)
|
|
128
192
|
entry.appendChild(label)
|
|
129
|
-
|
|
130
|
-
entry.addEventListener('click', async function () {
|
|
131
|
-
if (entry.disabled) return
|
|
132
|
-
entry.disabled = true
|
|
133
|
-
var labelNode = entry.querySelector('[data-part=label]')
|
|
134
|
-
if (labelNode !== null) labelNode.textContent = T.busy
|
|
135
|
-
try {
|
|
136
|
-
var outcome = await openPage()
|
|
137
|
-
if (outcome.ok === true) {
|
|
138
|
-
toast(String(outcome.via) === 'system-browser' ? T.handed : T.opened)
|
|
139
|
-
} else {
|
|
140
|
-
toast(T.failed + ':' + String(outcome.error || outcome.via || ''), 'bad')
|
|
141
|
-
}
|
|
142
|
-
} catch (error) {
|
|
143
|
-
toast(T.failed + ':' + (error instanceof Error ? error.message : String(error)), 'bad')
|
|
144
|
-
}
|
|
145
|
-
if (labelNode !== null) labelNode.textContent = T.label
|
|
146
|
-
entry.disabled = false
|
|
147
|
-
})
|
|
148
|
-
|
|
193
|
+
entry.addEventListener('click', onClick)
|
|
149
194
|
return entry
|
|
150
195
|
}
|
|
151
196
|
|
|
@@ -165,9 +210,7 @@ window.__ModuleLoader__.load({
|
|
|
165
210
|
}
|
|
166
211
|
|
|
167
212
|
/** Mount the sidebar entry with its self-healing observers. */
|
|
168
|
-
function mountSidebarEntry() {
|
|
169
|
-
if (document.querySelector('[' + ENTRY_ATTR + ']') !== null) return function () {}
|
|
170
|
-
var entry = createEntry()
|
|
213
|
+
function mountSidebarEntry(controller, entry) {
|
|
171
214
|
var root
|
|
172
215
|
var placed = false
|
|
173
216
|
|
|
@@ -200,22 +243,182 @@ window.__ModuleLoader__.load({
|
|
|
200
243
|
if (!root.contains(entry)) placed = placeEntry(root, entry)
|
|
201
244
|
})
|
|
202
245
|
|
|
246
|
+
var syncActive = function () {
|
|
247
|
+
if (controller.isOpen()) entry.dataset.active = 'true'
|
|
248
|
+
else delete entry.dataset.active
|
|
249
|
+
}
|
|
250
|
+
var unsubscribe = controller.subscribe(syncActive)
|
|
251
|
+
syncActive()
|
|
203
252
|
tryPlace()
|
|
204
253
|
|
|
205
254
|
return function () {
|
|
206
255
|
waitObserver.disconnect()
|
|
207
256
|
rootObserver.disconnect()
|
|
257
|
+
unsubscribe()
|
|
208
258
|
entry.remove()
|
|
209
259
|
}
|
|
210
260
|
}
|
|
211
261
|
|
|
212
|
-
/**
|
|
262
|
+
/** The center column that hosts full-page views. */
|
|
263
|
+
function conversationColumn() {
|
|
264
|
+
return document.querySelector('[data-pane="conversation"], [class*="centerCol"]') || undefined
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Mount the panel that carries the guest.
|
|
269
|
+
*
|
|
270
|
+
* The guest is created lazily on first open and then kept mounted while
|
|
271
|
+
* the panel is closed, so re-opening does not reload the page and the
|
|
272
|
+
* DeepSeek session stays where it was.
|
|
273
|
+
* @param controller - panel open state.
|
|
274
|
+
* @returns disposer removing the panel and releasing the guest.
|
|
275
|
+
*/
|
|
276
|
+
function mountPanel(controller) {
|
|
277
|
+
var container
|
|
278
|
+
var frame
|
|
279
|
+
var lease
|
|
280
|
+
var failed = false
|
|
281
|
+
|
|
282
|
+
var showMessage = function (text) {
|
|
283
|
+
var box = document.createElement('div')
|
|
284
|
+
box.className = 'dsh-wc-message'
|
|
285
|
+
box.textContent = text
|
|
286
|
+
container.appendChild(box)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Reserve a guest and attach its webview. */
|
|
290
|
+
var ensureGuest = async function () {
|
|
291
|
+
if (frame !== undefined || failed) return
|
|
292
|
+
try {
|
|
293
|
+
const reservation = await guests.acquire(STORAGE_IDENTITY)
|
|
294
|
+
if (reservation === null || typeof reservation !== 'object'
|
|
295
|
+
|| typeof reservation.lease !== 'string' || typeof reservation.partition !== 'string') {
|
|
296
|
+
throw new Error('the desktop browser bridge returned no reservation')
|
|
297
|
+
}
|
|
298
|
+
lease = reservation.lease
|
|
299
|
+
const element = document.createElement('webview')
|
|
300
|
+
element.className = FRAME_CLASS
|
|
301
|
+
element.setAttribute('name', reservation.lease)
|
|
302
|
+
element.setAttribute('partition', reservation.partition)
|
|
303
|
+
element.setAttribute('allowpopups', '')
|
|
304
|
+
element.setAttribute('src', 'about:blank#' + reservation.lease)
|
|
305
|
+
element.addEventListener('dom-ready', function () {
|
|
306
|
+
element.loadURL(PAGE_URL).catch(function (error) {
|
|
307
|
+
toast(T.failed + ':' + (error instanceof Error ? error.message : String(error)), 'bad')
|
|
308
|
+
})
|
|
309
|
+
}, { once: true })
|
|
310
|
+
frame = element
|
|
311
|
+
container.appendChild(element)
|
|
312
|
+
} catch (error) {
|
|
313
|
+
failed = true
|
|
314
|
+
showMessage(T.failed + ':' + (error instanceof Error ? error.message : String(error)))
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
var ensure = function () {
|
|
319
|
+
if (container !== undefined) {
|
|
320
|
+
if (container.isConnected) return
|
|
321
|
+
container.remove()
|
|
322
|
+
container = undefined
|
|
323
|
+
frame = undefined
|
|
324
|
+
}
|
|
325
|
+
var column = conversationColumn()
|
|
326
|
+
if (column === undefined) return
|
|
327
|
+
container = document.createElement('div')
|
|
328
|
+
container.setAttribute(VIEW_ATTR, '')
|
|
329
|
+
container.setAttribute('data-dsh-plugin', 'webchat')
|
|
330
|
+
column.appendChild(container)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
var waitObserver = new MutationObserver(function () { ensure() })
|
|
334
|
+
|
|
335
|
+
var applyActive = function () {
|
|
336
|
+
if (controller.isOpen()) {
|
|
337
|
+
OTHER_ACTIVE_ATTRS.forEach(function (attr) { document.documentElement.removeAttribute(attr) })
|
|
338
|
+
document.documentElement.setAttribute(ACTIVE_ATTR, '')
|
|
339
|
+
// Mount before announcing: the arbitration event dispatches into
|
|
340
|
+
// other plugins, and a throwing listener must not be able to leave
|
|
341
|
+
// this panel half-open.
|
|
342
|
+
ensure()
|
|
343
|
+
void ensureGuest()
|
|
344
|
+
document.dispatchEvent(new CustomEvent(ACTIVATE_EVENT, { detail: PANEL_NAME }))
|
|
345
|
+
} else {
|
|
346
|
+
document.documentElement.removeAttribute(ACTIVE_ATTR)
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
var onOtherActivate = function (event) {
|
|
350
|
+
var detail = event.detail
|
|
351
|
+
if ((detail === 'taskboard' || detail === 'ssh') && controller.isOpen()) controller.close()
|
|
352
|
+
}
|
|
353
|
+
var SIDEBAR_ROW_SELECTOR = '[class*="sessionRow"], [class*="projectRow"], [class*="searchResultRow"], [class*="searchResultWorkspace"], [class*="newSession"]'
|
|
354
|
+
var onClickSidebarRow = function (event) {
|
|
355
|
+
if (!controller.isOpen()) return
|
|
356
|
+
var target = event.target
|
|
357
|
+
if (target === null || target.closest === undefined) return
|
|
358
|
+
if (target.closest(SIDEBAR_ROW_SELECTOR) !== null) controller.close()
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
document.addEventListener('click', onClickSidebarRow, true)
|
|
362
|
+
document.addEventListener(ACTIVATE_EVENT, onOtherActivate)
|
|
363
|
+
var unsubscribe = controller.subscribe(applyActive)
|
|
364
|
+
applyActive()
|
|
365
|
+
ensure()
|
|
366
|
+
waitObserver.observe(document.body, { childList: true, subtree: true })
|
|
367
|
+
|
|
368
|
+
return function () {
|
|
369
|
+
document.removeEventListener('click', onClickSidebarRow, true)
|
|
370
|
+
document.removeEventListener(ACTIVATE_EVENT, onOtherActivate)
|
|
371
|
+
waitObserver.disconnect()
|
|
372
|
+
unsubscribe()
|
|
373
|
+
document.documentElement.removeAttribute(ACTIVE_ATTR)
|
|
374
|
+
if (frame !== undefined) frame.remove()
|
|
375
|
+
if (container !== undefined) container.remove()
|
|
376
|
+
frame = undefined
|
|
377
|
+
container = undefined
|
|
378
|
+
if (lease !== undefined) {
|
|
379
|
+
const releasing = lease
|
|
380
|
+
lease = undefined
|
|
381
|
+
try { void guests.release(releasing) } catch (error) { /* already gone */ }
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** Mount the entry, plus the panel when the desktop bridge is present. */
|
|
213
387
|
function apply(ctx) {
|
|
214
388
|
ensureStyle()
|
|
215
|
-
var
|
|
389
|
+
var controller = createController()
|
|
390
|
+
var disposers = []
|
|
391
|
+
|
|
392
|
+
if (guests !== undefined) {
|
|
393
|
+
// Desktop: the entry toggles the in-app panel.
|
|
394
|
+
var entry = createEntry(function () { controller.toggle() })
|
|
395
|
+
disposers.push(mountSidebarEntry(controller, entry))
|
|
396
|
+
disposers.push(mountPanel(controller))
|
|
397
|
+
} else {
|
|
398
|
+
// Web: no native guest to lend, so the entry asks the host for a window.
|
|
399
|
+
var fallbackEntry = createEntry(async function () {
|
|
400
|
+
if (fallbackEntry.disabled) return
|
|
401
|
+
fallbackEntry.disabled = true
|
|
402
|
+
try {
|
|
403
|
+
var outcome = await openInWindow()
|
|
404
|
+
if (outcome.ok === true) {
|
|
405
|
+
toast(String(outcome.via) === 'system-browser' ? T.fallbackHanded : T.fallbackOpened)
|
|
406
|
+
} else {
|
|
407
|
+
toast(T.fallbackFailed + ':' + String(outcome.error || outcome.via || ''), 'bad')
|
|
408
|
+
}
|
|
409
|
+
} catch (error) {
|
|
410
|
+
toast(T.fallbackFailed + ':' + (error instanceof Error ? error.message : String(error)), 'bad')
|
|
411
|
+
}
|
|
412
|
+
fallbackEntry.disabled = false
|
|
413
|
+
})
|
|
414
|
+
disposers.push(mountSidebarEntry(controller, fallbackEntry))
|
|
415
|
+
}
|
|
416
|
+
|
|
216
417
|
ctx.effect(function () {
|
|
217
|
-
return function () {
|
|
218
|
-
|
|
418
|
+
return function () {
|
|
419
|
+
disposers.splice(0).forEach(function (dispose) { dispose() })
|
|
420
|
+
}
|
|
421
|
+
}, 'dsh-webchat: ui mounts')
|
|
219
422
|
}
|
|
220
423
|
|
|
221
424
|
exports.apply = apply
|
package/lib/index.js
CHANGED
|
@@ -28,8 +28,16 @@
|
|
|
28
28
|
|
|
29
29
|
import { spawn } from 'node:child_process'
|
|
30
30
|
import { existsSync } from 'node:fs'
|
|
31
|
+
import { createRequire } from 'node:module'
|
|
31
32
|
import { join } from 'node:path'
|
|
32
33
|
|
|
34
|
+
/**
|
|
35
|
+
* A CommonJS `require` anchored at this module. Electron hands `electron` to
|
|
36
|
+
* CJS natively, while the harness's ESM loader resolves bare specifiers itself
|
|
37
|
+
* — which is why the ESM import below is tried first and this is the fallback.
|
|
38
|
+
*/
|
|
39
|
+
const requireHere = createRequire(import.meta.url)
|
|
40
|
+
|
|
33
41
|
/** Stable cordis plugin name. */
|
|
34
42
|
export const name = 'webchat'
|
|
35
43
|
|
|
@@ -65,6 +73,9 @@ function windowSlot() {
|
|
|
65
73
|
/** Last open attempt, reported to the panel. */
|
|
66
74
|
let lastAttempt = { ok: false, via: 'none', error: '', at: 0 }
|
|
67
75
|
|
|
76
|
+
/** Every strategy's outcome from the most recent open, oldest first. */
|
|
77
|
+
let lastAttempts = []
|
|
78
|
+
|
|
68
79
|
/**
|
|
69
80
|
* Remember one attempt.
|
|
70
81
|
* @param ok - whether the page was opened.
|
|
@@ -77,19 +88,41 @@ function record(ok, via, error = '') {
|
|
|
77
88
|
return lastAttempt
|
|
78
89
|
}
|
|
79
90
|
|
|
91
|
+
/** Render any thrown value as a message. */
|
|
92
|
+
function messageOf(error) {
|
|
93
|
+
return error instanceof Error ? error.message : String(error)
|
|
94
|
+
}
|
|
95
|
+
|
|
80
96
|
/**
|
|
81
|
-
* Load Electron lazily. A plugin that cannot reach it must still
|
|
82
|
-
*
|
|
83
|
-
* strategy instead of killing the entry at import time.
|
|
97
|
+
* Load Electron lazily, two ways. A plugin that cannot reach it must still
|
|
98
|
+
* load, and the probe stays inside the call so a failure falls through to the
|
|
99
|
+
* next strategy instead of killing the entry at import time.
|
|
100
|
+
*
|
|
101
|
+
* The ESM import is tried first because it is the honest form; the harness's
|
|
102
|
+
* loader resolves bare specifiers itself and does not always hand `electron`
|
|
103
|
+
* to Electron's own resolver, so a CommonJS require — which Electron serves
|
|
104
|
+
* natively — is the fallback.
|
|
84
105
|
* @returns the Electron module.
|
|
85
|
-
* @throws when
|
|
106
|
+
* @throws with every probe failure joined, when neither path yields a window API.
|
|
86
107
|
*/
|
|
87
108
|
async function electronApi() {
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
109
|
+
const usable = (mod) => (mod !== null && typeof mod === 'object' && mod.BrowserWindow !== undefined ? mod : undefined)
|
|
110
|
+
const failures = []
|
|
111
|
+
try {
|
|
112
|
+
const fromImport = usable(await import('electron'))
|
|
113
|
+
if (fromImport !== undefined) return fromImport
|
|
114
|
+
failures.push('esm import resolved but exposed no BrowserWindow')
|
|
115
|
+
} catch (error) {
|
|
116
|
+
failures.push(`esm import failed: ${messageOf(error)}`)
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const fromRequire = usable(requireHere('electron'))
|
|
120
|
+
if (fromRequire !== undefined) return fromRequire
|
|
121
|
+
failures.push('cjs require resolved but exposed no BrowserWindow')
|
|
122
|
+
} catch (error) {
|
|
123
|
+
failures.push(`cjs require failed: ${messageOf(error)}`)
|
|
91
124
|
}
|
|
92
|
-
|
|
125
|
+
throw new Error(failures.join(' | '))
|
|
93
126
|
}
|
|
94
127
|
|
|
95
128
|
/** Windows chromeless-window executables, in preference order. */
|
|
@@ -192,15 +225,21 @@ export const OPEN_STRATEGIES = [
|
|
|
192
225
|
* @returns the recorded attempt: ok, the winning `via`, or the last error.
|
|
193
226
|
*/
|
|
194
227
|
export async function openPage(strategies = OPEN_STRATEGIES) {
|
|
228
|
+
const attempts = []
|
|
195
229
|
let failure = { via: 'none', error: 'no strategy was tried' }
|
|
196
230
|
for (const strategy of strategies) {
|
|
197
231
|
try {
|
|
198
232
|
const detail = await strategy.run()
|
|
199
|
-
|
|
233
|
+
const via = typeof detail === 'string' && detail !== '' ? detail : strategy.via
|
|
234
|
+
attempts.push({ via: strategy.via, ok: true, error: '' })
|
|
235
|
+
lastAttempts = attempts
|
|
236
|
+
return record(true, via)
|
|
200
237
|
} catch (error) {
|
|
201
|
-
failure = { via: strategy.via, error:
|
|
238
|
+
failure = { via: strategy.via, error: messageOf(error) }
|
|
239
|
+
attempts.push({ via: strategy.via, ok: false, error: failure.error })
|
|
202
240
|
}
|
|
203
241
|
}
|
|
242
|
+
lastAttempts = attempts
|
|
204
243
|
return record(false, failure.via, failure.error)
|
|
205
244
|
}
|
|
206
245
|
|
|
@@ -229,6 +268,7 @@ export function apply(ctx) {
|
|
|
229
268
|
url: PAGE_URL,
|
|
230
269
|
appWindowOpen: slot.current !== null && slot.current.isDestroyed() === false,
|
|
231
270
|
last: lastAttempt,
|
|
271
|
+
attempts: lastAttempts,
|
|
232
272
|
})
|
|
233
273
|
},
|
|
234
274
|
}), 'dsh-webchat: state route')
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jaychang1989/dsh-webchat",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Opens the official DeepSeek web app (chat.deepseek.com)
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Opens the official DeepSeek web app (chat.deepseek.com) inside DeepSeek Harness: one sidebar entry renders the real page in the center column, through the same native browser guest the built-in side-card browser uses. No chat UI, no agent tools, no runtime dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
7
7
|
"deepseek-harness",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"cordis.patch.yml",
|
|
57
57
|
"NOTICE",
|
|
58
58
|
"README.md",
|
|
59
|
-
"README.
|
|
59
|
+
"README.en.md"
|
|
60
60
|
],
|
|
61
61
|
"scripts": {
|
|
62
62
|
"test": "node --test"
|
package/README.zh.md
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
# dsh-webchat
|
|
2
|
-
|
|
3
|
-
在 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 里打开 [chat.deepseek.com](https://chat.deepseek.com) 官方网页版:侧边栏一个入口,**点一下就直接打开官方页面**。
|
|
4
|
-
|
|
5
|
-
**不再重新实现聊天界面。** 官方网页版本身就是完整的客户端——模型选择、深度思考、智能搜索、历史记录、附件上传都在里面。本插件只负责把它打开,别的一概不管。
|
|
6
|
-
|
|
7
|
-
## 为什么是「开一个窗口」,而不是「嵌在面板里」
|
|
8
|
-
|
|
9
|
-
两条路都被封死了,这是实测结论而非选择:
|
|
10
|
-
|
|
11
|
-
- `chat.deepseek.com` 返回 **`Content-Security-Policy: frame-ancestors 'none'`** —— 任何 iframe 嵌入都会被浏览器拒绝;
|
|
12
|
-
- DSH 桌面端两个窗口都是 **`webviewTag: false`**,且 `will-attach-webview` 被显式拦截 —— `<webview>` 也不可用。
|
|
13
|
-
|
|
14
|
-
所以忠实呈现官方页面的唯一方式就是开一个真实窗口。
|
|
15
|
-
|
|
16
|
-
## 打开顺序
|
|
17
|
-
|
|
18
|
-
点击入口后会按顺序尝试,第一个成功的生效,结果用一条短提示告诉你实际用了哪种:
|
|
19
|
-
|
|
20
|
-
1. **`app-window`** —— 由 DSH 桌面端进程直接创建的窗口(桌面端就是 Electron)。已经开着就聚焦,不会重复开第二个。
|
|
21
|
-
2. **`app-window-shell`** —— 无边框的 Edge/Chrome 窗口(`--app=`),使用独立 `--user-data-dir`(`~/.dsh/dsh-webchat/app-window`),因此登录态与你的日常浏览器互不干扰。用于第 1 种被宿主拒绝的场景。
|
|
22
|
-
3. **`system-browser`** —— 交给操作系统默认浏览器,保证按钮任何时候都有反应。
|
|
23
|
-
|
|
24
|
-
登录一次即可:窗口自己的 profile 会保留登录态。
|
|
25
|
-
|
|
26
|
-
## 安装
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
dsh plugin --profile desktop add @jaychang1989/dsh-webchat
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
或直接从仓库安装:
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
|
-
dsh plugin --profile desktop add github:jaychang1989/dsh-webchat
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
> npm 包名带 scope,因为不带 scope 的 `dsh-webchat` 属于已停止维护的上游项目。scope 不影响插件身份:entry id 仍是 `webchat`。
|
|
39
|
-
|
|
40
|
-
**从上游或本插件 0.3.x 切换过来时**:它们都插入 `id: webchat` 这一行,同一个 profile 里不能并存(entry id 重复会在启动时报错)。先移除旧的再加新的,然后重启桌面端。
|
|
41
|
-
|
|
42
|
-
## 使用
|
|
43
|
-
|
|
44
|
-
1. 点侧边栏的「DeepSeek 网页」入口 —— 页面直接打开,不需要再点第二次;
|
|
45
|
-
2. 在弹出的窗口里登录一次,之后正常使用官方网页版。
|
|
46
|
-
|
|
47
|
-
点击后右下角会有一条短提示说明结果:`已打开 DeepSeek 网页` / `已交给系统默认浏览器打开` / 失败原因(例如 `HTTP 404`,通常意味着宿主半区还是旧版本,重启桌面端即可)。
|
|
48
|
-
|
|
49
|
-
## 与 0.3.x 的差异(这是一次大幅删减)
|
|
50
|
-
|
|
51
|
-
| 0.3.x | 0.4.0 |
|
|
52
|
-
| --- | --- |
|
|
53
|
-
| 用 Playwright 驱动网页,自研聊天面板重新渲染消息 | 不再驱动网页,直接开官方页面;自研面板删除 |
|
|
54
|
-
| 5 个 agent 工具(`webchat_status/send/recover/import/transfer`) | 全部删除 |
|
|
55
|
-
| 「转移到 harness」「从网页恢复」「导入」「导出」 | 全部删除 |
|
|
56
|
-
| 本地会话存储 `~/.dsh/dsh-webchat/transcripts.json` | 删除(只留窗口 profile 目录) |
|
|
57
|
-
| 15 条 `/api/dsh-webchat/*` 路由 | 只剩 2 条:`state` / `open` |
|
|
58
|
-
| 依赖 `playwright-core`,客户端产物需 React + 构建器 | **零运行时依赖**,客户端为纯 DOM,无构建步骤 |
|
|
59
|
-
| 约 3800 行(构建产物) | 约 700 行(手写 JS) |
|
|
60
|
-
|
|
61
|
-
## 环境要求
|
|
62
|
-
|
|
63
|
-
- DeepSeek Harness **0.2.0-rc.1 或更新的 0.2.x**
|
|
64
|
-
- Node.js >= 22
|
|
65
|
-
- 第 2 种策略(无边框窗口)需要本机装有 Edge 或 Chrome;第 1 种策略不需要
|
|
66
|
-
|
|
67
|
-
## 限制
|
|
68
|
-
|
|
69
|
-
- 官方网页端受 DeepSeek 官方风控;页面打不开时插件只负责把页面交出去,不介入其登录或请求。
|
|
70
|
-
- 第 1 种策略创建的窗口由 DSH 进程拥有:若你先关掉 DSH 主窗口、而它仍开着,DSH 不会退出(Electron 的 `window-all-closed` 语义)。关掉该窗口即可。
|
|
71
|
-
- 插件被重载时不会主动关闭已经打开的窗口(避免一次设置变更就关掉你正在看的会话)。
|
|
72
|
-
|
|
73
|
-
## 开发与测试
|
|
74
|
-
|
|
75
|
-
```bash
|
|
76
|
-
node --test # 11 个用例:宿主路由、窗口策略选择、浏览器半区在 DOM 桩里的挂载与按钮行为
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
没有构建步骤:`lib/index.js` 与 `lib/client.js` 就是手写的运行时代码。详见 [MAINTAINING.md](./MAINTAINING.md)。
|
|
80
|
-
|
|
81
|
-
## License
|
|
82
|
-
|
|
83
|
-
[Apache-2.0](./LICENSE) — 原始版权归 [xmuwenxiang/dsh-web-chat](https://github.com/xmuwenxiang/dsh-web-chat) 作者所有,详见 [NOTICE](./NOTICE)。
|