@mokuyoaxis/dsh-iris 0.1.3 → 0.1.4

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.
Files changed (51) hide show
  1. package/.dsh/skills/iris-compose-media/SKILL.md +23 -49
  2. package/.dsh/skills/iris-compose-media/references/task-v2-recovery.md +30 -0
  3. package/.dsh/skills/iris-compose-media/references/workflows.md +38 -0
  4. package/.dsh/skills/iris-verify-ui/SKILL.md +3 -25
  5. package/.dsh/skills/iris-verify-ui/agents/openai.yaml +4 -0
  6. package/.dsh/skills/iris-verify-ui/references/verdict-and-degradation.md +31 -0
  7. package/CHANGELOG.md +24 -2
  8. package/README.en.md +29 -7
  9. package/README.md +29 -7
  10. package/docs/ARCHITECTURE.md +27 -13
  11. package/docs/ARTIFACT_LIBRARY.md +24 -0
  12. package/docs/HOST_ADAPTER_CONTRACT.md +86 -0
  13. package/docs/HOST_DOCTOR.md +47 -0
  14. package/docs/PROVIDER_ADAPTER_CONTRACT.md +97 -0
  15. package/docs/PROVIDER_HEALTH.md +39 -0
  16. package/docs/PROVIDER_SUBMISSION_CONTRACT.md +2 -2
  17. package/docs/ROADMAP.md +18 -8
  18. package/docs/SECURITY.md +6 -2
  19. package/docs/TASK_SEMANTICS.md +3 -2
  20. package/docs/assets/diagrams/iris-architecture.png +0 -0
  21. package/docs/assets/diagrams/iris-architecture.svg +5 -5
  22. package/docs/releases/0.1.4.md +59 -0
  23. package/lib/actions.js +357 -221
  24. package/lib/adapters.js +7 -7
  25. package/lib/api.js +30 -11
  26. package/lib/artifacts.js +239 -0
  27. package/lib/bundled-skills.js +5 -5
  28. package/lib/client.js +370 -55
  29. package/lib/config.js +192 -9
  30. package/lib/doctor.js +175 -4
  31. package/lib/dsh-host-adapter.js +189 -0
  32. package/lib/host-contract.js +131 -0
  33. package/lib/host-runtime.js +156 -0
  34. package/lib/index.js +97 -113
  35. package/lib/locate.js +1 -1
  36. package/lib/media.js +16 -6
  37. package/lib/prompt-optimizer.js +10 -20
  38. package/lib/provider-adapter.js +371 -0
  39. package/lib/provider-adapters.js +279 -0
  40. package/lib/provider-health.js +59 -0
  41. package/lib/render.js +43 -12
  42. package/lib/tasks.js +29 -5
  43. package/lib/vision.js +23 -49
  44. package/package.json +14 -8
  45. package/user_guide.md +42 -2
  46. package/docs/assets/diagrams/iris-architecture.drawio +0 -1
  47. package/docs/assets/diagrams/iris-task-lifecycle.drawio +0 -1
  48. package/docs/assets/diagrams/iris-workflow-compose.drawio +0 -1
  49. package/docs/releases/0.1.1.md +0 -51
  50. package/docs/releases/0.1.2.md +0 -26
  51. package/docs/releases/0.1.3.md +0 -30
