@mokuyoaxis/dsh-iris 0.1.3 → 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.
- package/.dsh/skills/iris-compose-media/SKILL.md +23 -49
- package/.dsh/skills/iris-compose-media/references/task-v2-recovery.md +30 -0
- package/.dsh/skills/iris-compose-media/references/workflows.md +38 -0
- package/.dsh/skills/iris-verify-ui/SKILL.md +3 -25
- package/.dsh/skills/iris-verify-ui/agents/openai.yaml +4 -0
- package/.dsh/skills/iris-verify-ui/references/verdict-and-degradation.md +31 -0
- package/CHANGELOG.md +77 -2
- package/README.en.md +98 -19
- package/README.md +98 -19
- package/bin/dsh-iris.js +392 -13
- package/docs/AGENT_QUICKSTART.md +35 -0
- package/docs/ARCHITECTURE.md +82 -52
- package/docs/ARTIFACT_LIBRARY.md +24 -0
- package/docs/ARTIFACT_MANIFEST.md +60 -0
- package/docs/CORE_RUNTIME_CONTRACT.md +83 -0
- package/docs/DSH_CORE_MIGRATION.md +106 -0
- package/docs/DSH_RC2_ADAPTATION.md +36 -0
- package/docs/HEADLESS_CLI.md +217 -0
- package/docs/HOST_ADAPTER_CONTRACT.md +105 -0
- package/docs/HOST_DOCTOR.md +49 -0
- package/docs/MODEL_PORT_CONTRACT.md +300 -0
- package/docs/PROVIDER_ADAPTER_CONTRACT.md +138 -0
- package/docs/PROVIDER_HEALTH.md +39 -0
- package/docs/PROVIDER_RUNTIME_HARNESS.md +33 -0
- package/docs/PROVIDER_SUBMISSION_CONTRACT.md +2 -2
- package/docs/ROADMAP.md +42 -76
- package/docs/SECURITY.md +10 -5
- package/docs/TASK_SEMANTICS.md +3 -2
- package/docs/assets/diagrams/iris-architecture.png +0 -0
- package/docs/assets/diagrams/iris-architecture.svg +5 -5
- package/docs/assets/logo/README.md +55 -0
- package/docs/assets/logo/iris-bubble-concept.svg +67 -0
- package/docs/assets/logo/iris-bubble.svg +14 -0
- package/docs/assets/logo/iris-interim-flower.png +0 -0
- package/docs/assets/logo/iris-logo-mono-inverse.svg +78 -0
- package/docs/assets/logo/iris-logo-mono.svg +78 -0
- package/docs/assets/logo/iris-logo.svg +78 -0
- package/docs/assets/logo/iris-mark-mono-inverse.svg +42 -0
- package/docs/assets/logo/iris-mark-mono.svg +42 -0
- package/docs/assets/logo/iris-mark.svg +42 -0
- package/docs/releases/0.1.4.md +59 -0
- package/docs/releases/0.2.0.md +48 -0
- package/lib/actions.js +673 -525
- package/lib/adapters.js +58 -21
- package/lib/api.js +182 -16
- package/lib/artifacts.js +239 -0
- package/lib/bundled-skills.js +5 -5
- package/lib/client.js +1116 -86
- package/lib/command-service.js +420 -0
- package/lib/config.js +245 -49
- package/lib/core-artifact-store.js +598 -0
- package/lib/core-artifacts.js +4 -0
- package/lib/core-attention.js +103 -0
- package/lib/core-contract.js +105 -0
- package/lib/core-doctor.js +41 -0
- package/lib/core-lease-recovery.js +170 -0
- package/lib/core-runtime.js +232 -0
- package/lib/core-tasks.js +560 -0
- package/lib/core-user-projection.js +192 -0
- package/lib/doctor.js +260 -30
- package/lib/dsh-core-adapter.js +599 -0
- package/lib/dsh-host-adapter.js +240 -0
- package/lib/generation-input.js +95 -0
- package/lib/guard.js +3 -3
- package/lib/host-contract.js +131 -0
- package/lib/host-runtime.js +156 -0
- package/lib/index.js +255 -166
- package/lib/locate.js +1 -1
- package/lib/media.js +16 -6
- package/lib/model-invoker.js +218 -0
- package/lib/model-port-contract.js +333 -0
- package/lib/models.js +97 -11
- package/lib/prompt-optimizer.js +10 -20
- package/lib/provider-adapter.js +397 -0
- package/lib/provider-adapters.js +342 -0
- package/lib/provider-catalog.js +171 -0
- package/lib/provider-contract.js +72 -3
- package/lib/provider-health.js +59 -0
- package/lib/provider-protocol.js +51 -2
- package/lib/provider-task-runner.js +328 -0
- package/lib/render.js +43 -12
- package/lib/tasks.js +45 -9
- package/lib/vision.js +51 -56
- package/package.json +20 -14
- package/user_guide.md +67 -12
- package/docs/assets/diagrams/iris-architecture.drawio +0 -1
- package/docs/assets/diagrams/iris-task-lifecycle.drawio +0 -1
- package/docs/assets/diagrams/iris-workflow-compose.drawio +0 -1
- package/docs/releases/0.1.1.md +0 -51
- package/docs/releases/0.1.2.md +0 -26
- package/docs/releases/0.1.3.md +0 -30
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { allProviders, irisHome } from './config.js';
|
|
6
|
+
import { createCommandService } from './command-service.js';
|
|
7
|
+
import { createCoreArtifact, inspectCoreArtifact, listCoreArtifacts, readCoreArtifactBytes } from './core-artifacts.js';
|
|
8
|
+
import { createCoreRuntime } from './core-runtime.js';
|
|
9
|
+
import { inspectCoreTask, listCoreTasks, recoverCoreTask } from './core-tasks.js';
|
|
10
|
+
import { applyCoreAttentionAction, attentionDispositionOf, readCoreAttentionPrefs } from './core-attention.js';
|
|
11
|
+
import { projectCoreTaskUserRow } from './core-user-projection.js';
|
|
12
|
+
import { requireHostPort } from './host-contract.js';
|
|
13
|
+
import { createProviderTaskRunner } from './provider-task-runner.js';
|
|
14
|
+
import { createConfiguredProviderAdapter } from './provider-adapters.js';
|
|
15
|
+
import { imageCandidatesFromCatalog, providerForTaskFromCatalog, providerTaskBinding, transcribeCandidatesFromCatalog, ttsCandidatesFromCatalog, videoCandidatesFromCatalog } from './provider-catalog.js';
|
|
16
|
+
|
|
17
|
+
let writerTail = Promise.resolve();
|
|
18
|
+
const providerWatchers = new Map();
|
|
19
|
+
const DEFAULT_POLL_INTERVAL_MS = 2500;
|
|
20
|
+
const DEFAULT_MAX_WATCH_MS = 20 * 60 * 1000;
|
|
21
|
+
const POLL_ERROR_TOLERANCE = 5;
|
|
22
|
+
|
|
23
|
+
export class DshCoreAdapterError extends Error {
|
|
24
|
+
constructor(code, message) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = 'DshCoreAdapterError';
|
|
27
|
+
this.code = code;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function abortError() {
|
|
32
|
+
return Object.assign(new Error('已取消'), { name: 'AbortError' });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** DSH 只在这个边界选择 profile 数据根;Core 本身仍不推断 DSH_HOME。 */
|
|
36
|
+
export function dshCoreDataRoot() {
|
|
37
|
+
return path.join(irisHome(), 'core-v0');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function withRuntime(mode, callback, signal) {
|
|
41
|
+
if (signal?.aborted) throw abortError();
|
|
42
|
+
const runtime = createCoreRuntime({ dataRoot: dshCoreDataRoot(), mode });
|
|
43
|
+
runtime.start();
|
|
44
|
+
const onAbort = () => { runtime.dispose().catch(() => {}); };
|
|
45
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
46
|
+
try { return await callback(runtime, mode === 'writer' ? createCommandService(runtime) : null); }
|
|
47
|
+
finally {
|
|
48
|
+
signal?.removeEventListener('abort', onAbort);
|
|
49
|
+
await runtime.dispose();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function withWriter(callback, signal) {
|
|
54
|
+
const operation = writerTail.then(() => withRuntime('writer', callback, signal));
|
|
55
|
+
writerTail = operation.catch(() => {});
|
|
56
|
+
return operation;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function withReader(callback, signal) {
|
|
60
|
+
return withRuntime('reader', callback, signal);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 将 DSH 图片输入投影到共享 crop Command,再把中性 Artifact 读成宿主可保存的字节。
|
|
65
|
+
* bytes 输入先成为 host-input Artifact,输出通过 derived-from 保留关系。
|
|
66
|
+
*/
|
|
67
|
+
export async function cropForDsh(input = {}) {
|
|
68
|
+
return withWriter(async (runtime, commands) => {
|
|
69
|
+
if (input.signal?.aborted) throw Object.assign(new Error('已取消'), { name: 'AbortError' });
|
|
70
|
+
let commandInput;
|
|
71
|
+
if (input.bytes !== undefined) {
|
|
72
|
+
const source = await runtime.run('execute', ({ dataRoot }) => createCoreArtifact(dataRoot, {
|
|
73
|
+
bytes: input.bytes,
|
|
74
|
+
mediaType: input.mediaType,
|
|
75
|
+
kind: 'host-input',
|
|
76
|
+
metadata: {}
|
|
77
|
+
}));
|
|
78
|
+
commandInput = { artifact_id: source.id };
|
|
79
|
+
} else {
|
|
80
|
+
commandInput = { image_path: input.imagePath };
|
|
81
|
+
}
|
|
82
|
+
Object.assign(commandInput, {
|
|
83
|
+
left: input.left,
|
|
84
|
+
top: input.top,
|
|
85
|
+
width: input.width,
|
|
86
|
+
height: input.height
|
|
87
|
+
});
|
|
88
|
+
const result = await commands.execute('crop', commandInput);
|
|
89
|
+
if (input.signal?.aborted) throw Object.assign(new Error('已取消'), { name: 'AbortError' });
|
|
90
|
+
const materialized = await runtime.run('inspect', ({ dataRoot }) =>
|
|
91
|
+
readCoreArtifactBytes(dataRoot, result.artifact.id));
|
|
92
|
+
return Object.freeze({
|
|
93
|
+
artifact: result.artifact,
|
|
94
|
+
bytes: materialized.bytes,
|
|
95
|
+
mediaType: result.artifact.mediaType,
|
|
96
|
+
width: result.artifact.metadata.width,
|
|
97
|
+
height: result.artifact.metadata.height
|
|
98
|
+
});
|
|
99
|
+
}, input.signal);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** DSH 只把图片来源转换为 Command 输入,差异算法与 Artifact 提交由 Core 执行。 */
|
|
103
|
+
export function diffForDsh(input = {}) {
|
|
104
|
+
return withWriter(async (runtime, commands) => {
|
|
105
|
+
const commandInput = { grid: input.grid, top_regions: input.topRegions };
|
|
106
|
+
for (const [side, source] of [['a', input.imageA], ['b', input.imageB]]) {
|
|
107
|
+
if (input.signal?.aborted) throw abortError();
|
|
108
|
+
if (source?.bytes !== undefined) {
|
|
109
|
+
const artifact = await runtime.run('execute', ({ dataRoot }) => createCoreArtifact(dataRoot, {
|
|
110
|
+
bytes: source.bytes, mediaType: source.mediaType, kind: 'host-input', metadata: {}
|
|
111
|
+
}));
|
|
112
|
+
commandInput['image_' + side + '_artifact_id'] = artifact.id;
|
|
113
|
+
} else {
|
|
114
|
+
commandInput['image_' + side + '_path'] = source?.imagePath;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const result = await commands.execute('media.diff', commandInput);
|
|
118
|
+
const materialized = await runtime.run('inspect', ({ dataRoot }) =>
|
|
119
|
+
readCoreArtifactBytes(dataRoot, result.artifact.id));
|
|
120
|
+
if (input.signal?.aborted) throw abortError();
|
|
121
|
+
return Object.freeze({ ...materialized, mediaType: result.artifact.mediaType, metrics: result.metrics });
|
|
122
|
+
}, input.signal);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** 抽帧结果先落 Core Artifact,再只读转换为 DSH 可保存的附件字节。 */
|
|
126
|
+
export function framesForDsh(input = {}) {
|
|
127
|
+
return withWriter(async (runtime, commands) => {
|
|
128
|
+
const result = await commands.execute('media.frames', {
|
|
129
|
+
video_path: input.videoPath,
|
|
130
|
+
max_frames: input.maxFrames,
|
|
131
|
+
target_width: input.targetWidth,
|
|
132
|
+
format: input.format,
|
|
133
|
+
quality: input.quality
|
|
134
|
+
});
|
|
135
|
+
const frames = await runtime.run('inspect', ({ dataRoot }) => result.artifacts.map((artifact) => ({
|
|
136
|
+
...readCoreArtifactBytes(dataRoot, artifact.id),
|
|
137
|
+
mediaType: artifact.mediaType,
|
|
138
|
+
...artifact.metadata
|
|
139
|
+
})));
|
|
140
|
+
if (input.signal?.aborted) throw abortError();
|
|
141
|
+
return Object.freeze({ media: result.media, frames: Object.freeze(frames.map(Object.freeze)) });
|
|
142
|
+
}, input.signal);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** 提交一个 Core Provider Task;Provider Adapter 由 DSH 配置边界选择后显式传入。 */
|
|
146
|
+
export function submitProviderTaskForDsh(input = {}) {
|
|
147
|
+
return withWriter((runtime) => createProviderTaskRunner(runtime).submit({
|
|
148
|
+
capability: input.capability,
|
|
149
|
+
candidates: input.candidates,
|
|
150
|
+
providerInput: input.providerInput
|
|
151
|
+
}), input.signal);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** 单步观察已经受理的 Core Task;不会自动循环,也不会创建第二次 submit。 */
|
|
155
|
+
export function observeProviderTaskForDsh(input = {}) {
|
|
156
|
+
return withWriter((runtime) => createProviderTaskRunner(runtime).observe(
|
|
157
|
+
input.taskId, input.adapter, { resumeActive: input.resumeActive === true }
|
|
158
|
+
), input.signal);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* D1/D2 人工动作公共通路:与 Host 观察节拍共用同一个 resolver / binding 校验
|
|
163
|
+
* (configuredAdapterForTask),经 Command Service 显式执行一次人工命令;合法性
|
|
164
|
+
* 门在 Command 内于网络与写入之前完成。返回任务事实对应的五类用户投影行
|
|
165
|
+
* (无 providerId/binding/错误原文/路径)。
|
|
166
|
+
*/
|
|
167
|
+
function manualTaskCommandForDsh(command, input = {}) {
|
|
168
|
+
const taskId = String(input.taskId || '').trim();
|
|
169
|
+
if (!/^task_[a-f0-9]{24}$/.test(taskId)) {
|
|
170
|
+
return Promise.reject(new DshCoreAdapterError('IRIS_DSH_TASK_INVALID', 'Core Task ID 无效'));
|
|
171
|
+
}
|
|
172
|
+
return withWriter(async (runtime) => {
|
|
173
|
+
const commands = createCommandService(runtime, { resolveTaskAdapter: configuredAdapterForTask });
|
|
174
|
+
const result = await commands.execute(command, { task_id: taskId });
|
|
175
|
+
return runtime.run('inspect', ({ dataRoot }) => {
|
|
176
|
+
const artifacts = [];
|
|
177
|
+
for (const artifactId of result.task.artifactIds) {
|
|
178
|
+
try { artifacts.push(inspectCoreArtifact(dataRoot, artifactId)); }
|
|
179
|
+
catch (_) { /* 缺失媒体在投影行里降级为不可用 */ }
|
|
180
|
+
}
|
|
181
|
+
return Object.freeze({
|
|
182
|
+
taskId: result.task.id,
|
|
183
|
+
row: projectCoreTaskUserRow(result.task, artifacts)
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
}, input.signal);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** D1 人工 reobserve:显式单步 poll 一次,绝不 submit。 */
|
|
190
|
+
export function reobserveProviderTaskForDsh(input = {}) {
|
|
191
|
+
return manualTaskCommandForDsh('task.reobserve', input);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* D2 人工 redeliver:只对 outcome=succeeded / deliveryState=failed 的 Task
|
|
196
|
+
* 重新取回远端产物(re-poll 产物清单 → download → 新 Artifact),绝不 submit、
|
|
197
|
+
* 绝不重新生成。
|
|
198
|
+
*/
|
|
199
|
+
export function redeliverProviderTaskForDsh(input = {}) {
|
|
200
|
+
return manualTaskCommandForDsh('task.redeliver', input);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* D3 人工 cancel:只有 Provider 明确确认才写 outcome=canceled;不支持或无法确认
|
|
205
|
+
* 时保持真实状态(详见 core-tasks 的 recordCoreCancelResult 三分支)。
|
|
206
|
+
*/
|
|
207
|
+
export function cancelProviderTaskForDsh(input = {}) {
|
|
208
|
+
return manualTaskCommandForDsh('task.cancel', input);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 注意力处置(Host 偏好,零 Core 写入):acknowledge/restore/hide/unhide。
|
|
213
|
+
* 只校验任务存在;动作幂等(偏好不变时不写盘);返回装饰后的投影行。
|
|
214
|
+
*/
|
|
215
|
+
export async function disposeCoreTaskAttention(input = {}) {
|
|
216
|
+
const taskId = String(input.taskId || '').trim();
|
|
217
|
+
const action = String(input.action || '').trim();
|
|
218
|
+
if (!/^task_[a-f0-9]{24}$/.test(taskId)) {
|
|
219
|
+
throw new DshCoreAdapterError('IRIS_DSH_TASK_INVALID', 'Core Task ID 无效');
|
|
220
|
+
}
|
|
221
|
+
// 任务必须存在(reader 只读);不存在统一 404。
|
|
222
|
+
const task = await inspectProviderTaskForDsh(taskId, { signal: input.signal });
|
|
223
|
+
const applied = applyCoreAttentionAction(task.id, action);
|
|
224
|
+
const row = await withReader((runtime) => runtime.run('inspect', ({ dataRoot }) => {
|
|
225
|
+
const artifacts = [];
|
|
226
|
+
for (const artifactId of task.artifactIds) {
|
|
227
|
+
try { artifacts.push(inspectCoreArtifact(dataRoot, artifactId)); }
|
|
228
|
+
catch (_) { /* 缺失媒体在投影行里降级为不可用 */ }
|
|
229
|
+
}
|
|
230
|
+
return decorateCoreUserRows(
|
|
231
|
+
[projectCoreTaskUserRow(task, artifacts)],
|
|
232
|
+
listAllCoreTaskRecords(dataRoot),
|
|
233
|
+
readCoreAttentionPrefs()
|
|
234
|
+
)[0];
|
|
235
|
+
}), input.signal);
|
|
236
|
+
return Object.freeze({ taskId: task.id, changed: applied.changed, row });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* D4 人工 retry as new task:产生新的真实计费。候选链按当前 assignments/池实况
|
|
241
|
+
* 重新解析(不绑定旧 Provider/binding——旧配置可能正是失败原因),modelRef 可
|
|
242
|
+
* 显式指定。prompt 必须由调用方重新提供(Core 记录不持久化 Prompt)。
|
|
243
|
+
*/
|
|
244
|
+
export function retryProviderTaskForDsh(input = {}) {
|
|
245
|
+
const taskId = String(input.taskId || '').trim();
|
|
246
|
+
if (!/^task_[a-f0-9]{24}$/.test(taskId)) {
|
|
247
|
+
return Promise.reject(new DshCoreAdapterError('IRIS_DSH_TASK_INVALID', 'Core Task ID 无效'));
|
|
248
|
+
}
|
|
249
|
+
return withWriter(async (runtime) => {
|
|
250
|
+
const commands = createCommandService(runtime, {
|
|
251
|
+
resolveTaskCandidates: ({ capability, modelRef }) => {
|
|
252
|
+
const catalog = { providers: allProviders() };
|
|
253
|
+
const ref = modelRef || undefined;
|
|
254
|
+
const candidates = capability === 'video'
|
|
255
|
+
? videoCandidatesFromCatalog(catalog, ref)
|
|
256
|
+
: capability === 'tts'
|
|
257
|
+
? ttsCandidatesFromCatalog(catalog, ref)
|
|
258
|
+
: capability === 'transcribe'
|
|
259
|
+
? transcribeCandidatesFromCatalog(catalog, ref)
|
|
260
|
+
: imageCandidatesFromCatalog(catalog, ref);
|
|
261
|
+
return candidates.map((route) => ({
|
|
262
|
+
adapter: createConfiguredProviderAdapter(route.provider),
|
|
263
|
+
model: route.modelRef,
|
|
264
|
+
selectionReason: route.selectionReason,
|
|
265
|
+
providerBinding: providerTaskBinding(route.provider)
|
|
266
|
+
}));
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
const result = await commands.execute('task.retry', {
|
|
270
|
+
task_id: taskId,
|
|
271
|
+
provider_input: input.text !== undefined
|
|
272
|
+
? { text: input.text, ...(input.voice !== undefined ? { voice: input.voice } : {}) }
|
|
273
|
+
: input.audio_url !== undefined
|
|
274
|
+
? { audio_url: input.audio_url }
|
|
275
|
+
: { prompt: input.prompt },
|
|
276
|
+
model_ref: input.modelRef,
|
|
277
|
+
confirm_billing: input.confirmBilling
|
|
278
|
+
});
|
|
279
|
+
return runtime.run('inspect', ({ dataRoot }) => {
|
|
280
|
+
const artifacts = [];
|
|
281
|
+
for (const artifactId of result.task.artifactIds) {
|
|
282
|
+
try { artifacts.push(inspectCoreArtifact(dataRoot, artifactId)); }
|
|
283
|
+
catch (_) { /* 缺失媒体在投影行里降级为不可用 */ }
|
|
284
|
+
}
|
|
285
|
+
return Object.freeze({
|
|
286
|
+
taskId: result.task.id,
|
|
287
|
+
retriedFrom: result.retriedFrom,
|
|
288
|
+
row: projectCoreTaskUserRow(result.task, artifacts)
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
}, input.signal);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** DSH Host 观察接管的媒体能力白名单:图片、视频与转写共用 Task/Attempt 事实轴。 */
|
|
295
|
+
const WATCHABLE_CAPABILITIES = new Set(['image', 'video', 'transcribe']);
|
|
296
|
+
const WATCH_INTERVAL_BY_CAPABILITY = Object.freeze({ image: DEFAULT_POLL_INTERVAL_MS, video: 6000, transcribe: 2500 });
|
|
297
|
+
|
|
298
|
+
function observableProviderTask(task) {
|
|
299
|
+
return Boolean(task && WATCHABLE_CAPABILITIES.has(task.capability)
|
|
300
|
+
&& task.acceptance === 'accepted' && task.remoteTaskId
|
|
301
|
+
&& ['none', 'unknown'].includes(task.outcome)
|
|
302
|
+
&& task.phase !== 'terminal');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function configuredAdapterForTask(task) {
|
|
306
|
+
const provider = providerForTaskFromCatalog({ providers: allProviders() }, task);
|
|
307
|
+
return createConfiguredProviderAdapter(provider);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function stopProviderWatch(taskId, expectedState) {
|
|
311
|
+
const current = providerWatchers.get(taskId);
|
|
312
|
+
if (expectedState && current !== expectedState) return;
|
|
313
|
+
if (current?.timer) clearTimeout(current.timer);
|
|
314
|
+
providerWatchers.delete(taskId);
|
|
315
|
+
current?.controller.abort();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* DSH Host 拥有的有界 Core 观察器。每个 tick 重新读取 Task 与配置,最多执行
|
|
320
|
+
* 一次 poll;任何路径都不会调用 submit。Runtime/Writer 只在单次观察期间存活。
|
|
321
|
+
*/
|
|
322
|
+
export async function watchProviderTaskForDsh(input = {}) {
|
|
323
|
+
const taskId = String(input.taskId || '').trim();
|
|
324
|
+
if (!/^task_[a-f0-9]{24}$/.test(taskId)) {
|
|
325
|
+
throw new DshCoreAdapterError('IRIS_DSH_TASK_INVALID', 'Core Task ID 无效');
|
|
326
|
+
}
|
|
327
|
+
if (providerWatchers.has(taskId)) return false;
|
|
328
|
+
const initial = await inspectProviderTaskForDsh(taskId, { signal: input.signal });
|
|
329
|
+
if (!observableProviderTask(initial) || providerWatchers.has(taskId)) return false;
|
|
330
|
+
configuredAdapterForTask(initial); // binding/模型漂移在定时器和网络前失败
|
|
331
|
+
|
|
332
|
+
const state = {
|
|
333
|
+
timer: null,
|
|
334
|
+
controller: new AbortController(),
|
|
335
|
+
startedAt: Date.now(),
|
|
336
|
+
errors: 0,
|
|
337
|
+
intervalMs: Math.max(5, Number(input.intervalMs)
|
|
338
|
+
|| WATCH_INTERVAL_BY_CAPABILITY[initial.capability] || DEFAULT_POLL_INTERVAL_MS),
|
|
339
|
+
maxWatchMs: Math.max(50, Number(input.maxWatchMs) || DEFAULT_MAX_WATCH_MS)
|
|
340
|
+
};
|
|
341
|
+
providerWatchers.set(taskId, state);
|
|
342
|
+
|
|
343
|
+
const schedule = (delay = state.intervalMs) => {
|
|
344
|
+
if (providerWatchers.get(taskId) !== state) return;
|
|
345
|
+
state.timer = setTimeout(() => { tick().catch(() => stopProviderWatch(taskId, state)); }, delay);
|
|
346
|
+
if (state.timer.unref) state.timer.unref();
|
|
347
|
+
};
|
|
348
|
+
const tick = async () => {
|
|
349
|
+
if (providerWatchers.get(taskId) !== state) return;
|
|
350
|
+
const before = await inspectProviderTaskForDsh(taskId, { signal: state.controller.signal });
|
|
351
|
+
if (providerWatchers.get(taskId) !== state) return;
|
|
352
|
+
if (!observableProviderTask(before)) return stopProviderWatch(taskId, state);
|
|
353
|
+
if (Date.now() - state.startedAt > state.maxWatchMs) {
|
|
354
|
+
console.warn('[iris] Core Task 自动观察已达到本轮时限,保留远端受理事实:' + taskId);
|
|
355
|
+
return stopProviderWatch(taskId, state);
|
|
356
|
+
}
|
|
357
|
+
let adapter;
|
|
358
|
+
try {
|
|
359
|
+
adapter = configuredAdapterForTask(before);
|
|
360
|
+
} catch (error) {
|
|
361
|
+
console.warn('[iris] Core Task 无法按原 Provider binding 恢复观察:' + taskId
|
|
362
|
+
+ ' · ' + String(error?.code || error?.message || error));
|
|
363
|
+
return stopProviderWatch(taskId, state);
|
|
364
|
+
}
|
|
365
|
+
let after;
|
|
366
|
+
try {
|
|
367
|
+
after = await observeProviderTaskForDsh({
|
|
368
|
+
taskId, adapter, resumeActive: true, signal: state.controller.signal
|
|
369
|
+
});
|
|
370
|
+
} catch (error) {
|
|
371
|
+
if (error?.code === 'IRIS_CORE_DATA_ROOT_BUSY') return schedule();
|
|
372
|
+
state.errors += 1;
|
|
373
|
+
if (state.errors >= POLL_ERROR_TOLERANCE) {
|
|
374
|
+
console.warn('[iris] Core Task 自动观察连续失败,保留事实供人工继续:' + taskId);
|
|
375
|
+
return stopProviderWatch(taskId, state);
|
|
376
|
+
}
|
|
377
|
+
return schedule();
|
|
378
|
+
}
|
|
379
|
+
if (providerWatchers.get(taskId) !== state) return;
|
|
380
|
+
if (!observableProviderTask(after)) return stopProviderWatch(taskId, state);
|
|
381
|
+
if (after.outcome === 'unknown') state.errors += 1;
|
|
382
|
+
else state.errors = 0;
|
|
383
|
+
if (state.errors >= POLL_ERROR_TOLERANCE) {
|
|
384
|
+
console.warn('[iris] Core Task Provider 观察连续失败,保留事实供人工继续:' + taskId);
|
|
385
|
+
return stopProviderWatch(taskId, state);
|
|
386
|
+
}
|
|
387
|
+
schedule();
|
|
388
|
+
};
|
|
389
|
+
schedule(Math.min(500, state.intervalMs));
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** DSH 启动时只接管仍有明确远端受理证据的 Core 图片 Task。 */
|
|
394
|
+
export async function resumeProviderTaskWatchesForDsh(options = {}) {
|
|
395
|
+
if (!fs.existsSync(dshCoreDataRoot())) return Object.freeze([]);
|
|
396
|
+
const records = await withReader((runtime) => runtime.run('inspect', ({ dataRoot }) => {
|
|
397
|
+
const all = [];
|
|
398
|
+
let page;
|
|
399
|
+
do {
|
|
400
|
+
page = listCoreTasks(dataRoot, { offset: all.length, limit: 200 });
|
|
401
|
+
all.push(...page.tasks);
|
|
402
|
+
} while (all.length < page.total);
|
|
403
|
+
return all;
|
|
404
|
+
}));
|
|
405
|
+
const resumed = [];
|
|
406
|
+
for (const original of records) {
|
|
407
|
+
if (!WATCHABLE_CAPABILITIES.has(original.capability)) continue;
|
|
408
|
+
try {
|
|
409
|
+
if (observableProviderTask(original)) configuredAdapterForTask(original);
|
|
410
|
+
const needsRecovery = (original.phase === 'submitting' && original.acceptance === 'none')
|
|
411
|
+
|| original.cancelState === 'requested'
|
|
412
|
+
|| (original.outcome === 'succeeded' && ['pending', 'downloading'].includes(original.deliveryState))
|
|
413
|
+
|| (original.acceptance === 'accepted' && original.watchState === 'active');
|
|
414
|
+
const task = needsRecovery
|
|
415
|
+
? await withWriter((runtime) => runtime.run('recover', ({ dataRoot }) => recoverCoreTask(dataRoot, original.id)))
|
|
416
|
+
: original;
|
|
417
|
+
if (!observableProviderTask(task)) continue;
|
|
418
|
+
const createdAt = Date.parse(task.createdAt);
|
|
419
|
+
if (!Number.isFinite(createdAt) || Date.now() - createdAt > (options.maxWatchMs || DEFAULT_MAX_WATCH_MS)) {
|
|
420
|
+
console.warn('[iris] Core Task 超过自动接管窗口,保留事实供显式观察:' + task.id);
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
if (await watchProviderTaskForDsh({
|
|
424
|
+
taskId: task.id,
|
|
425
|
+
intervalMs: options.intervalMs,
|
|
426
|
+
maxWatchMs: options.maxWatchMs
|
|
427
|
+
})) resumed.push(task.id);
|
|
428
|
+
} catch (error) {
|
|
429
|
+
console.warn('[iris] Core Task 启动接管失败:' + original.id
|
|
430
|
+
+ ' · ' + String(error?.code || error?.message || error));
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return Object.freeze(resumed);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** 插件 Fiber 释放时停止 Host 定时器并中止在途观察;不请求远端取消或撤销既有受理/成功事实。 */
|
|
437
|
+
export function stopProviderTaskWatchesForDsh() {
|
|
438
|
+
for (const taskId of [...providerWatchers.keys()]) stopProviderWatch(taskId);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** 读取 Core Task 事实;reader 不初始化、修复或更新记录。 */
|
|
442
|
+
export function inspectProviderTaskForDsh(taskId, options = {}) {
|
|
443
|
+
return withReader((runtime) => runtime.run('inspect', ({ dataRoot }) =>
|
|
444
|
+
inspectCoreTask(dataRoot, taskId)), options.signal);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* 工作台只读候选快照;数据根不存在时不创建任何目录。
|
|
449
|
+
* userTasks 是用户任务区消费的五类状态安全投影(lib/core-user-projection.js),
|
|
450
|
+
* 单条损坏记录或媒体缺失只触发局部降级(degraded/droppedTasks),快照整体仍可加载。
|
|
451
|
+
*/
|
|
452
|
+
export function coreSnapshotForDsh(options = {}) {
|
|
453
|
+
if (!fs.existsSync(dshCoreDataRoot())) {
|
|
454
|
+
return Promise.resolve(Object.freeze({
|
|
455
|
+
schemaVersion: 0, available: false, readOnly: true, degraded: false, droppedTasks: 0,
|
|
456
|
+
tasks: Object.freeze({ total: 0, recent: Object.freeze([]) }),
|
|
457
|
+
artifacts: Object.freeze({ total: 0, recent: Object.freeze([]) }),
|
|
458
|
+
userTasks: Object.freeze([])
|
|
459
|
+
}));
|
|
460
|
+
}
|
|
461
|
+
return withReader((runtime) => runtime.run('inspect', ({ dataRoot }) => {
|
|
462
|
+
const tasks = listCoreTasks(dataRoot, { limit: options.limit || 12, skipInvalid: true });
|
|
463
|
+
let artifacts = { total: 0, artifacts: Object.freeze([]) };
|
|
464
|
+
let artifactDegraded = false;
|
|
465
|
+
try { artifacts = listCoreArtifacts(dataRoot, { limit: options.limit || 12 }); }
|
|
466
|
+
catch (_) { artifactDegraded = true; }
|
|
467
|
+
const userTasks = decorateCoreUserRows(
|
|
468
|
+
tasks.tasks.map((task) => projectCoreTaskUserRow(task, artifacts.artifacts)),
|
|
469
|
+
listAllCoreTaskRecords(dataRoot),
|
|
470
|
+
readCoreAttentionPrefs()
|
|
471
|
+
);
|
|
472
|
+
return Object.freeze({
|
|
473
|
+
schemaVersion: 0, available: true, readOnly: true,
|
|
474
|
+
degraded: artifactDegraded || tasks.dropped > 0,
|
|
475
|
+
droppedTasks: tasks.dropped,
|
|
476
|
+
tasks: Object.freeze({ total: tasks.total, recent: tasks.tasks }),
|
|
477
|
+
artifacts: Object.freeze({ total: artifacts.total, recent: artifacts.artifacts }),
|
|
478
|
+
userTasks: Object.freeze(userTasks)
|
|
479
|
+
});
|
|
480
|
+
}), options.signal);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** bounded:全量扫描仅供自动静默派生;超界仍有界退出。 */
|
|
484
|
+
const SUCCESSOR_SCAN_LIMIT = 2000;
|
|
485
|
+
function listAllCoreTaskRecords(dataRoot) {
|
|
486
|
+
const all = [];
|
|
487
|
+
let page;
|
|
488
|
+
do {
|
|
489
|
+
page = listCoreTasks(dataRoot, { offset: all.length, limit: 200, skipInvalid: true });
|
|
490
|
+
all.push(...page.tasks);
|
|
491
|
+
} while (all.length < page.total && all.length < SUCCESSOR_SCAN_LIMIT);
|
|
492
|
+
return all;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/** retriedFrom → 后继任务列表(自动静默只从事实派生,不缓存、不落偏好)。 */
|
|
496
|
+
function successorsByRetriedFrom(records) {
|
|
497
|
+
const map = new Map();
|
|
498
|
+
for (const record of records) {
|
|
499
|
+
if (!record.retriedFrom) continue;
|
|
500
|
+
if (!map.has(record.retriedFrom)) map.set(record.retriedFrom, []);
|
|
501
|
+
map.get(record.retriedFrom).push(record);
|
|
502
|
+
}
|
|
503
|
+
return map;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function suppressedByRetrySuccess(taskId, successors) {
|
|
507
|
+
return (successors.get(taskId) || [])
|
|
508
|
+
.some((record) => record.outcome === 'succeeded' && record.deliveryState === 'ready');
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* 投影行装饰:合并 Host 处置偏好与"重试成功自动静默"派生(每次都是事实重算)。
|
|
513
|
+
* historical 只控制 DSH 分区;canceled 的 Core 五类事实投影仍保持 attention 不变。
|
|
514
|
+
*/
|
|
515
|
+
function decorateCoreUserRows(rows, records, prefs) {
|
|
516
|
+
const successors = successorsByRetriedFrom(records);
|
|
517
|
+
const recordsById = new Map(records.map((record) => [record.id, record]));
|
|
518
|
+
return rows.map((row) => Object.freeze({
|
|
519
|
+
...row,
|
|
520
|
+
disposition: attentionDispositionOf(prefs.entries[row.id]),
|
|
521
|
+
suppressed: suppressedByRetrySuccess(row.id, successors),
|
|
522
|
+
historical: recordsById.get(row.id)?.outcome === 'canceled'
|
|
523
|
+
}));
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/** 为 DSH 同源媒体路由读取已验证 hash 的 Core 图片;不创建宿主 URL 或 token。 */
|
|
527
|
+
export async function readCoreArtifactMediaForDsh(artifactId, options = {}) {
|
|
528
|
+
const id = String(artifactId || '').trim();
|
|
529
|
+
if (!/^artifact_[a-f0-9]{24}$/.test(id) || !fs.existsSync(dshCoreDataRoot())) {
|
|
530
|
+
throw new DshCoreAdapterError('IRIS_DSH_ARTIFACT_NOT_FOUND', 'Core Artifact 不存在');
|
|
531
|
+
}
|
|
532
|
+
try {
|
|
533
|
+
const result = await withReader((runtime) => runtime.run('inspect', ({ dataRoot }) =>
|
|
534
|
+
readCoreArtifactBytes(dataRoot, id)), options.signal);
|
|
535
|
+
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');
|
|
538
|
+
}
|
|
539
|
+
return result;
|
|
540
|
+
} catch (error) {
|
|
541
|
+
if (error instanceof DshCoreAdapterError) throw error;
|
|
542
|
+
throw new DshCoreAdapterError('IRIS_DSH_ARTIFACT_NOT_FOUND', 'Core Artifact 不存在');
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* 将已经 ready 的 Core 图片投影为 DSH attachment blocks。重复调用只重复宿主展示,
|
|
548
|
+
* 不修改 Task/Artifact,也不触发 submit、poll 或 download。
|
|
549
|
+
*/
|
|
550
|
+
export async function projectCoreTaskForDsh(host, taskId, options = {}) {
|
|
551
|
+
const attachments = requireHostPort(host, 'attachments', 'Core 任务产物展示');
|
|
552
|
+
const materialized = await withReader((runtime) => runtime.run('inspect', ({ dataRoot }) => {
|
|
553
|
+
const task = inspectCoreTask(dataRoot, taskId);
|
|
554
|
+
if (task.outcome !== 'succeeded' || task.deliveryState !== 'ready' || !task.artifactIds.length) {
|
|
555
|
+
throw new DshCoreAdapterError('IRIS_DSH_TASK_NOT_READY', 'Core Task 尚无可展示的本地产物');
|
|
556
|
+
}
|
|
557
|
+
return {
|
|
558
|
+
task,
|
|
559
|
+
artifacts: task.artifactIds.map((artifactId) => readCoreArtifactBytes(dataRoot, artifactId))
|
|
560
|
+
};
|
|
561
|
+
}), options.signal);
|
|
562
|
+
|
|
563
|
+
const projected = [];
|
|
564
|
+
for (const item of materialized.artifacts) {
|
|
565
|
+
if (options.signal?.aborted) throw abortError();
|
|
566
|
+
if (!item.artifact.mediaType.startsWith('image/')) {
|
|
567
|
+
throw new DshCoreAdapterError('IRIS_DSH_ARTIFACT_UNSUPPORTED', '当前 DSH 投影只支持图片 Artifact');
|
|
568
|
+
}
|
|
569
|
+
let attachment;
|
|
570
|
+
try {
|
|
571
|
+
attachment = await attachments.saveImage({
|
|
572
|
+
data: new Uint8Array(item.bytes),
|
|
573
|
+
mediaType: item.artifact.mediaType,
|
|
574
|
+
name: 'iris-core-' + item.artifact.id + '.png'
|
|
575
|
+
});
|
|
576
|
+
} catch (_) {
|
|
577
|
+
throw new DshCoreAdapterError('IRIS_DSH_PROJECTION_FAILED', 'Core 产物无法保存为 DSH attachment');
|
|
578
|
+
}
|
|
579
|
+
if (!attachment || !String(attachment.attachmentId || '').trim()) {
|
|
580
|
+
throw new DshCoreAdapterError('IRIS_DSH_PROJECTION_FAILED', 'DSH attachment 返回无效');
|
|
581
|
+
}
|
|
582
|
+
projected.push(Object.freeze({ artifact: item.artifact, attachment }));
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const blocks = [];
|
|
586
|
+
for (const item of projected) {
|
|
587
|
+
blocks.push(Object.freeze({
|
|
588
|
+
type: 'text',
|
|
589
|
+
text: '[iris] Core 任务完成:' + materialized.task.id
|
|
590
|
+
+ '\nartifact: ' + item.artifact.id + '\nattachment: ' + item.attachment.attachmentId
|
|
591
|
+
}));
|
|
592
|
+
blocks.push(Object.freeze({ type: 'image', attachment: item.attachment }));
|
|
593
|
+
}
|
|
594
|
+
return Object.freeze({
|
|
595
|
+
task: materialized.task,
|
|
596
|
+
artifacts: Object.freeze(projected),
|
|
597
|
+
blocks: Object.freeze(blocks)
|
|
598
|
+
});
|
|
599
|
+
}
|