@mokuyoaxis/dsh-iris 0.2.0 → 0.2.1

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 (88) hide show
  1. package/.dsh/skills/iris-compose-media/SKILL.md +10 -5
  2. package/.dsh/skills/iris-compose-media/references/workflows.md +7 -1
  3. package/.dsh/skills/iris-verify-ui/SKILL.md +2 -1
  4. package/CHANGELOG.md +47 -2
  5. package/README.en.md +65 -29
  6. package/README.md +65 -29
  7. package/bin/dsh-iris.js +222 -21
  8. package/docs/AGENT_QUICKSTART.md +10 -6
  9. package/docs/ARCHITECTURE.md +25 -18
  10. package/docs/ARTIFACT_LIBRARY.md +5 -5
  11. package/docs/ARTIFACT_MANIFEST.md +1 -1
  12. package/docs/CLI_MANAGEMENT.md +181 -0
  13. package/docs/COMPOSITE_VISION.md +35 -0
  14. package/docs/CORE_RUNTIME_CONTRACT.md +8 -7
  15. package/docs/DSH_CORE_MIGRATION.md +48 -19
  16. package/docs/DSH_RC2_ADAPTATION.md +4 -2
  17. package/docs/FAULT_INJECTION.md +12 -2
  18. package/docs/HEADLESS_CLI.md +13 -7
  19. package/docs/HOST_ADAPTER_CONTRACT.md +10 -10
  20. package/docs/HOST_DOCTOR.md +3 -3
  21. package/docs/MODEL_PORT_CONTRACT.md +19 -14
  22. package/docs/OCR_MODEL.md +39 -0
  23. package/docs/PROMPT_OPTIMIZER.md +47 -0
  24. package/docs/PROVIDER_ADAPTER_CONTRACT.md +6 -4
  25. package/docs/PROVIDER_HEALTH.md +22 -5
  26. package/docs/PROVIDER_RUNTIME_HARNESS.md +1 -1
  27. package/docs/PROVIDER_SUBMISSION_CONTRACT.md +2 -2
  28. package/docs/ROADMAP.md +29 -19
  29. package/docs/SECURITY.md +5 -5
  30. package/docs/TASK_SEMANTICS.md +2 -2
  31. package/docs/VISION_CLI.md +132 -0
  32. package/docs/VISION_MODEL.md +51 -0
  33. package/docs/WORKBENCH_ARTIFACTS.md +67 -0
  34. package/docs/file-access-across-environments.md +8 -5
  35. package/docs/releases/0.2.1.md +56 -0
  36. package/lib/actions.js +187 -270
  37. package/lib/adapters.js +133 -22
  38. package/lib/api.js +49 -7
  39. package/lib/chromium-browser.js +117 -0
  40. package/lib/client.js +671 -101
  41. package/lib/command-service.js +121 -7
  42. package/lib/composite-vision-routing.js +42 -0
  43. package/lib/config.js +89 -10
  44. package/lib/core-artifact-store.js +45 -13
  45. package/lib/core-maintenance.js +213 -0
  46. package/lib/core-tasks.js +12 -5
  47. package/lib/core-user-projection.js +1 -0
  48. package/lib/dsh-core-adapter.js +63 -9
  49. package/lib/dsh-text-model-adapter.js +222 -0
  50. package/lib/dsh-vision-model-adapter.js +75 -0
  51. package/lib/generation-input.js +36 -5
  52. package/lib/headless-vision.js +181 -0
  53. package/lib/host-runtime.js +1 -0
  54. package/lib/http-vision-model-adapter.js +187 -0
  55. package/lib/image-edit-input.js +34 -0
  56. package/lib/index.js +205 -175
  57. package/lib/locate.js +86 -28
  58. package/lib/media-probe.js +2 -1
  59. package/lib/model-call-runtime.js +45 -0
  60. package/lib/model-invoker.js +4 -2
  61. package/lib/model-port-contract.js +13 -5
  62. package/lib/models.js +10 -5
  63. package/lib/ocr-model-routing.js +26 -0
  64. package/lib/ocr.js +110 -64
  65. package/lib/prompt-optimizer-config.js +2 -1
  66. package/lib/prompt-optimizer-core.js +121 -0
  67. package/lib/prompt-optimizer.js +37 -193
  68. package/lib/provider-adapters.js +85 -9
  69. package/lib/provider-catalog.js +19 -7
  70. package/lib/provider-config-service.js +236 -0
  71. package/lib/provider-contract.js +3 -2
  72. package/lib/provider-health.js +80 -1
  73. package/lib/provider-protocol.js +23 -0
  74. package/lib/provider-task-runner.js +58 -16
  75. package/lib/summarize.js +31 -20
  76. package/lib/summary-input.js +52 -0
  77. package/lib/task-wait.js +40 -0
  78. package/lib/video-input.js +30 -0
  79. package/lib/vision-core.js +42 -0
  80. package/lib/vision-image-input.js +83 -0
  81. package/lib/vision-input.js +31 -0
  82. package/lib/vision-model-routing.js +180 -0
  83. package/lib/vision.js +3 -2
  84. package/lib/workbench-artifacts.js +52 -0
  85. package/lib/workbench-management.js +151 -0
  86. package/lib/workbench-zip.js +39 -0
  87. package/package.json +1 -1
  88. package/user_guide.md +52 -22
