dsh-deepseek-chat
一个 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],证据见下),所以插件能直接:
document.querySelector('webview[data-sidebar-browser-frame]'),再用getURL()确认它确实是 chat.deepseek.com;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;
- 缓存(首选):网页版把每个会话的历史应答写进 IndexedDB 库
- 登录令牌:网页版把它放在
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=)当诊断线索。
- 回到宿主侧:把「来源 + 标题 + 分享链接 + 正文」拼成一条消息,经
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 轨道)下点这一项只会提示「先把侧边栏拉宽」—— 窄栏只有图标,悬停提示就是它唯一的身位, 等拉宽了再改更直观。
两条硬纪律(改动前务必先读)
sidebarRight/sidebarRightTabs绝不能写进inject数组。 本插件只inject: ["slots"]。多声明一个可能不存在的服务,会让这条 entry 永远停在PENDING, DSH 的启动审计会直接以web boot: 1 entry did not activate拒绝启动整个界面。 正确做法是点击时用ctx.get("sidebarRight")探测(DSH 里有 374 处在用ctx.get()这个惯用法), 拿不到就安静地走回退路。- 不要碰
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
脚本做四件事(每一步都会打印结果):
- 把 profile 的
package.json备份成package.json.bak-<时间戳>; - 在这个 profile 的
dependencies里加上"dsh-deepseek-chat": "link:<插件目录>"; - 把
dsh-deepseek-chat追加进dsh.profile.bundles; - 在
<profile>\node_modules\下建一个指向插件目录的 junction(目录联接,不复制文件)。
外加一件宿主半边做不到"只在需要时做"的事:注册 deepseek-chat: 协议(HKCU,不需要管理员权限)。
它现在只是③级回退,不想装就加 -SkipProtocol。
改动 lib/client.js 后必须重启 DSH Desktop —— 客户端产物是在页面启动时编进
window.__DSH_BOOT__ 的,热重载看不到它。
先看会改什么(不落盘、不改注册表):
powershell -File .\install-profile.ps1 -WhatIf
装到另一台电脑(便携安装)
整个插件目录就是产物:零依赖、没有构建步骤、不需要联网、不需要 npm、不需要管理员权限。 拷过去跑一次安装器即可:
- 目标机先装好 DSH Desktop,并至少启动过一次。 安装器要往
%USERPROFILE%\.dsh\profiles\desktop\里写东西,这个目录是 DSH 第一次运行时创建的; 找不到时安装器会直接报错,不会乱改(提示先启动一次那个 profile)。 - 把打包好的
dsh-deepseek-chat-1.0.0.zip拷过去,解压到本机磁盘(例如C:\DSH-plugins\)。 建议不要放在 OneDrive 之类的同步目录或网络盘里 —— 安装器要在这个目录上建 junction。 路径带中文没问题(本机就是D:\应用\…,已验证)。 - 双击解压出来的
dsh-deepseek-chat\install.cmd。若系统提示"文件来自其他计算机", 先右键 zip → 属性 → 勾选「解除锁定」再解压(或在解压后的目录里跑Get-ChildItem -Recurse | Unblock-File)。 - 完全退出并重开 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 窄条的时候:只有图标(悬停显示完整说明)。
点它不会直接打开任何东西 —— 它会在按钮上方就地展开五个选项:
- 在右侧边栏内打开 —— 在 DSH 应用内、右侧栏里内嵌打开 chat.deepseek.com,在那里登录即可;
- 在系统浏览器打开 —— 交给系统默认浏览器(登录态由浏览器自己保存);
- 读取右侧边栏内对话 —— 自动在右侧栏页面上点「分享」取链接,连同正文发进当前会话;
- 读取系统浏览器对话 —— 你在系统浏览器里点「分享」复制链接后点这个,插件读剪贴板再发进当前会话;
- 自定义提示词 —— 改前几项鼠标悬停时的那段说明(存在本机)。
再点一次按钮、或按 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
No comments yet. Be the first to write one.