DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

Lumos0121 /

Lumos0121/dsh-deepseek-chat

Verified

DSH Desktop sidebar plugin: open chat.deepseek.com in the right sidebar or the system browser, read the conversation back into the current session, and edit its own hover texts. 91 offline tests.

★ 1 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHubProject homepage
READMESource: main@114b20e6

dsh-deepseek-chat

test license: MIT

一个 DSH Desktop 插件:在侧边栏底部加一个按钮,点它弹出五个选项 —— 「在右侧边栏内打开」「在系统浏览器打开」负责打开 https://chat.deepseek.com/, 「读取右侧边栏内对话」「读取系统浏览器对话」负责把那边对话的内容一键送进当前会话, 最后一项「自定义提示词」让你把那几段鼠标悬停提示改成自己的话。

五项菜单在浅色与深色主题下的样子

只想用:到 Releases 下载 dsh-deepseek-chat-1.0.0.zip → 解压到本机磁盘(别放 OneDrive/网络盘)→ 双击解压出来的 install.cmd → 完全退出并重开 DSH Desktop。翻车点见「安装」。

想改它:

git clone https://github.com/Lumos0121/dsh-deepseek-chat.git
cd dsh-deepseek-chat
npm test        # 91 条离线用例:不联网、不碰注册表、不需要装 DSH

改完 lib/client.js 必须重启 DSH —— 客户端产物是在页面启动时编进 window.__DSH_BOOT__ 的, 热重载看不到它。维护者要发布新版,见「发布到 GitHub」。


它碰什么、不碰什么(用之前先看这段)

  • 插件自己不连任何服务器:产物里出现的地址只有 https://chat.deepseek.com/ (lib/index.js 的 CHAT_URL、lib/client.js 的 SHARE_URL_PREFIX)—— 没有遥测、没有统计、 没有第三方依赖,npm test 也完全离线。那些 /api/v0/... 请求都是那个网页自己发的, 用的是你在这个页面里的登录态。
  • 两个读取选项会走网页版自己的「分享」流程:读取时会 POST /api/v0/share/create 给这段对话 新建一条公开的分享链接(和你手点「分享」是同一条路,任何人拿到链接都能看这篇对话); 如果页面上已经有一条属于当前会话的链接,就直接用那条。不想留链接就别用这两个选项, 用过之后可以到网页版的分享记录里删掉。
  • 登录令牌不落地:插件只在 chat.deepseek.com 那个页面 / <webview> 的上下文里用它 (localStorage.userToken 或 cookie),不写文件、不写日志,也不发给 chat.deepseek.com 以外的任何地方。
  • 你自己改的悬停提示词只存在本机 localStorage(键 dsh-deepseek-chat:texts),不随包分发。
  • 这不是 DeepSeek 官方项目。读取靠的是网页版内部接口(/api/v0/chat/history_messages、 /api/v0/share/create、/api/v0/share/content、/api/v0/share/list)与 DOM 文案启发式, 网页一改版就可能失效;失效时插件会如实报错(见「失败怎么表现」),不假装成功。

点击之后会发生什么

点一下侧边栏底部的气泡图标 → 就地展开五个选项(再点一下收起,Esc 也可以):

选项 做什么 实现
在右侧边栏内打开 DSH 应用内、右侧栏里内嵌打开 ctx.sidebarRight.openTab('browser', { params: { url } });这条不可用时不假装成功,退回系统浏览器并如实播报「右侧栏不可用」
在系统浏览器打开 用系统默认浏览器打开(登录态由浏览器自己保存) window.open(url, "_blank") → Electron 主进程的 setWindowOpenHandler → shell.openExternal
读取右侧边栏内对话 直接用网页版自己的接口取整篇正文并建分享链接(不靠滚动位置),连同标题一起发进当前会话;接口不可用时退回页面点「分享」 见下方「读取对话」
读取系统浏览器对话 从剪贴板取你复制的分享链接(或整段正文)发进当前会话 同上(系统浏览器没有可编程接口,这条是半自动的)
自定义提示词 改上面几段悬停提示(存在本机,保存后即时生效) 见下方「自定义提示词」

五个选项各自带自己的兜底链,任何一步失败都不会假装成功:

  • 右侧栏:ctx.sidebarRightTabs.get('browser') 存在 + ctx.sidebarRight.openTab 是函数 → 两者都过才调用;否则(例如此地不是 Desktop、或宿主没启右侧栏浏览器包)直接落到系统浏览器, 并在无障碍状态位里写清「右侧栏不可用,已在系统浏览器中打开」。
  • 系统浏览器:window.open → 自定义协议 deepseek-chat://open(安装脚本注册到 HKCU\Software\Classes,由 Windows 去开浏览器)→ 动态插入 <a target="_blank"> 并点击 → 全失败时只在 role="status" 里如实写「未能自动打开」,提示手动访问。

window.open 之所以有效,是因为 DSH 的 Electron 主进程在主窗口上装了这样一个处理函数 (源码 createWindow() 里):

window.webContents.setWindowOpenHandler(({ url }) => {
  if (["http:", "https:"].includes(new URL(url).protocol)) shell.openExternal(url);
  return { action: "deny" };
});

也就是说这个 window.open 会被转交给操作系统,由默认浏览器打开,页面上不会弹出多余窗口。

首选路(右侧栏)的 API 不是猜的

都是本机 app.asar 里的第一方证据:

  • 提供方 @deepseek-ai/dsh-client-ui-sidebar-right 的插件体里逐字写着 ctx.reflect.provide("sidebarRightTabs", tabs) 与 ctx.reflect.provide("sidebarRight", controller);
  • 导航控制器的方法签名是 openTab(kind, options = {})(options = 摆放位置 + 该 kind 的导航参数);
  • 官方浏览器包 @deepseek-ai/dsh-client-ui-sidebar-browser 的 README 明确告诉客户端插件: "Client 插件可以调用 ctx.sidebarRight.openTab('browser', { params: { url } }) 打开 tab";
  • DSH 自己的 ui-chat 也是这么用的(ctx.get("sidebarRightTabs")?.get("browser") !== void 0 判断页签类型存在,再 ctx.sidebarRight.openTab("browser", { params: { url } }))。

为什么"内嵌"当初不行、现在行

聊天站在浏览器里没法内嵌:它的响应头带 Content-Security-Policy: frame-ancestors 'none'(本机实测 HTTP 200), 普通 <iframe> 在浏览器层面就被拒绝。DSH 右侧栏在 Web profile 用的正是 iframe,所以那条路走不通; 而 Desktop 用的是 Electron <webview> —— 独立 guest 进程,不受宿主页面 CSP 约束,因此能开。

如果你在浏览器里打开 DSH 的 Web 界面(而不是 Desktop 应用)点这个按钮, 右侧栏会用 iframe,聊天站多半会显示加载失败 —— 那是 frame-ancestors 'none' 的正常结果, 请改用右侧栏工具栏里的「在系统浏览器中打开」。

登录态:右侧栏的分区是进程内的,重启后不会被保留(官方文档原话:"Cookies and Web storage do not survive application restart")。这是已知且可接受的取舍;需要持久登录态时用回退路(系统浏览器)。


读取对话:把网页端内容送进当前会话

选项三:读取右侧边栏内对话(全自动)

右侧栏浏览器在 Desktop 端就是同一个 document 里的 <webview> 元素(第一方选择器 webview[data-sidebar-browser-frame],证据见下),所以插件能直接:

  1. document.querySelector('webview[data-sidebar-browser-frame]'),再用 getURL() 确认它确实是 chat.deepseek.com;
  2. guest.executeJavaScript("(" + readGuestScript.toString() + ")(" + JSON.stringify({shareId}) + ")", true) —— 这段函数被送进 guest 页面里执行;参数可选,宿主只在"手里已经有分享 id"时才递(见下文第五条路)。

主路:网页版接口(为什么不靠 DOM 抠字)。对话页和分享页都用虚拟列表(ds-virtual-list), 只有「当前滚到的那几条」在 DOM 里,整篇正文根本抓不全;而网页版自己的接口一次就给全文:

  • 正文有两个来源,先试页面自己的 IndexedDB 缓存,再试接口:
    • 缓存(首选):网页版把每个会话的历史应答写进 IndexedDB 库 deepseek-chat 的仓库 history-message(记录形如 {key: 会话 id, data: 应答, frontendVersion})。这条路不要令牌、 不看 DOM,正好绕开"这一栏的 token 只在内存里"这个死结;
    • 接口(备选):GET /api/v0/chat/history_messages?chat_session_id=<id>,只在缓存里真拿到 版本号时才追加 &cache_version=…&cache_reset_at=…(以前无条件拼字面量 null,已删掉);头带 x-client-bundle-id: com.deepseek.chat、x-client-platform: web、x-client-version: 2.5.0、 x-client-locale(形如 zh_CN)、x-client-timezone-offset(秒,东八区 28800;以前错写成 小时)、authorization: Bearer <token>(有才带,没有就走 cookie),并带 credentials: "include";两层信封 {code,msg,data:{biz_code,biz_msg,biz_data}} 全为 0 才算成功, 正文按角色拼成 【我】/【DeepSeek】(每条约 6000 字封顶、最多 400 条),标题取 chat_session.title;
  • 登录令牌:网页版把它放在 localStorage.userToken(值多半是 {"value":"...","__version":"0"} 这样的 JSON,也可能是裸串),会话 id 从 location.pathname 的 /s/<id> 里取。但 AppKit 的持久化开关 persistToken 为 false 时它根本不写 localStorage(token 只活在内存的 zustand store 里,任何 window.__* 全局都不暴露它)—— 所以"读不到令牌"是常态而不是异常:这时照旧试一次 cookie 路, 并用钩子从页面自己的请求头上抄一份 authorization 兜底;
  • 正文不在 message.content 上,而在 message.fragments[] 里:只有 REQUEST(提问)、RESPONSE、 TEMPLATE_RESPONSE 三类算正文,THINK(思维链)、FILE(附件)、TOOL_SEARCH/TOOL_OPEN/TOOL_FIND (联网与工具)都要跳过 —— 依据是 bundle 里的 INFO_TYPES: ["REQUEST","RESPONSE","TEMPLATE_RESPONSE"] 与 RESPONSE_TYPES / FILE_TYPES / TOOL_TYPES 三张表;同一份 payload 里若正文直接挂在 content 上(老形状)也照样认;
  • POST /api/v0/share/create,体为 {chat_session_id, message_ids},取回 share_id 拼成 https://chat.deepseek.com/share/<share_id>。服务端对 message_ids 有成对要求(biz_code 6 MESSAGES_MUST_APPEAR_IN_PAIRS),一次失败就只用「提问 + 回复」成对的子集重试一次;
  • 令牌过期、页面没有 fetch、网络断、接口抛错,全部如实写进下面的诊断(payload.api),绝不把这次点击挂死 (每次请求 15 秒超时)。

备路:DOM 点按钮。接口没给出链接时(比如这一栏没登录、接口被 WAF 拦),才回到页面上去点: 先扫已存在的 https://chat.deepseek.com/share/... 链接;没有就先对消息条派发 mouseover (有些操作条只在悬停时渲染),再按文案找分享入口 —— 文案、aria-label、title、data-testid、class 逐项匹配(图标按钮只有 aria-label),必要时先点开「更多 / …」菜单再找;找到后依次派发 pointerdown/mousedown/pointerup/mouseup/click(有的控件只认指针事件),最多等 3 秒(20×150ms) 等链接出现;仍没有就按用户实测的那三下接着点:① 右上角只有图标的「分享」→ ② 弹窗右下角的 「创建分享链接」→ ③ 中间的「创建并复制」。三步各自等一次链接出现;②那一下页面自己就会发 /api/v0/share/create,钩子通常在第二步就把 share_id 接住了,③那一下则是页面把链接写进剪贴板。 两个按钮分别用 CREATE_LINK_TEXT / COPY_LINK_TEXT 认(clickAndWait() 负责「点一下、等一等」), 绝不把「创建」和「复制」当成同一颗按钮。

补路:页面自己的出口(钩子)。点按钮这一步有个坑:网页版弹窗上那个「创建并复制」只调 navigator.clipboard.writeText,没有隐藏 textarea 兜底(bundle 里那个复制小工具 aY() 有兜底, 但这个按钮没用它)。主文档没焦点时 writeText 一定抛错,于是页面弹「复制失败」,而链接其实已经生成 并显示在弹窗里了 —— 这就是"链接看得见,却复制不出来"的原因。所以脚本一进页面(installCaptures(), 在任何点击之前)就装好三个只读钩子,原样保留页面行为、绝不吞掉它的响应:

  • navigator.clipboard.writeText:先把参数记下来(页面自己失败也无所谓),再 best-effort 调原函数;
  • document.execCommand("copy"):记下选区文本 / 活动元素的 value(老式复制路);
  • window.fetch:命中 /api/v0/share/create 时 response.clone().json() 取 biz_data.share_id (页面自己的请求也算),顺手把页面请求头里的 authorization 抄下来当 token 兜底 —— 侧栏这一栏 可能只把 token 留在内存里(AppKit 的 persistToken: false),localStorage 里根本没有 userToken。

从任意一处接到 share/<id> 或 share_id=<id> 都会规范化成 https://chat.deepseek.com/share/<id>, 并在播报里标明来路(payload.share = "captured" / source = "capture")。同理,没有 token 时不再 直接放弃:改用 credentials: "include" 的 cookie 路试一次(页面本来就是这么登录的);正文没取到、 但抄到了页面自己的 token,会再用它补一次全文。

