@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.
- package/.dsh/skills/iris-compose-media/SKILL.md +23 -49
- package/.dsh/skills/iris-compose-media/references/task-v2-recovery.md +30 -0
- package/.dsh/skills/iris-compose-media/references/workflows.md +38 -0
- package/.dsh/skills/iris-verify-ui/SKILL.md +3 -25
- package/.dsh/skills/iris-verify-ui/agents/openai.yaml +4 -0
- package/.dsh/skills/iris-verify-ui/references/verdict-and-degradation.md +31 -0
- package/CHANGELOG.md +24 -2
- package/README.en.md +29 -7
- package/README.md +29 -7
- package/docs/ARCHITECTURE.md +27 -13
- package/docs/ARTIFACT_LIBRARY.md +24 -0
- package/docs/HOST_ADAPTER_CONTRACT.md +86 -0
- package/docs/HOST_DOCTOR.md +47 -0
- package/docs/PROVIDER_ADAPTER_CONTRACT.md +97 -0
- package/docs/PROVIDER_HEALTH.md +39 -0
- package/docs/PROVIDER_SUBMISSION_CONTRACT.md +2 -2
- package/docs/ROADMAP.md +18 -8
- package/docs/SECURITY.md +6 -2
- package/docs/TASK_SEMANTICS.md +3 -2
- package/docs/assets/diagrams/iris-architecture.png +0 -0
- package/docs/assets/diagrams/iris-architecture.svg +5 -5
- package/docs/releases/0.1.4.md +59 -0
- package/lib/actions.js +357 -221
- package/lib/adapters.js +7 -7
- package/lib/api.js +30 -11
- package/lib/artifacts.js +239 -0
- package/lib/bundled-skills.js +5 -5
- package/lib/client.js +370 -55
- package/lib/config.js +192 -9
- package/lib/doctor.js +175 -4
- package/lib/dsh-host-adapter.js +189 -0
- package/lib/host-contract.js +131 -0
- package/lib/host-runtime.js +156 -0
- package/lib/index.js +97 -113
- package/lib/locate.js +1 -1
- package/lib/media.js +16 -6
- package/lib/prompt-optimizer.js +10 -20
- package/lib/provider-adapter.js +371 -0
- package/lib/provider-adapters.js +279 -0
- package/lib/provider-health.js +59 -0
- package/lib/render.js +43 -12
- package/lib/tasks.js +29 -5
- package/lib/vision.js +23 -49
- package/package.json +14 -8
- package/user_guide.md +42 -2
- package/docs/assets/diagrams/iris-architecture.drawio +0 -1
- package/docs/assets/diagrams/iris-task-lifecycle.drawio +0 -1
- package/docs/assets/diagrams/iris-workflow-compose.drawio +0 -1
- package/docs/releases/0.1.1.md +0 -51
- package/docs/releases/0.1.2.md +0 -26
- 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
|
-
*
|
|
10
|
-
* - 不 import dsh-builtin-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
|
-
* 基于
|
|
35
|
-
* @param {{browser?: object, waitMs?: number}} opts 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:
|
|
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
|
-
|
|
70
|
-
|
|
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
|
|
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
|
-
|
|
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)
|
|
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, {
|
|
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
|
-
* -
|
|
8
|
+
* - HostVisionBackend:由 Host Adapter 提供的可选视觉模型
|
|
9
9
|
*
|
|
10
|
-
* 调用方(工具)不感知底层:
|
|
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
|
|
43
|
-
constructor({
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
get
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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 {
|
|
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.
|
|
4
|
-
"description": "Multi-provider
|
|
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/
|
|
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
|
-
"
|
|
56
|
-
"
|
|
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": "
|
|
112
|
+
"dsh-iris": "bin/dsh-iris.js"
|
|
107
113
|
}
|
|
108
114
|
}
|