@jaychang1989/dsh-webchat 0.4.0 → 0.4.2

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 CHANGED
@@ -1,25 +1,26 @@
1
1
  dsh-webchat
2
2
  Copyright 2026 jaychang1989
3
3
 
4
- This product is a fork of dsh-web-chat
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
- 0.3.0 retargeted the plugin at the DeepSeek Harness 0.2.x plugin APIs:
9
+ Reused from that project, and the reason this notice exists:
9
10
 
10
- * the settings section is no longer registered by the plugin
11
- (`installSettingsSection` / `settingsNamespace` were removed from
12
- `@deepseek-ai/dsh-settings` in 0.1.7; forms are now derived from the
13
- profile entry and its exported `Config`);
14
- * session persistence is addressed through per-session handles
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
- 0.4.0 reduced the package to a launcher: it opens the official
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
- See MAINTAINING.md for the current structure and the constraints behind it.
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,88 @@
1
+ # dsh-webchat
2
+
3
+ [简体中文](./README.md) | **English**
4
+
5
+ Opens the official [chat.deepseek.com](https://chat.deepseek.com) web app from [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) in one click.
6
+
7
+ **One sidebar entry. Click it, and the official page opens in a window.**
8
+
9
+ That is the whole plugin. No chat UI, no transcript store, no agent tools — the official web app already is the client: model picker, deep think, smart search, history and attachments all live there.
10
+
11
+ ## Why it opens a window instead of a pane
12
+
13
+ Three routes are closed, and these are measurements rather than preferences:
14
+
15
+ | Route | Measured result |
16
+ | --- | --- |
17
+ | `<iframe>` | `chat.deepseek.com` answers with `Content-Security-Policy: frame-ancestors 'none'`, so the browser refuses to render it in a frame |
18
+ | Electron `<webview>` | both desktop windows run with `webviewTag: false`, and `will-attach-webview` is explicitly blocked |
19
+ | The desktop app's own `WebContentsView` | reserved for platform pages (account/top-up) with navigation pinned to that origin; not reachable from a plugin |
20
+
21
+ A real window is therefore the only faithful way to show the official page.
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ dsh plugin --profile desktop add @jaychang1989/dsh-webchat
27
+ ```
28
+
29
+ Or straight from this repository:
30
+
31
+ ```bash
32
+ dsh plugin --profile desktop add github:jaychang1989/dsh-webchat
33
+ ```
34
+
35
+ > 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`.
36
+
37
+ **Restart the desktop app after installing.**
38
+
39
+ ## Use
40
+
41
+ 1. Click the "DeepSeek 网页 / DeepSeek Web" entry in the sidebar.
42
+ 2. Sign in to DeepSeek once in the window it opens; the session is kept afterwards.
43
+
44
+ A short toast in the corner reports the outcome.
45
+
46
+ ## How it opens the page
47
+
48
+ Tried in order; the first that works wins, and the toast tells you which one it was:
49
+
50
+ 1. **App window** — a window created by the DSH desktop process itself (the desktop app is Electron). An already-open one is focused instead of duplicated.
51
+ 2. **Chromeless window** — Edge/Chrome in `--app=` mode with its own `--user-data-dir` (`~/.dsh/dsh-webchat/app-window`), so its login stays separate from your everyday browser. Used when (1) cannot reach Electron.
52
+ 3. **System browser** — the fallback, so a click always does something.
53
+
54
+ None of the three needs configuration.
55
+
56
+ ## Requirements
57
+
58
+ - DeepSeek Harness **0.2.0-rc.1 or a newer 0.2.x**
59
+ - Node.js >= 22
60
+ - Strategy 2 needs Edge or Chrome installed; strategy 1 does not
61
+
62
+ ## Known limitations
63
+
64
+ - The DeepSeek web front end has its own rate limiting and sign-in flow; the plugin only hands the page over and does not mediate its requests.
65
+ - A strategy-1 window is owned by the DSH process: if you close the DSH main window while it is still open, DSH will not quit (Electron's `window-all-closed`). Close that window too.
66
+ - Reloading the plugin does not close a window that is already open — otherwise a settings change would close the conversation you are reading.
67
+
68
+ ## Troubleshooting
69
+
70
+ | Symptom | Cause / fix |
71
+ | --- | --- |
72
+ | The toast says `HTTP 404` | The host half in memory is an older version (the host half loads once per process; the client half is re-fetched on every page load). **Restart the desktop app** |
73
+ | The toast says it handed the page to the system browser | Strategies 1 and 2 were both unavailable — check whether the host refused a window, or whether Edge/Chrome is installed |
74
+ | The entry is missing from the sidebar | Check that `@jaychang1989/dsh-webchat` is in the profile's `dsh.profile.bundles`, then restart the desktop app |
75
+
76
+ ## Development and tests
77
+
78
+ ```bash
79
+ node --test
80
+ ```
81
+
82
+ Thirteen cases: the host's two routes and its window-strategy selection, plus the browser half executed against a DOM stand-in (it mounts the entry, posts to the right route on click, and reports success, handoff and failure honestly).
83
+
84
+ There is no build step — `lib/index.js` and `lib/client.js` are the hand-written runtime, and the package has **zero runtime dependencies**. See [MAINTAINING.md](./MAINTAINING.md) (Chinese).
85
+
86
+ ## Origin and license
87
+
88
+ [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 window strategies, the entry-as-action, the toast — is new. See [NOTICE](./NOTICE) for the copyright and license statements.
package/README.md CHANGED
@@ -1,82 +1,88 @@
1
1
  # dsh-webchat
2
2
 
3
- Opens the official [chat.deepseek.com](https://chat.deepseek.com) web app from inside [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness): one sidebar entry, one button, and the official page itself.
3
+ **简体中文** | [English](./README.en.md)
4
4
 
5
- **It no longer reimplements the chat UI.** The official web app is the client — model picker, deep think, smart search, history and attachments all live there. This plugin opens it and nothing else.
5
+ 在 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 里一键打开 [chat.deepseek.com](https://chat.deepseek.com) 官方网页版。
6
6
 
7
- ## Why a window, and not a pane
7
+ **侧边栏一个入口,点一下,官方页面就开在一个窗口里。**
8
8
 
9
- Both embedding routes are closed, and that is a measurement rather than a choice:
9
+ 仅此而已。不做聊天界面,不做会话存储,不做 agent 工具——官方网页版本身就是完整的客户端,模型选择、深度思考、智能搜索、历史记录、附件上传都在里面。
10
10
 
11
- - `chat.deepseek.com` answers with **`Content-Security-Policy: frame-ancestors 'none'`**, so no iframe can host it;
12
- - the desktop build runs both windows with **`webviewTag: false`** and blocks `will-attach-webview`, so `<webview>` is unavailable too.
11
+ ## 为什么不嵌在面板里
13
12
 
14
- A real window is therefore the only faithful way to show the official page.
13
+ 三条路都被封死,这是实测结果而不是取舍:
15
14
 
16
- ## What opens, in order
17
-
18
- The button tries these in order and reports which one worked:
19
-
20
- 1. **`app-window`** — a window created by the DSH desktop process itself (the desktop app is Electron). An already-open one is focused instead of duplicated.
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.
15
+ | 方式 | 实测结果 |
16
+ | --- | --- |
17
+ | `<iframe>` | `chat.deepseek.com` 返回 `Content-Security-Policy: frame-ancestors 'none'`,浏览器直接拒绝渲染 |
18
+ | Electron `<webview>` | 桌面端两个窗口都是 `webviewTag: false`,且 `will-attach-webview` 被显式拦截 |
19
+ | 桌面端内置的 `WebContentsView` | 只服务平台页面(账号/充值),导航被限制在该 origin,第三方插件拿不到 |
23
20
 
24
- Sign in once; the window's own profile keeps the session.
21
+ 所以忠实呈现官方页面的唯一方式,就是开一个真实窗口。
25
22
 
26
- ## Install
23
+ ## 安装
27
24
 
28
25
  ```bash
29
26
  dsh plugin --profile desktop add @jaychang1989/dsh-webchat
30
27
  ```
31
28
 
32
- Or straight from this repository:
29
+ 或直接从仓库安装:
33
30
 
34
31
  ```bash
35
32
  dsh plugin --profile desktop add github:jaychang1989/dsh-webchat
36
33
  ```
37
34
 
38
- > The npm package is scoped because the unscoped `dsh-webchat` name belongs to the unmaintained upstream project. The scope does not change the plugin's identity: the entry id stays `webchat`.
35
+ > npm 包名带 scope,因为不带 scope 的 `dsh-webchat` 已被占用。scope 不影响插件身份:entry id 仍是 `webchat`。
39
36
 
40
- **Switching from upstream, or from this plugin's 0.3.x:** all of them insert the same `id: webchat` row, and one profile cannot hold the same entry id twice — boot fails on the duplicate. Remove the old package first, then add this one, then restart the desktop app.
37
+ 安装后**重启桌面端**。
41
38
 
42
- ## Use
39
+ ## 使用
43
40
 
44
- 1. Click the "DeepSeek 网页 / DeepSeek Web" entry in the sidebar.
45
- 2. Click "Open chat.deepseek.com" in the panel.
46
- 3. Sign in once in the window it opens; afterwards just use the official web app.
41
+ 1. 点侧边栏的「DeepSeek 网页」入口;
42
+ 2. 在弹出的窗口里登录一次 DeepSeek,之后登录态会保留。
47
43
 
48
- ## Difference from 0.3.x — this release deletes most of the plugin
44
+ 点击后右下角会有一条短提示说明结果。
49
45
 
50
- | 0.3.x | 0.4.0 |
51
- | --- | --- |
52
- | Drove the page with Playwright and re-rendered messages in a custom panel | Does not drive the page; opens it. Custom panel removed |
53
- | Five agent tools (`webchat_status/send/recover/import/transfer`) | All removed |
54
- | "Transfer to Harness", "recover from web", "import", "export" | All removed |
55
- | Local transcript store at `~/.dsh/dsh-webchat/transcripts.json` | Removed (only the window profile directory remains) |
56
- | Fifteen `/api/dsh-webchat/*` routes | Two: `state` and `open` |
57
- | Depended on `playwright-core`; the client half needed React and a bundler | **No runtime dependencies**; plain-DOM client, no build step |
58
- | ~3800 lines of build output | ~700 lines of hand-written JS |
46
+ ## 它怎么打开页面
47
+
48
+ 按顺序尝试,第一个成功的生效,提示条会告诉你实际用了哪一种:
49
+
50
+ 1. **应用窗口** —— 由 DSH 桌面端进程自己创建的窗口(桌面端就是 Electron)。已经开着就聚焦,不会重复开。
51
+ 2. **无边框窗口** —— Edge/Chrome 的 `--app=` 模式,使用独立 `--user-data-dir`(`~/.dsh/dsh-webchat/app-window`),因此登录态与你日常浏览器互不干扰。用于第 1 种拿不到 Electron 的场景。
52
+ 3. **系统默认浏览器** —— 兜底,保证点击一定有反应。
59
53
 
60
- ## Requirements
54
+ 三种都不需要额外配置。
61
55
 
62
- - DeepSeek Harness **0.2.0-rc.1 or a newer 0.2.x**
56
+ ## 环境要求
57
+
58
+ - DeepSeek Harness **0.2.0-rc.1 或更新的 0.2.x**
63
59
  - Node.js >= 22
64
- - Strategy 2 needs Edge or Chrome installed; strategy 1 does not
60
+ - 第 2 种方式需要本机装有 Edge 或 Chrome;第 1 种不需要
61
+
62
+ ## 已知限制
65
63
 
66
- ## Limitations
64
+ - 官方网页端有它自己的风控和登录流程,插件只负责把页面交出去,不介入其请求。
65
+ - 第 1 种方式创建的窗口由 DSH 进程拥有:如果你先关掉 DSH 主窗口、而它仍开着,DSH 不会退出(Electron 的 `window-all-closed` 语义)。把它一起关掉即可。
66
+ - 插件被重载时不会主动关闭已经打开的窗口——否则一次设置变更就会把你正在看的会话关掉。
67
67
 
68
- - The DeepSeek web front end applies its own rate limiting; the plugin hands the page over and does not mediate its login or requests.
69
- - A strategy-1 window is owned by the DSH process: if you close the DSH main window while it is still open, DSH will not quit (Electron's `window-all-closed`). Close that window too.
70
- - Reloading the plugin does not close a window that is already open, so a settings change cannot close the conversation you are reading.
68
+ ## 排查
71
69
 
72
- ## Development and tests
70
+ | 现象 | 原因 / 处理 |
71
+ | --- | --- |
72
+ | 提示条显示 `HTTP 404` | 宿主的旧版本还在内存里(宿主半区只在进程启动时加载,客户端半区每次刷新页面重新取)。**重启桌面端**即可 |
73
+ | 提示条显示「已交给系统默认浏览器打开」 | 前两种方式都不可用;看窗口是否被宿主拒绝,或本机没装 Edge/Chrome |
74
+ | 入口没出现在侧边栏 | 确认 profile 的 `dsh.profile.bundles` 里有 `@jaychang1989/dsh-webchat`,并重启桌面端 |
75
+
76
+ ## 开发与测试
73
77
 
74
78
  ```bash
75
- node --test # 11 cases: host routes, window-strategy selection, and the browser half mounted into a DOM stand-in
79
+ node --test
76
80
  ```
77
81
 
78
- There is no build step: `lib/index.js` and `lib/client.js` are the hand-written runtime. See [MAINTAINING.md](./MAINTAINING.md).
82
+ 13 个用例:宿主两条路由与窗口策略选择,以及浏览器半区在 DOM 桩里真实执行(挂载入口、点击后请求正确路由、成功/兜底/失败是否都如实提示)。
83
+
84
+ 没有构建步骤——`lib/index.js` 与 `lib/client.js` 就是手写的运行时代码,包内**零运行时依赖**。细节见 [MAINTAINING.md](./MAINTAINING.md)。
79
85
 
80
- ## License
86
+ ## 来源与许可
81
87
 
82
- [Apache-2.0](./LICENSE). Original copyright belongs to the authors of [xmuwenxiang/dsh-web-chat](https://github.com/xmuwenxiang/dsh-web-chat); see [NOTICE](./NOTICE).
88
+ [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
- # Note: the upstream package xmuwenxiang/dsh-web-chat inserts the same
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,14 +1,15 @@
1
1
  /**
2
2
  * dsh-webchat — browser half.
3
3
  *
4
- * One sidebar entry and one panel holding one button. The panel does not chat:
5
- * chat.deepseek.com cannot be framed (`frame-ancestors 'none'`) and this build
6
- * has `webviewTag: false`, so the official page opens in a window instead (see
7
- * lib/index.js). Everything here is plain DOM — no React, no build step.
4
+ * One sidebar entry. Clicking it opens the official DeepSeek web app in a
5
+ * window (see lib/index.js) and reports the outcome in a short toast. There is
6
+ * no panel and no in-GUI chat: chat.deepseek.com cannot be framed
7
+ * (`frame-ancestors 'none'`) and this build has `webviewTag: false`, so the
8
+ * official page can only live in a window of its own.
8
9
  *
9
- * The shell exposes no slot an external plugin can register into, so the entry
10
- * row and the center-column view are injected at the DOM level and self-heal
11
- * against React re-renders, exactly as the previous release did.
10
+ * Everything here is plain DOM — no React, no build step. The shell exposes no
11
+ * slot an external plugin can register into, so the entry row is injected at
12
+ * the DOM level and self-heals against React re-renders.
12
13
  */
13
14
  window.__ModuleLoader__.load({
14
15
  id: "@jaychang1989/dsh-webchat",
@@ -17,16 +18,10 @@ window.__ModuleLoader__.load({
17
18
  var exports = module.exports
18
19
 
19
20
  /** Routes served by lib/index.js. */
20
- var API = { state: '/api/dsh-webchat/state', open: '/api/dsh-webchat/open' }
21
- /** The page the launcher opens (mirrors PAGE_URL on the host half). */
22
- var PAGE_URL = 'https://chat.deepseek.com/'
21
+ var API = { open: '/api/dsh-webchat/open' }
23
22
  var ENTRY_ATTR = 'data-dsh-webchat-entry'
24
- var VIEW_ATTR = 'data-dsh-webchat-view'
25
- var ACTIVE_ATTR = 'data-dsh-webchat-active'
26
- var OTHER_ACTIVE_ATTRS = ['data-dsh-taskboard-active', 'data-dsh-ssh-active']
27
- var ACTIVATE_EVENT = 'dsh-panel-activate'
28
- var PANEL_NAME = 'webchat'
29
23
  var STYLE_ID = 'dsh-webchat-style'
24
+ var TOAST_ATTR = 'data-dsh-webchat-toast'
30
25
 
31
26
  var ZH = typeof navigator === 'object' && typeof navigator.language === 'string'
32
27
  && navigator.language.toLowerCase().indexOf('zh') === 0
@@ -35,66 +30,32 @@ window.__ModuleLoader__.load({
35
30
  var T = ZH
36
31
  ? {
37
32
  label: 'DeepSeek 网页',
38
- tooltip: '在窗口里打开 chat.deepseek.com',
39
- title: 'DeepSeek 网页',
40
- lede: '官方网页版就是完整的客户端:模型选择、深度思考、智能搜索、历史记录、附件上传都在里面。这里只负责把它打开。',
41
- open: '打开 chat.deepseek.com',
42
- reopen: '回到已打开的窗口',
43
- hint: '首次使用请在打开的这个窗口里登录 DeepSeek,之后登录态会保留。',
44
- checking: '检查中…',
45
- closed: '窗口未打开',
46
- opened: '窗口已打开',
33
+ tooltip: '打开 chat.deepseek.com',
47
34
  busy: '正在打开…',
48
- done: '已打开',
35
+ opened: '已打开 DeepSeek 网页',
36
+ handed: '已交给系统默认浏览器打开',
49
37
  failed: '打开失败',
50
38
  }
51
39
  : {
52
40
  label: 'DeepSeek Web',
53
- tooltip: 'Open chat.deepseek.com in a window',
54
- title: 'DeepSeek Web',
55
- lede: 'The official web app is the whole client: model picker, deep think, smart search, history and attachments all live there. This plugin only opens it.',
56
- open: 'Open chat.deepseek.com',
57
- reopen: 'Focus the open window',
58
- hint: 'Sign in once inside the window it opens; the session is kept afterwards.',
59
- checking: 'Checking…',
60
- closed: 'Window is closed',
61
- opened: 'Window is open',
41
+ tooltip: 'Open chat.deepseek.com',
62
42
  busy: 'Opening…',
63
- done: 'Opened',
43
+ opened: 'DeepSeek Web is open',
44
+ handed: 'Handed to the system default browser',
64
45
  failed: 'Could not open',
65
46
  }
66
47
 
67
- /** Panel and entry styling, scoped to this plugin's own attributes. */
48
+ /** Entry and toast styling, scoped to this plugin's own attribute. */
68
49
  var CSS = [
69
- "[data-pane='conversation'],[class*='centerCol']{position:relative}",
70
- '[' + VIEW_ATTR + ']{position:absolute;inset:0;display:none;z-index:60;background:var(--dsw-alias-bg-base)}',
71
- 'html[' + ACTIVE_ATTR + ']:not([data-dsh-taskboard-active]):not([data-dsh-ssh-active]) [' + VIEW_ATTR + ']{display:block}',
72
- "html[" + ACTIVE_ATTR + "]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-active]) [data-pane='conversation'] > :not([" + VIEW_ATTR + ']),',
73
- 'html[' + ACTIVE_ATTR + "]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-active]) [class*='centerCol'] > :not([" + VIEW_ATTR + ']){display:none !important}',
74
50
  '[' + 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}',
75
51
  '[' + ENTRY_ATTR + ']:hover{background:var(--dsw-specific-sidebar-nav-item-hover);color:var(--dsw-alias-label-primary)}',
76
- '[' + ENTRY_ATTR + '][data-active]{background:var(--dsw-specific-sidebar-nav-item-active);color:var(--dsw-alias-label-primary);font-weight:600}',
52
+ '[' + ENTRY_ATTR + '][disabled]{opacity:.55;cursor:progress}',
77
53
  '[' + ENTRY_ATTR + '] [data-part=icon]{display:inline-flex;align-items:center;justify-content:center;flex:none}',
78
54
  '[' + ENTRY_ATTR + '] [data-part=label]{overflow:hidden;text-overflow:ellipsis}',
79
55
  '[data-dsh-frame][data-sidebar-collapsed] [' + ENTRY_ATTR + ']{justify-content:center;padding:0}',
80
56
  '[data-dsh-frame][data-sidebar-collapsed] [' + ENTRY_ATTR + '] [data-part=label]{display:none}',
81
- '.dsh-wc-panel{box-sizing:border-box;display:flex;flex-direction:column;height:100%;min-height:0;padding:18px 20px;gap:14px;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family);overflow:auto}',
82
- '.dsh-wc-head{display:flex;align-items:center;gap:10px;flex:none}',
83
- '.dsh-wc-title{margin:0;font-size:16px;font-weight:700;letter-spacing:-.01em}',
84
- '.dsh-wc-chip{margin-left:auto;display:inline-flex;align-items:center;gap:7px;padding:4px 10px;border:1px solid var(--dsw-alias-border-l1);border-radius:999px;font-size:11.5px;color:var(--dsw-alias-label-secondary)}',
85
- '.dsh-wc-dot{width:7px;height:7px;border-radius:50%;background:var(--dsw-alias-label-tertiary);flex:none}',
86
- ".dsh-wc-dot[data-state=ok]{background:var(--dsw-alias-state-success,#22c55e)}",
87
- ".dsh-wc-dot[data-state=bad]{background:var(--dsw-alias-state-danger,#ef4444)}",
88
- ".dsh-wc-dot[data-state=busy]{background:var(--dsw-alias-state-info,#3b82f6)}",
89
- '.dsh-wc-card{margin:auto;max-width:520px;display:flex;flex-direction:column;align-items:center;gap:14px;text-align:center;padding:26px 22px;border:1px solid var(--dsw-alias-border-l1);border-radius:14px;background:var(--dsw-alias-bg-layer1,var(--dsw-alias-bg-elevated))}',
90
- '.dsh-wc-lede{margin:0;font-size:13px;line-height:1.65;color:var(--dsw-alias-label-secondary)}',
91
- '.dsh-wc-url{font-family:var(--dsw-font-mono,ui-monospace,Consolas,monospace);font-size:12px;color:var(--dsw-alias-label-tertiary)}',
92
- '.dsh-wc-button{display:inline-flex;align-items:center;gap:6px;padding:9px 18px;font-size:13.5px;font-weight:600;border:1px solid transparent;border-radius:9px;background:var(--dsw-alias-state-business-primary,#4f6bfa);color:#fff;cursor:pointer}',
93
- '.dsh-wc-button:hover:not(:disabled){filter:brightness(1.08)}',
94
- '.dsh-wc-button:disabled{opacity:.5;cursor:not-allowed}',
95
- '.dsh-wc-hint{margin:0;font-size:12px;line-height:1.6;color:var(--dsw-alias-label-tertiary)}',
96
- '.dsh-wc-result{margin:0;min-height:18px;font-size:12px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}',
97
- ".dsh-wc-result[data-state=bad]{color:var(--dsw-alias-state-danger,#ef4444)}",
57
+ '[' + TOAST_ATTR + ']{position:fixed;right:20px;bottom:20px;z-index:200;max-width:420px;padding:10px 14px;border-radius:10px;background:var(--dsw-alias-bg-elevated);border:1px solid var(--dsw-alias-border-l2);box-shadow:0 8px 24px rgba(0,0,0,.18);font-size:13px;color:var(--dsw-alias-label-primary);overflow-wrap:anywhere}',
58
+ '[' + TOAST_ATTR + '][data-state=bad]{border-color:var(--dsw-alias-state-danger,#ef4444);color:var(--dsw-alias-state-danger,#ef4444)}',
98
59
  ].join('\n')
99
60
 
100
61
  /** Inject the stylesheet once. */
@@ -106,28 +67,27 @@ window.__ModuleLoader__.load({
106
67
  document.head.appendChild(tag)
107
68
  }
108
69
 
109
- /** Tiny observable for the panel's open state. */
110
- function createController() {
111
- var open = false
112
- var listeners = new Set()
113
- var notify = function () {
114
- listeners.forEach(function (listener) {
115
- try {
116
- listener()
117
- } catch (error) {
118
- console.warn('[dsh-webchat] listener failed:', error)
119
- }
120
- })
121
- }
122
- return {
123
- isOpen: function () { return open },
124
- subscribe: function (listener) {
125
- listeners.add(listener)
126
- return function () { listeners.delete(listener) }
127
- },
128
- toggle: function () { open = !open; notify() },
129
- close: function () { if (!open) return; open = false; notify() },
130
- }
70
+ /** Show one transient message next to the panel corner. */
71
+ function toast(text, state) {
72
+ var previous = document.querySelector('[' + TOAST_ATTR + ']')
73
+ if (previous !== null) previous.remove()
74
+ var box = document.createElement('div')
75
+ box.setAttribute(TOAST_ATTR, '')
76
+ if (state !== undefined) box.setAttribute('data-state', state)
77
+ box.textContent = text
78
+ document.body.appendChild(box)
79
+ setTimeout(function () {
80
+ if (box.isConnected) box.remove()
81
+ }, state === 'bad' ? 8000 : 3000)
82
+ }
83
+
84
+ /** Call the host route; a non-JSON or non-2xx answer becomes an error field. */
85
+ async function openPage() {
86
+ const response = await fetch(API.open, { method: 'POST', headers: { accept: 'application/json' } })
87
+ let payload = {}
88
+ try { payload = await response.json() } catch (error) { payload = {} }
89
+ if (!response.ok && payload.error === undefined) payload.error = 'HTTP ' + response.status
90
+ return payload
131
91
  }
132
92
 
133
93
  /** The sidebar shell root, or undefined while it is not mounted. */
@@ -150,7 +110,7 @@ window.__ModuleLoader__.load({
150
110
  }
151
111
 
152
112
  /** Build the entry row. */
153
- function createEntry(controller) {
113
+ function createEntry() {
154
114
  var entry = document.createElement('button')
155
115
  entry.type = 'button'
156
116
  entry.setAttribute(ENTRY_ATTR, '')
@@ -166,7 +126,26 @@ window.__ModuleLoader__.load({
166
126
  label.textContent = T.label
167
127
  entry.appendChild(icon)
168
128
  entry.appendChild(label)
169
- entry.addEventListener('click', function () { controller.toggle() })
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
+
170
149
  return entry
171
150
  }
172
151
 
@@ -186,9 +165,9 @@ window.__ModuleLoader__.load({
186
165
  }
187
166
 
188
167
  /** Mount the sidebar entry with its self-healing observers. */
189
- function mountSidebarEntry(controller) {
168
+ function mountSidebarEntry() {
190
169
  if (document.querySelector('[' + ENTRY_ATTR + ']') !== null) return function () {}
191
- var entry = createEntry(controller)
170
+ var entry = createEntry()
192
171
  var root
193
172
  var placed = false
194
173
 
@@ -221,210 +200,22 @@ window.__ModuleLoader__.load({
221
200
  if (!root.contains(entry)) placed = placeEntry(root, entry)
222
201
  })
223
202
 
224
- var syncActive = function () {
225
- if (controller.isOpen()) entry.dataset.active = 'true'
226
- else delete entry.dataset.active
227
- }
228
- var unsubscribe = controller.subscribe(syncActive)
229
- syncActive()
230
203
  tryPlace()
231
204
 
232
205
  return function () {
233
206
  waitObserver.disconnect()
234
207
  rootObserver.disconnect()
235
- unsubscribe()
236
208
  entry.remove()
237
209
  }
238
210
  }
239
211
 
240
- /** The center column that hosts takeover panels. */
241
- function conversationColumn() {
242
- return document.querySelector('[data-pane="conversation"], [class*="centerCol"]') || undefined
243
- }
244
-
245
- /** Call one host route. */
246
- async function call(path, method) {
247
- const response = await fetch(path, { method: method || 'GET', headers: { accept: 'application/json' } })
248
- let payload = {}
249
- try { payload = await response.json() } catch (error) { payload = {} }
250
- if (!response.ok && payload.error === undefined) payload.error = 'HTTP ' + response.status
251
- return payload
252
- }
253
-
254
- /** Build the panel body once; returns the parts the caller updates. */
255
- function buildPanel(container) {
256
- var panel = document.createElement('div')
257
- panel.className = 'dsh-wc-panel'
258
-
259
- var head = document.createElement('div')
260
- head.className = 'dsh-wc-head'
261
- var title = document.createElement('h2')
262
- title.className = 'dsh-wc-title'
263
- title.textContent = T.title
264
- var chip = document.createElement('span')
265
- chip.className = 'dsh-wc-chip'
266
- var dot = document.createElement('span')
267
- dot.className = 'dsh-wc-dot'
268
- var chipText = document.createElement('span')
269
- chipText.textContent = T.checking
270
- chip.appendChild(dot)
271
- chip.appendChild(chipText)
272
- head.appendChild(title)
273
- head.appendChild(chip)
274
-
275
- var card = document.createElement('div')
276
- card.className = 'dsh-wc-card'
277
- var lede = document.createElement('p')
278
- lede.className = 'dsh-wc-lede'
279
- lede.textContent = T.lede
280
- var url = document.createElement('p')
281
- url.className = 'dsh-wc-url'
282
- url.textContent = PAGE_URL
283
- var button = document.createElement('button')
284
- button.type = 'button'
285
- button.className = 'dsh-wc-button'
286
- button.textContent = T.open
287
- var hint = document.createElement('p')
288
- hint.className = 'dsh-wc-hint'
289
- hint.textContent = T.hint
290
- var result = document.createElement('p')
291
- result.className = 'dsh-wc-result'
292
- card.appendChild(lede)
293
- card.appendChild(url)
294
- card.appendChild(button)
295
- card.appendChild(hint)
296
- card.appendChild(result)
297
-
298
- panel.appendChild(head)
299
- panel.appendChild(card)
300
- container.appendChild(panel)
301
-
302
- var mark = function (state, text) {
303
- dot.setAttribute('data-state', state)
304
- chipText.textContent = text
305
- }
306
- var idle = function () {
307
- button.disabled = false
308
- button.textContent = T.open
309
- }
310
-
311
- var refresh = async function () {
312
- try {
313
- var state = await call(API.state)
314
- mark(state.appWindowOpen ? 'ok' : '', state.appWindowOpen ? T.opened : T.closed)
315
- button.textContent = state.appWindowOpen ? T.reopen : T.open
316
- } catch (error) {
317
- mark('bad', T.closed)
318
- }
319
- }
320
-
321
- button.addEventListener('click', async function () {
322
- button.disabled = true
323
- button.textContent = T.busy
324
- mark('busy', T.busy)
325
- result.removeAttribute('data-state')
326
- result.textContent = ''
327
- try {
328
- var outcome = await call(API.open, 'POST')
329
- if (outcome.ok) {
330
- var via = String(outcome.via || '')
331
- mark('ok', T.done)
332
- result.textContent = via === 'system-browser'
333
- ? (ZH ? '已交给系统默认浏览器打开。' : 'Handed to the system default browser.')
334
- : (ZH ? '已打开应用窗口:' + via : 'Opened an app window: ' + via)
335
- } else {
336
- mark('bad', T.failed)
337
- result.setAttribute('data-state', 'bad')
338
- result.textContent = String(outcome.error || outcome.via || T.failed)
339
- }
340
- } catch (error) {
341
- mark('bad', T.failed)
342
- result.setAttribute('data-state', 'bad')
343
- result.textContent = error instanceof Error ? error.message : String(error)
344
- }
345
- idle()
346
- await refresh()
347
- })
348
-
349
- return { refresh: refresh }
350
- }
351
-
352
- /** Mount the panel view into the center column and bind its visibility. */
353
- function mountPanel(controller) {
354
- var container
355
- var panel
356
-
357
- var ensure = function () {
358
- if (container !== undefined) {
359
- if (container.isConnected) return
360
- container.remove()
361
- container = undefined
362
- panel = undefined
363
- }
364
- var column = conversationColumn()
365
- if (column === undefined) return
366
- container = document.createElement('div')
367
- container.setAttribute(VIEW_ATTR, '')
368
- container.setAttribute('data-dsh-plugin', 'webchat')
369
- column.appendChild(container)
370
- panel = buildPanel(container)
371
- void panel.refresh()
372
- }
373
-
374
- var waitObserver = new MutationObserver(function () { ensure() })
375
-
376
- var applyActive = function () {
377
- if (controller.isOpen()) {
378
- OTHER_ACTIVE_ATTRS.forEach(function (attr) { document.documentElement.removeAttribute(attr) })
379
- document.documentElement.setAttribute(ACTIVE_ATTR, '')
380
- document.dispatchEvent(new CustomEvent(ACTIVATE_EVENT, { detail: PANEL_NAME }))
381
- ensure()
382
- if (panel !== undefined) void panel.refresh()
383
- } else {
384
- document.documentElement.removeAttribute(ACTIVE_ATTR)
385
- }
386
- }
387
- var onOtherActivate = function (event) {
388
- var detail = event.detail
389
- if ((detail === 'taskboard' || detail === 'ssh') && controller.isOpen()) controller.close()
390
- }
391
- var SIDEBAR_ROW_SELECTOR = '[class*="sessionRow"], [class*="projectRow"], [class*="searchResultRow"], [class*="searchResultWorkspace"], [class*="newSession"]'
392
- var onClickSidebarRow = function (event) {
393
- if (!controller.isOpen()) return
394
- var target = event.target
395
- if (target === null || target.closest === undefined) return
396
- if (target.closest(SIDEBAR_ROW_SELECTOR) !== null) controller.close()
397
- }
398
-
399
- document.addEventListener('click', onClickSidebarRow, true)
400
- document.addEventListener(ACTIVATE_EVENT, onOtherActivate)
401
- var unsubscribe = controller.subscribe(applyActive)
402
- applyActive()
403
- ensure()
404
- waitObserver.observe(document.body, { childList: true, subtree: true })
405
-
406
- return function () {
407
- document.removeEventListener('click', onClickSidebarRow, true)
408
- document.removeEventListener(ACTIVATE_EVENT, onOtherActivate)
409
- waitObserver.disconnect()
410
- unsubscribe()
411
- document.documentElement.removeAttribute(ACTIVE_ATTR)
412
- if (container !== undefined) container.remove()
413
- container = undefined
414
- panel = undefined
415
- }
416
- }
417
-
418
- /** Mount the entry and the panel. */
212
+ /** Mount the entry. */
419
213
  function apply(ctx) {
420
214
  ensureStyle()
421
- var controller = createController()
422
- var disposers = [mountSidebarEntry(controller), mountPanel(controller)]
215
+ var dispose = mountSidebarEntry()
423
216
  ctx.effect(function () {
424
- return function () {
425
- disposers.splice(0).forEach(function (dispose) { dispose() })
426
- }
427
- }, 'dsh-webchat: ui mounts')
217
+ return function () { dispose() }
218
+ }, 'dsh-webchat: sidebar entry')
428
219
  }
429
220
 
430
221
  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 load, and the
82
- * import stays inside the call so a failed probe falls through to the next
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 this process has no BrowserWindow to offer.
106
+ * @throws with every probe failure joined, when neither path yields a window API.
86
107
  */
87
108
  async function electronApi() {
88
- const electron = await import('electron')
89
- if (electron === null || typeof electron !== 'object' || electron.BrowserWindow === undefined) {
90
- throw new Error('electron BrowserWindow is unavailable in this process')
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
- return electron
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
- return record(true, typeof detail === 'string' && detail !== '' ? detail : strategy.via)
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: error instanceof Error ? error.message : String(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.0",
4
- "description": "Opens the official DeepSeek web app (chat.deepseek.com) in a window from inside DeepSeek Harness: one sidebar entry, one button, no reimplementation — the official page keeps its model picker, deep think, smart search, history and attachments. Fork of xmuwenxiang/dsh-web-chat, reduced to a launcher.",
3
+ "version": "0.4.2",
4
+ "description": "Opens the official DeepSeek web app (chat.deepseek.com) in a window from DeepSeek Harness: one sidebar entry that opens the page directly, so the official client keeps its model picker, deep think, smart search, history and attachments. 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.zh.md"
59
+ "README.en.md"
60
60
  ],
61
61
  "scripts": {
62
62
  "test": "node --test"
package/README.zh.md DELETED
@@ -1,82 +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. 点面板里的「打开 chat.deepseek.com」;
46
- 3. 在弹出的窗口里登录一次,之后正常使用官方网页版。
47
-
48
- ## 与 0.3.x 的差异(这是一次大幅删减)
49
-
50
- | 0.3.x | 0.4.0 |
51
- | --- | --- |
52
- | 用 Playwright 驱动网页,自研聊天面板重新渲染消息 | 不再驱动网页,直接开官方页面;自研面板删除 |
53
- | 5 个 agent 工具(`webchat_status/send/recover/import/transfer`) | 全部删除 |
54
- | 「转移到 harness」「从网页恢复」「导入」「导出」 | 全部删除 |
55
- | 本地会话存储 `~/.dsh/dsh-webchat/transcripts.json` | 删除(只留窗口 profile 目录) |
56
- | 15 条 `/api/dsh-webchat/*` 路由 | 只剩 2 条:`state` / `open` |
57
- | 依赖 `playwright-core`,客户端产物需 React + 构建器 | **零运行时依赖**,客户端为纯 DOM,无构建步骤 |
58
- | 约 3800 行(构建产物) | 约 700 行(手写 JS) |
59
-
60
- ## 环境要求
61
-
62
- - DeepSeek Harness **0.2.0-rc.1 或更新的 0.2.x**
63
- - Node.js >= 22
64
- - 第 2 种策略(无边框窗口)需要本机装有 Edge 或 Chrome;第 1 种策略不需要
65
-
66
- ## 限制
67
-
68
- - 官方网页端受 DeepSeek 官方风控;页面打不开时插件只负责把页面交出去,不介入其登录或请求。
69
- - 第 1 种策略创建的窗口由 DSH 进程拥有:若你先关掉 DSH 主窗口、而它仍开着,DSH 不会退出(Electron 的 `window-all-closed` 语义)。关掉该窗口即可。
70
- - 插件被重载时不会主动关闭已经打开的窗口(避免一次设置变更就关掉你正在看的会话)。
71
-
72
- ## 开发与测试
73
-
74
- ```bash
75
- node --test # 11 个用例:宿主路由、窗口策略选择、浏览器半区在 DOM 桩里的挂载与按钮行为
76
- ```
77
-
78
- 没有构建步骤:`lib/index.js` 与 `lib/client.js` 就是手写的运行时代码。详见 [MAINTAINING.md](./MAINTAINING.md)。
79
-
80
- ## License
81
-
82
- [Apache-2.0](./LICENSE) — 原始版权归 [xmuwenxiang/dsh-web-chat](https://github.com/xmuwenxiang/dsh-web-chat) 作者所有,详见 [NOTICE](./NOTICE)。