第五条路:分享链接 / 分享页(公开接口,不需要登录)。上面几条路都要"这一栏已登录", 但分享链接本身是公开的:网页版读分享内容走的是 GET /api/v0/share/content?share_id=<id> (bundle 里 fetchShareContent() 逐字如此),正文在 data.biz_data.messages、标题在 biz_data.title, 信封与 history_messages 完全一样。本机实测:不带任何 cookie、不带 token 也返回 200 / biz_code 0, 所以:

  • 脚本发现自己在分享页上(location.pathname 匹配 /share/<id>)、或宿主把分享 id 递进来 (readGuestScript({ shareId }))时,直接读这个接口拿整篇正文 —— 这条分支用 payload.share = "share-page" / source = "share" 标注,播报写「链接就是分享页自己的地址, 正文由分享接口取回(这个接口不需要登录)」;
  • 正文兜底拿到、页面上又有分享链接时,也会用它把整篇补回来(播报写「正文由分享接口补上(N 条)」);
  • 只能在 chat.deepseek.com 源的 guest 里发这个请求:实测响应头没有 access-control-allow-origin (只有 content-type 与 x-content-type-options),主文档(127.0.0.1 源)直接 fetch 必被浏览器拦。 所以宿主侧新增 readShareViaGuest(shareId):右侧栏里恰好开着 chat.deepseek.com 时借它发一次同源请求, 换不回正文就返回 null(不假装有正文)。

不论走哪条路,document.body.innerText 都会作为正文兜底带回来(截 120000 字);拿不到链接时另附 页面上候选控件的文案/属性清单(tag|文案|aria=|title=|testid=|class=)当诊断线索。

  1. 回到宿主侧:把「来源 + 标题 + 分享链接 + 正文」拼成一条消息,经 sessions.binding(sessionId).session.prompt([{ type: "text", text }], "queue") 投进当前会话 —— 这正是 composer 自己走的那条路,所以效果和你手打一条消息完全一样。走接口取到的内容会标注 「分享链接与正文都由网页版接口取得,与页面滚动位置无关」;从页面出口接住的会标注 「链接是从页面自己的请求/剪贴板里接住的」。 顺带把链接写进剪贴板(本窗口写不成就请内嵌页自己写一次,因为焦点常在内嵌页那一帧), 写没写进去会如实播报:成功是「链接已复制到剪贴板」,失败是「链接没能写进剪贴板,请手动复制」, 不再谎报。

sessionId 从哪来:插件在 conversation.input.overlay(会话作用域槽位)里额外注册了一个渲染 null 的探针组件, 它的 standardProps 里带 sessionId,挂上就把当前活动会话 id 记下来,点读取时用它投递。 sidebarRight / sessions 一概不进 inject,都用 ctx.get() 探测(原因见「两条硬纪律」)。

选项四:读取系统浏览器对话(半自动)

系统浏览器那边 DSH 没有任何桥:没有 CDP(Edge 没开 --remote-debugging-port)、UI Automation 拿不到文档树 (Chromium 只有启用无障碍后才暴露)、IndexedDB / Local Storage 里也没有可读的正文。 插件不会去解密浏览器 cookie 或会话令牌(那是账号凭据,而且新版 Chromium 的 App-Bound Encryption 就是专门挡这个的)。

所以这条路的正确用法是:你在网页版点一下「分享」→ 复制链接,再点这个选项。插件读剪贴板后:

系统浏览器里那三下(分享图标 → 创建分享链接 → 创建并复制)只能你手点:DSH 没有任何桥能进外部浏览器的页面 (没有调试端口、UIA 拿不到文档树),插件也没法替你在 Edge 里点击。同一个流程在右侧栏内嵌页里是插件 自己点的(见选项三)—— 想在 DSH 里全自动,就用选项三。 剪贴板里一旦有链接,剩下的全自动:插件会自己去取整篇正文(必要时自己请右侧栏把这个分享页打开), 再把「链接 + 正文」一起发进当前会话当上下文。

剪贴板内容 行为
https://chat.deepseek.com/share/... 取链接 + 剩余正文一起投递 ✅;只有链接、没有正文时:右侧栏恰好开着 chat.deepseek.com 就借它调公开的分享接口取回整篇;没开着则插件自己请右侧栏把这个分享页打开(sidebarRight.openTab('browser', {params:{url}}),等最多 9 秒),再借它取回正文,播报写「插件已在右侧栏打开这个分享页,正文是借右侧栏那个页面取回来的:…」;右侧栏服务压根不可用(例如 Web 端)才只发链接,并如实说「没能借到右侧栏那个页面」+ 怎么办
别的 chat.deepseek.com 链接(不是分享链接) 不投递,如实说「这不是分享链接」
少于 40 字 不投递,提示「剪贴板内容太短」
一个本机路径 / 单个文件名(一眼不是对话,比如 D:\…\dsh-deepseek-chat) 不投递,如实说「剪贴板里没有分享链接」
其它长文本 整段当正文投递,并标注「来自剪贴板,没有检测到分享链接」

为什么优先要分享链接而不是抓正文?分享页不需要登录,任何能上网的读者都能打开它拿到完整对话; 而且 GET /api/v0/share/content 是公开接口(本机实测不带 cookie 也 200),所以只要手里有分享链接, 正文总能补回来 —— 前提是右侧栏里开着 chat.deepseek.com(CORS 只允许从它自己的源发这个请求)。

失败怎么表现

任何一步失败都写进按钮下面那行可见提示(窄栏时只写进 role="status" 无障碍播报),逐字说明原因,例如:

  • 右侧边栏里没有 chat.deepseek.com:先在菜单里点「在右侧边栏内打开」
  • 读取失败:网页版接口没能建出分享链接;网页版接口:正文取自页面自己的缓存(24 条);建分享链接失败:…
  • 读取失败:点了「分享」但没等到链接;网页版接口:取对话失败:HTTP 400;页面缓存也没读到:页面缓存里没有这个会话(缓存了 3 条)
  • 读取失败:点了「分享」但没等到链接;页面上的候选控件:分享 / 复制 / …
  • 读取失败:点开了「更多」菜单但里面没有分享;页面上的候选控件:…
  • 读取失败:没有拿到当前会话 id
  • 读取失败:右侧栏浏览器不支持脚本读取
  • 读取失败:点了「分享」但没等到链接;网页版接口:取到 12 条消息;页面出口:页面往剪贴板写过 1 次,但里面没有分享链接(片段:…)

读取进行中,两个读取选项会置灰(disabled + aria-disabled)防连点;15 秒内同样的内容不会重复投递。


自定义提示词(悬停那几段话)

菜单最后一项 「自定义提示词」 打开一个小面板,能改的就是鼠标悬停在按钮上时出现的那几段话:

字段 改的是
主按钮 侧边栏收起(窄栏)时悬停气泡图标看到的那句
在右侧边栏内打开 / 在系统浏览器打开 两个打开选项的悬停提示
读取右侧边栏内对话 / 读取系统浏览器对话 两个读取选项的悬停提示

