dsh-readvideo 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Li Yichen (李依宸)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,261 @@
1
- # Temporary Holding Version
1
+ # dsh-readvideo
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ > npm 包名是 `dsh-readvideo`,仓库名是 `dsh-read-video`。在 dsh 的插件页输入包名即可安装,
4
+ > 见「安装 · 方式一」。
5
+
6
+ DeepSeek Harness 插件:让模型"看"视频,但不把视频下载下来。
7
+
8
+ 给一条视频直链,插件用 HTTP Range 只取目标时间附近的一小段字节 —— 取某一秒的帧,
9
+ 或取某一段音频送去转写。整片 99% 的字节从头到尾不碰,磁盘上不会出现任何视频文件。
10
+
11
+ > **状态:骨架。** 只有 `video_probe`(URL 可跳读性探测)落地成了工具;取帧 /
12
+ > 取音频窗口 / 转写这三件事在 `spike/` 里验证通过,代码还没搬进插件。
13
+
14
+ ---
15
+
16
+ ## 为什么不是"下载后分析"
17
+
18
+ 下载整片再抽帧,慢、占盘、碰版权,而且大部分字节从来没被用到。浏览器拖进度条时
19
+ 并不下载整片,它发的是 `Range: bytes=...`。插件做的是同一件事。
20
+
21
+ 视频容器里本来就带索引:
22
+
23
+ | 容器 | 索引在哪 | 有什么用 |
24
+ | --- | --- | --- |
25
+ | MP4 | `moov` 里的 sample table(`stts` / `stsc` / `stco` / `stsz`) | 第 t 秒的样本在第几个字节、有多长 |
26
+ | HLS | `m3u8` 分片清单 | 每个分片的时间范围与 URL 都列好了 |
27
+
28
+ 拿到索引,一次 Range 请求就能只取目标区间。唯一的物理限制是帧间编码:要第 1800 秒
29
+ 那一帧,得从它前面最近的 I 帧(一般 1~10 秒一个 GOP)开始解。所以"取一帧"实际取的是
30
+ "最近关键帧到目标帧"那一小段,不是整个文件。
31
+
32
+ ---
33
+
34
+ ## 实测数据
35
+
36
+ `spike/remote_seek_spike.py` 跑出来的。目标 `media.w3.org/2010/05/bunny/movie.mp4`:
37
+ `Content-Length: 249224577`(237.7 MB),时长 `00:09:56.48`。
38
+
39
+ 计数方式是本地起一个**流式转发代理**,ffmpeg 指向它,代理把请求原样转给真实 CDN 并
40
+ 精确记录真正转发出去的字节数 —— 数字是量出来的,不是估的。
41
+
42
+ | 操作 | 从服务器实拉 | 占整片 | 请求数 | 耗时 | 产出 |
43
+ | --- | --- | --- | --- | --- | --- |
44
+ | 取第 30 秒一帧 | 2.0 MB | 0.839% | 4 | 10.5 s | 112.8 KB JPEG |
45
+ | 取第 8 分钟一帧 | 1.7 MB | 0.707% | 4 | 10.5 s | 52.4 KB JPEG |
46
+ | 第 8 分钟起 20 秒音频 | 6.6 MB | 2.784% | 4 | 13.1 s | 625.1 KB WAV |
47
+
48
+ 第 8 分钟对应文件里 185 MB 偏移,前面 176 MB 一个字节都没碰。
49
+
50
+ 四条比数字本身更值得留意的:
51
+
52
+ 1. **每次全新调用有约 1.3 MB 固定开销。** 这条片子是非 faststart:`mdat` 从第 40 字节
53
+ 铺到 237.2 MB,`moov` 索引在 237.2 MB 处也就是文件尾。所以每次都要先付
54
+ "探测 + 取索引"这笔钱(192 KB + 441 KB + 704 KB)。同会话里多次取帧要复用,
55
+ 不能反复付。
56
+ 2. **目标区域是开放式 Range(`bytes=X-`),靠 ffmpeg 拿到帧后主动断开才不收全量。**
57
+ 任何中间件都必须流式转发;先缓冲再回传会把"取一帧"放大成"拉完整个 range"。
58
+ 3. **音频窗口比取帧贵一个量级**(6.6 MB 对 0.4 MB 目标区域):muxed MP4 里音视频
59
+ 交织存放,取 20 秒音频必须扫过同样时长的视频数据。所以音频窗口要尽量短,
60
+ "字幕轨优先"的台阶排序是对的。
61
+ 4. **服务器不返回 `Accept-Ranges: bytes` 时跳读失效**,只能顺序读。这个用一次 HEAD
62
+ 就能提前测出来 —— `video_probe` 干的就是这件事。
63
+
64
+ ---
65
+
66
+ ## 成本台阶
67
+
68
+ 能停在便宜的台阶上就别往上走:
69
+
70
+ | 台阶 | 手段 | 代价 |
71
+ | --- | --- | --- |
72
+ | 0 | 页面元数据:标题 / 时长 / 简介 / 章节 / 封面 `og:image` | 一次 HTML 抓取 |
73
+ | 1 | 字幕轨(`<track>` 的 `.vtt` / `.srt`) | 几 KB 拿到全文,完全不用 ASR |
74
+ | 2 | 单帧取证:给定 `t` → 最近关键帧起解一帧 | 几十~几百 KB |
75
+ | 3 | 音频窗口:给定 `[t1,t2]` → 只取该段音频 → WAV → 转写 | 20 秒约 6.6 MB |
76
+ | 4 | 粗采样扫描:每 30 秒 1 帧,拼成雪碧图一次送进模型 | 只在要"整体理解"时用 |
77
+ | 5 | 全量流式转写 | 只在明确要求、且接受耗时的时候 |
78
+
79
+ 台阶 1 容易被忽略:**不少视频自带字幕轨,那"理解视频"这件事根本不用语音识别**,
80
+ 取个几 KB 的文本文件就完了 —— 比 ASR 便宜三个数量级。
81
+
82
+ ---
83
+
84
+ ## 安装
85
+
86
+ ### 方式一:从 npm 装(推荐)
87
+
88
+ 桌面应用 → 插件页 → 添加插件 → 填 `dsh-readvideo` → 重启。
89
+
90
+ 管理器做的事:`pnpm add dsh-readvideo` → 把它加进 profile 的 `dsh.profile.bundles` →
91
+ 包自带的 `cordis.patch.yml` 作为 **bundle 层自动生效**。用户不用手写任何配置,装完的形态:
92
+
93
+ ```
94
+ ~/.dsh/profiles/desktop/
95
+ ├── package.json # dependencies 与 dsh.profile.bundles 各多一行
96
+ ├── cordis.patch.yml # 不用动
97
+ └── node_modules/dsh-readvideo/
98
+ ├── cordis.patch.yml # 挂载条目来自这里
99
+ └── dist/index.js # 入口
100
+ ```
101
+
102
+ **这个形态下插件不需要自带 `node_modules`** —— `@deepseek-ai/*` 那类 peer 由宿主自己解析
103
+ (`dsh-mnemon` 同样只声明 peer,装成包照跑)。已实测:把本包的 tarball 用桌面自带的 pnpm
104
+ 装进一个干净 profile,启动日志零告警,`--dump-config` 里 `read-video` 恰好出现一次。
105
+
106
+ ### 本地挂载才需要插件自带依赖
107
+
108
+ ```bash
109
+ git clone https://github.com/RaganaBloodedge/dsh-read-video.git
110
+ cd dsh-read-video
111
+ npm install
112
+ ```
113
+
114
+ **用绝对路径挂载**(`--patch`,或把条目写进 profile 的 `cordis.patch.yml`)时,入口文件在
115
+ `node_modules` 之外,Node 找不到 `@deepseek-ai/*`,所以插件必须自带 `node_modules`。
116
+ 漏了这步,启动日志只给一句不含原因的 `read-video (file:///...): failed to import` ——
117
+ 真原因要单独 import 才看得到:`ERR_MODULE_NOT_FOUND: Cannot find package '@deepseek-ai/dsh-tools'`。
118
+
119
+ `npm install` 会顺带跑 `prepare`(即 `tsc`),产出 `dist/`。
120
+
121
+ ### 入口为什么分 `src` 与 `dist`
122
+
123
+ dsh 当年加载插件入口时走的是 Node 的类型剥离,而 **Node 拒绝对 `node_modules` 下的
124
+ `.ts` 文件剥类型**:
125
+
126
+ ```
127
+ ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING
128
+ Stripping types is currently unsupported for files under node_modules
129
+ ```
130
+
131
+ 于是两条路分开:
132
+
133
+ | 加载方式 | 入口 | 原因 |
134
+ | --- | --- | --- |
135
+ | 本地开发 `--patch` | `src/index.ts` | 文件在 `node_modules` 之外,Node 能直接剥类型,不用先编译 |
136
+ | 装进 `node_modules`(包名 / GitHub 仓库 / 本地目录) | `dist/index.js` | 只能加载 `.js`,所以包入口是 `dist/index.js` |
137
+
138
+ `dist/` 进了仓库,所以从 GitHub 装下来不用自己编译也能跑;`prepare` 脚本则在
139
+ `npm install` 和 git 依赖场景下重新生成一份。两条都留着是为了少一个失败点。
140
+
141
+ ### 方式二:绝对路径挂载进 profile(含桌面应用)
142
+
143
+ 把 `cordis.example.yml` 里的 `- insert:` 块粘进
144
+ `~/.dsh/profiles/desktop/cordis.patch.yml`,把 `<REPO>` 换成本仓库的绝对路径,
145
+ 然后重启应用。
146
+
147
+ desktop profile 被 Electron 独占管理,`dsh --profile desktop --dump-config` 会被直接
148
+ 拒绝("managed exclusively by the Electron application"),CLI 既不能 dump 也不能启动它。
149
+ 另外 desktop profile 目前没有 `node_modules`,如果挂载时报 `failed to import`,
150
+ 得先让 profile 能解析到本插件的依赖。
151
+
152
+ ### 方式三:本地开发(CLI,改代码即生效)
153
+
154
+ ```bash
155
+ cp cordis.example.yml cordis.local.yml # 先把里面的 <REPO> 换成绝对路径
156
+ dsh --profile web --patch "<cordis.local.yml 的绝对路径>" --dump-config # 只组合配置,最快自检
157
+ dsh --profile web --patch "<cordis.local.yml 的绝对路径>" # 真启动
158
+ ```
159
+
160
+ `--dump-config` 输出里出现 `- id: read-video` 就是组合成功。
161
+
162
+ ### 方式四:MCP server
163
+
164
+ **尚未实现。** `cordis.example.yml` 里的 `mcp-readvideo` 条目是占位,留着是为了记住
165
+ 这条路要走 `@deepseek-ai/dsh-mcp-client`、且 `serverName` 得匹配 `[A-Za-z0-9_-]{1,32}`。
166
+
167
+ ---
168
+
169
+ ## 已知坑
170
+
171
+ 都是实测踩出来的。违反了不报错,只是不生效:
172
+
173
+ | 症状 | 原因 | 处置 |
174
+ | --- | --- | --- |
175
+ | `1 entry did not activate` + `failed to import` | 插件目录没有 `node_modules` | `npm install` |
176
+ | `schema.required is not supported by the value schema DSL` | `output.schema` 的 object 层写了 `required: [...]` 数组 | 删掉,改成标在属性上 `required: true` |
177
+ | `patch: entry "x" not found` | 用了裸 patch 条目 | 包进 `- insert:` |
178
+ | 完全没日志、静默不生效 | `name` 写了相对路径 | 改成绝对路径(路径指向不存在的文件时,dsh 会明确报 `did not activate`,可据此判定) |
179
+ | `'pnpm' 不是内部或外部命令` | 没装 pnpm(桌面应用自带一份,CLI 走 PATH 找不到) | 先用 `--patch` |
180
+ | 装好后 import 报 `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING` | 包入口指向 `.ts`,而 Node 不剥 `node_modules` 下文件的类型 | 入口用 `dist/index.js`(见上面那节) |
181
+ | 按钮填 GitHub 地址后报 `Host key verification failed` / `Connection reset ... port 22` | pnpm 把 GitHub spec 一律解析成 `git+ssh://git@github.com/...`,而机器上没有 SSH key | 改用包名安装(见「发布到 npm」),别用仓库地址 |
182
+
183
+ `required` 那条值得单独说:它不是数组,而是标在**每个属性自己的 schema 上**
184
+ (`required: true`)。`parameters` 和 `output.schema` 用的是同一套 DSL。
185
+
186
+ ---
187
+
188
+ ## 「添加插件」按钮
189
+
190
+ 桌面应用里那个「输入插件的包名、GitHub 仓库地址或本地目录路径」的按钮走的是
191
+ `@deepseek-ai/dsh-plugin-manager`。三种来源最后都落到 `pnpm add <spec>`,装完把包加进
192
+ profile 的 `dsh.profile.bundles`;包里的 `dsh.bundle.patch` 随 bundle 层自动生效,所以
193
+ **不需要写 profile 的 `cordis.patch.yml`**(实测装完 `dsh-mnemon` 后用户的 patch 文件
194
+ 一个字节没变)。本地目录那一栏必须是绝对路径 —— 相对路径会被拒。
195
+
196
+ 三种来源的实测结论:
197
+
198
+ | 来源 | 能不能装上 | 说明 |
199
+ | --- | --- | --- |
200
+ | **包名**(如 `dsh-mnemon`) | 能 | 走 `pnpm add <名>`,装完 `node_modules/<包名>` 正常生成。本仓库发到 npm 后即可用 `dsh-readvideo` |
201
+ | **GitHub 仓库地址** | 不能(默认机器) | `github:user/repo`、`https://github.com/...`、`git+https://...git` 三种写法都被 pnpm 改写成 `git+ssh://git@github.com/...`,没配 SSH key 直接 `Host key verification failed` / `Connection reset ... port 22`。本机 `git config --global` 无 `insteadOf` 规则,不是本地配置造成的 |
202
+ | **本地目录(绝对路径)** | 能,但该目录要先 `npm install` | 走 `pnpm add <路径>`,插件用自己的 `node_modules` 解析 `@deepseek-ai/*` |
203
+
204
+ **早前这一节写过两条错的,一并更正**:桌面自带的 pnpm 11.7.0 **会**正常建链接
205
+ (`pnpm add zod` → `node_modules/zod` 存在且 `require.resolve` 通过,实测复现不了"不建链接");
206
+ `pnpmCommand` 也**救不了** GitHub 那条路 —— 官方文档写明应用拥有的 profile「由启动器提供
207
+ 内置包管理器调用方式,**它优先于 `pnpmCommand`**」,且「Desktop 包管理操作仍由 Desktop shell 负责」。
208
+
209
+ 一句话:**按钮只对「包名」和「本地绝对路径」这两种来源可用**。想让别人输一个名字就装上,
210
+ 就得发到 npm。
211
+
212
+ ---
213
+
214
+ ## 发布到 npm
215
+
216
+ 包名就是安装名 —— 用户在插件页输入的字符串就是 npm 包名,所以让它能被 `dsh-readvideo`
217
+ 装出来,要满足四件事:
218
+
219
+ 1. `package.json` 里 `dsh.bundle.patch` 指向 `cordis.patch.yml`,其中的 `- insert:` 行用
220
+ **包名**引用自己(不是绝对路径);
221
+ 2. `files` 必须含编译产物 `dist/` —— 安装 npm 包时**不会**跑 `prepare`,编译产物要自带;
222
+ 3. `publishConfig.access = "public"`(否则不会被当成公开包);
223
+ 4. **peer 区间要能覆盖 dsh 的 rc 版本**:写成 `^0.1.7-rc.2 || ^0.2.0-rc.1` 这种形式。
224
+ 装之前管理器会做一次 peer 兼容性检查,写死成 `0.2.0-rc.2` 的话,dsh 一升级别人就装不上了
225
+ —— 这是"别人装不上"最常见的原因。
226
+
227
+ 发布:
228
+
229
+ ```bash
230
+ npm login --registry https://registry.npmjs.org # 或 export NODE_AUTH_TOKEN=<token>
231
+ npm publish --registry https://registry.npmjs.org
232
+ npm view dsh-readvideo version # 回读核对;npmmirror 随后自动同步
233
+ ```
234
+
235
+ 参照 `dsh-mnemon`(同架构、70 个版本):它是 monorepo,用 changesets 管版本,CI 里
236
+ `NODE_AUTH_TOKEN` 逐层发布子包、**最后**发主包,并且每步都从 registry 装回来做 E2E 自检。
237
+ 本项目是单包,上面四步就够。
238
+
239
+ ---
240
+
241
+ ## 目录
242
+
243
+ ```
244
+ src/index.ts 插件入口,具名导出 name / inject / apply
245
+ dist/ 编译产物;装进 node_modules 时加载的是这里
246
+ tsconfig.json erasableSyntaxOnly,保证源码也能被 Node 直接剥类型加载
247
+ cordis.patch.yml 发布用 bundle 层,用户 profile 里引用包名
248
+ cordis.example.yml 挂载清单模板(dsh 要求绝对路径,所以只能给模板)
249
+ spike/remote_seek_spike.py 可行性验证:量出远程 seek 实际拉了多少字节
250
+ spike/requirements.txt spike 的依赖
251
+ ```
252
+
253
+ ## 外部依赖
254
+
255
+ - **ffmpeg**:spike 用的是 `imageio-ffmpeg` 自带的静态构建,`pip install` 即有,
256
+ 系统不用装。
257
+ - **Python**:目前只用在 spike 里量字节数,插件本体是 TypeScript。
258
+
259
+ ## License
260
+
261
+ MIT
@@ -0,0 +1,13 @@
1
+ # 把 dsh-readvideo 挂进一个 DSH profile。
2
+ # 安装后由插件管理器把下面这条 insert 行自动写进 profile 的 cordis.patch.yml,
3
+ # 因此用户不需要手写任何配置;行里的 name 是包名,从 profile 的 node_modules 解析。
4
+ #
5
+ # 手工核对:dsh --profile <profile> --dump-config (出现 "# == dsh-readvideo" 层即已挂载)
6
+ # 顶层必须是「patch 操作序列」,写成裸 `insert:` 映射会被静默忽略。
7
+ - insert:
8
+ - id: read-video
9
+ name: dsh-readvideo
10
+ config:
11
+ ffmpegPath: null # 留空 = 自动从 imageio-ffmpeg / PATH 发现
12
+ cacheDir: null # 留空 = <cwd>/.read-video-cache
13
+ asrProvider: local # local | cloud
@@ -0,0 +1,13 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ export declare const name = "dsh-readvideo";
3
+ /** 依赖的服务槽:框架会先备好 ctx.tools,再调用 apply */
4
+ export declare const inject: string[];
5
+ export interface Config {
6
+ /** ffmpeg 可执行文件路径;留空 = 自动发现(后续阶段用) */
7
+ ffmpegPath?: string | null;
8
+ /** 缓存目录;留空 = <cwd>/.read-video-cache */
9
+ cacheDir?: string | null;
10
+ /** 语音识别后端:local = dsh 的 ctx.speechToText,cloud = 自配 API */
11
+ asrProvider?: 'local' | 'cloud';
12
+ }
13
+ export declare function apply(ctx: Context, _config?: Config): void;
package/dist/index.js ADDED
@@ -0,0 +1,111 @@
1
+ // dsh-readvideo —— DeepSeek Harness 插件入口(骨架版:先只有 video_probe)。
2
+ //
3
+ // 职责很窄:把"按需取字节理解视频"的能力暴露成模型可调用的工具。
4
+ // 重的逻辑(Range 取数、容器索引解析、ffmpeg 解帧、拼图)后续放进 src/video/,
5
+ // 本文件只负责按 dsh 插件规范挂载。
6
+ //
7
+ // 本地开发: 照 cordis.example.yml 填好绝对路径,再
8
+ // dsh --profile web --patch "<填好的清单>"
9
+ // 持久挂载: 把清单里的 `- insert:` 块粘进 ~/.dsh/profiles/<name>/cordis.patch.yml
10
+ // 发布: 发到 npm registry 后,在 dsh 的插件页输入包名 dsh-readvideo 即可安装
11
+ // (管理器会读 package.json 的 dsh.bundle.patch 自动写 profile 配置)
12
+ //
13
+ // 两条硬规矩(违反了不报错,只是行为诡异):
14
+ // 1. 只用具名导出,绝不 export default —— 默认导出会让加载器丢掉 inject 等元数据;
15
+ // 2. 模块顶层不留副作用,一切在 apply 内完成(插件会被反复挂载/卸载)。
16
+ import { defineTool } from '@deepseek-ai/dsh-tools';
17
+ export const name = 'dsh-readvideo';
18
+ /** 依赖的服务槽:框架会先备好 ctx.tools,再调用 apply */
19
+ export const inject = ['tools'];
20
+ /** 人类可读的字节数,仅用于 render 的展示文本 */
21
+ function humanBytes(n) {
22
+ if (!Number.isFinite(n) || n < 0)
23
+ return String(n);
24
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
25
+ let value = n;
26
+ let i = 0;
27
+ while (value >= 1024 && i < units.length - 1) {
28
+ value /= 1024;
29
+ i += 1;
30
+ }
31
+ return `${i === 0 ? value : value.toFixed(1)} ${units[i]}`;
32
+ }
33
+ export function apply(ctx, _config = {}) {
34
+ // ctx.tools.register 返回卸载函数,插件卸载时由 Cordis 自动调用,无需手动清理。
35
+ ctx.tools.register(defineTool({
36
+ name: 'video_probe',
37
+ description: 'Probe a video URL without downloading it: HTTP status, byte length, whether the server supports Range requests (Accept-Ranges), and content type. Call this first for any "look at this video" request — it decides whether later steps can seek to a specific time instead of streaming the whole file.',
38
+ parameters: {
39
+ url: {
40
+ type: 'string',
41
+ required: true,
42
+ description: 'Video URL, or a page URL that contains one',
43
+ },
44
+ },
45
+ output: {
46
+ schema: {
47
+ type: 'object',
48
+ additionalProperties: false,
49
+ properties: {
50
+ url: { type: 'string' },
51
+ ok: { type: 'boolean' },
52
+ status: { type: 'number' },
53
+ contentType: { type: 'string' },
54
+ contentLength: { type: 'number' },
55
+ acceptRanges: { type: 'string' },
56
+ seekable: { type: 'boolean' },
57
+ error: { type: 'string' },
58
+ },
59
+ },
60
+ render(_args, value) {
61
+ const v = value;
62
+ if (!v.ok) {
63
+ return [{ type: 'text', text: `probe failed: ${v.url}\n ${v.error}` }];
64
+ }
65
+ const lines = [
66
+ `url: ${v.url}`,
67
+ `status: ${v.status}`,
68
+ `content-type: ${v.contentType}`,
69
+ `length: ${humanBytes(v.contentLength)}`,
70
+ `accept-ranges: ${v.acceptRanges || '(none)'}`,
71
+ `seekable: ${v.seekable ? 'yes — can jump to a timestamp' : 'no — server ignores Range, only sequential read'}`,
72
+ ];
73
+ return [{ type: 'text', text: lines.join('\n') }];
74
+ },
75
+ },
76
+ timeoutMs: 30_000,
77
+ async execute(args, exec) {
78
+ const url = String(args.url);
79
+ try {
80
+ const resp = await fetch(url, {
81
+ method: 'HEAD',
82
+ redirect: 'follow',
83
+ signal: exec.signal,
84
+ });
85
+ const acceptRanges = resp.headers.get('accept-ranges') ?? '';
86
+ return {
87
+ url: resp.url || url,
88
+ ok: resp.ok,
89
+ status: resp.status,
90
+ contentType: resp.headers.get('content-type') ?? '',
91
+ contentLength: Number(resp.headers.get('content-length') ?? 0),
92
+ acceptRanges,
93
+ seekable: acceptRanges.toLowerCase().includes('bytes'),
94
+ error: '',
95
+ };
96
+ }
97
+ catch (err) {
98
+ return {
99
+ url,
100
+ ok: false,
101
+ status: 0,
102
+ contentType: '',
103
+ contentLength: 0,
104
+ acceptRanges: '',
105
+ seekable: false,
106
+ error: err instanceof Error ? err.message : String(err),
107
+ };
108
+ }
109
+ },
110
+ }));
111
+ }
package/package.json CHANGED
@@ -1,6 +1,68 @@
1
1
  {
2
2
  "name": "dsh-readvideo",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "DeepSeek Harness 插件:按需取字节阅读视频(Range 跳读 + 抽帧 + 语音转写),不下载整片",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "./package.json": "./package.json"
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "cordis.patch.yml",
19
+ "README.md"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc",
23
+ "prepare": "tsc",
24
+ "prepublishOnly": "npm run build"
25
+ },
26
+ "keywords": [
27
+ "dsh-plugin",
28
+ "deepseek-harness",
29
+ "deepseek-harness-plugin",
30
+ "video",
31
+ "video-understanding",
32
+ "ffmpeg",
33
+ "http-range",
34
+ "multimodal",
35
+ "llm-tools"
36
+ ],
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/RaganaBloodedge/dsh-read-video.git"
40
+ },
41
+ "homepage": "https://github.com/RaganaBloodedge/dsh-read-video#readme",
42
+ "bugs": {
43
+ "url": "https://github.com/RaganaBloodedge/dsh-read-video/issues"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public",
47
+ "registry": "https://registry.npmjs.org",
48
+ "tag": "latest"
49
+ },
50
+ "dsh": {
51
+ "bundle": {
52
+ "patch": "./cordis.patch.yml"
53
+ }
54
+ },
55
+ "peerDependencies": {
56
+ "@deepseek-ai/cordis": "^4.0.4",
57
+ "@deepseek-ai/dsh-tools": "^0.1.7-rc.2 || ^0.2.0-rc.1"
58
+ },
59
+ "devDependencies": {
60
+ "@deepseek-ai/cordis": "~4.0.4",
61
+ "@deepseek-ai/dsh-tools": "0.2.0-rc.2",
62
+ "typescript": "^5.6.0",
63
+ "@types/node": "^22.10.0"
64
+ },
65
+ "engines": {
66
+ "node": ">=22.6"
67
+ }
68
+ }