fish-dock-plugin-filemgr 1.0.4 → 1.0.6

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/README.md CHANGED
@@ -1,27 +1,18 @@
1
- # 文件管理 · FISH Dock 插件
1
+ # 文件管理
2
2
 
3
- 把 FISH Dock 变成随手可用的文件工作台:上半逛目录、下半看内容,左边还能把常用文件先「暂存」起来,随时拖进别的程序。
4
-
5
- 插件 id `jassdingk.filemgr`,宿主 API 1,兼容 **FISH Dock v1.2.3 及更早的官方发行版**,不要求宿主做任何改动。
3
+ 把 FISH Dock 变成随手可用的文件工作台:左边逛目录、右边看内容,中间还能把常用文件先「暂存」起来,随时拖进别的程序。
6
4
 
7
5
  ## 功能
8
6
 
9
7
  ### 目录浏览 + 预览
10
8
 
11
- - **目录浏览**:磁盘根目录、用户目录、桌面 / 文档 / 下载逐层进入;支持把文件 / 文件夹直接拖进面板。
9
+ - **目录浏览**:从磁盘根目录、用户目录、桌面 / 文档 / 下载逐层进入;支持把文件 / 文件夹直接拖进面板。
12
10
  - **预览**:Markdown 渲染、图片(PNG / JPEG / GIF / WebP / BMP / AVIF / ICO / SVG)、Office 文本抽取(docx 段落与标题、xlsx 多工作表、pptx 每页文字,原生解析 OOXML,不需要装 Office)、代码高亮行号、纯文本与二进制兜底。
13
11
  - **上下并列**:上半是文件夹浏览器(面包屑 + 列表 + 上一级 / 刷新 / 常用文件夹 / 固定),下半是预览;中间分隔条可拖动调高,高度会被记住。
14
12
  - **框选与拖入暂存**:在列表里拖出选框可批量选中(Ctrl / Shift 点选切换),选中项直接拖到左侧暂存区即批量加入;拖到一半拖出列表会自动从「框选」切到「搬运」。
15
13
  - **常用文件夹**:记住打开过的目录,浮层里快速跳转;可把目录「固定」到最上面。
16
14
 
17
- ### 文件暂存(中转站)
18
-
19
- - 拖入文件或文件夹即加入暂存区,也可以右键 / 按钮添加。
20
- - **分组**:暂存区支持多个分组标签,点击切换,标签可折叠;删除分组只走标签右侧的 ×。分组之间完全隔离,切组不会串条目。
21
- - **拖出**:把暂存项直接拖到桌面、资源管理器、微信等外部程序。
22
- - 复制路径、复制为文件、多选批量、去重、清空,偏好持久化。
23
-
24
- ## 选择文件夹
15
+ ### 选择文件夹
25
16
 
26
17
  「打开文件夹」默认调用 **Windows 原生文件夹选择框**(`pick-folder.ps1` + IFileOpenDialog),选完直接进入该目录。
27
18
 
@@ -34,7 +25,7 @@
34
25
  | 自行编译、带 `fish:hold` 的宿主 | Windows 原生对话框 | 仅对话框期间临时锁定 |
35
26
  | 官方发行版(≤ v1.2.3) | 内置选择器 | 不锁定 |
36
27
 
37
- 宿主侧要支持这条路,需要三处改动:
28
+ 宿主侧要支持这条路,需要三处改动(都已在 `FISH-Dock` 上游以补丁形式验证过):
38
29
 
39
30
  1. `sdk/browser/bridge.js` 暴露 `hold(on)` → `parent.postMessage({type:'fish:hold', ticket, on}, '*')`
40
31
  2. `Wakuwaku/src/pet/extension-page.js` 收 `fish:hold` 写入 `record.hold`,并新增 `onHold(record, on)` 回调;`park()` / `enter()` / `retire()` 各自同步
@@ -42,76 +33,30 @@
42
33
 
43
34
  只改插件包、不重编译宿主时,插件自动走内置选择器,功能完整不缺。
44
35
 
45
- ## 安装
46
-
47
- ### 方式一:npm(最省事)
48
-
49
- ```bash
50
- npm i -g fish-dock-plugin-filemgr
51
- fish-dock-filemgr # 校验 + 拷到宿主 packages 目录 + 写注册表
52
- ```
53
-
54
- 装完重启 FISH Dock 即可。其它子命令:
55
-
56
- ```bash
57
- fish-dock-filemgr --dry-run # 只校验包完整性,不写磁盘
58
- fish-dock-filemgr --path # 只打印 .fish-plugin 路径,自己拖进宿主「从文件安装」
59
- fish-dock-filemgr --uninstall # 从宿主注册表移除本插件
60
- ```
61
-
62
- ### 方式二:打过发行包,走宿主「从文件安装」
63
-
64
- ```bash
65
- npm run build
66
- # 产物 dist/jassdingk.filemgr.fish-plugin,用 FISH Dock 插件页的「从文件安装」装上
67
- ```
68
-
69
- ### 方式三:开发加载
70
-
71
- ```bash
72
- npm run build
73
- # 产物 dist/filemgr 就是一个完整的开发目录(含 plugin.json),交给 FISH Dock 开发加载即可
74
- ```
36
+ ### 文件暂存(中转站)
75
37
 
