dsh-long-plugins 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/LICENSE +21 -0
- package/README.md +129 -0
- package/client/client.js +2821 -0
- package/client/vendor/chart.umd.min.js +20 -0
- package/client/vendor/docx-preview.min.js +8 -0
- package/client/vendor/jszip.min.js +13 -0
- package/client/vendor/pptxviewjs.min.js +1 -0
- package/cordis.patch.yml +12 -0
- package/dsh.plugin.json +14 -0
- package/lib/index.js +2027 -0
- package/lib/md2docx.py +210 -0
- package/package.json +58 -0
- package/patches/dsh-client-connection-heartbeat.sh +107 -0
- package/skill/dsh-common-plugins-install/SKILL.md +128 -0
- package/skill/dsh-long-plugins-install/SKILL.md +200 -0
- package/skill/dsh-upgrade/SKILL.md +89 -0
- package/skill/dsh-web-start-panel-install/SKILL.md +349 -0
- package/skill/dsh-web-win-service-install/SKILL.md +243 -0
package/lib/md2docx.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Generic Markdown -> styled .docx converter with a page-number footer.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
python3 md2docx.py <input.md> [output.docx]
|
|
7
|
+
|
|
8
|
+
If output.docx is omitted, it defaults to <input-name>.docx next to the input.
|
|
9
|
+
|
|
10
|
+
Renders headings (h1-h3), bold/italic inline, tables (pipe syntax),
|
|
11
|
+
ordered/unordered lists, blockquotes, and horizontal rules. Adds a centered
|
|
12
|
+
footer with an auto-updating PAGE field (updates when opened in Word or
|
|
13
|
+
exported to PDF).
|
|
14
|
+
|
|
15
|
+
Requires: python3 + python-docx (`pip install python-docx`).
|
|
16
|
+
"""
|
|
17
|
+
import re
|
|
18
|
+
import sys
|
|
19
|
+
import os
|
|
20
|
+
|
|
21
|
+
from docx import Document
|
|
22
|
+
from docx.shared import Pt, RGBColor
|
|
23
|
+
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
|
24
|
+
from docx.enum.table import WD_TABLE_ALIGNMENT
|
|
25
|
+
from docx.oxml.ns import qn
|
|
26
|
+
from docx.oxml import OxmlElement
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def die(msg):
|
|
30
|
+
print(f"md2docx: {msg}", file=sys.stderr)
|
|
31
|
+
sys.exit(1)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
if len(sys.argv) < 2:
|
|
35
|
+
die("usage: md2docx.py <input.md> [output.docx]")
|
|
36
|
+
|
|
37
|
+
SRC = os.path.abspath(sys.argv[1])
|
|
38
|
+
if not os.path.isfile(SRC):
|
|
39
|
+
die(f"input file not found: {SRC}")
|
|
40
|
+
OUT = (
|
|
41
|
+
os.path.abspath(sys.argv[2])
|
|
42
|
+
if len(sys.argv) > 2
|
|
43
|
+
else os.path.splitext(SRC)[0] + ".docx"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
with open(SRC, encoding="utf-8") as f:
|
|
47
|
+
lines = f.read().splitlines()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def add_page_number(paragraph):
|
|
51
|
+
"""Insert an auto-updating '第 N 页' page field into a footer paragraph."""
|
|
52
|
+
run = paragraph.add_run("第 ")
|
|
53
|
+
set_east_asia(run)
|
|
54
|
+
fld = OxmlElement("w:fldChar")
|
|
55
|
+
fld.set(qn("w:fldCharType"), "begin")
|
|
56
|
+
instr = OxmlElement("w:instrText")
|
|
57
|
+
instr.set(qn("xml:space"), "preserve")
|
|
58
|
+
instr.text = " PAGE "
|
|
59
|
+
sep = OxmlElement("w:fldChar")
|
|
60
|
+
sep.set(qn("w:fldCharType"), "separate")
|
|
61
|
+
t = OxmlElement("w:t")
|
|
62
|
+
t.text = "1"
|
|
63
|
+
end = OxmlElement("w:fldChar")
|
|
64
|
+
end.set(qn("w:fldCharType"), "end")
|
|
65
|
+
r = paragraph.add_run()
|
|
66
|
+
set_east_asia(r)
|
|
67
|
+
for el in (fld, instr, sep, t, end):
|
|
68
|
+
r._r.append(el)
|
|
69
|
+
run2 = paragraph.add_run(" 页")
|
|
70
|
+
set_east_asia(run2)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
doc = Document()
|
|
74
|
+
doc.add_heading(os.path.splitext(os.path.basename(SRC))[0], level=0)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# --- base styles ---
|
|
78
|
+
normal = doc.styles["Normal"]
|
|
79
|
+
normal.font.name = "Calibri"
|
|
80
|
+
normal.font.size = Pt(10.5)
|
|
81
|
+
normal._element.rPr.rFonts.set(qn("w:eastAsia"), "微软雅黑")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def set_east_asia(run):
|
|
85
|
+
run.font.name = "Calibri"
|
|
86
|
+
r = run._element
|
|
87
|
+
rPr = r.get_or_add_rPr()
|
|
88
|
+
rf = rPr.find(qn("w:rFonts"))
|
|
89
|
+
if rf is None:
|
|
90
|
+
rf = OxmlElement("w:rFonts")
|
|
91
|
+
rPr.append(rf)
|
|
92
|
+
rf.set(qn("w:eastAsia"), "微软雅黑")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def add_runs_with_bold(par, text):
|
|
96
|
+
"""Add text to paragraph, honoring **bold** and *italic* markers."""
|
|
97
|
+
for tok in re.split(r"(\*\*.*?\*\*|\*.*?\*)", text):
|
|
98
|
+
if not tok:
|
|
99
|
+
continue
|
|
100
|
+
if tok.startswith("**") and tok.endswith("**") and len(tok) > 4:
|
|
101
|
+
r = par.add_run(tok[2:-2])
|
|
102
|
+
r.bold = True
|
|
103
|
+
elif tok.startswith("*") and tok.endswith("*") and len(tok) > 2:
|
|
104
|
+
r = par.add_run(tok[1:-1])
|
|
105
|
+
r.italic = True
|
|
106
|
+
else:
|
|
107
|
+
r = par.add_run(tok)
|
|
108
|
+
set_east_asia(r)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def add_body_paragraph(text, style=None):
|
|
112
|
+
p = doc.add_paragraph(style=style)
|
|
113
|
+
add_runs_with_bold(p, text)
|
|
114
|
+
return p
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def flush_table():
|
|
118
|
+
global table_rows, in_table
|
|
119
|
+
if not table_rows:
|
|
120
|
+
in_table = False
|
|
121
|
+
return
|
|
122
|
+
ncols = max(len(r) for r in table_rows)
|
|
123
|
+
tbl = doc.add_table(rows=len(table_rows), cols=ncols)
|
|
124
|
+
tbl.style = "Light Grid Accent 1"
|
|
125
|
+
tbl.alignment = WD_TABLE_ALIGNMENT.CENTER
|
|
126
|
+
for ri, row in enumerate(table_rows):
|
|
127
|
+
for ci in range(ncols):
|
|
128
|
+
cell = tbl.cell(ri, ci)
|
|
129
|
+
cell.text = ""
|
|
130
|
+
cp = cell.paragraphs[0]
|
|
131
|
+
text = row[ci] if ci < len(row) else ""
|
|
132
|
+
add_runs_with_bold(cp, text)
|
|
133
|
+
if ri == 0:
|
|
134
|
+
for run in cp.runs:
|
|
135
|
+
run.bold = True
|
|
136
|
+
tbl.autofit = True
|
|
137
|
+
table_rows = []
|
|
138
|
+
in_table = False
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
table_rows = []
|
|
142
|
+
in_table = False
|
|
143
|
+
|
|
144
|
+
i = 0
|
|
145
|
+
while i < len(lines):
|
|
146
|
+
stripped = lines[i].strip()
|
|
147
|
+
if not stripped:
|
|
148
|
+
i += 1
|
|
149
|
+
continue
|
|
150
|
+
if re.fullmatch(r"-{3,}", stripped):
|
|
151
|
+
flush_table()
|
|
152
|
+
doc.add_paragraph()
|
|
153
|
+
i += 1
|
|
154
|
+
continue
|
|
155
|
+
if in_table and "|" in stripped and re.fullmatch(r"\|?[\s:|-]+\|?", stripped):
|
|
156
|
+
i += 1
|
|
157
|
+
continue
|
|
158
|
+
if stripped.startswith("|"):
|
|
159
|
+
in_table = True
|
|
160
|
+
table_rows.append([c.strip() for c in stripped.strip("|").split("|")])
|
|
161
|
+
i += 1
|
|
162
|
+
continue
|
|
163
|
+
if in_table:
|
|
164
|
+
flush_table()
|
|
165
|
+
if stripped.startswith("### "):
|
|
166
|
+
h = doc.add_heading(level=3)
|
|
167
|
+
add_runs_with_bold(h, stripped[4:])
|
|
168
|
+
i += 1
|
|
169
|
+
continue
|
|
170
|
+
if stripped.startswith("## "):
|
|
171
|
+
h = doc.add_heading(level=2)
|
|
172
|
+
add_runs_with_bold(h, stripped[3:])
|
|
173
|
+
i += 1
|
|
174
|
+
continue
|
|
175
|
+
if stripped.startswith("# "):
|
|
176
|
+
h = doc.add_heading(level=1)
|
|
177
|
+
add_runs_with_bold(h, stripped[2:])
|
|
178
|
+
i += 1
|
|
179
|
+
continue
|
|
180
|
+
if stripped.startswith("> "):
|
|
181
|
+
p = doc.add_paragraph(style="Intense Quote")
|
|
182
|
+
add_runs_with_bold(p, stripped[2:] + " ")
|
|
183
|
+
i += 1
|
|
184
|
+
continue
|
|
185
|
+
m = re.match(r"^(\d+)\.\s+(.*)$", stripped)
|
|
186
|
+
if m:
|
|
187
|
+
p = doc.add_paragraph(style="List Number")
|
|
188
|
+
add_runs_with_bold(p, m.group(2))
|
|
189
|
+
i += 1
|
|
190
|
+
continue
|
|
191
|
+
if stripped.startswith("- "):
|
|
192
|
+
p = doc.add_paragraph(style="List Bullet")
|
|
193
|
+
add_runs_with_bold(p, stripped[2:])
|
|
194
|
+
i += 1
|
|
195
|
+
continue
|
|
196
|
+
p = doc.add_paragraph()
|
|
197
|
+
add_runs_with_bold(p, stripped)
|
|
198
|
+
i += 1
|
|
199
|
+
|
|
200
|
+
if in_table:
|
|
201
|
+
flush_table()
|
|
202
|
+
|
|
203
|
+
# --- footer with page number field ---
|
|
204
|
+
footer = doc.sections[0].footer
|
|
205
|
+
fp = footer.paragraphs[0]
|
|
206
|
+
fp.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
207
|
+
add_page_number(fp)
|
|
208
|
+
|
|
209
|
+
doc.save(OUT)
|
|
210
|
+
print("Saved:", OUT)
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-long-plugins",
|
|
3
|
+
"version": "1.3.1",
|
|
4
|
+
"description": "Merged DSH web plugins: upload manager (drag-and-drop attach any file to the message), workspace output files, skill docs, account balance, Markdown-to-Word (md2docx), polished file preview, and an auto-repair install for DSH core patches (reverse-proxy WebSocket heartbeat, upgrade relink). | 一个插件整合 DSH Web 的常用增强:上传管理(拖放上传:拖任意文件到会话框直接附加)、工作区「输出文件」面板、技能文档浏览、账户余额显示、Markdown 转 Word(md2docx);并自带修复安装,自动重连 DSH 核心补丁(反代 WebSocket 心跳、升级重连)。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "lib/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./lib/index.js",
|
|
10
|
+
"./client": "./client/client.js",
|
|
11
|
+
"./package.json": "./package.json"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"lib",
|
|
15
|
+
"lib/md2docx.py",
|
|
16
|
+
"client",
|
|
17
|
+
"!client/*.bak",
|
|
18
|
+
"cordis.patch.yml",
|
|
19
|
+
"dsh.plugin.json",
|
|
20
|
+
"skill/dsh-long-plugins-install/SKILL.md",
|
|
21
|
+
"skill/dsh-upgrade/SKILL.md",
|
|
22
|
+
"skill/dsh-common-plugins-install/SKILL.md",
|
|
23
|
+
"skill/dsh-web-win-service-install/SKILL.md",
|
|
24
|
+
"skill/dsh-web-start-panel-install/SKILL.md",
|
|
25
|
+
"patches/dsh-client-connection-heartbeat.sh"
|
|
26
|
+
],
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"mammoth": "^1.12.1",
|
|
29
|
+
"exceljs": "^4.4.0"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"@deepseek-ai/dsh-tools": "*"
|
|
33
|
+
},
|
|
34
|
+
"peerDependenciesMeta": {
|
|
35
|
+
"@deepseek-ai/dsh-tools": {
|
|
36
|
+
"optional": true
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"dsh": {
|
|
40
|
+
"bundle": {
|
|
41
|
+
"patch": "./cordis.patch.yml"
|
|
42
|
+
},
|
|
43
|
+
"client": {
|
|
44
|
+
"inject": [
|
|
45
|
+
"@deepseek-ai/dsh-client-connection",
|
|
46
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
47
|
+
"@deepseek-ai/dsh-client-locale",
|
|
48
|
+
"@deepseek-ai/dsh-client-ui-slots",
|
|
49
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
50
|
+
"@deepseek-ai/dsh-client-ui-theme",
|
|
51
|
+
"@deepseek-ai/dsh-client-ui-conversation",
|
|
52
|
+
"@deepseek-ai/dsh-client-ui-input-trigger",
|
|
53
|
+
"@deepseek-ai/dsh-client-ui-layout"
|
|
54
|
+
],
|
|
55
|
+
"platform": "web"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# dsh-client-connection-heartbeat.sh — 给 DSH 核心下行 WebSocket 加心跳补丁
|
|
3
|
+
#
|
|
4
|
+
# 作用:反代/中间设备会按只读超时关停长期沉默的上游 WebSocket。提问/审批窗口
|
|
5
|
+
# 等待人类作答时 agent 循环被暂停、无帧流动,mux/host 流被切断后前端清空
|
|
6
|
+
# 「待回答问题」→ 窗口消失,只能靠刷新重连恢复。
|
|
7
|
+
# 修复:给 DSH 核心 dsh-client-connection 的下行 WebSocket 每 15s 只 ping 一次,
|
|
8
|
+
# 保持下游流量,让反代下等待作答的问题窗口不再消失。只 ping、不做 pong 判定、
|
|
9
|
+
# 不 terminate,避免后台/节流的手机端(pong 延迟)被误判为死连接而断开。
|
|
10
|
+
#
|
|
11
|
+
# 该补丁改的是 DSH 核心模块(不在本插件内)。装 dsh-long-plugins 时 install.sh 会
|
|
12
|
+
# 调本脚本自动重打;DSH 升级会覆盖核心 → 需重跑本脚本(或跑插件升级流程)。
|
|
13
|
+
#
|
|
14
|
+
# 幂等:可重复执行,已打过会跳过。
|
|
15
|
+
# 用法:sh dsh-client-connection-heartbeat.sh
|
|
16
|
+
set -e
|
|
17
|
+
echo "==> dsh-client-connection 心跳补丁"
|
|
18
|
+
|
|
19
|
+
# 定位 DSH 核心里 dsh-client-connection 的 index.js
|
|
20
|
+
# 优先用 npm root -g;找不到再探测常见全局路径。
|
|
21
|
+
NPM_ROOT="$(npm root -g 2>/dev/null || true)"
|
|
22
|
+
CANDIDATES=""
|
|
23
|
+
[ -n "$NPM_ROOT" ] && CANDIDATES="$NPM_ROOT/@deepseek-ai/dsh/node_modules/@deepseek-ai/dsh-client-connection/lib/index.js"
|
|
24
|
+
CANDIDATES="$CANDIDATES
|
|
25
|
+
/volume1/npm/global/lib/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai/dsh-client-connection/lib/index.js
|
|
26
|
+
/usr/local/lib/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai/dsh-client-connection/lib/index.js
|
|
27
|
+
$HOME/.dsh/../node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai/dsh-client-connection/lib/index.js"
|
|
28
|
+
|
|
29
|
+
IDX=""
|
|
30
|
+
for c in $CANDIDATES; do
|
|
31
|
+
[ -f "$c" ] && IDX="$c" && break
|
|
32
|
+
done
|
|
33
|
+
if [ -z "$IDX" ]; then
|
|
34
|
+
echo "错误:找不到 dsh-client-connection/lib/index.js(核心未装或路径不同)" >&2
|
|
35
|
+
echo " 请确认 DSH 已安装,并把 $IDX 路径填入本脚本,或联系维护者。" >&2
|
|
36
|
+
exit 1
|
|
37
|
+
fi
|
|
38
|
+
echo " 目标: $IDX"
|
|
39
|
+
|
|
40
|
+
# 幂等:已含心跳常量则跳过
|
|
41
|
+
if grep -q 'WEBSOCKET_HEARTBEAT_MS' "$IDX"; then
|
|
42
|
+
echo " 已打过,跳过 ✓"
|
|
43
|
+
exit 0
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# 待替换的原始代码未在 -> 提示人工核对
|
|
47
|
+
if ! grep -q 'var WebSocketDownlinks = class {' "$IDX"; then
|
|
48
|
+
echo " 警告:未找到 WebSocketDownlinks 类,可能 DSH 已改逻辑,请人工核对" >&2
|
|
49
|
+
exit 1
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
cp "$IDX" "$IDX.bak-heartbeat-$(date +%Y%m%d-%H%M)"
|
|
53
|
+
python3 - "$IDX" <<'PY'
|
|
54
|
+
import sys
|
|
55
|
+
p = sys.argv[1]
|
|
56
|
+
T = "\t"
|
|
57
|
+
with open(p, encoding="utf-8") as f:
|
|
58
|
+
s = f.read()
|
|
59
|
+
|
|
60
|
+
def rep(old, new, label):
|
|
61
|
+
global s
|
|
62
|
+
if old not in s:
|
|
63
|
+
sys.stderr.write(" 跳过(未找到): %s\n" % label); return
|
|
64
|
+
if s.count(old) > 1:
|
|
65
|
+
sys.stderr.write(" 警告(出现%d次,跳过): %s\n" % (s.count(old), label)); return
|
|
66
|
+
s = s.replace(old, new, 1)
|
|
67
|
+
print(" 已应用: %s" % label)
|
|
68
|
+
|
|
69
|
+
CONST_COMMENT = ("/** Downlink heartbeat interval: keep the browser streams from idling out under\n"
|
|
70
|
+
"* a reverse proxy, whose read timeout would drop a mux/host socket a paused\n"
|
|
71
|
+
"* agent leaves silent. Must stay below the proxy's read timeout. */\n")
|
|
72
|
+
rep("var WebSocketDownlinks = class {",
|
|
73
|
+
CONST_COMMENT + "const WEBSOCKET_HEARTBEAT_MS = 15_000;\nvar WebSocketDownlinks = class {",
|
|
74
|
+
"心跳常量")
|
|
75
|
+
|
|
76
|
+
o_ctor = ("host API supplying the typed event streams. */\n" + T + "constructor(api) {\n" + T + T + "this.api = api;")
|
|
77
|
+
p_ctor = ("host API supplying the typed event streams. */\n" + T + "constructor(api) {\n" + T + T + "this.api = api;\n" +
|
|
78
|
+
T + T + "// A reverse proxy cuts an upstream WebSocket that stays silent for its\n" +
|
|
79
|
+
T + T + "// read timeout; while a question or approval waits on the human the agent\n" +
|
|
80
|
+
T + T + "// loop is paused and no frames flow, so the mux/host streams drop and the\n" +
|
|
81
|
+
T + T + "// client clears its pending question. Pinging the clients every\n" +
|
|
82
|
+
T + T + "// WEBSOCKET_HEARTBEAT_MS keeps downstream traffic flowing so the proxy does\n" +
|
|
83
|
+
T + T + "// not idle the downlink out. The pings are only sent, never reaped: a\n" +
|
|
84
|
+
T + T + "// throttled/backgrounded mobile client may answer a pong late, and\n" +
|
|
85
|
+
T + T + "// terminating on a missed pong would reopen the very disconnect we are\n" +
|
|
86
|
+
T + T + "// preventing — dead sockets are cleaned by the ws close/error path anyway.\n" +
|
|
87
|
+
T + T + "this.heartbeat = setInterval(() => {\n" +
|
|
88
|
+
T + T + T + "for (const socket of this.server.clients) {\n" +
|
|
89
|
+
T + T + T + T + "if (socket.readyState === WebSocket.OPEN) socket.ping();\n" +
|
|
90
|
+
T + T + T + "}\n" +
|
|
91
|
+
T + T + "}, WEBSOCKET_HEARTBEAT_MS);\n" +
|
|
92
|
+
T + T + "this.heartbeat.unref?.();")
|
|
93
|
+
rep(o_ctor, p_ctor, "心跳 interval")
|
|
94
|
+
|
|
95
|
+
o_close = (T + "async close() {\n" + T + T + "for (const socket of this.server.clients) socket.terminate();")
|
|
96
|
+
p_close = (T + "async close() {\n" + T + T + "clearInterval(this.heartbeat);\n" + T + T + "for (const socket of this.server.clients) socket.terminate();")
|
|
97
|
+
rep(o_close, p_close, "close 清理心跳")
|
|
98
|
+
|
|
99
|
+
with open(p, "w", encoding="utf-8") as f:
|
|
100
|
+
f.write(s)
|
|
101
|
+
PY
|
|
102
|
+
|
|
103
|
+
echo " 已应用 ✓"
|
|
104
|
+
[ -f "$IDX" ] && node --check "$IDX" >/dev/null 2>&1 && echo " 语法检查 OK" || echo " 语法检查失败(请核对)" >&2
|
|
105
|
+
echo
|
|
106
|
+
echo "✅ 心跳补丁完成。请重启 dsh 并强刷浏览器。"
|
|
107
|
+
echo " (DSH 升级会覆盖核心,需重跑本脚本)"
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dsh-common-plugins-install
|
|
3
|
+
description: 在一台已装好 DSH 的机器上自动安装"常用插件"清单(当前:dsh-market/dsh-market、jasonrale/dsh-archive-manager;可扩展)。优先用 `dsh plugin --profile web add <npm包名>`(一条命令装依赖+bundle+install);未上 npm 的源码插件才用本地 file:链接。凡涉及远程 push 一律停下等用户确认。用户说"安装常用插件"、"装 dsh-market"、"装 dsh-archive-manager"、"新机器装插件清单"时调用。
|
|
4
|
+
whenToUse: 用户要求安装/部署 dsh-market、dsh-archive-manager 等常用 DSH 插件,或在一台新机器上按清单安装常用插件时调用。
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# DSH 常用插件安装
|
|
8
|
+
|
|
9
|
+
作为部署助手,在一台**已装好 DSH** 的机器上安装"常用插件"清单。这些插件都是**自描述的 DSH 插件**(自带 `cordis.patch.yml` / `dsh.bundle.patch`),安装步骤统一;**清单可扩展**——往后加插件只需在"插件目录"加一行。
|
|
10
|
+
|
|
11
|
+
> 本 skill 是**操作指引**,由你(agent)在目标机器上执行。真实 shell/文件权限来自该机器;凡**远程 push / 打 tag / 发 Release** 一律停下,等用户明确确认(硬性安全边界)。
|
|
12
|
+
|
|
13
|
+
## 插件目录(当前收录)
|
|
14
|
+
|
|
15
|
+
| 插件 | 包名 (bundle名) | 安装命令(npm 发布,推荐) | 简介 | 备注 |
|
|
16
|
+
|---|---|---|---|---|
|
|
17
|
+
| dsh-market | `dshmarket` | `dsh plugin --profile web add dshmarket` | DSH 可视化插件市场(浏览/搜索/一键安装社区插件) | npm 已发布;cordis id `dsh-market`;仓库 `dsh-market/dsh-market`(main);本机已装 1.19.0(latest) |
|
|
18
|
+
| dsh-archive-manager | `dsh-archive-manager` | `dsh plugin --profile web add dsh-archive-manager` | 归档会话管理器(重开/取消归档/硬删归档会话,带搜索与同步) | npm 已发布 v1.1.1;仓库 `jasonrale/dsh-archive-manager`(master);cordis id `archive-manager`;引擎 node ≥22 |
|
|
19
|
+
|
|
20
|
+
> **bundle 名 = 该包 `package.json` 的 `name`**。**npm 已发布的插件直接用 `dsh plugin --profile web add <包名>` 安装**——该命令会自动写入 `dependencies`、加入 `dsh.profile.bundles`、并跑 `pnpm install`(本机已实证)。仅当插件**未发布到 npm** 时,才用下方"源码插件"的 git clone + `file:` 方式。
|
|
21
|
+
|
|
22
|
+
## 前置检查(先探测,再动手)
|
|
23
|
+
|
|
24
|
+
执行前先探测环境,**不要假设本机路径**。缺了就让用户补:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
dsh --version # 是否装了 DSH;没有则提示先按官方文档装 DSH
|
|
28
|
+
echo "$DSH_HOME" # 未设则查 /volume1/dsh、$HOME/.dsh 常见位置
|
|
29
|
+
ls -d <候选>/profiles/web # 定位 profile 目录(一般 web)
|
|
30
|
+
node --version # 需 >=22(--expose-internals 依赖;archive-manager 也要求 >=22)
|
|
31
|
+
which node ; command -v node # 找 node 真实路径;找不到让用户给
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
- **DSH_HOME**:默认 `$HOME/.dsh`(Windows)或 `/volume1/dsh`(NAS),以用户实际值为准。
|
|
35
|
+
- **profile**:默认 `web`;用 `dsh --profile <p> --dump-config` 核对。
|
|
36
|
+
|
|
37
|
+
## 目录约定(先确认,再动手)
|
|
38
|
+
|
|
39
|
+
- 插件目录 = `<DSH_HOME>/plugins`(习惯上源码插件放这里;npm 插件由 pnpm 装进 profile 的 `node_modules`,两处不冲突)。
|
|
40
|
+
- bin 目录 = `<DSH_HOME>/bin`(放 DSH 运行时脚本,缺失则创建)。
|
|
41
|
+
- 用户工作/上传目录与本插件清单无关(这两个插件不是文件上传类),**无需**设置 `DSH_UPLOAD_DIR`。
|
|
42
|
+
- 若未约定插件目录,默认用 `<DSH_HOME>/plugins`,并告知"以后所有插件都放这里"。
|
|
43
|
+
|
|
44
|
+
## 安装步骤(统一机制)
|
|
45
|
+
|
|
46
|
+
> **两种来源**:① npm 已发布的插件 → `dsh plugin --profile web add <包名>`(推荐);② 未上 npm 的源码插件 → git clone + `file:` 链接(更新靠手动 `git pull`)。不用 `github:` 依赖(会失去本地 clone 目录、依赖解析更不稳)。
|
|
47
|
+
|
|
48
|
+
### 路径 A —— npm 已发布的插件(一条命令)
|
|
49
|
+
```sh
|
|
50
|
+
dsh plugin --profile web add <包名>
|
|
51
|
+
# 等效:npx @deepseek-ai/dsh plugin --profile web add <包名>
|
|
52
|
+
```
|
|
53
|
+
> 该命令自动完成:写入 `dependencies` → 加入 `dsh.profile.bundles` → `pnpm install`。装完用 `dsh --profile web --dump-config` 确认插件已插入。
|
|
54
|
+
|
|
55
|
+
### 路径 B —— 源码插件(未发布到 npm)
|
|
56
|
+
#### B1. 获取源码 → 放入 DSH 插件目录
|
|
57
|
+
```sh
|
|
58
|
+
mkdir -p "$DSH_HOME/plugins" "$DSH_HOME/bin" # 确保存在,缺失即创建
|
|
59
|
+
git clone https://github.com/<owner>/<repo>.git "$DSH_HOME/plugins/<包名>"
|
|
60
|
+
# 已有则更新:
|
|
61
|
+
git -C "$DSH_HOME/plugins/<包名>" pull --ff-only origin <默认分支>
|
|
62
|
+
```
|
|
63
|
+
> **Windows 下 clone github.com 超时/被墙**时,用 codeload tarball:
|
|
64
|
+
> `Invoke-WebRequest https://codeload.github.com/<owner>/<repo>/tar.gz/refs/heads/<分支> -OutFile p.tgz` + `tar -xzf`,把解压后内层目录移动为 `$DSH_HOME/plugins/<包名>`。
|
|
65
|
+
|
|
66
|
+
#### B2. 读 package.json,确定 bundle 名与依赖
|
|
67
|
+
- `name` → bundle 名(加入 `dsh.profile.bundles` 与依赖 `file:` 引用名)
|
|
68
|
+
- `dsh.client.platform` / `dsh.client.inject` → 前端是否注入、注入哪些
|
|
69
|
+
- `dsh.bundle.patch`(多为 `./cordis.patch.yml`)→ 插件自带 insert,通常无需在 profile 写配置
|
|
70
|
+
- `peerDependencies` → 多为 `@deepseek-ai/*`、`react`,DSH 宿主已提供,一般不必额外装
|
|
71
|
+
|
|
72
|
+
#### B3. 注入依赖 + 加 bundle(file: 绝对路径,勿用相对路径)
|
|
73
|
+
```sh
|
|
74
|
+
# 在 profile 目录执行,<profile> 通常为 web
|
|
75
|
+
node -e "const fs=require('fs');const p=JSON.parse(fs.readFileSync('package.json','utf8'));p.dependencies=p.dependencies||{};p.dependencies['<包名>']='file:'+'$DSH_HOME/plugins/<包名>';p.dsh=p.dsh||{};p.dsh.profile=p.dsh.profile||{};p.dsh.profile.bundles=p.dsh.profile.bundles||[];if(!p.dsh.profile.bundles.includes('<包名>'))p.dsh.profile.bundles.push('<包名>');fs.writeFileSync('package.json',JSON.stringify(p,null,2)+'\n')"
|
|
76
|
+
```
|
|
77
|
+
> 任何平台都要把 `<包名>` 加进 `dsh.profile.bundles`。
|
|
78
|
+
|
|
79
|
+
#### B4. pnpm install
|
|
80
|
+
```sh
|
|
81
|
+
cd "$DSH_HOME/profiles/<profile>" && pnpm install
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 通用:写 cordis.patch.yml(通常不需要)
|
|
85
|
+
插件自带 `cordis.patch.yml`(`dsh.bundle.patch`)会自动 insert 自身。**一般不需要在 profile 的 `cordis.patch.yml` 里加东西。** 仅当要覆盖插件默认配置(如 `priority`、`trustedHosts`、`skillsRoot`)时,才在 `<profile>/cordis.patch.yml` 追加 `- id: <cordis-id>` 的 config 覆盖条目。本清单两个插件都不需要。
|
|
86
|
+
|
|
87
|
+
### 通用:启动(用户手动执行,agent 不自重启)
|
|
88
|
+
```sh
|
|
89
|
+
node --expose-internals --max-old-space-size=8192 <dsh/lib/bin.js> web --port 3080 &
|
|
90
|
+
```
|
|
91
|
+
> **`--expose-internals` 必须有**,否则插件相关路由 404。以目标机器现有启动方式为准;Windows 可参考本机 `<DSH_HOME>/bin/restart-dsh.cmd`(会停旧进程再带 `--expose-internals` 起新进程,会中断当前会话,由用户在独立终端跑)。
|
|
92
|
+
|
|
93
|
+
## 验证清单(逐项)
|
|
94
|
+
1. `dsh --profile <profile> --dump-config` 组合树里出现该插件(id + inject)。
|
|
95
|
+
2. 设置面板 / 前端出现对应插件入口(market 有"市场"入口;archive-manager 有归档/会话入口)。
|
|
96
|
+
3. 重启后强刷浏览器(Ctrl/Cmd+Shift+R)。
|
|
97
|
+
4. 上传/预览/md2docx 等其它插件功能不受影响(若本机已装 dsh-long-plugins)。
|
|
98
|
+
|
|
99
|
+
## 更新插件版本
|
|
100
|
+
- **npm 插件**:`dsh plugin --profile web add <包名>@最新` 或按 npm 版本;之后重启。
|
|
101
|
+
- **源码插件(file:)**:`file:` 目录依赖在 `pnpm install`(含 `--force`)下**不会自动重拷**到 profile 的 `node_modules`,需手动同步:
|
|
102
|
+
```sh
|
|
103
|
+
# 1) 更新源码
|
|
104
|
+
git -C "$DSH_HOME/plugins/<包名>" pull --ff-only origin <分支>
|
|
105
|
+
# 2) 删除 profile 里的陈旧副本,再重装(否则 bundle 仍指向旧拷贝)
|
|
106
|
+
Remove-Item -Recurse -Force "$DSH_HOME/profiles/<profile>/node_modules/<包名>"
|
|
107
|
+
cd "$DSH_HOME/profiles/<profile>" && pnpm install
|
|
108
|
+
# 3) 重启 DSH web
|
|
109
|
+
```
|
|
110
|
+
> 否则会出现"版本显示旧 / 改动不生效"——因为运行的是 node_modules 里的**独立拷贝**,不是源码目录(Windows 上 pnpm 常回退为拷贝而非符号链接)。
|
|
111
|
+
|
|
112
|
+
## 硬性安全边界(必须遵守)
|
|
113
|
+
- **不主动 `git push` / 打 tag / 发 Release**;需要发版本时停下用 `ask_user_question` 等用户确认。
|
|
114
|
+
- 所有写操作(改 package.json / cordis.patch.yml / 重启服务)先向用户说明将要改哪个文件、做什么,再执行。
|
|
115
|
+
- 探测到 `DSH_HOME`/`node`/profile 目录缺失时**停下提示**,让用户补充,不硬猜。
|
|
116
|
+
|
|
117
|
+
## 往清单里加新插件
|
|
118
|
+
只需在"插件目录"表加一行(包名、是否上 npm、仓库 `owner/repo`、默认分支、简介)。**若已上 npm** 就填安装命令 `dsh plugin add <包名>`;**若未上 npm** 用路径 B(clone+file:)。若新插件是文件/上传类或需要 `trustedHosts`/`skillsRoot` 等配置,再在"写 cordis.patch.yml"给出对应覆盖项。
|
|
119
|
+
|
|
120
|
+
## 常见坑速查
|
|
121
|
+
| 症状 | 原因 | 解决 |
|
|
122
|
+
|---|---|---|
|
|
123
|
+
| 设置面板不出现插件 | 未加 bundle | 用 `dsh plugin add`(自动加);或手动把 `<包名>` 加入 `dsh.profile.bundles` |
|
|
124
|
+
| 插件路由 404 | 启动缺 `--expose-internals` | 启动命令加该 flag |
|
|
125
|
+
| **npm 插件 version 显示旧 / 改动不生效** | 版本没更新 | `dsh plugin add <包名>@<目标版本>` 后重启;源码插件则删 `node_modules/<包名>` 重装 |
|
|
126
|
+
| `file:` 源码插件改动不生效 | pnpm 不重拷副本 | 删 `node_modules/<包名>` 重装 + 重启 |
|
|
127
|
+
| clone github.com 超时/被墙 | 网络不通 github.com(raw/codeload 常可达) | codeload tarball 解压后移动到插件目录 |
|
|
128
|
+
| market 检查不到 `file:` 插件更新 | market 硬编码 `updateAvailable:false` | 手动 `git pull` 更新 |
|