@mokuyoaxis/dsh-iris 0.2.5 → 0.2.7

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 (83) hide show
  1. package/.dsh/skills/iris-use/SKILL.md +2 -2
  2. package/.dsh/skills/iris-use/references/media-contract.md +2 -2
  3. package/CHANGELOG.md +49 -1
  4. package/README.en.md +39 -22
  5. package/README.md +40 -21
  6. package/bin/dsh-iris.js +166 -35
  7. package/docs/AGENT_QUICKSTART.md +7 -7
  8. package/docs/ARCHITECTURE.md +9 -3
  9. package/docs/CLI_MANAGEMENT.md +5 -1
  10. package/docs/CORE_RUNTIME_CONTRACT.md +36 -5
  11. package/docs/CORE_SKILLS.md +62 -0
  12. package/docs/DSH_COMPATIBILITY.md +8 -4
  13. package/docs/DSH_CORE_MIGRATION.md +4 -2
  14. package/docs/DSH_PROVIDER_CONNECTIONS.md +34 -0
  15. package/docs/DSH_RUNTIME.md +36 -0
  16. package/docs/FUNCTION_MODEL_ASSIGNMENTS.md +2 -0
  17. package/docs/FUNCTION_MODEL_POOLS.md +84 -0
  18. package/docs/HEADLESS_CLI.md +16 -8
  19. package/docs/HOST_ADAPTER_CONTRACT.md +7 -2
  20. package/docs/HOST_DOCTOR.md +14 -2
  21. package/docs/IMAGE_FLOW.md +71 -0
  22. package/docs/LOCAL_API.md +60 -0
  23. package/docs/LOCAL_API_CONTRACT.md +115 -0
  24. package/docs/LOCAL_CREATION.md +50 -0
  25. package/docs/MUSIC.md +2 -2
  26. package/docs/PROVIDER_HEALTH.md +26 -2
  27. package/docs/ROADMAP.md +18 -4
  28. package/docs/SECURITY.md +20 -2
  29. package/docs/SYNC_DELIVERY_RECOVERY.md +2 -0
  30. package/docs/VISION_CLI.md +2 -2
  31. package/docs/WEB_PROTOTYPE.md +47 -0
  32. package/docs/releases/0.2.6.md +50 -0
  33. package/docs/releases/0.2.7.md +62 -0
  34. package/lib/actions.js +208 -90
  35. package/lib/adapters.js +36 -9
  36. package/lib/api.js +40 -39
  37. package/lib/application-runtime.js +186 -0
  38. package/lib/client.js +805 -182
  39. package/lib/composite-vision-routing.js +6 -6
  40. package/lib/config.js +140 -29
  41. package/lib/core-artifact-store.js +5 -0
  42. package/lib/core-maintenance.js +25 -2
  43. package/lib/core-runtime.js +19 -0
  44. package/lib/core-tasks.js +62 -8
  45. package/lib/creation-request-store.js +159 -0
  46. package/lib/doctor.js +20 -2
  47. package/lib/dsh-core-adapter.js +669 -564
  48. package/lib/dsh-host-adapter.js +14 -1
  49. package/lib/dsh-provider-config.js +35 -0
  50. package/lib/dsh-provider-source.js +102 -0
  51. package/lib/flow-store.js +195 -0
  52. package/lib/headless-vision.js +5 -5
  53. package/lib/host-contract.js +1 -0
  54. package/lib/http-vision-model-adapter.js +5 -2
  55. package/lib/image-flow.js +188 -0
  56. package/lib/index.js +103 -78
  57. package/lib/local-api-projection.js +27 -0
  58. package/lib/local-api.js +381 -0
  59. package/lib/local-creation.js +85 -0
  60. package/lib/model-directory-service.js +53 -0
  61. package/lib/model-directory.js +103 -0
  62. package/lib/models.js +71 -6
  63. package/lib/ocr-model-routing.js +2 -2
  64. package/lib/private-storage.js +12 -0
  65. package/lib/provider-adapters.js +42 -9
  66. package/lib/provider-catalog.js +14 -9
  67. package/lib/provider-config-scope.js +16 -0
  68. package/lib/provider-config-service.js +82 -13
  69. package/lib/provider-health.js +33 -0
  70. package/lib/provider-protocol.js +30 -0
  71. package/lib/provider-task-runner.js +22 -2
  72. package/lib/render-address.js +35 -0
  73. package/lib/render.js +52 -13
  74. package/lib/skill-installation.js +199 -0
  75. package/lib/vision-model-routing.js +25 -12
  76. package/lib/workbench-artifacts.js +1 -2
  77. package/lib/workbench-management.js +8 -9
  78. package/package.json +4 -3
  79. package/skills/iris-core/SKILL.md +40 -0
  80. package/skills/iris-core/agents/openai.yaml +4 -0
  81. package/skills/iris-core/references/cli.md +95 -0
  82. package/skills/iris-core/references/tasks.md +58 -0
  83. package/user_guide.md +32 -20
