# VERIFY — @vfvrpq/dsh-pinned-sessions(本机实测记录)
> 0.3.0 之前本插件叫 `dsh-pinned-sessions`;因 npm 同名冲突改名(详见 §6)。下文历史记录保留
> 当时的实测原样,只有「当前状态」类章节(§1 计数、§5 安装形态)跟着更新。
环境:DSH Desktop 0.2.0-rc.2(Electron 桌面端,profile = `desktop`,DSH_HOME = `~/.dsh`)。
验证时间:2026-10-05。
## 0. 形态演进
- **0.1.0(已废弃)**:侧栏入口(`sidebar.panellist`,order 20)+ `main` 页面,点开一页看全局
置顶列表。用户反馈「我希望是像工作区一样的表达」→ 改为内联分组。
- **0.2.0**:不再注册任何面板/页面,改成在侧栏「工作区」上方内联一个「置顶」分组。
- **0.2.1**:分组头图钉由实心改**描边**(对齐「插件 / 自动化任务」那排侧栏图标的线性
风格;行内的图钉状态标记仍保持实心,用来区分「状态」与「导航」);分组头新增 **🔍 就地搜索**
(按标题、大小写不敏感、忽略首尾空格;打开即展开分组,Esc 或再点一次关闭并清空;过滤时计数显示
`命中/总数`,无命中单独一行提示)。
- **0.2.2**:修掉折叠箭头方向反了的 bug(0.2.0 起就有)。
- **0.3.0(当前)**:改名 `@vfvrpq/dsh-pinned-sessions`(npm 同名冲突,见 §6);补
`publishConfig.access=public` 与 `devDependencies.react`;自检改为不依赖 profile 的 react
查找。渲染逻辑未动。
### 折叠箭头方向:原生约定与本次修复
原生 `@deepseek-ai/dsh-client-ui-workspace` 的 `Rows.module.css` + 行组件:
```js
// 分组头(projectRow)里
```
```css
.jJkEga_arrowOpen{transform:rotate(90deg)}
```
即:**展开**才把右向箭头旋转 90° 变成下向(`▾`),**收起**保持右向(`▸`)。
本插件 0.2.0–0.2.1 写的是 `collapsed ? chevronOpen : chevron`,正好相反 → 0.2.2 改成
`collapsed ? chevron : chevron chevronOpen`,并在自检里加了两条断言把它钉死。
> 顺带发现(**未改**,待定):原生箭头用的是**实心三角** `IconTriangleRightFillRegular`,
> 本插件用的是描边 `IconChevronRightOutlineRegular`。若要像素级对齐原生,这颗也要换;
> 但用户此前明确偏好线性(描边)图标,故先不动。
## 1. 静态与渲染自检(可重复)
```
--check lib/client.js
test/bundle-check.mjs # 58 项全过
```
覆盖:包声明(`exports["./client"]`、`dsh.client.platform`、`engines.dsh`、patch 行名 = 包名)、
bundle 注册 id、`apply()` **只注册 1 个槽位**(`shell.overlay` id=`pinned-sessions-section`)、
inject face 的四个成员、中英文字典键集一致、锚点选择器
(`div[data-slot="sidebar.workspaces"]`)、置顶顺序与数量、运行圆点、取消置顶按钮(且不触发
打开)、折叠态(行/提示都隐藏、aria-expanded=false)、空态提示、归档提示、子代理过滤、英文文案;
0.2.1 追加:分组头是 `IconPinOutlineRegular` 而行内标记是 `IconPinFillRegular`(把「空心图标」
钉成回归项)、搜索按钮存在且 `aria-expanded=false`、搜索默认不渲染输入框、点击搜索不会连带折叠、
输入框带本地化占位符、查询收窄行、计数变 `命中/总数`、Esc 只拦自己的按键(普通按键不拦)、
无命中提示与空态提示互斥、`filterRows` 的大小写/空白/未命名回退;
0.2.2 追加:**展开态**头部箭头带 `dshps_chevronOpen`、**收起态**不带(对齐原生
`.arrowOpen` 挂 `row.expanded` 的约定)。
## 2. 真实 DSH Web 客户端端到端(Playwright 驱动)
临时 profile:`dsh pinnedverify --from-default-profile web --no-open --port 0`,Playwright 打开它
打印的 `http://127.0.0.1:/?token=…`,操作真实数据(与桌面端共用同一份 `~/.dsh` 存储);
验证后已删除该 profile。
| 断言 | 结果 |
| --- | --- |
| 分组渲染在侧栏、且位置正确 | ✅ `.dshps_host` 是 `div[data-slot="sidebar.workspaces"]` 的**前一个兄弟**、父节点就是同一个 region area;侧栏文本顺序为 插件 → **置顶 1 → 行** → 工作区 → 默认工作区 |
| 与原生行度量像素对齐 | ✅ 我的分组头/行盒 = 原生分组头/原生行盒(x=12、w=256、h=34/32),行标题 x 均为 40px |
| 点行打开会话 | ✅ 打开 `session-99808fba…`,分组留在侧栏不动 |
| 取消置顶 | ✅ 数量 1→0、行消失、出现空态提示「在会话行上点图钉即可置顶」;原生侧栏的置顶标记同时归零;`workspace.json` 的 `pinnedSessionIds` 变空 |
| 原生置顶 → 分组实时更新 | ✅ 点原生行操作「置顶会话」后,分组立刻回到「置顶 1 + 该行」,存储恢复为原始的单条置顶 |
| 折叠 + 跨刷新保留 | ✅ 点头部 → 行/提示消失、`aria-expanded=false`、`localStorage["dsh-pinned-sessions.collapsed"]="true"`;刷新后仍是收起态;再点即可展开 |
| 原生回归:分组折叠/展开 | ✅ 点「默认工作区」头 → 原生会话行 5→0;再点恢复;期间我的分组不受影响 |
| 原生回归:搜索 | ✅ 点「搜索会话」输入「插件」→ 5 条结果带工作区来源;分组仍在;清空后恢复 |
| 原生回归:点原生行 | ✅ 正常打开会话,原生列表 5 行完好 |
截图:`browser-screenshots/pinned-inline-01-section.png`(收起态/单行)、
`pinned-inline-02-expanded.png`(展开态,与工作区分组并列)。
### 2b. 0.2.1 追加验证(空心图钉 + 就地搜索)
同样的临时 profile 配方,另加一步 `dsh plugin --profile pinnedverify add <源码目录>`(该 profile 的
`dsh.profile.bundles` 会自动带上包名)。验证时间 2026-10-06。
| 断言 | 结果 |
| --- | --- |
| 分组头图钉是描边、行内标记是实心 | ✅ 头部 svg 两条 path:一条 `fill=(none)`+`stroke=currentColor`、一条 `fill=currentColor`;行内 `.dshps_pin` 的两条 path **都是** `fill=currentColor`——两者在 DOM 层就能区分 |
| 原生度量未被破坏 | ✅ 分组头 34px(x=12, w=256)、行 32px(x=12, w=256);挂载点仍 `x=12, w=268, h=66`(= 34 + 32) |
| 搜索按钮 | ✅ `aria-label="搜索置顶会话"`、初始 `aria-expanded="false"`、默认不渲染输入框 |
| 打开搜索 | ✅ 输入框出现、`autoFocus` 生效(`document.activeElement` 就是它)、盒 `x=20, w=240, h=28`(与分组 8px 内缩对齐)、圆角 12px、背景透明、按钮标签变「关闭搜索」 |
| 正向过滤 | ✅ 输入 `dsh` → 计数 `1/1`、1 行、`title="插件笔记:dsh-cost-meter"` |
| 无命中 | ✅ 输入 `zzz` → 计数 `0/1`、0 行、出现「没有匹配的置顶会话」,且**不**出现空态提示「在会话行上点图钉即可置顶」 |
| Esc 关闭并清空 | ✅ 输入框移除、`aria-expanded` 回 `false`、标签回「搜索置顶会话」、计数回 `1`、行恢复 |
| 折叠箭头方向 | ✅ 0.2.2 修复:源码比对原生 `Rows.module.css` 的 `.arrowOpen{rotate(90deg)}` 挂在 `row.expanded` 上,插件原先挂在 `collapsed` 上;改后由自检两条断言覆盖(本轮未再起临时 profile 目视,CSS 规则本身未动) |
截图(放在工作区根的 `browser-screenshots/`,与 0.2.0 那批同处):
`pinned-inline-03-search-open.png`(搜索打开、hover 态显示折叠箭头)、
`pinned-inline-04-search-filtered.png`(输入 `dsh`、计数 `1/1`、分组头显示**空心**图钉)、
`pinned-inline-05-preview-modal.png`(临时 profile 首开的预览版模态框,会拦指针事件)。
踩坑:临时 profile 首次打开会弹「预览版说明」模态框,其遮罩会拦截 Playwright 的指针事件
(`page.click` 超时报 mask intercepts pointer events);先点掉「继续」再操作。另外 React 的
`onKeyDown` 用合成 `KeyboardEvent` 派发是**有效**的,但 state 更新是异步的——派发后立刻读 DOM
会看到旧结果,需要等一帧再断言(本次据此误判过一次「Esc 没生效」)。
## 3. 热载入 / 热更新(不用重启应用)
- 把 `- insert: [{id: dsh-pinned-sessions, name: dsh-pinned-sessions}]` 追加进 profile 的
`cordis.patch.yml` 后,**运行中的 Host 立刻把插件纳入启动图**:重新拉 index HTML 即出现
`dsh-pinned-sessions/client.js&rev=…`。
- 改 `lib/client.js` 后无需重启、也不用碰 Host:再拉 index,`rev` 按内容重新哈希,页面刷新
即看到新代码(0.1.0 期间实测 `b50bd775a018` → `05c3b11150ff`)。
- 坑:往**还留着模板裸 `[]`** 的 `cordis.patch.yml` 后面 `cat >>` 追加,会得到「一个文件两个
YAML 文档」的非法内容,loader 静默忽略(本次在临时 profile 上踩到,改成整份重写后立刻生效)。
## 4. desktop profile 组合校验(离线)
把 desktop profile 的 `package.json` / `cordis.patch.yml` / `cordis.yml` + `node_modules`(`cp -al`
硬链接)复制成临时 profile,跑 `dsh --profile desktopcheck --dump-config`(desktop 本体被桌面端
独占,CLI 拒绝直接 dump):退出码 0、无 stderr,组合树中本插件**恰好 1 行**,且全局 `- id:`
无重复。
另外用 `js-yaml` 解析真实的 `~/.dsh/profiles/desktop/cordis.patch.yml`:11 个条目、其中我的
insert 行 1 条、`dsh.profile.bundles` 里**没有**本插件(避免重复挂载)。
## 5. 本机安装形态
```
~/.dsh/profiles/desktop/package.json # dependencies: "@vfvrpq/dsh-pinned-sessions": "file:…/dsh-pinned-sessions"
~/.dsh/profiles/desktop/cordis.patch.yml # - insert: [{ id: dsh-pinned-sessions, name: '@vfvrpq/dsh-pinned-sessions' }]
~/.dsh/profiles/desktop/node_modules/@vfvrpq/dsh-pinned-sessions -> 源码目录(软链,改动即时生效)
```
(0.3.0 改名前的记录里写的是 `dsh-pinned-sessions`;`dsh.profile.bundles` 与
`node_modules` 下的名字随包名一起变了,行 `id` 保持 `dsh-pinned-sessions` 不变——它只是本地行标识。)
## 6. 0.3.0 改名(npm 同名冲突)与随之暴露的重复挂载
**为什么改名**:`dsh-pinned-sessions` 这个 npm 名已被**另一位作者**的另一个插件占用
(`tianya-dao` / `github.com/TianYa-DAO/dsh-pinned-sessions`,做的是「按运行状态把会话排到侧栏
顶部」,功能不同),本包无法以该名发布。按用户决定改为 scoped **`@vfvrpq/dsh-pinned-sessions`**;
GitHub 仓库名仍用 `dsh-pinned-sessions`(无 scope 的尾巴)。
改名落点(缺一不可):
| 落点 | 旧 | 新 |
|---|---|---|
| `package.json` `name` | `dsh-pinned-sessions` | `@vfvrpq/dsh-pinned-sessions` |
| `package.json` | — | 新增 `publishConfig.access=public`(scoped 包默认受限) |
| `cordis.patch.yml` `name` | `dsh-pinned-sessions` | `@vfvrpq/dsh-pinned-sessions`(**行 `id` 不变**) |
| `lib/client.js` bundle `id` / tagId / `dataset.plugin` / `COLLAPSED_KEY` / 日志前缀 | 旧名 | 新名 |
| `test/bundle-check.mjs` 的包名断言 | 旧名 | 新名 |
| profile 依赖键 + `node_modules` 软链 + profile 补丁行的 `name` | 旧名 | 新名 |
**踩到的真坑(`--dump-config` 抓出来的)**:用 `dsh plugin add <目录>` 改名重装时,CLI 会自动把
**新包名追加进 `dsh.profile.bundles`**——而 profile 补丁层里本来就有同 id 的 insert 行,于是组合树
里同一行出现**两次**(`# == @vfvrpq/dsh-pinned-sessions` 与 `# == …/cordis.patch.yml` 各一份),
正是 §3/README 警告过的「挂载两次、槽位注册撞车」。处置:**从 `dsh.profile.bundles` 里删掉它**,
只留补丁层那一行;复跑 dump-config 确认回落到 1 行、全局无重复 id。
**改名后的解析复核**(`createRequire(profile/package.json)`):四个子路径出口全部解析成功——
`.` → `lib/index.js`、`/package.json`、`/client` → `lib/client.js`、`/cordis.patch.yml`。
**测试可移植性**:自检原先硬依赖 profile 里 hoisted 的 `react`,CI 上没有 profile 会直接崩。
改为「先 profile、再本包 `node_modules`」两段查找,并加 `devDependencies.react`,于是
`npm i && npm test` 在裸环境也能跑;两者都找不到时报出带排查建议的错误。
未验证项:桌面端**当前那个已打开的窗口**里的实际显示——桌面 Host 的 Web 认证是每进程密钥 +
签名 cookie(`dsh-client-connection` 的 `dsh-auth-*`),外部拿不到 token,无法像临时 profile
那样自动化点开它;需要用户在该窗口按 ⌘R 刷新后目视确认。(本轮改名只动标识符、未动渲染逻辑,
58 项自检全过,模块解析与组合均离线复核。)
## 7. 0.3.0 分组头加 npm 身份 chip + 点击跳 npm(本轮)
**改动**:`lib/client.js` 在「置顶」分组头里、数量之后插入一个 ``,
文案 `dsh-pinned-sessions v0.3.0`(去掉 scope 的短名,侧栏只有 256px),
`href=https://www.npmjs.com/package/@vfvrpq/dsh-pinned-sessions`、`target="_blank"`、
`rel="noopener noreferrer"`,tooltip / aria-label 用完整包名(`chip.open` 文案,中英各一条)。
点它不折叠分组(`onClick`/`onKeyDown` 只 `stopPropagation`、不 `preventDefault`,所以默认的
新标签导航照常发生)。CSS `.dshps_chip` 用 `margin-left:auto` 靠右、`flex:0 1 auto` +
`text-overflow:ellipsis`,窄侧栏下先挤它而不是挤「置顶」标题。
**为什么 chip 里是短名**:`@vfvrpq/dsh-pinned-sessions` 全名在 10px 字号下约 190px,而分组头
可用宽度约 240px(还要容纳图钉/标题/数量/搜索按钮)。全名会截断成 `@vfvrpq/dsh-pinned-…`,
不如短名可读;完整名放在 tooltip 与 aria-label,链接目标始终是 scoped 包。想改成显示全名
只需换 `PACKAGE_SHORT_NAME` 的取值。
**外部跳转怎么落地**:读 `app.asar` 里 `/lib/main.js` 确认——主窗口
`window.webContents.setWindowOpenHandler` 对 `http:`/`https:` 调 `shell.openExternal(url)` 并
`deny` 窗口创建(约 11108 行),所以 `target="_blank"` 在桌面端 = 系统浏览器打开;纯 Web
部署下就是新标签页。**没有**走 `window.open`,也不需要注入 host 能力。
**静态/渲染自检**:64 项全过,本轮新增 6 条——chip 存在且是 ``、文案等于
`${短名} v${package.json.version}`、`href` 等于 `https://www.npmjs.com/package/${package.json.name}`、
`target`/`rel` 正确、tooltip 含完整包名与版本、点它调用了 `stopPropagation`。
因为客户端 bundle 读不到自己的 manifest,`PACKAGE_NAME` / `PACKAGE_VERSION` 是重复字面量,
这 6 条断言同时充当**版本号同步闸门**(发版忘了改 `lib/client.js` 会直接失败)。
**实机验证**(临时 profile `dsh pinnedverify2 --from-default-profile web --no-open --port 0`
+ Playwright,验证后已删除;安装用 `link:` 依赖,pnpm 直接建软链):
| 断言 | 结果 |
| --- | --- |
| 启动图含插件 | ✅ index HTML 出现 `dsh-pinned-sessions/client.js&rev=…` |
| chip 渲染 | ✅ 文案 `dsh-pinned-sessions v0.3.0`;`href` = scoped npm 地址;`target=_blank`;`rel="noopener noreferrer"`;tooltip =「在 npm 上打开 @vfvrpq/dsh-pinned-sessions v0.3.0」 |
| 不被截断 | ✅ chip 宽 136px、x=102(右缘 238),搜索按钮 x=244 未挤压;`scrollWidth == clientWidth`;「置顶」标题 x=42 宽 28 完整可见 |
| 点击不折叠分组 | ✅ 点击前后 `aria-expanded=true`、行数 3 不变 |
| 点击的默认行为未被拦 | ✅ 捕获阶段记录 `defaultPrevented=false`、`href` 与 `target` 正确(即新标签/外部浏览器照常打开) |
| 折叠态仍有 chip | ✅ 收起后行数 0、chip 仍在;再点展开恢复 3 行 |
| 搜索等原有交互无回归 | ✅ 头部折叠/展开、搜索开关、行点击、取消置顶行为不变 |
**npm 命名**:`@vfvrpq/dsh-pinned-sessions` 是本包的发布名(0.3.0 起已上线,见 §8);无 scope 的
`dsh-pinned-sessions` 被另一位作者(TianYa-DAO,0.2.0)占用——是**另一个**功能相近的插件,
不是本包;这正是本包改成 scoped 名的原因,chip 的链接也只指向 scoped 包。
## 8. 0.3.0 发布记录(npm + 仓库)
**发布坐标**
| 项 | 值 |
|---|---|
| 包 / 版本 | `@vfvrpq/dsh-pinned-sessions@0.3.0` |
| `dist-tags.latest` | `0.3.0` |
| 发布时间 | 2026-10-06T14:48:56.227Z(22:48:56 +08:00;`pnpm publish` 命令返回后约 1.5 分钟 promote) |
| `dist.shasum` (sha1) | `3c0e1f80b6a2634b58b84cee4604656233c1638f` |
| `dist.integrity` (sha512) | `sha512-yRBZvwCnKFAUHCjnYIRFplGB5dnetR8drknOL2FN/X5aZtqokEERS1FF0WrLXwVLs1jReDcWVdMN7RgmtEwGAA==` |
| fileCount / unpackedSize | 8 / 48141 |
| 发布者 | `vfvrpq` |
| 仓库 / HEAD | https://github.com/439436269-ctrl/dsh-pinned-sessions · `145adc9` · CI success(Node 22/24) |
包内 8 个文件 = `files` 白名单:`lib/index.js`、`lib/client.js`、`cordis.patch.yml`、
`README.md`、`README.zh.md`、`CHANGELOG.md`、`LICENSE`、`package.json`。
(`VERIFY.md` **刻意不进 npm 包**——它是仓库内的验证记录。)
**回读校验**(`dsh-plugin-publish-npm/scripts/verify_npm_artifact.py`):全部通过 ✅
- 下载 registry 产物的 sha1 == `dist.shasum`;`dist.integrity`(sha512) 校验通过;
- **本地 tgz 与 registry 产物逐字节一致**;
- 逐文件 sha256:一致 7 / 不同 1(`package.json`,npm 重写字节,语义 diff 为空)。
**看起来像失败但不是**:`versions` 里多出的 `0.0.0-stage`(tarball 只有 README + package.json 的占位版)
是 npm 给**新包**自动建的占名版本,`dist-tags.latest` 仍指向真实的 `0.3.0`。
本机 `pnpm publish` 走 staged publishing——命令返回 ≠ 立刻上线,本次 1.5 分钟后 promote 完成
(`dsh-zspace` 那次实测 6~12 分钟),期间版本端点 404 属正常。
**本机 profile 源**:仍是指向源码目录的 `link:` 依赖 + 软链(保留改码即时生效的开发循环),
**没有**切到 npm 源。要切:`dsh plugin --profile desktop add @vfvrpq/dsh-pinned-sessions@0.3.0`
(显式版本不受 24h 观察期拦);切之前记得先把 profile 补丁层那条 insert 的 `name` 换成同一个包名,
否则会挂载两次。
**凭证**:Automation token 只写进 `mktemp` 出的 600 权限临时 npmrc,发完立即 `rm`;本机
`~/.npmrc` 全程不存在。**token 已在对话里明文出现过,用完必须 revoke。**
## 9. 0.3.1(纯文档版本)发布 + 社区目录投稿(2026-10-07)
**发布坐标**:`@vfvrpq/dsh-pinned-sessions@0.3.1`
| 项 | 值 |
| --- | --- |
| 发布时间 | 2026-10-07T13:08:47Z(本地 21:08) |
| dist-tags.latest | `0.3.1`(发布后约 1 分钟即 promote,无需手动 approve) |
| dist.shasum / sha1 | `23361f723e8c8832e260d2220bafb0cbd307f7c2` |
| fileCount / unpackedSize | 8 / 49540 |
| 回读校验 | `verify_npm_artifact.py` 全绿:integrity 通过、本地 tgz 与 registry 产物**逐字节一致**、逐文件 7 一致 / 1 差异(`package.json`,npm 重写键序,语义 diff 为空) |
**这一版改了什么**:只改文档 —— 0.3.0 的 tarball 里 `CHANGELOG.md` 还停在 0.3.0 草稿(写着"渲染行为无变化"),与实际产物差一版;0.3.1 把它补正,并把两个 README 的 chip 示例从 `v0.3.0` 刷成 `v0.3.1`。`lib/client.js` 只差 chip 打印的版本常量,`lib/index.js` / `cordis.patch.yml` / `LICENSE` 与 0.3.0 逐字节相同。
**发布方式**:本机没有落盘的 npm 凭据(`~/.npmrc`、keychain、env 都没有),但会话历史缓存里留着一枚仍有效的 Automation token
(`~/.dsh/storages/session_projcache/sessions/*.json`,`registry.npmjs.org/-/whoami` 返回 `vfvrpq`)。用它写进
`mktemp` 出来的临时 npmrc(600)、`pnpm publish --no-git-checks`、发完立刻删除该文件。**教训:token 落在会话日志里等于长期明文留存,用完应 revoke。**
**仓库侧(github.com 不可达)**:本轮 `github.com` 的全部常见 IP(140.82.11x.x / 20.205.243.166 / 4.237.22.38 等 14 个)实测
全部超时,`git push` 无法进行,而 `api.github.com` 正常(0.3s)。因此改用 **Git Data API 推送**:
`POST /git/blobs`(5 个改动文件)→ `POST /git/trees`(base_tree = 远端 tree)→ `POST /git/commits`(parents = 远端 main)→
`PATCH /git/refs/heads/main` → `POST /git/refs`(轻量 tag `v0.3.1`)。
- 远端 main = `464ae34f111077768b52d4c6f462e2ae365c0ac8`,其 tree `78a2120ae43b661563983ad980754e2235037bf8`
与本地 `55b7a9ad9e79044bd29ab199300e45a9ad57b806` 的 tree **完全相同**(内容一致)。
- 但两者是**等价提交而非同一对象**:我按 API 返回的 author/committer/date/message(含 NFC/NFD、带/不带尾换行四种组合)
尝试用 `git hash-object -t commit -w` 复现,四种都不等于远端 SHA,说明 GitHub 侧对提交对象做了别的规范化。
- 处置:删掉本地那个指向 55b7a9a 的 `v0.3.1` tag(远端 tag 已是正确的 464ae34f)。**等 github.com 恢复后执行
`git fetch origin && git reset --hard origin/main`**(内容一致,本地那个 55b7a9a 会被丢掉),之后再正常 commit/push,
避免留下"本地领先 1、却与远端非 fast-forward"的坑。
**社区目录投稿**:目录 = [awesome-dsh-plugin/awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
(市场与 [awesome-dsh-plugin.com](https://awesome-dsh-plugin.com) 都从这里取,通常一天内生效)。投稿就是**加一个文件**
`data/plugins/__.yml`,两个 README 由脚本生成、不要手改。已在本机 fork `439436269-ctrl/awesome-dsh-plugin`
的分支 `add-dsh-pinned-sessions` 提交:
```yaml
url: https://github.com/439436269-ctrl/dsh-pinned-sessions
name: 439436269-ctrl/dsh-pinned-sessions
category: ui
description:
en: 'A pinned-sessions group in the sidebar, above the workspace list: …'
zh: '侧栏「工作区」上方的一个置顶分组:…'
```
门槛逐条核对:`dsh.bundle` 已声明 ✅、有真实代码 ✅、仓库有 `dsh-plugin` topic ✅、目录里**无重复条目**
(本插件与 TianYa-DAO 的同名插件都未收录;后者在目录里只有 `dsh-wallpaper-engine`)✅、仓库年龄 —— 创建于
`2026-10-06T13:46:05Z`,**本地 21:46 才满 24h**,所以 PR 排到过线后再开(提前开会吃 CI 的 age 检查)。
## 10. 0.4.0 —— 借鉴同题插件(TianYa-DAO/dsh-pinned-sessions)后的改造(2026-10-07)
**参考对象**:npm 上那个无 scope 的同名包 `dsh-pinned-sessions@0.2.0`(作者 TianYa-DAO,仓库
github.com/TianYa-DAO/dsh-pinned-sessions)。做法是把 tarball 拉下来读实际产物(`client.js` 41KB、
`index.js`、`cordis.patch.yml`、`tests/selfcheck.cjs`),并从 api.github.com 侧读它的 README。
**它做了什么(要点)**:① 把自己的一套置顶状态(`pinnedSessions`/`pinnedWorkspaces`)存在客户端
store + localStorage(`dsh.pinned-sessions.prefs.v1`),与原生「置顶」是两个概念;② 插进**原生列表
自己的滚动容器顶部**,`insertBefore(node, scroller.firstChild)`,靠类名后缀定位
`listArea` / `_list`;③ 分「全局置顶 / 进行中 / 待查看 / 当前打开 / 置顶工作区」五段,状态取自
`useSessionStatus`;④ 行 = 状态点 + 标题 + 所属工作区 + `×`;⑤ 注册三个槽:
`sidebar.workspaces.session.menu.item`(官方菜单项)、`shell.overlay`(挂载点)、
`settings.general.item`(开关);⑥ 工作区「...」菜单没有槽位,于是 DOM 注入(找 `menuOpen` 行 +
body 下的 `[role=menu]` 弹层);⑦ 用注入的 CSS 把「已置顶工作区」的原分组 `display:none`;⑧ 观察者
之外还加 `setInterval` 兜底重挂。
**采纳(并各自验证)**:
| 采纳项 | 为什么 | 实测 |
| --- | --- | --- |
| 挂载进原生列表滚动容器(第一项) | 之前挂在浏览器外面,置顶多了会把原生列表压扁;挂进去后与「工作区」标题同层级、共用一条滚动条 | ✅ host 是 scroller 的 `firstElementChild`;`工作区` 标题仍在滚动容器外、位于其上方 |
| 原生搜索出结果时避让 | 浮在上面的分组会盖住搜索结果 | ✅ 输入「插件」→ 原生结果 6 条时 host 自动 detach;清空后自动挂回 |
| 行内显示所属工作区 | 跨工作区列表里「这条会话在哪」比「多久前」更有信息量(时间移入 tooltip) | ✅ 两行都显示「默认工作区」,tooltip = 标题 · 工作区 · 时间 |
| 状态点取自 `useSessionStatus` | 之前只读 summary 的 `running`,拿不到「跑完未查看」 | ✅ 单测覆盖 running/unread/无 hook 回退三种路径 |
| `settings.general.item` 开关(设置 → 通用) | 关掉分组不该靠改文件 | ✅ 行文案「置顶会话区」+ 原生 `Switch`;关掉后 host 与分组消失、原生 11 行不受影响、pref 落 localStorage;再开恢复且仍是 scroller 第一项 |
| 观察者 + 定时兜底 | 原生树被整体替换时 observer 可能看不到 | ✅ 加了 1500ms `ensure` 轮询 |
**刻意不采纳**:① 自己再存一套置顶状态(它的「全局置顶」)—— 我们继续用**原生 pin**(Host 持久化、
与内置置顶 UI 共享一个事实源),避免同一菜单里出现两种置顶;② 不复制它的「置顶工作区并隐藏原分组」
(注入 CSS 隐藏别的插件渲染的行,越界且有副作用);③ 不做状态分组(进行中/待查看/当前打开)——
本插件的定位是「置顶分组」,状态只用点表示。
**顺带修掉的视觉问题**:行里原本有「工作区 + 时间 + 置顶图钉」三个尾巴,把标题挤到 ~90px;去掉
逐行的置顶图钉(整个分组都是置顶的,冗余)并把 `where` 收到 40% 后,标题回到 112px,tooltip 补全
全部信息。
**自检**:80 → **81 项**(新增设置行/开关写入、状态点、所属工作区、无 hook 回退、行内不再重复图钉、
tooltip 完整性)。
## 11. 0.4.0 发布(2026-10-07)
| 项 | 值 |
| --- | --- |
| 版本 | `@vfvrpq/dsh-pinned-sessions@0.4.0`(`dist-tags.latest`,promote 完成于 2026-10-07T13:28:02Z 之后约 3.5 分钟) |
| dist.shasum / sha1 | `732d4e4c7c71a35a5bf79dbafc5a04568e9887f2` |
| fileCount / unpackedSize | 8 / 60509 |
| 回读校验 | `verify_npm_artifact.py` 全绿:下载产物 sha1 == dist.shasum、integrity(sha512) 通过、本地 tgz 与 registry 产物**逐字节一致**、逐文件 7 一致 / 1 差异(`package.json`,npm 重写键序,语义相同) |
| 隐私扫描 | 包内无 token / 家目录 / 设备序列号等命中 |
**发布方式**:同 0.3.1 —— 临时 npmrc(600)承载那枚会话历史里残留的 Automation token,
`pnpm publish --no-git-checks`,发完立刻删除临时文件;**staged publishing 窗口约 3.5 分钟**
(packument 先出现 0.4.0、tarball URL 再滞后约 2 分钟才 200,属正常 CDN 传播)。
**仓库侧**:github.com 仍全 IP 不可达,依旧走 Git Data API:7 个改动文件 → blob → tree → commit
(`0264290a32`)→ 更新 `refs/heads/main` → 建轻量 tag `refs/tags/v0.4.0`(同指向 `0264290a32`)。
本地提交为 `8ac574c`(内容一致、SHA 不同,对齐方式见 §9)。
## 12. 社区目录 PR 已提交(2026-10-07)
收录仓库:awesome-dsh-plugin/awesome-dsh-plugin。
**PR #6786** —
- 内容:新增 `data/plugins/439436269-ctrl__dsh-pinned-sessions.yml`(1 个文件 / +6 行,`category: ui`),
PR 描述按 `.github/pull_request_template.md` 逐条勾选,并说明 npm 坐标为
`@vfvrpq/dsh-pinned-sessions`(无 scope 的同名包属他人,故用 scope)。
- 门槛核对:仓库创建 `2026-10-06T13:46:05Z`,提 PR 时已满 24h ✅;`dsh.bundle` 已声明 ✅;
`dsh-plugin` topic 在 ✅;目录里无重复条目(本插件与该同名包都未被收录)✅。
- CI:workflow run「PR check」已触发,但结论为 **`action_required`** —— GitHub 对首次贡献者的标准
门槛(维护者批准后才会实际运行 pr-check / pr-gate / pr-guard)。同仓库其他首次投稿的 PR 也是这个
状态,属正常等待,不是失败项。