@@ -0,0 +1,279 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * 已支持协议到 Provider Adapter v0 的映射。
5
+ *
6
+ * 凭据和 baseUrl 只被操作闭包捕获,不进入 Adapter 描述或能力快照。
7
+ * transport 默认为现有低层 HTTP 实现;测试可注入零网络 fixture。
8
+ */
9
+ import * as defaultTransport from './adapters.js';
10
+ import { ProviderContractError, providerErrorRecord } from './provider-contract.js';
11
+ import { defineProviderAdapter } from './provider-adapter.js';
12
+ import { inferMediaProtocol } from './provider-protocol.js';
13
+
14
+ const AMBIGUOUS_4XX = new Set([408, 409, 425, 499]);
15
+
16
+ function providerIdentity(provider) {
17
+ const id = String(provider?.id || '').trim();
18
+ const key = String(provider?.apiKey || '');
19
+ const baseUrl = String(provider?.baseUrl || '').trim();
20
+ if (!id) throw new TypeError('Provider 配置缺少 id');
21
+ if (!baseUrl) throw new TypeError('Provider 配置缺少 baseUrl');
22
+ return { id, key, baseUrl };
23
+ }
24
+
25
+ function mappedCategory(error, status) {
26
+ const raw = String(error?.category || '').toLowerCase();
27
+ if (error?.name === 'AbortError' || error?.code === 'ABORT_ERR') return 'aborted';
28
+ if (error?.name === 'TimeoutError' || /timeout|timed out|超时/i.test(String(error?.message || ''))) return 'timeout';
29
+ if (status === 401 || status === 403 || raw === 'auth') return 'authentication';
30
+ if (status === 429 && raw === 'quota') return 'quota';
31
+ if (status === 429 || raw === 'rate_limit') return 'rate_limit';
32
+ if (raw === 'invalid_parameter') return 'invalid_request';
33
+ if (raw === 'network' || error instanceof TypeError) return 'network';
34
+ if (raw === 'server' || status >= 500) return 'provider';
35
+ if (!status && /未返回|响应|协议|结构|json/i.test(String(error?.message || ''))) return 'protocol';
36
+ return 'provider';
37
+ }
38
+
39
+ /** 当前两个协议共享的错误映射;不保留 cause、rootCause、正文、URL 或凭据。 */
40
+ export function mapProviderAdapterError(error, context = {}) {
41
+ const statusValue = Number(error?.httpStatus ?? error?.status);
42
+ const httpStatus = Number.isInteger(statusValue) && statusValue >= 100 && statusValue <= 599
43
+ ? statusValue
44
+ : undefined;
45
+ const stage = String(context.stage || error?.stage || 'submit');
46
+ const category = mappedCategory(error, httpStatus);
47
+ let acceptance = context.acceptance || error?.acceptance;
48
+ if (stage === 'validate' || stage === 'prepare' || stage === 'upload') {
49
+ acceptance = 'not_accepted';
50
+ } else if (stage === 'submit') {
51
+ acceptance = httpStatus && httpStatus >= 400 && httpStatus < 500 && !AMBIGUOUS_4XX.has(httpStatus)
52
+ ? 'not_accepted'
53
+ : (error?.acceptance === 'not_accepted' ? 'not_accepted' : 'unknown');
54
+ } else if (!acceptance) {
55
+ acceptance = 'accepted';
56
+ }
57
+ const retryable = Boolean(
58
+ context.retryable ?? error?.retryable
59
+ ?? (httpStatus === 429 || (httpStatus && httpStatus >= 500)
60
+ || category === 'network' || category === 'timeout')
61
+ );
62
+ return providerErrorRecord(error, {
63
+ stage,
64
+ category,
65
+ acceptance,
66
+ retryable,
67
+ ...(httpStatus ? { httpStatus } : {})
68
+ });
69
+ }
70
+
71
+ function unsupportedCapability(capability, protocol) {
72
+ return new ProviderContractError(
73
+ '协议 ' + protocol + ' 不支持任务能力 ' + String(capability || ''),
74
+ { stage: 'validate', category: 'invalid_request', acceptance: 'not_accepted' }
75
+ );
76
+ }
77
+
78
+ function pollFailureKind(status) {
79
+ const value = String(status || '').toUpperCase();
80
+ if (value === 'CANCELED' || value === 'CANCELLED') return 'canceled';
81
+ if (value === 'UNKNOWN') return 'unknown';
82
+ return 'failed';
83
+ }
84
+
85
+ function operationsForDashScope(provider, transport) {
86
+ const { key, baseUrl } = providerIdentity(provider);
87
+ return {
88
+ async discover({ signal, timeoutMs } = {}) {
89
+ return { models: await transport.listModels({ key, baseUrl, signal, timeoutMs }) };
90
+ },
91
+
92
+ async submit(request = {}) {
93
+ const input = request.input || {};
94
+ const common = {
95
+ key, baseUrl, model: request.model, signal: request.signal,
96
+ ...(request.timeoutMs ? { timeoutMs: request.timeoutMs } : {})
97
+ };
98
+ if (request.capability === 'image') {
99
+ if (transport.dashscopeImageMode(request.model) === 'legacy-async') {
100
+ const remoteTaskId = await transport.submitImage({
101
+ ...common, prompt: input.prompt, size: input.size, n: input.n
102
+ });
103
+ return { kind: 'accepted', remoteTaskId };
104
+ }
105
+ const urls = await transport.generateImageMultimodal({
106
+ ...common, prompt: input.prompt, size: input.size, n: input.n
107
+ });
108
+ return { kind: 'completed', value: { kind: 'urls', items: urls } };
109
+ }
110
+ if (request.capability === 'video') {
111
+ const remoteTaskId = await transport.submitVideo({
112
+ ...common,
113
+ prompt: input.prompt,
114
+ imgDataUrl: input.imgDataUrl,
115
+ size: input.size,
116
+ duration: input.duration,
117
+ audioUrl: input.audioUrl,
118
+ resolution: input.resolution
119
+ });
120
+ return { kind: 'accepted', remoteTaskId };
121
+ }
122
+ if (request.capability === 'transcribe') {
123
+ const remoteTaskId = await transport.submitTranscription({
124
+ ...common, audioUrl: input.audioUrl
125
+ });
126
+ return { kind: 'accepted', remoteTaskId };
127
+ }
128
+ if (request.capability === 'tts') {
129
+ const value = await transport.synthesizeTts({
130
+ ...common, text: input.text, voice: input.voice
131
+ });
132
+ return { kind: 'completed', value };
133
+ }
134
+ throw unsupportedCapability(request.capability, 'dashscope');
135
+ },
136
+
137
+ async poll(request = {}) {
138
+ const common = {
139
+ key, baseUrl, remoteTaskId: request.remoteTaskId, signal: request.signal,
140
+ ...(request.timeoutMs ? { timeoutMs: request.timeoutMs } : {})
141
+ };
142
+ if (request.capability === 'transcribe') {
143
+ const result = await transport.pollTranscriptionTask(common);
144
+ if (!result.done) return { kind: 'pending', progress: result.status || 'running' };
145
+ if (!result.ok) {
146
+ return {
147
+ kind: pollFailureKind(result.status),
148
+ error: mapProviderAdapterError(result.message || '转写任务失败', {
149
+ stage: 'poll', acceptance: 'accepted'
150
+ })
151
+ };
152
+ }
153
+ return { kind: 'succeeded', value: { kind: 'text', text: String(result.text || '') } };
154
+ }
155
+ if (request.capability === 'image' || request.capability === 'video') {
156
+ const result = await transport.pollTask(common);
157
+ if (!result.done) return { kind: 'pending', progress: result.status || 'running' };
158
+ if (!result.ok) {
159
+ return {
160
+ kind: pollFailureKind(result.status),
161
+ error: mapProviderAdapterError(result.message || '生成任务失败', {
162
+ stage: 'poll', acceptance: 'accepted'
163
+ })
164
+ };
165
+ }
166
+ return {
167
+ kind: 'succeeded',
168
+ artifacts: (result.urls || []).map((url) => ({ kind: 'remote-url', url }))
169
+ };
170
+ }
171
+ throw unsupportedCapability(request.capability, 'dashscope');
172
+ },
173
+
174
+ async download(request = {}) {
175
+ const artifact = request.artifact || {};
176
+ if (artifact.kind !== 'remote-url' || !artifact.url) {
177
+ throw new ProviderContractError('download 需要 remote-url artifact', {
178
+ stage: 'download', category: 'protocol', acceptance: 'accepted'
179
+ });
180
+ }
181
+ const targetPath = String(request.targetPath || '').trim();
182
+ if (!targetPath) {
183
+ throw new ProviderContractError('download 需要 targetPath', {
184
+ stage: 'download', category: 'local_io', acceptance: 'accepted'
185
+ });
186
+ }
187
+ return {
188
+ bytes: await transport.downloadTo(artifact.url, targetPath, {
189
+ signal: request.signal,
190
+ ...(request.timeoutMs ? { timeoutMs: request.timeoutMs } : {})
191
+ })
192
+ };
193
+ },
194
+
195
+ mapError: mapProviderAdapterError
196
+ };
197
+ }
198
+
199
+ export function createDashScopeProviderAdapter(provider, { transport = defaultTransport } = {}) {
200
+ return defineProviderAdapter({
201
+ id: providerIdentity(provider).id,
202
+ protocol: 'dashscope',
203
+ capabilities: ['image', 'video', 'tts', 'transcribe'],
204
+ operations: operationsForDashScope(provider, transport),
205
+ unsupported: {
206
+ cancel: '当前 DashScope 媒体协议未提供经过验证的远端取消实现'
207
+ }
208
+ });
209
+ }
210
+
211
+ function operationsForOpenAiImages(provider, transport) {
212
+ const { key, baseUrl } = providerIdentity(provider);
213
+ return {
214
+ async discover({ signal, timeoutMs } = {}) {
215
+ return { models: await transport.listModels({ key, baseUrl, signal, timeoutMs }) };
216
+ },
217
+
218
+ async submit(request = {}) {
219
+ if (request.capability !== 'image') {
220
+ throw unsupportedCapability(request.capability, 'openai-images');
221
+ }
222
+ const input = request.input || {};
223
+ const items = await transport.openAiGenerateImage({
224
+ key,
225
+ baseUrl,
226
+ model: request.model,
227
+ prompt: input.prompt,
228
+ size: input.size,
229
+ n: input.n,
230
+ signal: request.signal,
231
+ ...(request.timeoutMs ? { timeoutMs: request.timeoutMs } : {})
232
+ });
233
+ return { kind: 'completed', value: { kind: 'openai', items } };
234
+ },
235
+
236
+ async download(request = {}) {
237
+ const artifact = request.artifact || {};
238
+ if (artifact.kind !== 'remote-url' || !artifact.url) {
239
+ throw new ProviderContractError('download 需要 remote-url artifact', {
240
+ stage: 'download', category: 'protocol', acceptance: 'accepted'
241
+ });
242
+ }
243
+ const targetPath = String(request.targetPath || '').trim();
244
+ if (!targetPath) {
245
+ throw new ProviderContractError('download 需要 targetPath', {
246
+ stage: 'download', category: 'local_io', acceptance: 'accepted'
247
+ });
248
+ }
249
+ return {
250
+ bytes: await transport.downloadTo(artifact.url, targetPath, {
251
+ signal: request.signal,
252
+ ...(request.timeoutMs ? { timeoutMs: request.timeoutMs } : {})
253
+ })
254
+ };
255
+ },
256
+
257
+ mapError: mapProviderAdapterError
258
+ };
259
+ }
260
+
261
+ export function createOpenAiImagesProviderAdapter(provider, { transport = defaultTransport } = {}) {
262
+ return defineProviderAdapter({
263
+ id: providerIdentity(provider).id,
264
+ protocol: 'openai-images',
265
+ capabilities: ['image'],
266
+ operations: operationsForOpenAiImages(provider, transport),
267
+ unsupported: {
268
+ poll: 'OpenAI Images 兼容路径在当前实现中同步返回结果',
269
+ cancel: '同步图片响应没有可验证的远端取消任务'
270
+ }
271
+ });
272
+ }
273
+
274
+ export function createConfiguredProviderAdapter(provider, options = {}) {
275
+ const protocol = options.protocol || provider?.mediaProtocol || inferMediaProtocol(provider?.baseUrl);
276
+ if (protocol === 'dashscope') return createDashScopeProviderAdapter(provider, options);
277
+ if (protocol === 'openai-images') return createOpenAiImagesProviderAdapter(provider, options);
278
+ throw new TypeError('iris: 不支持的 Provider 协议:' + String(protocol));
279
+ }
@@ -0,0 +1,59 @@
1
+ 'use strict';
2
+
3
+ export const PROVIDER_HEALTH_SCHEMA_VERSION = 1;
4
+ export const PROVIDER_HEALTH_FRESH_MS = 7 * 24 * 60 * 60 * 1000;
5
+ export const PROVIDER_HEALTH_STATES = Object.freeze([
6
+ 'unconfigured',
7
+ 'configured',
8
+ 'verified',
9
+ 'failed'
10
+ ]);
11
+
12
+ const DEFINITIVE_FAILURES = new Set(['authentication', 'auth', 'permission']);
13
+
14
+ function validTime(value) {
15
+ const ms = Date.parse(String(value || ''));
16
+ return Number.isFinite(ms) ? ms : 0;
17
+ }
18
+
19
+ export function isDefinitiveHealthFailure(value) {
20
+ const status = Number(value && (value.httpStatus ?? value.status));
21
+ const category = String(value && value.category || '').toLowerCase();
22
+ return status === 401 || status === 403 || DEFINITIVE_FAILURES.has(category);
23
+ }
24
+
25
+ export function healthObservationState(observation, {
26
+ now = Date.now(),
27
+ freshMs = PROVIDER_HEALTH_FRESH_MS
28
+ } = {}) {
29
+ if (!observation || typeof observation !== 'object') return 'configured';
30
+ const successAt = validTime(observation.lastSuccess && observation.lastSuccess.at);
31
+ const failureAt = validTime(observation.lastFailure && observation.lastFailure.at);
32
+ const cutoff = Number(now) - Number(freshMs);
33
+
34
+ if (failureAt >= cutoff && failureAt >= successAt
35
+ && isDefinitiveHealthFailure(observation.lastFailure)) {
36
+ return 'failed';
37
+ }
38
+ if (successAt >= cutoff && successAt > failureAt) return 'verified';
39
+ return 'configured';
40
+ }
41
+
42
+ export function aggregateHealthStates(states) {
43
+ const list = (Array.isArray(states) ? states : []).filter((state) =>
44
+ PROVIDER_HEALTH_STATES.includes(state) && state !== 'unconfigured');
45
+ if (!list.length) return 'unconfigured';
46
+ if (list.includes('verified')) return 'verified';
47
+ if (list.includes('configured')) return 'configured';
48
+ return 'failed';
49
+ }
50
+
51
+ export function newestHealthEvidence(observation) {
52
+ const entries = [
53
+ observation && observation.lastSuccess,
54
+ observation && observation.lastFailure,
55
+ observation && observation.lastTransient
56
+ ].filter((item) => item && validTime(item.at));
57
+ entries.sort((a, b) => validTime(b.at) - validTime(a.at));
58
+ return entries[0] || null;
59
+ }
package/lib/render.js CHANGED
@@ -6,8 +6,8 @@
6
6
  * Promise<{ png: Buffer }>