@@ -0,0 +1,213 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import crypto from 'node:crypto';
4
+ import { inspectCoreArtifact, refreshCoreArtifactInventory } from './core-artifacts.js';
5
+ import { inspectCoreTask, listCoreTasks } from './core-tasks.js';
6
+ import { atomicWritePrivate } from './private-storage.js';
7
+
8
+ const TASK = /^task_[a-f0-9]{24}$/;
9
+ const ARTIFACT = /^artifact_[a-f0-9]{24}$/;
10
+ const TRANSACTION = /^delete_[a-f0-9]{24}$/;
11
+ const MANAGED = /^(?:task-store\/v0\/tasks\/task_[a-f0-9]{24}\.json|artifact-store\/v0\/(?:records|manifests)\/artifact_[a-f0-9]{24}\.json|artifact-store\/v0\/objects\/artifact_[a-f0-9]{24}\.(?:png|jpg|webp|gif|mp4|webm|wav|mp3|ogg|flac|txt|json)|provider-staging\/v0\/download-[1-9][0-9]*-[a-f0-9]{16}\.part)$/;
12
+ const fail = (code, message) => { throw Object.assign(new Error(message), { code }); };
13
+ function fileDigest(file) {
14
+ const hash = crypto.createHash('sha256'), chunk = Buffer.allocUnsafe(1024 * 1024), fd = fs.openSync(file, 'r');
15
+ try { let size; while ((size = fs.readSync(fd, chunk, 0, chunk.length, null))) hash.update(chunk.subarray(0, size)); }
16
+ finally { fs.closeSync(fd); }
17
+ return hash.digest('hex');
18
+ }
19
+
20
+ function directory(root, relative, create = false) {
21
+ let current = root;
22
+ for (const segment of relative.split('/')) {
23
+ current = path.join(current, segment);
24
+ if (create && !fs.existsSync(current)) fs.mkdirSync(current, { mode: 0o700 });
25
+ const stat = fs.lstatSync(current);
26
+ if (!stat.isDirectory() || stat.isSymbolicLink()) fail('IRIS_CORE_MAINTENANCE_UNSAFE', 'Core 管理目录不安全');
27
+ }
28
+ return current;
29
+ }
30
+
31
+ function managedFile(root, relative) {
32
+ if (!MANAGED.test(relative)) fail('IRIS_CORE_MAINTENANCE_UNSAFE', '不是可管理的 Core 文件');
33
+ directory(root, path.posix.dirname(relative));
34
+ const file = path.join(root, relative);
35
+ const stat = fs.lstatSync(file);
36
+ if (!stat.isFile() || stat.isSymbolicLink()) fail('IRIS_CORE_MAINTENANCE_UNSAFE', 'Core 文件不是普通文件');
37
+ return { path: relative, size: stat.size, digest: fileDigest(file) };
38
+ }
39
+
40
+ function names(root, relative) {
41
+ if (!fs.existsSync(path.join(root, relative))) return [];
42
+ return fs.readdirSync(directory(root, relative));
43
+ }
44
+
45
+ function inventory(root) {
46
+ const tasks = [];
47
+ for (let offset = 0; ; offset += 200) {
48
+ const page = listCoreTasks(root, { offset, limit: 200 }); tasks.push(...page.tasks);
49
+ if (offset + 200 >= page.total) break;
50
+ }
51
+ const artifacts = names(root, 'artifact-store/v0/records').filter(name => ARTIFACT.test(name.slice(0, -5)) && name.endsWith('.json'))
52
+ .map(name => inspectCoreArtifact(root, name.slice(0, -5)));
53
+ return { tasks, artifacts };
54
+ }
55
+
56
+ function ids(value, pattern, label) {
57
+ if (value === undefined) return [];
58
+ if (!Array.isArray(value) || value.length > 200 || !value.every(id => typeof id === 'string' && pattern.test(id)) || new Set(value).size !== value.length) fail('IRIS_COMMAND_INPUT_INVALID', label + ' 必须是不重复的 Core ID 数组,最多 200 条');
59
+ return value;
60
+ }
61
+
62
+ export function planCoreDeletion(root, input) {
63
+ const taskIds = ids(input.task_ids, TASK, 'task_ids'), artifactIds = ids(input.artifact_ids, ARTIFACT, 'artifact_ids');
64
+ if (!taskIds.length && !artifactIds.length) fail('IRIS_COMMAND_INPUT_INVALID', '请明确指定 Task 或 Artifact ID');
65
+ const existing = inventory(root), selectedTasks = new Set(taskIds), selectedArtifacts = new Set(artifactIds);
66
+ const blockers = [], files = [];
67
+ for (const id of taskIds) {
68
+ const task = inspectCoreTask(root, id);
69
+ if (task.phase !== 'terminal' || task.acceptance === 'unknown' || task.outcome === 'unknown') blockers.push({ id, reason: 'task_not_settled' });
70
+ files.push(managedFile(root, 'task-store/v0/tasks/' + id + '.json'));
71
+ }
72
+ for (const id of artifactIds) {
73
+ inspectCoreArtifact(root, id);
74
+ const recordPath = 'artifact-store/v0/records/' + id + '.json';
75
+ const recordFile = managedFile(root, recordPath);
76
+ const record = JSON.parse(fs.readFileSync(path.join(root, recordPath)));
77
+ const manifestPath = 'artifact-store/v0/manifests/' + id + '.json';
78
+ const manifest = record.schemaVersion === 1 ? JSON.parse(fs.readFileSync(path.join(root, manifestPath))) : record;
79
+ files.push(recordFile);
80
+ if (record.schemaVersion === 1) files.push(managedFile(root, manifestPath));
81
+ files.push(managedFile(root, 'artifact-store/v0/objects/' + manifest.object));
82
+ }
83
+ for (const task of existing.tasks) {
84
+ if (selectedTasks.has(task.id)) continue;
85
+ for (const id of task.artifactIds) if (selectedArtifacts.has(id)) blockers.push({ id, referencedBy: task.id, reason: 'task_artifact_reference' });
86
+ if (selectedArtifacts.has(task.sourceArtifactId)) blockers.push({ id: task.sourceArtifactId, referencedBy: task.id, reason: 'task_input_reference' });
87
+ if (selectedTasks.has(task.retriedFrom)) blockers.push({ id: task.retriedFrom, referencedBy: task.id, reason: 'retry_reference' });
88
+ }
89
+ for (const artifact of existing.artifacts) {
90
+ if (selectedArtifacts.has(artifact.id)) continue;
91
+ if (selectedTasks.has(artifact.metadata.taskId)) blockers.push({ id: artifact.metadata.taskId, referencedBy: artifact.id, reason: 'artifact_task_reference' });
92
+ for (const edge of artifact.relations) if (selectedArtifacts.has(edge.artifactId)) blockers.push({ id: edge.artifactId, referencedBy: artifact.id, reason: 'artifact_relation' });
93
+ }
94
+ // 未提交 Manifest 也可能仍引用目标;未知或损坏的条目保留并阻止误判。
95
+ const committed = new Set(existing.artifacts.map(artifact => artifact.id));
96
+ for (const name of names(root, 'artifact-store/v0/manifests')) {
97
+ if (!/^artifact_[a-f0-9]{24}\.json$/.test(name) || committed.has(name.slice(0, -5))) continue;
98
+ const relative = 'artifact-store/v0/manifests/' + name; managedFile(root, relative);
99
+ const manifest = JSON.parse(fs.readFileSync(path.join(root, relative)));
100
+ for (const edge of manifest.relations || []) if (selectedArtifacts.has(edge.artifactId)) blockers.push({ id: edge.artifactId, referencedBy: name.slice(0, -5), reason: 'uncommitted_relation' });
101
+ }
102
+ return { operation: 'delete', task_ids: taskIds, artifact_ids: artifactIds, allowed: !blockers.length, blockers,
103
+ entries: files, bytes: files.reduce((sum, entry) => sum + entry.size, 0) };
104
+ }
105
+
106
+ export function planCoreCleanup(root, { older_than_ms = 3600000 } = {}) {
107
+ if (!Number.isSafeInteger(older_than_ms) || older_than_ms < 60000) fail('IRIS_COMMAND_INPUT_INVALID', 'older_than_ms 至少为 60000');
108
+ const existing = inventory(root);
109
+ const referenced = new Set(existing.artifacts.map(artifact => artifact.id));
110
+ for (const artifact of existing.artifacts) for (const edge of artifact.relations) referenced.add(edge.artifactId);
111
+ for (const task of existing.tasks) for (const id of task.artifactIds) referenced.add(id);
112
+ for (const task of existing.tasks) if (task.sourceArtifactId) referenced.add(task.sourceArtifactId);
113
+ for (const name of names(root, 'artifact-store/v0/manifests')) {
114
+ if (/^artifact_[a-f0-9]{24}\.json$/.test(name)) {
115
+ referenced.add(name.slice(0, -5));
116
+ const relative = 'artifact-store/v0/manifests/' + name; managedFile(root, relative);
117
+ const manifest = JSON.parse(fs.readFileSync(path.join(root, relative)));
118
+ for (const edge of manifest.relations || []) referenced.add(edge.artifactId);
119
+ }
120
+ }
121
+ const candidates = [];
122
+ for (const relativeDirectory of ['provider-staging/v0', 'artifact-store/v0/objects']) {
123
+ for (const name of names(root, relativeDirectory)) {
124
+ const relative = relativeDirectory + '/' + name;
125
+ if (!MANAGED.test(relative)) continue;
126
+ const stat = fs.lstatSync(path.join(root, relative));
127
+ if (!stat.isFile() || stat.isSymbolicLink() || Date.now() - stat.mtimeMs < older_than_ms) continue;
128
+ if (relativeDirectory === 'provider-staging/v0') {
129
+ const pid = Number(name.split('-')[1]);
130
+ try { process.kill(pid, 0); continue; } catch (error) { if (error.code !== 'ESRCH') continue; }
131
+ } else if (referenced.has(name.slice(0, name.lastIndexOf('.')))) continue;
132
+ candidates.push(managedFile(root, relative));
133
+ }
134
+ }
135
+ return { operation: 'cleanup', candidates, bytes: candidates.reduce((sum, entry) => sum + entry.size, 0) };
136
+ }
137
+
138
+ const transactionDirectory = (root, id, create = false) => {
139
+ if (!TRANSACTION.test(id)) fail('IRIS_COMMAND_INPUT_INVALID', '隔离事务 ID 无效');
140
+ return directory(root, 'maintenance/v0/quarantine/' + id, create);
141
+ };
142
+ const refresh = root => { if (fs.existsSync(path.join(root, 'artifact-store/v0'))) refreshCoreArtifactInventory(root); };
143
+
144
+ /** 写前清单 + rename;失败时补偿,保留事务用于检查/恢复,绝不永久 unlink 媒体。 */
145
+ export function quarantineCoreFiles(root, plan) {
146
+ if (!plan.allowed || !plan.entries.length) fail('IRIS_CORE_DELETE_BLOCKED', '删除计划为空或仍被引用,不能执行');
147
+ const id = 'delete_' + crypto.randomBytes(12).toString('hex'), base = transactionDirectory(root, id, true);
148
+ const journal = { schemaVersion: 0, id, state: 'prepared', createdAt: new Date().toISOString(), operation: plan.operation, entries: plan.entries };
149
+ const journalFile = path.join(base, 'transaction.json');
150
+ atomicWritePrivate(journalFile, JSON.stringify(journal, null, 2) + '\n');
151
+ const moved = [];
152
+ try {
153
+ for (const entry of journal.entries) {
154
+ const actual = managedFile(root, entry.path);
155
+ if (actual.digest !== entry.digest || actual.size !== entry.size) fail('IRIS_CORE_MAINTENANCE_CHANGED', '预览后文件发生变化');
156
+ directory(base, path.posix.dirname(entry.path), true);
157
+ fs.renameSync(path.join(root, entry.path), path.join(base, entry.path)); moved.push(entry);
158
+ }
159
+ refresh(root);
160
+ journal.state = 'committed'; atomicWritePrivate(journalFile, JSON.stringify(journal, null, 2) + '\n');
161
+ return { transactionId: id, state: journal.state, files: moved.length, bytes: moved.reduce((sum, entry) => sum + entry.size, 0), recoverable: true };
162
+ } catch (error) {
163
+ try {
164
+ for (const entry of [...moved].reverse()) fs.renameSync(path.join(base, entry.path), path.join(root, entry.path));
165
+ refresh(root); journal.state = 'rolled-back';
166
+ } catch (_) { journal.state = 'interrupted'; }
167
+ atomicWritePrivate(journalFile, JSON.stringify(journal, null, 2) + '\n');
168
+ throw Object.assign(new Error('Core 隔离操作未完成,已保留恢复清单:' + id + '(' + journal.state + ')'), { code: 'IRIS_CORE_MAINTENANCE_FAILED', transactionId: id, state: journal.state, cause: error });
169
+ }
170
+ }
171
+
172
+ export function inspectCoreTransactions(root) {
173
+ return names(root, 'maintenance/v0/quarantine').filter(id => TRANSACTION.test(id)).map(id => {
174
+ const base = transactionDirectory(root, id);
175
+ const file = path.join(base, 'transaction.json'), stat = fs.lstatSync(file);
176
+ if (!stat.isFile() || stat.isSymbolicLink()) fail('IRIS_CORE_MAINTENANCE_UNSAFE', '隔离清单不是普通文件');
177
+ const journal = JSON.parse(fs.readFileSync(file));
178
+ return { transactionId: id, state: journal.state, operation: journal.operation, createdAt: journal.createdAt, files: journal.entries.length };
179
+ });
180
+ }
181
+
182
+ export function restoreCoreTransaction(root, id) {
183
+ const base = transactionDirectory(root, id), journalFile = path.join(base, 'transaction.json');
184
+ const stat = fs.lstatSync(journalFile);
185
+ if (!stat.isFile() || stat.isSymbolicLink()) fail('IRIS_CORE_MAINTENANCE_UNSAFE', '隔离清单不安全');
186
+ const journal = JSON.parse(fs.readFileSync(journalFile));
187
+ if (journal.schemaVersion !== 0 || journal.id !== id || !Array.isArray(journal.entries)
188
+ || !['prepared', 'committed', 'interrupted'].includes(journal.state)) fail('IRIS_CORE_RESTORE_INVALID', '事务不处于可恢复状态');
189
+ const restoring = [];
190
+ for (const entry of journal.entries) {
191
+ if (!MANAGED.test(entry.path)) fail('IRIS_CORE_MAINTENANCE_UNSAFE', '隔离清单包含不支持的路径');
192
+ if (fs.existsSync(path.join(base, entry.path))) {
193
+ const actual = managedFile(base, entry.path);
194
+ if (actual.digest !== entry.digest || actual.size !== entry.size) fail('IRIS_CORE_MAINTENANCE_CHANGED', '隔离文件哈希或大小改变');
195
+ if (fs.existsSync(path.join(root, entry.path))) fail('IRIS_CORE_RESTORE_CONFLICT', '恢复目标已存在,不覆盖已有数据');
196
+ directory(root, path.posix.dirname(entry.path)); restoring.push(entry);
197
+ } else {
198
+ const actual = managedFile(root, entry.path);
199
+ if (actual.digest !== entry.digest) fail('IRIS_CORE_MAINTENANCE_CHANGED', '事务原位置文件改变');
200
+ }
201
+ }
202
+ const moved = [];
203
+ try {
204
+ for (const entry of restoring) { fs.renameSync(path.join(base, entry.path), path.join(root, entry.path)); moved.push(entry); }
205
+ refresh(root);
206
+ journal.state = 'restored'; atomicWritePrivate(journalFile, JSON.stringify(journal, null, 2) + '\n');
207
+ return { transactionId: id, state: 'restored', files: moved.length };
208
+ } catch (_) {
209
+ for (const entry of [...moved].reverse()) fs.renameSync(path.join(root, entry.path), path.join(base, entry.path));
210
+ refresh(root);
211
+ fail('IRIS_CORE_RESTORE_FAILED', '恢复失败,文件已回到隔离目录');
212
+ }
213
+ }
package/lib/core-tasks.js CHANGED
@@ -11,6 +11,7 @@ import { deriveLegacyStatus, semanticViolations } from './task-semantics.js';
11
11
  const STORE_DIR = 'task-store';