本包目前的五句默认文案(也就是新装一台电脑会看到的那五句):

  • 主按钮:DeepSeek 网页版(chat.deepseek.com)
  • 在右侧边栏内打开:在右侧边栏内嵌打开 chat.deepseek.com(需要每次重新登录)
  • 在系统浏览器打开:在系统浏览器内打开 chat.deepseek.com(无需每次重新登录)
  • 读取右侧边栏内对话:从右侧栏内嵌的 chat.deepseek.com 抓取当前对话(顺带点「分享」取链接)并发送到本对话
  • 读取系统浏览器对话:读取剪贴板里刚复制的分享链接或对话正文,并发送到本对话

要点:

  • 存在本机:localStorage 的 dsh-deepseek-chat:texts 键(一个 JSON)。不写任何全局变量, 也不碰 DSH 的配置 —— 想彻底忘掉这些改动,点面板里的「恢复默认」即可。
  • 默认文案是随包走的:五句出厂文案写在 lib/client.js 的 TITLE / OPTION_*_TITLE 五个常量里 (DEFAULT_TEXTS 只是引用它们)。所以本机改过的文案不会跟着 zip 走 —— 把插件拷到别的电脑装,那边看到的是这五个常量;想让新文案也随之分发,直接改这五行即可。 本包的这五句已按使用者自己的措辞定制过(见「维护注意」)。
  • 保存后即时生效:存下来之后重开 DSH 仍然是你的文案;写本地存储被环境拒绝时会如实播报 「提示词没能存下来:……重启就会丢」,但本次仍然生效,不假装保存成功。
  • 每段最长 200 字(超出直接截断),留空或存坏数据都自动用回默认文案(也就是上面那五个常量)。
  • 窄栏(56px 轨道)下点这一项只会提示「先把侧边栏拉宽」—— 窄栏只有图标,悬停提示就是它唯一的身位, 等拉宽了再改更直观。

两条硬纪律(改动前务必先读)

  1. sidebarRight / sidebarRightTabs 绝不能写进 inject 数组。 本插件只 inject: ["slots"]。多声明一个可能不存在的服务,会让这条 entry 永远停在 PENDING, DSH 的启动审计会直接以 web boot: 1 entry did not activate 拒绝启动整个界面。 正确做法是点击时用 ctx.get("sidebarRight") 探测(DSH 里有 374 处在用 ctx.get() 这个惯用法), 拿不到就安静地走回退路。
  2. 不要碰 ctx.styles。 那是 DSH 动态插件沙箱(dynamic lane)的 per-package builtin, 普通 bundle 插件的 ctx.styles 是 undefined,调用即抛 TypeError —— 本插件第一版就是这么把启动搞挂的(web boot: 1 entry did not activate / dsh-deepseek-chat: failed)。 样式只能用自建 <style> 标签插入(见 installStyles())。 另外 apply() 必须永不抛错,这是同一条教训的另一半。

安装

直接双击 install.cmd(它会自动挑 pwsh.exe,找不到就退回 powershell.exe)。

命令行等价写法(本机只有 Windows PowerShell 时用第二个):

cd "D:\应用\DeepSeek\插件\dsh-deepseek-chat-1.0.0\dsh-deepseek-chat"

# 有 PowerShell 7
pwsh -NoProfile -ExecutionPolicy Bypass -File .\install-profile.ps1

# 只有 Windows PowerShell 5.1(本机就是这种情况)
powershell -NoProfile -ExecutionPolicy Bypass -File .\install-profile.ps1

脚本做四件事(每一步都会打印结果):

  1. 把 profile 的 package.json 备份成 package.json.bak-<时间戳>;
  2. 在这个 profile 的 dependencies 里加上 "dsh-deepseek-chat": "link:<插件目录>";
  3. 把 dsh-deepseek-chat 追加进 dsh.profile.bundles;
  4. 在 <profile>\node_modules\ 下建一个指向插件目录的 junction(目录联接,不复制文件)。

外加一件宿主半边做不到"只在需要时做"的事:注册 deepseek-chat: 协议(HKCU,不需要管理员权限)。 它现在只是③级回退,不想装就加 -SkipProtocol。

改动 lib/client.js 后必须重启 DSH Desktop —— 客户端产物是在页面启动时编进 window.__DSH_BOOT__ 的,热重载看不到它。

先看会改什么(不落盘、不改注册表):

powershell -File .\install-profile.ps1 -WhatIf

装到另一台电脑(便携安装)

整个插件目录就是产物:零依赖、没有构建步骤、不需要联网、不需要 npm、不需要管理员权限。 拷过去跑一次安装器即可:

  1. 目标机先装好 DSH Desktop,并至少启动过一次。 安装器要往 %USERPROFILE%\.dsh\profiles\desktop\ 里写东西,这个目录是 DSH 第一次运行时创建的; 找不到时安装器会直接报错,不会乱改(提示先启动一次那个 profile)。
  2. 把打包好的 dsh-deepseek-chat-1.0.0.zip 拷过去,解压到本机磁盘(例如 C:\DSH-plugins\)。 建议不要放在 OneDrive 之类的同步目录或网络盘里 —— 安装器要在这个目录上建 junction。 路径带中文没问题(本机就是 D:\应用\…,已验证)。
  3. 双击解压出来的 dsh-deepseek-chat\install.cmd。若系统提示"文件来自其他计算机", 先右键 zip → 属性 → 勾选「解除锁定」再解压(或在解压后的目录里跑 Get-ChildItem -Recurse | Unblock-File)。
  4. 完全退出并重开 DSH Desktop(关窗口不算,要退出进程)。侧边栏底部就会出现「DeepSeek 网页版」。

装不上时先看这一条。 install.cmd 必须是 纯 ASCII + CRLF 换行:cmd.exe 在非 UTF-8 代码页下会错读 .cmd 里的 UTF-8 文本,并把相邻的 ASCII 字符一起吞掉,于是脚本自己的命令行被 撕成碎片,控制台刷出一大片 'stall.cmd' 不是内部或外部命令、'ODE' 不是… 这种报错,安装器 一步都没跑成(本机实测复现过)。所有中文提示都放在 install-profile.ps1(UTF-8 with BOM)里。 万一这个 .cmd 被某个编辑器改写坏了(记事本另存、Git 自动换行之类),绕过它直接用 PowerShell 跑主脚本就行 —— 效果完全一样:

powershell -NoProfile -ExecutionPolicy Bypass -File .\install-profile.ps1

或者在资源管理器里右键 install-profile.ps1 → 「使用 PowerShell 运行」。

安装器只有三个可选参数:

参数 缺省 用途
-Profile desktop DSH 的 profile 名
-DshHome 环境变量 DSH_HOME,否则 %USERPROFILE%\.dsh DSH 家目录
-PluginDir 脚本自己所在的目录 插件目录(一般不用给)
# 例:装进别的 profile,且 DSH 家目录不在默认位置
install.cmd -Profile tui -DshHome "D:\dsh-home"