7
7
  *
8
8
  * BrowserHtmlRenderer:基于宿主 webServer 的 /iris/render/ 静态路由 +
9
- * dsh-builtin-browser 的 ctx.browser 服务(可选访问,软耦合)。
10
- * - 不 import dsh-builtin-browser 包,只在运行时探测 ctx.get('browser')
9
+ * DSH Host Adapter 注入的 browser 端口底层服务(可选访问,软耦合)。
10
+ * - 不 import dsh-builtin-browser 包,不在通用渲染层探测 DSH ctx
11
11
  * - HTML 通过宿主路由以同源 HTTP 打开(避免 file:// 的 CORS 限制)
12
12
  * - 每次调用独立临时渲染目录,用后即删
13
13
  */
@@ -25,14 +25,42 @@ export class RenderError extends Error {
25
25
  }
26
26
  }
27
27
 
28
+ function throwIfAborted(signal) {
29
+ if (!signal?.aborted) return;
30
+ const error = new RenderError('iris_html_screenshot: 已取消');
31
+ error.name = 'AbortError';
32
+ throw error;
33
+ }
34
+
35
+ function waitForRender(ms, signal) {
36
+ if (!(ms > 0)) return Promise.resolve();
37
+ throwIfAborted(signal);
38
+ return new Promise((resolve, reject) => {
39
+ const done = () => {
40
+ clearTimeout(timer);
41
+ signal?.removeEventListener?.('abort', canceled);
42
+ resolve();
43
+ };
44
+ const canceled = () => {
45
+ clearTimeout(timer);
46
+ signal?.removeEventListener?.('abort', canceled);
47
+ const error = new RenderError('iris_html_screenshot: 已取消');
48
+ error.name = 'AbortError';
49
+ reject(error);
50
+ };
51
+ const timer = setTimeout(done, ms);
52
+ signal?.addEventListener?.('abort', canceled, { once: true });
53
+ });
54
+ }
55
+
28
56
  /** 渲染临时目录根:$DSH_HOME/iris/v1/render/ */
