dsh-explorer-open 0.1.0 → 0.1.1
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 +149 -138
- package/client.js +14 -0
- package/lib/index.js +37 -1
- package/native/DshOpenWith.msix +0 -0
- package/native/version.txt +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,138 +1,149 @@
|
|
|
1
|
-
# dsh-explorer-open
|
|
2
|
-
|
|
3
|
-
在 Windows 资源管理器的右键菜单里加一条 **「通过 dsh 打开」**:点一下就在 [DeepSeek Harness](https://github.com/deepseek-ai) 里打开这个文件夹(没有工作区就新建),并开一个新会话。
|
|
4
|
-
|
|
5
|
-
Win11 的**新式简洁菜单**和**旧式完整菜单**里都有这一条,都带图标。
|
|
6
|
-
|
|
7
|
-
- 仓库:<https://github.com/ame-code/dsh-explorer-open>
|
|
8
|
-
- npm:`dsh-explorer-open`
|
|
9
|
-
|
|
10
|
-
## 功能
|
|
11
|
-
|
|
12
|
-
| 在哪儿右键 | 打开的文件夹 | 放进新会话的草稿 |
|
|
13
|
-
| --- | --- | --- |
|
|
14
|
-
| 文件夹空白处 | 当前文件夹 | 无 |
|
|
15
|
-
| 文件夹图标上 | 这个文件夹 | 无 |
|
|
16
|
-
| 任意文件上 | 文件所在的文件夹 | `@文件名` 引用(工作区之外的路径不引用) |
|
|
17
|
-
|
|
18
|
-
- 每次都**新建会话**并切过去;工作区按完整路径复用,不会重复创建。
|
|
19
|
-
- dsh 没开时会先把它拉起来,请求排队等页面连上再执行。
|
|
20
|
-
- 插件停用/卸载时会**自己拆掉菜单**(旧式注册表键 + Win11 新式 MSIX 包),不留垃圾。
|
|
21
|
-
|
|
22
|
-
## 状态
|
|
23
|
-
|
|
24
|
-
当前 **0.1.x**:功能可用,但接口(`/api/plugins/...` 路径、`~/.dsh/dsh-explorer-open/` 状态目录、`DSH_EXPLORER_OPEN_*` 环境变量)和安装方式都还可能变,破坏性改动不保证向后兼容。
|
|
25
|
-
|
|
26
|
-
原生组件(`win11/`)有独立的版本号,见 `native/version.txt`;JS 侧版本见 `package.json`。
|
|
27
|
-
|
|
28
|
-
## 安装
|
|
29
|
-
|
|
30
|
-
### 从 npm 安装(推荐)
|
|
31
|
-
|
|
32
|
-
在 dsh 的插件设置里安装 `dsh-explorer-open`,或让 agent 执行安装(bundle 安装入口接受 npm 包名)。
|
|
33
|
-
|
|
34
|
-
首次启用时会自动装 Win11 新式菜单所需的 MSIX 包。因为是自签证书,包里没有受信的根,Windows 会拒绝安装并给出:
|
|
35
|
-
|
|
36
|
-
```
|
|
37
|
-
Deployment failed with HRESULT: 0x800B0109
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
这时以管理员身份运行一次插件目录下的 `native/trust-cert.bat`(把自签证书 `dsh-open-with.cer` 导入 `LocalMachine\TrustedPeople`),然后重启 dsh 即可。只需要做一次。
|
|
41
|
-
|
|
42
|
-
> 只想要旧式菜单、不想动证书:设环境变量 `DSH_EXPLORER_OPEN_NO_MSIX=1` 再启动 dsh,插件就不会去装 MSIX 包。
|
|
43
|
-
|
|
44
|
-
### 从源码/本地目录安装
|
|
45
|
-
|
|
46
|
-
```powershell
|
|
47
|
-
# 仓库本身就是插件包(package.json 在根目录)
|
|
48
|
-
plugin_manager install_bundle E:\work\tools\dsh-open-with
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
改过代码后要重启 dsh 才会重新加载宿主侧插件;客户端 bundle 需要刷新页面。
|
|
52
|
-
|
|
53
|
-
## 工作原理
|
|
54
|
-
|
|
55
|
-
```
|
|
56
|
-
资源管理器右键
|
|
57
|
-
├─ 旧式菜单:HKCU\Software\Classes\{Directory\Background, Directory, *}\shell\OpenWithDsh
|
|
58
|
-
│ └─ powershell -File launch.ps1 "%V" (无窗口,写入待办后退出)
|
|
59
|
-
└─ 新式菜单:MSIX 包身份 + 原生 IExplorerCommand
|
|
60
|
-
└─ 同一个 launch.ps1
|
|
61
|
-
↓
|
|
62
|
-
%USERPROFILE%\.dsh\dsh-explorer-open\pending.json {folder, file?, token, at}
|
|
63
|
-
↓ 宿主侧插件每 300ms 扫描
|
|
64
|
-
GET /api/plugins/dsh-explorer-open/request (带 x-dsh-explorer-open 头)
|
|
65
|
-
↓ 页面侧插件每 1s 轮询
|
|
66
|
-
workspaces.create → sessions.create → uiWorkspace.openSession
|
|
67
|
-
↓ 文件右键时
|
|
68
|
-
conversation.input.requestDraftInitialization(binding, { prompt: '@文件', clearPreviousDraft: true })
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
- 启动器用 PowerShell 5.1 写 UTF-8(**无 BOM**)、先写临时文件再改名,页面不会读到半个 JSON。
|
|
72
|
-
- `/api/...` 路由要求自定义请求头,跨站页面带不上它(会先触发预检),因此别的网页没法领走排队中的请求。
|
|
73
|
-
- dsh 可执行文件位置由插件写进 `config.json`,启动器按 config → `DSH_EXPLORER_OPEN_EXE` → 常见安装路径的顺序查找。
|
|
74
|
-
|
|
75
|
-
## win11/ 原生组件
|
|
76
|
-
|
|
77
|
-
Win11 新式菜单不接受「注册表 + exe」的旧式扩展,必须是一个**有包身份、已签名、且 DLL 内嵌了 msix 身份清单**的 COM 组件。`win11/` 就是这个组件:
|
|
78
|
-
|
|
79
|
-
```
|
|
80
|
-
win11/
|
|
81
|
-
├─ src/dsh_open_with.cpp IExplorerCommand 实现(GetTitle/GetIcon/GetState/Invoke)
|
|
82
|
-
├─ src/stub.cpp 满足清单的占位 exe(Windows.FullTrustApplication 入口,永不被启动)
|
|
83
|
-
├─ src/test-host.cpp 不经过资源管理器的自测宿主
|
|
84
|
-
├─ src/msix.manifest DLL 内嵌的 msix 身份(publisher/packageName/applicationId)
|
|
85
|
-
├─ src/msix.rc 把上面那份清单编进 PE 资源
|
|
86
|
-
├─ assets/dsh.ico 菜单图标
|
|
87
|
-
├─ package/AppxManifest.xml MSIX 清单:comServer + fileExplorerContextMenus
|
|
88
|
-
├─ build.sh 构建 → 签名 → 打包 → 同步到 ../native/
|
|
89
|
-
├─ install-msix.ps1 / uninstall-msix.ps1
|
|
90
|
-
└─ trust-cert.ps1 / trust-cert.bat 把自签证书导入受信存储
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
重新构建(需要 MinGW-w64、`windres`、Windows SDK BuildTools 的 `makeappx`/`signtool`,以及 `Cert:\CurrentUser\My` 里的 `CN=DshOpenWith` 证书):
|
|
94
|
-
|
|
95
|
-
```bash
|
|
96
|
-
cd win11
|
|
97
|
-
bash build.sh
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
`build.sh` 会把签名好的 `DshOpenWith.msix`、`dsh-open-with.cer` 和 `version.txt` 复制到 `../native/`,插件启用时按 `version.txt` 决定要不要重装。
|
|
101
|
-
|
|
102
|
-
### 踩过的坑(都已在代码里修掉)
|
|
103
|
-
|
|
104
|
-
1. **清单里的 GUID 不能带花括号**,否则 `Add-AppxPackage` 报 `0x80080204 / 0xC00CE169`。
|
|
105
|
-
2. **DLL 必须内嵌 msix 身份清单**,否则资源管理器根本不会加载这个 COM 类(表现为"装好了但菜单里没有")。
|
|
106
|
-
3. DLL 必须 `-static` 链接:包目录里没有 `libwinpthread-1.dll`。
|
|
107
|
-
4. `GetState` 不要依赖选中项解析结果:新式菜单取「文件夹空白处」时 `IShellItemArray` 是空的,返回 `ECS_HIDDEN` 会把菜单项藏起来。恒返回 `ECS_ENABLED`,路径解析放到 `Invoke`。
|
|
108
|
-
5. AppX 代理进程里 `GetModuleFileNameW(NULL)` 返回的是代理进程的路径,图标要按 `DllMain` 保存的 `hinst` 取自身目录。
|
|
109
|
-
6. 自签证书必须进 `LocalMachine\TrustedPeople`;`CurrentUser\Root` / `CurrentUser\TrustedPeople` 都不够。
|
|
110
|
-
|
|
111
|
-
## 开发
|
|
112
|
-
|
|
113
|
-
```bash
|
|
114
|
-
# 宿主侧:注册表、待办扫描、HTTP 路由、启用/停用清理
|
|
115
|
-
# USERPROFILE 指向一个空目录(要用 Windows 路径写法),DSH_EXPLORER_OPEN_TEST_HOME=1 是防误跑的开关
|
|
116
|
-
USERPROFILE='E:\tmp\dsh-explorer-open-home' DSH_EXPLORER_OPEN_TEST_HOME=1 \
|
|
117
|
-
DSH_EXPLORER_OPEN_REG_BASE='HKCU\Software\Classes\__dsh_explorer_open_test' \
|
|
118
|
-
DSH_EXPLORER_OPEN_NO_MSIX=1 node test/host-test.mjs
|
|
119
|
-
|
|
120
|
-
# 页面侧:调用顺序、参数、@引用拼装、失败回退
|
|
121
|
-
node test/client-test.mjs
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
两个脚本都用假的 ctx / fetch,不会碰真实的菜单和 MSIX 包。
|
|
125
|
-
|
|
126
|
-
- `HANDOFF.md`:开发笔记(决策记录、踩坑、待办),中文。
|
|
127
|
-
- `win11/build/` 与 `.probe/` 是构建产物和本地临时目录,不纳入版本库。
|
|
128
|
-
|
|
129
|
-
##
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
-
|
|
134
|
-
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
1
|
+
# dsh-explorer-open
|
|
2
|
+
|
|
3
|
+
在 Windows 资源管理器的右键菜单里加一条 **「通过 dsh 打开」**:点一下就在 [DeepSeek Harness](https://github.com/deepseek-ai) 里打开这个文件夹(没有工作区就新建),并开一个新会话。
|
|
4
|
+
|
|
5
|
+
Win11 的**新式简洁菜单**和**旧式完整菜单**里都有这一条,都带图标。
|
|
6
|
+
|
|
7
|
+
- 仓库:<https://github.com/ame-code/dsh-explorer-open>
|
|
8
|
+
- npm:`dsh-explorer-open`
|
|
9
|
+
|
|
10
|
+
## 功能
|
|
11
|
+
|
|
12
|
+
| 在哪儿右键 | 打开的文件夹 | 放进新会话的草稿 |
|
|
13
|
+
| --- | --- | --- |
|
|
14
|
+
| 文件夹空白处 | 当前文件夹 | 无 |
|
|
15
|
+
| 文件夹图标上 | 这个文件夹 | 无 |
|
|
16
|
+
| 任意文件上 | 文件所在的文件夹 | `@文件名` 引用(工作区之外的路径不引用) |
|
|
17
|
+
|
|
18
|
+
- 每次都**新建会话**并切过去;工作区按完整路径复用,不会重复创建。
|
|
19
|
+
- dsh 没开时会先把它拉起来,请求排队等页面连上再执行。
|
|
20
|
+
- 插件停用/卸载时会**自己拆掉菜单**(旧式注册表键 + Win11 新式 MSIX 包),不留垃圾。
|
|
21
|
+
|
|
22
|
+
## 状态
|
|
23
|
+
|
|
24
|
+
当前 **0.1.x**:功能可用,但接口(`/api/plugins/...` 路径、`~/.dsh/dsh-explorer-open/` 状态目录、`DSH_EXPLORER_OPEN_*` 环境变量)和安装方式都还可能变,破坏性改动不保证向后兼容。
|
|
25
|
+
|
|
26
|
+
原生组件(`win11/`)有独立的版本号,见 `native/version.txt`;JS 侧版本见 `package.json`。
|
|
27
|
+
|
|
28
|
+
## 安装
|
|
29
|
+
|
|
30
|
+
### 从 npm 安装(推荐)
|
|
31
|
+
|
|
32
|
+
在 dsh 的插件设置里安装 `dsh-explorer-open`,或让 agent 执行安装(bundle 安装入口接受 npm 包名)。
|
|
33
|
+
|
|
34
|
+
首次启用时会自动装 Win11 新式菜单所需的 MSIX 包。因为是自签证书,包里没有受信的根,Windows 会拒绝安装并给出:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
Deployment failed with HRESULT: 0x800B0109
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
这时以管理员身份运行一次插件目录下的 `native/trust-cert.bat`(把自签证书 `dsh-open-with.cer` 导入 `LocalMachine\TrustedPeople`),然后重启 dsh 即可。只需要做一次。
|
|
41
|
+
|
|
42
|
+
> 只想要旧式菜单、不想动证书:设环境变量 `DSH_EXPLORER_OPEN_NO_MSIX=1` 再启动 dsh,插件就不会去装 MSIX 包。
|
|
43
|
+
|
|
44
|
+
### 从源码/本地目录安装
|
|
45
|
+
|
|
46
|
+
```powershell
|
|
47
|
+
# 仓库本身就是插件包(package.json 在根目录)
|
|
48
|
+
plugin_manager install_bundle E:\work\tools\dsh-open-with
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
改过代码后要重启 dsh 才会重新加载宿主侧插件;客户端 bundle 需要刷新页面。
|
|
52
|
+
|
|
53
|
+
## 工作原理
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
资源管理器右键
|
|
57
|
+
├─ 旧式菜单:HKCU\Software\Classes\{Directory\Background, Directory, *}\shell\OpenWithDsh
|
|
58
|
+
│ └─ powershell -File launch.ps1 "%V" (无窗口,写入待办后退出)
|
|
59
|
+
└─ 新式菜单:MSIX 包身份 + 原生 IExplorerCommand
|
|
60
|
+
└─ 同一个 launch.ps1
|
|
61
|
+
↓
|
|
62
|
+
%USERPROFILE%\.dsh\dsh-explorer-open\pending.json {folder, file?, token, at}
|
|
63
|
+
↓ 宿主侧插件每 300ms 扫描
|
|
64
|
+
GET /api/plugins/dsh-explorer-open/request (带 x-dsh-explorer-open 头)
|
|
65
|
+
↓ 页面侧插件每 1s 轮询
|
|
66
|
+
workspaces.create → sessions.create → uiWorkspace.openSession
|
|
67
|
+
↓ 文件右键时
|
|
68
|
+
conversation.input.requestDraftInitialization(binding, { prompt: '@文件', clearPreviousDraft: true })
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
- 启动器用 PowerShell 5.1 写 UTF-8(**无 BOM**)、先写临时文件再改名,页面不会读到半个 JSON。
|
|
72
|
+
- `/api/...` 路由要求自定义请求头,跨站页面带不上它(会先触发预检),因此别的网页没法领走排队中的请求。
|
|
73
|
+
- dsh 可执行文件位置由插件写进 `config.json`,启动器按 config → `DSH_EXPLORER_OPEN_EXE` → 常见安装路径的顺序查找。
|
|
74
|
+
|
|
75
|
+
## win11/ 原生组件
|
|
76
|
+
|
|
77
|
+
Win11 新式菜单不接受「注册表 + exe」的旧式扩展,必须是一个**有包身份、已签名、且 DLL 内嵌了 msix 身份清单**的 COM 组件。`win11/` 就是这个组件:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
win11/
|
|
81
|
+
├─ src/dsh_open_with.cpp IExplorerCommand 实现(GetTitle/GetIcon/GetState/Invoke)
|
|
82
|
+
├─ src/stub.cpp 满足清单的占位 exe(Windows.FullTrustApplication 入口,永不被启动)
|
|
83
|
+
├─ src/test-host.cpp 不经过资源管理器的自测宿主
|
|
84
|
+
├─ src/msix.manifest DLL 内嵌的 msix 身份(publisher/packageName/applicationId)
|
|
85
|
+
├─ src/msix.rc 把上面那份清单编进 PE 资源
|
|
86
|
+
├─ assets/dsh.ico 菜单图标
|
|
87
|
+
├─ package/AppxManifest.xml MSIX 清单:comServer + fileExplorerContextMenus
|
|
88
|
+
├─ build.sh 构建 → 签名 → 打包 → 同步到 ../native/
|
|
89
|
+
├─ install-msix.ps1 / uninstall-msix.ps1
|
|
90
|
+
└─ trust-cert.ps1 / trust-cert.bat 把自签证书导入受信存储
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
重新构建(需要 MinGW-w64、`windres`、Windows SDK BuildTools 的 `makeappx`/`signtool`,以及 `Cert:\CurrentUser\My` 里的 `CN=DshOpenWith` 证书):
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
cd win11
|
|
97
|
+
bash build.sh
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`build.sh` 会把签名好的 `DshOpenWith.msix`、`dsh-open-with.cer` 和 `version.txt` 复制到 `../native/`,插件启用时按 `version.txt` 决定要不要重装。
|
|
101
|
+
|
|
102
|
+
### 踩过的坑(都已在代码里修掉)
|
|
103
|
+
|
|
104
|
+
1. **清单里的 GUID 不能带花括号**,否则 `Add-AppxPackage` 报 `0x80080204 / 0xC00CE169`。
|
|
105
|
+
2. **DLL 必须内嵌 msix 身份清单**,否则资源管理器根本不会加载这个 COM 类(表现为"装好了但菜单里没有")。
|
|
106
|
+
3. DLL 必须 `-static` 链接:包目录里没有 `libwinpthread-1.dll`。
|
|
107
|
+
4. `GetState` 不要依赖选中项解析结果:新式菜单取「文件夹空白处」时 `IShellItemArray` 是空的,返回 `ECS_HIDDEN` 会把菜单项藏起来。恒返回 `ECS_ENABLED`,路径解析放到 `Invoke`。
|
|
108
|
+
5. AppX 代理进程里 `GetModuleFileNameW(NULL)` 返回的是代理进程的路径,图标要按 `DllMain` 保存的 `hinst` 取自身目录。
|
|
109
|
+
6. 自签证书必须进 `LocalMachine\TrustedPeople`;`CurrentUser\Root` / `CurrentUser\TrustedPeople` 都不够。
|
|
110
|
+
|
|
111
|
+
## 开发
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# 宿主侧:注册表、待办扫描、HTTP 路由、启用/停用清理
|
|
115
|
+
# USERPROFILE 指向一个空目录(要用 Windows 路径写法),DSH_EXPLORER_OPEN_TEST_HOME=1 是防误跑的开关
|
|
116
|
+
USERPROFILE='E:\tmp\dsh-explorer-open-home' DSH_EXPLORER_OPEN_TEST_HOME=1 \
|
|
117
|
+
DSH_EXPLORER_OPEN_REG_BASE='HKCU\Software\Classes\__dsh_explorer_open_test' \
|
|
118
|
+
DSH_EXPLORER_OPEN_NO_MSIX=1 node test/host-test.mjs
|
|
119
|
+
|
|
120
|
+
# 页面侧:调用顺序、参数、@引用拼装、失败回退
|
|
121
|
+
node test/client-test.mjs
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
两个脚本都用假的 ctx / fetch,不会碰真实的菜单和 MSIX 包。
|
|
125
|
+
|
|
126
|
+
- `HANDOFF.md`:开发笔记(决策记录、踩坑、待办),中文。
|
|
127
|
+
- `win11/build/` 与 `.probe/` 是构建产物和本地临时目录,不纳入版本库。
|
|
128
|
+
|
|
129
|
+
## 排查
|
|
130
|
+
|
|
131
|
+
点菜单后如果只把 dsh 窗口调到前面、没有打开工作区,先看两个地方:
|
|
132
|
+
|
|
133
|
+
1. `~/.dsh/dsh-explorer-open/trace.log` —— 宿主记录页面何时取走请求(`poll served=...`)、页面回报的处理结果(`ack stage=start|ok|fail`)。
|
|
134
|
+
- 只有 `poll served` 没有 `ack`:页面里的客户端没跑起来,刷新页面(Ctrl+R)或重启 dsh。
|
|
135
|
+
- `ack stage=fail`:带的是失败原因,多半是工作区/会话服务报错。
|
|
136
|
+
- 连 `poll served` 都没有:请求没到宿主,检查 `~/.dsh/dsh-explorer-open/pending.json` 是否被写出来
|
|
137
|
+
(经典菜单走注册表里的 `launch.ps1`;Win11 新式菜单走 MSIX 里的 `DshOpenWith.dll`,两者都指向这个目录)。
|
|
138
|
+
2. `LOCALAPPDATA%\dsh-explorer-open-command.log` —— 只有设了 `DSH_EXPLORER_OPEN_DEBUG=1` 时,原生组件才写这个日志。
|
|
139
|
+
|
|
140
|
+
## 已知限制
|
|
141
|
+
|
|
142
|
+
- 只在 Windows 上工作(注册表 + MSIX + 资源管理器)。
|
|
143
|
+
- `*` 键会让这一条出现在**所有**文件类型上,包括没有关联程序的。
|
|
144
|
+
- 以管理员权限运行的资源管理器不会显示用户级(HKCU)菜单项。
|
|
145
|
+
- 新式菜单需要证书受信;分发第三方时应该用正式代码签名证书,而不是自签。
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
MIT
|
package/client.js
CHANGED
|
@@ -2,6 +2,7 @@ window.__ModuleLoader__.load({
|
|
|
2
2
|
id: 'dsh-explorer-open',
|
|
3
3
|
factory(require) {
|
|
4
4
|
const REQUEST_URL = '/api/plugins/dsh-explorer-open/request'
|
|
5
|
+
const ACK_URL = '/api/plugins/dsh-explorer-open/ack'
|
|
5
6
|
// 自定义请求头:跨站页面无法在不触发预检的情况下带上它,
|
|
6
7
|
// 可以挡住别的网页偷偷 GET 掉排队中的请求。
|
|
7
8
|
const REQUEST_HEADER = 'x-dsh-explorer-open'
|
|
@@ -15,6 +16,16 @@ window.__ModuleLoader__.load({
|
|
|
15
16
|
else console.warn(`[dsh-explorer-open] ${message}`, error)
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
/** 把处理进度回报给宿主(写 trace.log),失败不影响主流程。 */
|
|
20
|
+
function ack(stage, detail) {
|
|
21
|
+
try {
|
|
22
|
+
const url = `${ACK_URL}?stage=${encodeURIComponent(stage)}&detail=${encodeURIComponent(detail ?? '')}`
|
|
23
|
+
void fetch(url, { cache: 'no-store', headers: { [REQUEST_HEADER]: '1' } })
|
|
24
|
+
} catch {
|
|
25
|
+
// 忽略
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
18
29
|
/** 打开工作区(已存在则复用),在其中新建一个会话,再切过去。 */
|
|
19
30
|
async function handle(folder, file) {
|
|
20
31
|
// create 对同一路径是幂等的:有就返回已有的,没有才新建
|
|
@@ -113,9 +124,12 @@ window.__ModuleLoader__.load({
|
|
|
113
124
|
for (const request of data.requests) {
|
|
114
125
|
if (request === null || typeof request.folder !== 'string') continue
|
|
115
126
|
const file = typeof request.file === 'string' ? request.file : undefined
|
|
127
|
+
ack('start', file ?? request.folder)
|
|
116
128
|
try {
|
|
117
129
|
await handle(request.folder, file)
|
|
130
|
+
ack('ok', file ?? request.folder)
|
|
118
131
|
} catch (error) {
|
|
132
|
+
ack('fail', `${file ?? request.folder} :: ${error?.message ?? error}`)
|
|
119
133
|
log(`打开失败: ${request.folder}`, error)
|
|
120
134
|
}
|
|
121
135
|
}
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
2
2
|
import { homedir } from 'node:os'
|
|
3
3
|
import { dirname, join } from 'node:path'
|
|
4
4
|
import { fileURLToPath } from 'node:url'
|
|
@@ -12,6 +12,20 @@ const PENDING_FILE = join(PENDING_DIR, 'pending.json')
|
|
|
12
12
|
/** 页面询问「有没有新的右键请求」的接口。 */
|
|
13
13
|
export const REQUEST_PATH = '/api/plugins/dsh-explorer-open/request'
|
|
14
14
|
|
|
15
|
+
/** 页面回报处理结果的接口(诊断用,写 trace.log)。 */
|
|
16
|
+
export const ACK_PATH = '/api/plugins/dsh-explorer-open/ack'
|
|
17
|
+
|
|
18
|
+
/** 追踪日志:记录页面取走请求、以及处理结果。 */
|
|
19
|
+
const TRACE_FILE = join(PENDING_DIR, 'trace.log')
|
|
20
|
+
|
|
21
|
+
function trace(line) {
|
|
22
|
+
try {
|
|
23
|
+
appendFileSync(TRACE_FILE, `${new Date().toISOString()} ${line}\n`, 'utf8')
|
|
24
|
+
} catch {
|
|
25
|
+
// 诊断日志失败不影响主流程
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
15
29
|
/** 页面轮询时必须带上的请求头,见 client.js。 */
|
|
16
30
|
const REQUEST_HEADER = 'x-dsh-explorer-open'
|
|
17
31
|
|
|
@@ -191,6 +205,27 @@ export function apply(ctx) {
|
|
|
191
205
|
return () => clearInterval(timer)
|
|
192
206
|
})
|
|
193
207
|
|
|
208
|
+
// 2b. 页面把处理结果回报到这里,写进 trace.log 便于排查
|
|
209
|
+
ctx.inject(['webServer'], (webCtx) => {
|
|
210
|
+
webCtx.effect(() =>
|
|
211
|
+
webCtx.webServer.register({
|
|
212
|
+
kind: 'exact',
|
|
213
|
+
path: ACK_PATH,
|
|
214
|
+
resource: async (request) => {
|
|
215
|
+
if (request?.headers?.get?.(REQUEST_HEADER) !== '1') {
|
|
216
|
+
return new Response(JSON.stringify({ error: 'forbidden' }), { status: 403 })
|
|
217
|
+
}
|
|
218
|
+
const url = new URL(request.url, 'http://localhost')
|
|
219
|
+
trace(`ack stage=${url.searchParams.get('stage') ?? ''} detail=${url.searchParams.get('detail') ?? ''}`)
|
|
220
|
+
return new Response(JSON.stringify({ ok: true }), {
|
|
221
|
+
status: 200,
|
|
222
|
+
headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' },
|
|
223
|
+
})
|
|
224
|
+
},
|
|
225
|
+
}),
|
|
226
|
+
)
|
|
227
|
+
})
|
|
228
|
+
|
|
194
229
|
// 3. 页面轮询这个接口领取请求
|
|
195
230
|
ctx.inject(['webServer'], (webCtx) => {
|
|
196
231
|
webCtx.effect(() =>
|
|
@@ -208,6 +243,7 @@ export function apply(ctx) {
|
|
|
208
243
|
const now = Date.now()
|
|
209
244
|
const taken = queue.filter((item) => now - item.at <= REQUEST_TTL_MS)
|
|
210
245
|
queue = []
|
|
246
|
+
if (taken.length > 0) trace(`poll served=${taken.map((item) => item.file ?? item.folder).join(',')}`)
|
|
211
247
|
const reply = { status: taken.length === 0 ? 'idle' : 'open', requests: taken }
|
|
212
248
|
return new Response(JSON.stringify(reply), {
|
|
213
249
|
status: 200,
|
package/native/DshOpenWith.msix
CHANGED
|
Binary file
|
package/native/version.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.0.
|
|
1
|
+
1.0.0.8
|