@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.
- package/.dsh/skills/iris-compose-media/SKILL.md +10 -5
- package/.dsh/skills/iris-compose-media/references/workflows.md +7 -1
- package/.dsh/skills/iris-verify-ui/SKILL.md +2 -1
- package/CHANGELOG.md +47 -2
- package/README.en.md +65 -29
- package/README.md +65 -29
- package/bin/dsh-iris.js +222 -21
- package/docs/AGENT_QUICKSTART.md +10 -6
- package/docs/ARCHITECTURE.md +25 -18
- package/docs/ARTIFACT_LIBRARY.md +5 -5
- package/docs/ARTIFACT_MANIFEST.md +1 -1
- package/docs/CLI_MANAGEMENT.md +181 -0
- package/docs/COMPOSITE_VISION.md +35 -0
- package/docs/CORE_RUNTIME_CONTRACT.md +8 -7
- package/docs/DSH_CORE_MIGRATION.md +48 -19
- package/docs/DSH_RC2_ADAPTATION.md +4 -2
- package/docs/FAULT_INJECTION.md +12 -2
- package/docs/HEADLESS_CLI.md +13 -7
- package/docs/HOST_ADAPTER_CONTRACT.md +10 -10
- package/docs/HOST_DOCTOR.md +3 -3
- package/docs/MODEL_PORT_CONTRACT.md +19 -14
- package/docs/OCR_MODEL.md +39 -0
- package/docs/PROMPT_OPTIMIZER.md +47 -0
- package/docs/PROVIDER_ADAPTER_CONTRACT.md +6 -4
- package/docs/PROVIDER_HEALTH.md +22 -5
- package/docs/PROVIDER_RUNTIME_HARNESS.md +1 -1
- package/docs/PROVIDER_SUBMISSION_CONTRACT.md +2 -2
- package/docs/ROADMAP.md +29 -19
- package/docs/SECURITY.md +5 -5
- package/docs/TASK_SEMANTICS.md +2 -2
- package/docs/VISION_CLI.md +132 -0
- package/docs/VISION_MODEL.md +51 -0
- package/docs/WORKBENCH_ARTIFACTS.md +67 -0
- package/docs/file-access-across-environments.md +8 -5
- package/docs/releases/0.2.1.md +56 -0
- package/lib/actions.js +187 -270
- package/lib/adapters.js +133 -22
- package/lib/api.js +49 -7
- package/lib/chromium-browser.js +117 -0
- package/lib/client.js +671 -101
- package/lib/command-service.js +121 -7
- package/lib/composite-vision-routing.js +42 -0
- package/lib/config.js +89 -10
- package/lib/core-artifact-store.js +45 -13
- package/lib/core-maintenance.js +213 -0
- package/lib/core-tasks.js +12 -5
- package/lib/core-user-projection.js +1 -0
- package/lib/dsh-core-adapter.js +63 -9
- package/lib/dsh-text-model-adapter.js +222 -0
- package/lib/dsh-vision-model-adapter.js +75 -0
- package/lib/generation-input.js +36 -5
- package/lib/headless-vision.js +181 -0
- package/lib/host-runtime.js +1 -0
- package/lib/http-vision-model-adapter.js +187 -0
- package/lib/image-edit-input.js +34 -0
- package/lib/index.js +205 -175
- package/lib/locate.js +86 -28
- package/lib/media-probe.js +2 -1
- package/lib/model-call-runtime.js +45 -0
- package/lib/model-invoker.js +4 -2
- package/lib/model-port-contract.js +13 -5
- package/lib/models.js +10 -5
- package/lib/ocr-model-routing.js +26 -0
- package/lib/ocr.js +110 -64
- package/lib/prompt-optimizer-config.js +2 -1
- package/lib/prompt-optimizer-core.js +121 -0
- package/lib/prompt-optimizer.js +37 -193
- package/lib/provider-adapters.js +85 -9
- package/lib/provider-catalog.js +19 -7
- package/lib/provider-config-service.js +236 -0
- package/lib/provider-contract.js +3 -2
- package/lib/provider-health.js +80 -1
- package/lib/provider-protocol.js +23 -0
- package/lib/provider-task-runner.js +58 -16
- package/lib/summarize.js +31 -20
- package/lib/summary-input.js +52 -0
- package/lib/task-wait.js +40 -0
- package/lib/video-input.js +30 -0
- package/lib/vision-core.js +42 -0
- package/lib/vision-image-input.js +83 -0
- package/lib/vision-input.js +31 -0
- package/lib/vision-model-routing.js +180 -0
- package/lib/vision.js +3 -2
- package/lib/workbench-artifacts.js +52 -0
- package/lib/workbench-management.js +151 -0
- package/lib/workbench-zip.js +39 -0
- package/package.json +1 -1
- 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:
|
|
241
|
-
tasks: Object.freeze(
|
|
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),
|
package/lib/dsh-core-adapter.js
CHANGED
|
@@ -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
|
-
&&
|
|
537
|
-
|
|
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 + '.
|
|
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');
|