dsh-chat-link-actions 0.2.3 → 0.2.4
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 +4 -4
- package/client.js +25 -28
- package/index.js +39 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# dsh-chat-link-actions(对话里的文件链接,右键就有动作)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**对话里的文件路径,右键就能引用回输入框,也能在文件管理器里直接定位。菜单是 App 自己画的,不是系统弹窗。**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
长任务跑完,记录里一串路径;想引用回去得手打,想在文件管理器里找到又是另一回事。这个插件把右键菜单接到对话里的文件链接上。
|
|
6
6
|
|
|
7
7
|
## 三个动作
|
|
8
8
|
|
|
9
9
|
| 右键文件链接 | 做什么 |
|
|
10
10
|
| --- | --- |
|
|
11
11
|
| **引用到对话** | 插成输入框里的引用芯片(`(</> 文件名)`),和拖文件、`@` 选文件的结果一样 |
|
|
12
|
-
|
|
|
12
|
+
| **在文件管理器中查看** | 打开所在目录并选中这个文件(macOS 打开访达、Windows 打开资源管理器、Linux 打开所在目录) |
|
|
13
13
|
| **复制路径** | 绝对路径进剪贴板,直接粘给别的窗口 |
|
|
14
14
|
|
|
15
15
|
## 认哪些链接
|
|
@@ -33,7 +33,7 @@ dsh plugin install dsh-chat-link-actions
|
|
|
33
33
|
## 不做什么
|
|
34
34
|
|
|
35
35
|
- 只认本机存在的绝对路径:网络路径、已删掉的文件,会直接说找不到。
|
|
36
|
-
-
|
|
36
|
+
- 不上传、不打开编辑器;「在文件管理器中查看」只让系统文件管理器选中文件,不修改它。
|
|
37
37
|
- 不改对话内容:插入是一次正常的输入框编辑,可以撤销,也会进草稿。
|
|
38
38
|
|
|
39
39
|
## 给 agent 的话
|
package/client.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* menu. Two actions:
|
|
7
7
|
*
|
|
8
8
|
* 引用到对话 insert `@path` into the composer as one undoable edit
|
|
9
|
-
*
|
|
9
|
+
* 在文件管理器中查看 reveal the file in the OS file manager (服务端接口)
|
|
10
10
|
*
|
|
11
11
|
* Wiring:
|
|
12
12
|
* - `inputActions` is a standard prop of session-scoped slots
|
|
@@ -327,10 +327,6 @@ window.__ModuleLoader__.load({
|
|
|
327
327
|
|
|
328
328
|
/* ── wording ────────────────────────────────────────────────────────── */
|
|
329
329
|
|
|
330
|
-
function isMac() {
|
|
331
|
-
return /Mac/i.test(navigator.platform || navigator.userAgent || "");
|
|
332
|
-
}
|
|
333
|
-
|
|
334
330
|
/* ── menu styling: project tokens only, no new palette ──────────────── */
|
|
335
331
|
|
|
336
332
|
var STYLE_ID = "dsh-chat-link-actions-style";
|
|
@@ -550,7 +546,7 @@ window.__ModuleLoader__.load({
|
|
|
550
546
|
if (!inserted) toast("已复制引用");
|
|
551
547
|
});
|
|
552
548
|
} },
|
|
553
|
-
{ id: "reveal", label:
|
|
549
|
+
{ id: "reveal", label: "在文件管理器中查看", icon: "reveal", run: function () {
|
|
554
550
|
reveal(path);
|
|
555
551
|
} },
|
|
556
552
|
{ id: "copy", label: "复制路径", icon: "copy", run: function () {
|
|
@@ -642,9 +638,13 @@ window.__ModuleLoader__.load({
|
|
|
642
638
|
return;
|
|
643
639
|
}
|
|
644
640
|
var settled = false;
|
|
641
|
+
var probeTimer = setTimeout(function () {
|
|
642
|
+
finish("unknown");
|
|
643
|
+
}, 900);
|
|
645
644
|
function finish(kind) {
|
|
646
645
|
if (settled) return;
|
|
647
646
|
settled = true;
|
|
647
|
+
clearTimeout(probeTimer);
|
|
648
648
|
done(kind);
|
|
649
649
|
}
|
|
650
650
|
function askDirectory() {
|
|
@@ -674,35 +674,41 @@ window.__ModuleLoader__.load({
|
|
|
674
674
|
toast("目标文件不存在");
|
|
675
675
|
return;
|
|
676
676
|
}
|
|
677
|
+
revealViaRoute(path);
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/** 首选:直接请系统文件管理器选中。 */
|
|
682
|
+
function revealViaRoute(path) {
|
|
683
|
+
fetch("/chat-link-actions?action=reveal&path=" + encodeURIComponent(path), { method: "POST" }).then(function (response) {
|
|
684
|
+
if (response.ok) return;
|
|
685
|
+
if (response.status === 404) {
|
|
686
|
+
toast("目标文件不存在");
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
revealRemote(path);
|
|
690
|
+
}, function () {
|
|
677
691
|
revealRemote(path);
|
|
678
692
|
});
|
|
679
693
|
}
|
|
680
694
|
|
|
681
|
-
/**
|
|
695
|
+
/** 退路:交给应用打开。 */
|
|
682
696
|
function revealRemote(path) {
|
|
683
697
|
var remote = sessionRemote();
|
|
684
698
|
var session = remote === null ? null : remote.session;
|
|
685
699
|
if (session === null || session === undefined || typeof session.openWorkspacePath !== "function") {
|
|
686
|
-
|
|
700
|
+
toast("无法打开文件管理器");
|
|
687
701
|
return;
|
|
688
702
|
}
|
|
689
703
|
try {
|
|
690
704
|
session.openWorkspacePath({ path: path, action: "reveal" }).then(function (result) {
|
|
691
705
|
if (result !== null && result !== undefined && result.ok === true) return;
|
|
692
|
-
|
|
693
|
-
toast("目标文件不存在");
|
|
694
|
-
return;
|
|
695
|
-
}
|
|
696
|
-
revealViaRoute(path);
|
|
706
|
+
toast(isUnreachablePath(result === null || result === undefined ? null : result.error) ? "目标文件不存在" : "无法打开文件管理器");
|
|
697
707
|
}, function (error) {
|
|
698
|
-
|
|
699
|
-
toast("目标文件不存在");
|
|
700
|
-
return;
|
|
701
|
-
}
|
|
702
|
-
revealViaRoute(path);
|
|
708
|
+
toast(isUnreachablePath(error) ? "目标文件不存在" : "无法打开文件管理器");
|
|
703
709
|
});
|
|
704
710
|
} catch (error) {
|
|
705
|
-
|
|
711
|
+
toast("无法打开文件管理器");
|
|
706
712
|
}
|
|
707
713
|
}
|
|
708
714
|
|
|
@@ -711,15 +717,6 @@ window.__ModuleLoader__.load({
|
|
|
711
717
|
return error !== null && error !== undefined && error.code === "gateway/bad-request";
|
|
712
718
|
}
|
|
713
719
|
|
|
714
|
-
/** 本地通道:请应用在系统文件管理器里选中路径。 */
|
|
715
|
-
function revealViaRoute(path) {
|
|
716
|
-
fetch("/chat-link-actions?action=reveal&path=" + encodeURIComponent(path), { method: "POST" }).then(function (response) {
|
|
717
|
-
if (!response.ok) toast("无法打开文件管理器");
|
|
718
|
-
}, function () {
|
|
719
|
-
toast("无法打开文件管理器");
|
|
720
|
-
});
|
|
721
|
-
}
|
|
722
|
-
|
|
723
720
|
/* ── interception ───────────────────────────────────────────────────── */
|
|
724
721
|
|
|
725
722
|
function onContextMenu(event) {
|
package/index.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* the argv is never shell-interpolated.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { execFile } from 'node:child_process'
|
|
12
12
|
import { stat } from 'node:fs/promises'
|
|
13
13
|
import { dirname, isAbsolute } from 'node:path'
|
|
14
14
|
|
|
@@ -40,6 +40,33 @@ function revealCommand(path, platform = process.platform) {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/** The argv that only opens the containing directory, used when selection fails. */
|
|
44
|
+
function openDirectoryCommand(path, platform = process.platform) {
|
|
45
|
+
switch (platform) {
|
|
46
|
+
case 'darwin':
|
|
47
|
+
return { command: 'open', args: [dirname(path)] }
|
|
48
|
+
case 'win32':
|
|
49
|
+
return { command: 'explorer.exe', args: [dirname(path)] }
|
|
50
|
+
case 'android':
|
|
51
|
+
return { command: 'termux-open', args: [dirname(path)] }
|
|
52
|
+
default:
|
|
53
|
+
return { command: 'xdg-open', args: [dirname(path)] }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Run one launcher and resolve its outcome instead of throwing. */
|
|
58
|
+
function run(command, args) {
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
execFile(command, args, { timeout: 3000, windowsHide: true }, (error, stdout, stderr) => {
|
|
61
|
+
if (error === null) {
|
|
62
|
+
resolve({ ok: true })
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
resolve({ ok: false, error, stderr: String(stderr ?? '') })
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
43
70
|
function reply(res, status, body) {
|
|
44
71
|
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' })
|
|
45
72
|
res.end(JSON.stringify(body))
|
|
@@ -48,23 +75,17 @@ function reply(res, status, body) {
|
|
|
48
75
|
export function apply(ctx) {
|
|
49
76
|
const fence = (req) => ctx.connection.requestRejection(req) === undefined
|
|
50
77
|
|
|
51
|
-
/**
|
|
78
|
+
/** Select the file; when selection is refused, at least open its directory. */
|
|
52
79
|
const launch = async (path) => {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const timer = setTimeout(() => done('launched'), 1500)
|
|
63
|
-
const child = spawn(command, args, { detached: true, stdio: 'ignore' })
|
|
64
|
-
child.once('spawn', () => done('launched'))
|
|
65
|
-
child.once('error', (error) => done(error))
|
|
66
|
-
child.unref()
|
|
67
|
-
})
|
|
80
|
+
const select = revealCommand(path)
|
|
81
|
+
const selected = await run(select.command, select.args)
|
|
82
|
+
if (selected.ok) return { ok: true }
|
|
83
|
+
ctx.logger?.warn?.(`chat-link-actions: select via ${select.command} failed: ${String(selected.stderr || selected.error?.message || selected.error)}`)
|
|
84
|
+
const open = openDirectoryCommand(path)
|
|
85
|
+
const opened = await run(open.command, open.args)
|
|
86
|
+
if (opened.ok) return { ok: true }
|
|
87
|
+
ctx.logger?.warn?.(`chat-link-actions: open ${dirname(path)} failed: ${String(opened.stderr || opened.error?.message || opened.error)}`)
|
|
88
|
+
return { ok: false }
|
|
68
89
|
}
|
|
69
90
|
|
|
70
91
|
ctx.effect(() => ctx.webServer.register({
|
|
@@ -85,10 +106,7 @@ export function apply(ctx) {
|
|
|
85
106
|
const info = await stat(path).catch(() => null)
|
|
86
107
|
if (info === null) return reply(res, 404, { ok: false, error: 'not-found' })
|
|
87
108
|
const outcome = await launch(path)
|
|
88
|
-
if (outcome
|
|
89
|
-
ctx.logger?.warn?.(`chat-link-actions: file manager launch failed: ${String(outcome?.message ?? outcome)}`)
|
|
90
|
-
return reply(res, 500, { ok: false, error: 'launch-failed' })
|
|
91
|
-
}
|
|
109
|
+
if (!outcome.ok) return reply(res, 500, { ok: false, error: 'launch-failed' })
|
|
92
110
|
return reply(res, 200, { ok: true })
|
|
93
111
|
} catch (error) {
|
|
94
112
|
return reply(res, 400, { ok: false, error: String(error?.message ?? error) })
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-chat-link-actions",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Right-click actions on file links in the DSH transcript: quote the path into the composer, or reveal the file in the OS file manager. Menus are drawn in the app's own style, never the system one.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|