@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
@@ -2,21 +2,11 @@
2
2
  /** DSH 对话框级提示词优化:只处理显式提交的草稿,不读取会话历史或附件。 */
3
3
  import crypto from 'node:crypto';
4
4
  import { loadPromptOptimizerConfig, PROMPT_TARGETS } from './prompt-optimizer-config.js';
5
+ import { hasHostPort, requireHostPort } from './host-contract.js';
5
6
 
6
7
  export const MAX_PROMPT_INPUT_BYTES = 32 * 1024;
7
8
  export const MAX_PROMPT_OUTPUT_CHARS = 16000;
8
9
 
9
- function service(ctx, name) {
10
- if (!ctx) return undefined;
11
- if (typeof ctx.get === 'function') {
12
- try {
13
- const value = ctx.get(name);
14
- if (value) return value;
15
- } catch (_) { /* 未注入的可选服务 */ }
16
- }
17
- try { return ctx[name]; } catch (_) { return undefined; }
18
- }
19
-
20
10
  function modelSelection(value, field) {
21
11
  if (!value || typeof value !== 'object') return null;
22
12
  const provider = typeof value.provider === 'string' ? value.provider.trim() : '';
@@ -32,15 +22,15 @@ function modelSelection(value, field) {
32
22
  return out;
33
23
  }
34
24
 
35
- export function resolvePromptOptimizerRoute(ctx, config, requestedRoute) {
25
+ export function resolvePromptOptimizerRoute(host, config, requestedRoute) {
36
26
  if (config.route.mode === 'fixed') {
37
27
  return { ...modelSelection(config.route, 'route'), source: 'iris-fixed' };
38
28
  }
39
29
  const current = modelSelection(requestedRoute);
40
30
  if (current) return { ...current, source: 'current-session' };
41
- const defaults = service(ctx, 'agentDefaultModel');
42
- const fallback = defaults && typeof defaults.currentSelection === 'function'
43
- ? modelSelection(defaults.currentSelection())
31
+ const textModel = hasHostPort(host, 'textModel') ? host.ports.textModel : undefined;
32
+ const fallback = textModel && typeof textModel.currentSelection === 'function'
33
+ ? modelSelection(textModel.currentSelection())
44
34
  : null;
45
35
  if (fallback) return { ...fallback, source: 'host-default' };
46
36
  throw new Error('Iris 找不到可用文本模型:请先在 DSH 会话中选择模型,或在 JSON 配置中设置 fixed 路由');
@@ -131,11 +121,11 @@ function cleanOutput(value) {
131
121
 
132
122
  /**
133
123
  * 执行一次显式的提示词优化。
134
- * @param {object} ctx DSH Host ctx
124
+ * @param {object} host Iris Host Adapter
135
125
  * @param {object} input {text,target,route,sessionId}
136
126
  * @param {{signal?:AbortSignal}} options
137
127
  */
138
- export async function optimizePrompt(ctx, input, { signal } = {}) {
128
+ export async function optimizePrompt(host, input, { signal } = {}) {
139
129
  const original = typeof input?.text === 'string' ? input.text : '';
140
130
  const text = original.trim();
141
131
  if (!text) throw new Error('请输入需要优化的提示词');
@@ -148,9 +138,9 @@ export async function optimizePrompt(ctx, input, { signal } = {}) {
148
138
 
149
139
  const loaded = loadPromptOptimizerConfig();
150
140
  const config = loaded.config;
151
- const route = resolvePromptOptimizerRoute(ctx, config, input && input.route);
152
- const llm = service(ctx, 'llm');
153
- if (!llm || typeof llm.stream !== 'function') throw new Error('DSH LLM 服务不可用,无法优化提示词');
141
+ const route = resolvePromptOptimizerRoute(host, config, input && input.route);
142
+ const llm = requireHostPort(host, 'textModel', '提示词优化');
143
+ if (typeof llm.stream !== 'function') throw new Error('Iris Host textModel 缺少 stream,无法优化提示词');
154
144
 
155
145
  const deadline = composeSignal(signal, config.generation.timeoutMs);
156
146
  const partials = new Map();
@@ -0,0 +1,371 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Iris Provider Adapter v0 完整生命周期契约。
5
+ *
6
+ * 本模块不读取配置、不调用网络、不创建 Task,也不依赖 DSH。具体供应商只把
7
+ * discovery/submit/poll/cancel/download/error mapping 映射到这些稳定结果。
8
+ */
9
+ import {
10
+ ProviderContractError,
11
+ normalizeSubmissionResult,
12
+ providerErrorRecord,
13
+ resultFromThrown
14
+ } from './provider-contract.js';
15
+
16
+ export const PROVIDER_ADAPTER_CONTRACT_VERSION = 0;
17
+
18
+ export const PROVIDER_OPERATIONS = Object.freeze([
19
+ 'discover',
20
+ 'submit',
21
+ 'poll',
22
+ 'cancel',
23
+ 'download',
24
+ 'mapError'
25
+ ]);
26
+
27
+ export const PROVIDER_TASK_CAPABILITIES = Object.freeze([
28
+ 'image',
29
+ 'video',
30
+ 'tts',
31
+ 'transcribe'
32
+ ]);
33
+
34
+ export const PROVIDER_POLL_KINDS = Object.freeze([
35
+ 'pending',
36
+ 'succeeded',
37
+ 'failed',
38
+ 'canceled',
39
+ 'unknown'
40
+ ]);
41
+
42
+ export const PROVIDER_CANCEL_KINDS = Object.freeze([
43
+ 'canceled',
44
+ 'not_supported',
45
+ 'unknown'
46
+ ]);
47
+
48
+ const ID = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
49
+ const PROTOCOL = /^[a-z0-9][a-z0-9._-]*$/;
50
+
51
+ function isPlainObject(value) {
52
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
53
+ }
54
+
55
+ function validateOperation(name) {
56
+ if (!PROVIDER_OPERATIONS.includes(name)) throw new Error('未知 Provider 操作:' + String(name));
57
+ }
58
+
59
+ function normalizedReason(value, fallback) {
60
+ const text = String(value || fallback || '').trim();
61
+ if (!text) throw new TypeError('Provider unsupported 原因不能为空');
62
+ return text.slice(0, 500);
63
+ }
64
+
65
+ /**
66
+ * 建立只读 Adapter 描述。每个生命周期操作必须恰好出现在 operations 或
67
+ * unsupported 之一,避免“方法不存在”被误解成临时故障。
68
+ */
69
+ export function defineProviderAdapter(input) {
70
+ if (!isPlainObject(input)) throw new TypeError('Provider Adapter 必须是对象');
71
+ const allowed = new Set([
72
+ 'contractVersion', 'id', 'protocol', 'capabilities', 'operations', 'unsupported'
73
+ ]);
74
+ for (const key of Object.keys(input)) {
75
+ if (!allowed.has(key)) throw new Error('Provider Adapter 不允许顶层字段:' + key);
76
+ }
77
+
78
+ const contractVersion = input.contractVersion ?? PROVIDER_ADAPTER_CONTRACT_VERSION;
79
+ if (contractVersion !== PROVIDER_ADAPTER_CONTRACT_VERSION) {
80
+ throw new Error('不支持的 Provider Adapter 契约版本:' + String(contractVersion));
81
+ }
82
+ const id = String(input.id || '').trim();
83
+ const protocol = String(input.protocol || '').trim();
84
+ if (!ID.test(id)) throw new Error('Provider Adapter id 必须是稳定标识');
85
+ if (!PROTOCOL.test(protocol)) throw new Error('Provider Adapter protocol 必须是小写稳定标识');
86
+
87
+ const capabilities = [...new Set(Array.isArray(input.capabilities) ? input.capabilities : [])];
88
+ if (!capabilities.length || capabilities.some((item) => !PROVIDER_TASK_CAPABILITIES.includes(item))) {
89
+ throw new Error('Provider Adapter capabilities 必须使用已知媒体任务能力');
90
+ }
91
+
92
+ const supplied = input.operations ?? {};
93
+ const missing = input.unsupported ?? {};
94
+ if (!isPlainObject(supplied) || !isPlainObject(missing)) {
95
+ throw new TypeError('Provider Adapter operations/unsupported 必须是对象');
96
+ }
97
+
98
+ const operations = {};
99
+ const unsupported = {};
100
+ for (const name of Object.keys(supplied)) validateOperation(name);
101
+ for (const name of Object.keys(missing)) validateOperation(name);
102
+ for (const name of PROVIDER_OPERATIONS) {
103
+ const hasOperation = Object.prototype.hasOwnProperty.call(supplied, name);
104
+ const hasUnsupported = Object.prototype.hasOwnProperty.call(missing, name);
105
+ if (hasOperation === hasUnsupported) {
106
+ throw new Error('Provider 操作 ' + name + ' 必须恰好声明为 supported 或 unsupported');
107
+ }
108
+ if (hasOperation) {
109
+ if (typeof supplied[name] !== 'function') {
110
+ throw new TypeError('Provider 操作 ' + name + ' 必须是函数');
111
+ }
112
+ operations[name] = supplied[name];
113
+ } else {
114
+ unsupported[name] = Object.freeze({ reason: normalizedReason(missing[name], name + ' 未实现') });
115
+ }
116
+ }
117
+ if (!operations.submit || !operations.mapError) {
118
+ throw new Error('Provider Adapter 必须实现 submit 与 mapError');
119
+ }
120
+
121
+ return Object.freeze({
122
+ contractVersion,
123
+ id,
124
+ protocol,
125
+ capabilities: Object.freeze(capabilities),
126
+ operations: Object.freeze(operations),
127
+ unsupported: Object.freeze(unsupported)
128
+ });
129
+ }
130
+
131
+ export class ProviderOperationError extends Error {
132
+ constructor(adapter, operation) {
133
+ validateOperation(operation);
134
+ const providerId = adapter?.id || 'unknown-provider';
135
+ const reason = adapter?.unsupported?.[operation]?.reason || '操作未实现';
136
+ super('Iris Provider ' + providerId + ' 不支持 ' + operation + ':' + reason);
137
+ this.name = 'ProviderOperationError';
138
+ this.code = 'IRIS_PROVIDER_OPERATION_UNSUPPORTED';
139
+ this.providerId = providerId;
140
+ this.protocol = adapter?.protocol || 'unknown';
141
+ this.operation = operation;
142
+ }
143
+ }
144
+
145
+ export function hasProviderOperation(adapter, operation) {
146
+ validateOperation(operation);
147
+ return Boolean(adapter?.operations && typeof adapter.operations[operation] === 'function');
148
+ }
149
+
150
+ export function requireProviderOperation(adapter, operation) {
151
+ if (!hasProviderOperation(adapter, operation)) throw new ProviderOperationError(adapter, operation);
152
+ return adapter.operations[operation];
153
+ }
154
+
155
+ /** 只输出可序列化能力事实;不会暴露闭包中的 key、baseUrl 或 live transport。 */
156
+ export function providerAdapterSnapshot(adapter) {
157
+ const operations = {};
158
+ for (const name of PROVIDER_OPERATIONS) {
159
+ operations[name] = hasProviderOperation(adapter, name)
160
+ ? Object.freeze({ status: 'supported' })
161
+ : Object.freeze({
162
+ status: 'unsupported',
163
+ reason: adapter?.unsupported?.[name]?.reason || '操作未实现'
164
+ });
165
+ }
166
+ return Object.freeze({
167
+ contractVersion: PROVIDER_ADAPTER_CONTRACT_VERSION,
168
+ provider: Object.freeze({
169
+ id: adapter?.id || 'unknown-provider',
170
+ protocol: adapter?.protocol || 'unknown'
171
+ }),
172
+ capabilities: Object.freeze([...(adapter?.capabilities || [])]),
173
+ operations: Object.freeze(operations)
174
+ });
175
+ }
176
+
177
+ export function normalizeDiscoveryResult(value) {
178
+ const source = Array.isArray(value) ? value : value?.models;
179
+ if (!Array.isArray(source)) throw new TypeError('Provider discover 必须返回 models 数组');
180
+ const models = [];
181
+ const seen = new Set();
182
+ for (const item of source) {
183
+ const id = String(typeof item === 'string' ? item : item?.id || '').trim();
184
+ if (!id || seen.has(id)) continue;
185
+ seen.add(id);
186
+ const model = { id };
187
+ if (Array.isArray(item?.capabilities)) {
188
+ const capabilities = [...new Set(item.capabilities.filter((cap) => typeof cap === 'string' && cap))];
189
+ if (capabilities.length) model.capabilities = Object.freeze(capabilities);
190
+ }
191
+ models.push(Object.freeze(model));
192
+ }
193
+ return Object.freeze({ models: Object.freeze(models) });
194
+ }
195
+
196
+ function normalizeArtifacts(value) {
197
+ if (value === undefined) return Object.freeze([]);
198
+ if (!Array.isArray(value)) throw new TypeError('Provider poll artifacts 必须是数组');
199
+ return Object.freeze(value.map((item) => {
200
+ if (!isPlainObject(item) || item.kind !== 'remote-url') {
201
+ throw new TypeError('Provider artifact 当前只支持 remote-url');
202
+ }
203
+ const url = String(item.url || '').trim();
204
+ if (!url) throw new TypeError('Provider remote-url artifact 缺少 url');
205
+ return Object.freeze({
206
+ kind: 'remote-url',
207
+ url,
208
+ ...(typeof item.mediaType === 'string' && item.mediaType ? { mediaType: item.mediaType } : {})
209
+ });
210
+ }));
211
+ }
212
+
213
+ /**
214
+ * poll 标准结果。兼容旧 {done,ok,urls,text,status,message} 仅用于 0.1.x 渐进迁移;
215
+ * 新 Adapter 必须直接返回 kind 结果。
216
+ */
217
+ export function normalizeProviderPollResult(value) {
218
+ if (!isPlainObject(value)) throw new TypeError('Provider poll 必须返回结构化结果');
219
+
220
+ if (!PROVIDER_POLL_KINDS.includes(value.kind) && typeof value.done === 'boolean') {
221
+ if (!value.done) return Object.freeze({
222
+ kind: 'pending',
223
+ ...(value.status ? { progress: String(value.status) } : {})
224
+ });
225
+ if (value.ok) {
226
+ return Object.freeze({
227
+ kind: 'succeeded',
228
+ artifacts: normalizeArtifacts((value.urls || []).map((url) => ({ kind: 'remote-url', url }))),
229
+ ...(typeof value.text === 'string'
230
+ ? { value: Object.freeze({ kind: 'text', text: value.text }) }
231
+ : {})
232
+ });
233
+ }
234
+ return Object.freeze({
235
+ kind: 'failed',
236
+ error: Object.freeze(providerErrorRecord(value.message || '供应商任务失败', {
237
+ stage: 'poll', category: 'provider', acceptance: 'accepted'
238
+ }))
239
+ });
240
+ }
241
+
242
+ if (!PROVIDER_POLL_KINDS.includes(value.kind)) {
243
+ throw new TypeError('未知 Provider poll kind:' + String(value.kind));
244
+ }
245
+ if (value.kind === 'pending') {
246
+ return Object.freeze({
247
+ kind: 'pending',
248
+ ...(value.progress !== undefined ? { progress: String(value.progress) } : {})
249
+ });
250
+ }
251
+ if (value.kind === 'succeeded') {
252
+ return Object.freeze({
253
+ kind: 'succeeded',
254
+ artifacts: normalizeArtifacts(value.artifacts),
255
+ ...(value.value !== undefined ? { value: value.value } : {})
256
+ });
257
+ }
258
+ const stage = value.kind === 'canceled' ? 'cancel' : 'poll';
259
+ return Object.freeze({
260
+ kind: value.kind,
261
+ error: Object.freeze(providerErrorRecord(value.error || value.message || ('Provider poll ' + value.kind), {
262
+ stage,
263
+ category: value.kind === 'canceled' ? 'aborted' : undefined,
264
+ acceptance: 'accepted'
265
+ }))
266
+ });
267
+ }
268
+
269
+ export function normalizeProviderCancelResult(value) {
270
+ if (!isPlainObject(value) || !PROVIDER_CANCEL_KINDS.includes(value.kind)) {
271
+ throw new TypeError('Provider cancel 必须返回 canceled/not_supported/unknown');
272
+ }
273
+ if (value.kind === 'canceled') return Object.freeze({ kind: 'canceled' });
274
+ if (value.kind === 'not_supported') {
275
+ return Object.freeze({ kind: 'not_supported', reason: normalizedReason(value.reason, '供应商不支持远端取消') });
276
+ }
277
+ return Object.freeze({
278
+ kind: 'unknown',
279
+ error: Object.freeze(providerErrorRecord(value.error || value.message || '远端取消结果未知', {
280
+ stage: 'cancel', acceptance: 'accepted'
281
+ }))
282
+ });
283
+ }
284
+
285
+ export function normalizeProviderDownloadResult(value) {
286
+ const bytes = Number(typeof value === 'number' ? value : value?.bytes);
287
+ if (!Number.isSafeInteger(bytes) || bytes < 0) {
288
+ throw new TypeError('Provider download 必须返回非负安全整数 bytes');
289
+ }
290
+ return Object.freeze({ bytes });
291
+ }
292
+
293
+ function mappedError(adapter, error, context) {
294
+ const mapper = requireProviderOperation(adapter, 'mapError');
295
+ let mapped;
296
+ try {
297
+ mapped = mapper(error, context);
298
+ } catch (mappingError) {
299
+ mapped = mappingError;
300
+ }
301
+ const source = mapped || error;
302
+ const defaults = {};
303
+ for (const key of ['stage', 'category', 'acceptance', 'retryable']) {
304
+ if (source == null || source[key] === undefined) defaults[key] = context[key];
305
+ }
306
+ return providerErrorRecord(source, defaults);
307
+ }
308
+
309
+ function operationErrorContext(operation, context) {
310
+ const supplied = isPlainObject(context) ? context : {};
311
+ if (operation === 'submit') return { stage: 'submit', acceptance: 'unknown', ...supplied };
312
+ if (operation === 'discover') return { stage: 'validate', acceptance: 'not_accepted', ...supplied };
313
+ if (operation === 'poll') return { stage: 'poll', acceptance: 'accepted', ...supplied };
314
+ if (operation === 'cancel') return { stage: 'cancel', acceptance: 'accepted', ...supplied };
315
+ if (operation === 'download') return { stage: 'download', acceptance: 'accepted', ...supplied };
316
+ return supplied;
317
+ }
318
+
319
+ /**
320
+ * 生命周期统一调用入口。submit 异常转为保守四态结果;cancel 异常转为 unknown;
321
+ * poll/download/discover 保持抛错,让调度器应用各自的重试和事实规则。
322
+ */
323
+ export async function invokeProviderOperation(adapter, operation, input, context = {}) {
324
+ validateOperation(operation);
325
+ if (operation === 'mapError') {
326
+ return mappedError(adapter, input, context);
327
+ }
328
+ const fn = requireProviderOperation(adapter, operation);
329
+ const errorContext = operationErrorContext(operation, context);
330
+ let value;
331
+ try {
332
+ value = await fn(input, context);
333
+ } catch (error) {
334
+ const evidenceContext = { ...errorContext };
335
+ for (const key of ['stage', 'category', 'acceptance', 'retryable']) {
336
+ if (error && error[key] !== undefined) evidenceContext[key] = error[key];
337
+ }
338
+ const safe = mappedError(adapter, error, evidenceContext);
339
+ if (operation === 'submit') return resultFromThrown(safe);
340
+ if (operation === 'cancel') return normalizeProviderCancelResult({ kind: 'unknown', error: safe });
341
+ throw new ProviderContractError(safe.safeMessage, safe);
342
+ }
343
+
344
+ if (operation === 'discover') return normalizeDiscoveryResult(value);
345
+ if (operation === 'submit') return normalizeSubmissionResult(value);
346
+ if (operation === 'poll') return normalizeProviderPollResult(value);
347
+ if (operation === 'cancel') return normalizeProviderCancelResult(value);
348
+ if (operation === 'download') return normalizeProviderDownloadResult(value);
349
+ throw new Error('未处理的 Provider 操作:' + operation);
350
+ }
351
+
352
+ /** 0.1.x 任务观察器兼容视图;0.2.0 Command 完全消费 canonical poll 后可删除。 */
353
+ export function providerPollTaskView(value) {
354
+ const result = normalizeProviderPollResult(value);
355
+ if (result.kind === 'pending') {
356
+ return { kind: result.kind, done: false, ok: false, urls: [], status: result.progress || 'running' };
357
+ }
358
+ if (result.kind === 'succeeded') {
359
+ const urls = result.artifacts.map((item) => item.url);
360
+ if (result.value?.kind === 'text') urls.push(String(result.value.text || ''));
361
+ return { kind: result.kind, done: true, ok: true, urls, value: result.value };
362
+ }
363
+ return {
364
+ kind: result.kind,
365
+ done: true,
366
+ ok: false,
367
+ urls: [],
368
+ message: result.error.safeMessage,
369
+ error: result.error
370
+ };
371
+ }