安装器写的是目标机自己的 profile:dependencies["dsh-deepseek-chat"] = "link:<目标机上的插件目录>"、 dsh.profile.bundles 加一项、在 <profile>\node_modules\ 下建 junction、再注册 deepseek-chat: 协议, 所以两边路径不同也没关系(本机那份 package.json 里的 link: 路径不会被带走)。 卸载同样是 install.cmd -Uninstall。

卸载

powershell -File .\install-profile.ps1 -Uninstall

它会删掉 dependencies 条目和 bundles 条目、把 junction 摘掉(用 rmdir,不会顺着联接删掉插件源码), 并清掉 deepseek-chat: 协议。同样需要重启 DSH Desktop。

重启后怎么用

侧边栏底部会出现一个对话气泡图标:

  • 侧边栏是宽的时候:图标 + DeepSeek 网页版 文字;
  • 侧边栏收成 56px 窄条的时候:只有图标(悬停显示完整说明)。

点它不会直接打开任何东西 —— 它会在按钮上方就地展开五个选项:

  1. 在右侧边栏内打开 —— 在 DSH 应用内、右侧栏里内嵌打开 chat.deepseek.com,在那里登录即可;
  2. 在系统浏览器打开 —— 交给系统默认浏览器(登录态由浏览器自己保存);
  3. 读取右侧边栏内对话 —— 自动在右侧栏页面上点「分享」取链接,连同正文发进当前会话;
  4. 读取系统浏览器对话 —— 你在系统浏览器里点「分享」复制链接后点这个,插件读剪贴板再发进当前会话;
  5. 自定义提示词 —— 改前几项鼠标悬停时的那段说明(存在本机)。

再点一次按钮、或按 Esc,菜单收起。选「在右侧边栏内打开」但右侧栏不可用时, 它会改用系统浏览器,并在无障碍状态位里如实播报「右侧栏不可用,已在系统浏览器中打开」。

选「自定义提示词」,菜单会就地换成一个小表单:五格对应上面五段悬停提示, 改完点「保存」即时生效;「恢复默认」把五格填回插件自带的默认文案(本包这五句已按你的措辞定制);「取消」什么都不改; 正在改的时候按 Esc 等同于取消(菜单收起,草稿丢掉)。 窄栏(56px)下只有图标,这一项会提示先把侧边栏拉宽。


开发与自检

零依赖,不需要构建步骤:lib/client.js 是手工预打包的 plain-CJS 产物 (window.__ModuleLoader__.load({ id, factory })),DSH 直接把它编进客户端 boot 包。 lib/index.js 是宿主半边,只做静态自检和协议注册。

# 91 个离线用例:产物形状 / 插槽接线(按钮 + 会话 id 探针)/ 样式 /
# 五选项菜单的展开、收起、分隔线、置灰与各自路径 / 右侧栏门控与回退 /
# 系统浏览器四级回退 / 组件宽窄两种渲染 / 宿主异常安全 / 协议查询口径回归护栏 /
# 读取链:成功投递、没有 webview、不是目标站点、点不到分享但有正文、载荷坏掉、
# guest 抛错、没有会话 id、没有 sessions 服务、prompt 返回 ok:false、15 秒去重、
# 读取中禁用防连点、剪贴板分享链接/空/非分享/太短/读不到/是本机路径六种情形 /
# guest 脚本在假 DOM 上真跑一遍:只有 aria-label 的图标按钮、分享藏在「更多」里、
# 页面上已有链接、完全没有分享入口时诊断带了 tag/aria/class,
# 网页版接口主路(正文优先取自页面 IndexedDB 缓存、缓存空时退回接口、
# 请求头 x-client-bundle-id/时区秒数齐全且 URL 里没有字面量 null、
# 成对子集重试 + token 两种存法 + 没登录/没 fetch/网络断三态 + 接口失败退回 DOM +
# 没 token 时走 cookie 路),
# 页面出口钩子(页面自己 writeText 被拒仍接住链接、页面自己发 share/create 时从应答接住 share_id)、
# 公开分享接口(分享页 /share/<id> 不动按钮就读回整篇、页面上有分享链接时用接口补正文、
# 宿主把分享 id 递进脚本、宿主递的链接优先于这一栏开着的会话(不串正文)、
# 分享链接防串台(页面残留着上一个对话的链接时宁可不给链接也不串台 /
# 验掉旧链接后仍能点出当前会话的新链接,靠 /api/v0/share/list 的 chat_session_id 验明正身)、
# 系统浏览器「只有链接」时有/没有内嵌页两种播报)、
# 选项三的三步点击(分享图标 → 创建分享链接 → 创建并复制,链接由第三步的复制接住)、
# 选项四「只有链接」时插件自己请右侧栏打开分享页再取正文(openTab 参数与播报都断言)、
# 借不到右侧栏时只投链接并说清怎么办、
# 播报措辞诚实性(写不进剪贴板就不许说已复制)、
# 装机脚本编码护栏(install.cmd 纯 ASCII + CRLF、install-profile.ps1 带 BOM)、
# 自定义提示词(默认文案 / 保存生效并写盘 / 存坏数据退默认 / 恢复默认 /
# 写盘被拒时如实播报但本次仍生效 / 窄栏只提示先拉宽 / 收起时丢掉没保存的草稿)、
# 以及「脚本拿到的链接 → 一条发进会话的消息」的端到端串联(DOM 路与接口路各一遍)
node --test "test/*.test.mjs"

写测试时记住一个坑:槽位交出的是函数组件,view.render() 得到的是惰性元素, 之后每次遍历都会用「当时的 state」重新渲染。要断言「此刻」的样子必须先 materialize(view.render()),否则会在读取中的用例里看到"已经发完了"的终态。

测试全程在 vm 沙箱里跑,不连真实运行时、不写注册表、不弹任何窗口。

样式:只用真 token,绝不写死颜色

本机的主题变量前缀是 --dsw-*,不是 --dsh-*。--dsh-* 全机只有 69 支,而且全是 --dsh-sidebar-width、--dsh-composer-*、--dsh-scrollbar-* 这类布局量;颜色、边框、 悬停底色、阴影一支都没有。第一版按钮"看着像没美化",原因就是写了 var(--dsh-border) / var(--dsh-fill-hover) 这些并不存在的名字,全部退化成 fallback。

现在 lib/client.js 只引用下面 14 支,每一支都在本机 app.asar 里确认过定义:

变量 浅色 深色
--dsw-alias-interactive-bg-hover / -active #2631480f / #2631481a #ffffff14 / #ffffff24
--dsw-alias-label-primary / -secondary #0f1115 / #61666b #f9fafb / #cfd3d6
--dsw-alias-border-l1 / -l2 #0000000a / #0000001a #ffffff0f / #ffffff1f
--dsw-menu-surface-fill + --dsw-menu-backdrop-filter #f8f9fa94 + blur(40px) saturate(150%) #43454a73 + 同
--dsw-elevation-panel(回退 --dsw-shadow-lv1) 官方浮层投影 同
--dsw-radius-sm(8px)/ --dsw-radius-md(12px) 同 同
--dsw-alias-brand-primary(:focus-visible 描边) #0f1115 #f9fafb
--dsw-alias-bg-layer-2(菜单底色的兜底) #fff #2c2c2e