12
12
  const STORE_VERSION = 'v0';
13
13
  const TASK_ID = /^task_[a-f0-9]{24}$/;
14
+ const ARTIFACT_ID = /^artifact_[a-f0-9]{24}$/;
14
15
  const ATTEMPT_ID = /^attempt_[a-f0-9]{24}$/;
15
16
  const PROVIDER_BINDING = /^sha256:[a-f0-9]{64}$/;
16
17
  const CAPABILITIES = new Set(['image', 'video', 'tts', 'transcribe']);
@@ -106,7 +107,8 @@ function validateRecord(record, expectedId) {
106
107
  || !Number.isSafeInteger(record.revision) || record.revision < 1
107
108
  || typeof record.createdAt !== 'string' || typeof record.updatedAt !== 'string'
108
109
  || (record.providerBinding !== undefined && !PROVIDER_BINDING.test(record.providerBinding))
109
- || (record.retriedFrom !== undefined && !TASK_ID.test(record.retriedFrom))) {
110
+ || (record.retriedFrom !== undefined && !TASK_ID.test(record.retriedFrom))
111
+ || (record.sourceArtifactId !== undefined && (record.capability !== 'image' || !ARTIFACT_ID.test(record.sourceArtifactId)))) {
110
112
  fail('IRIS_TASK_INVALID', 'Task 记录结构无效');
111
113
  }
