@lyhue1991/dsh-soup 0.2.0 → 0.4.0

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/lib/index.js CHANGED
@@ -533,6 +533,65 @@ export function apply(ctx) {
533
533
  'dsh-soup: streaming download route',
534
534
  )
535
535
 
536
+ /**
537
+ * 图片内联端点:GET /api/dsh-soup/img?p=<路径>。
538
+ * 浏览器端 Markdown 预览会把「相对路径图片」改写为这里的绝对 URL,
539
+ * 由 <img> 内联加载真实图片字节(JupyterLab md 图片同思路)。只读、无
540
+ * Content-Disposition(内联显示而非下载)。
541
+ * 安全模型:confine 围栏(只读工作区/会话目录)+ 仅图片类型 + 大小上限 +
542
+ * 不返回任何 CORS 头 + 拒绝跨站(Sec-Fetch-Site: cross-site 与不同源 Origin)。
543
+ * 注意 <img> 只能确认「是否 200/404」,无法读取像素,跨站利用面很小。
544
+ */
545
+ async function imageHandler(req, res) {
546
+ try {
547
+ const site = String(req.headers['sec-fetch-site'] || '')
548
+ if (site === 'cross-site') {
549
+ writeJson(res, 403, { ok: false, error: '跨站图片请求已拒绝' })
550
+ return
551
+ }
552
+ const origin = req.headers.origin
553
+ if (typeof origin === 'string' && origin !== '') {
554
+ let originHost = ''
555
+ try { originHost = new URL(origin).host } catch { originHost = '' }
556
+ const host = typeof req.headers.host === 'string' ? req.headers.host : ''
557
+ if (!originHost || originHost !== host) {
558
+ writeJson(res, 403, { ok: false, error: '跨源图片请求已拒绝' })
559
+ return
560
+ }
561
+ }
562
+ let p = ''
563
+ try { p = new URL(req.url || '/img?', 'http://local').searchParams.get('p') || '' } catch { p = '' }
564
+ if (!p) { writeJson(res, 400, { ok: false, error: '缺少图片路径' }); return }
565
+ const rp = await confine(p, '图片路径')
566
+ const mime = IMAGE_MIME[extOf(rp)]
567
+ if (!mime) { writeJson(res, 415, { ok: false, error: '不是可预览的图片类型' }); return }
568
+ let st
569
+ try { st = await stat(rp) } catch { writeJson(res, 404, { ok: false, error: '图片不可读' }); return }
570
+ if (st.isDirectory() || st.size > IMAGE_MAX_BYTES) {
571
+ writeJson(res, 413, { ok: false, error: '图片过大或目标为目录' }); return
572
+ }
573
+ const fh = await openFile(rp, 'r')
574
+ let buf
575
+ try {
576
+ buf = Buffer.alloc(st.size)
577
+ await fh.read(buf, 0, st.size, 0)
578
+ } finally { await fh.close() }
579
+ res.writeHead(200, {
580
+ 'content-type': mime,
581
+ 'content-length': String(st.size),
582
+ 'cache-control': 'no-store',
583
+ })
584
+ res.end(buf)
585
+ } catch (err) {
586
+ if (!res.headersSent) writeJson(res, (err && err.statusCode) || 500, { ok: false, error: String((err && err.message) || err) })
587
+ else res.end()
588
+ }
589
+ }
590
+ ctx.effect(
591
+ () => ctx.webServer.register({ kind: 'exact', path: '/api/dsh-soup/img', handler: imageHandler }),
592
+ 'dsh-soup: inline image route',
593
+ )
594
+
536
595
  /** 统一路由 dispatch:每个 action 返回可 JSON 序列化的结果。 */
537
596
  async function handleAction(body) {
538
597
  const action = body && body.action
@@ -657,6 +716,25 @@ export function apply(ctx) {
657
716
  downloadTickets.set(token, { path: rp, name: path.basename(rp), exp: Date.now() + 120000 })
658
717
  return { ok: true, name: path.basename(rp), size: dst.size, url: `/api/dsh-soup/dl?t=${token}` }
659
718
  }
719
+ case 'mtime': {
720
+ // 轻量变更探测:对一批路径返回 mtime(+size/目录标志),供浏览器端
721
+ // 低频轮询比较、变了才真正重拉——避免引入跨平台文件 watcher。
722
+ // 上限 64 个路径;越界/不可达的路径返回 null(客户端视为消失)。
723
+ const paths = Array.isArray(args && args.paths) ? args.paths.slice(0, 64) : []
724
+ const mtimes = {}
725
+ for (const raw of paths) {
726
+ const p = String(raw || '')
727
+ if (!p) continue
728
+ try {
729
+ const rp = await confine(p, '探测路径', sessionIdArg)
730
+ const st = await stat(rp)
731
+ mtimes[p] = st.isDirectory() ? { m: st.mtimeMs, d: 1 } : { m: st.mtimeMs, s: st.size }
732
+ } catch {
733
+ mtimes[p] = null
734
+ }
735
+ }
736
+ return { ok: true, mtimes }
737
+ }
660
738
  case 'speed-status': {
661
739
  const sid = args && args.sessionId ? String(args.sessionId) : ''
662
740
  return { ok: true, ...speedStatus(sid) }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lyhue1991/dsh-soup",
3
3
  "description": "DSH 项目资源管理器:右侧 details 列文件树(并列网格、宽度与左侧工作区一致 264-420px),有 Session log 时在其右侧、空白新会话在输入框上方右端提供 📁 切换按钮;支持多选、双击展开/打开、右键菜单(打开/系统打开/重命名/废纸篓/新建/复制路径)、拖拽移动、上传。文件「打开」在会话区新增「文件」标签页(原生 conversation.view,与对话/轨迹同级):文本可编辑 ⌘S 保存、图片预览、二进制提示。附带模型吞吐徽标:输入框正上方显示「正在等待模型...」与彩色 t/s 徽标(数据来自 llm/stream 真实流)。附带 GoalBar 多行展示:把原生单行截断 GoalBar 放开为多行完整显示,编辑用 textarea,工具与数据仍走原生 goal。",
4
- "version": "0.2.0",
4
+ "version": "0.4.0",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {