@orderg-x/dsh-video-preview 0.4.0 → 0.4.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 CHANGED
@@ -133,3 +133,7 @@ node test/test-auto.mjs # 自动播放:新 / 老文件、正文优先、
133
133
  DeepSeek Harness 0.2.1-rc.2 桌面端端到端实测:回复下方播放器(mp4 / HEVC 的 mov / m4a,中文文件名)、侧边栏预览和播放、avi 的转码提示、文件被移走后的出错提示;伪造登录 cookie 的请求被拒;模型用 say / avconvert 生成的 m4a、mp4(含带空格的中文文件名)没调 `play_video`,回合结束时播放器实时出现且能播;视频卡悬停预览 / 点击播放 / 下载、音频条播放、超过 3 个时折叠展开、深浅两种主题下的配色。
134
134
 
135
135
  参考实现:[@goodandready/dsh-image-gen](https://www.npmjs.com/package/@goodandready/dsh-image-gen)(客户端插件套路)与官方 `dsh-tool-present`(deliverables 通道)。
136
+
137
+ ## 反馈
138
+
139
+ 遇到问题或想要新功能,[提一个 issue](https://github.com/OrderG-X/dsh-video-preview/issues/new/choose)。报问题时写上 DSH 版本和插件版本,修起来最快。
@@ -89,6 +89,13 @@ export function sessionCwd(ctx, session) {
89
89
  return typeof cwd === 'string' && path.isAbsolute(cwd) ? cwd : ''
90
90
  }
91
91
 
92
+ /** Windows 上 DSH 用 Git Bash 跑命令,模型给的路径常是 /c/Users/… 这种写法:换成 C:\\Users\\…;别的系统原样 */
93
+ export function fromBashPath(p, platform = process.platform) {
94
+ if (platform !== 'win32') return p
95
+ const m = /^\/([a-zA-Z])(?:\/(.*))?$/.exec(p)
96
+ return m ? `${m[1].toUpperCase()}:\\${(m[2] || '').replace(/\//g, '\\')}` : p
97
+ }
98
+
92
99
  /**
93
100
  * 候选片段 → 每种写法一组绝对路径(相对路径按工作目录挨个试,越靠后的目录越新)。
94
101
  * 写法按顺序:字面的先试(文件名本身就可能带 %20),再试 URL 解码、shell 转义去掉的。
@@ -103,9 +110,12 @@ function resolveAll(raw, cwds) {
103
110
  } catch { /* 不是编码过的,只用字面的 */ }
104
111
  }
105
112
  for (const v of [...variants]) {
106
- if (v.includes('\\')) variants.push(v.replace(/\\(.)/g, '$1')) // shell 里转义的空格:My\ Movies
113
+ if (!v.includes('\\')) continue
114
+ // shell 里转义的空格:My\ Movies。Windows 上反斜杠是路径分隔符,只去掉空格前面那个
115
+ variants.push(process.platform === 'win32' ? v.replace(/\\ /g, ' ') : v.replace(/\\(.)/g, '$1'))
107
116
  }
108
117
  return variants.map((v) => {
118
+ v = fromBashPath(v)
109
119
  if (v === '~' || v.startsWith('~/')) v = path.join(os.homedir(), v.slice(1))
110
120
  return path.isAbsolute(v) ? [path.normalize(v)] : cwds.map((c) => path.resolve(c, v))
111
121
  })
package/lib/client.js CHANGED
@@ -503,7 +503,7 @@ window.__ModuleLoader__.load({
503
503
  items.push({
504
504
  url: '/dsh-video-preview/preview?p=' + encodeURIComponent(p),
505
505
  kind: isAudio ? 'audio' : 'video',
506
- name: (typeof f.title === 'string' && f.title) || p.split('/').pop() || p,
506
+ name: (typeof f.title === 'string' && f.title) || p.split(/[\\/]/).pop() || p,
507
507
  autoplay: !!f.autoplay,
508
508
  loop: !!f.loop,
509
509
  })
@@ -573,7 +573,7 @@ window.__ModuleLoader__.load({
573
573
  return h('p', { style: { padding: 16, color: '#888', fontSize: 13 } }, t(key, ZH[key]))
574
574
  }
575
575
  const audio = AUDIO_RE.test(p)
576
- const media = { url: src, kind: audio ? 'audio' : 'video', name: p.split('/').pop() || p }
576
+ const media = { url: src, kind: audio ? 'audio' : 'video', name: p.split(/[\\/]/).pop() || p }
577
577
  if (reason) {
578
578
  return h('div', { style: { padding: 16 } }, h(ErrorCard, { reason, media, t }))
579
579
  }
package/lib/index.js CHANGED
@@ -22,10 +22,11 @@
22
22
  */
23
23
 
24
24
  import fs from 'node:fs'
25
+ import os from 'node:os'
25
26
  import path from 'node:path'
26
27
  import crypto from 'node:crypto'
27
28
  import { pipeline } from 'node:stream'
28
- import { autoPresent, sessionCwd } from './auto-present.js'
29
+ import { autoPresent, sessionCwd, fromBashPath } from './auto-present.js'
29
30
 
30
31
  export const name = '@orderg-x/dsh-video-preview'
31
32
  export const inject = ['tools', 'webServer', 'sessionProjections']
@@ -61,7 +62,7 @@ const LOOPBACK_HOST = /^(127\.0\.0\.1|localhost|\[::1\])(:\d+)?$/i
61
62
  * 创建的会话)token 就对不上,媒体请求全部 403。
62
63
  */
63
64
  function loadSecret() {
64
- const file = path.join(process.env.HOME || '.', '.dsh', 'cache', 'dsh-video-preview.secret')
65
+ const file = path.join(os.homedir(), '.dsh', 'cache', 'dsh-video-preview.secret')
65
66
  try {
66
67
  const s = fs.readFileSync(file, 'utf8').trim()
67
68
  if (s) return s
@@ -110,6 +111,9 @@ function kindOf(file) {
110
111
  }
111
112
 
112
113
  /** shell 单引号:文件名里的 $(...)、反引号、双引号都原样当字面,单引号写成 '\'' */
114
+ /** 各系统装 ffmpeg 的一句话 */
115
+ const FFMPEG_INSTALL = { darwin: 'brew install ffmpeg', win32: 'winget install ffmpeg', linux: 'sudo apt install ffmpeg(或用系统自带的包管理器)' }
116
+
113
117
  function shq(s) {
114
118
  return "'" + String(s).replace(/'/g, "'\\''") + "'"
115
119
  }
@@ -127,7 +131,7 @@ function convertHint(file) {
127
131
 
128
132
  /** 工具参数里的路径 → 绝对路径:~ 换成家目录,相对路径按会话的工作目录算;没有工作目录就返回空串 */
129
133
  function resolvePath(raw, cwd) {
130
- const p = String(raw || '').replace(/^~(?=\/|$)/, process.env.HOME || '~')
134
+ const p = fromBashPath(String(raw || '')).replace(/^~(?=[\\/]|$)/, os.homedir())
131
135
  if (path.isAbsolute(p)) return path.resolve(p)
132
136
  return cwd ? path.resolve(cwd, p) : ''
133
137
  }
@@ -401,7 +405,7 @@ export function apply(ctx) {
401
405
  const { out, cmd } = convertHint(file)
402
406
  throw new Error(
403
407
  `DSH 的播放器(Chromium 内核)放不了 ${ext} 格式。先转成 ${path.extname(out)} 再播:\n${cmd}\n` +
404
- `没装 ffmpeg 的话:brew install ffmpeg。转好后用 play_video 播 ${out}`)
408
+ `没装 ffmpeg 的话:${FFMPEG_INSTALL[process.platform] || FFMPEG_INSTALL.linux}。转好后用 play_video 播 ${out}`)
405
409
  }
406
410
  const kind = kindOf(file)
407
411
  if (!kind) {
package/package.json CHANGED
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "name": "@orderg-x/dsh-video-preview",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "type": "module",
5
+ "scripts": {
6
+ "test": "node --test --test-concurrency=1 test/*.mjs"
7
+ },
5
8
  "description": "Inline video and audio playback for DeepSeek Harness: a play_video tool plus a native player under the reply and in the sidebar, served by a guarded, Range-capable streaming route.",
6
9
  "license": "MIT",
7
10
  "repository": {