76
- 两种产物都由 `tools/build.mjs` 一次生成。`sdk/` 里带的 `bridge.js` 与 `plugin.mjs` 是 FISH Dock v1.2.3 的原始副本,打包时会分别拷到包根目录的 `bridge.js` 和 `plugins/filemgr/sdk.mjs`——与官方 `tools/stage-development.mjs` 的做法一致。
38
+ - 拖入文件或文件夹即加入暂存区,也可以右键 / 按钮添加。
39
+ - **分组**:暂存区支持多个分组标签,点击切换,标签可折叠;删除分组只走标签右侧的 ×。
40
+ - **拖出**:把暂存项直接拖到桌面、资源管理器、微信等外部程序。
41
+ - 复制路径、复制为文件、多选批量、去重、清空,偏好持久化。
77
42
 
78
43
  ## 说明
79
44
 
80
45
  - 只读取路径白名单内的文件,路径解析后必须落在允许的根目录之下;UNC 路径一律拒绝。
81
- - 「打开文件夹」在支持 `fish:hold` 的宿主上会短暂启动系统 PowerShell 调起 Windows 原生文件夹对话框(`pick-folder.ps1`);不加 `fish:hold` 的宿主上不会启动子进程,改用内置选择器。除此之外不启动任何子进程。
46
+ - 「打开文件夹」在支持 `fish:hold` 的宿主上会短暂启动系统 PowerShell 调起 Windows 原生文件夹对话框(`pick-folder.ps1`),用于拿到用户亲手选择的路径;不加 `fish:hold` 的宿主上不会启动子进程,走内置选择器。除此之外不启动任何子进程。
82
47
  - 页面运行在宿主沙箱里(`default-src 'none'`),PDF 与音视频不做内联播放,会提示用系统程序打开。
83
48
  - 不发起网络请求。
84
- - 大文件保护:单次文本预览上限 800 KB、目录条目上限 2000 条、Office 文件上限 200 MB,超限会截断并给出提示,不会把插件进程撑死。
85
49
 
86
50
  ## 存储
87
51
 
88
- 偏好(布局、最近打开、额外根目录、暂存条目与分组、固定目录)保存在插件命名空间的 `config/prefs.json`。
52
+ 偏好(布局、最近打开、额外根目录、暂存分组)保存在插件命名空间的 `config/prefs.json`。
89
53
 
90
- ## 目录结构
54
+ ## 开发
91
55
 
56
+ ```bash
57
+ # 本地暂存(模拟官方 tools/stage-development.mjs 的产出)
58
+ node _stage_filemgr.mjs
59
+ # 产物
60
+ # dist/filemgr.fish-plugin
61
+ # _stage/filemgr/ ← 可直接用 FISH Dock 的「从文件安装」装上验证
92
62
  ```
93
- plugins/filemgr/
94
- backend.mjs 后端入口:命令路由 + 帧体积兜底
95
- preview.mjs 目录树 / 文本 / 图片 / Office 读取
96
- stash.mjs 暂存区数据与拖出服务
97
- page.html 页面结构(上下并列 + 左侧暂存)
98
- page.js 页面逻辑(浏览、预览、框选、暂存、目录选择器)
99
- page.css 布局与主题
100
- filemgr.css 浏览器列表、行内按钮、高亮
101
- stash.css 暂存区与分组标签
102
- dock.css 与宿主共享的配色变量
103
- pick-folder.ps1 原生文件夹对话框(内联 C#,不依赖 .cs 文件)
104
- pick-files.ps1 原生多文件对话框
105
- clip.ps1 复制到剪贴板
106
- sdk/
107
- browser/bridge.js 宿主注入页面的桥(官方 v1.2.3 原版)
108
- node/plugin.mjs 后端 SDK(官方 v1.2.3 原版)
109
- tools/
110
- build.mjs 打发行包 / 开发目录
111
- bin/
112
- fish-dock-filemgr.mjs npm 全局命令:校验 + 安装 / 卸载插件
113
- ```
114
-
115
- ## 许可
116
-
117
- MIT。`sdk/` 下的两个文件来自 [Fish-under-sea/FISH-Dock](https://github.com/Fish-under-sea/FISH-Dock)(MIT),保留原作者版权。
package/package.json CHANGED
@@ -1,25 +1,17 @@
1
1
  {
2
2
  "name": "fish-dock-plugin-filemgr",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "FISH Dock 文件管理插件:目录浏览 + 文件预览(Markdown / 图片 / Office / 代码)+ 文件暂存中转站,兼容 FISH Dock v1.2.3 及更早官方发行版。",
5
5
  "type": "module",
6
- "bin": {
7
- "fish-dock-filemgr": "bin/fish-dock-filemgr.mjs"
8
- },
9
6
  "files": [
10
- "bin/",
11
- "dist/",
7
+ "plugin.json",
12
8
  "README.md",
13
- "LICENSE"
9
+ "bridge.js",
10
+ "plugins"
14
11
  ],
15
12
  "engines": {
16
13
  "node": ">=18"
17
14
  },
18
- "scripts": {
19
- "build": "node tools/build.mjs",
20
- "install-plugin": "node bin/fish-dock-filemgr.mjs",
21
- "prepack": "node tools/build.mjs"
22
- },
23
15
  "keywords": [
24
16
  "fish-dock",
25
17
  "fishdock",
@@ -2,7 +2,7 @@
2
2
  "schema": 1,
3
3
  "id": "jassdingk.filemgr",
4
4
  "name": "文件管理",
5
- "version": "1.0.4",
5
+ "version": "1.0.6",
6
6
  "hostApi": 1,
7
7
  "platforms": [
8
8
  "windows-x86_64"
@@ -27,18 +27,19 @@
27
27
  ],
28
28
  "files": {
29
29
  "bridge.js": "f38f93994e3cb2b71f2ff1810b3f68fb42f4261550f8ab01b0bfa3eecd5f94ee",
30
+ "package.json": "d2b066289c54ebfb40da8591f8dbaf261c8cbd0dcb80ea03f5206c7f0aa50e35",
30
31
  "plugins/filemgr/backend.mjs": "b448aa688ff11f2abc4c5281b4714646ed6c384473d873cfe431a8e64b066d26",
31
32
  "plugins/filemgr/clip.ps1": "b0a32fca35cf2d25133338f46e20a0db20d78c5bb6b5c2aab899998bfbd6c6ab",
32
33
  "plugins/filemgr/dock.css": "e795214dd39de3b473ac0b1a7b79f6922a88c761fca4d3ea4da8fd9b60188bcf",
33
- "plugins/filemgr/filemgr.css": "16c621471133d29c6942781bedf86e0a4de806865ff004b666bcad8b1929434e",
34
+ "plugins/filemgr/filemgr.css": "79b878b29499284935d71dfa877716232f85ed40d864437382a059fa8ced0479",
34
35
  "plugins/filemgr/FolderPicker.ps1": "4b7f86c543e06fd8f93fa5d8002997d944c8189cb0ae031bedf864ea89196427",
35
36
  "plugins/filemgr/page.css": "0801a05b5148cd091a68957beac21cc982a7818ef4707cd06fd11b20adbc7765",
36
37
  "plugins/filemgr/page.html": "3cfda31be29cd23b1e0f7f6004b6c03522c3526b78f9df4711e320962a660831",
37
- "plugins/filemgr/page.js": "282f11d803ddc8bb87a0a99d8af68315f4ff7304672072da838a07a47c5cdc62",
38
+ "plugins/filemgr/page.js": "74a7ccae18f7857be9656f14db07c2bddbf4591fff57a7eb16722ee4128e739d",
38
39
  "plugins/filemgr/pick-files.ps1": "88bff92d9a2e5b69e9411bb372441ff0b86899b7f5b31f5ed8d154c4f6fc2f37",
39
40
  "plugins/filemgr/pick-folder.ps1": "ff031c14df32149c1c80c7ab6a21710c1a68e823e96e3068c458a60902ae3b6f",
40
41
  "plugins/filemgr/preview.mjs": "a5596e77180fe7928443244a0148b6d3ea8381a1f886ee01f9a08be038c7ee80",
41
- "plugins/filemgr/sdk.mjs": "76b052b8d99ff6b6b0b149aee7479d1129bb248a959625642282184978065f38",
42
+ "plugins/filemgr/sdk.mjs": "91061dae337157c88e2303d1899f9149ccecd656df4675c76dd5721bc405bf07",
42
43
  "plugins/filemgr/stash.css": "fce2aaa13a86dbd7675b685c533f0bb916c7e610fc612a3ade660c67069f5662",
43
44
  "plugins/filemgr/stash.mjs": "c33cea6b845a3847950d26a10b9aba1ef68d39b1f9fc423b23055dead7638866",
44
45
  "README.md": "e9fb4ca9831b778a1d530b67518fcaf2b42cf02494b9d68deaad16bf2b947687"
@@ -71,6 +71,14 @@ body[data-resizing=true]{user-select:none;cursor:col-resize}
71
71
  .stash-group>.stash-tag.active>.stash-tag-count{opacity:.8}
72
72
  .stash-group>.stash-tag.active>.stash-tag-del{color:#1a1035;opacity:.75}
73
73
  .stash-group>.stash-tag.active>.stash-tag-del:hover{color:#7a1010}
74
+ /* 拖动文件悬停到某个分组标签上:高亮该分组名,提示松手即落入此分组 */
75
+ .stash-group>.stash-tag.drop-hover{
76
+ color:#1a1035;background:var(--dock-accent);border-color:var(--dock-accent);
77
+ box-shadow:0 0 0 2px color-mix(in srgb,var(--dock-accent) 40%,transparent);
78
+ }
79
+ .stash-group>.stash-tag.drop-hover>.stash-tag-dot{background:#1a1035}
80
+ .stash-group>.stash-tag.drop-hover>.stash-tag-count{opacity:.85}
81
+ .stash-group>.stash-tag.drop-hover>.stash-tag-del{color:#1a1035;opacity:.75}
74
82
  /* 右侧文件列表:已在暂存/分组里的文件 */
75
83
  .filelist .row.stashed .nm{color:var(--dock-accent)}
76
84
  .stash-badge{flex:none;font-size:10px;line-height:1;padding:2px 6px;border-radius:999px;
@@ -1472,6 +1472,7 @@
1472
1472
 
1473
1473
  const summary = el('summary', {
1474
1474
  class: 'stash-tag' + (active ? ' active' : ''),
1475
+ dataset: { group: bucket.key }, // 供拖拽命中:拖到此标签上即高亮该组名并落入该分组
1475
1476
  title: `${bucket.label} · ${bucket.items.length} 项(点箭头/整条折叠;点右侧漏斗图标只在看本组与全部间切换)`,
1476
1477
  })
1477
1478
  // 单击整条标签 = 折叠/展开(summary 默认行为,不拦截)。
@@ -1765,11 +1766,12 @@
1765
1766
  // 批量加入(框选后拖进暂存区走这一条)。
1766
1767
  // 后端 stash.add 本来就吃 paths 数组,所以只是把多行一次性递过去;
1767
1768
  // 逐个报告"加了几项/几项重复",比只弹一个含糊的 toast 更好懂。
1768
- async function addManyToStash(paths) {
1769
+ async function addManyToStash(paths, group) {
1769
1770
  const list = (paths || []).filter(Boolean)
1770
1771
  if (!list.length) return
1771
- const group = state.view === 'all' ? '' : state.view
1772
- const row = await callSoft('stash.add', { paths: list, group })
1772
+ // 显式传入的分组(拖到某个分组标签上)优先;未指定时回退到当前视图分组。
1773
+ const targetGroup = group === undefined ? (state.view === 'all' ? '' : state.view) : group
1774
+ const row = await callSoft('stash.add', { paths: list, group: targetGroup })
1773
1775
  if (row && row.ok) {
1774
1776
  const added = Array.isArray(row.items) ? row.items.length : 0
1775
1777
  const dup = list.length - added
@@ -1800,6 +1802,7 @@
1800
1802
  let moved = false
1801
1803
  let suppressClick = false
1802
1804
  let ghost = null
1805
+ let dropGroup = undefined // 拖拽时命中的分组标签 key(undefined=未命中,回退当前视图)
1803
1806
 
1804
1807
  const rows = () => Array.from(list.querySelectorAll('.row[data-path]'))
1805
1808
 
@@ -1836,6 +1839,8 @@
1836
1839
  if (ghost) { ghost.remove(); ghost = null }
1837
1840
  for (const node of list.querySelectorAll('.row.drag-src')) node.classList.remove('drag-src')
1838
1841
  if (stashCol) stashCol.classList.remove('drop-target')
1842
+ dropGroup = undefined
1843
+ for (const node of document.querySelectorAll('.stash-tag.drop-hover')) node.classList.remove('drop-hover')
1839
1844
  }
1840
1845
 
1841
1846
  list.addEventListener('pointerdown', event => {
@@ -1920,8 +1925,14 @@
1920
1925
  } else if (mode === 'drag' && ghost) {
1921
1926
  ghost.style.left = `${event.clientX + 14}px`
1922
1927
  ghost.style.top = `${event.clientY + 14}px`
1923
- const over = stashCol && document.elementFromPoint(event.clientX, event.clientY)?.closest('#stash-col')
1928
+ const under = document.elementFromPoint(event.clientX, event.clientY)
1929
+ const over = stashCol && under?.closest('#stash-col')
1924
1930
  if (stashCol) stashCol.classList.toggle('drop-target', !!over)
1931
+ // 命中哪个分组标签,就高亮哪个分组名并把它作为落点;未命中分组标签则回退当前视图分组。
1932
+ const hitTag = under && under.closest('.stash-tag[data-group]')
1933
+ dropGroup = hitTag ? hitTag.dataset.group : undefined
1934
+ for (const node of document.querySelectorAll('.stash-tag.drop-hover')) node.classList.remove('drop-hover')
1935
+ if (hitTag) hitTag.classList.add('drop-hover')
1925
1936
  }
1926
1937
  event.preventDefault()
1927
1938
  })
@@ -1930,6 +1941,7 @@
1930
1941
  if (mode === 'idle') return
1931
1942
  const was = mode
1932
1943
  const paths = dragPaths.slice()
1944
+ const group = dropGroup // 松手前记录命中的分组(reset 会清空)
1933
1945
  const dropped = was === 'drag' && !!(
1934
1946
  stashCol && document.elementFromPoint(event.clientX, event.clientY)?.closest('#stash-col')
1935
1947
  )
@@ -1940,7 +1952,7 @@
1940
1952
  setTimeout(() => { suppressClick = false }, 0)
1941
1953
  }
1942
1954
  reset()
1943
- if (dropped && paths.length) await addManyToStash(paths)
1955
+ if (dropped && paths.length) await addManyToStash(paths, group)
1944
1956
  }
1945
1957
 
1946
1958
  list.addEventListener('pointerup', event => { finish(event) })
@@ -3,7 +3,7 @@ const CATEGORIES = new Set(['credentials','roster','keys','config','exports','ba
3
3
  const DEFAULT_LIMIT = 1024 * 1024;
4
4
  export function createPlugin({ id, input = process.stdin, output = process.stdout,
5
5
  activate = async () => {}, deactivate = async () => {}, onEvent = async () => {}, commands = {},
6
- onError = () => {}, maxLine = DEFAULT_LIMIT, requestTimeout = 5000 } = {}) {
6
+ onError = () => {}, maxLine = DEFAULT_LIMIT, requestTimeout = 12000 } = {}) {
7
7
  if (!/^[a-z0-9]+(?:-[a-z0-9]+)*\.[a-z0-9]+(?:-[a-z0-9]+)*$/.test(id ?? '')) throw new Error('插件 ID 格式无效');
8
8
  if (!Number.isSafeInteger(maxLine) || maxLine < 128 || maxLine > DEFAULT_LIMIT) throw new Error('协议帧大小无效');
9
9
  if (!Number.isSafeInteger(requestTimeout) || requestTimeout < 1 || requestTimeout > 30000) throw new Error('宿主调用时限无效');
@@ -94,7 +94,10 @@ export function createPlugin({ id, input = process.stdin, output = process.stdou
94
94
  if (!row || typeof row !== 'object' || Array.isArray(row)) throw new Error('插件协议应为 JSON 对象');
95
95
  if (!Object.hasOwn(row,'method')) {
96
96
  const waiter = pending.get(row.id);
97
- if (!waiter) throw new Error('宿主响应编号不匹配');
97
+ // 迟到/重复的宿主响应(原请求已因超时被清理):静默丢弃,绝不让整个插件会话崩溃。
98
+ // 原实现此处 throw 会经 receive() → close()+onError() 杀死插件;任何一次超过
99
+ // requestTimeout 的存储调用,其迟到响应都会触发。调用方早已通过超时 reject 感知失败。
100
+ if (!waiter) return;
98
101
  pending.delete(row.id); clearTimeout(waiter.timer);
99
102
  if (Object.hasOwn(row,'result') === Object.hasOwn(row,'error')) return waiter.reject(new Error('宿主响应结构无效'));
100
103
  if (row.error) waiter.reject(new Error('宿主存储操作失败')); else waiter.resolve(row.result);
package/LICENSE DELETED
@@ -1,32 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Fish-under-sea
4
- Copyright (c) 2026 Pengjie Zhou
5
- Copyright (c) 2026 Ebony-Vinyl
6
-
7
- Permission is hereby granted, free of charge, to any person obtaining a copy
8
- of this software and associated documentation files (the "Software"), to deal
9
- in the Software without restriction, including without limitation the rights
10
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
- copies of the Software, and to permit persons to whom the Software is
12
- furnished to do so, subject to the following conditions:
13
-
14
- The above copyright notice and this permission notice shall be included in all
15
- copies or substantial portions of the Software.
16
-
17
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
- SOFTWARE.
24
-
25
- ---
26
-
27
- The root license covers FISH Dock integration code and MIT-licensed upstream
28
- code, subject to the original notices retained in Wakuwaku/LICENSE and
29
- dsh-our-free-model/LICENSE. Other bundled libraries retain their own licenses.
30
- This license does not grant rights to pet sprites, application icons,
31
- wallpapers, third-party trademarks, accounts, or model services. Verify the
32
- applicable permissions before redistributing artwork or other assets.
@@ -1,151 +0,0 @@
1
- #!/usr/bin/env node
2
- // fish-dock-filemgr —— 把「文件管理」插件装进本机 FISH Dock。
3
- //
4
- // 做的事和宿主「从文件安装 .fish-plugin」等价:
5
- // 1. 校验 dist/filemgr 里每个文件的 sha256 与 plugin.json 登记值一致
6
- // 2. 拷到 %USERPROFILE%\.fish-dock\host\packages\<id>\<version>\<整包 sha256>\
7
- // 3. 往 host/config/installed-plugins.json 写注册条目(manifest 按宿主解析后的形状补全)
8
- //
9
- // 仅支持 Windows(宿主只发 windows-x86_64)。
10
- import fs from 'node:fs'
11
- import os from 'node:os'
12
- import path from 'node:path'
13
- import crypto from 'node:crypto'
14
- import { fileURLToPath } from 'node:url'
15
- import { spawnSync } from 'node:child_process'
16
-
17
- const HERE = path.dirname(fileURLToPath(import.meta.url))
18
- const PKG_ROOT = path.resolve(HERE, '..')
19
- const DEV = path.join(PKG_ROOT, 'dist', 'filemgr')
20
- const ARCHIVE = path.join(PKG_ROOT, 'dist', 'jassdingk.filemgr.fish-plugin')
21
-
22
- const C = { r: '\x1b[0m', b: '\x1b[1m', dim: '\x1b[2m', g: '\x1b[32m', y: '\x1b[33m', red: '\x1b[31m', cyan: '\x1b[36m' }
23
- const say = (...a) => console.log(...a)
24
- const die = msg => { console.error(`${C.red}✗ ${msg}${C.r}`); process.exit(1) }
25
- const sha256 = b => crypto.createHash('sha256').update(b).digest('hex')
26
-
27
- const argv = process.argv.slice(2)
28
- const has = (...names) => names.some(n => argv.includes(n))
29
-
30
- if (has('-h', '--help')) {
31
- say(`
32
- ${C.b}fish-dock-filemgr${C.r} — 安装 FISH Dock「文件管理」插件
33
-
34
- ${C.cyan}fish-dock-filemgr${C.r} 安装 / 升级到当前版本
35
- ${C.cyan}fish-dock-filemgr --path${C.r} 只打印 .fish-plugin 包路径(自行拖进宿主安装)
36
- ${C.cyan}fish-dock-filemgr --dry-run${C.r} 只做校验,不写磁盘
37
- ${C.cyan}fish-dock-filemgr --uninstall${C.r} 从注册表移除本插件
38
-
39
- 安装完成后需要 ${C.y}重启 FISH Dock${C.r} 才会加载新版本。
40
- `)
41
- process.exit(0)
42
- }
43
-
44
- if (process.platform !== 'win32') die('本插件只支持 Windows(FISH Dock 宿主仅提供 windows-x86_64)')
45
-
46
- if (!fs.existsSync(path.join(DEV, 'plugin.json'))) {
47
- die(`找不到 dist/filemgr/plugin.json。\n 这是发行包里预构建的产物;如果你是源码检出,先跑 ${C.cyan}npm run build${C.r}`)
48
- }
49
-
50
- const manifest = JSON.parse(fs.readFileSync(path.join(DEV, 'plugin.json'), 'utf8'))
51
- const version = manifest.version
52
- const HOST = path.join(os.homedir(), '.fish-dock', 'host')
53
- const REGISTRY = path.join(HOST, 'config', 'installed-plugins.json')
54
-
55
- if (has('--path')) {
56
- say(fs.existsSync(ARCHIVE) ? ARCHIVE : path.join(DEV, 'plugin.json'))
57
- process.exit(0)
58
- }
59
-
60
- // ---------- uninstall ----------
61
- if (has('--uninstall')) {
62
- if (!fs.existsSync(REGISTRY)) die(`注册表不存在:${REGISTRY}\n 看起来这台机器还没装过 FISH Dock 插件。`)
63
- const reg = JSON.parse(fs.readFileSync(REGISTRY, 'utf8'))
64
- if (!reg.entries || !reg.entries[manifest.id]) {
65
- say(`${C.y}!${C.r} 注册表里没有 ${manifest.id},无需卸载`)
66
- process.exit(0)
67
- }
68
- const backup = REGISTRY + '.bak-' + Date.now()
69
- fs.copyFileSync(REGISTRY, backup)
70
- delete reg.entries[manifest.id]
71
- const tmp = REGISTRY + '.tmp-' + process.pid
72
- fs.writeFileSync(tmp, JSON.stringify(reg, null, 2))
73
- fs.renameSync(tmp, REGISTRY)
74
- say(`${C.g}✓${C.r} 已从注册表移除 ${C.b}${manifest.id}${C.r}`)
75
- say(` ${C.dim}注册表备份 ${backup}${C.r}`)
76
- say(` ${C.dim}包目录可留可删:${path.join(HOST, 'packages', manifest.id)}${C.r}`)
77
- say(` ${C.y}重启 FISH Dock 后生效${C.r}`)
78
- process.exit(0)
79
- }
80
-
81
- // ---------- 校验 ----------
82
- const declared = manifest.files || {}
83
- const files = Object.keys(declared)
84
- if (!files.length) die('plugin.json 里没有 files 清单,包不完整')
85
- let bad = 0
86
- for (const name of files) {
87
- const abs = path.join(DEV, name)
88
- if (!fs.existsSync(abs)) { console.error(`${C.red}缺失${C.r} ${name}`); bad++; continue }
89
- const got = sha256(fs.readFileSync(abs))
90
- if (got !== String(declared[name]).toLowerCase()) { console.error(`${C.red}哈希不符${C.r} ${name}\n 期望 ${declared[name]}\n 实际 ${got}`); bad++ }
91
- }
92
- if (bad) die(`包内文件校验失败(${bad} 个异常),已中止,未写入任何文件`)
93
-
94
- const digest = sha256(fs.readFileSync(ARCHIVE))
95
- say(`${C.dim}插件${C.r} ${C.b}${manifest.id}${C.r} ${C.cyan}v${version}${C.r}`)
96
- say(`${C.dim}文件${C.r} ${files.length} 个全部校验通过`)
97
- say(`${C.dim}整包${C.r} sha256 ${digest.slice(0, 16)}…`)
98
-
99
- if (has('--dry-run')) { say(`\n${C.y}= dry-run,未写入磁盘 =${C.r}`); process.exit(0) }
100
-
101
- // ---------- 拷文件 ----------
102
- const target = path.join(HOST, 'packages', manifest.id, version, digest)
103
- fs.mkdirSync(target, { recursive: true })
104
- for (const name of files) {
105
- const dst = path.join(target, name)
106
- fs.mkdirSync(path.dirname(dst), { recursive: true })
107
- fs.copyFileSync(path.join(DEV, name), dst)
108
- }
109
- fs.copyFileSync(path.join(DEV, 'plugin.json'), path.join(target, 'plugin.json'))
110
- say(`${C.dim}落盘${C.r} ${target}`)
111
-
112
- // ---------- 写注册表 ----------
113
- // 宿主 Manifest::parse 用 serde(deny_unknown_fields) + camelCase,且 backend.args / locksSettings
114
- // 有 serde(default)——注册表里存的是解析后的 to_value 结果,所以这两个字段要显式补上。
115
- const full = {
116
- ...manifest,
117
- backend: { ...manifest.backend, args: manifest.backend?.args ?? [] },
118
- locksSettings: manifest.locksSettings ?? false,
119
- }
120
-
121
- const registry = fs.existsSync(REGISTRY) ? JSON.parse(fs.readFileSync(REGISTRY, 'utf8')) : { schema: 1, entries: {} }
122
- registry.entries = registry.entries || {}
123
- const previous = registry.entries[manifest.id] || null
124
- registry.entries[manifest.id] = {
125
- manifest: full,
126
- digest,
127
- official: false,
128
- enabled: previous ? previous.enabled !== false : true,
129
- autostart: previous ? previous.autostart !== false : true,
130
- }
131
-
132
- if (fs.existsSync(REGISTRY)) {
133
- const backup = REGISTRY + '.bak-' + Date.now()
134
- fs.copyFileSync(REGISTRY, backup)
135
- say(`${C.dim}备份${C.r} ${backup}`)
136
- }
137
- const tmp = REGISTRY + '.tmp-' + process.pid
138
- fs.writeFileSync(tmp, JSON.stringify(registry, null, 2))
139
- fs.renameSync(tmp, REGISTRY)
140
-
141
- say('')
142
- say(`${C.g}✓ 已安装${C.r} ${C.b}${manifest.id}${C.r} v${version}${previous ? '(覆盖旧版)' : ''}`)
143
-
144
- // ---------- 提示重启 ----------
145
- const running = spawnSync('tasklist', ['/FI', 'IMAGENAME eq DSH-FISH.exe', '/NH'], { encoding: 'utf8' })
146
- if ((running.stdout || '').includes('DSH-FISH.exe')) {
147
- say(`${C.y}${C.b}FISH Dock 正在运行${C.r} —— 需要重启它才会加载新版本:`)
148
- say(` ${C.dim}右下角托盘图标退出,或任务管理器结束 DSH-FISH.exe,然后重新打开${C.r}`)
149
- } else {
150
- say(`${C.dim}打开 FISH Dock 即可在插件页看到「文件管理」${C.r}`)
151
- }
@@ -1,62 +0,0 @@
1
- # 文件管理
2
-
3
- 把 FISH Dock 变成随手可用的文件工作台:左边逛目录、右边看内容,中间还能把常用文件先「暂存」起来,随时拖进别的程序。
4
-
5
- ## 功能
6
-
7
- ### 目录浏览 + 预览
8
-
9
- - **目录浏览**:从磁盘根目录、用户目录、桌面 / 文档 / 下载逐层进入;支持把文件 / 文件夹直接拖进面板。
10
- - **预览**:Markdown 渲染、图片(PNG / JPEG / GIF / WebP / BMP / AVIF / ICO / SVG)、Office 文本抽取(docx 段落与标题、xlsx 多工作表、pptx 每页文字,原生解析 OOXML,不需要装 Office)、代码高亮行号、纯文本与二进制兜底。
11
- - **上下并列**:上半是文件夹浏览器(面包屑 + 列表 + 上一级 / 刷新 / 常用文件夹 / 固定),下半是预览;中间分隔条可拖动调高,高度会被记住。
12
- - **框选与拖入暂存**:在列表里拖出选框可批量选中(Ctrl / Shift 点选切换),选中项直接拖到左侧暂存区即批量加入;拖到一半拖出列表会自动从「框选」切到「搬运」。
13
- - **常用文件夹**:记住打开过的目录,浮层里快速跳转;可把目录「固定」到最上面。
14
-
15
- ### 选择文件夹
16
-
17
- 「打开文件夹」默认调用 **Windows 原生文件夹选择框**(`pick-folder.ps1` + IFileOpenDialog),选完直接进入该目录。
18
-
19
- 宿主原本会在系统对话框抢走焦点时自动收起设置面板。插件只在**弹出对话框的那一刻**调用 `bridge.js` 的 `hold(true)`,选完(或取消/报错)立刻在 `finally` 里 `hold(false)` 放开——平时浏览、拖放都不锁,点面板外面照常能关。
20
-
21
- 宿主没有 `fish:hold` 能力(官方发行版 ≤ v1.2.3)时,插件自动回落到**内置自绘选择器**(用后端 `preview.tree` 逐层浏览),功能完整,只是不锁面板:
22
-
23
- | 宿主 | 「打开文件夹」 | 面板锁定 |
24
- |---|---|---|
25
- | 自行编译、带 `fish:hold` 的宿主 | Windows 原生对话框 | 仅对话框期间临时锁定 |
26
- | 官方发行版(≤ v1.2.3) | 内置选择器 | 不锁定 |
27
-
28
- 宿主侧要支持这条路,需要三处改动(都已在 `FISH-Dock` 上游以补丁形式验证过):
29
-
30
- 1. `sdk/browser/bridge.js` 暴露 `hold(on)` → `parent.postMessage({type:'fish:hold', ticket, on}, '*')`
31
- 2. `Wakuwaku/src/pet/extension-page.js` 收 `fish:hold` 写入 `record.hold`,并新增 `onHold(record, on)` 回调;`park()` / `enter()` / `retire()` 各自同步
32
- 3. `Wakuwaku/src/pet/settings.js` 维护 `pluginHold` 标志,`onBlur` 判定里加 `!pluginHold`
33
-
34
- 只改插件包、不重编译宿主时,插件自动走内置选择器,功能完整不缺。
35
-
36
- ### 文件暂存(中转站)
37
-
38
- - 拖入文件或文件夹即加入暂存区,也可以右键 / 按钮添加。
39
- - **分组**:暂存区支持多个分组标签,点击切换,标签可折叠;删除分组只走标签右侧的 ×。
40
- - **拖出**:把暂存项直接拖到桌面、资源管理器、微信等外部程序。
41
- - 复制路径、复制为文件、多选批量、去重、清空,偏好持久化。
42
-
43
- ## 说明
44
-
45
- - 只读取路径白名单内的文件,路径解析后必须落在允许的根目录之下;UNC 路径一律拒绝。
46
- - 「打开文件夹」在支持 `fish:hold` 的宿主上会短暂启动系统 PowerShell 调起 Windows 原生文件夹对话框(`pick-folder.ps1`),用于拿到用户亲手选择的路径;不加 `fish:hold` 的宿主上不会启动子进程,走内置选择器。除此之外不启动任何子进程。
47
- - 页面运行在宿主沙箱里(`default-src 'none'`),PDF 与音视频不做内联播放,会提示用系统程序打开。
48
- - 不发起网络请求。
49
-
50
- ## 存储
51
-
52
- 偏好(布局、最近打开、额外根目录、暂存分组)保存在插件命名空间的 `config/prefs.json`。
53
-
54
- ## 开发
55
-
56
- ```bash
57
- # 本地暂存(模拟官方 tools/stage-development.mjs 的产出)
58
- node _stage_filemgr.mjs
59
- # 产物
60
- # dist/filemgr.fish-plugin
61
- # _stage/filemgr/ ← 可直接用 FISH Dock 的「从文件安装」装上验证
62
- ```
Binary file
File without changes