dsh-media-converter 1.2.0 → 1.2.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 +10 -2
- package/dist/index.js +36 -26
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,6 +19,12 @@ DeepSeek Harness 的本地多媒体工具箱插件:转换、裁剪、提取音
|
|
|
19
19
|
|
|
20
20
|
界面是一个「动作面板」:选文件 → 搜索或点动作 → 参数就地展开 → 结果就地显示。支持 `⌘K` 聚焦搜索、回车执行第一个动作、Esc 收起。
|
|
21
21
|
|
|
22
|
+

|
|
23
|
+
|
|
24
|
+
选中一张图片后:左侧原图、右侧实时预览,并排 / 滑块两种对比方式,下方是「转换 / 查看」分组的动作列表。
|
|
25
|
+
|
|
26
|
+

|
|
27
|
+
|
|
22
28
|
## 给 Agent 用的工具
|
|
23
29
|
|
|
24
30
|
同一个引擎也注册成工具 `media_toolbox`,助手可以直接调用:
|
|
@@ -44,12 +50,14 @@ DeepSeek Harness 的本地多媒体工具箱插件:转换、裁剪、提取音
|
|
|
44
50
|
|
|
45
51
|
## 安装
|
|
46
52
|
|
|
47
|
-
已发布到 npm(`dsh-media-converter
|
|
53
|
+
已发布到 npm(`dsh-media-converter`)。一行装完:
|
|
48
54
|
|
|
49
55
|
```sh
|
|
50
|
-
dsh plugin add dsh-media-converter
|
|
56
|
+
dsh plugin --profile web add dsh-media-converter
|
|
51
57
|
```
|
|
52
58
|
|
|
59
|
+
`web` 换成你自己的 profile 名即可。也可以在 Harness 里用 `plugin_manager` 的 `install_bundle` 装。
|
|
60
|
+
|
|
53
61
|
从源码装(开发用):`plugin_manager` 的 `install_bundle`,target 指向本仓库目录——它会在 profile 的 `package.json` 里登记包名,并在 `node_modules/` 下建一条软链指向本目录。
|
|
54
62
|
|
|
55
63
|
从 GitHub 装(未发布 npm 时的退路):
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
// DSH Host plugin: local media toolkit — convert, trim, extract tracks, and
|
|
2
2
|
// move media to/from Base64 through ffmpeg.
|
|
3
3
|
//
|
|
4
|
+
// Security has one home: every route asks the composition's `connection`
|
|
5
|
+
// service for a rejection first (the Host/Origin fence plus browser
|
|
6
|
+
// authentication, the same guard the `/api` bridge applies). No connection
|
|
7
|
+
// service means no trusted caller, so requests are refused rather than served.
|
|
8
|
+
//
|
|
4
9
|
// The Web page (client.js) never touches the filesystem: it uploads picked
|
|
5
|
-
// files to this plugin's
|
|
10
|
+
// files to this plugin's routes, the Host runs one ffmpeg process
|
|
6
11
|
// per job, and the page polls the job for progress. Results land in
|
|
7
12
|
// ~/Downloads/media-toolbox (config.outputDir overrides it) and are served back
|
|
8
13
|
// for preview from that same directory only.
|
|
@@ -241,18 +246,17 @@ export function apply(ctx, config) {
|
|
|
241
246
|
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
|
|
242
247
|
res.end(JSON.stringify(payload));
|
|
243
248
|
};
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}
|
|
253
|
-
catch {
|
|
249
|
+
// Every route crosses the connection trust fence before it touches the body
|
|
250
|
+
// or the disk: 403 answers a foreign Host/Origin, 401 a caller without the
|
|
251
|
+
// browser-session cookie. 503 when the composition owns no connection
|
|
252
|
+
// service — fail closed, never serve an unauthenticated request.
|
|
253
|
+
const connection = ctx.get('connection');
|
|
254
|
+
const rejectUnauthenticated = (req, res) => {
|
|
255
|
+
const rejection = connection ? connection.requestRejection({ headers: req.headers }) : 503;
|
|
256
|
+
if (rejection === undefined)
|
|
254
257
|
return false;
|
|
255
|
-
|
|
258
|
+
json(res, rejection, errorPayload('unauthorized'));
|
|
259
|
+
return true;
|
|
256
260
|
};
|
|
257
261
|
// The preview and the reveal may only reach into the output directory. The
|
|
258
262
|
// check resolves both sides first: a plain prefix check is defeated by a
|
|
@@ -605,6 +609,8 @@ export function apply(ctx, config) {
|
|
|
605
609
|
handler: (req, res) => {
|
|
606
610
|
if (req.method !== 'GET')
|
|
607
611
|
return json(res, 405, errorPayload('GET only'));
|
|
612
|
+
if (rejectUnauthenticated(req, res))
|
|
613
|
+
return;
|
|
608
614
|
json(res, 200, statusPayload(ffmpeg, outDir, Math.round(maxUploadBytes / 1048576), version, composerButton, groups));
|
|
609
615
|
},
|
|
610
616
|
}),
|
|
@@ -614,8 +620,8 @@ export function apply(ctx, config) {
|
|
|
614
620
|
handler: async (req, res) => {
|
|
615
621
|
if (req.method !== 'POST')
|
|
616
622
|
return json(res, 405, errorPayload('POST only'));
|
|
617
|
-
if (
|
|
618
|
-
return
|
|
623
|
+
if (rejectUnauthenticated(req, res))
|
|
624
|
+
return;
|
|
619
625
|
try {
|
|
620
626
|
const body = await readJson(req);
|
|
621
627
|
// Only defined keys are merged; an empty string resets that key to
|
|
@@ -670,8 +676,8 @@ export function apply(ctx, config) {
|
|
|
670
676
|
handler: async (req, res) => {
|
|
671
677
|
if (req.method !== 'POST')
|
|
672
678
|
return json(res, 405, errorPayload('POST only'));
|
|
673
|
-
if (
|
|
674
|
-
return
|
|
679
|
+
if (rejectUnauthenticated(req, res))
|
|
680
|
+
return;
|
|
675
681
|
if (!ffmpeg)
|
|
676
682
|
return json(res, 200, noFfmpeg);
|
|
677
683
|
let file = '';
|
|
@@ -740,8 +746,8 @@ export function apply(ctx, config) {
|
|
|
740
746
|
handler: async (req, res) => {
|
|
741
747
|
if (req.method !== 'POST')
|
|
742
748
|
return json(res, 405, errorPayload('POST only'));
|
|
743
|
-
if (
|
|
744
|
-
return
|
|
749
|
+
if (rejectUnauthenticated(req, res))
|
|
750
|
+
return;
|
|
745
751
|
if (!ffmpeg)
|
|
746
752
|
return json(res, 200, noFfmpeg);
|
|
747
753
|
try {
|
|
@@ -763,8 +769,8 @@ export function apply(ctx, config) {
|
|
|
763
769
|
handler: async (req, res) => {
|
|
764
770
|
if (req.method !== 'POST')
|
|
765
771
|
return json(res, 405, errorPayload('POST only'));
|
|
766
|
-
if (
|
|
767
|
-
return
|
|
772
|
+
if (rejectUnauthenticated(req, res))
|
|
773
|
+
return;
|
|
768
774
|
try {
|
|
769
775
|
const body = await readJson(req);
|
|
770
776
|
const job = jobs.get(String(body.id || ''));
|
|
@@ -800,6 +806,8 @@ export function apply(ctx, config) {
|
|
|
800
806
|
handler: (req, res) => {
|
|
801
807
|
if (req.method !== 'GET')
|
|
802
808
|
return json(res, 405, errorPayload('GET only'));
|
|
809
|
+
if (rejectUnauthenticated(req, res))
|
|
810
|
+
return;
|
|
803
811
|
const id = requestUrl(req).searchParams.get('id');
|
|
804
812
|
const job = id ? jobs.get(id) : undefined;
|
|
805
813
|
if (!job)
|
|
@@ -813,6 +821,8 @@ export function apply(ctx, config) {
|
|
|
813
821
|
handler: async (req, res) => {
|
|
814
822
|
if (req.method !== 'GET')
|
|
815
823
|
return json(res, 405, errorPayload('GET only'));
|
|
824
|
+
if (rejectUnauthenticated(req, res))
|
|
825
|
+
return;
|
|
816
826
|
const file = requestUrl(req).searchParams.get('file');
|
|
817
827
|
if (!insideOutput(file))
|
|
818
828
|
return json(res, 403, errorPayload('只能预览输出目录里的文件'));
|
|
@@ -825,8 +835,8 @@ export function apply(ctx, config) {
|
|
|
825
835
|
handler: async (req, res) => {
|
|
826
836
|
if (req.method !== 'POST')
|
|
827
837
|
return json(res, 405, errorPayload('POST only'));
|
|
828
|
-
if (
|
|
829
|
-
return
|
|
838
|
+
if (rejectUnauthenticated(req, res))
|
|
839
|
+
return;
|
|
830
840
|
try {
|
|
831
841
|
const { file } = await readJson(req);
|
|
832
842
|
if (!insideOutput(file))
|
|
@@ -846,8 +856,8 @@ export function apply(ctx, config) {
|
|
|
846
856
|
handler: async (req, res) => {
|
|
847
857
|
if (req.method !== 'POST')
|
|
848
858
|
return json(res, 405, errorPayload('POST only'));
|
|
849
|
-
if (
|
|
850
|
-
return
|
|
859
|
+
if (rejectUnauthenticated(req, res))
|
|
860
|
+
return;
|
|
851
861
|
let upload;
|
|
852
862
|
let mime;
|
|
853
863
|
try {
|
|
@@ -887,8 +897,8 @@ export function apply(ctx, config) {
|
|
|
887
897
|
handler: async (req, res) => {
|
|
888
898
|
if (req.method !== 'POST')
|
|
889
899
|
return json(res, 405, errorPayload('POST only'));
|
|
890
|
-
if (
|
|
891
|
-
return
|
|
900
|
+
if (rejectUnauthenticated(req, res))
|
|
901
|
+
return;
|
|
892
902
|
try {
|
|
893
903
|
const body = await readJson(req);
|
|
894
904
|
const payload = decodeBase64Payload(body.text);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-media-converter",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "Local media toolbox for DeepSeek Harness: convert, trim, split tracks, export subtitles, compress images, edit metadata, and Base64 — all through local ffmpeg.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|