112
114
  if (record.attempts.some((attempt, index) => !attempt || !ATTEMPT_ID.test(attempt.id)
@@ -170,6 +172,8 @@ export function createCoreTask(dataRoot, input = {}) {
170
172
  if (!CAPABILITIES.has(capability)) fail('IRIS_TASK_INPUT_INVALID', 'Task capability 无效');
171
173
  const retriedFrom = input.retriedFrom === undefined ? undefined : String(input.retriedFrom);
172
174
  if (retriedFrom !== undefined && !TASK_ID.test(retriedFrom)) fail('IRIS_TASK_INPUT_INVALID', 'retriedFrom 必须是合法 Task ID');
175
+ const sourceArtifactId = input.sourceArtifactId;
176
+ if (sourceArtifactId !== undefined && (capability !== 'image' || !ARTIFACT_ID.test(sourceArtifactId))) fail('IRIS_TASK_INPUT_INVALID', '改图 sourceArtifactId 必须是合法 Artifact ID');
173
177
  const now = new Date().toISOString();
174
178
  return writeRecord(dataRoot, {
175
179
  schemaVersion: 0,
@@ -189,7 +193,8 @@ export function createCoreTask(dataRoot, input = {}) {
189
193
  createdAt: now,
190
194
  updatedAt: now,
191
195
  // D4 retry as new task:显式单向关系(新任务指向旧任务),绝不复制 Prompt。
192
- ...(retriedFrom !== undefined ? { retriedFrom } : {})
196
+ ...(retriedFrom !== undefined ? { retriedFrom } : {}),
197
+ ...(sourceArtifactId !== undefined ? { sourceArtifactId } : {})
193
198
  });
194
199
  }
195
200
 
@@ -202,7 +207,7 @@ export function inspectCoreTask(dataRoot, id) {
202
207
  * skipInvalid 只服务用户侧只读投影:单条损坏记录跳过并计入 dropped,
203
208
  * 其余记录照常返回;默认严格模式不变,损坏记录仍使整体读取失败。
204
209
  */
205
- export function listCoreTasks(dataRoot, { offset = 0, limit = 50, skipInvalid = false } = {}) {
210
+ export function listCoreTasks(dataRoot, { offset = 0, limit = 50, skipInvalid = false, status, capability, provider_id, model_ref, outcome, delivery_state } = {}) {
206
211
  const start = Math.max(0, Math.trunc(Number(offset) || 0));
207
212
  const size = Math.max(1, Math.min(200, Math.trunc(Number(limit) || 50)));
208
213
  const directory = taskDirectory(dataRoot);
@@ -236,9 +241,11 @@ export function listCoreTasks(dataRoot, { offset = 0, limit = 50, skipInvalid =
236
241
  }
237
242
  }
238
243
  records.sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)));
244
+ const matches = records.filter(record => Object.entries({ status, capability, providerId: provider_id, modelRef: model_ref, outcome, deliveryState: delivery_state })
245
+ .every(([field, value]) => value === undefined || record[field] === value));
239
246
  return Object.freeze({
240
- schemaVersion: 0, total: records.length, offset: start, limit: size, dropped,
241
- tasks: Object.freeze(records.slice(start, start + size).map(publicTask))
247
+ schemaVersion: 0, total: matches.length, offset: start, limit: size, dropped,
248
+ tasks: Object.freeze(matches.slice(start, start + size).map(publicTask))
242
249
  });
243
250
  }
244
251
 
@@ -150,6 +150,7 @@ export function projectCoreTaskUserRow(record, artifacts = []) {
150
150
  updatedAt: String(record && record.updatedAt || ''),
151
151
  revision: Number.isSafeInteger(record && record.revision) ? record.revision : 0,
152
152
  artifactIds: Object.freeze(artifactIds),
153
+ ...(record && record.sourceArtifactId ? { sourceArtifactId: record.sourceArtifactId } : {}),
153
154
  mediaReady,
154
155
  observable: coreTaskObservable(record),
155
156
  redeliverable: coreTaskRedeliverable(record),
@@ -2,16 +2,19 @@
2
2
 
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
- import { allProviders, irisHome } from './config.js';
5
+ import { allProviders, irisHome, recordRateLimit } from './config.js';
6
6
  import { createCommandService } from './command-service.js';
7
- import { createCoreArtifact, inspectCoreArtifact, listCoreArtifacts, readCoreArtifactBytes } from './core-artifacts.js';
7
+ import { createCoreArtifact, inspectCoreArtifact, listCoreArtifacts, listCoreArtifactMetadata, readCoreArtifactBytes } from './core-artifacts.js';
8
8
  import { createCoreRuntime } from './core-runtime.js';
9
+ import { readSummaryFrames } from './summary-input.js';
10
+ import { readVisionArtifact } from './vision-input.js';
9
11
  import { inspectCoreTask, listCoreTasks, recoverCoreTask } from './core-tasks.js';
10
12
  import { applyCoreAttentionAction, attentionDispositionOf, readCoreAttentionPrefs } from './core-attention.js';
11
13
  import { projectCoreTaskUserRow } from './core-user-projection.js';
12
14
  import { requireHostPort } from './host-contract.js';
13
15
  import { createProviderTaskRunner } from './provider-task-runner.js';
14
- import { createConfiguredProviderAdapter } from './provider-adapters.js';
16
+ import { createConfiguredProviderAdapter as createProviderAdapter } from './provider-adapters.js';
17
+ import { parseModelRef } from './models.js';
15
18
  import { imageCandidatesFromCatalog, providerForTaskFromCatalog, providerTaskBinding, transcribeCandidatesFromCatalog, ttsCandidatesFromCatalog, videoCandidatesFromCatalog } from './provider-catalog.js';
16
19
 
17
20
  let writerTail = Promise.resolve();
@@ -20,6 +23,10 @@ const DEFAULT_POLL_INTERVAL_MS = 2500;
20
23
  const DEFAULT_MAX_WATCH_MS = 20 * 60 * 1000;
21
24
  const POLL_ERROR_TOLERANCE = 5;
22
25
 
26
+ function createConfiguredProviderAdapter(provider, options = {}) {
27
+ return createProviderAdapter(provider, { ...options, onRateLimit: recordRateLimit });
28
+ }
29
+
23
30
  export class DshCoreAdapterError extends Error {
24
31
  constructor(code, message) {
25
32
  super(message);
@@ -99,6 +106,16 @@ export async function cropForDsh(input = {}) {
99
106
  }, input.signal);
100
107
  }
101
108
 
109
+ /** Browser 只提供截图字节;DSH/CLI 共用 HTML Command 和 Artifact 提交。 */
110
+ export function htmlForDsh(input = {}) {
111
+ return withWriter(async runtime => {
112
+ const commands = createCommandService(runtime, { browser: input.browser });
113
+ const result = await commands.execute('media.html', { html: input.html, width: input.width,
114
+ height: input.height, full_page: input.fullPage });
115
+ return runtime.run('inspect', ({ dataRoot }) => readCoreArtifactBytes(dataRoot, result.artifact.id));
116
+ }, input.signal);
117
+ }
118
+
102
119
  /** DSH 只把图片来源转换为 Command 输入,差异算法与 Artifact 提交由 Core 执行。 */
103
120
  export function diffForDsh(input = {}) {
104
121
  return withWriter(async (runtime, commands) => {
@@ -142,12 +159,23 @@ export function framesForDsh(input = {}) {
142
159
  }, input.signal);
143
160
  }
144
161
 
162
+ /** 已有抽帧 Artifact 直接进入共享摘要;reader 不取得写者租约。 */
163
+ export function summaryFramesForDsh(input = {}) {
164
+ return withReader(runtime => readSummaryFrames(runtime, input.frameArtifactIds, input.signal), input.signal);
165
+ }
166
+
167
+ /** 共享单图读取器用于 look/locate/OCR,Host 不解释 Core 对象路径。 */
168
+ export function visionImageForDsh(input = {}) {
169
+ return withReader(runtime => readVisionArtifact(runtime, input.artifactId, input.signal), input.signal);
170
+ }
171
+
145
172
  /** 提交一个 Core Provider Task;Provider Adapter 由 DSH 配置边界选择后显式传入。 */
146
173
  export function submitProviderTaskForDsh(input = {}) {
147
174
  return withWriter((runtime) => createProviderTaskRunner(runtime).submit({
148
175
  capability: input.capability,
149
176
  candidates: input.candidates,
150
- providerInput: input.providerInput
177
+ providerInput: input.providerInput,
178
+ sourceArtifactId: input.sourceArtifactId
151
179
  }), input.signal);
152
180
  }
153
181
 
@@ -272,7 +300,7 @@ export function retryProviderTaskForDsh(input = {}) {
272
300
  ? { text: input.text, ...(input.voice !== undefined ? { voice: input.voice } : {}) }
273
301
  : input.audio_url !== undefined
274
302
  ? { audio_url: input.audio_url }
275
- : { prompt: input.prompt },
303
+ : { prompt: input.prompt, ...(input.sourceArtifactId !== undefined ? { source_artifact_id: input.sourceArtifactId } : {}) },
276
304
  model_ref: input.modelRef,
277
305
  confirm_billing: input.confirmBilling
278
306
  });
@@ -304,7 +332,7 @@ function observableProviderTask(task) {
304
332
 
305
333
  function configuredAdapterForTask(task) {
306
334
  const provider = providerForTaskFromCatalog({ providers: allProviders() }, task);
307
- return createConfiguredProviderAdapter(provider);
335
+ return createConfiguredProviderAdapter(provider, { modelId: parseModelRef(task.modelRef)?.modelId });
308
336
  }
309
337
 
310
338
  function stopProviderWatch(taskId, expectedState) {
@@ -444,6 +472,30 @@ export function inspectProviderTaskForDsh(taskId, options = {}) {
444
472
  inspectCoreTask(dataRoot, taskId)), options.signal);
445
473
  }
446
474
 
475
+ /** 作品目录与任务 snapshot 分开查询,目录不加载媒体字节、不取得 writer 租约。 */
476
+ export function galleryArtifactsForDsh(options = {}) {
477
+ if (!fs.existsSync(dshCoreDataRoot())) return Promise.resolve({ artifacts: [], revision: 0, dropped: 0 });
478
+ return withReader((runtime) => runtime.run('inspect', ({ dataRoot }) =>
479
+ listCoreArtifactMetadata(dataRoot)), options.signal);
480
+ }
481
+
482
+ export function inspectWorkbenchArtifactForDsh(artifactId, options = {}) {
483
+ return withReader(runtime => runtime.run('inspect', ({ dataRoot }) =>
484
+ inspectCoreArtifact(dataRoot, artifactId)), options.signal);
485
+ }
486
+
487
+ /** 工作台管理沿用 CLI 的维护命令;预览/事务列表只读,执行使用既有串行 writer。 */
488
+ export function maintainWorkbenchForDsh(command, input, options = {}) {
489
+ if (!['core.delete', 'core.transactions', 'core.restore'].includes(command)) {
490
+ throw new DshCoreAdapterError('IRIS_COMMAND_INPUT_INVALID', '不支持的作品管理命令');
491
+ }
492
+ if (command === 'core.transactions' && !fs.existsSync(dshCoreDataRoot())) return Promise.resolve({ transactions: [] });
493
+ if (command === 'core.restore' || input.confirm_delete === true) {
494
+ return withWriter((_runtime, commands) => commands.execute(command, input), options.signal);
495
+ }
496
+ return withReader(runtime => createCommandService(runtime).execute(command, input), options.signal);
497
+ }
498
+
447
499
  /**
448
500
  * 工作台只读候选快照;数据根不存在时不创建任何目录。
449
501
  * userTasks 是用户任务区消费的五类状态安全投影(lib/core-user-projection.js),
@@ -533,8 +585,9 @@ export async function readCoreArtifactMediaForDsh(artifactId, options = {}) {
533
585
  const result = await withReader((runtime) => runtime.run('inspect', ({ dataRoot }) =>
534
586
  readCoreArtifactBytes(dataRoot, id)), options.signal);
535
587
  if (!result.artifact.mediaType.startsWith('image/') && result.artifact.mediaType !== 'video/mp4'
536
- && !result.artifact.mediaType.startsWith('audio/') && result.artifact.mediaType !== 'text/plain') {
537
- throw new DshCoreAdapterError('IRIS_DSH_ARTIFACT_UNSUPPORTED', '当前 DSH 媒体预览只支持图片、mp4 视频、音频与纯文本 Artifact');
588
+ && result.artifact.mediaType !== 'video/webm' && !result.artifact.mediaType.startsWith('audio/')
589
+ && result.artifact.mediaType !== 'text/plain' && result.artifact.mediaType !== 'application/json') {
590
+ throw new DshCoreAdapterError('IRIS_DSH_ARTIFACT_UNSUPPORTED', '当前 DSH 媒体预览不支持此 Artifact 类型');
538
591
  }
539
592
  return result;
540
593
  } catch (error) {
@@ -568,10 +621,11 @@ export async function projectCoreTaskForDsh(host, taskId, options = {}) {
568
621
  }
569
622
  let attachment;
570
623
  try {
624
+ const extension = item.artifact.mediaType === 'image/jpeg' ? 'jpg' : item.artifact.mediaType.slice(6);
571
625
  attachment = await attachments.saveImage({
572
626
  data: new Uint8Array(item.bytes),
573
627
  mediaType: item.artifact.mediaType,
574
- name: 'iris-core-' + item.artifact.id + '.png'
628
+ name: 'iris-core-' + item.artifact.id + '.' + extension
575
629
  });
576
630
  } catch (_) {
577
631
  throw new DshCoreAdapterError('IRIS_DSH_PROJECTION_FAILED', 'Core 产物无法保存为 DSH attachment');