所以按钮的悬停/按下底色、菜单卡片、阴影、圆角、明暗自适应全部来自官方 token, CSS 里没有一个十六进制颜色。test/client.test.mjs 里有一条回归护栏:插件 CSS 引用到的 每一支变量都必须落在允许清单内,凭印象编一个变量名会直接让测试红掉。

预览页(不重启也能看效果)

preview/preview.html 是自检用的静态预览:主题变量从本机 app.asar 的主题块原样取出, 样式表直接从 lib/client.js 里截获,文案也全部取自产物自身的导出,所以它显示的就是产物本身的样子 (浅色/深色 × 收起/展开/展开+提示 × 宽栏 240px/窄栏 60px,旁边还画了同一座位上的邻居 ◍ 用来对齐观感)。

# 改完 CSS 后重新生成;脚本会逐支打印这些变量在两种主题下的真实取值,
# 任何一支在真实主题里不存在就直接报错(默认读 D:\应用\DeepSeek\resources\app.asar,
# 需要时用 $env:DSH_ASAR 覆盖)
node preview/make-preview.mjs

想看图又不启动 DSH,可以用本机 Edge 无头截图:

& "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe" --headless=new --disable-gpu `
  --hide-scrollbars --window-size=1180,1560 --screenshot="preview.png" `
  (([uri]"$PWD\preview\preview.html").AbsoluteUri)

本机已做过的验证

