@mokuyoaxis/dsh-iris 0.1.4 → 0.2.0

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 (64) hide show
  1. package/CHANGELOG.md +54 -1
  2. package/README.en.md +72 -15
  3. package/README.md +72 -15
  4. package/bin/dsh-iris.js +392 -13
  5. package/docs/AGENT_QUICKSTART.md +35 -0
  6. package/docs/ARCHITECTURE.md +80 -64
  7. package/docs/ARTIFACT_MANIFEST.md +60 -0
  8. package/docs/CORE_RUNTIME_CONTRACT.md +83 -0
  9. package/docs/DSH_CORE_MIGRATION.md +106 -0
  10. package/docs/DSH_RC2_ADAPTATION.md +36 -0
  11. package/docs/HEADLESS_CLI.md +217 -0
  12. package/docs/HOST_ADAPTER_CONTRACT.md +31 -12
  13. package/docs/HOST_DOCTOR.md +2 -0
  14. package/docs/MODEL_PORT_CONTRACT.md +300 -0
  15. package/docs/PROVIDER_ADAPTER_CONTRACT.md +51 -10
  16. package/docs/PROVIDER_RUNTIME_HARNESS.md +33 -0
  17. package/docs/PROVIDER_SUBMISSION_CONTRACT.md +1 -1
  18. package/docs/ROADMAP.md +42 -86
  19. package/docs/SECURITY.md +4 -3
  20. package/docs/assets/logo/README.md +55 -0
  21. package/docs/assets/logo/iris-bubble-concept.svg +67 -0
  22. package/docs/assets/logo/iris-bubble.svg +14 -0
  23. package/docs/assets/logo/iris-interim-flower.png +0 -0
  24. package/docs/assets/logo/iris-logo-mono-inverse.svg +78 -0
  25. package/docs/assets/logo/iris-logo-mono.svg +78 -0
  26. package/docs/assets/logo/iris-logo.svg +78 -0
  27. package/docs/assets/logo/iris-mark-mono-inverse.svg +42 -0
  28. package/docs/assets/logo/iris-mark-mono.svg +42 -0
  29. package/docs/assets/logo/iris-mark.svg +42 -0
  30. package/docs/releases/0.2.0.md +48 -0
  31. package/lib/actions.js +374 -362
  32. package/lib/adapters.js +52 -15
  33. package/lib/api.js +152 -5
  34. package/lib/client.js +763 -48
  35. package/lib/command-service.js +420 -0
  36. package/lib/config.js +56 -43
  37. package/lib/core-artifact-store.js +598 -0
  38. package/lib/core-artifacts.js +4 -0
  39. package/lib/core-attention.js +103 -0
  40. package/lib/core-contract.js +105 -0
  41. package/lib/core-doctor.js +41 -0
  42. package/lib/core-lease-recovery.js +170 -0
  43. package/lib/core-runtime.js +232 -0
  44. package/lib/core-tasks.js +560 -0
  45. package/lib/core-user-projection.js +192 -0
  46. package/lib/doctor.js +92 -33
  47. package/lib/dsh-core-adapter.js +599 -0
  48. package/lib/dsh-host-adapter.js +58 -7
  49. package/lib/generation-input.js +95 -0
  50. package/lib/guard.js +3 -3
  51. package/lib/index.js +159 -54
  52. package/lib/model-invoker.js +218 -0
  53. package/lib/model-port-contract.js +333 -0
  54. package/lib/models.js +97 -11
  55. package/lib/provider-adapter.js +49 -23
  56. package/lib/provider-adapters.js +84 -21
  57. package/lib/provider-catalog.js +171 -0
  58. package/lib/provider-contract.js +72 -3
  59. package/lib/provider-protocol.js +51 -2
  60. package/lib/provider-task-runner.js +328 -0
  61. package/lib/tasks.js +16 -4
  62. package/lib/vision.js +28 -7
  63. package/package.json +8 -8
  64. package/user_guide.md +27 -12
package/lib/adapters.js CHANGED
@@ -9,7 +9,7 @@ import fs from 'node:fs';
9
9
  import path from 'node:path';
10
10
  import { Readable } from 'node:stream';
11
11
  import { pipeline } from 'node:stream/promises';
12
- import { dashscopeApiBase } from './provider-protocol.js';
12
+ import { dashscopeApiBase, isDashScopeBaseUrl } from './provider-protocol.js';
13
13
  import { chmodPrivateFile, ensurePrivateDir, privateSibling } from './private-storage.js';
14
14
 
15
15
  /**
@@ -31,7 +31,10 @@ function timeoutSignal(ms, external) {
31
31
  }
32
32
 
33
33
  function authHeaders(key) {
34
- return { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' };
34
+ return {
35
+ ...(typeof key === 'string' && key.trim() ? { Authorization: `Bearer ${key}` } : {}),
36
+ 'Content-Type': 'application/json'
37
+ };
35
38
  }
36
39
 
37
40
  async function readJson(res) {
@@ -317,25 +320,59 @@ export async function openAiGenerateImage({ key, baseUrl, model, prompt, size =
317
320
  return out;
318
321
  }
319
322
 
320
- /* ---------------- 模型发现(OpenAI 兼容 GET /models,各家通用) ---------------- */
323
+ /* ---------------- 模型发现(OpenAI GET /models + DashScope GET /api/v1/models) ---------------- */
321
324
  /**
322
- * 拉取 provider 可用模型 id 列表。OpenAI 标准端点,DashScope compatible-mode 亦支持。
323
- * 免费只读;返回 [{ id }] 原样(分类由 models.js 规则负责)。
324
- * @returns {Promise<string[]>}
325
+ * 拉取供应商模型目录。DashScope 官方端点分页读取并保留 capabilities;
326
+ * 普通 OpenAI 兼容端点维持 GET /models 与字符串 id 返回形状。
327
+ * @returns {Promise<Array<string|{id:string,capabilities?:string[]}>>}
325
328
  */
