dsh-everything-search 1.0.0
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/LICENSE +21 -0
- package/README.md +113 -0
- package/cordis.patch.yml +6 -0
- package/lib/client.js +406 -0
- package/lib/es.exe +0 -0
- package/lib/index.js +114 -0
- package/package.json +39 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-everything-search contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# 🔍 Everything 全盘搜索
|
|
4
|
+
|
|
5
|
+
**DeepSeek Harness(DSH)插件** —— 基于 [Everything](https://www.voidtools.com/) 的全盘极速文件搜索 · 多选加入上下文 · 设置可自定义
|
|
6
|
+
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
[](https://github.com/)
|
|
9
|
+
[](https://github.com/)
|
|
10
|
+
|
|
11
|
+
[✨ 功能](#-功能) · [📥 安装](#-安装) · [🖥 使用](#-使用) · [⚙️ 设置](#️-设置) · [❓ FAQ](#-常见问题)
|
|
12
|
+
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## ✨ 功能
|
|
18
|
+
|
|
19
|
+
| 模块 | 能力 |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| **全盘极速搜索** | 调用 Everything 索引,瞬间搜遍全部硬盘(C / D / E / F …),比 `grep` / `glob` 更快更全 |
|
|
22
|
+
| **Everything 语法** | 支持 `*.pdf`、`ext:png`、`文件夹名\` 等 Everything 原生搜索语法 |
|
|
23
|
+
| **类型筛选** | 全部 / 文件夹 / 文件 三种范围,切换即自动重新搜索 |
|
|
24
|
+
| **文件夹限定** | 可指定「范围」文件夹路径,只搜该文件夹内(`-path`) |
|
|
25
|
+
| **多选加入上下文** | 搜索结果可勾选多个文件/文件夹,一键以 `@引用` 写入输入框,加入对话上下文 |
|
|
26
|
+
| **自定义设置** | 默认结果数量、默认类型、搜索引擎路径(es.exe)均可修改,localStorage 落盘 |
|
|
27
|
+
| **模型工具** | 注册 `everything_search` 工具,Agent 可直接询问"帮我找某个文件" |
|
|
28
|
+
| **es.exe 随包** | 搜索工具 es.exe 随插件打包在 `lib/es.exe`,随包即用,无需手动放置或依赖本机 Everything 安装 |
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 📥 安装
|
|
33
|
+
|
|
34
|
+
### 前置条件
|
|
35
|
+
|
|
36
|
+
- 已安装并运行 **DeepSeek Harness**
|
|
37
|
+
- 本机已安装 **Everything**(用于建立文件索引;es.exe 命令行工具随插件打包)
|
|
38
|
+
|
|
39
|
+
### 安装(本地 / 源码方式)
|
|
40
|
+
|
|
41
|
+
在 DSH 环境(或已配置 `DSH_HOME`)中运行:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
dsh plugin --profile web add <本插件包路径>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
例如(Windows):
|
|
48
|
+
|
|
49
|
+
```powershell
|
|
50
|
+
dsh plugin --profile web add <本插件包路径>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
> `link:` 会把插件链接为依赖;`dsh plugin add` 会应用 `cordis.patch.yml` 把插件行插入 web 插件表。
|
|
54
|
+
|
|
55
|
+
### 安装(发布到 npm / Git 后)
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
dsh plugin --profile web add dsh-everything-search
|
|
59
|
+
# 或
|
|
60
|
+
dsh plugin --profile web add github:yourname/dsh-everything-search
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 生效
|
|
64
|
+
|
|
65
|
+
安装完成后**重启 DeepSeek Harness**,输入框工具栏出现放大镜 🔍 按钮即生效。之后**刷新浏览器,插件始终保持**(正式 web 打包插件,自动加载)。
|
|
66
|
+
|
|
67
|
+
### 卸载
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
dsh plugin --profile web rm dsh-everything-search
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 🖥 使用
|
|
76
|
+
|
|
77
|
+
1. 点击输入框工具栏的 **放大镜 🔍** 按钮
|
|
78
|
+
2. 输入关键词(支持 Everything 语法),回车或点「搜索」
|
|
79
|
+
3. 用「类型」「范围」筛选;勾选想要的文件/文件夹
|
|
80
|
+
4. 点「**加入上下文**」→ 选中的路径以 `@引用` 写入输入框,确认发送即可让模型读取
|
|
81
|
+
|
|
82
|
+
或直接在对话里让 Agent 调用 `everything_search` 工具帮你搜。
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## ⚙️ 设置
|
|
87
|
+
|
|
88
|
+
设置 → 侧边栏「**Everything 搜索**」:
|
|
89
|
+
|
|
90
|
+
- **默认结果数量**:每次搜索最多返回条数(20 / 50 / 100 / 200)
|
|
91
|
+
- **默认类型**:打开面板时默认筛选(全部 / 文件夹 / 文件)
|
|
92
|
+
- **搜索引擎路径**:es.exe 路径(默认随插件打包位置,可修改)
|
|
93
|
+
|
|
94
|
+
设置保存在浏览器 localStorage,刷新后保留。
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## ❓ FAQ
|
|
99
|
+
|
|
100
|
+
**为什么需要安装 Everything?**
|
|
101
|
+
插件用 Everything 的 `es.exe` 命令行工具搜索。Everything 需要先建立全盘索引(首次运行时会自动建)。es.exe 随插件打包,但索引服务(Everything 主程序)建议本机安装。
|
|
102
|
+
|
|
103
|
+
**可以纯离线使用吗?**
|
|
104
|
+
可以 —— es.exe 随插件打包在 `lib/es.exe`,无需联网下载;搜索走本机 Everything 索引。
|
|
105
|
+
|
|
106
|
+
**支持中文路径吗?**
|
|
107
|
+
支持。搜索采用 UTF-8 导出再读取,中文文件名/路径不乱码。
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## 📄 License
|
|
112
|
+
|
|
113
|
+
[MIT](LICENSE)
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# dsh-everything-search bundle patch: inserts the dual-face plugin row into the
|
|
2
|
+
# web profile roster. Applied as a profile bundle layer (the `dsh.bundle.patch`
|
|
3
|
+
# manifest field) over dsh-base. Install with `dsh plugin --profile web add <path>`.
|
|
4
|
+
- insert:
|
|
5
|
+
- id: dsh-everything-search
|
|
6
|
+
name: 'dsh-everything-search'
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
// dsh-everything-search —— Client 半端(静态 web 插件形态,ModuleLoader bundle)
|
|
2
|
+
// 经 /plugins/dsh-everything-search/client.js 加载。RPC 通过 fetch POST /evs/api/<name>。
|
|
3
|
+
window.__ModuleLoader__.load({
|
|
4
|
+
id: 'dsh-everything-search',
|
|
5
|
+
factory: (require) => {
|
|
6
|
+
var module = { exports: {} }
|
|
7
|
+
var exports = module.exports
|
|
8
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
|
|
9
|
+
var React = require('react')
|
|
10
|
+
|
|
11
|
+
async function apiCall(name, args) {
|
|
12
|
+
const res = await fetch('/evs/api/' + name, {
|
|
13
|
+
method: 'POST',
|
|
14
|
+
headers: { 'Content-Type': 'application/json' },
|
|
15
|
+
body: JSON.stringify(args || {}),
|
|
16
|
+
})
|
|
17
|
+
if (!res.ok) throw new Error('HTTP ' + res.status)
|
|
18
|
+
return await res.json()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function insertStyles(css) {
|
|
22
|
+
try {
|
|
23
|
+
const style = document.createElement('style')
|
|
24
|
+
style.textContent = css
|
|
25
|
+
document.head.appendChild(style)
|
|
26
|
+
return () => { try { style.remove() } catch (e) { /* ignore */ } }
|
|
27
|
+
} catch (e) { return () => {} }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ---- 共享状态(按钮、覆盖层、设置页协调) ----
|
|
31
|
+
var searchOpen = false
|
|
32
|
+
var listeners = new Set()
|
|
33
|
+
var commitHandler = null
|
|
34
|
+
function notifier() { listeners.forEach(function (fn) { fn() }) }
|
|
35
|
+
function openSearch() { if (!searchOpen) { searchOpen = true; notifier() } }
|
|
36
|
+
function closeSearch() { if (searchOpen) { searchOpen = false; notifier() } }
|
|
37
|
+
function setCommitHandler(fn) { commitHandler = fn }
|
|
38
|
+
function commit(paths) { if (commitHandler) commitHandler(paths) }
|
|
39
|
+
|
|
40
|
+
// ---- 插件设置(localStorage 落盘) ----
|
|
41
|
+
var settings = { limit: 50, kind: 'all', path: '' }
|
|
42
|
+
var settingsListeners = new Set()
|
|
43
|
+
function getSettings() { return settings }
|
|
44
|
+
function updateSettings(patch) {
|
|
45
|
+
settings = Object.assign({}, settings, patch)
|
|
46
|
+
settingsListeners.forEach(function (fn) { fn() })
|
|
47
|
+
}
|
|
48
|
+
function subscribeSettings(fn) { settingsListeners.add(fn); return function () { settingsListeners.delete(fn) } }
|
|
49
|
+
function loadStorage() {
|
|
50
|
+
try {
|
|
51
|
+
if (typeof localStorage === 'undefined') return null
|
|
52
|
+
const raw = localStorage.getItem('evsr-settings')
|
|
53
|
+
if (!raw) return null
|
|
54
|
+
const p = JSON.parse(raw)
|
|
55
|
+
return {
|
|
56
|
+
limit: Number(p.limit) > 0 ? Math.min(Number(p.limit), 200) : 50,
|
|
57
|
+
kind: (p.kind === 'folder' || p.kind === 'file' || p.kind === 'all') ? p.kind : 'all',
|
|
58
|
+
path: (typeof p.path === 'string' && p.path.trim()) ? p.path : '',
|
|
59
|
+
}
|
|
60
|
+
} catch (e) { return null }
|
|
61
|
+
}
|
|
62
|
+
function saveStorage() {
|
|
63
|
+
try {
|
|
64
|
+
if (typeof localStorage === 'undefined') return
|
|
65
|
+
localStorage.setItem('evsr-settings', JSON.stringify({ limit: settings.limit, kind: settings.kind, path: settings.path }))
|
|
66
|
+
} catch (e) { /* ignore */ }
|
|
67
|
+
}
|
|
68
|
+
function reloadSettings() { const p = loadStorage(); if (p) updateSettings(p) }
|
|
69
|
+
function saveSettings(patch) { updateSettings(patch); saveStorage() }
|
|
70
|
+
|
|
71
|
+
// 拉取插件默认 es.exe 路径(打包在插件 lib 里的),作为路径默认值
|
|
72
|
+
apiCall('default-path').then(function (p) {
|
|
73
|
+
if (typeof p === 'string' && p.trim()) { if (!getSettings().path) saveSettings({ path: p }) }
|
|
74
|
+
}, function () { /* ignore */ })
|
|
75
|
+
|
|
76
|
+
function toMention(path) {
|
|
77
|
+
const p = String(path || '').replace(/\\/g, '/')
|
|
78
|
+
if (/[\u0000-\u001f\u007f-\u009f"]/.test(p)) return null
|
|
79
|
+
if (/\s/.test(p)) return '@"' + p + '"'
|
|
80
|
+
return '@' + p
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
var inject = []
|
|
84
|
+
|
|
85
|
+
function apply(ctx) {
|
|
86
|
+
insertStyles(
|
|
87
|
+
'.evs-trigger { display: inline-flex; align-items: center; justify-content: center; background: transparent; border: none; cursor: pointer; padding: 5px 7px; border-radius: 8px; color: var(--dsw-alias-label-primary); }\n' +
|
|
88
|
+
'.evs-trigger:hover { background: var(--dsw-alias-bg-layer-2); }\n' +
|
|
89
|
+
'.evs-trigger svg { display: block; }\n' +
|
|
90
|
+
'.evs-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,.5); z-index: 9999; pointer-events: auto; }\n' +
|
|
91
|
+
'.evs-modal { position: fixed; bottom: 0; max-height: 88vh; background: var(--dsw-alias-bg-overlay); color: var(--dsw-alias-label-primary); border: 1px solid var(--dsw-alias-border-l1); border-bottom: none; border-radius: 14px 14px 0 0; box-shadow: 0 -12px 40px rgba(0,0,0,.45); display: flex; flex-direction: column; overflow: hidden; }\n' +
|
|
92
|
+
'.evs-titlebar { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; border-bottom: 1px solid var(--dsw-alias-border-l1); }\n' +
|
|
93
|
+
'.evs-tb-title { font-size: 14px; font-weight: 600; }\n' +
|
|
94
|
+
'.evs-tb-close { background: transparent; border: none; color: var(--dsw-alias-label-secondary); cursor: pointer; font-size: 12px; padding: 4px 8px; border-radius: 6px; }\n' +
|
|
95
|
+
'.evs-tb-close:hover { background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); }\n' +
|
|
96
|
+
'.evs-body { padding: 12px 16px 0; display: flex; flex-direction: column; gap: 12px; flex: 1; overflow: hidden; }\n' +
|
|
97
|
+
'.evs-desc { font-size: 13px; color: var(--dsw-alias-label-secondary); }\n' +
|
|
98
|
+
'.evs-search-row { display: flex; gap: 8px; align-items: center; }\n' +
|
|
99
|
+
'.evs-input { flex: 1; padding: 8px 12px; border-radius: 8px; border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); font: inherit; }\n' +
|
|
100
|
+
'.evs-input:focus { outline: 2px solid var(--dsw-alias-brand-primary); }\n' +
|
|
101
|
+
'.evs-filter-row { display: flex; align-items: center; gap: 10px; }\n' +
|
|
102
|
+
'.evs-scope-row { display: flex; align-items: center; gap: 10px; }\n' +
|
|
103
|
+
'.evs-filter-label { font-size: 12px; color: var(--dsw-alias-label-secondary); white-space: nowrap; }\n' +
|
|
104
|
+
'.evs-seg { display: flex; gap: 4px; background: var(--dsw-alias-bg-layer-2); padding: 3px; border-radius: 8px; }\n' +
|
|
105
|
+
'.evs-seg-btn { padding: 4px 14px; border: none; background: transparent; color: var(--dsw-alias-label-secondary); cursor: pointer; border-radius: 6px; font-size: 12px; }\n' +
|
|
106
|
+
'.evs-seg-btn.active { background: var(--dsw-alias-bg-overlay); color: var(--dsw-alias-label-primary); box-shadow: 0 1px 3px rgba(0,0,0,.25); }\n' +
|
|
107
|
+
'.evs-results { flex: 1; overflow: auto; border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; }\n' +
|
|
108
|
+
'.evs-results-head { display: flex; justify-content: space-between; padding: 6px 10px; font-size: 12px; color: var(--dsw-alias-label-secondary); border-bottom: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-2); }\n' +
|
|
109
|
+
'.evs-row { display: flex; gap: 8px; align-items: center; padding: 6px 10px; cursor: pointer; font-family: monospace; font-size: 12px; }\n' +
|
|
110
|
+
'.evs-row:hover { background: var(--dsw-alias-bg-layer-2); }\n' +
|
|
111
|
+
'.evs-row.checked { background: var(--dsw-alias-bg-layer-2); }\n' +
|
|
112
|
+
'.evs-row input { flex-shrink: 0; }\n' +
|
|
113
|
+
'.evs-path { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n' +
|
|
114
|
+
'.evs-empty { padding: 16px 10px; color: var(--dsw-alias-label-secondary); font-size: 12px; text-align: center; }\n' +
|
|
115
|
+
'.evs-foot { display: flex; gap: 8px; align-items: center; padding: 12px 16px; border-top: 1px solid var(--dsw-alias-border-l1); }\n' +
|
|
116
|
+
'.evs-count { margin-right: auto; font-size: 12px; color: var(--dsw-alias-label-secondary); }\n' +
|
|
117
|
+
'.evs-btn { padding: 7px 14px; border-radius: 8px; border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); cursor: pointer; white-space: nowrap; font-size: 13px; }\n' +
|
|
118
|
+
'.evs-btn:hover { background: var(--dsw-alias-brand-primary); color: var(--dsw-alias-bg-base); }\n' +
|
|
119
|
+
'.evs-btn:disabled { opacity: .5; cursor: default; }\n' +
|
|
120
|
+
'.evs-btn.primary { background: var(--dsw-alias-brand-primary); color: var(--dsw-alias-bg-base); border-color: transparent; }\n' +
|
|
121
|
+
'.evs-btn.primary:hover { opacity: .9; }\n' +
|
|
122
|
+
'.evs-err { padding: 6px 10px; color: var(--dsw-alias-state-error-primary); font-size: 12px; }\n' +
|
|
123
|
+
'.evs-set { padding: 10px 4px 24px; display: flex; flex-direction: column; gap: 0; max-width: 620px; }\n' +
|
|
124
|
+
'.evs-set-intro { font-size: 13px; color: var(--dsw-alias-label-secondary); opacity: .8; padding-bottom: 16px; }\n' +
|
|
125
|
+
'.evs-set-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 14px 0; border-bottom: 1px solid var(--dsw-alias-border-l1); }\n' +
|
|
126
|
+
'.evs-set-info { display: flex; flex-direction: column; gap: 3px; min-width: 0; }\n' +
|
|
127
|
+
'.evs-set-label { font-size: 14px; color: var(--dsw-alias-label-primary); font-weight: 500; }\n' +
|
|
128
|
+
'.evs-set-desc { font-size: 12px; color: var(--dsw-alias-label-secondary); opacity: .8; }\n' +
|
|
129
|
+
'.evs-set-ctl { padding: 7px 12px; border-radius: 8px; border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); font: inherit; }\n' +
|
|
130
|
+
'.evs-set-input { flex: 1; min-width: 0; padding: 7px 12px; border-radius: 8px; border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); font-family: monospace; font-size: 12px; margin-left: 12px; }\n'
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
const slots = ctx.get('slots')
|
|
134
|
+
if (slots === undefined) return
|
|
135
|
+
|
|
136
|
+
function measureComposer() {
|
|
137
|
+
try {
|
|
138
|
+
if (typeof document === 'undefined') return null
|
|
139
|
+
const el = document.querySelector('textarea')
|
|
140
|
+
if (!el) return null
|
|
141
|
+
const rect = el.getBoundingClientRect()
|
|
142
|
+
if (!rect.width) return null
|
|
143
|
+
return { left: rect.left, width: rect.width }
|
|
144
|
+
} catch (e) { return null }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function SearchOverlay() {
|
|
148
|
+
const [open, setOpen] = React.useState(searchOpen)
|
|
149
|
+
const [query, setQuery] = React.useState('')
|
|
150
|
+
const [kind, setKind] = React.useState(getSettings().kind)
|
|
151
|
+
const [scope, setScope] = React.useState('')
|
|
152
|
+
const [busy, setBusy] = React.useState(false)
|
|
153
|
+
const [results, setResults] = React.useState([])
|
|
154
|
+
const [selected, setSelected] = React.useState([])
|
|
155
|
+
const [err, setErr] = React.useState('')
|
|
156
|
+
const [geom, setGeom] = React.useState(null)
|
|
157
|
+
|
|
158
|
+
React.useEffect(function () {
|
|
159
|
+
const fn = function () { setOpen(searchOpen) }
|
|
160
|
+
listeners.add(fn)
|
|
161
|
+
return function () { listeners.delete(fn) }
|
|
162
|
+
}, [])
|
|
163
|
+
|
|
164
|
+
React.useEffect(function () {
|
|
165
|
+
if (!open) return
|
|
166
|
+
reloadSettings()
|
|
167
|
+
setKind(getSettings().kind)
|
|
168
|
+
function measure() { setGeom(measureComposer()) }
|
|
169
|
+
measure()
|
|
170
|
+
let onResize = null
|
|
171
|
+
if (typeof window !== 'undefined') {
|
|
172
|
+
onResize = function () { measure() }
|
|
173
|
+
window.addEventListener('resize', onResize)
|
|
174
|
+
}
|
|
175
|
+
return function () { if (onResize) window.removeEventListener('resize', onResize) }
|
|
176
|
+
}, [open])
|
|
177
|
+
|
|
178
|
+
if (!open) return null
|
|
179
|
+
|
|
180
|
+
async function runSearch(q, k) {
|
|
181
|
+
const qq = (q !== undefined ? q : String(query || '')).trim()
|
|
182
|
+
const kk = k !== undefined ? k : kind
|
|
183
|
+
if (!qq) { setResults([]); setErr(''); return }
|
|
184
|
+
setBusy(true); setErr('')
|
|
185
|
+
try {
|
|
186
|
+
const a = { query: qq, limit: getSettings().limit, foldersOnly: kk === 'folder', filesOnly: kk === 'file' }
|
|
187
|
+
const pth = getSettings().path
|
|
188
|
+
if (pth) a.path = pth
|
|
189
|
+
const sc = String(scope || '').trim()
|
|
190
|
+
if (sc) a.scope = sc
|
|
191
|
+
const r = await apiCall('search', a)
|
|
192
|
+
if (r && r.ok) { setResults(r.results || []) } else { setResults([]); setErr(r && r.error ? r.error : '搜索失败') }
|
|
193
|
+
} catch (e) {
|
|
194
|
+
setResults([]); setErr(String(e && e.message ? e.message : e))
|
|
195
|
+
}
|
|
196
|
+
setBusy(false)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function chooseKind(k) {
|
|
200
|
+
setKind(k)
|
|
201
|
+
const q = String(query || '').trim()
|
|
202
|
+
if (q) runSearch(q, k)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function toggle(p) {
|
|
206
|
+
setSelected(function (prev) {
|
|
207
|
+
return prev.indexOf(p) >= 0 ? prev.filter(function (x) { return x !== p }) : prev.concat([p])
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
function toggleAll() {
|
|
211
|
+
setSelected(function (prev) {
|
|
212
|
+
if (prev.length === results.length) return []
|
|
213
|
+
return results.slice()
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
function addToContext() {
|
|
217
|
+
if (selected.length === 0) return
|
|
218
|
+
commit(selected)
|
|
219
|
+
closeSearch()
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const rows = results.map(function (p, i) {
|
|
223
|
+
const checked = selected.indexOf(p) >= 0
|
|
224
|
+
return React.createElement('label', { className: checked ? 'evs-row checked' : 'evs-row', key: i },
|
|
225
|
+
React.createElement('input', { type: 'checkbox', checked: checked, onChange: function () { toggle(p) } }),
|
|
226
|
+
React.createElement('span', { className: 'evs-path' }, p),
|
|
227
|
+
)
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
const modalStyle = geom
|
|
231
|
+
? { left: geom.left, width: geom.width }
|
|
232
|
+
: { left: '50%', transform: 'translateX(-50%)', maxWidth: '920px' }
|
|
233
|
+
|
|
234
|
+
const segs = [
|
|
235
|
+
{ key: 'all', label: '全部' },
|
|
236
|
+
{ key: 'folder', label: '文件夹' },
|
|
237
|
+
{ key: 'file', label: '文件' },
|
|
238
|
+
]
|
|
239
|
+
|
|
240
|
+
return React.createElement('div', { className: 'evs-backdrop', onClick: closeSearch },
|
|
241
|
+
React.createElement('div', { className: 'evs-modal', style: modalStyle, onClick: function (e) { e.stopPropagation() } },
|
|
242
|
+
React.createElement('div', { className: 'evs-titlebar' },
|
|
243
|
+
React.createElement('span', { className: 'evs-tb-title' }, 'Everything 全盘搜索'),
|
|
244
|
+
React.createElement('button', { className: 'evs-tb-close', onClick: closeSearch }, '✕ 关闭'),
|
|
245
|
+
),
|
|
246
|
+
React.createElement('div', { className: 'evs-body' },
|
|
247
|
+
React.createElement('div', { className: 'evs-desc' }, '在全盘快速搜索文件 / 文件夹,勾选后可加入对话上下文'),
|
|
248
|
+
React.createElement('div', { className: 'evs-search-row' },
|
|
249
|
+
React.createElement('input', {
|
|
250
|
+
className: 'evs-input',
|
|
251
|
+
placeholder: '输入关键词,回车搜索(支持 *.pdf、ext:ini 等语法)',
|
|
252
|
+
value: query,
|
|
253
|
+
onChange: function (e) { setQuery(e.target.value) },
|
|
254
|
+
onKeyDown: function (e) { if (e.key === 'Enter') runSearch() },
|
|
255
|
+
}),
|
|
256
|
+
React.createElement('button', { className: 'evs-btn', onClick: function () { runSearch() }, disabled: busy }, busy ? '…' : '搜索'),
|
|
257
|
+
),
|
|
258
|
+
React.createElement('div', { className: 'evs-filter-row' },
|
|
259
|
+
React.createElement('span', { className: 'evs-filter-label' }, '类型'),
|
|
260
|
+
React.createElement('div', { className: 'evs-seg' },
|
|
261
|
+
segs.map(function (s) {
|
|
262
|
+
return React.createElement('button', {
|
|
263
|
+
key: s.key,
|
|
264
|
+
className: kind === s.key ? 'evs-seg-btn active' : 'evs-seg-btn',
|
|
265
|
+
onClick: function () { chooseKind(s.key) },
|
|
266
|
+
}, s.label)
|
|
267
|
+
}),
|
|
268
|
+
),
|
|
269
|
+
),
|
|
270
|
+
React.createElement('div', { className: 'evs-scope-row' },
|
|
271
|
+
React.createElement('span', { className: 'evs-filter-label' }, '范围'),
|
|
272
|
+
React.createElement('input', {
|
|
273
|
+
className: 'evs-input',
|
|
274
|
+
placeholder: '可选:限定文件夹,如 C:/Project(留空搜全盘)',
|
|
275
|
+
value: scope,
|
|
276
|
+
onChange: function (e) { setScope(e.target.value) },
|
|
277
|
+
onKeyDown: function (e) { if (e.key === 'Enter') runSearch() },
|
|
278
|
+
}),
|
|
279
|
+
),
|
|
280
|
+
err ? React.createElement('div', { className: 'evs-err' }, err) : null,
|
|
281
|
+
React.createElement('div', { className: 'evs-results' },
|
|
282
|
+
React.createElement('div', { className: 'evs-results-head' },
|
|
283
|
+
React.createElement('span', null, '结果'),
|
|
284
|
+
React.createElement('span', null, String(results.length) + ' 条'),
|
|
285
|
+
),
|
|
286
|
+
rows.length > 0 ? rows : React.createElement('div', { className: 'evs-empty' }, '无匹配结果'),
|
|
287
|
+
),
|
|
288
|
+
),
|
|
289
|
+
React.createElement('div', { className: 'evs-foot' },
|
|
290
|
+
React.createElement('span', { className: 'evs-count' }, '已选 ' + selected.length),
|
|
291
|
+
React.createElement('button', { className: 'evs-btn', onClick: toggleAll }, '全选/清空'),
|
|
292
|
+
React.createElement('button', { className: 'evs-btn primary', onClick: addToContext, disabled: selected.length === 0 }, '加入上下文'),
|
|
293
|
+
),
|
|
294
|
+
),
|
|
295
|
+
)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function SettingsPage() {
|
|
299
|
+
const [s, setS] = React.useState(getSettings())
|
|
300
|
+
const [pathEdit, setPathEdit] = React.useState(getSettings().path)
|
|
301
|
+
React.useEffect(function () {
|
|
302
|
+
reloadSettings()
|
|
303
|
+
return subscribeSettings(function () { setS(getSettings()); setPathEdit(getSettings().path) })
|
|
304
|
+
}, [])
|
|
305
|
+
function commitPath() {
|
|
306
|
+
const p = String(pathEdit || '').trim()
|
|
307
|
+
if (p) { saveSettings({ path: p }) } else { setPathEdit(getSettings().path) }
|
|
308
|
+
}
|
|
309
|
+
const segs = [
|
|
310
|
+
{ key: 'all', label: '全部' },
|
|
311
|
+
{ key: 'folder', label: '文件夹' },
|
|
312
|
+
{ key: 'file', label: '文件' },
|
|
313
|
+
]
|
|
314
|
+
return React.createElement('div', { className: 'evs-set' },
|
|
315
|
+
React.createElement('div', { className: 'evs-set-intro' }, 'Everything 全盘搜索插件配置'),
|
|
316
|
+
React.createElement('div', { className: 'evs-set-row' },
|
|
317
|
+
React.createElement('div', { className: 'evs-set-info' },
|
|
318
|
+
React.createElement('div', { className: 'evs-set-label' }, '默认结果数量'),
|
|
319
|
+
React.createElement('div', { className: 'evs-set-desc' }, '搜索面板每次最多返回的条数'),
|
|
320
|
+
),
|
|
321
|
+
React.createElement('select', {
|
|
322
|
+
className: 'evs-set-ctl',
|
|
323
|
+
value: String(s.limit),
|
|
324
|
+
onChange: function (e) { saveSettings({ limit: Number(e.target.value) }) },
|
|
325
|
+
},
|
|
326
|
+
[20, 50, 100, 200].map(function (v) {
|
|
327
|
+
return React.createElement('option', { key: v, value: String(v) }, String(v) + ' 条')
|
|
328
|
+
}),
|
|
329
|
+
),
|
|
330
|
+
),
|
|
331
|
+
React.createElement('div', { className: 'evs-set-row' },
|
|
332
|
+
React.createElement('div', { className: 'evs-set-info' },
|
|
333
|
+
React.createElement('div', { className: 'evs-set-label' }, '默认类型'),
|
|
334
|
+
React.createElement('div', { className: 'evs-set-desc' }, '打开面板时默认筛选的类型'),
|
|
335
|
+
),
|
|
336
|
+
React.createElement('div', { className: 'evs-seg' },
|
|
337
|
+
segs.map(function (seg) {
|
|
338
|
+
return React.createElement('button', {
|
|
339
|
+
key: seg.key,
|
|
340
|
+
className: s.kind === seg.key ? 'evs-seg-btn active' : 'evs-seg-btn',
|
|
341
|
+
onClick: function () { saveSettings({ kind: seg.key }) },
|
|
342
|
+
}, seg.label)
|
|
343
|
+
}),
|
|
344
|
+
),
|
|
345
|
+
),
|
|
346
|
+
React.createElement('div', { className: 'evs-set-row' },
|
|
347
|
+
React.createElement('div', { className: 'evs-set-info' },
|
|
348
|
+
React.createElement('div', { className: 'evs-set-label' }, '搜索引擎路径'),
|
|
349
|
+
React.createElement('div', { className: 'evs-set-desc' }, 'Everything 官方命令行工具(随插件打包,可修改)'),
|
|
350
|
+
),
|
|
351
|
+
React.createElement('input', {
|
|
352
|
+
className: 'evs-set-input',
|
|
353
|
+
type: 'text',
|
|
354
|
+
value: pathEdit,
|
|
355
|
+
onChange: function (e) { setPathEdit(e.target.value) },
|
|
356
|
+
onBlur: commitPath,
|
|
357
|
+
onKeyDown: function (e) { if (e.key === 'Enter') commitPath() },
|
|
358
|
+
placeholder: getSettings().path || 'es.exe 路径',
|
|
359
|
+
}),
|
|
360
|
+
),
|
|
361
|
+
)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function SearchButton(props) {
|
|
365
|
+
React.useEffect(function () {
|
|
366
|
+
if (!props.inputActions) return
|
|
367
|
+
setCommitHandler(function (paths) {
|
|
368
|
+
const mentions = paths.map(toMention).filter(Boolean)
|
|
369
|
+
if (mentions.length === 0) return
|
|
370
|
+
const current = (props.input && props.input.draft) || ''
|
|
371
|
+
const next = current.trim() ? current.trim() + ' ' + mentions.join(' ') : mentions.join(' ')
|
|
372
|
+
props.inputActions.setDraft(next)
|
|
373
|
+
})
|
|
374
|
+
}, [props.inputActions, props.input])
|
|
375
|
+
return React.createElement('button', { className: 'evs-trigger', onClick: openSearch, title: 'Everything 全盘搜索并加入上下文', 'aria-label': 'Everything 全盘搜索' },
|
|
376
|
+
React.createElement('svg', { viewBox: '0 0 1024 1024', width: '18', height: '18', fill: 'currentColor', style: { display: 'block' } },
|
|
377
|
+
React.createElement('path', { d: 'M975.648 975.648c-36 36-94.336 36-130.336 0L682.656 812.992c-66.88 42.88-145.952 68.448-231.264 68.448-237.536 0-430.08-192.512-430.08-430.048S213.856 21.344 451.392 21.344 881.44 213.888 881.44 451.392c0 85.344-25.568 164.384-68.448 231.296l162.656 162.656c36 36 36 94.368 0 130.336zM451.424 144.224c-169.664 0-307.2 137.536-307.2 307.168S281.76 758.56 451.424 758.56c169.632 0 307.168-137.536 307.168-307.168 0.064-169.632-137.536-307.168-307.168-307.168z' }),
|
|
378
|
+
),
|
|
379
|
+
)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
slots.inject('conversation.input.left', function () {
|
|
383
|
+
return slots.register(
|
|
384
|
+
{ name: 'conversation.input.left', id: 'everything-search-btn' },
|
|
385
|
+
function (props) { return React.createElement(SearchButton, props) }
|
|
386
|
+
)
|
|
387
|
+
})
|
|
388
|
+
slots.inject('shell.overlay', function () {
|
|
389
|
+
return slots.register(
|
|
390
|
+
{ name: 'shell.overlay', id: 'everything-search-overlay' },
|
|
391
|
+
function () { return React.createElement(SearchOverlay) }
|
|
392
|
+
)
|
|
393
|
+
})
|
|
394
|
+
slots.inject('settings.section', function () {
|
|
395
|
+
return slots.register(
|
|
396
|
+
{ name: 'settings.section', id: 'everything-search', order: 90, label: 'Everything 搜索' },
|
|
397
|
+
function () { return React.createElement(SettingsPage) }
|
|
398
|
+
)
|
|
399
|
+
})
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
exports.inject = inject
|
|
403
|
+
exports.apply = apply
|
|
404
|
+
return module.exports
|
|
405
|
+
},
|
|
406
|
+
})
|
package/lib/es.exe
ADDED
|
Binary file
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// dsh-everything-search —— Host 半端(静态 web 插件形态)
|
|
2
|
+
// es.exe 随插件打包在 lib/es.exe,插件自行调用;RPC 通过 ctx.webServer HTTP 路由。
|
|
3
|
+
import { spawn } from 'node:child_process'
|
|
4
|
+
import { readFileSync, unlinkSync } from 'node:fs'
|
|
5
|
+
import { join, dirname } from 'node:path'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
import { tmpdir } from 'node:os'
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
10
|
+
const ES = join(__dirname, 'es.exe')
|
|
11
|
+
const TEMP = tmpdir()
|
|
12
|
+
|
|
13
|
+
export const inject = ['webServer', 'tools']
|
|
14
|
+
|
|
15
|
+
export function apply(ctx) {
|
|
16
|
+
const webServer = ctx.get('webServer')
|
|
17
|
+
const tools = ctx.get('tools')
|
|
18
|
+
|
|
19
|
+
async function runSearch(args) {
|
|
20
|
+
const query = String((args && args.query) || '').trim()
|
|
21
|
+
if (!query) return { ok: false, error: 'empty query' }
|
|
22
|
+
const esPath = String((args && args.path) || '').trim() || ES
|
|
23
|
+
const scope = String((args && args.scope) || '').trim()
|
|
24
|
+
let limit = Number((args && args.limit) !== undefined ? args.limit : 50)
|
|
25
|
+
if (!Number.isFinite(limit) || limit <= 0) limit = 50
|
|
26
|
+
if (limit > 200) limit = 200
|
|
27
|
+
|
|
28
|
+
const tmp = join(TEMP, 'evs-current.txt')
|
|
29
|
+
const argv = [esPath]
|
|
30
|
+
if (args && args.foldersOnly) argv.push('/ad')
|
|
31
|
+
if (args && args.filesOnly) argv.push('/a-d')
|
|
32
|
+
if (args && args.wholeWord) argv.push('-ww')
|
|
33
|
+
if (args && args.matchPath) argv.push('-p')
|
|
34
|
+
if (args && args.regex) argv.push('-r')
|
|
35
|
+
if (scope) argv.push('-path', scope)
|
|
36
|
+
argv.push('-n', String(limit), '-utf8-bom', '-export-txt', tmp)
|
|
37
|
+
argv.push(query)
|
|
38
|
+
|
|
39
|
+
return await new Promise((resolve) => {
|
|
40
|
+
let child
|
|
41
|
+
try {
|
|
42
|
+
child = spawn(argv[0], argv.slice(1), { cwd: 'C:\\', windowsHide: true, stdio: ['ignore', 'ignore', 'pipe'] })
|
|
43
|
+
} catch (err) {
|
|
44
|
+
return resolve({ ok: false, error: String(err && err.message || err) })
|
|
45
|
+
}
|
|
46
|
+
let stderr = ''
|
|
47
|
+
child.stderr.on('data', (d) => { stderr += String(d) })
|
|
48
|
+
child.on('error', (err) => {
|
|
49
|
+
try { unlinkSync(tmp) } catch (e) { /* ignore */ }
|
|
50
|
+
resolve({ ok: false, error: String(err && err.message || err) })
|
|
51
|
+
})
|
|
52
|
+
child.on('close', (code) => {
|
|
53
|
+
let text = ''
|
|
54
|
+
try { text = readFileSync(tmp, 'utf-8') } catch (e) { /* ignore */ }
|
|
55
|
+
try { text = text.replace(/^\uFEFF/, '') } catch (e) { /* ignore */ }
|
|
56
|
+
const all = text.split(/\r?\n/).map((s) => s.trim()).filter(Boolean)
|
|
57
|
+
try { unlinkSync(tmp) } catch (e) { /* ignore */ }
|
|
58
|
+
if (code !== 0 && all.length === 0) {
|
|
59
|
+
resolve({ ok: false, error: 'es.exe exited ' + code + ': ' + stderr.trim(), count: 0, results: [] })
|
|
60
|
+
} else {
|
|
61
|
+
resolve({ ok: true, count: all.length, results: all, exitCode: code })
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (webServer) {
|
|
68
|
+
webServer.register({
|
|
69
|
+
kind: 'exact',
|
|
70
|
+
path: '/evs/api/search',
|
|
71
|
+
handler: async (req, res) => {
|
|
72
|
+
let body = ''
|
|
73
|
+
try { for await (const chunk of req) body += chunk } catch (e) { /* ignore */ }
|
|
74
|
+
let args = {}
|
|
75
|
+
try { args = body ? JSON.parse(body) : {} } catch (e) { args = {} }
|
|
76
|
+
let result
|
|
77
|
+
try { result = await runSearch(args) } catch (e) { result = { ok: false, error: String(e && e.message || e) } }
|
|
78
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' })
|
|
79
|
+
res.end(JSON.stringify(result))
|
|
80
|
+
},
|
|
81
|
+
})
|
|
82
|
+
webServer.register({
|
|
83
|
+
kind: 'exact',
|
|
84
|
+
path: '/evs/api/default-path',
|
|
85
|
+
handler: async (req, res) => {
|
|
86
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' })
|
|
87
|
+
res.end(JSON.stringify(ES))
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (tools) {
|
|
93
|
+
tools.register({
|
|
94
|
+
name: 'everything_search',
|
|
95
|
+
description: '用 Everything 索引在全部硬盘上极速搜索文件,返回匹配的完整路径。适合快速找文件,比 grep/glob 更快更全。',
|
|
96
|
+
parameters: {
|
|
97
|
+
type: 'object',
|
|
98
|
+
properties: {
|
|
99
|
+
query: { type: 'string', description: 'Everything 搜索词,支持 Everything 语法,例如 *.pdf、ext:ini、文件夹名。' },
|
|
100
|
+
limit: { type: 'number', description: '最多返回条数,默认 50,上限 200。' },
|
|
101
|
+
foldersOnly: { type: 'boolean', description: '只搜文件夹。' },
|
|
102
|
+
filesOnly: { type: 'boolean', description: '只搜文件。' },
|
|
103
|
+
wholeWord: { type: 'boolean', description: '整词匹配。' },
|
|
104
|
+
matchPath: { type: 'boolean', description: '同时匹配完整路径和文件名。' },
|
|
105
|
+
regex: { type: 'boolean', description: '把查询当正则表达式。' },
|
|
106
|
+
scope: { type: 'string', description: '可选:限定在指定文件夹内搜索,如 C:\\Project。' },
|
|
107
|
+
},
|
|
108
|
+
required: ['query'],
|
|
109
|
+
},
|
|
110
|
+
output: { schema: {}, render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }] },
|
|
111
|
+
async execute(args) { return await runSearch(args || {}) },
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-everything-search",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Everything 文件搜索插件(DeepSeek Harness / DSH):全盘极速搜索文件与文件夹,多选后一键加入对话上下文,设置可自定义,es.exe 随插件打包。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./lib/index.js",
|
|
9
|
+
"./client": "./lib/client.js",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"dsh": {
|
|
13
|
+
"bundle": {
|
|
14
|
+
"patch": "./cordis.patch.yml"
|
|
15
|
+
},
|
|
16
|
+
"client": {
|
|
17
|
+
"inject": [],
|
|
18
|
+
"platform": "web"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"lib",
|
|
23
|
+
"cordis.patch.yml"
|
|
24
|
+
],
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/dbaks/dsh-everything-search"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"deepseek-harness",
|
|
32
|
+
"dsh",
|
|
33
|
+
"cordis",
|
|
34
|
+
"plugin",
|
|
35
|
+
"everything",
|
|
36
|
+
"search",
|
|
37
|
+
"file-search"
|
|
38
|
+
]
|
|
39
|
+
}
|