@mokuyoaxis/dsh-iris 0.2.1 → 0.2.2
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 +23 -1
- package/README.en.md +25 -25
- package/README.md +25 -25
- package/docs/AGENT_QUICKSTART.md +7 -7
- package/docs/ARCHITECTURE.md +10 -2
- package/docs/ARTIFACT_LIBRARY.md +1 -1
- package/docs/CLI_MANAGEMENT.md +5 -3
- package/docs/COMPOSITE_VISION.md +1 -1
- package/docs/CORE_RUNTIME_CONTRACT.md +9 -1
- package/docs/DSH_CORE_MIGRATION.md +1 -1
- package/docs/FAULT_INJECTION.md +10 -1
- package/docs/HEADLESS_CLI.md +4 -2
- package/docs/HOST_ADAPTER_CONTRACT.md +1 -1
- package/docs/MODEL_PORT_CONTRACT.md +4 -2
- package/docs/OCR_MODEL.md +1 -1
- package/docs/PROMPT_OPTIMIZER.md +33 -3
- package/docs/PROVIDER_ADAPTER_CONTRACT.md +10 -2
- package/docs/PROVIDER_HEALTH.md +1 -1
- package/docs/ROADMAP.md +10 -2
- package/docs/SECURITY.md +1 -1
- package/docs/TASK_SEMANTICS.md +1 -1
- package/docs/VISION_CLI.md +1 -1
- package/docs/VISION_MODEL.md +1 -1
- package/docs/WORKBENCH_ARTIFACTS.md +1 -1
- package/docs/releases/0.2.2.md +31 -0
- package/lib/actions.js +12 -16
- package/lib/adapters.js +88 -24
- package/lib/capability.js +23 -16
- package/lib/client.js +268 -22
- package/lib/config.js +13 -4
- package/lib/core-lease-recovery.js +64 -92
- package/lib/core-runtime.js +45 -47
- package/lib/core-writer-lease.js +134 -0
- package/lib/models.js +19 -37
- package/lib/prompt-optimizer-config.js +19 -9
- package/lib/prompt-optimizer-core.js +133 -7
- package/lib/provider-adapters.js +2 -1
- package/lib/provider-config-service.js +3 -4
- package/lib/provider-protocol.js +61 -0
- package/lib/provider-task-runner.js +6 -4
- package/package.json +1 -1
- package/user_guide.md +30 -16
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Text/Vision Model Port v0 契约设计
|
|
2
2
|
|
|
3
|
-
状态:**Iris 0.2.
|
|
3
|
+
状态:**Iris 0.2.2。** 0.2.0 已包含 M1 契约;0.2.2 接入 DSH 文本适配器与共享优化消费者、HTTP/DSH Vision Port,以及 look/relook、OCR、定位、拼图摘要和图片自述。端口仍为内部契约,不增加公开 export 或生产依赖,不改变 [Host Adapter v0](HOST_ADAPTER_CONTRACT.md) 的现有方法。新增按次规则与 UI 见 [提示词优化系统](PROMPT_OPTIMIZER.md),单图预算与宿主附件副作用见 [单图视觉调用](VISION_MODEL.md),复合操作见 [长图 OCR](OCR_MODEL.md) 和 [定位与摘要](COMPOSITE_VISION.md)。
|
|
4
4
|
|
|
5
5
|
0.2.1 还提供 [视觉 CLI](VISION_CLI.md)、可选账号/模型 `visionInput` 和设置命令,见 [看图输入预算](CLI_MANAGEMENT.md#看图输入预算);不改变请求/完成 DTO、契约版本或公开 exports。该设置用于看图、OCR 分块、定位和聊天改图的发送副本,不是供应商能力上限声明;聊天改图的媒体任务语义仍由 Core Runner 管理。
|
|
6
6
|
|
|
@@ -254,9 +254,11 @@ Host 引用、session ID 与 source metadata 可存在于 DSH 适配器闭包,
|
|
|
254
254
|
|
|
255
255
|
提示词优化仍由 DSH 边界读取 v1 配置、解析固定/会话/默认模型,返回选择来源供 UI 展示。共享核心只接收选定端口与模板;保留原草稿、预览后写回和不携带会话历史的行为。Provider 健康反馈由入口消费安全结果/错误,模型端口不导入健康 Store。
|
|
256
256
|
|
|
257
|
+
0.2.2 的一般功能在既有消费者上增加 faithful / rewrite / suggestions 策略;忠实整理校验原文片段覆盖和顺序,独立建议不自动加入正文。新策略与五用途不改变 Model Port 的单次调用契约,不新增自持文本能力、独立提示词 CLI 或公开 SDK;具体输出协议和旧调用兼容见 [提示词优化系统](PROMPT_OPTIMIZER.md)。
|
|
258
|
+
|
|
257
259
|
## 实现切片与退出条件
|
|
258
260
|
|
|
259
|
-
0.2.0 包含 M1;0.2.1 包含 M2/M3/M4 和 M5 独立视觉 CLI,详见 [视觉 CLI](VISION_CLI.md)
|
|
261
|
+
0.2.0 包含 M1;0.2.1 包含 M2/M3/M4 和 M5 独立视觉 CLI,详见 [视觉 CLI](VISION_CLI.md)。一般提示词功能的新增编辑策略属于 0.2.2,特殊功能及独立提示词入口后续安排。
|
|
260
262
|
|
|
261
263
|
| 切片 | 范围 | 退出条件 |
|
|
262
264
|
|---|---|---|
|
package/docs/OCR_MODEL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 长图 OCR
|
|
2
2
|
|
|
3
|
-
状态:**Iris 0.2.
|
|
3
|
+
状态:**Iris 0.2.2**。定位、拼图摘要及自述见 [复合视觉调用](COMPOSITE_VISION.md)。
|
|
4
4
|
|
|
5
5
|
Agent 的 `iris_long_ocr` 与工作台 OCR 使用相同分块业务和共享 Vision Ports。工具接受 `image_path`、`artifact_id` 或 `attachment_id` 三选一;工作台动作接受 `image_path` 或 `artifact_id` 二选一,现有表单继续输入文件路径。Core ID 读取当前 profile 的既有图片,核验内容哈希和 MIME,不依赖原文件、不导出临时图片、不写入 Core。工具文字保留 Core ID,动作 JSON 带 `artifactId`;CLI `vision ocr` 同样支持,详见 [视觉 CLI](VISION_CLI.md)。复用可选的账号/模型输入预算,不新增公开 SDK export 或生产依赖。
|
|
6
6
|
|
package/docs/PROMPT_OPTIMIZER.md
CHANGED
|
@@ -1,10 +1,36 @@
|
|
|
1
1
|
# 提示词优化与规则组装
|
|
2
2
|
|
|
3
|
-
状态:**Iris 0.2.
|
|
3
|
+
状态:**Iris 0.2.2**。在既有按次规则、只组装和可编辑预览上增加一般功能,特殊功能待后续安排。
|
|
4
|
+
|
|
5
|
+
## 一般功能(0.2.2)
|
|
6
|
+
|
|
7
|
+
面板将一般功能放在可折叠卡片中,按“通用、画图、改图、视频、数字人”选择用途。当前草稿、本次规则、操作、可编辑结果和对照属于同一功能区域;折叠不清空选择、规则或结果,也不触发模型请求。指令与模型设置独立折叠,可在调用前查看当前系统指令和目标模板。浏览器原生对话框在顶层显示,避免宿主悬浮按钮遮住结果。桌面和窄屏使用同一流程,支持点击背景或 Escape 关闭、焦点恢复与对话框内键盘焦点循环。
|
|
8
|
+
|
|
9
|
+
| 目标 | 标识 | 内置优化重点 |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| 通用 | `general` | 目标、背景、约束、输出和验收 |
|
|
12
|
+
| 画图 | `image` | 主体、场景、构图与已指定风格 |
|
|
13
|
+
| 改图 | `image-edit` | 明确修改范围,保持原图其余特征 |
|
|
14
|
+
| 视频 | `video` | 动作先后、镜头与连续性 |
|
|
15
|
+
| 数字人 | `s2v` | 已有首帧与音频下的人物动作、表情和镜头 |
|
|
16
|
+
|
|
17
|
+
默认指令参照项目 Skills 的职责、目标、输入、保持/改变和输出组织方式,并使用实际 Iris 工具语义。S2V 的旧内置“首尾帧”标签有误,本轮修正为首帧+音频数字人。优化器仍只编辑草稿,不读取图片/音频、不生成媒体,也不冒充已经检查素材;具体方位、数量、颜色、数值、引用原文和否定条件必须保留。模型实际是否遵守仍需用户检查原文对照。
|
|
18
|
+
|
|
19
|
+
v1 配置兼容:增加 `targets["image-edit"]`,旧配置缺少此项时只在内存补齐,不自动重写文件。既有 systemPrompt、目标模板与模型路由优先保留,包含用户自定义 s2v 文本;用户显式恢复默认后才替换为新的内置指令。没有新增规则持久格式、特殊功能或 CLI。
|
|
20
|
+
|
|
21
|
+
### 编辑方式(0.2.2)
|
|
22
|
+
|
|
23
|
+
新界面默认选择 **忠实整理**:模型只选择相邻原文片段之间的分段位置,程序从原稿拼回正文。每个片段必须恰好出现一次、句序保持,遗漏、重复、调换或新增内容都会拒绝。条件、动作先后、引文、代码和复杂格式采用保守分段;已有完整文本允许保持原样。本次保证的是原文片段和顺序,不是自动证明所有排版变化的语义完全相同。
|
|
24
|
+
|
|
25
|
+
需要等义润色或明确改变某个项目时,选择 **改写表达** 并填写本次改写规则。例如“只把白色丝袜改为黑色,其余保持”授权改变指定颜色;其余事实和否定继续保留。忠实整理不会静默执行或丢弃改写规则。改写表达仍是模型生成文字,新增含义与遗漏需查看对照,不能以结果非空证明忠实。
|
|
26
|
+
|
|
27
|
+
结果中的 **可选补充与待确认** 独立于正文。点击“获取补充建议”才新增一次模型调用,不自动生成第二轮;默认未勾选,勾选也不改正文,点击“采纳所选建议”后才加入可编辑预览。采纳与撤回不调用模型、不直接写回或发送。手动修改预览会清除旧建议绑定,避免撤回旧快照覆盖你的编辑;再次获取建议针对当前文字。失败、取消、会话切换或请求期间文字变化不会把迟到建议写入预览。
|
|
28
|
+
|
|
29
|
+
只组装仍为零模型调用。旧后端未声明编辑能力时保留原“智能优化”流程,不发送其不支持的新策略。会话/fixed 模型选择保持,不因为选择“画图”用途就进入生图模型池,也不硬编码供应商默认模型或失败后自动切换。
|
|
4
30
|
|
|
5
31
|
## 使用
|
|
6
32
|
|
|
7
|
-
在 DSH 输入框写入未发送的纯文本草稿,打开
|
|
33
|
+
在 DSH 输入框写入未发送的纯文本草稿,打开 🫧 并选择用途。0.2.2 提供五用途和一般功能新布局;旧 0.2.1 的四目标调用仍兼容。
|
|
8
34
|
|
|
9
35
|
“本次规则”可填写以下内容;留空表示不选择。规则只在当前组件中保留,刷新后重置,不改写已有 `prompt-optimizer.json`。
|
|
10
36
|
|
|
@@ -18,6 +44,8 @@
|
|
|
18
44
|
|
|
19
45
|
结果先进入可编辑预览。可以查看原文、本次参与规则、实际模型与思考策略,以及折叠相同首尾后的中间替换区。这是文本改动对照,不代表模型解释了每处改动的原因。手动编辑也会更新对照;复制和写回使用编辑后的文字。
|
|
20
46
|
|
|
47
|
+
实现前的真实模型补测发现,文本模型别名也可能返回图片链接,甚至返回扩写正文加图片链接。改写结果会在拼接前后缀之前检查未由原稿/明确改写规则提供的新链接、媒体标记和明显的执行完成/拒绝回复;已有合法链接、原文引用及只组装保留。忠实整理与建议模式另检查专用 JSON 形状、片段序列或建议字段,未授权正文不能混入。检查不构成任意自然语言的完整语义证明。拒绝后不自动重试、切换模型或写回;上游可能已经处理请求,不能据此推定没有计费或媒体副作用。
|
|
48
|
+
|
|
21
49
|
写回不会自动发送。出现新草稿时先确认替换;出现结构化 `@`、`/` 引用或忙碌状态时拒绝写回。切换会话清除旧预览并取消在途操作,即便两边草稿文本相同。取消、关闭或卸载会传递 abort,迟到结果不能恢复旧预览或覆盖新操作。恢复原文只恢复本次操作前的文字,不自动恢复附件/结构化引用。
|
|
22
50
|
|
|
23
51
|
## 规则来源与限制
|
|
@@ -42,6 +70,8 @@
|
|
|
42
70
|
|
|
43
71
|
现有 `POST /iris/api/prompt-optimizer/optimize` 增加可选 `mode`(`optimize` / `assemble`)与 `rules`;省略时沿用原优化行为。规则形状为 `{ id, label, kind, text, position? }`:kind 是 `optimization` 或 `output`,只有 output 设置 `prefix` / `suffix`。调用者不能伪造 source;响应由程序标记为 `user-selected`。直接 API 在 assemble 模式携带 optimization 规则会拒绝;UI 只组装只提交输出规则。
|
|
44
72
|
|
|
45
|
-
|
|
73
|
+
0.2.2增加可选 `editing: "faithful" | "rewrite" | "suggestions"`,仅用于 optimize。省略时保持旧文字改写协议与 `optimized` 文本响应,不破坏旧调用;新界面显式传 faithful。faithful 允许输出前后缀但不接受 optimization 规则,响应另含 `editing: { strategy, coverage: "exact", order: "preserved", groups }`。suggestions 不接受任何规则,`optimized` 逐字等于原稿,另含 `suggestions: [{ id, label, text }]`、`questions: string[]`,ID 由程序生成;本轮指令要求至多三条有用建议/问题,协议上限分别为六条,label 最多 80、text/问题最多 600 字符。它们未经事实验证或采纳,不能当成用户已有要求。
|
|
74
|
+
|
|
75
|
+
配置 API 在配置对象外返回 `capabilities: { rules: true, assemble: true }`;0.2.2另声明 `targets: ["general", "image", "image-edit", "video", "s2v"]` 与 `editing: ["faithful", "rewrite", "suggestions"]`。客户端读到明确能力才启用规则、只组装、新改图目标与新编辑策略。没有 targets 声明时只启用旧四目标,没有 editing 声明时保持旧智能优化;能力字段不进入配置导出文件。
|
|
46
76
|
|
|
47
77
|
参见 [Model Port 契约](MODEL_PORT_CONTRACT.md) 与 [架构边界](ARCHITECTURE.md)。浏览器/Android 交互和真实模型质量需各自验收。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Provider Adapter v0 生命周期契约
|
|
2
2
|
|
|
3
|
-
状态:**Iris 0.2.
|
|
3
|
+
状态:**Iris 0.2.2;沿用 v0 生命周期内部契约。** 0.1.4 建立六操作契约,0.2.0 增加可选输入准备能力,0.2.2 增加聊天/Responses 生图及模型级协议覆盖。 这是 Iris Core 模块与供应商协议实现之间的内部边界,尚未冻结为第三方公开 SDK。DashScope 与 OpenAI Images 兼容实现通过同一套零网络 conformance runner;聊天/Responses 图片协议另有提交、交付与 CLI 回归。
|
|
4
4
|
|
|
5
5
|
## 目标与依赖方向
|
|
6
6
|
|
|
@@ -74,6 +74,8 @@ Core Attempt 持久化 `providerId::modelId` 复合身份,具体 Provider Adap
|
|
|
74
74
|
|
|
75
75
|
`download` 物化已经确认成功的产物:`remote-url` 执行下载,`inline-base64` 直接写入私有 staging。inline 正文只作为 non-enumerable 的短生命周期字段传给 Runner,不进入 JSON、Task 或 Manifest。生图产物按真实字节校验 PNG/JPEG/WebP,保留字节、MIME、后缀和哈希。物化失败只能改变交付状态,不能触发生成重提。OpenAI Images、聊天生图与当前 Responses 生图切片均同步返回图片,所以 `poll` 与 `cancel` 显式 unsupported;Responses 纯文字、缺图或未完成结果保留受理未知,不扫描其他接口。DashScope 当前没有经过验证的远端取消接口,因此 `cancel` 同样显式 unsupported,而不是伪装取消成功。
|
|
76
76
|
|
|
77
|
+
0.2.2对视频、音频及文本 staging 复用 Artifact Store 的文件复制接收路径,哈希仍按 1 MiB 分块计算,不再为落库整文件 `readFileSync`。Profile、大小核对、元数据、关系边和交付失败事实保持。图片仍读取字节验证真实格式;inline 响应的解码以及工作台预览/ZIP 等读取路径仍可能使用完整 Buffer,本改动不声称端到端流式传输。
|
|
78
|
+
|
|
77
79
|
聊天改图仍使用 `capability:image`:Runner 从当前 Core 校验并读取来源 Artifact,短生命周期 `input.image:{bytes,mediaType}` 与 `prompt` 交给聊天适配器。适配器按实际模型 `visionInput` 准备副本,HTTP 用户消息使用 `text` 与 `image_url` data URL。普通 Images/Responses 不接受此输入,编辑候选链预先过滤协议。Task 仅保存可选 `sourceArtifactId`,用于未知/失败任务的来源保护和交付时写入新 Artifact 的 `derived-from`;旧记录无需迁移,重试仍重新提供输入。来源或编辑输入预算失败发生在 HTTP 之前。
|
|
78
80
|
|
|
79
81
|
## 可选输入准备与上传边界
|
|
@@ -92,14 +94,20 @@ Host 和 CLI 通过 `prepareProviderInput(adapter, { model, filePath, signal?, t
|
|
|
92
94
|
|
|
93
95
|
`submit` 抛错由统一调用器转换成保守四态;`cancel` 抛错转换为 `unknown`;`discover/poll/download` 抛出脱敏的 `ProviderContractError`,由各自调用方应用重试和事实规则。
|
|
94
96
|
|
|
97
|
+
0.2.2的生图传输同时覆盖请求与响应正文读取。包装的网络异常在尚未取得响应头时标记 `stage=submit`,取得响应头后正文读取失败时标记 `stage=response`;已知网络超时码归类 timeout,两者都保留 `acceptance=unknown`,不凭 HTTP 200 响应头断言完整生成成功。可用的底层网络错误码放入既有 `providerCode` 字段,提示区分 DNS、TLS、连接建立、等待响应头及正文读取;没有明确错误码时只描述实际阶段。原生 AbortError、TimeoutError 或 ABORT_ERR 保留原异常及取消/超时分类,未附加正文读取阶段证据时沿用提交调用的阶段。完整 HTTP 业务响应仍按鉴权、限流/额度、参数或供应商错误分类,明确分类不被错误文字中的 timeout 覆盖。
|
|
98
|
+
|
|
99
|
+
本地尺寸校验使用 `validate / invalid_request / not_accepted`,在 HTTP 前失败。分隔符按协议归一,DashScope 多模态只执行已有证据的总像素范围,不推断单边上限;OpenAI 兼容网关的数值集合留给上游。`retryable` 表示错误类别可重试,不授权重提受理未知的收费请求;Core 的受理门继续先于候选切换。DSH 动作错误附带同一脱敏 `safeMessage`,不输出原始 cause、正文或凭据。
|
|
100
|
+
|
|
95
101
|
## 当前内置实现
|
|
96
102
|
|
|
97
103
|
| 协议 | discover | submit | poll | cancel | download | mapError |
|
|
98
104
|
|---|---:|---:|---:|---:|---:|---:|
|
|
99
105
|
| DashScope | 支持(`/api/v1/models` 分页与能力元数据) | 图片/视频/TTS/转写 | 图片/视频/转写 | 显式不支持 | 支持 | 支持 |
|
|
100
106
|
| OpenAI Images 兼容 | 支持 | 同步图片 | 显式不支持 | 显式不支持 | 支持 | 支持 |
|
|
107
|
+
| OpenAI 聊天生图兼容 | 支持 | 同步图片/单来源改图 | 显式不支持 | 显式不支持 | 支持 | 支持 |
|
|
108
|
+
| OpenAI Responses 生图兼容 | 支持 | 同步单图 | 显式不支持 | 显式不支持 | 支持 | 支持 |
|
|
101
109
|
|
|
102
|
-
|
|
110
|
+
本表描述已实现的协议生命周期;网关 Gemini 模型可以使用兼容协议,不代表原生 Gemini Adapter 已接入。原生 Gemini、Fal 与 Replicate 仍在后续路线中。
|
|
103
111
|
|
|
104
112
|
## conformance runner
|
|
105
113
|
|
package/docs/PROVIDER_HEALTH.md
CHANGED
package/docs/ROADMAP.md
CHANGED
|
@@ -4,7 +4,7 @@ Iris 正从 DSH 媒体插件演进为可独立运行、可接入不同 Agent 宿
|
|
|
4
4
|
|
|
5
5
|
## 当前状态
|
|
6
6
|
|
|
7
|
-
当前版本为 **Iris 0.2.
|
|
7
|
+
当前版本为 **Iris 0.2.2**。包名保持 `@mokuyoaxis/dsh-iris`,同时提供 DSH 插件与无 DSH 的 Headless CLI。本版变化见 [0.2.2 发布说明](releases/0.2.2.md)。
|
|
8
8
|
|
|
9
9
|
0.2.1 的更新重点是 **完成 OpenAI 兼容图片与视觉工作流**:生成图片 → Core Artifact → 按 ID 看图/OCR → 聊天改图 → 新 Artifact,CLI 与 DSH 工作台共用同一链路。本版已实现以下能力:
|
|
10
10
|
|
|
@@ -21,9 +21,17 @@ Node.js 最低版本保持 22.0.0,DSH 插件只声明支持准确的 `0.2.0-rc
|
|
|
21
21
|
|
|
22
22
|
## 近期优先:真实使用验收
|
|
23
23
|
|
|
24
|
+
0.2.2采用明确功能能力:移除裸 DashScope 和名称自动赋能,保留功能入口、按功能分配的有序模型池及安全 failover。工作台提供明确功能标签设置;已有配置不自动迁移。Music 接入与相关网关排查暂缓,恢复前需取得可验证的真实音频。
|
|
25
|
+
|
|
26
|
+
0.2.2 的 Core 可靠性已补齐 writer 意图与显式恢复证据,非图片交付复用文件接收和分块哈希;普通 writer 不自动抢占,未知/损坏现场继续拒绝。实现及边界见 [Core Runtime](CORE_RUNTIME_CONTRACT.md) 与 [Provider Adapter](PROVIDER_ADAPTER_CONTRACT.md)。
|
|
27
|
+
|
|
28
|
+
0.2.2 的提示词一般功能已提供折叠五用途、原文/顺序保持的忠实整理、明确改写与单独获取/采纳的建议,保留自定义 v1 配置和旧 API。特殊功能待定义,独立提示词 CLI 和英文默认指令候选未接入;详见 [提示词优化系统](PROMPT_OPTIMIZER.md)。
|
|
29
|
+
|
|
30
|
+
0.2.2 的生图尺寸归一、本地未受理校验及网络/正文诊断已实现,受理未知仍停止自动重提;不以本地代码保证公网线路稳定。见 [错误与取消传播](PROVIDER_ADAPTER_CONTRACT.md#错误与取消传播)。
|
|
31
|
+
|
|
24
32
|
0.2.1 的发布检查覆盖完整测试、lint、实际 tarball、敏感信息、仓库外安装与跨平台 CI。服务层与包安装验收之外,近期优先补齐真实浏览器/移动端及异步重启使用证据。
|
|
25
33
|
|
|
26
|
-
工作台两轮功能已实现;第三轮仍需补浏览器/移动端的筛选、分页、跨页选择、识别、改图、下载及回收区使用验收,以及真实异步任务重启后的 CLI 接管。已有真实图片生成、按 ID 看图和聊天改图链路验证,完整会话与重启范围继续以 [验证说明](DSH_CORE_MIGRATION.md#验证证据与限制)
|
|
34
|
+
工作台两轮功能已实现;第三轮仍需补浏览器/移动端的筛选、分页、跨页选择、识别、改图、下载及回收区使用验收,以及真实异步任务重启后的 CLI 接管。已有真实图片生成、按 ID 看图和聊天改图链路验证,完整会话与重启范围继续以 [验证说明](DSH_CORE_MIGRATION.md#验证证据与限制) 为准。上述变化纳入 0.2.2;完整工作台与重启范围的剩余验收仍保留。
|
|
27
35
|
|
|
28
36
|
DSH 工作台与 Headless CLI 均复用 Core 的引用保护、可恢复隔离和恢复命令;legacy `outputs/` 的永久删除仍由旧工作台处理。两种删除语义与已实现的作品管理见 [工作台作品管理](WORKBENCH_ARTIFACTS.md)。
|
|
29
37
|
|
package/docs/SECURITY.md
CHANGED
package/docs/TASK_SEMANTICS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Iris Task/Attempt v2 语义契约
|
|
2
2
|
|
|
3
|
-
状态:**v0.1.3 已实现;v0.1.4 Host/Provider Adapter、Host Doctor 与作品库 v0 已接入。** Task v2 已覆盖图片、视频、转写与 TTS,包括同步/异步、上传/提交、混合协议候选、工作台稳定用户态、状态流防倒退、重新观察、重新交付、提醒已读/恢复和知情人工重试。本文的 Task v2 字段描述 legacy 任务,不等同于 Core DTO。作品库 v0 分离旧本地作品生命周期;0.2.0 已实现 Core Task/Attempt 与 Artifact Manifest,0.2.
|
|
3
|
+
状态:**v0.1.3 已实现;v0.1.4 Host/Provider Adapter、Host Doctor 与作品库 v0 已接入。** Task v2 已覆盖图片、视频、转写与 TTS,包括同步/异步、上传/提交、混合协议候选、工作台稳定用户态、状态流防倒退、重新观察、重新交付、提醒已读/恢复和知情人工重试。本文的 Task v2 字段描述 legacy 任务,不等同于 Core DTO。作品库 v0 分离旧本地作品生命周期;0.2.0 已实现 Core Task/Attempt 与 Artifact Manifest,0.2.2 的媒体、聊天改图和维护入口共享 Core 事实,见 [Core Runtime](CORE_RUNTIME_CONTRACT.md)。
|
|
4
4
|
|
|
5
5
|
## 1. 为什么需要 v2
|
|
6
6
|
|
package/docs/VISION_CLI.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 独立视觉 CLI
|
|
2
2
|
|
|
3
|
-
状态:**Iris 0.2.
|
|
3
|
+
状态:**Iris 0.2.2**。安装 `npm install @mokuyoaxis/dsh-iris@0.2.2` 后运行 `npx dsh-iris ...`,或从源码运行 `node bin/dsh-iris.js ...`。需要 Node.js ≥ 22、已有依赖 Sharp;从视频文件抽帧还需要 PATH 中的 ffmpeg、ffprobe,复用既有 Core 帧不需要。
|
|
4
4
|
|
|
5
5
|
## 四个命令
|
|
6
6
|
|
package/docs/VISION_MODEL.md
CHANGED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Iris 0.2.2:可控提示词编辑与媒体运行可靠性
|
|
2
|
+
|
|
3
|
+
0.2.2 在既有图片与视觉工作流上,升级提示词优化的一般功能,并加固 Core 恢复、媒体交付和生图错误诊断。包名仍为 `@mokuyoaxis/dsh-iris`,Node.js 最低为 22.0.0,DSH 插件仅声明支持 `0.2.0-rc.2`;配置、Task/Artifact 版本和生产依赖保持。
|
|
4
|
+
|
|
5
|
+
## 提示词优化
|
|
6
|
+
|
|
7
|
+
- 一般功能可折叠,支持通用、画图、改图、视频与数字人五种用途,可查看草稿、改动及实际指令。
|
|
8
|
+
- 默认“忠实整理”:模型只决定相邻片段的分段位置,程序从原文按原顺序拼回;遗漏、重复或调换片段会拒绝。它保证原文片段和句序,不声称自动证明所有排版变化的语义相同。
|
|
9
|
+
- 明确选择“改写表达”才执行改写规则;改写结果仍需人工核对。
|
|
10
|
+
- 可选建议独立获取,默认不加入正文;用户选择并采纳后才改变预览,支持撤回。获取建议会额外调用一次模型,采纳/撤回不调用模型、不直接写回或发送。
|
|
11
|
+
- 保留旧自定义 v1 配置和旧文字 API,不自动改写个人配置;内置指令按 Skills 的目标、输入、约束和输出组织,S2V 明确为首帧加音频数字人。
|
|
12
|
+
- 拒绝原稿和明确规则未授权的新链接/媒体输出,保护原稿和已有预览;不自动重试。合法引用与原样组装保持。
|
|
13
|
+
|
|
14
|
+
## 功能模型池与媒体可靠性
|
|
15
|
+
|
|
16
|
+
- 能力来自明确模型标签、旧能力字段绑定或供应商能力元数据;裸 DashScope 账号和模型名称不再自动赋予功能。发现保留未标注模型,供用户设置功能标签。已有明确配置(含空标签)优先,有序候选与复合模型身份保持。
|
|
17
|
+
- Core writer 创建前原子发布完整 owner 意图,释放时整体退役目录;显式恢复保留连续步骤与审计。普通 writer 不自动抢占,活跃、未知、损坏或无 owner 的旧现场仍拒绝。需要支持 hard link 的私有文件系统,不承诺断电、共享存储或混用旧 writer 的新增保证。
|
|
18
|
+
- 视频、音频与文本 staging 复用文件接收和分块哈希,减少整文件读取;本地交付失败保留远端成功,重新交付不重新生成。图片格式验证和 inline 解码仍可能使用完整 Buffer。
|
|
19
|
+
- 生图尺寸按协议归一分隔符,DashScope 多模态只执行有证据的总像素规则,合法长边不再被误拒绝。本地尺寸错误在 HTTP 前明确为未受理。
|
|
20
|
+
- 网络异常保留请求/正文阶段与可用错误码,原生取消/超时和完整 HTTP 业务错误保持各自分类;DSH 即时错误显示脱敏原因。受理未知仍禁止自动重提收费请求,HTTP 200 响应头不等于图片已交付。
|
|
21
|
+
- 修复故障注入夹具的路径别名与分隔符假设,保持实际中断检查点验证,不通过跳过 Windows 测试绕过问题。
|
|
22
|
+
|
|
23
|
+
## 升级与边界
|
|
24
|
+
|
|
25
|
+
在 DSH `0.2.0-rc.2` 的 Web profile 使用 `dsh plugin --profile web add @mokuyoaxis/dsh-iris@0.2.2`;CLI 使用 `npm install @mokuyoaxis/dsh-iris@0.2.2`。升级后重启宿主并刷新客户端。旧宿主继续固定使用 Iris 0.1.4;其他 DSH 版本需独立验证。
|
|
26
|
+
|
|
27
|
+
仅有裸账号而没有明确功能标签的配置,升级后需要给实际模型设置功能,再加入对应有序候选池;不会自动创建默认模型或迁移个人文件。原自定义 Prompt 不被覆盖,只有显式恢复默认才采用新内置模板。
|
|
28
|
+
|
|
29
|
+
Music 继续暂缓,特殊功能待定义。英文默认 Prompt 和严格纯正文候选仍是实验,未在本版部署。原生 Gemini/Fal/Replicate、独立提示词 CLI/SDK、同步无远端 ID 的持久缓存恢复未接入;完整工作台第三轮与真实异步重启接管验收仍待补齐。网关 Gemini 兼容生图成功不等于原生 Gemini Adapter 或具体底层模型身份已验证;一次线路成功不保证持续稳定。
|
|
30
|
+
|
|
31
|
+
详细用法见 [提示词优化](../PROMPT_OPTIMIZER.md)、[Core Runtime](../CORE_RUNTIME_CONTRACT.md)、[Provider Adapter](../PROVIDER_ADAPTER_CONTRACT.md) 和 [用户指南](../../user_guide.md)。
|
package/lib/actions.js
CHANGED
|
@@ -136,7 +136,8 @@ function imageRoute(provider, args) {
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
function taskActionError(taskId, message, details) {
|
|
139
|
-
const
|
|
139
|
+
const diagnostic = details?.safeMessage ? ':' + details.safeMessage : '';
|
|
140
|
+
const error = new Error('iris: ' + message + '(task: ' + taskId + ')' + diagnostic);
|
|
140
141
|
error.taskId = taskId;
|
|
141
142
|
if (details) error.details = details;
|
|
142
143
|
return error;
|
|
@@ -1049,7 +1050,7 @@ register('providers_set_models', async (host, args) => {
|
|
|
1049
1050
|
});
|
|
1050
1051
|
|
|
1051
1052
|
/**
|
|
1052
|
-
* 模型发现:读取供应商目录 →
|
|
1053
|
+
* 模型发现:读取供应商目录 → 明确能力元数据 → 合并模型池。
|
|
1053
1054
|
* 免费只读;拉不到/不支持则原样保留现有池(优雅降级),报人话错误。
|
|
1054
1055
|
*/
|
|
1055
1056
|
register('providers_discover', async (host, args, { signal }) => {
|
|
@@ -1063,16 +1064,11 @@ register('providers_discover', async (host, args, { signal }) => {
|
|
|
1063
1064
|
} catch (err) {
|
|
1064
1065
|
throw new Error('iris: 模型发现失败(保留现有池):' + String(err.message || err));
|
|
1065
1066
|
}
|
|
1066
|
-
const
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
if (!media.length) {
|
|
1072
|
-
return { ok: true, total: discoveredModels.length, media: 0, text: `[iris] ${p.name}:拉到 ${discoveredModels.length} 个模型,无媒体能力匹配——可在模型池区手动添加并标能力` };
|
|
1073
|
-
}
|
|
1074
|
-
store.mergeDiscoveredModels(p.id, media);
|
|
1075
|
-
return { ok: true, total: discoveredModels.length, media: media.length, text: `[iris] ${p.name}:发现 ${discoveredModels.length} 个模型,${media.length} 个媒体能力模型已入池(官方元数据优先)` };
|
|
1067
|
+
const entries = discoveredModels.map((model) => ({ id: model.id, capabilities: models.capabilitiesOfDiscoveredModel(model) }));
|
|
1068
|
+
const mediaCount = entries.filter((model) => model.capabilities.length).length;
|
|
1069
|
+
store.mergeDiscoveredModels(p.id, entries);
|
|
1070
|
+
return { ok: true, total: discoveredModels.length, media: mediaCount,
|
|
1071
|
+
text: `[iris] ${p.name}:发现 ${entries.length} 个模型,${mediaCount} 个有能力声明;未标注模型请在模型池中设置能力` };
|
|
1076
1072
|
});
|
|
1077
1073
|
|
|
1078
1074
|
/** 能力实测(红色测试图):对某供应商的 vision 模型跑 testVisionCapability */
|
|
@@ -1177,17 +1173,17 @@ async function probeModel(host, p, modelId, capability, signal) {
|
|
|
1177
1173
|
return { ok: false, note: '未知能力' };
|
|
1178
1174
|
}
|
|
1179
1175
|
|
|
1180
|
-
/**
|
|
1176
|
+
/** 手动添加模型到池;省略或清空能力时保留未标注模型。 */
|
|
1181
1177
|
register('providers_add_model', async (host, args) => {
|
|
1182
1178
|
const id = String(args.id || '');
|
|
1183
1179
|
const modelId = String(args.model_id || '').trim();
|
|
1184
1180
|
if (!modelId) throw new Error('iris: model_id 不能为空');
|
|
1185
|
-
const caps = Array.isArray(args.capabilities)
|
|
1181
|
+
const caps = Array.isArray(args.capabilities)
|
|
1186
1182
|
? args.capabilities.filter((c) => cap.isKnownCapability(c))
|
|
1187
|
-
:
|
|
1183
|
+
: undefined;
|
|
1188
1184
|
const entry = store.addProviderModel(id, modelId, caps);
|
|
1189
1185
|
if (!entry) throw new Error('iris: 供应商不存在');
|
|
1190
|
-
return { ok: true, text: `[iris] 已添加模型 ${modelId}(能力:${caps.join(',') || '未标注'})` };
|
|
1186
|
+
return { ok: true, text: `[iris] 已添加模型 ${modelId}(能力:${(caps || []).join(',') || '未标注'})` };
|
|
1191
1187
|
});
|
|
1192
1188
|
|
|
1193
1189
|
/** 从池移除模型 */
|
package/lib/adapters.js
CHANGED
|
@@ -11,7 +11,7 @@ import fs from 'node:fs';
|
|
|
11
11
|
import path from 'node:path';
|
|
12
12
|
import { Readable } from 'node:stream';
|
|
13
13
|
import { pipeline } from 'node:stream/promises';
|
|
14
|
-
import { dashscopeApiBase, isDashScopeBaseUrl } from './provider-protocol.js';
|
|
14
|
+
import { dashscopeApiBase, imageSizeViolation, isDashScopeBaseUrl, normalizeImageSize } from './provider-protocol.js';
|
|
15
15
|
import { chmodPrivateFile, ensurePrivateDir, privateSibling } from './private-storage.js';
|
|
16
16
|
import { isFreeQuotaExhausted, retryAfterDelay } from './provider-health.js';
|
|
17
17
|
import { ProviderContractError } from './provider-contract.js';
|
|
@@ -95,17 +95,65 @@ function classifyHttpError(status, body, fallback, headers) {
|
|
|
95
95
|
...(status === 429 ? { retryAfterMs: retryAfterDelay(headers?.get('retry-after')) } : {}) });
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
/** 包装 fetch 层的网络异常(无 HTTP
|
|
99
|
-
export function networkError(err) {
|
|
98
|
+
/** 包装 fetch 层的网络异常(无 HTTP 状态)。hint 补一句可执行的排查方向。 */
|
|
99
|
+
export function networkError(err, hint) {
|
|
100
|
+
const code = String(err?.cause?.code || err?.code || '');
|
|
100
101
|
const e = new ProviderError({
|
|
101
102
|
category: 'network',
|
|
102
103
|
status: 0,
|
|
103
|
-
message: '网络错误:' + String((err && err.message) || err),
|
|
104
|
-
rootCause: err
|
|
104
|
+
message: '网络错误:' + String((err && err.message) || err) + (hint ? '(' + hint + ')' : ''),
|
|
105
|
+
rootCause: err,
|
|
106
|
+
...(/^[A-Z][A-Z0-9_]{1,63}$/.test(code) ? { providerCode: code } : {})
|
|
105
107
|
});
|
|
106
108
|
return e;
|
|
107
109
|
}
|
|
108
110
|
|
|
111
|
+
/** 超时/取消必须保持原分类:轮询 errStreak 容错与 submitGuard 都依赖 network 与 timeout 的区分。 */
|
|
112
|
+
function isAbortLike(err) {
|
|
113
|
+
return err?.name === 'AbortError' || err?.name === 'TimeoutError' || err?.code === 'ABORT_ERR';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function transportFetchError(err, response) {
|
|
117
|
+
if (isAbortLike(err)) return err;
|
|
118
|
+
const code = String(err?.cause?.code || err?.code || '');
|
|
119
|
+
const detail = {
|
|
120
|
+
ENOTFOUND: 'DNS 未找到目标主机',
|
|
121
|
+
EAI_AGAIN: 'DNS 解析暂时失败',
|
|
122
|
+
ECONNREFUSED: '目标连接被拒绝',
|
|
123
|
+
UND_ERR_CONNECT_TIMEOUT: '连接建立超时',
|
|
124
|
+
UND_ERR_HEADERS_TIMEOUT: '等待响应头超时',
|
|
125
|
+
UND_ERR_BODY_TIMEOUT: '响应正文读取超时',
|
|
126
|
+
ETIMEDOUT: '网络操作超时',
|
|
127
|
+
ECONNRESET: '连接被重置',
|
|
128
|
+
UND_ERR_SOCKET: '底层连接中断',
|
|
129
|
+
CERT_HAS_EXPIRED: 'TLS 证书已过期',
|
|
130
|
+
DEPTH_ZERO_SELF_SIGNED_CERT: 'TLS 证书不受信任',
|
|
131
|
+
UNABLE_TO_VERIFY_LEAF_SIGNATURE: 'TLS 证书链验证失败'
|
|
132
|
+
}[code];
|
|
133
|
+
const phase = response
|
|
134
|
+
? `已收到 HTTP ${response.status} 响应头,读取响应正文失败`
|
|
135
|
+
: '请求传输失败,尚未收到响应头';
|
|
136
|
+
const error = networkError(err, phase + (detail ? ';' + detail : '') +
|
|
137
|
+
(code && /^[A-Z][A-Z0-9_]{1,63}$/.test(code) ? ';' + code : '') + ';受理结果未知,禁止自动重提');
|
|
138
|
+
if (['UND_ERR_CONNECT_TIMEOUT', 'UND_ERR_HEADERS_TIMEOUT', 'UND_ERR_BODY_TIMEOUT', 'ETIMEDOUT'].includes(code)) {
|
|
139
|
+
error.category = 'timeout';
|
|
140
|
+
}
|
|
141
|
+
error.stage = response ? 'response' : 'submit';
|
|
142
|
+
error.acceptance = 'unknown';
|
|
143
|
+
return error;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** 只包装网络读取;HTTP 业务错误和图片协议校验由调用方处理。 */
|
|
147
|
+
async function fetchImageJson(url, options) {
|
|
148
|
+
let response;
|
|
149
|
+
try {
|
|
150
|
+
response = await fetch(url, options);
|
|
151
|
+
return { res: response, body: await readJson(response) };
|
|
152
|
+
} catch (err) {
|
|
153
|
+
throw transportFetchError(err, response);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
109
157
|
function providerError(status, body, fallback, headers) {
|
|
110
158
|
return classifyHttpError(status, body, fallback, headers);
|
|
111
159
|
}
|
|
@@ -125,20 +173,34 @@ export function dashscopeImageMode(model) {
|
|
|
125
173
|
return 'legacy-async';
|
|
126
174
|
}
|
|
127
175
|
|
|
176
|
+
/**
|
|
177
|
+
* 尺寸在进入请求体前统一「归一 + 校验」:
|
|
178
|
+
* 协议差异(分隔符、数值边界)全部来自 provider-protocol.js,本地违规明确标记为未受理。
|
|
179
|
+
* 归一的意义是跨协议可用 —— 例如把 `1024x1536` 送给 DashScope 多模态(服务端要求 `*`),
|
|
180
|
+
* 或把 `1024*1536` 送给 OpenAI 兼容端点(服务端要求 `x`),都不再需要调用方自己记格式。
|
|
181
|
+
*/
|
|
182
|
+
export function applyImageSize({ protocol, mode, size }) {
|
|
183
|
+
const violation = imageSizeViolation({ protocol, mode, size });
|
|
184
|
+
if (violation) throw new ProviderContractError(violation, {
|
|
185
|
+
stage: 'validate', category: 'invalid_request', acceptance: 'not_accepted'
|
|
186
|
+
});
|
|
187
|
+
return normalizeImageSize({ protocol, mode, size });
|
|
188
|
+
}
|
|
189
|
+
|
|
128
190
|
/** 新版多模态同步协议:响应中直接返回图片 URL。 */
|
|
129
191
|
export async function generateImageMultimodal({ key, baseUrl, model, prompt, size, n = 1, timeoutMs = T_SYNC_GEN, signal }) {
|
|
130
192
|
const apiBase = dashscopeApiBase(baseUrl);
|
|
131
|
-
const
|
|
193
|
+
const requestSize = applyImageSize({ protocol: 'dashscope', mode: 'multimodal-sync', size });
|
|
194
|
+
const { res, body } = await fetchImageJson(`${apiBase}/services/aigc/multimodal-generation/generation`, {
|
|
132
195
|
method: 'POST',
|
|
133
196
|
headers: authHeaders(key),
|
|
134
197
|
signal: timeoutSignal(timeoutMs, signal),
|
|
135
198
|
body: JSON.stringify({
|
|
136
199
|
model,
|
|
137
200
|
input: { messages: [{ role: 'user', content: [{ text: prompt }] }] },
|
|
138
|
-
parameters: { ...(
|
|
201
|
+
parameters: { ...(requestSize ? { size: requestSize } : {}), n: Number(n) || 1 }
|
|
139
202
|
})
|
|
140
203
|
});
|
|
141
|
-
const body = await readJson(res);
|
|
142
204
|
if (!res.ok) throw providerError(res.status, body, '生成失败', res.headers);
|
|
143
205
|
const urls = [];
|
|
144
206
|
for (const choice of (body && body.output && body.output.choices) || []) {
|
|
@@ -160,13 +222,13 @@ export async function startImageGeneration(opts) {
|
|
|
160
222
|
}
|
|
161
223
|
export async function submitImage({ key, baseUrl, model, prompt, size, n = 1, timeoutMs = T_SUBMIT, signal }) {
|
|
162
224
|
const apiBase = dashscopeApiBase(baseUrl);
|
|
163
|
-
const
|
|
225
|
+
const requestSize = applyImageSize({ protocol: 'dashscope', mode: 'legacy-async', size });
|
|
226
|
+
const { res, body } = await fetchImageJson(`${apiBase}/services/aigc/text2image/image-synthesis`, {
|
|
164
227
|
method: 'POST',
|
|
165
228
|
headers: { ...authHeaders(key), 'X-DashScope-Async': 'enable' },
|
|
166
229
|
signal: timeoutSignal(timeoutMs, signal),
|
|
167
|
-
body: JSON.stringify({ model, input: { prompt }, parameters: { ...(
|
|
230
|
+
body: JSON.stringify({ model, input: { prompt }, parameters: { ...(requestSize ? { size: requestSize } : {}), n: Number(n) || 1 } })
|
|
168
231
|
});
|
|
169
|
-
const body = await readJson(res);
|
|
170
232
|
if (!res.ok) throw providerError(res.status, body, '提交失败', res.headers);
|
|
171
233
|
const taskId = body && body.output && body.output.task_id;
|
|
172
234
|
if (!taskId) throw new Error('提交成功但未返回 task_id');
|
|
@@ -312,13 +374,13 @@ export async function synthesizeTts({ key, baseUrl, model = 'qwen-tts-latest', t
|
|
|
312
374
|
/* ---------------- OpenAI Images 兼容(同步 base64/url) ---------------- */
|
|
313
375
|
export async function openAiGenerateImage({ key, baseUrl, model, prompt, size = '1024x1024', n = 1, timeoutMs = T_SYNC_GEN, signal }) {
|
|
314
376
|
const base = String(baseUrl || '').replace(/\/+$/, '');
|
|
315
|
-
const
|
|
377
|
+
const requestSize = applyImageSize({ protocol: 'openai-images', size });
|
|
378
|
+
const { res, body } = await fetchImageJson(`${base}/images/generations`, {
|
|
316
379
|
method: 'POST',
|
|
317
380
|
headers: authHeaders(key),
|
|
318
381
|
signal: timeoutSignal(timeoutMs, signal),
|
|
319
|
-
body: JSON.stringify({ model, prompt, size, n: Number(n) || 1 })
|
|
382
|
+
body: JSON.stringify({ model, prompt, size: requestSize, n: Number(n) || 1 })
|
|
320
383
|
});
|
|
321
|
-
const body = await readJson(res);
|
|
322
384
|
if (!res.ok) throw providerError(res.status, body, '生成失败', res.headers);
|
|
323
385
|
const items = (body && Array.isArray(body.data) && body.data) || [];
|
|
324
386
|
const out = [];
|
|
@@ -389,12 +451,11 @@ export async function openAiChatGenerateImage({ key, baseUrl, model, prompt, ima
|
|
|
389
451
|
{ type: 'text', text: prompt },
|
|
390
452
|
{ type: 'image_url', image_url: { url: 'data:' + image.mediaType + ';base64,' + Buffer.from(image.bytes).toString('base64') } }
|
|
391
453
|
] : prompt;
|
|
392
|
-
const res = await
|
|
454
|
+
const { res, body } = await fetchImageJson(`${base}/chat/completions`, {
|
|
393
455
|
method: 'POST', headers: authHeaders(key), signal: timeoutSignal(timeoutMs, signal),
|
|
394
456
|
body: JSON.stringify({ model, stream: false, messages: [{ role: 'user', content }],
|
|
395
457
|
...(Number(n) > 1 ? { n: Number(n) } : {}) })
|
|
396
458
|
});
|
|
397
|
-
const body = await readJson(res);
|
|
398
459
|
if (!res.ok) throw providerError(res.status, body, '聊天生图失败', res.headers);
|
|
399
460
|
return chatImageItems(body);
|
|
400
461
|
}
|
|
@@ -407,13 +468,13 @@ export async function openAiResponsesGenerateImage({ key, baseUrl, model, prompt
|
|
|
407
468
|
});
|
|
408
469
|
}
|
|
409
470
|
const base = String(baseUrl || '').replace(/\/+$/, '');
|
|
410
|
-
const
|
|
471
|
+
const requestSize = applyImageSize({ protocol: 'openai-responses-images', size });
|
|
472
|
+
const { res, body } = await fetchImageJson(`${base}/responses`, {
|
|
411
473
|
method: 'POST', headers: authHeaders(key), signal: timeoutSignal(timeoutMs, signal),
|
|
412
474
|
body: JSON.stringify({ model, input: prompt, stream: false, store: false, background: false,
|
|
413
|
-
tools: [{ type: 'image_generation', action: 'generate', ...(
|
|
475
|
+
tools: [{ type: 'image_generation', action: 'generate', ...(requestSize ? { size: requestSize } : {}) }],
|
|
414
476
|
tool_choice: { type: 'image_generation' } })
|
|
415
477
|
});
|
|
416
|
-
const body = await readJson(res);
|
|
417
478
|
if (!res.ok) throw providerError(res.status, body, 'Responses 生图失败', res.headers);
|
|
418
479
|
if (body?.status !== 'completed') throw imageResponseError('Responses 生图未返回同步完成结果;禁止自动重提');
|
|
419
480
|
const output = Array.isArray(body.output) ? body.output : [];
|
|
@@ -469,11 +530,14 @@ export async function listModels({ key, baseUrl, timeoutMs = T_SUBMIT, signal })
|
|
|
469
530
|
? item.inference_metadata.request_modality.map((value) => String(value).toLowerCase()) : [];
|
|
470
531
|
const responseModalities = Array.isArray(item?.inference_metadata?.response_modality)
|
|
471
532
|
? item.inference_metadata.response_modality.map((value) => String(value).toLowerCase()) : [];
|
|
472
|
-
|
|
473
|
-
if (
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
533
|
+
// 明确能力代码(含空数组/未知新能力)优先,不能被通用模态推断追加用途。
|
|
534
|
+
if (!Array.isArray(item?.capabilities)) {
|
|
535
|
+
if (responseModalities.includes('image')) capabilities.push('IG');
|
|
536
|
+
if (responseModalities.includes('video')) capabilities.push('VG');
|
|
537
|
+
if (requestModalities.includes('audio') && responseModalities.includes('text')) capabilities.push('ASR');
|
|
538
|
+
if (requestModalities.includes('text') && responseModalities.includes('audio')) capabilities.push('TTS');
|
|
539
|
+
if (requestModalities.includes('image') && responseModalities.includes('text')) capabilities.push('VU');
|
|
540
|
+
}
|
|
477
541
|
const normalizedCapabilities = [...new Set(capabilities)];
|
|
478
542
|
found.set(id, normalizedCapabilities.length ? { id, capabilities: normalizedCapabilities } : id);
|
|
479
543
|
}
|
package/lib/capability.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - 能力常量:image-gen / video-gen / tts / transcribe / vision
|
|
7
7
|
* - 严格选择:不具备某能力的 Provider 不得被兜底选中
|
|
8
8
|
* - 有序 failover:同一能力按 providers.json 声明顺序返回,调用方按序尝试
|
|
9
|
-
* -
|
|
9
|
+
* - 能力只来自明确标签或旧模型字段绑定,不按供应商端点赋能
|
|
10
10
|
*
|
|
11
11
|
* 本模块是纯函数,不读写磁盘;只吃 provider 记录、吐能力判断。
|
|
12
12
|
*/
|
|
@@ -30,36 +30,43 @@ const INFER_FIELD = {
|
|
|
30
30
|
[CAPABILITIES.VISION]: 'visionModel'
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
+
/** 模型级明确标签优先;旧能力字段只为同名模型提供明确绑定。 */
|
|
34
|
+
export function configuredModelCapabilities(provider, model) {
|
|
35
|
+
if (Array.isArray(model?.capabilities)) {
|
|
36
|
+
return [...new Set(model.capabilities.filter(isKnownCapability))];
|
|
37
|
+
}
|
|
38
|
+
const id = typeof model === 'string' ? model.trim() : String(model?.id || '').trim();
|
|
39
|
+
if (!id) return [];
|
|
40
|
+
const bound = Object.entries(INFER_FIELD)
|
|
41
|
+
.filter(([, field]) => typeof provider?.[field] === 'string' && provider[field].trim() === id)
|
|
42
|
+
.map(([capability]) => capability);
|
|
43
|
+
return Array.isArray(provider?.capabilities) ? bound.filter((capability) => provider.capabilities.includes(capability)) : bound;
|
|
44
|
+
}
|
|
45
|
+
|
|
33
46
|
export function isKnownCapability(capability) {
|
|
34
47
|
return KNOWN.has(capability);
|
|
35
48
|
}
|
|
36
49
|
|
|
37
50
|
/**
|
|
38
51
|
* 一个 provider 的能力列表(去重、只留已知能力)。
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* ② 否则按模型字段推断(imageModel→image-gen 等);
|
|
42
|
-
* ③ 仍为空且是 DashScope 端点 → 全部媒体能力
|
|
43
|
-
* (百炼单一 API 服务 t2i/t2v/tts/compatible-vision,裸账号常见于迁移期)。
|
|
52
|
+
* 显式模型池优先,包括空池;其次是账号明确标签(含空数组),
|
|
53
|
+
* 最后兼容旧能力字段绑定。账号地址和模型名称都不证明能力。
|
|
44
54
|
* @param {object} p provider 记录
|
|
45
55
|
* @returns {string[]}
|
|
46
56
|
*/
|
|
47
57
|
export function capabilitiesOf(p) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
58
|
+
if (Array.isArray(p?.models)) {
|
|
59
|
+
return [...new Set(p.models.flatMap((model) => configuredModelCapabilities(p, model)))];
|
|
60
|
+
}
|
|
61
|
+
if (Array.isArray(p?.capabilities)) {
|
|
62
|
+
return [...new Set(p.capabilities.filter(isKnownCapability))];
|
|
51
63
|
}
|
|
52
64
|
// 旧配置迁移:按模型字段推断(有该能力模型才认为具备该能力)
|
|
53
65
|
const inferred = [];
|
|
54
66
|
for (const [cap, field] of Object.entries(INFER_FIELD)) {
|
|
55
|
-
if (p && p[field]) inferred.push(cap);
|
|
56
|
-
}
|
|
57
|
-
if (inferred.length) return inferred;
|
|
58
|
-
// 裸 DashScope 账号兜底迁移
|
|
59
|
-
if (p && /dashscope/i.test(String(p.baseUrl || ''))) {
|
|
60
|
-
return Object.values(CAPABILITIES);
|
|
67
|
+
if (typeof p?.[field] === 'string' && p[field].trim()) inferred.push(cap);
|
|
61
68
|
}
|
|
62
|
-
return
|
|
69
|
+
return inferred;
|
|
63
70
|
}
|
|
64
71
|
|
|
65
72
|
/**
|