@@ -0,0 +1,159 @@
1
+ // Private local creation intents. No Host, HTTP, Provider or Runtime dependencies.
2
+ import crypto from 'node:crypto';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { atomicCreatePrivate, atomicWritePrivate } from './private-storage.js';
6
+ import { inspectFlowRoot, validateFlowCreation } from './flow-store.js';
7
+ import { WRITER_LEASE_DIR, WRITER_INTENT_FILE } from './core-writer-lease.js';
8
+
9
+ const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/;
10
+ const ID = /^req_[a-f0-9]{64}$/;
11
+ const ARTIFACT = /^artifact_[a-f0-9]{24}$/;
12
+ const SHA = /^[a-f0-9]{64}$/;
13
+ const DIGEST = /^sha256:[a-f0-9]{64}$/;
14
+ const TYPES = new Set(['image/png', 'image/jpeg', 'image/webp']);
15
+ const MAX_IMAGE = 20 * 1024 * 1024;
16
+ const iso = value => typeof value === 'string' && Number.isFinite(Date.parse(value)) && new Date(value).toISOString() === value;
17
+ const hash = value => crypto.createHash('sha256').update(value).digest('hex');
18
+ const copy = value => structuredClone(value);
19
+ function keys(value, allowed, required = allowed) {
20
+ return value && typeof value === 'object' && !Array.isArray(value)
21
+ && Object.keys(value).every(key => allowed.includes(key)) && required.every(key => Object.hasOwn(value, key));
22
+ }
23
+ export function creationFail(code, message) { throw Object.assign(new Error(message), { code }); }
24
+ const invalid = () => creationFail('IRIS_CREATION_STORE_INVALID', '创建请求区未知、损坏或尚未完整初始化');
25
+ function directory(root, segments, create = false) {
26
+ let current = root;
27
+ for (const segment of segments) {
28
+ current = path.join(current, segment);
29
+ if (create && !fs.existsSync(current)) fs.mkdirSync(current, { mode: 0o700 });
30
+ const stat = fs.lstatSync(current);
31
+ if (!stat.isDirectory() || stat.isSymbolicLink()) invalid();
32
+ }
33
+ return current;
34
+ }
35
+ function jsonFile(file) {
36
+ const stat = fs.lstatSync(file);
37
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 128 * 1024) invalid();
38
+ try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch (_) { invalid(); }
39
+ }
40
+
41
+ export function inspectCreationRoot(root, { allowAbsent = false } = {}) {
42
+ let base;
43
+ try { base = directory(root, ['creation-requests']); }
44
+ catch (error) {
45
+ if (error.code === 'ENOENT' && !fs.existsSync(path.join(root, 'creation-requests'))) {
46
+ if (allowAbsent) return null;
47
+ creationFail('IRIS_CREATION_ROOT_REQUIRED', '请先在空的专用 Flow 根显式执行 api init');
48
+ }
49
+ if (error.code === 'ENOENT') invalid();
50
+ throw error;
51
+ }
52
+ try {
53
+ if (fs.readdirSync(base).some(name => name !== 'v0')) invalid();
54
+ const version = directory(root, ['creation-requests', 'v0']);
55
+ if (fs.readdirSync(version).some(name => !['root.json', 'requests'].includes(name)
56
+ && !/^root\.json\.tmp-[1-9][0-9]*-[a-f0-9]{12}$/.test(name))) invalid();
57
+ const marker = jsonFile(path.join(version, 'root.json'));
58
+ if (!keys(marker, ['schemaVersion', 'kind', 'rootId', 'createdAt']) || marker.schemaVersion !== 0
59
+ || marker.kind !== 'iris-creation-requests' || !UUID.test(marker.rootId) || !iso(marker.createdAt)) invalid();
60
+ directory(root, ['creation-requests', 'v0', 'requests']);
61
+ return copy(marker);
62
+ } catch (error) { if (error.code === 'ENOENT') invalid(); throw error; }
63
+ }
64
+
65
+ export function initializeCreationRoot(root) {
66
+ const existing = inspectCreationRoot(root, { allowAbsent: true });
67
+ if (existing) { listCreationRequests(root); return existing; }
68
+ inspectFlowRoot(root);
69
+ if (fs.readdirSync(root).some(name => !['flow-store', WRITER_LEASE_DIR, WRITER_INTENT_FILE].includes(name))
70
+ || fs.readdirSync(path.join(root, 'flow-store')).some(name => name !== 'v0')
71
+ || fs.readdirSync(path.join(root, 'flow-store/v0')).some(name => !['root.json', 'runs'].includes(name))
72
+ || fs.readdirSync(path.join(root, 'flow-store/v0/runs')).length) {
73
+ creationFail('IRIS_CREATION_ROOT_NOT_EMPTY', '请求区仅在新的空 Flow 根初始化;不转换已有创作数据');
74
+ }
75
+ const marker = { schemaVersion: 0, kind: 'iris-creation-requests', rootId: crypto.randomUUID(), createdAt: new Date().toISOString() };
76
+ directory(root, ['creation-requests', 'v0', 'requests'], true);
77
+ atomicCreatePrivate(path.join(root, 'creation-requests/v0/root.json'), JSON.stringify(marker, null, 2) + '\n');
78
+ return copy(marker);
79
+ }
80
+
81
+ export function creationRequestId(marker, kind, key) {
82
+ if (!UUID.test(key)) creationFail('IRIS_CREATION_KEY_INVALID', 'Idempotency-Key 须为单个小写 UUIDv4');
83
+ if (!['image-import', 'flow-create'].includes(kind)) throw new TypeError('未知本地创建类型');
84
+ return 'req_' + hash(JSON.stringify(['creation-requests/v0', marker.rootId, kind, key]));
85
+ }
86
+ export function creationInputDigest(kind, input) {
87
+ return 'sha256:' + hash(JSON.stringify(kind === 'image-import'
88
+ ? ['image-import/v0', input.mediaType, input.size, input.sha256]
89
+ : ['flow-create/v0', 'image-edit/v0', input.source_artifact_id, input.model_ref, input.prompt, input.size ?? null, 1]));
90
+ }
91
+ function validate(record, id, marker) {
92
+ if (!keys(record, ['schemaVersion', 'id', 'rootId', 'kind', 'inputDigest', 'createdAt', 'intent', 'state', 'completedAt', 'result'],
93
+ ['schemaVersion', 'id', 'rootId', 'kind', 'inputDigest', 'createdAt', 'intent', 'state'])
94
+ || record.schemaVersion !== 0 || !ID.test(id) || record.id !== id || record.rootId !== marker.rootId
95
+ || !DIGEST.test(record.inputDigest) || !iso(record.createdAt) || record.intent?.createdAt !== record.createdAt
96
+ || !['prepared', 'succeeded'].includes(record.state)) invalid();
97
+ const intent = record.intent;
98
+ let expectedResult, expectedDigest;
99
+ if (record.kind === 'image-import') {
100
+ if (!keys(intent, ['artifactId', 'mediaType', 'size', 'sha256', 'metadata', 'createdAt'])
101
+ || !ARTIFACT.test(intent.artifactId) || !TYPES.has(intent.mediaType) || !SHA.test(intent.sha256)
102
+ || !Number.isSafeInteger(intent.size) || intent.size < 1 || intent.size > MAX_IMAGE
103
+ || !keys(intent.metadata, ['width', 'height']) || !['width', 'height'].every(key => Number.isSafeInteger(intent.metadata[key]) && intent.metadata[key] > 0)) invalid();
104
+ expectedDigest = creationInputDigest(record.kind, intent);
105
+ expectedResult = { artifactId: intent.artifactId };
106
+ } else if (record.kind === 'flow-create') {
107
+ try { validateFlowCreation(intent); } catch (_) { invalid(); }
108
+ expectedDigest = creationInputDigest(record.kind, intent.input);
109
+ expectedResult = { runId: intent.runId };
110
+ } else invalid();
111
+ if (record.inputDigest !== expectedDigest) invalid();
112
+ if (record.state === 'prepared') {
113
+ if (Object.hasOwn(record, 'completedAt') || Object.hasOwn(record, 'result')) invalid();
114
+ } else if (!iso(record.completedAt) || !keys(record.result, Object.keys(expectedResult))
115
+ || JSON.stringify(record.result) !== JSON.stringify(expectedResult)) invalid();
116
+ return record;
117
+ }
118
+ export function readCreationRequest(root, id, marker = inspectCreationRoot(root)) {
119
+ if (!ID.test(id)) invalid();
120
+ let record;
121
+ try { record = jsonFile(path.join(root, 'creation-requests/v0/requests', id + '.json')); }
122
+ catch (error) { if (error.code === 'ENOENT') return null; throw error; }
123
+ return copy(validate(record, id, marker));
124
+ }
125
+ export function listCreationRequests(root) {
126
+ const marker = inspectCreationRoot(root, { allowAbsent: true });
127
+ if (!marker) return [];
128
+ const records = [];
129
+ for (const name of fs.readdirSync(path.join(root, 'creation-requests/v0/requests'))) {
130
+ if (/^req_[a-f0-9]{64}\.json\.tmp-[1-9][0-9]*-[a-f0-9]{12}$/.test(name)) continue;
131
+ if (!/^req_[a-f0-9]{64}\.json$/.test(name)) invalid();
132
+ const record = readCreationRequest(root, name.slice(0, -5), marker);
133
+ if (!record) invalid();
134
+ records.push(record);
135
+ }
136
+ return records;
137
+ }
138
+ export function publishCreationRequest(root, record) {
139
+ const marker = inspectCreationRoot(root);
140
+ validate(record, record.id, marker);
141
+ atomicCreatePrivate(path.join(root, 'creation-requests/v0/requests', record.id + '.json'), JSON.stringify(record, null, 2) + '\n');
142
+ return copy(record);
143
+ }
144
+ export function completeCreationRequest(root, record) {
145
+ const current = readCreationRequest(root, record.id);
146
+ if (!current || JSON.stringify(current) !== JSON.stringify(record) || current.state !== 'prepared') invalid();
147
+ const finished = { ...record, state: 'succeeded', completedAt: new Date().toISOString(),
148
+ result: record.kind === 'image-import' ? { artifactId: record.intent.artifactId } : { runId: record.intent.runId } };
149
+ validate(finished, record.id, inspectCreationRoot(root));
150
+ atomicWritePrivate(path.join(root, 'creation-requests/v0/requests', record.id + '.json'), JSON.stringify(finished, null, 2) + '\n');
151
+ return copy(finished);
152
+ }
153
+ export function creationReferences(root) {
154
+ return listCreationRequests(root).filter(record => record.state === 'prepared').map(record => ({
155
+ requestId: record.id, taskIds: record.kind === 'flow-create' ? [record.intent.taskId] : [],
156
+ artifactIds: record.kind === 'image-import' ? [record.intent.artifactId]
157
+ : [record.intent.source.artifactId, record.intent.comparisonArtifactId]
158
+ }));
159
+ }
package/lib/doctor.js CHANGED
@@ -103,16 +103,23 @@ function inspectConfig(dataDir, checks) {
103
103
  let incomplete = 0;
104
104
  let enabled = 0;
105
105
  let models = 0;
106
+ let hostReferences = 0;
106
107
  for (const provider of providers) {
107
108
  if (!plain(provider)) { incomplete++; continue; }
108
109
  if (provider.enabled !== false) enabled++;
109
- if (provider.enabled !== false && (!providerMediaBaseUrl(provider)
110
+ const reference = provider.dshProvider;
111
+ const managed = plain(reference) && typeof reference.provider === 'string' && reference.provider.trim()
112
+ && typeof reference.settingsNs === 'string' && reference.settingsNs.trim();
113
+ if (provider.enabled !== false && managed) hostReferences++;
114
+ if (provider.enabled !== false && !managed && (!providerMediaBaseUrl(provider)
110
115
  || (provider.auth !== 'none' && !providerApiKey(provider).trim()))) incomplete++;
111
116
  models += Array.isArray(provider.models) ? provider.models.length : 0;
112
117
  }
113
118
  if (incomplete) checks.push(result('providers', 'warn', `${incomplete} 个启用供应商缺少端点、所需凭据或结构无效(auth:none 不要求密钥)`, '在 Iris 工作台补全或停用对应供应商。'));
114
119
  else if (!enabled) checks.push(result('providers', 'warn', '没有启用的供应商', '在 Iris 工作台启用至少一个供应商。'));
115
120
  else checks.push(result('providers', 'ok', `${enabled} 个供应商已启用;凭据未输出`));
121
+ if (hostReferences) checks.push(result('provider-references', 'warn', `${hostReferences} 个 DSH 连接引用需由运行中宿主解析;离线未验证连接`,
122
+ '在 DSH 的 Iris 工作台检查原连接与功能池候选,无需在 Iris 重填宿主密钥。'));
116
123
  const assignments = config.value.assignments || {};
117
124
  const assignmentCount = Object.keys(assignments).length;
118
125
  if (!models) checks.push(result('models', 'warn', '模型池为空或尚未发现模型', '在工作台对供应商执行“发现模型”,再核对能力标签。'));
@@ -414,7 +421,18 @@ export function hostDoctor(host, evidence = {}, options = {}) {
414
421
  + (missingSkills.length ? ':' + missingSkills.join(', ') : ''),
415
422
  '工具仍可直接调用;检查 DSH Skill registry 和插件启动日志。'));
416
423
 
417
- capabilityCheck(checks, snapshot, 'browser', 'browser', 'HTML Browser');
424
+ if (capabilityCheck(checks, snapshot, 'browser', 'browser', 'HTML Browser')) {
425
+ checks[checks.length - 1] = result('browser', 'ok', 'HTML 浏览器接口已提供;实际截图需单独自检',
426
+ '点击“截图自检”验证当前浏览器能否启动、打开页面并生成图片。');
427
+ const address = host.ports.browser.address;
428
+ if (address) {
429
+ checks.push(result('render-address', address.status === 'resolved' ? 'ok' : 'warn',
430
+ address.status === 'resolved'
431
+ ? '截图地址:' + address.baseUrl + (address.source === 'environment' ? '(DSH_WEB_BASE)' : '(宿主监听)')
432
+ : address.message,
433
+ address.status === 'resolved' ? '' : '显式配置 DSH_WEB_BASE;不需要额外的端口转发。'));
434
+ }
435
+ }
418
436
  capabilityCheck(checks, snapshot, 'attachments', 'attachments', '附件读写');
419
437
  capabilityCheck(checks, snapshot, 'sessions', 'sessions', '会话查询');
420
438
  capabilityCheck(checks, snapshot, 'text-model', 'textModel', '宿主文本模型');