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.
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.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",