29
57
  export function renderRoot() {
30
58
  return path.join(irisHome(), 'render');
31
59
  }
32
60
 
33
61
  /**
34
- * 基于 ctx.browser(dsh-builtin-browser)的 HTML → PNG 渲染器。
35
- * @param {{browser?: object, waitMs?: number}} opts browser 为 ctx.get('browser') 服务
62
+ * 基于 DSH Adapter 持有的原生 browser 服务完成 HTML → PNG。
63
+ * @param {{browser?: object, waitMs?: number}} opts browser 只在 DSH Adapter 内构造
36
64
  */
37
65
  export class BrowserHtmlRenderer {
38
66
  constructor({ browser, waitMs = 1200 }) {
@@ -40,13 +68,14 @@ export class BrowserHtmlRenderer {
40
68
  this.waitMs = waitMs;
41
69
  }
42
70
 
43
- async render({ html, width, height, fullPage }) {
71
+ async render({ html, width, height, fullPage, signal }) {
44
72
  const b = this.browser;
45
73
  if (!b || typeof b.open !== 'function' || typeof b.openUrl !== 'function' || typeof b.screenshot !== 'function') {
46
- throw new RenderError('iris: 浏览器服务不可用(ctx.get("browser") 为空)——需要启用 dsh-builtin-browser 插件');
74
+ throw new RenderError('iris: 浏览器服务不可用——需要启用兼容的 dsh-builtin-browser 插件');
47
75
  }
48
76
  const text = String(html || '').trim();
49
77
  if (!text) throw new RenderError('iris_html_screenshot: html 不能为空');
78
+ throwIfAborted(signal);
50
79
 
51
80
  // 每次调用独立子目录(随机名),写 index.html
52
81
  const dir = path.join(renderRoot(), crypto.randomBytes(6).toString('hex'));
@@ -63,13 +92,15 @@ export class BrowserHtmlRenderer {
63
92
  const pngPath = path.join(dir, 'render.png');
64
93
 
65
94
  try {
66
- const session = await b.open();
95
+ const session = await b.open({ signal });
96
+ throwIfAborted(signal);
67
97
  try {
68
- await b.openUrl(session, { url, newTab: true });
69
- if (this.waitMs > 0) {
70
- await new Promise((r) => setTimeout(r, this.waitMs)); // 等页面加载(无加载完成事件可用的折中)
71
- }
72
- await b.screenshot(session, { fullPage: fullPage !== false, savePath: pngPath });
98
+ await b.openUrl(session, { url, newTab: true, signal });
99
+ throwIfAborted(signal);
100
+ await waitForRender(this.waitMs, signal); // 等页面加载(无加载完成事件可用的折中)
101
+ throwIfAborted(signal);
102
+ await b.screenshot(session, { fullPage: fullPage !== false, savePath: pngPath, signal });
103
+ throwIfAborted(signal);
73
104
  } finally {
74
105
  try { await b.close(session); } catch (_) { /* 幂等 */ }
75
106
  }
package/lib/tasks.js CHANGED
@@ -19,6 +19,7 @@ import { parseModelRef } from './models.js';
19
19
  import { attentionDisposition, deriveLegacyStatus, deriveUserState, semanticViolations } from './task-semantics.js';
20
20
  export { attentionDisposition } from './task-semantics.js';
21
21
  import { providerErrorRecord, redactProviderMessage } from './provider-contract.js';
22
+ import { providerPollTaskView } from './provider-adapter.js';
22
23
 
23
24
  const FILE = () => path.join(irisHome(), 'tasks.json');
24
25
  /** 产物目录:$DSH_HOME/iris/v1/outputs */
@@ -508,8 +509,8 @@ export function stopWatchAll() {
508
509
  * 开始盯守一个异步任务(提交成功后调用)。
509
510
  * @param {object} task 已 create 的任务记录(须含 remoteTaskId)
510
511
  * @param {object} deps
511
- * key(): string —— 每次轮询现取凭据
512
- * poll({key, remoteTaskId}): {done,ok,urls,message} —— 协议适配器的查询函数
512
+ * key?(): string —— 仅供旧 poll 兼容;v0 Adapter 自持凭据闭包
513
+ * poll({key?, remoteTaskId}): Provider poll v0(兼容旧 done/ok 形状)—— 协议适配器查询
513
514
  * onSuccess(task, {urls}): string[] —— 结果转存,返回相对 outputs/ 的文件名列表
514
515
  * intervalMs?: number —— 轮询间隔(默认 2500)
515
516
  * maxWatchMs?: number —— 盯守上限(默认 20 分钟)
@@ -551,7 +552,11 @@ export function watch(task, deps) {
551
552
  }
552
553
  let r;
553
554
  try {
554
- r = await deps.poll({ key: deps.key(), remoteTaskId: t.remoteTaskId });
555
+ const raw = await deps.poll({
556
+ key: typeof deps.key === 'function' ? deps.key() : undefined,
557
+ remoteTaskId: t.remoteTaskId
558
+ });
559
+ r = providerPollTaskView(raw);
555
560
  } catch (err) {
556
561
  const n = (errStreak.get(task.id) || 0) + 1;
557
562
  errStreak.set(task.id, n);
@@ -571,10 +576,29 @@ export function watch(task, deps) {
571
576
  }
572
577
  if (!r.ok) {
573
578
  const message = redactProviderMessage(r.message || '任务失败');
574
- if (!v2) return finishLegacy({ status: 'failed', error: message });
579
+ if (!v2) {
580
+ return finishLegacy({ status: r.kind === 'canceled' ? 'canceled' : 'failed', error: message });
581
+ }
582
+ if (r.kind === 'unknown') {
583
+ return finishV2({
584
+ phase: 'terminal', watchState: 'exhausted', outcome: 'unknown',
585
+ lastError: r.error || providerErrorRecord(message, {
586
+ stage: 'poll', category: 'provider', acceptance: 'accepted'
587
+ }),
588
+ error: message, finishedAt: new Date().toISOString()
589
+ });
590
+ }
591
+ if (r.kind === 'canceled') {
592
+ return finishV2({
593
+ phase: 'terminal', watchState: 'idle', outcome: 'canceled', cancelState: 'remote_confirmed',
594
+ lastError: r.error, error: message, finishedAt: new Date().toISOString()
595
+ });
596
+ }
575
597
  return finishV2({
576
598
  phase: 'terminal', watchState: 'idle', outcome: 'failed',
577
- lastError: providerErrorRecord(message, { stage: 'poll', category: 'provider', acceptance: 'accepted' }),
599
+ lastError: r.error || providerErrorRecord(message, {
600
+ stage: 'poll', category: 'provider', acceptance: 'accepted'
601
+ }),
578
602
  error: message, finishedAt: new Date().toISOString()
579
603
  });
580
604
  }
package/lib/vision.js CHANGED
@@ -5,12 +5,13 @@
5
5
  * 把“识图”拆成「VisionBackend 接口 +
6
6
  * 两个实现」,让自持栈与 DSH 全局模型可替换、按序降级。
7
7
  * - SelfStackVisionBackend:自持栈(OpenAI 兼容 /chat/completions,qwen-vl)
8
- * - GlobalLlmVisionBackend:DSH 全局视觉模型(ctx.llm.stream())
8
+ * - HostVisionBackend:由 Host Adapter 提供的可选视觉模型
9
9
  *
10
- * 调用方(工具)不感知底层:buildVisionBackends 按 provider 顺序组装,
10
+ * 调用方(工具)不感知底层:buildVisionBackendsFromHost 按 provider 顺序组装,
11
11
  * askWithBackends 依次尝试、保留每个后端的失败现场。
12
12
  */
13
13
  import * as adapters from './adapters.js';
14
+ import { hasHostPort } from './host-contract.js';
14
15
 
15
16
  /** 后端接口约定:id / kind / model 只读,analyze(request) → Promise<string> */
16
17
 
@@ -39,52 +40,16 @@ export class SelfStackVisionBackend {
39
40
  }
40
41
  }
41
42
 
42
- export class GlobalLlmVisionBackend {
43
- constructor({ llm }) {
44
- this.llm = llm;
45
- }
46
- get id() {
47
- return 'global';
48
- }
49
- get kind() {
50
- return 'global';
51
- }
52
- get model() {
53
- return '全局视觉模型';
54
- }
55
- async analyze({ question, ref, signal }) {
56
- const chunks = [];
57
- for await (const chunk of this.llm.stream({
58
- sessionId: undefined,
59
- provider: undefined,
60
- model: undefined,
61
- signal,
62
- messages: [
63
- {
64
- role: 'user',
65
- content: [
66
- { type: 'text', text: question },
67
- { type: 'image', attachment: ref }
68
- ]
69
- }
70
- ]
71
- })) {
72
- const t = chunk && chunk.delta;
73
- if (typeof t === 'string' && t) chunks.push(t);
74
- if (chunks.join('').length > 6000) break;
75
- }
76
- return chunks.join('').trim();
77
- }
43
+ export class HostVisionBackend {
44
+ constructor({ port }) { this.port = port; }
45
+ get id() { return this.port.id || 'host-vision'; }
46
+ get kind() { return this.port.kind || 'global'; }
47
+ get model() { return this.port.model || '宿主视觉模型'; }
48
+ async analyze(request) { return this.port.analyze(request); }
78
49
  }
79
50
 
80
- /**
81
- * 按 provider 顺序组装视觉后端链(自持栈在前,全局模型兜底在后)。
82
- * 只接受已声明/推断 vision 能力的 provider(由调用方用 capability 过滤)。
83
- * @param {object} ctx DSH ctx(用于取 llm 服务)
84
- * @param {{providers: Array, model?: string}} opts model 覆盖全部自持栈的 vision 模型
85
- * @returns {Array} 有序后端
86
- */
87
- export function buildVisionBackends(ctx, { providers, model }) {
51
+ /** Host Adapter 入口;Command 不读取 DSH ctx。 */
52
+ export function buildVisionBackendsFromHost(host, { providers, model }) {
88
53
  const backends = [];
89
54
  for (const p of providers || []) {
90
55
  if (p && p.type === 'openai') {
@@ -92,8 +57,9 @@ export function buildVisionBackends(ctx, { providers, model }) {
92
57
  backends.push(new SelfStackVisionBackend({ provider }));
93
58
  }
94
59
  }
95
- const llm = ctx && typeof ctx.get === 'function' ? ctx.get('llm') : undefined;
96
- if (llm && typeof llm.stream === 'function') backends.push(new GlobalLlmVisionBackend({ llm }));
60
+ if (hasHostPort(host, 'visionModel') && typeof host.ports.visionModel.analyze === 'function') {
61
+ backends.push(new HostVisionBackend({ port: host.ports.visionModel }));
62
+ }
97
63
  return backends;
98
64
  }
99
65
 
@@ -116,6 +82,7 @@ export async function askWithBackends(backends, request) {
116
82
  errors.push({
117
83
  backendId: b.id,
118
84
  kind: b.kind,
85
+ model: b.model,
119
86
  category: err && err.category,
120
87
  status: err && err.status,
121
88
  message: String((err && err.message) || err)
@@ -164,7 +131,14 @@ export async function testVisionCapability(backend, { timeoutMs = 10000, signal
164
131
  const ok = RED_HINTS.test(String(answer || ''));
165
132
  return { ok, answer: String(answer || '') };
166
133
  } catch (err) {
167
- return { ok: false, answer: '', error: String((err && err.message) || err), timedOut: !!err.timedOut };
134
+ return {
135
+ ok: false,
136
+ answer: '',
137
+ error: String((err && err.message) || err),
138
+ timedOut: !!err.timedOut,
139
+ category: err && err.category,
140
+ status: err && err.status
141
+ };
168
142
  } finally {
169
143
  if (timer) clearTimeout(timer);
170
144
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mokuyoaxis/dsh-iris",
3
- "version": "0.1.3",
4
- "description": "Multi-provider media generation, visual understanding, prompt optimization, and an integrated Iris workbench for DeepSeek Harness.",
3
+ "version": "0.1.4",
4
+ "description": "Multi-provider image, video, and speech generation, transcription, visual understanding, and prompt optimization with an integrated Iris workbench for DeepSeek Harness.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
@@ -24,13 +24,16 @@
24
24
  "CHANGELOG.md",
25
25
  "screenshots.json",
26
26
  "docs",
27
- "!docs/assets/examples/orange-cat-reading.png",
27
+ "!docs/assets/diagrams/*.drawio",
28
+ "!docs/releases/0.1.1.md",
29
+ "!docs/releases/0.1.2.md",
30
+ "!docs/releases/0.1.3.md",
28
31
  ".dsh/skills/iris-verify-ui",
29
32
  ".dsh/skills/iris-compose-media"
30
33
  ],
31
34
  "dsh": {
32
35
  "engines": {
33
- "dsh": ">=0.1.2-rc.1 <0.1.3-0"
36
+ "dsh": ">=0.1.2-rc.1 <0.1.3-0 || 0.1.5-rc.1"
34
37
  },
35
38
  "bundle": {
36
39
  "patch": "./cordis.patch.yml"
@@ -49,11 +52,14 @@
49
52
  "keywords": [
50
53
  "deepseek-harness",
51
54
  "dsh-plugin",
52
- "vision",
53
55
  "image-generation",
56
+ "video-generation",
57
+ "text-to-speech",
58
+ "transcription",
59
+ "vision",
54
60
  "dashscope",
55
- "tts",
56
- "prompt-optimization"
61
+ "prompt-optimization",
62
+ "media-workbench"
57
63
  ],
58
64
  "license": "MIT",
59
65
  "engines": {
@@ -103,6 +109,6 @@
103
109
  "url": "https://github.com/mokuyoaxis/dsh-iris/issues"
104
110
  },
105
111
  "bin": {
106
- "dsh-iris": "./bin/dsh-iris.js"
112
+ "dsh-iris": "bin/dsh-iris.js"
107
113
  }
108
114
  }