检查 结果
node --check lib/client.js / ESM import lib/index.js ✅
从 profile 目录 require.resolve("dsh-deepseek-chat/package.json") ✅ 经 junction 解析到插件目录
宿主自己的 auditClientBundle()(对真实产物静态自检) ✅ 0 条告警
客户端插件树里的激活状态(sidebar.footer.action 的 occupants) ✅ dsh-deepseek-chat = active: true
插件 CSS 用到的 14 支 --dsw-* 变量在浅色/深色下均有定义 ✅ preview/make-preview.mjs 逐支打印取值,无一缺失
sidebarRight / sidebarRightTabs 服务与 browser 页签 kind ✅ 源码与官方 README 双向确认(见上文)
右侧栏页面就是 DOM 里的 <webview data-sidebar-browser-frame> ✅ 第一方 installNativeKeyboard() 里逐字用 target.matches("webview[data-sidebar-browser-frame]") + getAttribute("name") 做键盘路由;主窗口 webPreferences.webviewTag = true
sessions.binding(id).session.prompt(parts, "queue") 就是 composer 的投递路 ✅ 客户端 sessions 服务契约里逐字含 `prompt(content: PromptContentPart[], mode: 'queue'
会话作用域槽位提供 sessionId(探针组件取当前会话) ✅ conversation.input.overlay / conversation.input.right 等的 standardProps 里逐字含 sessionId: SessionId、inputActions: InputActions
executeJavaScript 可用性 ⚠️ 运行时未验证:Electron <webview> 自带该方法(DSH 自身 0 处调用),插件侧第一次真机点「读取右侧边栏内对话」即可验证
页面出口钩子(剪贴板 / execCommand / /api/v0/share/create 应答嗅探) ✅ 离线复现:剪贴板 stub 拒绝写入时仍从页面那一次 writeText 接住链接;页面自己发 share/create 时从它自己的应答(clone())接住 share_id。真机效果待重启 DSH 后确认
正文取自页面自己的 IndexedDB 缓存(库 deepseek-chat / 仓库 history-message) ✅ 离线复现:假 IDB 里放一条记录,脚本不点按钮、也不打接口就读出整篇正文并建分享链接(payload.source === "cache")
分享正文接口 GET /api/v0/share/content 是公开的 ✅ 本机实测(_r/share-read.mjs,不带 cookie / 不带 token):HTTP 200、biz_code 0、标题 来自分享的对话、2 条消息、正文 1039 字;离线用例另跑通「分享页 /share/」「页面上有链接时补正文」「宿主递分享 id」三条
分享接口的 CORS ⚠️ 实测没有 access-control-allow-origin → 只能从 guest(chat.deepseek.com 源)发;这正是不在主文档里 fetch 它的原因
分享链接不串台(DOM 里翻出来的链接必须验明是当前会话的) ✅ 离线复现两条:页面残留上一个对话的链接时 payload.url === ""、share === "stale-link"、stale 里写明是哪个会话的;验掉旧链接后仍能点出当前会话的新链接(/api/v0/share/list 的 chat_session_id 就是判据)
自定义提示词面板(五项菜单 + 本机存储) ✅ 离线复现七条:默认文案 / 保存后悬停提示即时变且写进 localStorage / 坏 JSON 与空串退默认 / 「恢复默认」清键 / 写盘被拒时播报「没能存下来」但本次仍生效 / 窄栏只提示先拉宽 / Esc 与再点按钮收起时丢掉没保存的草稿
protocolRegistered() / ensureProtocol() ✅ 均返回 true(幂等,不再重写注册表)
HKCU\Software\Classes\deepseek-chat 四项 ✅ Default=URL:deepseek-chat Protocol、URL Protocol=``、DefaultIcon=shell32.dll,13、shell\open\command=Start-Process 命令
profile package.json:依赖 + bundles + junction ✅ 三项校验通过(脚本自己打印)

维护注意(血的教训)

  • install-profile.ps1 必须存成「UTF-8 with BOM」。Windows PowerShell 5.1 没有 BOM 时会按 ANSI(GBK) 读文件,中文注释会变成乱码并引发语法错误("$Protocol://open" 里的 $Protocol: 会被当成变量名)。 改完这个脚本请确认前三字节是 EF BB BF。
  • 判断协议是否注册要查命名值 reg query <key> /v "URL Protocol"。 查 /ve 只会拿到 (Default) 值 URL:deepseek-chat Protocol,里面不含子串 URL Protocol, 会让「已注册」永远判定为「未注册」,于是每次启动都重写注册表。测试里有这条回归护栏。
  • install.cmd 必须纯 ASCII + CRLF 换行,里面一个中文都不能有。cmd.exe 在 GBK 代码页下读 UTF-8 文本时会错配字节、把相邻 ASCII 字符吃掉,于是整份 .cmd 被撕成碎片逐条报 不是内部或外部命令(本机复现:'stall.cmd'/'ODE'/'hell.exe')。要改这个启动器请用 CRLF 保存,并确认没有 >0x7E 的字节;中文提示一律留给 install-profile.ps1。
  • dsh.profile.bundles 不能出现 null:profile 里原本没有 bundles 时,PowerShell 的 @($null) 会造出「一个元素是 null」的数组,写成 [null, "dsh-deepseek-chat"]。 现在写入前与校验时都先 Where-Object { $_ } 滤掉空值。
  • profile 的 package.json 必须无 BOM 写入(Node 的 JSON.parse 遇到 BOM 直接失败)。
  • 写样式只能引用 --dsw-* 真 token。凭印象写 --dsh-border / --dsh-accent 这一类名字不会报错, 只会让所有反馈静默消失(CSS 变量未定义就是无效声明)。改完先跑 node preview/make-preview.mjs, 它会打印每支变量的浅色/深色取值,未定义即报错;测试里的允许清单会再兜一层。
  • preview/make-preview.mjs 里读 app.asar 的块偏移是硬编码的(本机 DSH 0.2.0-rc.2 的布局)。 DSH 升级后重命名/移动了主题块,脚本会以「切片不含主题变量」这类信息直接失败,不会静默画错图; 这时重新扫一遍块边界、更新偏移即可。
  • shoot.ps1 / install-profile.ps1 这类带中文的 PowerShell 脚本必须存成「UTF-8 with BOM」。 Windows PowerShell 5.1 没有 BOM 时按 ANSI(GBK) 读文件,中文路径会变成乱码, 表现为"脚本明明跑通了,却报 Test-Path 失败"或语法错误。确认前三字节是 EF BB BF。
  • 读对话靠的是「文案启发式」,网页改版就要跟着改。readGuestScript() 里点按钮靠正则 /^(分享|共享|Share)$/i、/(分享|共享|share)/i(图标按钮只有 aria-label)、 /^(更多|更多操作|菜单|展开|More|…)$/i(分享可能收在「更多」菜单里)与 /(创建分享链接|复制链接|Copy\s*link|…)/i,都在那个函数内部(要用 fn.toString() 整段送进 guest,所以不能引用外部变量)。改完先跑 node --test, test/client.test.mjs 里的 runGuestScript() 会把这段脚本放进 vm 里对着假 DOM 真跑一遍 (图标按钮 / 「更多」菜单 / 已有链接 / 没有入口四种形态);真机上失败时提示行会列出页面上的 候选控件(tag|文案|aria=|title=|testid=|class=),照它改正则即可。 两处踩过的坑:① 一个元素的 aria-label / class 必须逐项拿正则去测,拼成一串再测会让 /^更多$/ 这种带锚点的正则永远失配(partsOf() 就是为此拆开的); ② 控件可能只监听指针事件,所以点的时候要依次派发 pointerdown/mousedown/pointerup/mouseup/click, 光 .click() 不灵。 ③ 别把"链接到手"押在剪贴板上。网页版「创建并复制」只调 navigator.clipboard.writeText (bundle 里另一个复制工具 aY() 才有 textarea 兜底,这个按钮没用),主文档没焦点时必然抛错, 页面于是报「复制失败」——而链接此时已经渲染在弹窗里了。所以脚本一进页面就装钩子 (navigator.clipboard.writeText / document.execCommand("copy") / window.fetch 上的 /api/v0/share/create 应答嗅探),从页面自己的出口把 share_id 接住;宿主侧写剪贴板也改成 看返回值说话(noteForShare()),不许再谎报「已复制到剪贴板」。测试里用 runGuestScript({ clipboard: { writeText: () => Promise.reject(...) }, expose }) 复现这两条路 —— expose 会把脚本装好钩子之后的 window.fetch / navigator 交出来,用例就能在脚本跑到一半时 冒充页面自己发请求、自己写剪贴板。 ④ 分享链接要防止串台。用户实测:在右侧栏里换成另一个对话后,读回来的还是上一个对话的链接 —— 因为上一次读取点开的分享弹窗会一直摆在页面上(<input value="…/share/<id>">), 换对话后 DOM 里那条链接就成了残留物。所以现在 DOM 里翻出来的链接一律要先验明正身: takeShareLink() → linkIsCurrent() → GET /api/v0/share/list?count=50 拿 shares[].chat_session_id 和当前页面地址里的会话 id 对比;对不上(或查不出来)就记进 skipLinks 并不收,如实写进 payload.stale(播报里会出现「页面里那条链接没有收下:这条链接是会话 … 的」),然后照常去点分享 按钮给当前对话新建一条。只有钩子在这次读取里当场接住的链接(captured.shareId)才免验 —— 那一定是刚刚发生的事。宁可这次不给链接,也不给错链接。 ⑤ 交给分享接口的 message_ids 只包含「成对」的消息。conversationFrom() 里 ids 与 pairs 现在是同一串:落单的提问(刚发出、还没回复)不进 message_ids,因此服务端不再回 biz_code 6 MESSAGES_MUST_APPEAR_IN_PAIRS。makeShareLink() 里那段"失败后只用成对子集再试一次" 的退路保留着当保险,但因为两串永远相同(same 判定),实际不会再触发 —— 改动这里时别把它当成 活的逻辑;正文依旧是全文(落单那条也在正文里),只是不参与分享。
  • 自定义提示词存在 localStorage 的 dsh-deepseek-chat:texts(键名 TEXTS_KEY,五格对应 menu / sidebar / external / readSidebar / readExternal)。readTexts() / writeTexts() / resetTexts() 全部包了 try/catch:本地存储读不到、写不进、内容是坏 JSON、值不是字符串或是空串, 一律退回 DEFAULT_TEXTS(就是产物里那几个常量),绝不抛错(apply() 永不抛错是硬纪律)。 写盘失败时 writeTexts() 返回 false,组件据此播报「提示词没能存下来」,但本次会话仍然用新文案 —— 渲染时取的是 textOf(key) 而不是常量,所以保存后不需要重启、也不该假装保存成功。菜单是五项了, 任何断言"恰好四项"的测试都要同步(本机踩过一次:openMenu() 里的 items.length === 4)。
  • 本包的五句默认文案是按使用者自己的措辞定制过的(TITLE / OPTION_SIDEBAR_TITLE / OPTION_EXTERNAL_TITLE,其余两句本来就一致)。改它们等于改"出厂值",也会随 zip 分发到别的电脑; 而面板里改的文案只存在本机 localStorage,不会跟着 zip 走。想回到最初那版措辞,改这五行即可。 测试里有一条用例原本硬编码了旧措辞(items[1].props["aria-label"].includes("系统默认浏览器")), 已改成与 DEFAULT_TEXTS 逐项比对 —— 以后再改文案不必动测试。
  • 接口请求头少一样就可能是 HTTP 400。x-client-bundle-id = HP(brand):deepseek → com.deepseek.chat、 英国版 dseek → com.deepseek.chat.nov;x-client-timezone-offset 是 60 × 分钟 的秒 (东八区 28800,不是 480);x-client-locale 形如 zh_CN;cache_version / cache_reset_at 只在缓存里真有版本号时才带(别拼字面量 null)。测试里有一条用例把这四样全钉住。
  • 正文优先读页面自己的 IndexedDB 缓存:库 deepseek-chat、仓库 history-message、键 = 会话 id、 值形如 {key, data, frontendVersion}。这条路不要令牌、也不看滚动位置,网页版自己就是这么回填历史的; 测试里用 fakeIndexedDB([[sid, record]]) 复现,缓存命中时断言"不该再打接口"。
  • 分享正文走公开接口,而且只能从 chat.deepseek.com 的源里发:GET /api/v0/share/content?share_id=<id> → data.biz_data.{title, messages}(信封与 history_messages 一样)。本机实测:不带 cookie、不带 token 也返回 200 / biz_code 0,所以"只有分享链接"也能补回整篇正文;但响应头没有 access-control-allow-origin,主文档(127.0.0.1 源)直接 fetch 必被浏览器拦 —— 必须借右侧栏那个 chat.deepseek.com 的 guest 发(readShareViaGuest()),换不回来就老实说没正文, 别假装拿到了。想手工验证:node _r\share-read.mjs <分享链接>。
  • 想读 DSH 自带包的源码/文档时:app.asar 里有些条目抽出来是 base64 乱码, 不要整文件抽取。改用「把整个 asar 读进内存 → 按关键字搜绝对偏移 → 就地读命中窗口」, 并用 integrity.hash 以外的证据交叉验证(该哈希与内容不是同一算法,对明文文件也会 BAD)。
  • 这个仓库靠根目录的 .gitattributes(* -text)活着。install.cmd 必须保持 CRLF、 install-profile.ps1 必须保持 UTF-8 with BOM,而 Git 默认的 core.autocrlf 会把它们改掉 —— Linux/macOS 上 checkout 一次就足以让「装机脚本编码」用例变红,Windows 上则可能直接把安装器弄坏。 新建仓库、git clone 到别的机器、或用别的客户端提交时,先确认这个文件还在、git check-attr -a install.cmd 显示 text: unset。另外测试夹具里不许出现真实的分享链接/会话 id(见「发布到 GitHub」)。

发布到 GitHub(维护者备忘)

仓库长什么样

仓库根目录就是插件包本身(package.json 在第一层),所以 git clone 下来直接能 npm test、 直接能双击 install.cmd,不用再从子目录里翻。分发用的 zip 不入库,挂在 Release 上。

该入库 不该入库
lib/(产物本体)、install.cmd、install-profile.ps1、cordis.patch.yml、package.json *.zip(分发包,走 Release)
test/(91 条离线用例)、preview/(预览页脚本 + 生成结果)、docs/preview.png node_modules/、package.json.bak-*
README.md、LICENSE、.gitattributes、.gitignore、.github/workflows/test.yml 带本机绝对路径的临时脚本(_r\ 那类)

首次上传(本机还没装 Git 时)

winget install --id Git.Git -e          # 装 Git(装完新开一个终端)
winget install --id GitHub.cli -e       # 可选:gh,用来建仓库 / 发 Release
gh auth login                           # 只在用 gh 时需要

cd "D:\应用\DeepSeek\插件\dsh-deepseek-chat-1.0.0\dsh-deepseek-chat"
git init -b main
git add -A
git commit -m "dsh-deepseek-chat 1.0.0:五项侧边栏菜单 + 读取对话 + 自定义提示词"

然后二选一建远端仓库:

# A. 用 gh:自动建仓库、加 remote、推上去(--public 可换 --private)
gh repo create dsh-deepseek-chat --public --source . --remote origin --push

# B. 手动:先在网页上建一个空仓库,再
git remote add origin https://github.com/Lumos0121/dsh-deepseek-chat.git
git push -u origin main

网页上建仓库时不要勾「Add a README / .gitignore / LICENSE」—— 本仓库已经有这三样, 勾了首次 git push 会因历史不相关被拒(真被拒了就 git pull --rebase origin main 再推)。

首次 git push 要登录 GitHub:Windows 上 Git 会弹浏览器授权(Git Credential Manager), 或者用 https://<用户名>:<Personal Access Token>@github.com/... 这种一次性地址。 不要把 token 写进 remote 长期保存(git remote -v 会明文露出来)。

发一个带 zip 的 Release(别人下这个)

# 1) 先确认套件是绿的
cd "D:\应用\DeepSeek\插件\dsh-deepseek-chat-1.0.0\dsh-deepseek-chat"
npm test

# 2) 从「已提交的树」打包:顶层只一层 dsh-deepseek-chat\,且不会把 .git 打进去
$zip = "D:\应用\DeepSeek\插件\dsh-deepseek-chat-1.0.0.zip"
git archive --format=zip --prefix=dsh-deepseek-chat/ -o $zip HEAD

# 3) 打标签 + 发 Release,把 zip 作为附件
git tag -a v1.0.0 -m "dsh-deepseek-chat 1.0.0"
git push origin v1.0.0
gh release create v1.0.0 $zip --title "dsh-deepseek-chat 1.0.0" --notes "第一个公开版:五项菜单、读取对话、自定义提示词。"

没有 gh 也行:把 zip 拖进网页版 Release 编辑页的附件区即可。

为什么用 git archive 而不是 Compress-Archive / CreateFromDirectory:仓库根目录现在有 .git\ (几十 MB 的历史)和各种点文件,直接压整个目录会把 .git 一起打进去;而 git archive 打的是**已提交的树**, 天然排除 .git、.gitignore 里排除的东西和未提交的改动,也保证「包 = 仓库里那一版」。 它同样不会做换行转换,install.cmd 的 CRLF 与 install-profile.ps1 的 BOM 都能原样进包 (打完记得抽验:install.cmd 无 >0x7E 字节、行尾全是 CRLF;install-profile.ps1 前三字节是 EF BB BF)。

两条别踩

  • 别删 .gitattributes。里面的 * -text 是唯一保证 install.cmd 保持 CRLF、 install-profile.ps1 保持 BOM 的东西;一旦 Git 做了换行转换,test/host.test.mjs 里那条 「装机脚本编码」用例会在 CI 上直接变红 —— 而它红得有道理,那正是会把安装器弄坏的情况。
  • 推之前扫一遍本机隐私:真实分享链接 / 会话 id / 本机用户名都不该进公开仓库。用例里的 https://chat.deepseek.com/share/<id> 与示例标题必须是编造的占位值(现在是 FixtureShare1234 与「示例分享:八页文档字数估算」),别把开发时抓的真实链接再贴回测试里。

兼容性

  • DSH Desktop(profile desktop);目标插槽 sidebar.footer.action
  • 额外依赖 DSH 自带的 @deepseek-ai/dsh-client-ui-sidebar-right 与 @deepseek-ai/dsh-client-ui-sidebar-browser(Desktop 默认启用;缺失时自动走系统浏览器回退)
  • Node >=22(插件本身不挑,跟随 DSH 运行时)
  • 仅 Windows 会注册自定义协议;其它平台按钮照样工作,只是少一级兜底

License

MIT

—/ 5

No ratings yet

Verified DSH bundle

Commit 114b20e61adc

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout