@mokuyoaxis/dsh-iris 0.3.0-alpha.1 → 0.3.0-beta.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/CHANGELOG.md +63 -1
- package/README.en.md +20 -13
- package/README.md +20 -13
- package/bin/dsh-iris.js +143 -51
- package/docs/AGENT_CORE_QUICKSTART.md +2 -2
- package/docs/AGENT_QUICKSTART.md +1 -1
- package/docs/ARCHITECTURE.md +5 -5
- package/docs/ARTIFACT_MANIFEST.md +4 -0
- package/docs/AUDIO_FLOWS.md +20 -4
- package/docs/BETA_SCOPE.md +42 -0
- package/docs/CONTACT_SHEET.md +1 -1
- package/docs/CORE_CONNECTIONS.md +3 -1
- package/docs/CORE_QUICKSTART.md +7 -5
- package/docs/CORE_RUNTIME_CONTRACT.md +3 -3
- package/docs/CORE_SERVICE.md +9 -5
- package/docs/CORE_SKILLS.md +2 -2
- package/docs/DSH_COMPATIBILITY.md +11 -1
- package/docs/DSH_CORE_MIGRATION.md +6 -0
- package/docs/DSH_RUNTIME.md +2 -2
- package/docs/HEADLESS_CLI.md +29 -4
- package/docs/HOST_ADAPTER_CONTRACT.md +5 -3
- package/docs/IMAGE_FLOW.md +2 -2
- package/docs/IMAGE_GENERATION.md +1 -1
- package/docs/LOCAL_API.md +2 -2
- package/docs/LOCAL_API_CONTRACT.md +17 -15
- package/docs/LOCAL_CREATION.md +2 -2
- package/docs/LOCAL_OPERATIONS.md +2 -2
- package/docs/LOCAL_SELECTION.md +1 -1
- package/docs/LOCAL_WEB.md +11 -5
- package/docs/MCP.md +30 -9
- package/docs/MEDIA_SCHEDULING.md +72 -0
- package/docs/NETWORK_RESOURCES.md +106 -0
- package/docs/NETWORK_ROUTING.md +103 -0
- package/docs/NETWORK_WORKBENCH.md +65 -0
- package/docs/PANEL_EXTENSIONS.md +1 -1
- package/docs/PLUGINS.md +9 -5
- package/docs/PLUGIN_DEVELOPMENT.md +39 -0
- package/docs/PLUGIN_STARTUP.md +15 -2
- package/docs/PROVIDER_ADAPTER_CONTRACT.md +8 -2
- package/docs/PROVIDER_CREDENTIALS.md +67 -0
- package/docs/PROVIDER_EXTENSIONS.md +8 -4
- package/docs/PROVIDER_HEALTH.md +4 -2
- package/docs/PROVIDER_NETWORK.md +45 -0
- package/docs/PROVIDER_PLUGIN_CONTRACT.md +24 -0
- package/docs/PROVIDER_SETTINGS.md +13 -7
- package/docs/PROVIDER_SUBMISSION_CONTRACT.md +3 -1
- package/docs/ROADMAP.md +6 -6
- package/docs/SECURITY.md +1 -1
- package/docs/SYNC_DELIVERY_RECOVERY.md +5 -1
- package/docs/VIDEO_FLOWS.md +89 -0
- package/docs/VISION_MODEL.md +2 -0
- package/docs/WEB_PROTOTYPE.md +3 -3
- package/docs/WEB_RECOVERY.md +2 -2
- package/docs/WORKBENCH_ARTIFACTS.md +6 -0
- package/docs/WORKBENCH_LOGIN.md +2 -2
- package/docs/releases/0.3.0-beta.1.md +34 -0
- package/docs/screenshots.md +2 -2
- package/lib/actions.js +5 -4
- package/lib/adapters.js +58 -41
- package/lib/api.js +23 -13
- package/lib/application-runtime.js +44 -15
- package/lib/application-writer.js +193 -0
- package/lib/client.d.ts +3 -3
- package/lib/client.js +62 -25
- package/lib/comfyui-adapter.js +3 -2
- package/lib/comfyui-client.js +10 -1
- package/lib/command-service.js +18 -5
- package/lib/config.js +13 -9
- package/lib/configured-image-runtime.js +28 -9
- package/lib/connection-skill.js +9 -0
- package/lib/core-artifact-store.js +68 -26
- package/lib/core-bridge-tools.js +5 -3
- package/lib/core-capability-catalog.js +21 -11
- package/lib/core-delivery-cache.js +8 -3
- package/lib/core-service-client.js +6 -2
- package/lib/core-tasks.js +86 -11
- package/lib/creation-request-store.js +8 -5
- package/lib/doctor.js +2 -2
- package/lib/dsh-core-adapter.js +61 -9
- package/lib/dsh-provider-config.js +1 -1
- package/lib/dsh-service-bridge.js +1 -0
- package/lib/flow-store.js +64 -17
- package/lib/image-flow.js +39 -17
- package/lib/index.js +10 -5
- package/lib/local-api-projection.js +6 -2
- package/lib/local-api.js +137 -49
- package/lib/local-creation.js +12 -5
- package/lib/local-operations.js +2 -2
- package/lib/local-request-root.js +2 -2
- package/lib/local-web-assets.js +2 -0
- package/lib/mcp-http.js +82 -9
- package/lib/mcp-server.js +23 -1
- package/lib/media-scheduler.js +194 -0
- package/lib/model-directory-service.js +25 -8
- package/lib/music-transport.js +4 -2
- package/lib/network-config-service.js +123 -0
- package/lib/network-config.js +127 -0
- package/lib/network-doctor.js +94 -0
- package/lib/network-management.js +51 -0
- package/lib/network-policy.js +19 -0
- package/lib/network-route-evidence.js +38 -0
- package/lib/network-routing.js +283 -0
- package/lib/operation-runner.js +83 -38
- package/lib/operation-store.js +6 -6
- package/lib/plugins.d.ts +25 -6
- package/lib/plugins.js +2 -1
- package/lib/provider-adapter.js +2 -2
- package/lib/provider-adapters.js +37 -5
- package/lib/provider-catalog.js +33 -16
- package/lib/provider-config-format.js +51 -0
- package/lib/provider-config-service.js +52 -22
- package/lib/provider-context.d.ts +90 -0
- package/lib/provider-context.js +42 -0
- package/lib/provider-credentials.js +97 -0
- package/lib/provider-diagnostics.js +5 -5
- package/lib/provider-health.js +10 -5
- package/lib/provider-monitor.js +5 -3
- package/lib/provider-network.js +52 -0
- package/lib/provider-protocol.js +3 -2
- package/lib/provider-registry.js +15 -5
- package/lib/provider-response.js +10 -0
- package/lib/provider-settings.js +14 -12
- package/lib/provider-task-runner.js +113 -37
- package/lib/provider-transport.js +16 -0
- package/lib/render.js +11 -1
- package/lib/scheduling-policy.js +66 -0
- package/lib/tasks.js +3 -2
- package/lib/video-flow-input.js +16 -0
- package/lib/video-input.js +12 -5
- package/lib/vision-model-routing.js +2 -0
- package/lib/web/app.js +107 -46
- package/lib/web/connections.js +151 -12
- package/lib/web/creation.js +29 -25
- package/lib/web/flow-view.js +4 -3
- package/lib/web/gallery.js +17 -16
- package/lib/web/index.html +58 -5
- package/lib/web/media-client.js +18 -4
- package/lib/web/network.js +232 -0
- package/lib/web/plugin-panel.js +4 -2
- package/lib/web/plugin-status.js +140 -0
- package/lib/web/providers.js +34 -13
- package/lib/web/recovery.js +2 -2
- package/lib/web/styles.css +54 -1
- package/lib/web/transport.js +6 -1
- package/lib/workbench-artifacts.js +3 -2
- package/package.json +5 -2
- package/skills/iris-core/references/comfyui.md +1 -1
- package/templates/plugin-starter/LICENSE +21 -0
- package/templates/plugin-starter/README.md +74 -0
- package/templates/plugin-starter/assets/peach.png +0 -0
- package/templates/plugin-starter/assets/violet.png +0 -0
- package/templates/plugin-starter/index.mjs +27 -0
- package/templates/plugin-starter/package.json +18 -0
- package/templates/plugin-starter/scripts/core-cli.mjs +20 -0
- package/templates/plugin-starter/scripts/environment.mjs +65 -0
- package/templates/plugin-starter/scripts/mcp-probe.mjs +22 -0
- package/templates/plugin-starter/scripts/setup.mjs +42 -0
- package/templates/plugin-starter/scripts/smoke.mjs +76 -0
- package/templates/plugin-starter/web/board.css +27 -0
- package/templates/plugin-starter/web/board.mjs +160 -0
- package/templates/plugin-starter/web/png.mjs +23 -0
- package/templates/provider-starter/LICENSE +21 -0
- package/templates/provider-starter/README.md +50 -0
- package/templates/provider-starter/index.d.mts +5 -0
- package/templates/provider-starter/index.mjs +165 -0
- package/templates/provider-starter/package.json +13 -0
- package/templates/provider-starter/scripts/core-cli.mjs +22 -0
- package/templates/provider-starter/scripts/environment.mjs +122 -0
- package/templates/provider-starter/scripts/fixture.mjs +54 -0
- package/templates/provider-starter/scripts/mcp-probe.mjs +21 -0
- package/templates/provider-starter/scripts/protocol-smoke.mjs +48 -0
- package/templates/provider-starter/scripts/setup.mjs +44 -0
- package/templates/provider-starter/scripts/smoke.mjs +145 -0
- package/user_guide.md +3 -3
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# 文生视频 · 持久流程与资源交付
|
|
2
|
+
|
|
3
|
+
这是 **0.3.0-beta.1 新增功能**。使用准确beta版本或npm `beta`通道;旧alpha.1没有下面的 `--video`、视频 Web 表单与 MCP 分组。原有 CLI / DSH 视频入口继续可用。
|
|
4
|
+
|
|
5
|
+
协议按实际模型选择。`wan2.2-t2v-plus`等旧系列应对照官方[万相2.1–2.6文生视频接口](https://help.aliyun.com/zh/model-studio/legacy-wan-text-to-video-api-reference);官方当前主视频页已转向wan2.7新协议,不能仅凭同品牌模型套用其字段或声称已有实现覆盖。
|
|
6
|
+
|
|
7
|
+
## 新建专用工作区
|
|
8
|
+
|
|
9
|
+
以下路径必须是新的空 Flow 根:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
node bin/dsh-iris.js flow init --data-root /absolute/new-video-core
|
|
13
|
+
node bin/dsh-iris.js execution init --video --data-root /absolute/new-video-core
|
|
14
|
+
node bin/dsh-iris.js web serve --data-root /absolute/new-video-core \
|
|
15
|
+
--provider-config /absolute/private/providers.json --allow-execution \
|
|
16
|
+
--auth-store /absolute/private/video-auth \
|
|
17
|
+
--connection-store /absolute/private/video-connections \
|
|
18
|
+
--allow-connection-management --allow-mcp
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`--video` 显式建立 `creation-requests/v4`,同时包含原图片、语音与转写流程。不能与 `--audio` 或 `--image-generation` 混用;已有 v0–v3 根不升级、迁移或降级。旧版本在请求区门禁拒绝 v4;不要让旧版本 CLI 写入此根。
|
|
22
|
+
|
|
23
|
+
初始化、读取目录、创建连接与保存视频意图均不生成。只有明确的首次继续调用模型,可能产生费用。模型池仍来自真实配置和既有用途规则;此固定流程要求用户选择一个实际 `modelRef`,不会跨请求轮询或悄悄切换模型。
|
|
24
|
+
|
|
25
|
+
beta.1修复统一检查明确的t2v/i2v/s2v/r2v模型家族:文生与图生候选不互选,固定文生视频Flow拒绝已知i2v/s2v/r2v模型。CLI/DSH和显式视频重试使用相同规则。没有这些家族标记的兼容模型ID延续原有t2v/i2v路径,实际支持仍由协议和上游决定;本片不新增模型元数据格式或迁移目录。
|
|
26
|
+
|
|
27
|
+
独立 CLI `flow create/continue` 已补齐视频候选装配(beta.1修复),无需启动 Web:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
node bin/dsh-iris.js flow create --data-root /absolute/new-video-core \
|
|
31
|
+
--provider-config /absolute/private/providers.json \
|
|
32
|
+
--input '{"recipe":"video-generate/v0","model_ref":"provider::t2v-model","prompt":"鸢尾花随风轻摆","duration":5}'
|
|
33
|
+
node bin/dsh-iris.js flow continue run_0123456789abcdef01234567 \
|
|
34
|
+
--data-root /absolute/new-video-core --provider-config /absolute/private/providers.json
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Run ID 使用创建命令的实际返回值。首次继续仅提交一次;异步未结束时再次明确继续只观察原 Task,不重提。该固定流程仍仅文生视频;独立 `task retry` 的模式和媒体输入规则见[CLI 重试](HEADLESS_CLI.md#检查-task-与-artifact),不会因此扩展本流程范围。
|
|
38
|
+
|
|
39
|
+
## 输入、任务和交付边界
|
|
40
|
+
|
|
41
|
+
- 固定配方为 `video-generate/v0`:明确 `modelRef`、`prompt`,可选 `size` 和数值型 `duration`(1–60 秒)。Prompt 最多20,000字符、尺寸最多64字符;具体参数支持由实际模型决定,目录不等于实测。
|
|
42
|
+
- 首片仅文生视频;不接受首帧、音频、S2V、任意供应商 JSON、本机路径或外部结果 URL。原 CLI / DSH 的图生视频和 S2V 入口不受影响。
|
|
43
|
+
- 创建预留唯一 Run/Task,并持久保存创建 UUID 和输入摘要;不立即提交。继续保存独立 Operation UUID,一次有限调用只提交或观察原 Task,受理未知不重新提交。
|
|
44
|
+
- 服务重启、刷新、历史读取不自动生成或查询供应商。异步任务需明确点击“查看原任务”再观察一次;停止等待只终止本地操作,不代表远端取消、退款或未计费。
|
|
45
|
+
- 原 Task 已成功但落盘失败时接回原交付;已有完整缓存可在无 Provider 的重启后修复。保留原 Run/Task/Operation/Artifact 身份。
|
|
46
|
+
- 视频为单一结果,无图片选择步骤。交付和读取限 MP4/WebM、最多 **32 MiB**;API 验证保存记录、大小及 SHA-256。没有完整编码探测、Range 或边下边播,客户端收到完整 Blob 后播放;真实模型的编码兼容性仍需验收。
|
|
47
|
+
- 类型或大小不符时保留原 Task 与已有交付证据,明确报错,不重新生成。工作台不输出供应商签名 URL、密钥或服务器存储路径。
|
|
48
|
+
|
|
49
|
+
beta.1修复会先检查 Manifest、媒体类型及实际普通文件大小,再读取固定流程的结果。超出32 MiB时保留已成功的原 Task/Artifact;重启后再次继续仍明确拒绝,不整读该文件或重新生成。这个上限属于固定 Flow/Core Web/MCP 视频读取入口;通用 Provider Task 的视频下载上限仍为1 GiB。DSH同源作品媒体GET按流发送、HEAD不分配完整文件缓冲,两者在发送前保留分块SHA-256校验;没有新增Range或编码探测。
|
|
50
|
+
|
|
51
|
+
Web 的 **创作 → 生成视频** 使用实际候选,参数默认折叠;结果提供原视频播放器和下载。**作品 → 视频** 点击“载入视频”才读取,不自动播放;离页、换分类和退出释放媒体。撤销执行权限的只读服务仍可按读取授权播放已有结果。浏览器恢复表只存身份,不存 Prompt 或凭据;丢失创建回包后接回原意图,不自动首次提交。
|
|
52
|
+
|
|
53
|
+
## HTTP 与 MCP
|
|
54
|
+
|
|
55
|
+
HTTP 路径均以 `/api/v0` 开头,沿用 Bearer、Host/Origin、连接功能和具体工具授权:
|
|
56
|
+
|
|
57
|
+
| 入口 | 含义 |
|
|
58
|
+
|---|---|
|
|
59
|
+
| GET/HEAD `/video-models` | 真实池内文生视频候选;不探测,无密钥/端点 |
|
|
60
|
+
| POST `/videos` | JSON 意图,稳定 UUIDv4 `Idempotency-Key`;同 Key 同输入重放,不同输入409 |
|
|
61
|
+
| GET/HEAD `/artifacts/:id/video` | 授权后的原 MP4/WebM |
|
|
62
|
+
| GET/HEAD `/runs/:id/download` | 单一结果原字节及匹配扩展名 |
|
|
63
|
+
| Run / Operation 的既有入口 | 创建查询、有限继续、原操作查询和本地停止 |
|
|
64
|
+
|
|
65
|
+
JSON 正文最多64 KiB。v0–v3 根不开放此媒体组,旧连接和默认 image-edit 不自动扩权。MCP 由用户明确选择 `video-generate`,再分别选择 read/create/execute/stop 和需要的具体工具:
|
|
66
|
+
|
|
67
|
+
| 工具 | 行为 |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `iris_core_video_models` | 读取实际候选 |
|
|
70
|
+
| `iris_core_create_video` | 保存意图,尚不生成 |
|
|
71
|
+
| `iris_core_video` | 校验并引用已保存的原视频,不调用模型 |
|
|
72
|
+
|
|
73
|
+
继续、查询和停止复用原工具,并按原 Run 媒体组校验。MCP 无原生 video 内容块,视频工具返回标准 `resource_link`,URI 为 `iris://video/ARTIFACT_ID`。客户端通过标准 `resources/read` 获取 Base64 BlobResourceContents;每次读取重新检查 Core 当前授权。仅选择视频读取工具才注册资源模板,缩权或撤销后旧进程也无法继续读取。连接专属 Skill 由实际工具生成视频指导。
|
|
74
|
+
|
|
75
|
+
客户端可能只显示引用,不提供播放器;拿到引用不等于看过视频。DSH 原生共享桥暂不投影视频资源,选择视频组时明确拒绝启动;使用通用 MCP、Core Web,或原 DSH 视频工具。
|
|
76
|
+
|
|
77
|
+
## 验证范围
|
|
78
|
+
|
|
79
|
+
以下测试和`scripts/`开发验收命令从源码仓库根运行,不随npm包分发。定向用例:`tests/video-flow.mjs`、`tests/video-api.mjs`、`tests/video-recovery.mjs`。真实 Chromium 场景:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
node scripts/verify-local-web-browser.mjs \
|
|
83
|
+
--browser-executable /absolute/path/to/chromium \
|
|
84
|
+
--output /absolute/new-video-evidence --video
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
覆盖实际小视频解码/播放、原字节下载、重启接回、丢失创建回包、停止、只读、窄屏和暗色;标准 SDK 的 HTTP/stdio 2025/2026 路径验证资源与权限。这些回归使用受控 Provider。
|
|
88
|
+
|
|
89
|
+
另于2026-11-01完成百炼北京 `wan2.2-t2v-plus` 的真实单例:832×480、5秒H.264/MP4,1次生成提交、2次原任务查询;隔离服务重启接回同一Task、正式MCP资源读取、真实Chromium播放/下载与SHA一致均通过。未修改日常连接或查询实际账单。该结果仅覆盖这个模型、参数和当时环境,**不代表所有上游、账号、编码或客户端播放器均已实测**。
|
package/docs/VISION_MODEL.md
CHANGED
|
@@ -48,4 +48,6 @@ DSH rc.2 会压缩宿主附件。看图、OCR 和定位允许这种宿主规范
|
|
|
48
48
|
|
|
49
49
|
[长图 OCR](OCR_MODEL.md)、[定位、拼图摘要与自述](COMPOSITE_VISION.md) 均已消费共享端口与整次预算;各自另验分块、bbox 解析、一张拼图一次生成及可选 Core 转写。legacy `vision.js` / Host `visionModel.analyze()` 兼容实现仍保留,现有业务入口不再调用。Model Port 的迁移切片本身不新增公开 SDK export 或生产依赖;0.2.1 提供 [视觉 CLI](VISION_CLI.md) 和可选 `visionInput`。复杂图像质量与 Android 浏览器仍需独立验证。
|
|
50
50
|
|
|
51
|
+
beta.1另修复旧 `adapters.visionStream` 兼容入口:累计SSE正文最多2 MiB,声明长度与实际字节均检查;外部取消及超时在发送前、收到响应后、逐块及结束时保留失败,已收到文字也不返回部分成功。主业务仍使用上文具有严格终态校验的Model Port,不把旧入口的宽松解析等同于该契约。
|
|
52
|
+
|
|
51
53
|
契约与迁移说明见 [Model Port](MODEL_PORT_CONTRACT.md) 和 [DSH → Core](DSH_CORE_MIGRATION.md)。
|
package/docs/WEB_PROTOTYPE.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
这是面向 **0.3.0** 的交互预览,包含创作、作品和连接草稿三个页面。首轮布局已围绕大图预览、紧凑参数面板和默认折叠的详情整理,仍用于验证操作方向,尚未定稿。
|
|
4
4
|
|
|
5
|
-
Demo 可在源码树中独立预览,无需 DSH;使用内存演示与内置样图,不连接模型、不计费,也不写入 Core 数据。0.3
|
|
5
|
+
Demo 可在源码树中独立预览,无需 DSH;使用内存演示与内置样图,不连接模型、不计费,也不写入 Core 数据。0.3 beta的真实工作台已接入固定图片编辑及[独立文生图](IMAGE_GENERATION.md);npm 0.2.7 不含该页面,也不携带 prototypes/ 和预览脚本。
|
|
6
6
|
|
|
7
7
|
## 启动预览
|
|
8
8
|
|
|
@@ -46,9 +46,9 @@ node scripts/preview-workbench.mjs
|
|
|
46
46
|
|
|
47
47
|
原型位于 `prototypes/workbench/`,使用原生 HTML/CSS/JavaScript,复用 Iris 图标和示例图片,没有新框架或生产依赖。运行详情和工具预览默认折叠,关闭显示“展开”、打开显示“收起”;提供浅/深色、键盘焦点与 320/390px 布局。
|
|
48
48
|
|
|
49
|
-
0.2.7 已有 [默认只读 API](LOCAL_API.md)及显式开启的 [本地上传/Run 创建](LOCAL_CREATION.md),带持久请求意图和身份接回;0.3
|
|
49
|
+
0.2.7 已有 [默认只读 API](LOCAL_API.md)及显式开启的 [本地上传/Run 创建](LOCAL_CREATION.md),带持久请求意图和身份接回;0.3 beta另有 [Operation 与有限执行服务](CORE_SERVICE.md)、[版本约束选择](LOCAL_SELECTION.md)及真实创作页面。此 Demo 仍独立使用内存样例,其布局、提示词和样例效果用于说明方向,不代表模型能力;真实页面的恢复、故障与发布门禁继续独立验收。
|
|
50
50
|
|
|
51
|
-
[本地 API 契约](LOCAL_API_CONTRACT.md)
|
|
51
|
+
[本地 API 契约](LOCAL_API_CONTRACT.md)区分已发布的只读/本地创建,以及0.3提供的有限执行、本地停止和 owner 持久选择。相同 Run 的 Task 复用、HTTP 创建幂等与选择的版本门禁分别验证;替换演示传输时仍须单独验证浏览器写入、原身份恢复与无 DSH 的完整流程。
|
|
52
52
|
|
|
53
53
|
源码树可运行下面的检查;浏览器检查需显式提供已经安装的 Chromium,不下载或安装浏览器。无沙箱仅在受控测试环境确有需要时显式追加 `--no-sandbox`,不改变生产浏览器策略。
|
|
54
54
|
|
package/docs/WEB_RECOVERY.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# 工作台请求核对与刷新恢复(0.3
|
|
1
|
+
# 工作台请求核对与刷新恢复(0.3 beta)
|
|
2
2
|
|
|
3
3
|
本页描述源码中的 W1b 恢复及 W1c 页面接入。npm 0.2.7 不含这些新增接口与浏览器客户端;[真实工作台](LOCAL_WEB.md)已接入明确创作、选择和本地停止,“操作核对”默认折叠,自动恢复始终只读。
|
|
4
4
|
|
|
@@ -15,7 +15,7 @@ GET /api/v0/creation-requests?kind=flow-create&key=<original-lowercase-UUIDv4>
|
|
|
15
15
|
{contractVersion:0, kind, state:"prepared"|"succeeded", result}
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
创建类型另包括 `generation-create`;新 v3 音频工作区增加 `audio-import`、`speech-create`、`transcription-create`,返回同样的有限身份,不返回音频或朗读正文。音频恢复与大小边界见[音频流程](AUDIO_FLOWS.md)。
|
|
18
|
+
创建类型另包括 `generation-create`;新 v3 音频工作区增加 `audio-import`、`speech-create`、`transcription-create`,返回同样的有限身份,不返回音频或朗读正文。音频恢复与大小边界见[音频流程](AUDIO_FLOWS.md)。beta.1的 v4 根另有 `video-create`,同样只保存原 UUID/Run 身份,不存 Prompt;查询或找回已创建意图不自动首次生成,见[视频流程](VIDEO_FLOWS.md)。
|
|
19
19
|
|
|
20
20
|
`result` 为 `{artifactId}`、`{runId}` 或 `null`。查询会核对原意图与已有结果;prepared 尚无目标时返回 null,已有目标核验通过后才返回引用。它不补写完成回执、不重建目标、不观察 Task 或调用 Provider。已推进或选择的 Run 不退回初始状态。没有原请求为 404;原结果不可用或证据损坏为 409,并保留原记录。
|
|
21
21
|
|
|
@@ -36,6 +36,12 @@ Core静态PNG/JPEG/WebP图片卡提供“改图”。填写修改指令,可选
|
|
|
36
36
|
|
|
37
37
|
目录查询复用已提交的 Manifest 与派生 Index,只读取元数据和文件大小,不反复读取视频等媒体的完整字节、不重建索引或取得 Core writer 租约。打开媒体、看图/OCR、CLI inspect/list/export 保留原有完整内容哈希核验;目录里显示的哈希不表示本次浏览已经重新核验了全部文件内容。
|
|
38
38
|
|
|
39
|
+
beta.1把可兼容读取的早期Core提交记录单列为 `legacyCoreSkipped`,不计入损坏/缺失的 `droppedCore`。这些作品仍可按原ID检查或导出;需要编入当前目录时,由用户显式运行 `artifact rebuild --data-root /absolute/core`,该写操作可能升级早期记录。浏览和刷新不自动迁移,也不为分类重新哈希媒体;结构、大小或索引信息不符仍按不可读取处理。
|
|
40
|
+
|
|
41
|
+
DSH Core快照也复用这条元数据路径:兼容早期条目单列`legacySkipped`,坏条目单列`dropped`,正常作品继续显示。新作品提交对早期条目复用已校验索引中的摘要与大小,不读取历史媒体;缺少或不匹配的摘要要求显式重建,不补造内容证据。重建和库存刷新在写入前标记索引待刷新,失败后普通列表明确报错,成功发布新索引才清除标记;原作品记录和按ID读取保持。
|
|
42
|
+
|
|
43
|
+
beta.1中,DSH作品媒体GET先分块核验内容,再流式发送;HEAD同样验证哈希但不读取完整文件到内存。客户端断开或实例关闭会排空并关闭媒体流,原作品不变;不增加Range支持。图库索引未完成刷新时会明确报错并由Doctor提示,不以缺项的旧列表冒充完整目录;保留现场后可显式执行 `artifact rebuild`。
|
|
44
|
+
|
|
39
45
|
OCR 动作 JSON 附 `status/totalChunks/successfulChunks/failedChunks/skippedChunks`,便于界面表达完整度。识别正文不自动保存为 Core Artifact。
|
|
40
46
|
|
|
41
47
|
## 选择、详情与批量下载
|
package/docs/WORKBENCH_LOGIN.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# 工作台访问口令(0.3
|
|
1
|
+
# 工作台访问口令(0.3 beta)
|
|
2
2
|
|
|
3
|
-
普通 `web serve` 使用一个访问口令,不要求账号、邮箱或读取 Token 文件。**稳定 npm 0.2.7 没有 `web serve`;下文针对0.3.0-
|
|
3
|
+
普通 `web serve` 使用一个访问口令,不要求账号、邮箱或读取 Token 文件。**稳定 npm 0.2.7 没有 `web serve`;下文针对0.3.0-beta.1。** 源码树可把 `dsh-iris` 替换为 `node bin/dsh-iris.js`。
|
|
4
4
|
|
|
5
5
|
## 首次设置与日常使用
|
|
6
6
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Iris 0.3.0-beta.1: routing, scheduling and durable video
|
|
2
|
+
|
|
3
|
+
The first 0.3 beta builds on alpha.1's standalone Core workbench, generic MCP, ComfyUI and explicit plugins. It freezes the [minimal plugin contracts and main user flows](https://github.com/mokuyoaxis/dsh-iris/blob/v0.3.0-beta.1/docs/BETA_SCOPE.md) within this beta scope. The broader SDK remains experimental.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @mokuyoaxis/dsh-iris@0.3.0-beta.1
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
The npm `beta` channel selects this release. Stable `latest` remains **0.2.7**, and `alpha` retains **0.3.0-alpha.1**. Follow the [Core quickstart](https://github.com/mokuyoaxis/dsh-iris/blob/v0.3.0-beta.1/docs/CORE_QUICKSTART.md) for a new workspace.
|
|
10
|
+
|
|
11
|
+
## Added since alpha.1
|
|
12
|
+
|
|
13
|
+
- **Explicit network resources and routing.** Manage HTTP/HTTPS proxy nodes and fixed or ordered pools, bind a Provider, and inspect the route recorded for a task. An owner-only Web/CLI/API Doctor separates local configuration from an explicit bounded GET check. Unbound Providers inherit the actual process network environment; Iris does not configure Clash, control system TUN or infer geographic suitability.
|
|
14
|
+
- **Opt-in bounded media scheduling.** One physical root still has one writer. Network waits can overlap while short writes remain serialized, with separate HTTP concurrency, dispatch-rate and unfinished-remote-task limits. The default remains serial. Unknown acceptance never triggers automatic resubmission.
|
|
15
|
+
- **Durable text-to-video in Core Web and MCP.** A new v4 root adds actual model candidates, size/duration inputs, original Run/Task/Operation recovery, local stop and MP4/WebM playback/download up to 32 MiB. MCP exposes standard resource links and checks authorization on each read. Image-to-video, S2V, music and vision retain their existing CLI/DSH entries.
|
|
16
|
+
- **Environment credential references.** Explicit `apiKeyEnv` configuration stores a variable name and resolves its value inside Core. Inline keys remain supported. Conflicting inline/env/DSH sources are rejected; workflow reports and model-directory imports protect captured credentials. No automatic `.env` loading or host-configuration scanning is added.
|
|
17
|
+
- **Independent plugin starters.** The package includes a business/page/PNG-preview starter and a Wan asynchronous Provider starter. Provider factory context v1 lends Core's network port and resolved connection; Core owns scheduling, lifecycle, persistent tasks and storage. Plugin identities, exact dependencies and client grants remain explicit.
|
|
18
|
+
- **Clearer MCP onboarding and plugin status.** The workbench connects selection/authorization, configuration export and connection checks. Generic stdio/Streamable HTTP remain available with explicit client presets. Configuration inspection, protocol handshake and actual Agent use are separate facts.
|
|
19
|
+
|
|
20
|
+
## Reliability fixes
|
|
21
|
+
|
|
22
|
+
Paid submissions do not automatically follow redirects or replay ambiguous requests. Queue rejection stays local to the affected Operation; accepted-task recovery preserves the original Attempt. Source-artifact permissions, multipart/Request interoperability, bounded MCP HTTP load, network-client retirement, media size checks and audio container metadata have been corrected.
|
|
23
|
+
|
|
24
|
+
Artifact index failures remain visible and recoverable. Normal commits refresh metadata without hashing the entire media library; explicit rebuild and media reads retain content verification. DSH screenshot delivery confirms that the requested Iris HTML route actually served the page before accepting a PNG. Video model families, transcription retries and the browser-only DSH client type declaration have also been aligned.
|
|
25
|
+
|
|
26
|
+
## Compatibility and validation
|
|
27
|
+
|
|
28
|
+
Existing roots and grants are **not migrated or expanded automatically**. Video requires an explicitly initialized new v4 root; network bindings use a separate versioned configuration. Keep compatible readers and a complete backup when trying a newer format. DSH's existing exact host-version range remains unchanged. The Node.js minimum is **22.1.0**: isolated testing reproduced native fetch body-cancellation failures on 22.0.0, including without Iris, while 22.1.0 and current Node 22 pass. Package engines, Doctor and the minimum CI job use the same baseline.
|
|
29
|
+
|
|
30
|
+
Release checks cover the Ubuntu/Windows and minimum/current Node 22 matrix, actual tarball consumers, strict NodeNext public types and Chromium workbench scenarios. Controlled Providers and local proxies are distinguished from real accounts. Selected Alibaba Cloud image/video/audio paths have separate real-account evidence; this does not certify every model, account, GPU, browser or harness. Live ComfyUI GPU inference, automatic quotas/GC, weighted model/proxy rotation and a plugin marketplace remain outside this beta.
|
|
31
|
+
|
|
32
|
+
## 中文摘要
|
|
33
|
+
|
|
34
|
+
beta.1 主要新增网络出口池与Doctor、有界并发调度、Core Web/MCP文生视频、环境凭据引用,以及随包分发的两类独立插件模板;同时完善MCP接入流程与插件状态,修复这轮审计确认的凭据、提交/恢复、媒体限额、索引和DSH集成问题。使用npm `beta`或准确版本安装,稳定版仍为0.2.7。最低Node为22.1.0;旧根不自动迁移,视频需显式新建v4根;最小契约收口不代表完整SDK或全部上游已验收。
|
package/docs/screenshots.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
## Core 工作台开发预览 / Core workbench development preview
|
|
4
4
|
|
|
5
|
-
2026-10-29
|
|
5
|
+
2026-10-29 重拍,使用alpha.1发布前的源码、独立演示数据根及 Chrome for Testing 153.0.8010.12(Linux ARM64)。全新浏览器 profile 只使用内置明亮/暗色主题;流程与作品是此前从零使用演练已完成记录的独立副本,没有读取用户背景、真实供应商凭据或日常浏览器会话。
|
|
6
6
|
|
|
7
|
-
Recaptured on 2026-10-29 from the
|
|
7
|
+
Recaptured on 2026-10-29 from the pre-alpha.1 source in Chrome for Testing 153.0.8010.12 on Linux ARM64. A fresh browser profile uses only the built-in light/dark themes. The separate demo data root contains copies of completed records from the earlier first-use walkthrough. No personal background, real provider credentials or daily browser session is used.
|
|
8
8
|
|
|
9
9
|
| 创作与下载 / Studio and download | 完整六类总览 / All six capabilities |
|
|
10
10
|
|---|---|
|
package/lib/actions.js
CHANGED
|
@@ -30,7 +30,7 @@ import { formatOcrResult } from './ocr.js';
|
|
|
30
30
|
import { runOcrRequest } from './ocr-model-routing.js';
|
|
31
31
|
import { ffmpegAvailable, extractFrames, probeVideo } from './media-probe.js';
|
|
32
32
|
import { ensurePrivateDir, writePrivateFile } from './private-storage.js';
|
|
33
|
-
import { selectMediaProtocol, selectImageProtocol, providerForImageModel, supportsImageEdit, supportsModelCapability, dashscopeTtsProtocolIssue, dashscopeTtsInputIssue } from './provider-protocol.js';
|
|
33
|
+
import { selectMediaProtocol, selectImageProtocol, providerForImageModel, providerApiKey, supportsImageEdit, supportsModelCapability, dashscopeTtsProtocolIssue, dashscopeTtsInputIssue } from './provider-protocol.js';
|
|
34
34
|
import { musicCandidatesFromCatalog, providerTaskBinding } from './provider-catalog.js';
|
|
35
35
|
import { invokeProviderOperation, providerPollTaskView, prepareProviderInput } from './provider-adapter.js';
|
|
36
36
|
import { createConfiguredProviderAdapter as createProviderAdapter } from './provider-adapters.js';
|
|
@@ -43,6 +43,7 @@ import { ModelPortError } from './model-port-contract.js';
|
|
|
43
43
|
import { compileComfyWorkflow, projectComfyWorkflow } from './comfyui-workflow.js';
|
|
44
44
|
import { createComfyUiClient } from './comfyui-client.js';
|
|
45
45
|
import { inspectComfyWorkflow } from './comfyui-inspection.js';
|
|
46
|
+
import { createProviderSecretScrubber } from './provider-credentials.js';
|
|
46
47
|
|
|
47
48
|
const PROVIDER_ACTIONS = new Set(['image', 'image_edit', 'video', 'music', 'tts', 'transcribe', 'look', 'relook',
|
|
48
49
|
'locate', 'ocr', 'media_summarize', 'task_reobserve', 'task_redeliver', 'task_manual_retry',
|
|
@@ -1104,7 +1105,7 @@ register('providers_list', async (host, args) => {
|
|
|
1104
1105
|
...selectMediaProtocol(p),
|
|
1105
1106
|
visionInput: p.visionInput || null,
|
|
1106
1107
|
directory: modelDirectorySummary(p),
|
|
1107
|
-
apiKeyHint: p.dshProvider ? '由 DSH 管理' : String(p.apiKey || '').length <= 8 ? '****' : String(p.apiKey).slice(0, 3) + '****' + String(p.apiKey).slice(-4),
|
|
1108
|
+
apiKeyHint: p.dshProvider ? '由 DSH 管理' : p.apiKeyEnv ? '由环境变量提供' : String(p.apiKey || '').length <= 8 ? '****' : String(p.apiKey).slice(0, 3) + '****' + String(p.apiKey).slice(-4),
|
|
1108
1109
|
models: models.providerModelsWithDirectory(p).map((m) => ({
|
|
1109
1110
|
id: m.id,
|
|
1110
1111
|
capabilities: m.capabilities,
|
|
@@ -1154,11 +1155,11 @@ register('providers_check_comfy_workflow', async (_host, args, { signal }) => {
|
|
|
1154
1155
|
const file = path.join(store.irisHome(), 'providers.json'), before = fs.readFileSync(file);
|
|
1155
1156
|
const workflow = compileComfyWorkflow(args.workflow !== undefined ? args.workflow : route.comfyWorkflow,
|
|
1156
1157
|
{ workflow_parameters: args.workflow_parameters });
|
|
1158
|
+
const scrub = createProviderSecretScrubber([route]);
|
|
1157
1159
|
const report = await inspectComfyWorkflow(createComfyUiClient(route), workflow, { signal });
|
|
1158
1160
|
signal?.throwIfAborted();
|
|
1159
1161
|
if (!fs.readFileSync(file).equals(before)) throw Object.assign(new Error('检查期间配置已变化,请重新检查'), { code: 'IRIS_CONFIG_CHANGED' });
|
|
1160
|
-
|
|
1161
|
-
return { ok: true, report: key ? JSON.parse(JSON.stringify(report).split(key).join('[redacted]')) : report };
|
|
1162
|
+
return { ok: true, report: scrub(report) };
|
|
1162
1163
|
});
|
|
1163
1164
|
|
|
1164
1165
|
/** 新增/更新供应商 */
|
package/lib/adapters.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* 所有函数只收显式 key;错误统一 throw Error(人话)。
|
|
9
9
|
*/
|
|
10
10
|
import fs from 'node:fs';
|
|
11
|
+
import { inheritedProviderFetch } from './provider-network.js';
|
|
11
12
|
import path from 'node:path';
|
|
12
13
|
import { Readable } from 'node:stream';
|
|
13
14
|
import { pipeline } from 'node:stream/promises';
|
|
@@ -146,10 +147,10 @@ function transportFetchError(err, response) {
|
|
|
146
147
|
}
|
|
147
148
|
|
|
148
149
|
/** 只包装网络读取;HTTP 业务错误和图片协议校验由调用方处理。 */
|
|
149
|
-
async function fetchImageJson(url, options) {
|
|
150
|
+
async function fetchImageJson(url, options, fetchPort) {
|
|
150
151
|
let response;
|
|
151
152
|
try {
|
|
152
|
-
response = await
|
|
153
|
+
response = await fetchPort(url, options);
|
|
153
154
|
return { res: response, body: await readJson(response, response.ok ? PROVIDER_RESPONSE_LIMITS.mediaJson : PROVIDER_RESPONSE_LIMITS.json) };
|
|
154
155
|
} catch (err) {
|
|
155
156
|
if (err instanceof ProviderContractError) throw err;
|
|
@@ -191,11 +192,12 @@ export function applyImageSize({ protocol, mode, size }) {
|
|
|
191
192
|
}
|
|
192
193
|
|
|
193
194
|
/** 新版多模态同步协议:响应中直接返回图片 URL。 */
|
|
194
|
-
export async function generateImageMultimodal({ key, baseUrl, model, prompt, image, size, n = 1, timeoutMs = T_SYNC_GEN, signal }) {
|
|
195
|
+
export async function generateImageMultimodal({ key, baseUrl, model, prompt, image, size, n = 1, timeoutMs = T_SYNC_GEN, signal, fetchPort = inheritedProviderFetch }) {
|
|
195
196
|
const apiBase = dashscopeApiBase(baseUrl);
|
|
196
197
|
const requestSize = applyImageSize({ protocol: 'dashscope', mode: 'multimodal-sync', size });
|
|
197
198
|
const { res, body } = await fetchImageJson(`${apiBase}/services/aigc/multimodal-generation/generation`, {
|
|
198
199
|
method: 'POST',
|
|
200
|
+
redirect: 'error',
|
|
199
201
|
headers: authHeaders(key),
|
|
200
202
|
signal: timeoutSignal(timeoutMs, signal),
|
|
201
203
|
body: JSON.stringify({
|
|
@@ -206,7 +208,7 @@ export async function generateImageMultimodal({ key, baseUrl, model, prompt, ima
|
|
|
206
208
|
] }] },
|
|
207
209
|
parameters: { ...(requestSize ? { size: requestSize } : {}), n: Number(n) || 1 }
|
|
208
210
|
})
|
|
209
|
-
});
|
|
211
|
+
}, fetchPort);
|
|
210
212
|
if (!res.ok) throw providerError(res.status, body, '生成失败', res.headers);
|
|
211
213
|
const urls = [];
|
|
212
214
|
for (const choice of (body && body.output && body.output.choices) || []) {
|
|
@@ -226,15 +228,16 @@ export async function startImageGeneration(opts) {
|
|
|
226
228
|
}
|
|
227
229
|
return { remoteTaskId: await submitImage(opts), urls: [] };
|
|
228
230
|
}
|
|
229
|
-
export async function submitImage({ key, baseUrl, model, prompt, size, n = 1, timeoutMs = T_SUBMIT, signal }) {
|
|
231
|
+
export async function submitImage({ key, baseUrl, model, prompt, size, n = 1, timeoutMs = T_SUBMIT, signal, fetchPort = inheritedProviderFetch }) {
|
|
230
232
|
const apiBase = dashscopeApiBase(baseUrl);
|
|
231
233
|
const requestSize = applyImageSize({ protocol: 'dashscope', mode: 'legacy-async', size });
|
|
232
234
|
const { res, body } = await fetchImageJson(`${apiBase}/services/aigc/text2image/image-synthesis`, {
|
|
233
235
|
method: 'POST',
|
|
236
|
+
redirect: 'error',
|
|
234
237
|
headers: { ...authHeaders(key), 'X-DashScope-Async': 'enable' },
|
|
235
238
|
signal: timeoutSignal(timeoutMs, signal),
|
|
236
239
|
body: JSON.stringify({ model, input: { prompt }, parameters: { ...(requestSize ? { size: requestSize } : {}), n: Number(n) || 1 } })
|
|
237
|
-
});
|
|
240
|
+
}, fetchPort);
|
|
238
241
|
if (!res.ok) throw providerError(res.status, body, '提交失败', res.headers);
|
|
239
242
|
const taskId = body && body.output && body.output.task_id;
|
|
240
243
|
if (!taskId) throw new Error('提交成功但未返回 task_id');
|
|
@@ -250,7 +253,7 @@ export async function submitImage({ key, baseUrl, model, prompt, size, n = 1, ti
|
|
|
250
253
|
* - 其他(t2v/i2v):POST /services/aigc/video-generation/video-synthesis,
|
|
251
254
|
* imgDataUrl 支持公网 URL 或 data:image base64。
|
|
252
255
|
*/
|
|
253
|
-
export async function submitVideo({ key, baseUrl, model, prompt, imgDataUrl, size = '1280*720', duration, audioUrl, resolution, timeoutMs = T_SUBMIT, signal }) {
|
|
256
|
+
export async function submitVideo({ key, baseUrl, model, prompt, imgDataUrl, size = '1280*720', duration, audioUrl, resolution, timeoutMs = T_SUBMIT, signal, fetchPort = inheritedProviderFetch }) {
|
|
254
257
|
const apiBase = dashscopeApiBase(baseUrl);
|
|
255
258
|
const isS2V = /s2v/i.test(model || '');
|
|
256
259
|
let url, input, parameters;
|
|
@@ -267,11 +270,12 @@ export async function submitVideo({ key, baseUrl, model, prompt, imgDataUrl, siz
|
|
|
267
270
|
url = `${apiBase}/services/aigc/video-generation/video-synthesis`;
|
|
268
271
|
input = { prompt };
|
|
269
272
|
if (imgDataUrl) input.img_url = imgDataUrl;
|
|
270
|
-
parameters = { size };
|
|
273
|
+
parameters = { size: typeof size === 'string' ? size.replace(/^(\d+)[xX](\d+)$/, '$1*$2') : size };
|
|
271
274
|
if (duration) parameters.duration = Number(duration);
|
|
272
275
|
}
|
|
273
|
-
const res = await
|
|
276
|
+
const res = await fetchPort(url, {
|
|
274
277
|
method: 'POST',
|
|
278
|
+
redirect: 'error',
|
|
275
279
|
headers,
|
|
276
280
|
signal: timeoutSignal(timeoutMs, signal),
|
|
277
281
|
body: JSON.stringify({ model, input, parameters })
|
|
@@ -289,7 +293,7 @@ export async function submitVideo({ key, baseUrl, model, prompt, imgDataUrl, siz
|
|
|
289
293
|
* 返回 oss:// 形式 URL;调用模型时请求头须带 X-DashScope-OssResourceResolve: enable
|
|
290
294
|
* (submitVideo 的 s2v 分支已自动处理)。
|
|
291
295
|
*/
|
|
292
|
-
export async function uploadTempFile({ key, baseUrl, model, filePath, bytes, filename, timeoutMs = T_UPLOAD, signal }) {
|
|
296
|
+
export async function uploadTempFile({ key, baseUrl, model, filePath, bytes, filename, timeoutMs = T_UPLOAD, signal, fetchPort = inheritedProviderFetch }) {
|
|
293
297
|
if ((filePath === undefined) === (bytes === undefined)) throw new TypeError('上传须且只能提供本地路径或明确字节');
|
|
294
298
|
const name = filePath !== undefined ? path.basename(filePath) : filename;
|
|
295
299
|
if (typeof name !== 'string' || !name || name.length > 128 || /[\/\\\0]/.test(name)
|
|
@@ -298,7 +302,7 @@ export async function uploadTempFile({ key, baseUrl, model, filePath, bytes, fil
|
|
|
298
302
|
const u = new URL(`${apiBase}/uploads`);
|
|
299
303
|
u.searchParams.set('action', 'getPolicy');
|
|
300
304
|
u.searchParams.set('model', model);
|
|
301
|
-
const pres = await
|
|
305
|
+
const pres = await fetchPort(u, { headers: authHeaders(key), signal: timeoutSignal(timeoutMs, signal) });
|
|
302
306
|
const pbody = await readJson(pres);
|
|
303
307
|
if (!pres.ok || !pbody || !pbody.data) throw providerError(pres.status, pbody, '获取上传凭证失败', pres.headers);
|
|
304
308
|
const pol = pbody.data;
|
|
@@ -312,7 +316,8 @@ export async function uploadTempFile({ key, baseUrl, model, filePath, bytes, fil
|
|
|
312
316
|
fd.append('key', keyPath);
|
|
313
317
|
fd.append('success_action_status', '200');
|
|
314
318
|
fd.append('file', new Blob([bytes ?? fs.readFileSync(filePath)]), name);
|
|
315
|
-
const up = await
|
|
319
|
+
const up = await fetchPort(pol.upload_host, { method: 'POST', redirect: 'error', body: fd, signal: timeoutSignal(timeoutMs, signal) });
|
|
320
|
+
await up.body?.cancel();
|
|
316
321
|
if (up.status !== 200) throw new Error(`临时上传失败:HTTP ${up.status}`);
|
|
317
322
|
return 'oss://' + keyPath;
|
|
318
323
|
}
|
|
@@ -320,9 +325,9 @@ export async function uploadTempFile({ key, baseUrl, model, filePath, bytes, fil
|
|
|
320
325
|
const TERMINAL = new Set(['SUCCEEDED', 'FAILED', 'CANCELED', 'UNKNOWN']);
|
|
321
326
|
|
|
322
327
|
/** @returns {{done:boolean, ok:boolean, urls:string[], message?:string}} */
|
|
323
|
-
export async function pollTask({ key, baseUrl, remoteTaskId, timeoutMs = T_POLL, signal }) {
|
|
328
|
+
export async function pollTask({ key, baseUrl, remoteTaskId, timeoutMs = T_POLL, signal, fetchPort = inheritedProviderFetch }) {
|
|
324
329
|
const apiBase = dashscopeApiBase(baseUrl);
|
|
325
|
-
const res = await
|
|
330
|
+
const res = await fetchPort(`${apiBase}/tasks/${encodeURIComponent(remoteTaskId)}`, {
|
|
326
331
|
headers: authHeaders(key),
|
|
327
332
|
signal: timeoutSignal(timeoutMs, signal)
|
|
328
333
|
});
|
|
@@ -363,10 +368,11 @@ export async function pollTask({ key, baseUrl, remoteTaskId, timeoutMs = T_POLL,
|
|
|
363
368
|
}
|
|
364
369
|
|
|
365
370
|
/* ---------------- DashScope:qwen-tts(同步) ---------------- */
|
|
366
|
-
export async function synthesizeTts({ key, baseUrl, model = 'qwen-tts-latest', text, voice = 'Cherry', timeoutMs = T_SYNC_GEN, signal }) {
|
|
371
|
+
export async function synthesizeTts({ key, baseUrl, model = 'qwen-tts-latest', text, voice = 'Cherry', timeoutMs = T_SYNC_GEN, signal, fetchPort = inheritedProviderFetch }) {
|
|
367
372
|
const apiBase = dashscopeApiBase(baseUrl);
|
|
368
|
-
const res = await
|
|
373
|
+
const res = await fetchPort(`${apiBase}/services/aigc/multimodal-generation/generation`, {
|
|
369
374
|
method: 'POST',
|
|
375
|
+
redirect: 'error',
|
|
370
376
|
headers: authHeaders(key),
|
|
371
377
|
signal: timeoutSignal(timeoutMs, signal),
|
|
372
378
|
body: JSON.stringify({ model, input: { text, voice } })
|
|
@@ -389,15 +395,16 @@ export async function synthesizeTts({ key, baseUrl, model = 'qwen-tts-latest', t
|
|
|
389
395
|
}
|
|
390
396
|
|
|
391
397
|
/* ---------------- OpenAI Images 兼容(同步 base64/url) ---------------- */
|
|
392
|
-
export async function openAiGenerateImage({ key, baseUrl, model, prompt, size = '1024x1024', n = 1, timeoutMs = T_SYNC_GEN, signal }) {
|
|
398
|
+
export async function openAiGenerateImage({ key, baseUrl, model, prompt, size = '1024x1024', n = 1, timeoutMs = T_SYNC_GEN, signal, fetchPort = inheritedProviderFetch }) {
|
|
393
399
|
const base = String(baseUrl || '').replace(/\/+$/, '');
|
|
394
400
|
const requestSize = applyImageSize({ protocol: 'openai-images', size });
|
|
395
401
|
const { res, body } = await fetchImageJson(`${base}/images/generations`, {
|
|
396
402
|
method: 'POST',
|
|
403
|
+
redirect: 'error',
|
|
397
404
|
headers: authHeaders(key),
|
|
398
405
|
signal: timeoutSignal(timeoutMs, signal),
|
|
399
406
|
body: JSON.stringify({ model, prompt, size: requestSize, n: Number(n) || 1 })
|
|
400
|
-
});
|
|
407
|
+
}, fetchPort);
|
|
401
408
|
if (!res.ok) throw providerError(res.status, body, '生成失败', res.headers);
|
|
402
409
|
const items = (body && Array.isArray(body.data) && body.data) || [];
|
|
403
410
|
const out = [];
|
|
@@ -457,7 +464,7 @@ function chatImageItems(body, label = '聊天生图') {
|
|
|
457
464
|
return items;
|
|
458
465
|
}
|
|
459
466
|
|
|
460
|
-
export async function openAiChatGenerateImage({ key, baseUrl, model, prompt, image, size, n = 1, timeoutMs = T_SYNC_GEN, signal }) {
|
|
467
|
+
export async function openAiChatGenerateImage({ key, baseUrl, model, prompt, image, size, n = 1, timeoutMs = T_SYNC_GEN, signal, fetchPort = inheritedProviderFetch }) {
|
|
461
468
|
if (typeof size === 'string' && size.trim()) {
|
|
462
469
|
throw new ProviderContractError('聊天生图协议不支持 size 参数;请在提示词中描述所需尺寸', {
|
|
463
470
|
stage: 'validate', category: 'invalid_request', acceptance: 'not_accepted'
|
|
@@ -469,16 +476,16 @@ export async function openAiChatGenerateImage({ key, baseUrl, model, prompt, ima
|
|
|
469
476
|
{ type: 'image_url', image_url: { url: 'data:' + image.mediaType + ';base64,' + Buffer.from(image.bytes).toString('base64') } }
|
|
470
477
|
] : prompt;
|
|
471
478
|
const { res, body } = await fetchImageJson(`${base}/chat/completions`, {
|
|
472
|
-
method: 'POST', headers: authHeaders(key), signal: timeoutSignal(timeoutMs, signal),
|
|
479
|
+
method: 'POST', redirect: 'error', headers: authHeaders(key), signal: timeoutSignal(timeoutMs, signal),
|
|
473
480
|
body: JSON.stringify({ model, stream: false, messages: [{ role: 'user', content }],
|
|
474
481
|
...(Number(n) > 1 ? { n: Number(n) } : {}) })
|
|
475
|
-
});
|
|
482
|
+
}, fetchPort);
|
|
476
483
|
if (!res.ok) throw providerError(res.status, body, '聊天生图失败', res.headers);
|
|
477
484
|
return chatImageItems(body);
|
|
478
485
|
}
|
|
479
486
|
|
|
480
487
|
/** 同步 Responses 生图:主模型由 model 指定,图片工具模型沿用服务端默认。 */
|
|
481
|
-
export async function openAiResponsesGenerateImage({ key, baseUrl, model, prompt, size, n = 1, timeoutMs = T_SYNC_GEN, signal }) {
|
|
488
|
+
export async function openAiResponsesGenerateImage({ key, baseUrl, model, prompt, size, n = 1, timeoutMs = T_SYNC_GEN, signal, fetchPort = inheritedProviderFetch }) {
|
|
482
489
|
if (Number(n) !== 1) {
|
|
483
490
|
throw new ProviderContractError('Responses 生图当前只支持 n=1', {
|
|
484
491
|
stage: 'validate', category: 'invalid_request', acceptance: 'not_accepted'
|
|
@@ -487,11 +494,11 @@ export async function openAiResponsesGenerateImage({ key, baseUrl, model, prompt
|
|
|
487
494
|
const base = String(baseUrl || '').replace(/\/+$/, '');
|
|
488
495
|
const requestSize = applyImageSize({ protocol: 'openai-responses-images', size });
|
|
489
496
|
const { res, body } = await fetchImageJson(`${base}/responses`, {
|
|
490
|
-
method: 'POST', headers: authHeaders(key), signal: timeoutSignal(timeoutMs, signal),
|
|
497
|
+
method: 'POST', redirect: 'error', headers: authHeaders(key), signal: timeoutSignal(timeoutMs, signal),
|
|
491
498
|
body: JSON.stringify({ model, input: prompt, stream: false, store: false, background: false,
|
|
492
499
|
tools: [{ type: 'image_generation', action: 'generate', ...(requestSize ? { size: requestSize } : {}) }],
|
|
493
500
|
tool_choice: { type: 'image_generation' } })
|
|
494
|
-
});
|
|
501
|
+
}, fetchPort);
|
|
495
502
|
if (!res.ok) throw providerError(res.status, body, 'Responses 生图失败', res.headers);
|
|
496
503
|
if (body?.status !== 'completed') throw imageResponseError('Responses 生图未返回同步完成结果;禁止自动重提');
|
|
497
504
|
const output = Array.isArray(body.output) ? body.output : [];
|
|
@@ -515,7 +522,7 @@ export async function openAiResponsesGenerateImage({ key, baseUrl, model, prompt
|
|
|
515
522
|
* 普通 OpenAI 兼容端点维持 GET /models 与字符串 id 返回形状。
|
|
516
523
|
* @returns {Promise<Array<string|{id:string,capabilities?:string[]}>>}
|
|
517
524
|
*/
|
|
518
|
-
export async function listModels({ key, baseUrl, timeoutMs = T_SUBMIT, signal }) {
|
|
525
|
+
export async function listModels({ key, baseUrl, timeoutMs = T_SUBMIT, signal, fetchPort = inheritedProviderFetch }) {
|
|
519
526
|
const base = String(baseUrl || '').trim().replace(/\/+$/, '');
|
|
520
527
|
if (!base) throw new Error('iris: 供应商缺 baseUrl,无法发现模型');
|
|
521
528
|
const dashscope = isDashScopeBaseUrl(base);
|
|
@@ -530,7 +537,7 @@ export async function listModels({ key, baseUrl, timeoutMs = T_SUBMIT, signal })
|
|
|
530
537
|
url.searchParams.set('page_no', String(page));
|
|
531
538
|
url.searchParams.set('page_size', '100');
|
|
532
539
|
}
|
|
533
|
-
const res = await
|
|
540
|
+
const res = await fetchPort(url, { headers: authHeaders(key), signal: timeoutSignal(timeoutMs, signal) });
|
|
534
541
|
const body = await readJson(res);
|
|
535
542
|
if (!res.ok) throw providerError(res.status, body, '模型发现失败', res.headers);
|
|
536
543
|
if (!body || body.success === false) throw new ProviderContractError('模型目录响应无效,已保留上次完整目录', {
|
|
@@ -593,14 +600,17 @@ export async function listModels({ key, baseUrl, timeoutMs = T_SUBMIT, signal })
|
|
|
593
600
|
* @param {AbortSignal} [signal] 取消传播
|
|
594
601
|
* @returns {Promise<string>} 完整回答文本
|
|
595
602
|
*/
|
|
596
|
-
export async function visionStream({ key, baseUrl, model = 'qwen-vl-plus', prompt, imageDataUrl, signal, timeoutMs = T_VISION }) {
|
|
603
|
+
export async function visionStream({ key, baseUrl, model = 'qwen-vl-plus', prompt, imageDataUrl, signal, timeoutMs = T_VISION, fetchPort = inheritedProviderFetch }) {
|
|
597
604
|
const base = String(baseUrl || '').trim().replace(/\/+$/, '');
|
|
605
|
+
const requestSignal = timeoutSignal(timeoutMs, signal);
|
|
606
|
+
requestSignal.throwIfAborted();
|
|
598
607
|
let res;
|
|
599
608
|
try {
|
|
600
|
-
res = await
|
|
609
|
+
res = await fetchPort(`${base}/chat/completions`, {
|
|
601
610
|
method: 'POST',
|
|
611
|
+
redirect: 'error',
|
|
602
612
|
headers: authHeaders(key),
|
|
603
|
-
signal:
|
|
613
|
+
signal: requestSignal,
|
|
604
614
|
body: JSON.stringify({
|
|
605
615
|
model,
|
|
606
616
|
stream: true,
|
|
@@ -616,8 +626,10 @@ export async function visionStream({ key, baseUrl, model = 'qwen-vl-plus', promp
|
|
|
616
626
|
})
|
|
617
627
|
});
|
|
618
628
|
} catch (err) {
|
|
629
|
+
requestSignal.throwIfAborted();
|
|
619
630
|
throw networkError(err); // 网络层失败归类为 network
|
|
620
631
|
}
|
|
632
|
+
if (requestSignal.aborted) { await res.body?.cancel(); requestSignal.throwIfAborted(); }
|
|
621
633
|
if (!res.ok) {
|
|
622
634
|
const text = await readProviderText(res, { maxBytes: PROVIDER_RESPONSE_LIMITS.json, stage: 'response', acceptance: 'unknown' });
|
|
623
635
|
let body = null;
|
|
@@ -627,8 +639,8 @@ export async function visionStream({ key, baseUrl, model = 'qwen-vl-plus', promp
|
|
|
627
639
|
let full = '';
|
|
628
640
|
const decoder = new TextDecoder();
|
|
629
641
|
let buffer = '';
|
|
630
|
-
for await (const chunk of res.
|
|
631
|
-
|
|
642
|
+
for await (const chunk of boundedProviderBody(res, { maxBytes: PROVIDER_RESPONSE_LIMITS.json, stage: 'response', acceptance: 'unknown' })) {
|
|
643
|
+
requestSignal.throwIfAborted();
|
|
632
644
|
buffer += decoder.decode(chunk, { stream: true });
|
|
633
645
|
let idx;
|
|
634
646
|
while ((idx = buffer.indexOf('\n')) >= 0) {
|
|
@@ -646,6 +658,7 @@ export async function visionStream({ key, baseUrl, model = 'qwen-vl-plus', promp
|
|
|
646
658
|
}
|
|
647
659
|
}
|
|
648
660
|
}
|
|
661
|
+
requestSignal.throwIfAborted();
|
|
649
662
|
return full.trim();
|
|
650
663
|
}
|
|
651
664
|
|
|
@@ -660,15 +673,15 @@ export function extFromUrl(url, fallback = 'bin') {
|
|
|
660
673
|
return fallback;
|
|
661
674
|
}
|
|
662
675
|
|
|
663
|
-
export async function downloadTo(url, filePath, { timeoutMs = T_DOWNLOAD, signal, maxBytes = PROVIDER_RESPONSE_LIMITS.video } = {}) {
|
|
664
|
-
|
|
676
|
+
export async function downloadTo(url, filePath, { timeoutMs = T_DOWNLOAD, signal, maxBytes = PROVIDER_RESPONSE_LIMITS.video, fetchPort = inheritedProviderFetch } = {}) {
|
|
677
|
+
ensurePrivateDir(path.dirname(filePath));
|
|
678
|
+
const tmp = privateSibling(filePath, 'download');
|
|
679
|
+
const res = await fetchPort(url, { signal: timeoutSignal(timeoutMs, signal) });
|
|
665
680
|
if (!res.ok) {
|
|
666
681
|
await res.body?.cancel();
|
|
667
682
|
throw Object.assign(new Error(`下载结果失败:HTTP ${res.status}`), { stage: 'download', category: 'provider', httpStatus: res.status, acceptance: 'accepted' });
|
|
668
683
|
}
|
|
669
684
|
if (!res.body) throw new Error('下载结果失败:响应没有内容');
|
|
670
|
-
ensurePrivateDir(path.dirname(filePath));
|
|
671
|
-
const tmp = privateSibling(filePath, 'download');
|
|
672
685
|
try {
|
|
673
686
|
const output = fs.createWriteStream(tmp, { flags: 'wx', mode: 0o600 });
|
|
674
687
|
const closed = new Promise(resolve => output.once('close', resolve));
|
|
@@ -688,7 +701,7 @@ export async function downloadTo(url, filePath, { timeoutMs = T_DOWNLOAD, signal
|
|
|
688
701
|
* @param {{key:string, model:string, audioUrl:string}} opts audioUrl 为 oss:// 或公网 URL
|
|
689
702
|
* @returns {Promise<string>} remoteTaskId
|
|
690
703
|
*/
|
|
691
|
-
export async function submitTranscription({ key, baseUrl, model = 'qwen-audio-3.0-asr-flash-filetrans', audioUrl, timeoutMs = T_SUBMIT, signal }) {
|
|
704
|
+
export async function submitTranscription({ key, baseUrl, model = 'qwen-audio-3.0-asr-flash-filetrans', audioUrl, timeoutMs = T_SUBMIT, signal, fetchPort = inheritedProviderFetch }) {
|
|
692
705
|
const apiBase = dashscopeApiBase(baseUrl);
|
|
693
706
|
const headers = { ...authHeaders(key), 'X-DashScope-Async': 'enable' };
|
|
694
707
|
if (/^oss:\/\//.test(audioUrl || '')) headers['X-DashScope-OssResourceResolve'] = 'enable';
|
|
@@ -696,8 +709,9 @@ export async function submitTranscription({ key, baseUrl, model = 'qwen-audio-3.
|
|
|
696
709
|
const input = /^qwen3-asr-.*filetrans/i.test(model || '')
|
|
697
710
|
? { file_url: audioUrl }
|
|
698
711
|
: { file_urls: [audioUrl] };
|
|
699
|
-
const res = await
|
|
712
|
+
const res = await fetchPort(`${apiBase}/services/audio/asr/transcription`, {
|
|
700
713
|
method: 'POST',
|
|
714
|
+
redirect: 'error',
|
|
701
715
|
headers,
|
|
702
716
|
signal: timeoutSignal(timeoutMs, signal),
|
|
703
717
|
body: JSON.stringify({ model, input })
|
|
@@ -728,9 +742,9 @@ export function transcriptionText(payload) {
|
|
|
728
742
|
}
|
|
729
743
|
|
|
730
744
|
/** 轮询 ASR 任务;正式结果位于 results[].transcription_url 指向的 JSON。 */
|
|
731
|
-
export async function pollTranscriptionTask({ key, baseUrl, remoteTaskId, timeoutMs = T_POLL, signal }) {
|
|
745
|
+
export async function pollTranscriptionTask({ key, baseUrl, remoteTaskId, timeoutMs = T_POLL, signal, fetchPort = inheritedProviderFetch }) {
|
|
732
746
|
const apiBase = dashscopeApiBase(baseUrl);
|
|
733
|
-
const res = await
|
|
747
|
+
const res = await fetchPort(`${apiBase}/tasks/${encodeURIComponent(remoteTaskId)}`, {
|
|
734
748
|
headers: authHeaders(key),
|
|
735
749
|
signal: timeoutSignal(timeoutMs, signal)
|
|
736
750
|
});
|
|
@@ -751,8 +765,11 @@ export async function pollTranscriptionTask({ key, baseUrl, remoteTaskId, timeou
|
|
|
751
765
|
if (typeof result.transcription_text === 'string') text += result.transcription_text;
|
|
752
766
|
else if (typeof result.text === 'string') text += result.text;
|
|
753
767
|
if (status === 'SUCCEEDED' && result.subtask_status !== 'FAILED' && typeof result.transcription_url === 'string') {
|
|
754
|
-
const transcriptRes = await
|
|
755
|
-
if (!transcriptRes.ok)
|
|
768
|
+
const transcriptRes = await fetchPort(result.transcription_url, { signal: timeoutSignal(timeoutMs, signal) });
|
|
769
|
+
if (!transcriptRes.ok) {
|
|
770
|
+
await transcriptRes.body?.cancel();
|
|
771
|
+
throw new Error(`下载转写结果失败:HTTP ${transcriptRes.status}`);
|
|
772
|
+
}
|
|
756
773
|
const transcriptBody = await readJson(transcriptRes);
|
|
757
774
|
if (!transcriptBody) throw new Error('下载转写结果失败:响应不是 JSON');
|
|
758
775
|
text += transcriptionText(transcriptBody);
|