dsh-remote-plugin 0.6.6 → 0.6.7
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/apk/dsh-remote.apk +0 -0
- package/gateway.cjs +119 -1
- package/index.mjs +10 -1
- package/package.json +1 -1
- package/public/app.js +419 -39
- package/public/desktop/desktop.css +45 -2
- package/public/desktop/desktop.html +40 -0
- package/public/desktop/desktop.js +283 -17
- package/public/desktop/i18n.js +33 -1
- package/public/index.html +58 -2
- package/public/styles.css +44 -0
- package/public/update.json +8 -4
- package/public/version.json +1 -1
package/apk/dsh-remote.apk
CHANGED
|
Binary file
|
package/gateway.cjs
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* TOKEN_FILE 令牌文件, 默认 ~/.dsh-remote/token
|
|
21
21
|
* DSH_REMOTE_FS_ROOT 文件传输允许根, 默认 ~, 使用系统路径分隔符配置多根
|
|
22
22
|
* DSH_REMOTE_FS_MAX_UPLOAD 上传字节上限, 默认 2147483648 (2GB)
|
|
23
|
+
* DSH_REMOTE_WORKBENCH 工作台绑定文件, 默认 ~/.dsh-remote/workbench.json
|
|
23
24
|
*/
|
|
24
25
|
'use strict'
|
|
25
26
|
|
|
@@ -48,6 +49,7 @@ const WS_IDLE_MS = Number(process.env.GATEWAY_WS_IDLE_MS) || 60000
|
|
|
48
49
|
const UPSTREAM = new URL(process.env.DSH_UPSTREAM || 'http://127.0.0.1:3080')
|
|
49
50
|
const TOKEN_FILE = process.env.TOKEN_FILE || path.join(os.homedir(), '.dsh-remote', 'token')
|
|
50
51
|
const NOTES_FILE = process.env.DSH_REMOTE_NOTES || path.join(os.homedir(), '.dsh-remote', 'device-notes.json')
|
|
52
|
+
const WORKBENCH_FILE = process.env.DSH_REMOTE_WORKBENCH || path.join(os.homedir(), '.dsh-remote', 'workbench.json')
|
|
51
53
|
const STARTED_AT = Date.now()
|
|
52
54
|
const DSH_SERVICE = String(process.env.DSH_REMOTE_DSH_SERVICE || 'dsh-web').trim()
|
|
53
55
|
|
|
@@ -914,11 +916,20 @@ function serveStatic(req, res, url) {
|
|
|
914
916
|
return
|
|
915
917
|
}
|
|
916
918
|
const ext = path.extname(filePath).toLowerCase()
|
|
919
|
+
const lastModified = st.mtime.toUTCString()
|
|
920
|
+
const mtimeSec = Math.floor(st.mtime.getTime() / 1000) * 1000
|
|
917
921
|
cors(res)
|
|
922
|
+
const ims = req.headers['if-modified-since']
|
|
923
|
+
if (ims && new Date(ims).getTime() >= mtimeSec) {
|
|
924
|
+
res.writeHead(304, { 'last-modified': lastModified })
|
|
925
|
+
res.end()
|
|
926
|
+
return
|
|
927
|
+
}
|
|
918
928
|
res.writeHead(200, {
|
|
919
929
|
'content-type': MIME[ext] || 'application/octet-stream',
|
|
920
930
|
'cache-control': ext === '.html' || ext === '.js' || ext === '.css' ? 'no-cache' : 'public, max-age=300',
|
|
921
|
-
'content-length': st.size
|
|
931
|
+
'content-length': st.size,
|
|
932
|
+
'last-modified': lastModified
|
|
922
933
|
})
|
|
923
934
|
if (req.method === 'HEAD') res.end()
|
|
924
935
|
else fs.createReadStream(filePath).pipe(res)
|
|
@@ -1753,6 +1764,112 @@ function serveFs(req, res, url) {
|
|
|
1753
1764
|
fsJson(res, 404, { error: 'not-found' })
|
|
1754
1765
|
}
|
|
1755
1766
|
|
|
1767
|
+
// ---------- /workbench 工作台绑定 ----------
|
|
1768
|
+
// 工作台绑定一个文件夹;其下的子文件夹由客户端映射为 DSH 项目工作区。
|
|
1769
|
+
function workbenchPathInfo(rawPath) {
|
|
1770
|
+
if (typeof rawPath !== 'string' || !path.isAbsolute(rawPath)) return { error: 'bad-path' }
|
|
1771
|
+
const abs = path.resolve(rawPath)
|
|
1772
|
+
let st
|
|
1773
|
+
try { st = fs.statSync(abs) } catch (err) {
|
|
1774
|
+
return { error: err.code === 'ENOENT' ? 'not-found' : 'permission-denied' }
|
|
1775
|
+
}
|
|
1776
|
+
if (!st.isDirectory()) return { error: 'not-a-directory' }
|
|
1777
|
+
const checked = fsRealChecked(abs)
|
|
1778
|
+
if (checked.error) return { error: checked.error === 'forbidden' ? 'outside-roots' : checked.error }
|
|
1779
|
+
return { path: checked.abs }
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
function loadWorkbench() {
|
|
1783
|
+
try {
|
|
1784
|
+
const raw = JSON.parse(fs.readFileSync(WORKBENCH_FILE, 'utf8'))
|
|
1785
|
+
if (!raw || typeof raw.path !== 'string' || !raw.path) return null
|
|
1786
|
+
const checked = workbenchPathInfo(raw.path)
|
|
1787
|
+
return checked.path ? { path: checked.path } : null
|
|
1788
|
+
} catch {
|
|
1789
|
+
return null
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
function saveWorkbench(binding) {
|
|
1794
|
+
try {
|
|
1795
|
+
fs.mkdirSync(path.dirname(WORKBENCH_FILE), { recursive: true })
|
|
1796
|
+
fs.writeFileSync(WORKBENCH_FILE, JSON.stringify(binding, null, 2) + '\n')
|
|
1797
|
+
return true
|
|
1798
|
+
} catch {
|
|
1799
|
+
return false
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
function serveWorkbench(req, res, url) {
|
|
1804
|
+
const sub = url.pathname.slice('/workbench'.length)
|
|
1805
|
+
if (req.method === 'OPTIONS') {
|
|
1806
|
+
cors(res)
|
|
1807
|
+
res.writeHead(204)
|
|
1808
|
+
res.end()
|
|
1809
|
+
return
|
|
1810
|
+
}
|
|
1811
|
+
if (!fsAuthorized(req, url, res)) return
|
|
1812
|
+
|
|
1813
|
+
if (sub === '' && req.method === 'GET') {
|
|
1814
|
+
const binding = loadWorkbench()
|
|
1815
|
+
fsJson(res, 200, {
|
|
1816
|
+
bound: !!binding,
|
|
1817
|
+
path: binding?.path || null,
|
|
1818
|
+
title: binding ? path.basename(binding.path) : null
|
|
1819
|
+
})
|
|
1820
|
+
return
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
if (sub === '/bind' && req.method === 'POST') {
|
|
1824
|
+
let body = ''
|
|
1825
|
+
let done = false
|
|
1826
|
+
const fail = (status, payload) => {
|
|
1827
|
+
if (done || res.headersSent) return
|
|
1828
|
+
done = true
|
|
1829
|
+
fsJson(res, status, payload)
|
|
1830
|
+
}
|
|
1831
|
+
req.on('data', chunk => {
|
|
1832
|
+
if (done) return
|
|
1833
|
+
body += chunk
|
|
1834
|
+
if (Buffer.byteLength(body) > 4096) {
|
|
1835
|
+
req.destroy()
|
|
1836
|
+
fail(413, { error: 'too-large' })
|
|
1837
|
+
}
|
|
1838
|
+
})
|
|
1839
|
+
req.on('error', () => { if (!done) done = true })
|
|
1840
|
+
req.on('end', () => {
|
|
1841
|
+
if (done) return
|
|
1842
|
+
try {
|
|
1843
|
+
const rawPath = JSON.parse(body || '{}')?.path
|
|
1844
|
+
const checked = workbenchPathInfo(rawPath)
|
|
1845
|
+
if (checked.error) {
|
|
1846
|
+
const status = checked.error === 'forbidden' ? 403 : 400
|
|
1847
|
+
fail(status, { error: checked.error, detail: checked.error === 'outside-roots' ? '绑定目录必须在文件传输允许根目录内' : undefined })
|
|
1848
|
+
return
|
|
1849
|
+
}
|
|
1850
|
+
if (!saveWorkbench({ path: checked.path })) {
|
|
1851
|
+
fail(500, { error: 'save-failed' })
|
|
1852
|
+
return
|
|
1853
|
+
}
|
|
1854
|
+
done = true
|
|
1855
|
+
fsJson(res, 200, { bound: true, path: checked.path, title: path.basename(checked.path) })
|
|
1856
|
+
} catch {
|
|
1857
|
+
fail(400, { error: 'bad-request' })
|
|
1858
|
+
}
|
|
1859
|
+
})
|
|
1860
|
+
return
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
if (sub === '/unbind' && req.method === 'POST') {
|
|
1864
|
+
try { fs.rmSync(WORKBENCH_FILE, { force: true }) } catch {}
|
|
1865
|
+
fsJson(res, 200, { bound: false })
|
|
1866
|
+
return
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
res.writeHead(405, { allow: 'GET, POST' })
|
|
1870
|
+
res.end()
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1756
1873
|
// ---------- /api 代理 ----------
|
|
1757
1874
|
function proxyApi(req, res, url) {
|
|
1758
1875
|
if (req.method === 'OPTIONS') {
|
|
@@ -1851,6 +1968,7 @@ const server = http.createServer((req, res) => {
|
|
|
1851
1968
|
try {
|
|
1852
1969
|
const url = new URL(req.url, 'http://dsh-remote.local')
|
|
1853
1970
|
if (url.pathname === '/fs' || url.pathname.startsWith('/fs/')) return serveFs(req, res, url)
|
|
1971
|
+
if (url.pathname === '/workbench' || url.pathname.startsWith('/workbench/')) return serveWorkbench(req, res, url)
|
|
1854
1972
|
if (url.pathname === '/feedback') return serveFeedback(req, res, url)
|
|
1855
1973
|
if (url.pathname.startsWith('/admin/api')) return serveAdminApi(req, res, url)
|
|
1856
1974
|
if (url.pathname.startsWith('/stats')) return serveStats(req, res, url)
|
package/index.mjs
CHANGED
|
@@ -150,7 +150,7 @@ function runExit(cmd, args) {
|
|
|
150
150
|
|
|
151
151
|
const GATEWAY_ENV_KEYS = [
|
|
152
152
|
'TOKEN', 'TOKEN_FILE', 'DSH_REMOTE_TOKEN', 'DSH_REMOTE_FS_ROOT', 'DSH_REMOTE_FS_MAX_UPLOAD',
|
|
153
|
-
'DSH_REMOTE_NOTES', 'DSH_REMOTE_DSH_SERVICE', 'DSH_REMOTE_FEEDBACK_URL',
|
|
153
|
+
'DSH_REMOTE_NOTES', 'DSH_REMOTE_WORKBENCH', 'DSH_REMOTE_DSH_SERVICE', 'DSH_REMOTE_FEEDBACK_URL',
|
|
154
154
|
'UPDATE_CHECK_URL', 'UPDATE_INTERVAL_MS', 'UPDATE_PROXY', 'GATEWAY_WS_IDLE_MS',
|
|
155
155
|
'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'NO_PROXY',
|
|
156
156
|
'http_proxy', 'https_proxy', 'all_proxy', 'no_proxy'
|
|
@@ -704,10 +704,19 @@ async function serveStatic(req, res, ctx) {
|
|
|
704
704
|
return
|
|
705
705
|
}
|
|
706
706
|
const { abs, info } = found
|
|
707
|
+
const lastModified = info.mtime.toUTCString()
|
|
708
|
+
const mtimeSec = Math.floor(info.mtime.getTime() / 1000) * 1000
|
|
709
|
+
const ims = req.headers['if-modified-since']
|
|
710
|
+
if (ims && new Date(ims).getTime() >= mtimeSec) {
|
|
711
|
+
res.writeHead(304, { 'last-modified': lastModified })
|
|
712
|
+
res.end()
|
|
713
|
+
return
|
|
714
|
+
}
|
|
707
715
|
res.writeHead(200, {
|
|
708
716
|
'content-type': MIME[extname(abs)] ?? 'application/octet-stream',
|
|
709
717
|
'content-length': info.size,
|
|
710
718
|
'cache-control': 'no-cache',
|
|
719
|
+
'last-modified': lastModified,
|
|
711
720
|
})
|
|
712
721
|
if (req.method === 'HEAD') {
|
|
713
722
|
res.end()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-remote-plugin",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.7",
|
|
4
4
|
"description": "DSH Remote 官方 bundle 插件:DSH 左侧原生边栏入口 + 右侧抽屉内嵌管理控制台;内置网关随 DSH 自动启停(systemd 独立单元),抽屉直显令牌与设备监控;网关提供 /fs/* 文件传输端点(列表/断点下载/上传),配合 Android App 远程操控会话/审批/提问/goal 与文件互传(多服务器测速切换、聊天记录离线缓存)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|