@mokuyoaxis/dsh-iris 0.1.0 → 0.1.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 +31 -1
- package/README.md +28 -9
- package/docs/ROADMAP.md +31 -20
- package/docs/releases/0.1.1.md +51 -0
- package/lib/actions.js +33 -27
- package/lib/adapters.js +42 -26
- package/lib/api.js +31 -9
- package/lib/capability.js +1 -1
- package/lib/client.js +26 -17
- package/lib/config.js +56 -22
- package/lib/guard.js +37 -24
- package/lib/index.js +10 -8
- package/lib/locate.js +1 -1
- package/lib/media-probe.js +4 -4
- package/lib/media.js +1 -1
- package/lib/private-storage.js +77 -0
- package/lib/provider-protocol.js +25 -0
- package/lib/render.js +3 -2
- package/lib/tasks.js +21 -13
- package/lib/vision.js +2 -2
- package/package.json +19 -15
- package/user_guide.md +34 -9
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,35 @@
|
|
|
6
6
|
|
|
7
7
|
当前没有尚未发布的用户可见变更。
|
|
8
8
|
|
|
9
|
+
## [0.1.1] - 2026-09-05
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- 明确 ai-paint 是维护者未公开的本地前身而非用户依赖;插件默认启动不再读取其固定路径,仅在用户显式设置 `IRIS_IMPORT_WORKBENCH_CONFIG` 时由宿主本地一次性导入,已有供应商时跳过。
|
|
14
|
+
- `npm test` 使用 Node.js 调度器,移除 POSIX Shell 循环,保留逐文件进程隔离和失败即停。
|
|
15
|
+
- 对齐公开路线图与 `v0.1.0` 标签,区分当前 DSH 插件能力和未来宿主无关 Core/CLI/工作台。
|
|
16
|
+
- 工作台新增安全的媒体协议自动判断与显式选择;模型实测改为按能力触发,真实调用前确认,视频与转写不再提交空样本探针。
|
|
17
|
+
- 测试临时目录改用系统 API 并自动清理;`ffmpeg` 探测不再依赖 POSIX `which`。
|
|
18
|
+
- 增加 Linux/Windows × Node.js 20.10/22 CI 矩阵和发布前完整测试钩子。
|
|
19
|
+
- 新增标签驱动的 Release 工作流:推送 `vX.Y.Z` 标签即自动跑全量测试、校验标签与 package.json 版本一致、创建附 tarball 的 GitHub Release 并发布 npm(含 provenance;未配置 `NPM_TOKEN` 时仅跳过 npm 发布)。
|
|
20
|
+
- Node.js 最低版本调整为 20.10;20.9 无法解析当前图片依赖使用的 JSON import attributes。
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- 修复 DSH 0.1.2+ Web 客户端按完整 npm 包名校验时,Iris 仍以旧短名注册而导致整个插件组合包加载失败的问题;同时将客户端依赖声明对齐新版的 Renderer/Session 服务。
|
|
25
|
+
- 旧配置为 `null` 或供应商凭据字段类型错误时,跳过无效内容而不使导入崩溃。
|
|
26
|
+
- 语法合法但根节点、供应商、模型、任务或分配结构错误的持久化文件现在会被隔离并安全重建,不再静默丢失后续更新。
|
|
27
|
+
- 大媒体下载改为流式原子落盘,避免把完整视频一次性载入内存,并清理失败的临时文件。
|
|
28
|
+
- Windows CI:Skill 测试先归一化 CRLF 再解析 frontmatter,并新增 `.gitattributes` 强制所有平台以 LF 检出(Windows runner 默认 `core.autocrlf=true` 曾致 `npm test` 失败)。
|
|
29
|
+
- Windows CI:`tests/pixels.mjs` 改用 `fileURLToPath` 解析模块路径;`URL.pathname` 在 Windows 上返回 `/D:/...`,拼出的非法临时目录路径使该测试进程直接崩溃。
|
|
30
|
+
- 上传接口竞态:写入流改为惰性创建、`.part` 清理等待 fd 关闭,空上传或提前失败的请求不再可能残留孤儿 `.part` 文件。
|
|
31
|
+
|
|
32
|
+
### Security
|
|
33
|
+
|
|
34
|
+
- DashScope 凭据在网络请求发出前绑定到阿里云官方 HTTPS 域名,阻止错误协议配置把 API Key 发给第三方端点。
|
|
35
|
+
- POSIX 上统一以 `0700` 目录和 `0600` 文件保存 Iris 配置、任务、上传及产物;启动时收紧既有 Iris 树且不跟随符号链接。
|
|
36
|
+
- `/iris/*` 默认只接受回环 Host;LAN/反向代理需显式配置 `IRIS_TRUSTED_HOSTS`,公开文档明确该列表不替代身份认证。
|
|
37
|
+
|
|
9
38
|
## [0.1.0] - 2026-09-04
|
|
10
39
|
|
|
11
40
|
### Added
|
|
@@ -49,5 +78,6 @@
|
|
|
49
78
|
- 上传采用流式限额、临时 `.part` 文件和原子落盘,失败时会清理未完成文件。
|
|
50
79
|
- 修改状态的路由拒绝明确的跨站请求,媒体文件继续使用随机能力令牌访问。
|
|
51
80
|
|
|
52
|
-
[Unreleased]: https://github.com/mokuyoaxis/dsh-iris/compare/v0.1.
|
|
81
|
+
[Unreleased]: https://github.com/mokuyoaxis/dsh-iris/compare/v0.1.1...HEAD
|
|
82
|
+
[0.1.1]: https://github.com/mokuyoaxis/dsh-iris/compare/v0.1.0...v0.1.1
|
|
53
83
|
[0.1.0]: https://github.com/mokuyoaxis/dsh-iris/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
<h1 align="center">dsh-iris</h1>
|
|
6
6
|
|
|
7
|
-
<p align="center"><strong>DeepSeek Harness
|
|
7
|
+
<p align="center"><strong>Iris 多模态生产运行时 · 当前通过 DeepSeek Harness 插件使用</strong></p>
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
10
10
|
<a href="#deepseek-harness-适配"><img alt="DeepSeek Harness compatible" src="https://img.shields.io/badge/DeepSeek%20Harness-compatible-4D6BFE.svg?style=flat-square"></a>
|
|
11
|
-
<a href="
|
|
12
|
-
<a href="https://nodejs.org/"><img alt="Node.js 20.
|
|
13
|
-
<a href="
|
|
11
|
+
<a href="https://www.npmjs.com/package/@mokuyoaxis/dsh-iris"><img alt="npm version" src="https://img.shields.io/npm/v/%40mokuyoaxis%2Fdsh-iris.svg?style=flat-square"></a>
|
|
12
|
+
<a href="https://nodejs.org/"><img alt="Node.js 20.10 or newer" src="https://img.shields.io/badge/Node.js-%3E%3D20.10-339933.svg?style=flat-square&logo=nodedotjs&logoColor=white"></a>
|
|
13
|
+
<a href="https://github.com/mokuyoaxis/dsh-iris/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/mokuyoaxis/dsh-iris/actions/workflows/ci.yml/badge.svg"></a>
|
|
14
14
|
<a href="LICENSE"><img alt="MIT License" src="https://img.shields.io/badge/license-MIT-1689FF.svg?style=flat-square"></a>
|
|
15
15
|
</p>
|
|
16
16
|
|
|
@@ -18,9 +18,17 @@ dsh-iris 为 Agent 和 Iris 工作台提供图像、视频、语音与视觉理
|
|
|
18
18
|
|
|
19
19
|
项目目前处于早期版本,接口和配置格式仍可能随版本迭代调整。
|
|
20
20
|
|
|
21
|
+
Iris 的长期方向是可独立运行、可接入不同 Agent 宿主的媒体生产核心。DeepSeek Harness 是首个受支持宿主;当前版本仍需要 DSH,独立 Core、CLI 和工作台属于后续路线,尚未提供。
|
|
22
|
+
|
|
23
|
+
## 与 ai-paint 的关系
|
|
24
|
+
|
|
25
|
+
`ai-paint` 是 Iris 维护者本机未公开的前身项目,不是 Iris 的公开依赖,也不是用户需要下载、安装或自行创建的目录。没有该项目的用户直接按下文在 Iris 工作台配置供应商即可。
|
|
26
|
+
|
|
27
|
+
早期版本从维护者本机的 ai-paint 配置读取模型与凭据,既用于迁移前身项目,也让宿主进程可以在本地复用已有 API Key,避免为迁移而把密钥粘贴到 Agent 会话、命令行参数或文档中。0.1.1 将两者的配置彻底分离:Iris 只使用自己的配置;旧配置仅可通过把 `IRIS_IMPORT_WORKBENCH_CONFIG` 显式设为本地配置文件的绝对路径进行一次性导入,不会默认扫描 ai-paint,也不会在两边同步。该变量保存的是文件路径而不是 API Key,来源文件不会被修改;具体操作见[用户指南](user_guide.md#从旧工作台显式导入)。
|
|
28
|
+
|
|
21
29
|
## 最快开始
|
|
22
30
|
|
|
23
|
-
已经安装 DeepSeek Harness 且 `pnpm` 在 PATH 中时,把 Iris 加入 Web profile:
|
|
31
|
+
已经安装 DeepSeek Harness `0.1.2-rc.1` 且 `pnpm` 在 PATH 中时,把 Iris 加入 Web profile:
|
|
24
32
|
|
|
25
33
|
```bash
|
|
26
34
|
dsh plugin --profile web add @mokuyoaxis/dsh-iris
|
|
@@ -53,18 +61,20 @@ DSH 的 profile 与插件命令由[官方安装说明](https://github.com/deepse
|
|
|
53
61
|
|
|
54
62
|
## 使用前准备
|
|
55
63
|
|
|
56
|
-
- 满足所用 DeepSeek Harness 版本要求的 Node.js;dsh-iris 自身最低为 20.
|
|
64
|
+
- 满足所用 DeepSeek Harness 版本要求的 Node.js;dsh-iris 自身最低为 20.10
|
|
57
65
|
- 一个可用的 DeepSeek Harness 环境,以及 PATH 中的 `pnpm`
|
|
58
66
|
- 至少一个受支持的媒体或视觉服务供应商
|
|
59
67
|
- 使用视频抽帧和视频摘要时,需要系统提供 `ffmpeg` 与 `ffprobe`
|
|
60
68
|
|
|
61
|
-
安装插件后,在 Iris
|
|
69
|
+
安装插件后,在 Iris 工作台中添加供应商并为所需能力分配模型。供应商密钥保存在宿主侧的 Iris 配置中;POSIX 系统上 Iris 自有目录和文件分别收紧为 `0700` 与 `0600`,界面和接口不会返回完整密钥。Windows 的 mode 不等同于 ACL,仍需依赖当前用户目录和系统账户权限。
|
|
62
70
|
|
|
63
71
|
## DeepSeek Harness 适配
|
|
64
72
|
|
|
65
73
|
dsh-iris 按 DeepSeek Harness 插件形态提供服务端与 Web 客户端入口:服务端注册 14 个 Agent 工具,并复用宿主的工具、路由和生命周期服务;客户端通过 DSH 模块加载器接入设置页、会话输入区和全局悬浮层。插件不会启动独立服务或额外监听端口。
|
|
66
74
|
|
|
67
|
-
|
|
75
|
+
当前自动化测试覆盖插件装载、工具注册、客户端槽位和路由行为。0.1.1 已在 Linux ARM64 的干净与真实 Web profile 中,使用 DSH `0.1.2-rc.1`、Node.js `22.23.2` 完成宿主烟测;浏览器启动图、完整组合 bundle、Iris 客户端工厂和三个 UI 座位均已验证。Iris 自身仍以 Node.js `>=20.10` 为最低基线。
|
|
76
|
+
|
|
77
|
+
0.1.1 明确支持 DSH `>=0.1.2-rc.1 <0.1.3-0`,不再兼容 0.1.0/0.1.1 的旧客户端 Runtime。DSH 仍在快速演进,后续预览版须经验证后再扩大范围;兼容徽章不代表官方认证。若 DSH 要求更高 Node 版本,以 DSH 为准。
|
|
68
78
|
|
|
69
79
|
## 工具
|
|
70
80
|
|
|
@@ -126,6 +136,8 @@ Iris 支持三种文件来源,推荐顺序如下:
|
|
|
126
136
|
| `outputs/` | 生成和处理后的媒体文件 |
|
|
127
137
|
| `uploads/` | 浏览器上传的临时输入副本 |
|
|
128
138
|
|
|
139
|
+
0.1.1 首次装载时会收紧既有 `$DSH_HOME/iris/v1/` 树的 POSIX 权限,不修改文件内容,也不跟随符号链接。大媒体下载采用流式私有临时文件与原子替换,不再把整段视频载入内存。
|
|
140
|
+
|
|
129
141
|
异步任务会在后台轮询。插件重启后,可以继续接管仍在远端执行的任务。取消信号会传递到本地等待与轮询流程;是否能取消远端计算,取决于供应商接口。
|
|
130
142
|
|
|
131
143
|
音频和视频通过带随机令牌的 Iris 媒体链接访问。图片会尽量转存为 DSH 持久附件,方便在会话中继续使用。
|
|
@@ -133,7 +145,10 @@ Iris 支持三种文件来源,推荐顺序如下:
|
|
|
133
145
|
## 安全说明
|
|
134
146
|
|
|
135
147
|
- 供应商密钥不会通过状态接口返回。
|
|
136
|
-
-
|
|
148
|
+
- DashScope 媒体协议只允许把密钥发送到阿里云官方 HTTPS 域名;其他 Base URL 默认推断为 OpenAI Images 兼容协议。
|
|
149
|
+
- `/iris/*` 默认只接受回环 Host;LAN 或反向代理必须显式设置 `IRIS_TRUSTED_HOSTS`,修改状态的请求还会检查跨站来源。
|
|
150
|
+
- `IRIS_TRUSTED_HOSTS` 不是认证机制。对公网或不可信网络开放 DSH 时,必须在反向代理或宿主层配置身份认证与 HTTPS。
|
|
151
|
+
- 模型实测只按单项能力运行,并在真实供应商调用前确认;视频和转写不会用空样本自动提交付费探针。
|
|
137
152
|
- HTML 截图在不具备同源权限的沙箱页面中渲染,脚本和外部网络默认不可用。
|
|
138
153
|
- 媒体链接使用随机能力令牌,文件路径只从任务记录解析。
|
|
139
154
|
- Iris 不提供独立账号体系,多用户隔离和访问控制由 DeepSeek Harness 部署负责。
|
|
@@ -165,6 +180,10 @@ Iris 支持三种文件来源,推荐顺序如下:
|
|
|
165
180
|
|
|
166
181
|
测试覆盖配置合并、模型身份、任务生命周期、生成动作、HTTP/SSE 路由、客户端交互和安全边界。项目采用 ES Modules,不需要构建步骤。
|
|
167
182
|
|
|
183
|
+
测试调度使用 Node.js,逐文件启动独立进程,首个失败即停止,不依赖 Bash;临时目录使用系统 API 并在退出时清理。GitHub Actions 定义 Linux/Windows × Node.js 20.10/22 矩阵,发布前钩子会重新运行完整测试。原生 Windows/WSL 的 DSH 宿主烟测仍待实机验证。
|
|
184
|
+
|
|
185
|
+
发布由 `v*` 标签触发 `.github/workflows/release.yml`:跑完整测试后打包,用工作流内置 `GITHUB_TOKEN` 创建 GitHub Release(附 tarball,发布说明取自 CHANGELOG 对应章节),并在配置了 `NPM_TOKEN` secret 时发布到 npm。本机无需任何 GitHub 凭据。
|
|
186
|
+
|
|
168
187
|
## 文档
|
|
169
188
|
|
|
170
189
|
- [用户指南](user_guide.md)
|
package/docs/ROADMAP.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 路线图
|
|
2
2
|
|
|
3
|
-
dsh-iris
|
|
3
|
+
dsh-iris 已具备媒体生成、视觉处理和任务管理主流程。长期方向是宿主无关的多模态生产运行时,DSH 是首个受支持宿主。当前仍通过 DSH 插件使用,独立 Core、CLI 和工作台尚未实现。
|
|
4
4
|
|
|
5
5
|
变更详情见[变更记录](../CHANGELOG.md),完整工具清单见[README](../README.md)。
|
|
6
6
|
|
|
@@ -17,40 +17,50 @@ dsh-iris 已具备完整的媒体生成、视觉处理和任务管理主流程
|
|
|
17
17
|
- 面向 Agent 与用户的统一视频生成实现
|
|
18
18
|
- 两个仓库级 Agent Skills:UI 视觉验收与多步骤媒体编排
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
当前版本为 `v0.1.1`。该补丁完成配置独立、安全与工程收口,并把 Web loader 适配固定到已验证的 DSH `0.1.2-rc.1` 接口;更高预览版在进入支持范围前需要重新运行宿主 canary。
|
|
21
21
|
|
|
22
|
-
##
|
|
22
|
+
## 已完成:0.1.1
|
|
23
|
+
|
|
24
|
+
### 配置独立与工程收口
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
- 对齐公开文档和已发布标签,明确当前能力与未来方向。
|
|
27
|
+
- Iris 独立保存配置;ai-paint 仅作为维护者未公开的本地前身和可选迁移来源,不是用户依赖。停止默认读取,仅保留显式指定本地来源的一次性安全导入。
|
|
28
|
+
- 用 Node.js 调度测试,去除对 POSIX Shell 循环的依赖。
|
|
29
|
+
- 绑定供应商凭据与协议端点,统一私有存储权限和流式原子落盘,并验证损坏配置恢复。
|
|
30
|
+
- 将付费能力实测改为单项显式确认,视频与转写不做空样本自动探针。
|
|
31
|
+
- 默认将 Iris HTTP 路由限制在回环 Host,为反代提供显式信任列表和认证部署说明。
|
|
32
|
+
- 增加 Linux/Windows 与 Node.js 20.9/22 CI 矩阵、发布前测试护栏,并以当前 DSH 预览版完成宿主烟测。
|
|
33
|
+
- 核对打包清单,记录真实环境和未验证平台。
|
|
34
|
+
|
|
35
|
+
## 下一步
|
|
25
36
|
|
|
26
|
-
|
|
27
|
-
- 在全新 DeepSeek Harness 环境验证插件装载、客户端入口、工具注册和路由注册。
|
|
28
|
-
- 检查 npm 包内容、公开文档链接和最低 Node.js 版本。
|
|
29
|
-
- 整理版本号、发布日期和升级说明后再创建正式发布。
|
|
37
|
+
### 后续 0.1.x:可信基座
|
|
30
38
|
|
|
31
|
-
|
|
39
|
+
- 区分提交未受理、已受理与受理未知,完善 Attempt、取消和恢复语义。
|
|
40
|
+
- 用故障注入验证提交响应丢失、下载和落盘失败时不会重复提交。
|
|
41
|
+
- 定义最小宿主与供应商契约,提供离线诊断入口。
|
|
32
42
|
|
|
33
|
-
|
|
34
|
-
- 评估按媒体类型调整上传上限,兼顾大视频与远程部署。
|
|
35
|
-
- 在真实 DSH 会话验证 `iris-compose-media` 的触发与停止边界,再决定两个仓库级 Skills 的自动注册方式。
|
|
43
|
+
### 0.2.0:独立 Core 与产物记录
|
|
36
44
|
|
|
37
|
-
|
|
45
|
+
- 统一 Agent、工作台与 CLI 的命令执行核心,将 DSH 集成集中到宿主适配器。
|
|
46
|
+
- 为产物保存内容哈希、来源和派生关系,支持检查与导出。
|
|
47
|
+
- 在未安装 DSH 的干净环境完成本地动作和供应商任务验收。
|
|
38
48
|
|
|
39
|
-
|
|
49
|
+
### 后续方向
|
|
40
50
|
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
- 最近使用文件和更易发现的历史产物入口
|
|
51
|
+
- 0.3:有界、可恢复的媒体工作流,以及用户显式启动的本地 API 和最小独立工作台。
|
|
52
|
+
- 0.4:根据真实消费者需要开放供应商和宿主适配契约。
|
|
53
|
+
- 1.0:冻结经过验证的公共契约和升级策略。版本表示目标聚合点,不代表发布日期承诺。
|
|
45
54
|
|
|
46
55
|
## 兼容性与限制
|
|
47
56
|
|
|
48
|
-
- Node.js 最低版本为 20.
|
|
57
|
+
- Node.js 最低版本为 20.10。
|
|
49
58
|
- 图片处理依赖 `sharp`,非标准 libc 或移动终端环境可能没有可用的预编译包。
|
|
50
59
|
- 视频抽帧与视频摘要依赖 `ffmpeg` 和 `ffprobe`。
|
|
51
60
|
- WSL、远程浏览器和容器部署应优先使用上传或会话附件,避免混淆浏览器路径与宿主路径。
|
|
52
61
|
- HTML 截图运行在离线沙箱中,远程资源和页面脚本不可用。
|
|
53
62
|
- 模型发现只能列出候选项,具体能力需要通过实际请求确认。
|
|
63
|
+
- Node 测试调度器不依赖 Bash;原生 Windows/WSL 完整测试与 DSH 冒烟尚待验证,不因调度器改写而宣称已支持全部平台。
|
|
54
64
|
|
|
55
65
|
## 质量要求
|
|
56
66
|
|
|
@@ -67,6 +77,7 @@ dsh-iris 已具备完整的媒体生成、视觉处理和任务管理主流程
|
|
|
67
77
|
|
|
68
78
|
- 在插件内实现账号、权限或多租户系统。
|
|
69
79
|
- 接管 DeepSeek Harness 的通用聊天模型配置。
|
|
70
|
-
-
|
|
80
|
+
- 由 DSH 插件隐式启动独立后台服务或额外监听端口。
|
|
81
|
+
- 在本地工作台阶段建设公网 SaaS、组织账号和多租户系统。
|
|
71
82
|
- 自动探测全部模型能力并触发可能计费的请求。
|
|
72
83
|
- 仅为未来可能使用的供应商预建没有调用方的适配层。
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
Iris v0.1.1 is a compatibility and hardening release of the media and visual
|
|
2
|
+
workspace for DeepSeek Harness, targeting DSH `>=0.1.2-rc.1 <0.1.3-0`.
|
|
3
|
+
|
|
4
|
+
## Highlights
|
|
5
|
+
- Register the client under the full npm package name, fixing a load failure
|
|
6
|
+
on DSH 0.1.2+ Web clients.
|
|
7
|
+
- Raise the minimum Node.js to 20.10; the image dependency requires JSON
|
|
8
|
+
import attributes that 20.9 cannot parse.
|
|
9
|
+
- The workbench now auto-detects media protocols with an explicit override,
|
|
10
|
+
and probes models per capability with confirmation before any paid call.
|
|
11
|
+
|
|
12
|
+
## Reliability and data safety
|
|
13
|
+
- Corrupted persistence files are quarantined with the original evidence
|
|
14
|
+
preserved and rebuilt safely, instead of crashing or silently losing updates.
|
|
15
|
+
- Large media downloads use streaming atomic writes; failed downloads clean
|
|
16
|
+
up their temporary files.
|
|
17
|
+
- Legacy ai-paint configuration is imported only when `IRIS_IMPORT_WORKBENCH_CONFIG`
|
|
18
|
+
is set explicitly; default startup no longer reads external paths.
|
|
19
|
+
- DashScope credentials are bound to official HTTPS endpoints before any
|
|
20
|
+
request is sent.
|
|
21
|
+
- On POSIX, Iris state is stored under `0700` directories and `0600` files;
|
|
22
|
+
`/iris/*` accepts loopback hosts only unless `IRIS_TRUSTED_HOSTS` is configured.
|
|
23
|
+
|
|
24
|
+
## Testing and release pipeline
|
|
25
|
+
- The full regression suite runs on a Linux/Windows × Node.js 20.10/22 matrix,
|
|
26
|
+
with a pure Node.js scheduler, per-file isolation, and automatic temp cleanup.
|
|
27
|
+
- Windows support hardened end to end: LF-forced checkout, CRLF-tolerant
|
|
28
|
+
parsers, and cross-platform module paths.
|
|
29
|
+
- Releases are now tag-driven: pushing `vX.Y.Z` runs the full suite, verifies
|
|
30
|
+
the tag against `package.json`, creates this Release with the packaged
|
|
31
|
+
tarball, and publishes to npm with provenance.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
```bash
|
|
35
|
+
dsh plugin --profile web add @mokuyoaxis/dsh-iris
|
|
36
|
+
dsh web
|
|
37
|
+
```
|
|
38
|
+
> Requires Node.js 20.10.0 or later and DSH `>=0.1.2-rc.1`. Video frame
|
|
39
|
+
> extraction and video summaries require ffmpeg and ffprobe.
|
|
40
|
+
|
|
41
|
+
See the [README](https://github.com/mokuyoaxis/dsh-iris#readme) and
|
|
42
|
+
[User Guide](https://github.com/mokuyoaxis/dsh-iris/blob/v0.1.1/user_guide.md)
|
|
43
|
+
for provider and model configuration.
|
|
44
|
+
|
|
45
|
+
Image, video, and speech operations may incur charges from the configured provider.
|
|
46
|
+
|
|
47
|
+
## Package
|
|
48
|
+
- npm: [@mokuyoaxis/dsh-iris](https://www.npmjs.com/package/@mokuyoaxis/dsh-iris)
|
|
49
|
+
- Changelog: [v0.1.1](https://github.com/mokuyoaxis/dsh-iris/blob/v0.1.1/CHANGELOG.md)
|
|
50
|
+
|
|
51
|
+
**Full Changelog**: https://github.com/mokuyoaxis/dsh-iris/compare/v0.1.0...v0.1.1
|
package/lib/actions.js
CHANGED
|
@@ -24,6 +24,8 @@ import { BrowserHtmlRenderer } from './render.js';
|
|
|
24
24
|
import { longOcr } from './ocr.js';
|
|
25
25
|
import { ffmpegAvailable, extractFrames, probeVideo } from './media-probe.js';
|
|
26
26
|
import { buildContactSheet, summarizeMedia } from './summarize.js';
|
|
27
|
+
import { ensurePrivateDir, writePrivateFile } from './private-storage.js';
|
|
28
|
+
import { inferMediaProtocol } from './provider-protocol.js';
|
|
27
29
|
|
|
28
30
|
const MEDIA_TYPES = {
|
|
29
31
|
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
@@ -49,8 +51,7 @@ async function describeFirstImage(ctx, ref, absPath, originalPrompt) {
|
|
|
49
51
|
/* ---------------- IO 目录 ---------------- */
|
|
50
52
|
function outputDir() {
|
|
51
53
|
const dir = path.join(store.irisHome(), 'outputs');
|
|
52
|
-
|
|
53
|
-
return dir;
|
|
54
|
+
return ensurePrivateDir(dir);
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
function throwIfAborted(signal) {
|
|
@@ -160,7 +161,7 @@ register('image', async (ctx, args, { signal }) => {
|
|
|
160
161
|
const prompt = String(args.prompt || '').trim();
|
|
161
162
|
if (!prompt) throw new Error('iris: prompt 不能为空');
|
|
162
163
|
const submitted = await submitWithFailover(cap.CAPABILITIES.IMAGE, '图像', signal, args.model, async (provider) => {
|
|
163
|
-
const protocol =
|
|
164
|
+
const protocol = provider.mediaProtocol || inferMediaProtocol(provider.baseUrl);
|
|
164
165
|
const model = args.model ? (models.parseModelRef(String(args.model))?.modelId || String(args.model)) : (provider.imageModel || (protocol === 'openai-images' ? 'gpt-image-1' : 'wan2.2-t2i-flash'));
|
|
165
166
|
const task = tasks.create({
|
|
166
167
|
cap: 'image', providerId: provider.id, providerName: provider.name,
|
|
@@ -168,7 +169,7 @@ register('image', async (ctx, args, { signal }) => {
|
|
|
168
169
|
});
|
|
169
170
|
if (protocol === 'dashscope') {
|
|
170
171
|
const started = await tasks.submitGuard(task, () => adapters.startImageGeneration({
|
|
171
|
-
key: provider.apiKey, model, prompt, size: args.size, n: args.n, signal
|
|
172
|
+
key: provider.apiKey, baseUrl: provider.baseUrl, model, prompt, size: args.size, n: args.n, signal
|
|
172
173
|
}));
|
|
173
174
|
const remoteTaskId = started.remoteTaskId;
|
|
174
175
|
if (!remoteTaskId) {
|
|
@@ -190,7 +191,7 @@ register('image', async (ctx, args, { signal }) => {
|
|
|
190
191
|
tasks.update(task.id, { remoteTaskId });
|
|
191
192
|
tasks.watch(tasks.get(task.id), {
|
|
192
193
|
key: () => provider.apiKey,
|
|
193
|
-
poll: ({ key, remoteTaskId }) => adapters.pollTask({ key, remoteTaskId }),
|
|
194
|
+
poll: ({ key, remoteTaskId }) => adapters.pollTask({ key, baseUrl: provider.baseUrl, remoteTaskId }),
|
|
194
195
|
intervalMs: 2500,
|
|
195
196
|
onSuccess: async (t, r) => {
|
|
196
197
|
const names = [];
|
|
@@ -212,7 +213,7 @@ register('image', async (ctx, args, { signal }) => {
|
|
|
212
213
|
const written = [];
|
|
213
214
|
for (let i = 0; i < outs.length; i++) {
|
|
214
215
|
const p = path.join(outputDir(), `${task.id}-${i}.png`);
|
|
215
|
-
if (outs[i].b64)
|
|
216
|
+
if (outs[i].b64) writePrivateFile(p, Buffer.from(outs[i].b64, 'base64'));
|
|
216
217
|
else await adapters.downloadTo(outs[i].url, p, { signal });
|
|
217
218
|
written.push(p);
|
|
218
219
|
}
|
|
@@ -255,8 +256,8 @@ register('video', async (ctx, args, { signal }) => {
|
|
|
255
256
|
const audioPath = String(args.audio_path || '').trim();
|
|
256
257
|
if (!audioPath || !path.isAbsolute(audioPath)) throw new Error('iris: s2v 的 audio_path 必须是绝对路径');
|
|
257
258
|
if (!fs.existsSync(audioPath)) throw new Error('iris: 音频文件不存在:' + audioPath);
|
|
258
|
-
const imageUrl = await adapters.uploadTempFile({ key: provider.apiKey, model, filePath: frame.absPath, signal });
|
|
259
|
-
const audioUrl = await adapters.uploadTempFile({ key: provider.apiKey, model, filePath: audioPath, signal });
|
|
259
|
+
const imageUrl = await adapters.uploadTempFile({ key: provider.apiKey, baseUrl: provider.baseUrl, model, filePath: frame.absPath, signal });
|
|
260
|
+
const audioUrl = await adapters.uploadTempFile({ key: provider.apiKey, baseUrl: provider.baseUrl, model, filePath: audioPath, signal });
|
|
260
261
|
submitArgs = { imgDataUrl: imageUrl, audioUrl, resolution: args.resolution || '480P' };
|
|
261
262
|
} else {
|
|
262
263
|
submitArgs = {
|
|
@@ -266,12 +267,12 @@ register('video', async (ctx, args, { signal }) => {
|
|
|
266
267
|
duration: args.duration ? Number(args.duration) : undefined
|
|
267
268
|
};
|
|
268
269
|
}
|
|
269
|
-
return adapters.submitVideo({ key: provider.apiKey, model, ...submitArgs, signal });
|
|
270
|
+
return adapters.submitVideo({ key: provider.apiKey, baseUrl: provider.baseUrl, model, ...submitArgs, signal });
|
|
270
271
|
});
|
|
271
272
|
tasks.update(task.id, { remoteTaskId });
|
|
272
273
|
tasks.watch(tasks.get(task.id), {
|
|
273
274
|
key: () => provider.apiKey,
|
|
274
|
-
poll: ({ key, remoteTaskId: id }) => adapters.pollTask({ key, remoteTaskId: id }),
|
|
275
|
+
poll: ({ key, remoteTaskId: id }) => adapters.pollTask({ key, baseUrl: provider.baseUrl, remoteTaskId: id }),
|
|
275
276
|
intervalMs: 6000,
|
|
276
277
|
onSuccess: async (t, r) => {
|
|
277
278
|
const names = [];
|
|
@@ -302,11 +303,11 @@ register('tts', async (ctx, args, { signal }) => {
|
|
|
302
303
|
const voice = args.voice || (provider.ttsVoice || 'Cherry');
|
|
303
304
|
const task = tasks.create({ cap: 'tts', providerId: provider.id, providerName: provider.name, model, voice, prompt: text.slice(0, 500) });
|
|
304
305
|
const fileName = await tasks.submitGuard(task, async () => {
|
|
305
|
-
const r = await adapters.synthesizeTts({ key: provider.apiKey, model, text, voice, signal });
|
|
306
|
+
const r = await adapters.synthesizeTts({ key: provider.apiKey, baseUrl: provider.baseUrl, model, text, voice, signal });
|
|
306
307
|
const name = `iris-tts-${Date.now()}.${r.audioUrl ? adapters.extFromUrl(r.audioUrl, 'wav') : 'wav'}`;
|
|
307
308
|
const p = path.join(outputDir(), name);
|
|
308
309
|
if (r.audioUrl) await adapters.downloadTo(r.audioUrl, p, { signal });
|
|
309
|
-
else
|
|
310
|
+
else writePrivateFile(p, Buffer.from(r.audioB64, 'base64'));
|
|
310
311
|
return name;
|
|
311
312
|
});
|
|
312
313
|
tasks.update(task.id, { status: 'succeeded', files: [fileName], finishedAt: new Date().toISOString() });
|
|
@@ -530,7 +531,7 @@ register('providers_list', async (ctx, args) => {
|
|
|
530
531
|
type: p.type || 'openai',
|
|
531
532
|
baseUrl: p.baseUrl,
|
|
532
533
|
enabled: p.enabled !== false,
|
|
533
|
-
mediaProtocol: p.mediaProtocol ||
|
|
534
|
+
mediaProtocol: p.mediaProtocol || inferMediaProtocol(p.baseUrl),
|
|
534
535
|
apiKeyHint: String(p.apiKey || '').length <= 8 ? '****' : String(p.apiKey).slice(0, 3) + '****' + String(p.apiKey).slice(-4),
|
|
535
536
|
models: models.providerModels(p).map((m) => ({ id: m.id, capabilities: m.capabilities, verified: m.verified || null, source: m.source || 'auto' })),
|
|
536
537
|
capabilities: store.capabilitiesOf(p)
|
|
@@ -599,6 +600,7 @@ register('providers_test_vision', async (ctx, args) => {
|
|
|
599
600
|
const backends = buildVisionBackends(ctx, { providers: visionProviders });
|
|
600
601
|
const backend = backends[0];
|
|
601
602
|
if (!backend) return { ok: true, tested: false, text: '该供应商的 vision 配置不可用(类型非 openai)' };
|
|
603
|
+
if (args.confirm_paid !== true) throw new Error('iris: 视觉实测会发起真实供应商请求,请先明确确认');
|
|
602
604
|
const r = await testVisionCapability(backend, { timeoutMs: 15000, signal: args._signal });
|
|
603
605
|
return {
|
|
604
606
|
ok: true,
|
|
@@ -612,7 +614,7 @@ register('providers_test_vision', async (ctx, args) => {
|
|
|
612
614
|
|
|
613
615
|
/* ============ 模型池逐模型管理(阶段 9 P3:手动增删改 + 逐能力实测) ============ */
|
|
614
616
|
|
|
615
|
-
/** 按能力实测单个模型;返回 {ok, note}
|
|
617
|
+
/** 按能力实测单个模型;返回 {ok, note}。真实调用仅在 UI 明确确认后执行。 */
|
|
616
618
|
async function probeModel(ctx, p, modelId, capability) {
|
|
617
619
|
if (capability === cap.CAPABILITIES.VISION) {
|
|
618
620
|
const backend = buildVisionBackends(ctx, { providers: [p], model: modelId })[0];
|
|
@@ -621,34 +623,32 @@ async function probeModel(ctx, p, modelId, capability) {
|
|
|
621
623
|
return { ok: r.ok, note: r.ok ? '认出红色测试图' : (r.error || '未识别出红色') };
|
|
622
624
|
}
|
|
623
625
|
if (capability === cap.CAPABILITIES.TRANSCRIBE) {
|
|
624
|
-
|
|
626
|
+
return { skipped: true, ok: false, note: '需真实音频,请通过“音频转写”动作验证' };
|
|
625
627
|
}
|
|
626
628
|
if (capability === cap.CAPABILITIES.TTS) {
|
|
627
|
-
const r = await adapters.synthesizeTts({ key: p.apiKey, model: modelId, text: '你好', voice: 'Cherry' });
|
|
629
|
+
const r = await adapters.synthesizeTts({ key: p.apiKey, baseUrl: p.baseUrl, model: modelId, text: '你好', voice: 'Cherry' });
|
|
628
630
|
const ok = !!(r.audioUrl || r.audioB64);
|
|
629
631
|
return { ok, note: ok ? '合成返回音频' : '无音频返回' };
|
|
630
632
|
}
|
|
631
633
|
if (capability === cap.CAPABILITIES.IMAGE) {
|
|
632
|
-
if ((p.mediaProtocol ||
|
|
634
|
+
if ((p.mediaProtocol || inferMediaProtocol(p.baseUrl)) === 'openai-images') {
|
|
633
635
|
const outs = await adapters.openAiGenerateImage({ key: p.apiKey, baseUrl: p.baseUrl, model: modelId, prompt: 'red circle', size: '256x256', n: 1 });
|
|
634
636
|
return { ok: outs.length > 0, note: outs.length ? '生成返回图片' : '无图片返回' };
|
|
635
637
|
}
|
|
636
|
-
const started = await adapters.startImageGeneration({ key: p.apiKey, model: modelId, prompt: '红色圆形', size: '512*512', n: 1 });
|
|
638
|
+
const started = await adapters.startImageGeneration({ key: p.apiKey, baseUrl: p.baseUrl, model: modelId, prompt: '红色圆形', size: '512*512', n: 1 });
|
|
637
639
|
if (!started.remoteTaskId) {
|
|
638
640
|
return { ok: (started.urls || []).length > 0, note: (started.urls || []).length ? '生成成功' : '无图片返回' };
|
|
639
641
|
}
|
|
640
642
|
// 有界轮询到终态(图像通常 5–15s)
|
|
641
643
|
for (let i = 0; i < 20; i++) {
|
|
642
644
|
await new Promise((r) => setTimeout(r, 3000));
|
|
643
|
-
const st = await adapters.pollTask({ key: p.apiKey, remoteTaskId: started.remoteTaskId });
|
|
645
|
+
const st = await adapters.pollTask({ key: p.apiKey, baseUrl: p.baseUrl, remoteTaskId: started.remoteTaskId });
|
|
644
646
|
if (st.done) return { ok: st.ok && (st.urls || []).length > 0, note: st.ok ? '生成成功' : (st.message || '生成失败') };
|
|
645
647
|
}
|
|
646
648
|
return { ok: false, note: '生成超时(>60s)' };
|
|
647
649
|
}
|
|
648
650
|
if (capability === cap.CAPABILITIES.VIDEO) {
|
|
649
|
-
|
|
650
|
-
const taskId = await adapters.submitVideo({ key: p.apiKey, model: modelId, prompt: 'test', size: '480*832' });
|
|
651
|
-
return { ok: !!taskId, note: taskId ? '提交受理(未等成片)' : '无 task_id' };
|
|
651
|
+
return { skipped: true, ok: false, note: '视频提交即可能计费,请通过“视频生成”动作验证' };
|
|
652
652
|
}
|
|
653
653
|
return { ok: false, note: '未知能力' };
|
|
654
654
|
}
|
|
@@ -688,12 +688,18 @@ register('providers_test_model', async (ctx, args) => {
|
|
|
688
688
|
const modelId = String(args.model_id || '').trim();
|
|
689
689
|
const capability = String(args.capability || '').trim();
|
|
690
690
|
if (!modelId || !cap.isKnownCapability(capability)) throw new Error('iris: 需 model_id 与合法 capability');
|
|
691
|
+
if (![cap.CAPABILITIES.VIDEO, cap.CAPABILITIES.TRANSCRIBE].includes(capability) && args.confirm_paid !== true) {
|
|
692
|
+
throw new Error('iris: 模型实测会发起真实供应商请求,请先明确确认');
|
|
693
|
+
}
|
|
691
694
|
let result;
|
|
692
695
|
try {
|
|
693
696
|
result = await probeModel(ctx, p, modelId, capability);
|
|
694
697
|
} catch (err) {
|
|
695
698
|
result = { ok: false, note: String(err.message || err) };
|
|
696
699
|
}
|
|
700
|
+
if (result.skipped) {
|
|
701
|
+
return { ok: true, skipped: true, passed: false, text: `[iris] ${modelId} · ${capability} 已跳过:${result.note}` };
|
|
702
|
+
}
|
|
697
703
|
store.setModelVerified(p.id, modelId, capability, result);
|
|
698
704
|
return {
|
|
699
705
|
ok: true,
|
|
@@ -832,8 +838,8 @@ register('attachment_export', async (ctx, args) => {
|
|
|
832
838
|
} catch (_) { /* ignore */ }
|
|
833
839
|
if (!ref) throw new Error('iris: 该 attachment 不在本会话中、也不是 iris 产物:' + id);
|
|
834
840
|
const stored = await att.readImage({ attachmentId: ref.attachmentId, mediaType: ref.mediaType }, null);
|
|
835
|
-
|
|
836
|
-
|
|
841
|
+
ensurePrivateDir(uploads);
|
|
842
|
+
writePrivateFile(cached, Buffer.from(stored.data));
|
|
837
843
|
return { ok: true, path: cached, name: ref.name || path.basename(cached) };
|
|
838
844
|
});
|
|
839
845
|
|
|
@@ -850,14 +856,14 @@ register('transcribe', async (ctx, args, { signal }) => {
|
|
|
850
856
|
model, prompt: 'audio transcribe: ' + path.basename(audioPath)
|
|
851
857
|
});
|
|
852
858
|
const remoteTaskId = await tasks.submitGuard(task, async () => {
|
|
853
|
-
const audioUrl = await adapters.uploadTempFile({ key: provider.apiKey, model, filePath: audioPath, signal });
|
|
854
|
-
return adapters.submitTranscription({ key: provider.apiKey, model, audioUrl, signal });
|
|
859
|
+
const audioUrl = await adapters.uploadTempFile({ key: provider.apiKey, baseUrl: provider.baseUrl, model, filePath: audioPath, signal });
|
|
860
|
+
return adapters.submitTranscription({ key: provider.apiKey, baseUrl: provider.baseUrl, model, audioUrl, signal });
|
|
855
861
|
});
|
|
856
862
|
tasks.update(task.id, { remoteTaskId });
|
|
857
863
|
tasks.watch(tasks.get(task.id), {
|
|
858
864
|
key: () => provider.apiKey,
|
|
859
865
|
poll: async ({ key, remoteTaskId: id }) => {
|
|
860
|
-
const r = await adapters.pollTranscriptionTask({ key, remoteTaskId: id });
|
|
866
|
+
const r = await adapters.pollTranscriptionTask({ key, baseUrl: provider.baseUrl, remoteTaskId: id });
|
|
861
867
|
return { done: r.done, ok: r.ok, urls: r.text ? [r.text] : [], message: r.message };
|
|
862
868
|
},
|
|
863
869
|
intervalMs: 2500,
|