326
329
  export async function listModels({ key, baseUrl, timeoutMs = T_SUBMIT, signal }) {
327
330
  const base = String(baseUrl || '').trim().replace(/\/+$/, '');
328
331
  if (!base) throw new Error('iris: 供应商缺 baseUrl,无法发现模型');
329
- const res = await fetch(`${base}/models`, { headers: authHeaders(key), signal: timeoutSignal(timeoutMs, signal) });
330
- const body = await readJson(res);
331
- if (!res.ok) throw providerError(res.status, body, '模型发现失败');
332
- const items = (body && Array.isArray(body.data) && body.data) || (Array.isArray(body && body.models) ? body.models : []);
333
- const ids = [];
334
- for (const it of items) {
335
- const id = typeof it === 'string' ? it : it && it.id;
336
- if (typeof id === 'string' && id && !ids.includes(id)) ids.push(id);
332
+ const dashscope = isDashScopeBaseUrl(base);
333
+ const endpoint = dashscope ? dashscopeApiBase(base) + '/models' : base + '/models';
334
+ const found = new Map();
335
+ const maxPages = dashscope ? 50 : 1;
336
+ let loaded = 0;
337
+ for (let page = 1; page <= maxPages; page++) {
338
+ const url = new URL(endpoint);
339
+ if (dashscope) {
340
+ url.searchParams.set('page_no', String(page));
341
+ url.searchParams.set('page_size', '100');
342
+ }
343
+ const res = await fetch(url, { headers: authHeaders(key), signal: timeoutSignal(timeoutMs, signal) });
344
+ const body = await readJson(res);
345
+ if (!res.ok) throw providerError(res.status, body, '模型发现失败');
346
+ const output = body && body.output;
347
+ const items = (output && Array.isArray(output.models) && output.models)
348
+ || (body && Array.isArray(body.data) && body.data)
349
+ || (Array.isArray(body && body.models) ? body.models : []);
350
+ loaded += items.length;
351
+ for (const item of items) {
352
+ const id = String(typeof item === 'string' ? item : item?.id || item?.model || '').trim();
353
+ if (!id || found.has(id)) continue;
354
+ const capabilities = Array.isArray(item?.capabilities)
355
+ ? item.capabilities.filter((value) => typeof value === 'string' && value)
356
+ : [];
357
+ const requestModalities = Array.isArray(item?.inference_metadata?.request_modality)
358
+ ? item.inference_metadata.request_modality.map((value) => String(value).toLowerCase()) : [];
359
+ const responseModalities = Array.isArray(item?.inference_metadata?.response_modality)
360
+ ? item.inference_metadata.response_modality.map((value) => String(value).toLowerCase()) : [];
361
+ if (responseModalities.includes('image')) capabilities.push('IG');
362
+ if (responseModalities.includes('video')) capabilities.push('VG');
363
+ if (requestModalities.includes('audio') && responseModalities.includes('text')) capabilities.push('ASR');
364
+ if (requestModalities.includes('text') && responseModalities.includes('audio')) capabilities.push('TTS');
365
+ if (requestModalities.includes('image') && responseModalities.includes('text')) capabilities.push('VU');
366
+ const normalizedCapabilities = [...new Set(capabilities)];
367
+ found.set(id, normalizedCapabilities.length ? { id, capabilities: normalizedCapabilities } : id);
368
+ }
369
+ if (!dashscope) break;
370
+ const statedTotal = Number(output?.total);
371
+ const total = Number.isFinite(statedTotal) && statedTotal >= 0 ? statedTotal : null;
372
+ if (!items.length || (total !== null && loaded >= total) || items.length < 100) break;
373
+ if (page === maxPages) throw new Error('iris: 模型目录超过安全分页上限,已保留原模型池');
337
374
  }
338
- return ids;
375
+ return [...found.values()];
339
376
  }
340
377
 
341
378
  /* ---------------- 视觉理解(OpenAI 兼容 SSE 流式) ---------------- */
package/lib/api.js CHANGED
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * 输出纪律(与整仓一致):
9
9
  * - 只回标量字段(字符串/数字/布尔/受限数组),apiKey 永不明文,只给 hint;
10
- * - 文件路径只给相对 outputs/ 的 basename + 授权播放链接(token 随机 128bit);
10
+ * - legacy 文件路径只给相对 outputs/ 的 basename + 128-bit token 链接;Core 媒体按随机 Artifact ID 读取,不宣称独立 token;
11
11
  * - tasks/providers 读的是 $DSH_HOME/iris/v1 的持久 JSON,与任务盯守共用同一存储。
12
12
  */
13
13
  import fs from 'node:fs';
@@ -19,10 +19,11 @@ import * as tasks from './tasks.js';
19
19
  import * as artifacts from './artifacts.js';
20
20
  import { runAction } from './actions.js';
21
21
  import { chmodPrivateFile, ensurePrivateDir } from './private-storage.js';
22
- import { inferMediaProtocol } from './provider-protocol.js';
22
+ import { selectMediaProtocol } from './provider-protocol.js';
23
23
  import { deriveUserState } from './task-semantics.js';
24
24
  import { hostDoctor } from './doctor.js';
25
25
  import { hostRuntimeEvidence, recordHostClient } from './host-runtime.js';
26
+ import { cancelProviderTaskForDsh, coreSnapshotForDsh, disposeCoreTaskAttention, readCoreArtifactMediaForDsh, redeliverProviderTaskForDsh, reobserveProviderTaskForDsh, retryProviderTaskForDsh } from './dsh-core-adapter.js';
26
27
  import { optimizePrompt } from './prompt-optimizer.js';
27
28
  import {
28
29
  importPromptOptimizerConfig,
@@ -55,8 +56,9 @@ function providerPublic(p) {
55
56
  name: p.name || p.id,
56
57
  type: p.type || 'openai',
57
58
  baseUrl: p.baseUrl,
59
+ mediaBaseUrl: p.mediaBaseUrl || '',
58
60
  enabled: p.enabled !== false,
59
- mediaProtocol: p.mediaProtocol || inferMediaProtocol(p.baseUrl),
61
+ ...selectMediaProtocol(p),
60
62
  apiKeyHint: keyHint(p.apiKey),
61
63
  capabilities: capabilitiesOf(p),
62
64
  // 迁移/推断结果是否与原始声明不同(UI 提示旧配置已推断)
@@ -337,6 +339,13 @@ export function serveApi(req, res, host) {
337
339
  if (urlPath === '/iris/api/upload') {
338
340
  return handleUpload(req, res);
339
341
  }
342
+ // D4 人工 retry as new task:产生新的真实计费,请求体必须显式 confirmBilling:true
343
+ const coreTaskRetry = urlPath.match(/^\/iris\/api\/core\/task\/([^/]+)\/retry\/?$/);
344
+ if (coreTaskRetry) return handleCoreTaskRetry(coreTaskRetry[1], req, res);
345
+ // D1/D2/D3 人工动作:reobserve 显式单步 poll 一次;redeliver 只对 succeeded+failed
346
+ // 重新取回产物;cancel 只有 Provider 明确确认才写已取消。三者绝不 submit。
347
+ const coreTaskAction = urlPath.match(/^\/iris\/api\/core\/task\/([^/]+)\/(reobserve|redeliver|cancel|acknowledge|restore|hide|unhide)\/?$/);
348
+ if (coreTaskAction) return handleCoreTaskManual(coreTaskAction[2], coreTaskAction[1], req, res);
340
349
  const promptMatch = typeof host === 'object' && urlPath.match(/^\/iris\/api\/prompt-optimizer\/(optimize|import|reset|enabled)\/?$/);
341
350
  if (promptMatch) return handlePromptOptimizer(host, promptMatch[1], req, res);
342
351
  const actionMatch = typeof host === 'object' && urlPath.match(/^\/iris\/api\/actions\/([A-Za-z0-9_-]+)\/?$/);
@@ -359,6 +368,13 @@ export function serveApi(req, res, host) {
359
368
  if (!host || typeof host !== 'object') return sendJson(res, 503, { error: 'host unavailable' });
360
369
  return sendJson(res, 200, method === 'HEAD' ? {} : hostDoctor(host, hostRuntimeEvidence()));
361
370
  }
371
+ if (urlPath === '/iris/api/core/snapshot') {
372
+ if (method === 'HEAD') return sendJson(res, 200, {});
373
+ const query = new URL(req.url || '', 'http://iris.local').searchParams;
374
+ return serveCoreSnapshot(res, { limit: Number(query.get('limit') || 12) });
375
+ }
376
+ const coreMediaMatch = urlPath.match(/^\/iris\/api\/core\/artifact\/(artifact_[a-f0-9]{24})\/media$/);
377
+ if (coreMediaMatch) return serveCoreArtifactMedia(coreMediaMatch[1], method, res);
362
378
  if (urlPath === '/iris/api/artifacts') {
363
379
  const query = new URL(req.url || '', 'http://iris.local').searchParams;
364
380
  const page = artifacts.page({ offset: Number(query.get('offset') || 0), limit: Number(query.get('limit') || 24) });
@@ -384,6 +400,37 @@ export function serveApi(req, res, host) {
384
400
  }
385
401
  }
386
402
 
403
+ async function serveCoreArtifactMedia(artifactId, method, res) {
404
+ try {
405
+ const result = await readCoreArtifactMediaForDsh(artifactId);
406
+ if (res.writableEnded || res.destroyed) return;
407
+ res.writeHead(200, {
408
+ 'Content-Type': result.artifact.mediaType,
409
+ 'Content-Length': result.bytes.length,
410
+ 'Content-Disposition': 'inline',
411
+ 'Cache-Control': 'private, max-age=3600',
412
+ 'X-Content-Type-Options': 'nosniff',
413
+ 'Cross-Origin-Resource-Policy': 'same-origin',
414
+ 'Referrer-Policy': 'no-referrer'
415
+ });
416
+ res.end(method === 'HEAD' ? undefined : result.bytes);
417
+ } catch (_) {
418
+ return sendJson(res, 404, { error: 'not found' });
419
+ }
420
+ }
421
+
422
+ async function serveCoreSnapshot(res, options = {}) {
423
+ try {
424
+ return sendJson(res, 200, await coreSnapshotForDsh({ limit: options.limit || 12 }));
425
+ } catch (error) {
426
+ console.error('[iris] Core 只读快照失败:', error && error.code || 'unknown');
427
+ return sendJson(res, 409, {
428
+ schemaVersion: 0, available: true, readOnly: true,
429
+ error: { code: String(error && error.code || 'IRIS_CORE_SNAPSHOT_FAILED') }
430
+ });
431
+ }
432
+ }
433
+
387
434
  function sendJson(res, status, obj) {
388
435
  if (res.writableEnded || res.destroyed) return; // 已结束/已销毁的连接不再写(end 后二次 end 防御)
389
436
  if (!res.headersSent) res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff' });
@@ -424,8 +471,9 @@ function handleJsonPost(req, res, execute) {
424
471
  const result = await execute(body, controller.signal);
425
472
  finish(200, result);
426
473
  } catch (err) {
427
- finish(controller.signal.aborted ? 499 : 400, {
428
- error: controller.signal.aborted ? 'request canceled' : String((err && err.message) || err)
474
+ finish(controller.signal.aborted ? 499 : (Number(err && err.statusCode) || 400), {
475
+ error: controller.signal.aborted ? 'request canceled' : String((err && err.message) || err),
476
+ ...(err && err.code ? { code: String(err.code) } : {})
429
477
  });
430
478
  }
431
479
  });
@@ -436,6 +484,105 @@ function handleAction(host, name, req, res) {
436
484
  return handleJsonPost(req, res, (args, signal) => runAction(host, name, args, { signal }));
437
485
  }
438
486
 
487
+ /**
488
+ * POST /iris/api/core/task/:id/(reobserve|redeliver) —— D1/D2 人工动作。
489
+ * 成功返回 200 + 任务最新五类用户投影行;坏 ID/未知/不满足动作门映射为稳定 4xx。
490
+ * 任何拒绝都在 Provider 调用与 Task 写入之前发生;错误体不含绝对路径、Key、
491
+ * 供应商身份或 binding。请求体不接受参数(Task ID 只在 URL 里),客户端断开
492
+ * 经 AbortSignal 传入在途操作,与 GUI actions 同一约定。
493
+ */
494
+ const CORE_TASK_MANUAL_ACTIONS = Object.freeze({
495
+ reobserve: { command: 'task.reobserve', run: reobserveProviderTaskForDsh },
496
+ redeliver: { command: 'task.redeliver', run: redeliverProviderTaskForDsh },
497
+ cancel: { command: 'task.cancel', run: cancelProviderTaskForDsh },
498
+ // 注意力处置是 Host 偏好(不写 Core 事实):acknowledge/restore/hide/unhide。
499
+ acknowledge: { command: 'attention.acknowledge', run: (input) => disposeCoreTaskAttention({ ...input, action: 'acknowledge' }) },
500
+ restore: { command: 'attention.restore', run: (input) => disposeCoreTaskAttention({ ...input, action: 'restore' }) },
501
+ hide: { command: 'attention.hide', run: (input) => disposeCoreTaskAttention({ ...input, action: 'hide' }) },
502
+ unhide: { command: 'attention.unhide', run: (input) => disposeCoreTaskAttention({ ...input, action: 'unhide' }) }
503
+ });
504
+
505
+ /**
506
+ * POST /iris/api/core/task/:id/retry —— D4 人工重试为新任务。
507
+ * body 必须携带 { prompt, confirmBilling: true, modelRef? }:Core 记录不持久化
508
+ * Prompt,只能由调用方重新提供;缺显式计费确认在读取旧 Task 之前拒绝。
509
+ * 成功返回 200 + 新任务的五类用户投影行与 retriedFrom 关系。
510
+ */
511
+ function handleCoreTaskRetry(taskId, req, res) {
512
+ if (!/^task_[a-f0-9]{24}$/.test(taskId)) {
513
+ return sendJson(res, 400, { error: { code: 'IRIS_DSH_TASK_INVALID', message: 'Core Task ID 无效' } });
514
+ }
515
+ handleJsonPost(req, res, async (body, signal) => {
516
+ try {
517
+ if (!body || body.confirmBilling !== true) {
518
+ const error = new Error('重试会创建新任务并可能产生重复生成费用;请求体必须显式 confirmBilling:true');
519
+ error.code = 'IRIS_COMMAND_BILLING_CONFIRM_REQUIRED';
520
+ error.statusCode = 400;
521
+ throw error;
522
+ }
523
+ const prompt = String(body.prompt || '');
524
+ const result = await retryProviderTaskForDsh({
525
+ taskId,
526
+ prompt,
527
+ ...(body.text !== undefined ? { text: String(body.text) } : {}),
528
+ ...(body.voice !== undefined ? { voice: String(body.voice) } : {}),
529
+ ...(body.audio_url !== undefined ? { audio_url: String(body.audio_url) } : {}),
530
+ modelRef: typeof body.modelRef === 'string' && body.modelRef.trim() ? body.modelRef.trim() : undefined,
531
+ confirmBilling: true,
532
+ signal
533
+ });
534
+ return {
535
+ ok: true,
536
+ contractVersion: 0,
537
+ command: 'task.retry',
538
+ retriedFrom: result.retriedFrom,
539
+ task: result.row
540
+ };
541
+ } catch (error) {
542
+ const code = String(error && error.code || '');
543
+ error.statusCode = Number(error.statusCode)
544
+ || (code === 'IRIS_TASK_NOT_FOUND' ? 404
545
+ : (code === 'IRIS_TASK_NOT_RETRYABLE' || code.startsWith('IRIS_PROVIDER_')) ? 409
546
+ : 400);
547
+ throw error;
548
+ }
549
+ });
550
+ }
551
+
552
+ function handleCoreTaskManual(action, taskId, req, res) {
553
+ if (!/^task_[a-f0-9]{24}$/.test(taskId)) {
554
+ return sendJson(res, 400, { error: { code: 'IRIS_DSH_TASK_INVALID', message: 'Core Task ID 无效' } });
555
+ }
556
+ const entry = CORE_TASK_MANUAL_ACTIONS[action];
557
+ if (!entry) return sendJson(res, 404, { error: 'not found' });
558
+ const controller = new AbortController();
559
+ let completed = false;
560
+ const finish = (status, body) => {
561
+ if (completed) return;
562
+ completed = true;
563
+ sendJson(res, status, body);
564
+ };
565
+ const disconnect = () => { if (!completed) controller.abort(); };
566
+ if (typeof req.on === 'function') req.on('aborted', disconnect);
567
+ if (typeof res.on === 'function') res.on('close', disconnect);
568
+ if (typeof req.resume === 'function') req.resume(); // 无 body 入参;避免小请求体滞留
569
+ entry.run({ taskId, signal: controller.signal })
570
+ .then((result) => finish(200, {
571
+ ok: true,
572
+ contractVersion: 0,
573
+ command: entry.command,
574
+ task: result.row
575
+ }))
576
+ .catch((error) => {
577
+ const code = String(error && error.code || 'IRIS_CORE_MANUAL_ACTION_FAILED');
578
+ const status = controller.signal.aborted ? 499
579
+ : code === 'IRIS_TASK_NOT_FOUND' ? 404
580
+ : (code === 'IRIS_TASK_ID_INVALID' || code === 'IRIS_DSH_TASK_INVALID') ? 400
581
+ : 409;
582
+ finish(status, { error: { code, message: String((error && error.message) || '无法执行 Core Task 动作') } });
583
+ });
584
+ }
585
+
439
586
  function handlePromptOptimizer(host, operation, req, res) {
440
587
  return handleJsonPost(req, res, async (body, signal) => {
441
588
  if (operation === 'optimize') return optimizePrompt(host, body, { signal });