dsh-prompt-snippets 1.3.0 → 1.3.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 +3 -1
- package/client.js +110 -14
- package/index.js +89 -44
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -108,7 +108,9 @@ and reload the page.
|
|
|
108
108
|
面板里点 🕘 展开该片段的历史,可回看历次正文;`/snippet show` 也会显示 revision 号。
|
|
109
109
|
- **命令面板(全局快捷唤起)**:任意时刻按 `Ctrl+Shift+K`(或 `Ctrl+Shift+P`)弹出片段搜索
|
|
110
110
|
浮层,输入关键字即时过滤(名称/分类/标签/正文),`↑` `↓` 选择、`Enter` 插入、`Esc` 关闭;
|
|
111
|
-
收藏片段自动置顶,无需先点开 dock
|
|
111
|
+
收藏片段自动置顶,无需先点开 dock 面板。选中后按 `Ctrl+Enter`(Mac `Cmd+Enter`)则是
|
|
112
|
+
**插入并立即发送**;带必填占位符的片段仍需先填表单,无法直接发送。`↑` `↓` 切换选中项时
|
|
113
|
+
列表自动滚动跟随,始终把选中项保持在可视区内。
|
|
112
114
|
|
|
113
115
|
## 使用
|
|
114
116
|
|
package/client.js
CHANGED
|
@@ -334,6 +334,12 @@ window.__ModuleLoader__.load({
|
|
|
334
334
|
maxHeight: "120px",
|
|
335
335
|
overflow: "auto",
|
|
336
336
|
},
|
|
337
|
+
historyLink: {
|
|
338
|
+
color: "var(--dsw-alias-brand-primary)",
|
|
339
|
+
cursor: "pointer",
|
|
340
|
+
textDecoration: "underline",
|
|
341
|
+
textUnderlineOffset: "2px",
|
|
342
|
+
},
|
|
337
343
|
// ── v1.3 命令面板(全局快捷唤起 overlay) ──
|
|
338
344
|
paletteOverlay: {
|
|
339
345
|
position: "fixed",
|
|
@@ -366,7 +372,7 @@ window.__ModuleLoader__.load({
|
|
|
366
372
|
outline: "none",
|
|
367
373
|
fontFamily: "inherit",
|
|
368
374
|
},
|
|
369
|
-
paletteList: { overflowY: "auto", padding: "6px", flex: 1 },
|
|
375
|
+
paletteList: { overflowY: "auto", padding: "6px", flex: 1, minHeight: 0 },
|
|
370
376
|
paletteRow: {
|
|
371
377
|
display: "flex",
|
|
372
378
|
alignItems: "center",
|
|
@@ -750,12 +756,12 @@ window.__ModuleLoader__.load({
|
|
|
750
756
|
return [...groups.entries()];
|
|
751
757
|
}
|
|
752
758
|
|
|
753
|
-
/**
|
|
759
|
+
/** 大小写不敏感、命中名称/分类/标签/内容任一处。 */
|
|
754
760
|
function filterSnippets(snippets, query) {
|
|
755
761
|
const q = query.trim().toLowerCase();
|
|
756
762
|
if (!q) return snippets;
|
|
757
763
|
return snippets.filter((s) => {
|
|
758
|
-
const hay = `${s.name}\n${s.category || ""}\n${s.content}`.toLowerCase();
|
|
764
|
+
const hay = `${s.name}\n${s.category || ""}\n${(s.tags || []).join(" ")}\n${s.content}`.toLowerCase();
|
|
759
765
|
return hay.includes(q);
|
|
760
766
|
});
|
|
761
767
|
}
|
|
@@ -984,6 +990,7 @@ window.__ModuleLoader__.load({
|
|
|
984
990
|
const [paletteQuery, setPaletteQuery] = useState("");
|
|
985
991
|
const [paletteSelected, setPaletteSelected] = useState(-1);
|
|
986
992
|
const paletteInputRef = useRef(null);
|
|
993
|
+
const paletteListRef = useRef(null);
|
|
987
994
|
const searchRef = useRef(null);
|
|
988
995
|
const nameRef = useRef(null);
|
|
989
996
|
// 面板 DOM 引用:拖拽/缩放期间直接改 style.left/top/width/height,
|
|
@@ -1034,11 +1041,13 @@ window.__ModuleLoader__.load({
|
|
|
1034
1041
|
|
|
1035
1042
|
useEffect(() => {
|
|
1036
1043
|
const onKey = (e) => {
|
|
1037
|
-
|
|
1044
|
+
// 用 e.code(物理键位)而非 e.key:Shift+K 会让 e.key 变成大写 "K",
|
|
1045
|
+
// 导致 Ctrl+Shift+K 永远匹配不到小写 "k"。
|
|
1046
|
+
if (e.code === "KeyK" && (e.ctrlKey || e.metaKey) && e.shiftKey) {
|
|
1038
1047
|
e.preventDefault();
|
|
1039
1048
|
setPaletteOpen(true);
|
|
1040
1049
|
setOpen(false);
|
|
1041
|
-
} else if (e.
|
|
1050
|
+
} else if (e.code === "KeyP" && (e.ctrlKey || e.metaKey) && e.shiftKey) {
|
|
1042
1051
|
e.preventDefault();
|
|
1043
1052
|
setPaletteOpen(true);
|
|
1044
1053
|
setOpen(false);
|
|
@@ -1061,6 +1070,22 @@ window.__ModuleLoader__.load({
|
|
|
1061
1070
|
return sorted.slice(0, 30);
|
|
1062
1071
|
}, [snippets, paletteQuery]);
|
|
1063
1072
|
|
|
1073
|
+
// 选中项变化时,把对应的列表行滚进可视区(否则上下键切换超过可视高度后不跟随)。
|
|
1074
|
+
useEffect(() => {
|
|
1075
|
+
if (!paletteOpen || paletteSelected < 0 || !paletteListRef.current) return;
|
|
1076
|
+
const el = paletteListRef.current;
|
|
1077
|
+
const row = el.querySelector(`[data-ps-index="${paletteSelected}"]`);
|
|
1078
|
+
if (!row) return;
|
|
1079
|
+
const elTop = el.getBoundingClientRect().top;
|
|
1080
|
+
const rowTop = row.getBoundingClientRect().top;
|
|
1081
|
+
const rowBottom = row.getBoundingClientRect().bottom;
|
|
1082
|
+
if (rowTop < elTop) {
|
|
1083
|
+
el.scrollTop += rowTop - elTop;
|
|
1084
|
+
} else if (rowBottom > el.getBoundingClientRect().bottom) {
|
|
1085
|
+
el.scrollTop += rowBottom - el.getBoundingClientRect().bottom;
|
|
1086
|
+
}
|
|
1087
|
+
}, [paletteOpen, paletteSelected]);
|
|
1088
|
+
|
|
1064
1089
|
const openPalette = () => {
|
|
1065
1090
|
setPaletteQuery("");
|
|
1066
1091
|
setPaletteSelected(-1);
|
|
@@ -1074,9 +1099,9 @@ window.__ModuleLoader__.load({
|
|
|
1074
1099
|
setPaletteSelected(-1);
|
|
1075
1100
|
};
|
|
1076
1101
|
|
|
1077
|
-
const paletteChoose = (snippet) => {
|
|
1102
|
+
const paletteChoose = (snippet, sendAfter = false) => {
|
|
1078
1103
|
closePalette();
|
|
1079
|
-
insert(snippet);
|
|
1104
|
+
insert(snippet, sendAfter);
|
|
1080
1105
|
};
|
|
1081
1106
|
|
|
1082
1107
|
// §3.1:启动时问一次模型可用性。拿不到就不降级为「假装可用」——
|
|
@@ -1295,12 +1320,32 @@ window.__ModuleLoader__.load({
|
|
|
1295
1320
|
Promise.resolve(rpc.call(CHANNEL, "touch", { ids: [snippetId] })).catch(() => {});
|
|
1296
1321
|
};
|
|
1297
1322
|
|
|
1323
|
+
// 发送当前草稿(Ctrl+Enter 插入并发送时调用;失败不打断,交给用户手动补救)。
|
|
1324
|
+
const submitDraft = () => {
|
|
1325
|
+
try {
|
|
1326
|
+
inputActions.submit();
|
|
1327
|
+
} catch (_) {
|
|
1328
|
+
setError("发送失败:请确认输入框里有内容再试。");
|
|
1329
|
+
}
|
|
1330
|
+
};
|
|
1331
|
+
|
|
1298
1332
|
// ────────── 插入:直插 / 按默认值插 / 弹表单 ──────────
|
|
1299
|
-
|
|
1333
|
+
// sendAfter:插入成功后立即发送草稿。带必填占位符的片段仍需先填表单,
|
|
1334
|
+
// 无法自动发送,此时降级为「弹表单、填完手动发送」。
|
|
1335
|
+
const insert = (snippet, sendAfter = false) => {
|
|
1300
1336
|
const plan = fillPlan(snippet);
|
|
1301
1337
|
if (plan.kind === "form") {
|
|
1302
1338
|
setFilling({ snippet, values: {} });
|
|
1303
1339
|
setError(null);
|
|
1340
|
+
// 从命令面板(palette)触发时主面板是关闭的,表单渲染在 open 面板内部,
|
|
1341
|
+
// 必须同时打开主面板并定位,否则「按回车后表单被吞」。
|
|
1342
|
+
if (!open) {
|
|
1343
|
+
if (!panelPos) {
|
|
1344
|
+
const saved = loadSavedPos();
|
|
1345
|
+
setPanelPos(saved ? clampPos(saved.left, saved.top, panelSize) : clampPos(GAP, GAP, panelSize));
|
|
1346
|
+
}
|
|
1347
|
+
setOpen(true);
|
|
1348
|
+
}
|
|
1304
1349
|
return;
|
|
1305
1350
|
}
|
|
1306
1351
|
const { tokens } = scanPlaceholders(snippet.content);
|
|
@@ -1312,7 +1357,11 @@ window.__ModuleLoader__.load({
|
|
|
1312
1357
|
touch(snippet.id);
|
|
1313
1358
|
setError(null);
|
|
1314
1359
|
setOpen(false);
|
|
1315
|
-
if (
|
|
1360
|
+
if (sendAfter) {
|
|
1361
|
+
submitDraft();
|
|
1362
|
+
} else if (plan.kind === "defaults") {
|
|
1363
|
+
setNotice("已用默认值填充,可在 ✏️ 里改");
|
|
1364
|
+
}
|
|
1316
1365
|
};
|
|
1317
1366
|
|
|
1318
1367
|
const setFillValue = (name, value) => {
|
|
@@ -1988,6 +2037,10 @@ window.__ModuleLoader__.load({
|
|
|
1988
2037
|
const opts = Array.isArray(d.options) ? d.options : [];
|
|
1989
2038
|
const label = (typeof d.label === "string" && d.label ? d.label : nm) + (required ? " · 必填" : "");
|
|
1990
2039
|
const val = typeof cur.values[nm] === "string" ? cur.values[nm] : "";
|
|
2040
|
+
// 长文本变量(变量名带「代码/原文/记录/要点/说明/内容/text/body」或已含换行)用 textarea,
|
|
2041
|
+
// 单行 input 会吃掉换行,粘贴多行代码/会议记录没法看。
|
|
2042
|
+
const multiline =
|
|
2043
|
+
/代码|code|原文|记录|要点|说明|内容|text|body/i.test(nm) || (typeof val === "string" && val.includes("\n"));
|
|
1991
2044
|
return h(
|
|
1992
2045
|
"label",
|
|
1993
2046
|
{ key: nm, style: style.field },
|
|
@@ -2005,6 +2058,14 @@ window.__ModuleLoader__.load({
|
|
|
2005
2058
|
!required ? h("option", { value: "" }, "(不填)") : null,
|
|
2006
2059
|
opts.map((o) => h("option", { key: o, value: o }, o))
|
|
2007
2060
|
)
|
|
2061
|
+
: multiline
|
|
2062
|
+
? h("textarea", {
|
|
2063
|
+
className: "ps-textarea",
|
|
2064
|
+
style: { ...style.textarea, minHeight: "54px" },
|
|
2065
|
+
value: val,
|
|
2066
|
+
placeholder: typeof d.default === "string" ? d.default : "",
|
|
2067
|
+
onChange: (ev) => setFillValue(nm, ev.target.value),
|
|
2068
|
+
})
|
|
2008
2069
|
: h("input", {
|
|
2009
2070
|
type: "text",
|
|
2010
2071
|
className: "ps-input",
|
|
@@ -2251,7 +2312,7 @@ window.__ModuleLoader__.load({
|
|
|
2251
2312
|
{
|
|
2252
2313
|
type: "button",
|
|
2253
2314
|
className: "ps-icon",
|
|
2254
|
-
style: { ...style.icon, ...style.
|
|
2315
|
+
style: history && history.id === s.id ? { ...style.icon, ...style.itemEditing } : style.icon,
|
|
2255
2316
|
onClick: () => showHistory(s),
|
|
2256
2317
|
title: "查看历史版本",
|
|
2257
2318
|
"aria-label": `历史「${s.name}」`,
|
|
@@ -2357,7 +2418,8 @@ window.__ModuleLoader__.load({
|
|
|
2357
2418
|
} else if (e.key === "Enter") {
|
|
2358
2419
|
e.preventDefault();
|
|
2359
2420
|
const pick = paletteSelected >= 0 ? paletteResults[paletteSelected] : paletteResults[0];
|
|
2360
|
-
|
|
2421
|
+
// Enter 插入;Ctrl+Enter / Meta+Enter 插入并立即发送。
|
|
2422
|
+
if (pick) paletteChoose(pick, e.ctrlKey || e.metaKey);
|
|
2361
2423
|
} else if (e.key === "Escape") {
|
|
2362
2424
|
e.preventDefault();
|
|
2363
2425
|
closePalette();
|
|
@@ -2366,7 +2428,7 @@ window.__ModuleLoader__.load({
|
|
|
2366
2428
|
}),
|
|
2367
2429
|
h(
|
|
2368
2430
|
"div",
|
|
2369
|
-
{ style: style.paletteList },
|
|
2431
|
+
{ style: style.paletteList, ref: paletteListRef },
|
|
2370
2432
|
paletteResults.length === 0
|
|
2371
2433
|
? h("div", { style: style.empty }, snippets.length === 0 ? "还没有片段,点 dock 的「+ 新建」保存一个。" : `没有匹配「${paletteQuery}」的片段。`)
|
|
2372
2434
|
: paletteResults.map((s, i) =>
|
|
@@ -2376,6 +2438,7 @@ window.__ModuleLoader__.load({
|
|
|
2376
2438
|
key: s.id,
|
|
2377
2439
|
type: "button",
|
|
2378
2440
|
className: "ps-item",
|
|
2441
|
+
"data-ps-index": i,
|
|
2379
2442
|
style: i === paletteSelected ? style.paletteRowActive : style.paletteRow,
|
|
2380
2443
|
onMouseEnter: () => setPaletteSelected(i),
|
|
2381
2444
|
onClick: () => paletteChoose(s),
|
|
@@ -2393,7 +2456,7 @@ window.__ModuleLoader__.load({
|
|
|
2393
2456
|
h(
|
|
2394
2457
|
"div",
|
|
2395
2458
|
{ style: style.paletteHint },
|
|
2396
|
-
"↑↓ 选择 · Enter 插入 · Esc 关闭 · Ctrl+Shift+K 唤起"
|
|
2459
|
+
"↑↓ 选择 · Enter 插入 · Ctrl+Enter 插入并发送 · Esc 关闭 · Ctrl+Shift+K 唤起"
|
|
2397
2460
|
)
|
|
2398
2461
|
)
|
|
2399
2462
|
);
|
|
@@ -2421,7 +2484,40 @@ window.__ModuleLoader__.load({
|
|
|
2421
2484
|
h(
|
|
2422
2485
|
"div",
|
|
2423
2486
|
{ style: style.historyContent },
|
|
2424
|
-
h(
|
|
2487
|
+
h(
|
|
2488
|
+
"div",
|
|
2489
|
+
{ style: { color: C.textDim, fontSize: "10px" } },
|
|
2490
|
+
`保存于 ${fmtTime(v.at)} · `,
|
|
2491
|
+
h(
|
|
2492
|
+
"span",
|
|
2493
|
+
{
|
|
2494
|
+
style: style.historyLink,
|
|
2495
|
+
role: "button",
|
|
2496
|
+
tabIndex: 0,
|
|
2497
|
+
onClick: () => {
|
|
2498
|
+
if (!insertText(v.content)) {
|
|
2499
|
+
setError("插入失败:请先把光标放进输入框再试。");
|
|
2500
|
+
return;
|
|
2501
|
+
}
|
|
2502
|
+
setError(null);
|
|
2503
|
+
setNotice("已插入旧版内容");
|
|
2504
|
+
},
|
|
2505
|
+
onKeyDown: (e) => {
|
|
2506
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
2507
|
+
e.preventDefault();
|
|
2508
|
+
if (!insertText(v.content)) {
|
|
2509
|
+
setError("插入失败:请先把光标放进输入框再试。");
|
|
2510
|
+
return;
|
|
2511
|
+
}
|
|
2512
|
+
setError(null);
|
|
2513
|
+
setNotice("已插入旧版内容");
|
|
2514
|
+
}
|
|
2515
|
+
},
|
|
2516
|
+
title: "把这一版的内容插入输入框",
|
|
2517
|
+
},
|
|
2518
|
+
"插入旧版"
|
|
2519
|
+
)
|
|
2520
|
+
),
|
|
2425
2521
|
v.content
|
|
2426
2522
|
)
|
|
2427
2523
|
)
|
package/index.js
CHANGED
|
@@ -34,48 +34,78 @@ const TEMPLATE_PACK = [
|
|
|
34
34
|
category: "开发",
|
|
35
35
|
tags: ["代码", "审查"],
|
|
36
36
|
content: "请审查下面这段代码,重点关注 {{关注点}},指出潜在 bug、性能问题与可读性改进,并给出修改建议。\n\n```\n{{代码}}\n```",
|
|
37
|
+
variables: {
|
|
38
|
+
关注点: {
|
|
39
|
+
label: "关注点",
|
|
40
|
+
options: ["正确性、边界与命名", "性能与内存", "安全漏洞", "可读性与重构"],
|
|
41
|
+
default: "正确性、边界与命名",
|
|
42
|
+
},
|
|
43
|
+
代码: { label: "待审查代码", required: true },
|
|
44
|
+
},
|
|
37
45
|
},
|
|
38
46
|
{
|
|
39
47
|
name: "Git 提交信息",
|
|
40
48
|
category: "开发",
|
|
41
49
|
tags: ["git", "提交"],
|
|
42
50
|
content: "请根据以下变更,生成一条规范的 git commit message(约定式提交风格,一行标题 + 空行 + 要点式正文):\n\n{{变更说明}}",
|
|
51
|
+
variables: { 变更说明: { label: "变更说明", required: true } },
|
|
43
52
|
},
|
|
44
53
|
{
|
|
45
54
|
name: "Bug 排查",
|
|
46
55
|
category: "开发",
|
|
47
56
|
tags: ["bug", "排查"],
|
|
48
57
|
content: "我遇到一个问题:{{现象}}。相关环境:{{环境}}。已尝试:{{已尝试}}。请按「现象 → 根因假设 → 验证方法 → 修复方案」逐步分析。",
|
|
58
|
+
variables: {
|
|
59
|
+
现象: { label: "问题现象", required: true },
|
|
60
|
+
环境: { label: "相关环境", default: "操作系统 / 版本 / 依赖" },
|
|
61
|
+
已尝试: { label: "已尝试的方法" },
|
|
62
|
+
},
|
|
49
63
|
},
|
|
50
64
|
{
|
|
51
65
|
name: "周报",
|
|
52
66
|
category: "写作",
|
|
53
67
|
tags: ["周报", "工作"],
|
|
54
68
|
content: "请根据以下要点,帮我整理成一份结构清晰的周报(本周完成 / 进行中 / 下周计划 / 风险与求助):\n\n{{要点}}",
|
|
69
|
+
variables: { 要点: { label: "工作要点", required: true } },
|
|
55
70
|
},
|
|
56
71
|
{
|
|
57
72
|
name: "翻译润色",
|
|
58
73
|
category: "写作",
|
|
59
74
|
tags: ["翻译", "润色"],
|
|
60
75
|
content: "请把下面的内容翻译成 {{目标语言}},并润色为自然流畅的表达,保留原意与格式:\n\n{{原文}}",
|
|
76
|
+
variables: {
|
|
77
|
+
目标语言: {
|
|
78
|
+
label: "目标语言",
|
|
79
|
+
options: ["英文", "简体中文", "繁体中文", "日文", "韩文", "法文", "德文"],
|
|
80
|
+
default: "英文",
|
|
81
|
+
},
|
|
82
|
+
原文: { label: "待翻译内容", required: true },
|
|
83
|
+
},
|
|
61
84
|
},
|
|
62
85
|
{
|
|
63
86
|
name: "日报",
|
|
64
87
|
category: "写作",
|
|
65
88
|
tags: ["日报", "工作"],
|
|
66
89
|
content: "今天是 {{date}},请根据以下要点帮我生成一份工作日报(今日完成 / 明日计划 / 需要协调):\n\n{{要点}}",
|
|
90
|
+
variables: { 要点: { label: "工作要点", required: true } },
|
|
67
91
|
},
|
|
68
92
|
{
|
|
69
93
|
name: "会议纪要",
|
|
70
94
|
category: "写作",
|
|
71
95
|
tags: ["会议", "纪要"],
|
|
72
96
|
content: "请把下面的会议记录整理成纪要(结论 / 待办与负责人 / 时间点):\n\n{{记录}}",
|
|
97
|
+
variables: { 记录: { label: "会议记录", required: true } },
|
|
73
98
|
},
|
|
74
99
|
{
|
|
75
100
|
name: "学习计划",
|
|
76
101
|
category: "学习",
|
|
77
102
|
tags: ["学习", "计划"],
|
|
78
103
|
content: "我想在 {{时间}} 内掌握 {{主题}},目前基础是 {{当前水平}}。请制定一个可执行的学习计划,分阶段列出目标、内容与检验方式。",
|
|
104
|
+
variables: {
|
|
105
|
+
时间: { label: "学习周期", default: "3 个月" },
|
|
106
|
+
主题: { label: "学习主题", required: true },
|
|
107
|
+
当前水平: { label: "当前基础", default: "零基础" },
|
|
108
|
+
},
|
|
79
109
|
},
|
|
80
110
|
];
|
|
81
111
|
|
|
@@ -292,7 +322,7 @@ function seedTemplates() {
|
|
|
292
322
|
const now = Date.now();
|
|
293
323
|
const snippets = TEMPLATE_PACK.map((t, i) => {
|
|
294
324
|
const { names } = scanPlaceholders(t.content);
|
|
295
|
-
const { variables } = normalizeVariables(names, {});
|
|
325
|
+
const { variables } = normalizeVariables(names, t.variables || {});
|
|
296
326
|
return {
|
|
297
327
|
id: randomUUID(),
|
|
298
328
|
name: t.name,
|
|
@@ -957,17 +987,23 @@ async function saveSnippet(payload) {
|
|
|
957
987
|
if (other) {
|
|
958
988
|
return conflictError("name", `已有另一个片段叫「${name}」。`, other);
|
|
959
989
|
}
|
|
960
|
-
// 记历史:被覆盖的这一版(name/content/variables
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
990
|
+
// 记历史:被覆盖的这一版(name/content/variables 快照)。
|
|
991
|
+
// 只改分类/标签/置顶这类元数据时内容与变量都没动,不该产生一条历史版本,
|
|
992
|
+
// 否则「换了个标签」就多出一次可回退的旧版,历史列表会迅速被噪声塞满。
|
|
993
|
+
const contentChanged = existing.content !== content;
|
|
994
|
+
const variablesChanged = JSON.stringify(existing.variables) !== JSON.stringify(variables);
|
|
995
|
+
if (contentChanged || variablesChanged) {
|
|
996
|
+
historyId = existing.id;
|
|
997
|
+
historySnapshot = {
|
|
998
|
+
name: existing.name,
|
|
999
|
+
content: existing.content,
|
|
1000
|
+
variables: existing.variables,
|
|
1001
|
+
category: existing.category,
|
|
1002
|
+
tags: existing.tags,
|
|
1003
|
+
at: existing.updatedAt,
|
|
1004
|
+
revision: existing.revision,
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
971
1007
|
existing.name = name;
|
|
972
1008
|
existing.content = content;
|
|
973
1009
|
existing.variables = variables;
|
|
@@ -1570,22 +1606,7 @@ async function dispatch(ctx, endpoint, payload, signal) {
|
|
|
1570
1606
|
// 不重新落盘、不重新生成内容——否则会覆盖掉用户手工改过的导出文件。
|
|
1571
1607
|
if (typeof payload?.revealPath === "string" && payload.revealPath.trim() !== "") {
|
|
1572
1608
|
const p = payload.revealPath.trim();
|
|
1573
|
-
|
|
1574
|
-
let revealError = null;
|
|
1575
|
-
if (process.platform === "win32") {
|
|
1576
|
-
try {
|
|
1577
|
-
const { spawn } = await import("node:child_process");
|
|
1578
|
-
await new Promise((resolve) => {
|
|
1579
|
-
const child = spawn("explorer.exe", [`/select,${p}`], { stdio: "ignore", detached: true });
|
|
1580
|
-
child.on("error", () => resolve(false));
|
|
1581
|
-
child.on("spawn", () => resolve(true));
|
|
1582
|
-
setTimeout(() => resolve(true), 500);
|
|
1583
|
-
});
|
|
1584
|
-
revealed = true;
|
|
1585
|
-
} catch (error) {
|
|
1586
|
-
revealError = error instanceof Error ? error.message : String(error);
|
|
1587
|
-
}
|
|
1588
|
-
}
|
|
1609
|
+
const { revealed, revealError } = await revealInFileManager(p);
|
|
1589
1610
|
return { ok: true, value: { path: p, revealed, revealError: revealError || undefined } };
|
|
1590
1611
|
}
|
|
1591
1612
|
// payload.write = 文件名(字符串)时,落盘到数据目录的 exports/ 并回传路径。
|
|
@@ -1600,24 +1621,12 @@ async function dispatch(ctx, endpoint, payload, signal) {
|
|
|
1600
1621
|
} catch (error) {
|
|
1601
1622
|
return fail("internal", `写入导出文件失败:${error instanceof Error ? error.message : String(error)}`);
|
|
1602
1623
|
}
|
|
1603
|
-
// 请求方要求打开导出文件所在目录时,用系统默认方式 reveal(Windows: explorer /select
|
|
1604
|
-
//
|
|
1624
|
+
// 请求方要求打开导出文件所在目录时,用系统默认方式 reveal(Windows: explorer /select;
|
|
1625
|
+
// macOS: open -R;Linux: xdg-open 目录)。打开失败只降级不报错——用户仍可凭回传的绝对路径手动定位。
|
|
1605
1626
|
let revealed = false;
|
|
1606
1627
|
let revealError = null;
|
|
1607
|
-
if (payload.reveal === true
|
|
1608
|
-
|
|
1609
|
-
const { spawn } = await import("node:child_process");
|
|
1610
|
-
await new Promise((resolve) => {
|
|
1611
|
-
const p = spawn("explorer.exe", [`/select,${target}`], { stdio: "ignore", detached: true });
|
|
1612
|
-
p.on("error", () => resolve(false));
|
|
1613
|
-
p.on("spawn", () => resolve(true));
|
|
1614
|
-
// explorer 可能已经存在则立即退出,也可能挂起;500ms 兜底,不阻塞导出结果
|
|
1615
|
-
setTimeout(() => resolve(true), 500);
|
|
1616
|
-
});
|
|
1617
|
-
revealed = true;
|
|
1618
|
-
} catch (error) {
|
|
1619
|
-
revealError = error instanceof Error ? error.message : String(error);
|
|
1620
|
-
}
|
|
1628
|
+
if (payload.reveal === true) {
|
|
1629
|
+
({ revealed, revealError } = await revealInFileManager(target));
|
|
1621
1630
|
}
|
|
1622
1631
|
return {
|
|
1623
1632
|
ok: true,
|
|
@@ -1686,6 +1695,42 @@ const ADD_USAGE = "/snippet add <名称> <内容> [--cat=<分类>] [--tag=<标
|
|
|
1686
1695
|
const USAGE = `${ADD_USAGE} | list | show <名称> | rm <名称|#id> | undo <名称|#id> | mv <名称> <新名称> | pin <名称|#id> | unpin <名称|#id> | tag <名称|#id> <标签…> | optimize <名称> | export [文件名] | import <文件路径> [--replace] [--dry]`;
|
|
1687
1696
|
|
|
1688
1697
|
/** 导出文件名不能带路径分隔符,也不能是 `.`/`..`,避免写出数据目录。 */
|
|
1698
|
+
/**
|
|
1699
|
+
* 用系统默认方式在文件管理器里定位一个已存在的文件。
|
|
1700
|
+
* win32 → explorer /select,;darwin → open -R;linux 等 → xdg-open 所在目录。
|
|
1701
|
+
* 打开失败只降级(revealed=false + revealError),不抛异常。
|
|
1702
|
+
*/
|
|
1703
|
+
async function revealInFileManager(filePath) {
|
|
1704
|
+
let revealed = false;
|
|
1705
|
+
let revealError = null;
|
|
1706
|
+
try {
|
|
1707
|
+
const { spawn } = await import("node:child_process");
|
|
1708
|
+
let cmd;
|
|
1709
|
+
let args;
|
|
1710
|
+
if (process.platform === "win32") {
|
|
1711
|
+
cmd = "explorer.exe";
|
|
1712
|
+
args = [`/select,${filePath}`];
|
|
1713
|
+
} else if (process.platform === "darwin") {
|
|
1714
|
+
cmd = "open";
|
|
1715
|
+
args = ["-R", filePath];
|
|
1716
|
+
} else {
|
|
1717
|
+
cmd = "xdg-open";
|
|
1718
|
+
args = [dirname(filePath)];
|
|
1719
|
+
}
|
|
1720
|
+
await new Promise((resolve) => {
|
|
1721
|
+
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
1722
|
+
child.on("error", () => resolve(false));
|
|
1723
|
+
child.on("spawn", () => resolve(true));
|
|
1724
|
+
// 文件管理器可能立即退出也可能挂起;500ms 兜底,不阻塞导出结果
|
|
1725
|
+
setTimeout(() => resolve(true), 500);
|
|
1726
|
+
});
|
|
1727
|
+
revealed = true;
|
|
1728
|
+
} catch (error) {
|
|
1729
|
+
revealError = error instanceof Error ? error.message : String(error);
|
|
1730
|
+
}
|
|
1731
|
+
return { revealed, revealError };
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1689
1734
|
function safeExportName(raw) {
|
|
1690
1735
|
const text = typeof raw === "string" ? raw.trim() : "";
|
|
1691
1736
|
const base = text === "" ? "snippets" : text;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-prompt-snippets",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Save reusable prompt snippets with categories, {{placeholder}} fill forms & dropdown enums, version history, pinned/tag search, and crash-safe atomic JSON storage — insert them into the composer from a draggable dock.",
|
|
6
6
|
"keywords": [
|