@mokuyoaxis/dsh-iris 0.2.5 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dsh/skills/iris-use/SKILL.md +2 -2
- package/.dsh/skills/iris-use/references/media-contract.md +2 -2
- package/CHANGELOG.md +49 -1
- package/README.en.md +39 -22
- package/README.md +40 -21
- package/bin/dsh-iris.js +166 -35
- package/docs/AGENT_QUICKSTART.md +7 -7
- package/docs/ARCHITECTURE.md +9 -3
- package/docs/CLI_MANAGEMENT.md +5 -1
- package/docs/CORE_RUNTIME_CONTRACT.md +36 -5
- package/docs/CORE_SKILLS.md +62 -0
- package/docs/DSH_COMPATIBILITY.md +8 -4
- package/docs/DSH_CORE_MIGRATION.md +4 -2
- package/docs/DSH_PROVIDER_CONNECTIONS.md +34 -0
- package/docs/DSH_RUNTIME.md +36 -0
- package/docs/FUNCTION_MODEL_ASSIGNMENTS.md +2 -0
- package/docs/FUNCTION_MODEL_POOLS.md +84 -0
- package/docs/HEADLESS_CLI.md +16 -8
- package/docs/HOST_ADAPTER_CONTRACT.md +7 -2
- package/docs/HOST_DOCTOR.md +14 -2
- package/docs/IMAGE_FLOW.md +71 -0
- package/docs/LOCAL_API.md +60 -0
- package/docs/LOCAL_API_CONTRACT.md +115 -0
- package/docs/LOCAL_CREATION.md +50 -0
- package/docs/MUSIC.md +2 -2
- package/docs/PROVIDER_HEALTH.md +26 -2
- package/docs/ROADMAP.md +18 -4
- package/docs/SECURITY.md +20 -2
- package/docs/SYNC_DELIVERY_RECOVERY.md +2 -0
- package/docs/VISION_CLI.md +2 -2
- package/docs/WEB_PROTOTYPE.md +47 -0
- package/docs/releases/0.2.6.md +50 -0
- package/docs/releases/0.2.7.md +62 -0
- package/lib/actions.js +208 -90
- package/lib/adapters.js +36 -9
- package/lib/api.js +40 -39
- package/lib/application-runtime.js +186 -0
- package/lib/client.js +805 -182
- package/lib/composite-vision-routing.js +6 -6
- package/lib/config.js +140 -29
- package/lib/core-artifact-store.js +5 -0
- package/lib/core-maintenance.js +25 -2
- package/lib/core-runtime.js +19 -0
- package/lib/core-tasks.js +62 -8
- package/lib/creation-request-store.js +159 -0
- package/lib/doctor.js +20 -2
- package/lib/dsh-core-adapter.js +669 -564
- package/lib/dsh-host-adapter.js +14 -1
- package/lib/dsh-provider-config.js +35 -0
- package/lib/dsh-provider-source.js +102 -0
- package/lib/flow-store.js +195 -0
- package/lib/headless-vision.js +5 -5
- package/lib/host-contract.js +1 -0
- package/lib/http-vision-model-adapter.js +5 -2
- package/lib/image-flow.js +188 -0
- package/lib/index.js +103 -78
- package/lib/local-api-projection.js +27 -0
- package/lib/local-api.js +381 -0
- package/lib/local-creation.js +85 -0
- package/lib/model-directory-service.js +53 -0
- package/lib/model-directory.js +103 -0
- package/lib/models.js +71 -6
- package/lib/ocr-model-routing.js +2 -2
- package/lib/private-storage.js +12 -0
- package/lib/provider-adapters.js +42 -9
- package/lib/provider-catalog.js +14 -9
- package/lib/provider-config-scope.js +16 -0
- package/lib/provider-config-service.js +82 -13
- package/lib/provider-health.js +33 -0
- package/lib/provider-protocol.js +30 -0
- package/lib/provider-task-runner.js +22 -2
- package/lib/render-address.js +35 -0
- package/lib/render.js +52 -13
- package/lib/skill-installation.js +199 -0
- package/lib/vision-model-routing.js +25 -12
- package/lib/workbench-artifacts.js +1 -2
- package/lib/workbench-management.js +8 -9
- package/package.json +4 -3
- package/skills/iris-core/SKILL.md +40 -0
- package/skills/iris-core/agents/openai.yaml +4 -0
- package/skills/iris-core/references/cli.md +95 -0
- package/skills/iris-core/references/tasks.md +58 -0
- package/user_guide.md +32 -20
package/lib/adapters.js
CHANGED
|
@@ -367,7 +367,15 @@ export async function synthesizeTts({ key, baseUrl, model = 'qwen-tts-latest', t
|
|
|
367
367
|
body: JSON.stringify({ model, input: { text, voice } })
|
|
368
368
|
});
|
|
369
369
|
const body = await readJson(res);
|
|
370
|
-
if (!res.ok)
|
|
370
|
+
if (!res.ok) {
|
|
371
|
+
const error = providerError(res.status, body, '合成失败', res.headers);
|
|
372
|
+
if (res.status === 404 && (/NotFound\.Model|model_not_found/i.test(error.providerCode)
|
|
373
|
+
|| /model (?:not exist|can not be found|.*does not exist)/i.test(String(error.rootCause)))) {
|
|
374
|
+
error.category = 'invalid_parameter';
|
|
375
|
+
error.message = `当前语音接口找不到模型 ${model}(${new URL(apiBase).hostname});请核对模型 ID、所属地域及调用协议。上游:${error.rootCause}`;
|
|
376
|
+
}
|
|
377
|
+
throw error;
|
|
378
|
+
}
|
|
371
379
|
const audio = body && body.output && body.output.audio;
|
|
372
380
|
const url = audio && (audio.url || audio.audio_url);
|
|
373
381
|
const b64 = audio && typeof audio.data === 'string' ? audio.data : null;
|
|
@@ -510,6 +518,7 @@ export async function listModels({ key, baseUrl, timeoutMs = T_SUBMIT, signal })
|
|
|
510
518
|
const found = new Map();
|
|
511
519
|
const maxPages = dashscope ? 50 : 1;
|
|
512
520
|
let loaded = 0;
|
|
521
|
+
let expectedTotal = null;
|
|
513
522
|
for (let page = 1; page <= maxPages; page++) {
|
|
514
523
|
const url = new URL(endpoint);
|
|
515
524
|
if (dashscope) {
|
|
@@ -519,14 +528,24 @@ export async function listModels({ key, baseUrl, timeoutMs = T_SUBMIT, signal })
|
|
|
519
528
|
const res = await fetch(url, { headers: authHeaders(key), signal: timeoutSignal(timeoutMs, signal) });
|
|
520
529
|
const body = await readJson(res);
|
|
521
530
|
if (!res.ok) throw providerError(res.status, body, '模型发现失败', res.headers);
|
|
531
|
+
if (!body || body.success === false) throw new ProviderContractError('模型目录响应无效,已保留上次完整目录', {
|
|
532
|
+
stage: 'validate', category: 'protocol', acceptance: 'not_accepted'
|
|
533
|
+
});
|
|
522
534
|
const output = body && body.output;
|
|
523
535
|
const items = (output && Array.isArray(output.models) && output.models)
|
|
524
536
|
|| (body && Array.isArray(body.data) && body.data)
|
|
525
|
-
|| (Array.isArray(body && body.models) ? body.models :
|
|
537
|
+
|| (Array.isArray(body && body.models) ? body.models : null);
|
|
538
|
+
if (!items) throw new ProviderContractError('模型目录缺少模型数组,已保留上次完整目录', {
|
|
539
|
+
stage: 'validate', category: 'protocol', acceptance: 'not_accepted'
|
|
540
|
+
});
|
|
526
541
|
loaded += items.length;
|
|
527
542
|
for (const item of items) {
|
|
528
|
-
const
|
|
529
|
-
if (
|
|
543
|
+
const rawId = typeof item === 'string' ? item : item?.id || item?.model;
|
|
544
|
+
if (typeof rawId !== 'string' || !rawId.trim()) throw new ProviderContractError('模型目录包含无效模型 ID,已保留上次完整目录', {
|
|
545
|
+
stage: 'validate', category: 'protocol', acceptance: 'not_accepted'
|
|
546
|
+
});
|
|
547
|
+
const id = rawId.trim();
|
|
548
|
+
if (found.has(id)) continue;
|
|
530
549
|
const capabilities = Array.isArray(item?.capabilities)
|
|
531
550
|
? item.capabilities.filter((value) => typeof value === 'string' && value)
|
|
532
551
|
: [];
|
|
@@ -538,17 +557,25 @@ export async function listModels({ key, baseUrl, timeoutMs = T_SUBMIT, signal })
|
|
|
538
557
|
if (!Array.isArray(item?.capabilities)) {
|
|
539
558
|
if (responseModalities.includes('image')) capabilities.push('IG');
|
|
540
559
|
if (responseModalities.includes('video')) capabilities.push('VG');
|
|
541
|
-
|
|
542
|
-
if (requestModalities.
|
|
560
|
+
// 音频理解不证明支持文件转写;混合视频/图像/文本输出也不证明独立TTS。
|
|
561
|
+
if (requestModalities.length === 1 && requestModalities[0] === 'text'
|
|
562
|
+
&& responseModalities.length === 1 && responseModalities[0] === 'audio') capabilities.push('TTS');
|
|
543
563
|
if (requestModalities.includes('image') && responseModalities.includes('text')) capabilities.push('VU');
|
|
544
564
|
}
|
|
545
565
|
const normalizedCapabilities = [...new Set(capabilities)];
|
|
546
566
|
found.set(id, normalizedCapabilities.length ? { id, capabilities: normalizedCapabilities } : id);
|
|
547
567
|
}
|
|
548
568
|
if (!dashscope) break;
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
|
|
569
|
+
const total = output?.total === undefined ? expectedTotal : Number(output.total);
|
|
570
|
+
if (total !== null && (!Number.isSafeInteger(total) || total < 0
|
|
571
|
+
|| (expectedTotal !== null && total !== expectedTotal) || loaded > total || (!items.length && loaded < total)
|
|
572
|
+
|| (loaded === total && found.size !== loaded))) {
|
|
573
|
+
throw new ProviderContractError('模型目录分页不完整或总数变化,已保留上次完整目录', {
|
|
574
|
+
stage: 'validate', category: 'protocol', acceptance: 'not_accepted'
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
if (total !== null) expectedTotal = total;
|
|
578
|
+
if ((total !== null && loaded === total) || (total === null && items.length < 100)) break;
|
|
552
579
|
if (page === maxPages) throw new Error('iris: 模型目录超过安全分页上限,已保留原模型池');
|
|
553
580
|
}
|
|
554
581
|
return [...found.values()];
|
package/lib/api.js
CHANGED
|
@@ -26,7 +26,6 @@ import { selectMediaProtocol } from './provider-protocol.js';
|
|
|
26
26
|
import { deriveUserState } from './task-semantics.js';
|
|
27
27
|
import { hostDoctor } from './doctor.js';
|
|
28
28
|
import { hostRuntimeEvidence, recordHostClient } from './host-runtime.js';
|
|
29
|
-
import { cancelProviderTaskForDsh, coreSnapshotForDsh, disposeCoreTaskAttention, readCoreArtifactMediaForDsh, redeliverProviderTaskForDsh, reobserveProviderTaskForDsh, retryProviderTaskForDsh } from './dsh-core-adapter.js';
|
|
30
29
|
import { optimizePrompt } from './prompt-optimizer.js';
|
|
31
30
|
import { PROMPT_OPTIMIZER_CAPABILITIES } from './prompt-optimizer-core.js';
|
|
32
31
|
import {
|
|
@@ -66,7 +65,8 @@ function providerPublic(p) {
|
|
|
66
65
|
mediaBaseUrl: p.mediaBaseUrl || '',
|
|
67
66
|
enabled: p.enabled !== false,
|
|
68
67
|
...selectMediaProtocol(p),
|
|
69
|
-
apiKeyHint: keyHint(p.apiKey),
|
|
68
|
+
apiKeyHint: p.dshProvider ? '由 DSH 管理' : keyHint(p.apiKey),
|
|
69
|
+
...(p.dshProvider ? { dshProvider: p.dshProvider } : {}),
|
|
70
70
|
capabilities: capabilitiesOf(p),
|
|
71
71
|
// 迁移/推断结果是否与原始声明不同(UI 提示旧配置已推断)
|
|
72
72
|
capabilityInferred: !Array.isArray(p.capabilities) || p.capabilities.length === 0,
|
|
@@ -331,16 +331,17 @@ function serveSse(res) {
|
|
|
331
331
|
}
|
|
332
332
|
|
|
333
333
|
/** webServer 前缀路由 handler:状态、任务、动作与提示词优化器 API。 */
|
|
334
|
-
export function serveApi(req, res, host) {
|
|
334
|
+
export function serveApi(req, res, host, core) {
|
|
335
|
+
if (!core || core.snapshot().state !== 'open') return sendJson(res, 503, { error: 'Iris DSH 实例正在关闭或不可用', code: 'IRIS_DSH_RUNTIME_CLOSED' });
|
|
335
336
|
try {
|
|
336
337
|
const method = String(req.method || 'GET').toUpperCase();
|
|
337
338
|
const urlPath = String(req.url || '').split('?')[0];
|
|
338
339
|
|
|
339
340
|
// POST /iris/api/actions/:name —— GUI 直连触发工具执行
|
|
340
341
|
if (method === 'POST') {
|
|
341
|
-
if (urlPath === '/iris/api/works/download') return handleWorkbenchPost(req, res, downloadWorkbenchSelection, true);
|
|
342
|
-
if (urlPath === '/iris/api/works/delete') return handleWorkbenchPost(req, res, deleteWorkbenchArtifacts);
|
|
343
|
-
if (urlPath === '/iris/api/works/restore') return handleWorkbenchPost(req, res, restoreWorkbenchTransaction);
|
|
342
|
+
if (urlPath === '/iris/api/works/download') return handleWorkbenchPost(req, res, downloadWorkbenchSelection, true, core);
|
|
343
|
+
if (urlPath === '/iris/api/works/delete') return handleWorkbenchPost(req, res, deleteWorkbenchArtifacts, false, core);
|
|
344
|
+
if (urlPath === '/iris/api/works/restore') return handleWorkbenchPost(req, res, restoreWorkbenchTransaction, false, core);
|
|
344
345
|
if (urlPath === '/iris/api/host-client') {
|
|
345
346
|
if (!host || typeof host !== 'object') return sendJson(res, 503, { error: 'host unavailable' });
|
|
346
347
|
return handleJsonPost(req, res, (body) => recordHostClient(body));
|
|
@@ -351,16 +352,16 @@ export function serveApi(req, res, host) {
|
|
|
351
352
|
}
|
|
352
353
|
// D4 人工 retry as new task:产生新的真实计费,请求体必须显式 confirmBilling:true
|
|
353
354
|
const coreTaskRetry = urlPath.match(/^\/iris\/api\/core\/task\/([^/]+)\/retry\/?$/);
|
|
354
|
-
if (coreTaskRetry) return handleCoreTaskRetry(coreTaskRetry[1], req, res);
|
|
355
|
+
if (coreTaskRetry) return handleCoreTaskRetry(coreTaskRetry[1], req, res, core);
|
|
355
356
|
// D1/D2/D3 人工动作:reobserve 显式单步 poll 一次;redeliver 只对 succeeded+failed
|
|
356
357
|
// 重新取回产物;cancel 只有 Provider 明确确认才写已取消。三者绝不 submit。
|
|
357
358
|
const coreTaskAction = urlPath.match(/^\/iris\/api\/core\/task\/([^/]+)\/(reobserve|redeliver|cancel|acknowledge|restore|hide|unhide)\/?$/);
|
|
358
|
-
if (coreTaskAction) return handleCoreTaskManual(coreTaskAction[2], coreTaskAction[1], req, res);
|
|
359
|
+
if (coreTaskAction) return handleCoreTaskManual(coreTaskAction[2], coreTaskAction[1], req, res, core);
|
|
359
360
|
const promptMatch = typeof host === 'object' && urlPath.match(/^\/iris\/api\/prompt-optimizer\/(optimize|import|reset|enabled)\/?$/);
|
|
360
|
-
if (promptMatch) return handlePromptOptimizer(host, promptMatch[1], req, res);
|
|
361
|
+
if (promptMatch) return handlePromptOptimizer(host, promptMatch[1], req, res, core);
|
|
361
362
|
const actionMatch = typeof host === 'object' && urlPath.match(/^\/iris\/api\/actions\/([A-Za-z0-9_-]+)\/?$/);
|
|
362
363
|
if (!actionMatch) { return sendJson(res, 405, { error: 'method not allowed' }); }
|
|
363
|
-
return handleAction(host, actionMatch[1], req, res);
|
|
364
|
+
return handleAction(host, actionMatch[1], req, res, core);
|
|
364
365
|
}
|
|
365
366
|
// GET/HEAD 原逻辑
|
|
366
367
|
if (method !== 'GET' && method !== 'HEAD') {
|
|
@@ -381,16 +382,16 @@ export function serveApi(req, res, host) {
|
|
|
381
382
|
if (urlPath === '/iris/api/core/snapshot') {
|
|
382
383
|
if (method === 'HEAD') return sendJson(res, 200, {});
|
|
383
384
|
const query = new URL(req.url || '', 'http://iris.local').searchParams;
|
|
384
|
-
return serveCoreSnapshot(res, { limit: Number(query.get('limit') || 12) });
|
|
385
|
+
return serveCoreSnapshot(res, { limit: Number(query.get('limit') || 12) }, core);
|
|
385
386
|
}
|
|
386
387
|
if (urlPath === '/iris/api/works') {
|
|
387
|
-
return serveWorkbenchArtifacts(req, method, res);
|
|
388
|
+
return serveWorkbenchArtifacts(req, method, res, core);
|
|
388
389
|
}
|
|
389
|
-
if (urlPath === '/iris/api/works/transactions') return serveWorkbenchRead(res, method, () => workbenchTransactions());
|
|
390
|
+
if (urlPath === '/iris/api/works/transactions') return serveWorkbenchRead(res, method, () => workbenchTransactions({ core }));
|
|
390
391
|
const workDetailMatch = urlPath.match(/^\/iris\/api\/works\/(core|legacy)\/([^/]+)$/);
|
|
391
|
-
if (workDetailMatch) return serveWorkbenchRead(res, method, () => workbenchDetail(workDetailMatch[1], workDetailMatch[2]));
|
|
392
|
+
if (workDetailMatch) return serveWorkbenchRead(res, method, () => workbenchDetail(workDetailMatch[1], workDetailMatch[2], { core }));
|
|
392
393
|
const coreMediaMatch = urlPath.match(/^\/iris\/api\/core\/artifact\/(artifact_[a-f0-9]{24})\/media$/);
|
|
393
|
-
if (coreMediaMatch) return serveCoreArtifactMedia(coreMediaMatch[1], method, res);
|
|
394
|
+
if (coreMediaMatch) return serveCoreArtifactMedia(coreMediaMatch[1], method, res, core);
|
|
394
395
|
if (urlPath === '/iris/api/artifacts') {
|
|
395
396
|
const query = new URL(req.url || '', 'http://iris.local').searchParams;
|
|
396
397
|
const page = artifacts.page({ offset: Number(query.get('offset') || 0), limit: Number(query.get('limit') || 24) });
|
|
@@ -416,11 +417,11 @@ export function serveApi(req, res, host) {
|
|
|
416
417
|
}
|
|
417
418
|
}
|
|
418
419
|
|
|
419
|
-
async function serveWorkbenchArtifacts(req, method, res) {
|
|
420
|
+
async function serveWorkbenchArtifacts(req, method, res, core) {
|
|
420
421
|
const params = new URL(req.url || '', 'http://iris.local').searchParams;
|
|
421
422
|
const input = Object.fromEntries(params);
|
|
422
423
|
for (const key of ['offset', 'limit']) if (input[key] !== undefined) input[key] = Number(input[key]);
|
|
423
|
-
try { return sendJson(res, 200, method === 'HEAD' ? {} : await workbenchArtifacts(input)); }
|
|
424
|
+
try { return sendJson(res, 200, method === 'HEAD' ? {} : await workbenchArtifacts(input, { core })); }
|
|
424
425
|
catch (error) { return sendJson(res, error.statusCode || 500, { error: '作品读取失败', code: error.code || 'IRIS_GALLERY_FAILED' }); }
|
|
425
426
|
}
|
|
426
427
|
|
|
@@ -429,9 +430,9 @@ async function serveWorkbenchRead(res, method, execute) {
|
|
|
429
430
|
catch (error) { const safe = workbenchError(error); return sendJson(res, safe.statusCode || 400, { error: safe.message, code: safe.code }); }
|
|
430
431
|
}
|
|
431
432
|
|
|
432
|
-
function handleWorkbenchPost(req, res, execute, download = false) {
|
|
433
|
+
function handleWorkbenchPost(req, res, execute, download = false, core) {
|
|
433
434
|
return handleJsonPost(req, res, async (body, signal) => {
|
|
434
|
-
try { return await execute(body, { signal }); }
|
|
435
|
+
try { return await core.runHostOperation(operationSignal => execute(body, { signal: operationSignal, core }), { signal }); }
|
|
435
436
|
catch (error) { throw workbenchError(error); }
|
|
436
437
|
}, download ? (response, _status, bytes) => {
|
|
437
438
|
if (response.writableEnded || response.destroyed) return;
|
|
@@ -442,9 +443,9 @@ function handleWorkbenchPost(req, res, execute, download = false) {
|
|
|
442
443
|
} : sendJson);
|
|
443
444
|
}
|
|
444
445
|
|
|
445
|
-
async function serveCoreArtifactMedia(artifactId, method, res) {
|
|
446
|
+
async function serveCoreArtifactMedia(artifactId, method, res, core) {
|
|
446
447
|
try {
|
|
447
|
-
const result = await readCoreArtifactMediaForDsh(artifactId);
|
|
448
|
+
const result = await core.readCoreArtifactMediaForDsh(artifactId);
|
|
448
449
|
if (res.writableEnded || res.destroyed) return;
|
|
449
450
|
res.writeHead(200, {
|
|
450
451
|
'Content-Type': result.artifact.mediaType,
|
|
@@ -461,9 +462,9 @@ async function serveCoreArtifactMedia(artifactId, method, res) {
|
|
|
461
462
|
}
|
|
462
463
|
}
|
|
463
464
|
|
|
464
|
-
async function serveCoreSnapshot(res, options = {}) {
|
|
465
|
+
async function serveCoreSnapshot(res, options = {}, core) {
|
|
465
466
|
try {
|
|
466
|
-
return sendJson(res, 200, await coreSnapshotForDsh({ limit: options.limit || 12 }));
|
|
467
|
+
return sendJson(res, 200, await core.coreSnapshotForDsh({ limit: options.limit || 12 }));
|
|
467
468
|
} catch (error) {
|
|
468
469
|
console.error('[iris] Core 只读快照失败:', error && error.code || 'unknown');
|
|
469
470
|
return sendJson(res, 409, {
|
|
@@ -526,8 +527,8 @@ function handleJsonPost(req, res, execute, respond = sendJson) {
|
|
|
526
527
|
req.on('error', () => finish(400, { error: 'bad request' }));
|
|
527
528
|
}
|
|
528
529
|
|
|
529
|
-
function handleAction(host, name, req, res) {
|
|
530
|
-
return handleJsonPost(req, res, (args, signal) => runAction(host, name, args, { signal }));
|
|
530
|
+
function handleAction(host, name, req, res, core) {
|
|
531
|
+
return handleJsonPost(req, res, (args, signal) => runAction(host, name, args, { signal, core }));
|
|
531
532
|
}
|
|
532
533
|
|
|
533
534
|
/**
|
|
@@ -538,14 +539,14 @@ function handleAction(host, name, req, res) {
|
|
|
538
539
|
* 经 AbortSignal 传入在途操作,与 GUI actions 同一约定。
|
|
539
540
|
*/
|
|
540
541
|
const CORE_TASK_MANUAL_ACTIONS = Object.freeze({
|
|
541
|
-
reobserve: { command: 'task.reobserve', run: reobserveProviderTaskForDsh },
|
|
542
|
-
redeliver: { command: 'task.redeliver', run: redeliverProviderTaskForDsh },
|
|
543
|
-
cancel: { command: 'task.cancel', run: cancelProviderTaskForDsh },
|
|
542
|
+
reobserve: { command: 'task.reobserve', run: (core, input) => core.reobserveProviderTaskForDsh(input) },
|
|
543
|
+
redeliver: { command: 'task.redeliver', run: (core, input) => core.redeliverProviderTaskForDsh(input) },
|
|
544
|
+
cancel: { command: 'task.cancel', run: (core, input) => core.cancelProviderTaskForDsh(input) },
|
|
544
545
|
// 注意力处置是 Host 偏好(不写 Core 事实):acknowledge/restore/hide/unhide。
|
|
545
|
-
acknowledge: { command: 'attention.acknowledge', run: (input) => disposeCoreTaskAttention({ ...input, action: 'acknowledge' }) },
|
|
546
|
-
restore: { command: 'attention.restore', run: (input) => disposeCoreTaskAttention({ ...input, action: 'restore' }) },
|
|
547
|
-
hide: { command: 'attention.hide', run: (input) => disposeCoreTaskAttention({ ...input, action: 'hide' }) },
|
|
548
|
-
unhide: { command: 'attention.unhide', run: (input) => disposeCoreTaskAttention({ ...input, action: 'unhide' }) }
|
|
546
|
+
acknowledge: { command: 'attention.acknowledge', run: (core, input) => core.disposeCoreTaskAttention({ ...input, action: 'acknowledge' }) },
|
|
547
|
+
restore: { command: 'attention.restore', run: (core, input) => core.disposeCoreTaskAttention({ ...input, action: 'restore' }) },
|
|
548
|
+
hide: { command: 'attention.hide', run: (core, input) => core.disposeCoreTaskAttention({ ...input, action: 'hide' }) },
|
|
549
|
+
unhide: { command: 'attention.unhide', run: (core, input) => core.disposeCoreTaskAttention({ ...input, action: 'unhide' }) }
|
|
549
550
|
});
|
|
550
551
|
|
|
551
552
|
/**
|
|
@@ -554,7 +555,7 @@ const CORE_TASK_MANUAL_ACTIONS = Object.freeze({
|
|
|
554
555
|
* Prompt,只能由调用方重新提供;缺显式计费确认在读取旧 Task 之前拒绝。
|
|
555
556
|
* 成功返回 200 + 新任务的五类用户投影行与 retriedFrom 关系。
|
|
556
557
|
*/
|
|
557
|
-
function handleCoreTaskRetry(taskId, req, res) {
|
|
558
|
+
function handleCoreTaskRetry(taskId, req, res, core) {
|
|
558
559
|
if (!/^task_[a-f0-9]{24}$/.test(taskId)) {
|
|
559
560
|
return sendJson(res, 400, { error: { code: 'IRIS_DSH_TASK_INVALID', message: 'Core Task ID 无效' } });
|
|
560
561
|
}
|
|
@@ -567,7 +568,7 @@ function handleCoreTaskRetry(taskId, req, res) {
|
|
|
567
568
|
throw error;
|
|
568
569
|
}
|
|
569
570
|
const prompt = String(body.prompt || '');
|
|
570
|
-
const result = await retryProviderTaskForDsh({
|
|
571
|
+
const result = await core.retryProviderTaskForDsh({
|
|
571
572
|
taskId,
|
|
572
573
|
prompt,
|
|
573
574
|
...(body.source_artifact_id !== undefined ? { sourceArtifactId: body.source_artifact_id } : {}),
|
|
@@ -596,7 +597,7 @@ function handleCoreTaskRetry(taskId, req, res) {
|
|
|
596
597
|
});
|
|
597
598
|
}
|
|
598
599
|
|
|
599
|
-
function handleCoreTaskManual(action, taskId, req, res) {
|
|
600
|
+
function handleCoreTaskManual(action, taskId, req, res, core) {
|
|
600
601
|
if (!/^task_[a-f0-9]{24}$/.test(taskId)) {
|
|
601
602
|
return sendJson(res, 400, { error: { code: 'IRIS_DSH_TASK_INVALID', message: 'Core Task ID 无效' } });
|
|
602
603
|
}
|
|
@@ -613,7 +614,7 @@ function handleCoreTaskManual(action, taskId, req, res) {
|
|
|
613
614
|
if (typeof req.on === 'function') req.on('aborted', disconnect);
|
|
614
615
|
if (typeof res.on === 'function') res.on('close', disconnect);
|
|
615
616
|
if (typeof req.resume === 'function') req.resume(); // 无 body 入参;避免小请求体滞留
|
|
616
|
-
entry.run({ taskId, signal: controller.signal })
|
|
617
|
+
entry.run(core, { taskId, signal: controller.signal })
|
|
617
618
|
.then((result) => finish(200, {
|
|
618
619
|
ok: true,
|
|
619
620
|
contractVersion: 0,
|
|
@@ -630,8 +631,8 @@ function handleCoreTaskManual(action, taskId, req, res) {
|
|
|
630
631
|
});
|
|
631
632
|
}
|
|
632
633
|
|
|
633
|
-
function handlePromptOptimizer(host, operation, req, res) {
|
|
634
|
-
return handleJsonPost(req, res,
|
|
634
|
+
function handlePromptOptimizer(host, operation, req, res, core) {
|
|
635
|
+
return handleJsonPost(req, res, (body, requestSignal) => core.runHostOperation(async signal => {
|
|
635
636
|
if (operation === 'optimize') return optimizePrompt(host, body, { signal });
|
|
636
637
|
if (operation === 'import') {
|
|
637
638
|
const imported = body && body.config !== undefined ? body.config : body;
|
|
@@ -644,7 +645,7 @@ function handlePromptOptimizer(host, operation, req, res) {
|
|
|
644
645
|
}
|
|
645
646
|
if (operation === 'enabled') return { ok: true, ...setPromptOptimizerEnabled(body && body.enabled), capabilities: PROMPT_CONFIG_CAPABILITIES };
|
|
646
647
|
throw new Error('unknown prompt optimizer operation');
|
|
647
|
-
});
|
|
648
|
+
}, { signal: requestSignal }));
|
|
648
649
|
}
|
|
649
650
|
|
|
650
651
|
/* ---------- 文件选择器 L2:本地文件上传(阶段 10) ---------- */
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { normalizeCoreOptions } from './core-contract.js';
|
|
4
|
+
import { createCoreRuntime } from './core-runtime.js';
|
|
5
|
+
import { createCommandService } from './command-service.js';
|
|
6
|
+
import { normalizeGenerationInput } from './generation-input.js';
|
|
7
|
+
import { createProviderTaskRunner } from './provider-task-runner.js';
|
|
8
|
+
import { createImageFlowService } from './image-flow.js';
|
|
9
|
+
import { createLocalCreationService } from './local-creation.js';
|
|
10
|
+
|
|
11
|
+
// Internal first-slice execution surface, not a public API or an HTTP allowlist.
|
|
12
|
+
const COMMAND_MODES = Object.freeze({
|
|
13
|
+
'artifact.import': 'writer', 'artifact.export': 'writer', 'media.diff': 'writer',
|
|
14
|
+
'task.observe': 'writer', 'task.reobserve': 'writer', 'task.redeliver': 'writer', 'task.cancel': 'writer',
|
|
15
|
+
'artifact.inspect': 'reader', 'artifact.list': 'reader', 'task.inspect': 'reader', 'task.list': 'reader',
|
|
16
|
+
'flow.init': 'writer', 'flow.create': 'writer', 'flow.continue': 'writer', 'flow.select': 'writer', 'flow.export': 'writer',
|
|
17
|
+
'flow.inspect': 'reader', 'flow.list': 'reader',
|
|
18
|
+
'creation.init': 'writer', 'creation.image': 'writer', 'creation.flow': 'writer'
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export class ApplicationRuntimeError extends Error {
|
|
22
|
+
constructor(code, message) { super(message); this.name = 'ApplicationRuntimeError'; this.code = code; }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function aborted() {
|
|
26
|
+
return Object.assign(new ApplicationRuntimeError('IRIS_APP_ABORTED', 'Iris 操作已取消'), { name: 'AbortError' });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function closed() {
|
|
30
|
+
return new ApplicationRuntimeError('IRIS_APP_CLOSED', 'Iris 应用运行时正在关闭或已关闭');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Instance-owned FIFO and operation-scoped Core leases. No I/O until an explicit operation. */
|
|
34
|
+
export function createApplicationRuntime({ dataRoot, resolveImageCandidates, resolveTaskAdapter } = {}) {
|
|
35
|
+
const root = normalizeCoreOptions({ dataRoot }).dataRoot;
|
|
36
|
+
if (resolveImageCandidates !== undefined && typeof resolveImageCandidates !== 'function') {
|
|
37
|
+
throw new TypeError('resolveImageCandidates 必须是显式候选解析函数');
|
|
38
|
+
}
|
|
39
|
+
if (resolveTaskAdapter !== undefined && typeof resolveTaskAdapter !== 'function') {
|
|
40
|
+
throw new TypeError('resolveTaskAdapter 必须是显式 Task Provider 解析函数');
|
|
41
|
+
}
|
|
42
|
+
let state = 'open';
|
|
43
|
+
const queue = [];
|
|
44
|
+
const active = new Set();
|
|
45
|
+
let writer = null;
|
|
46
|
+
let disposePromise;
|
|
47
|
+
let cleanupFailure;
|
|
48
|
+
|
|
49
|
+
function snapshot() {
|
|
50
|
+
return Object.freeze({ state, queuedWrites: queue.length, activeWrites: Number(Boolean(writer)),
|
|
51
|
+
activeReaders: active.size - Number(Boolean(writer)), cleanupFailed: Boolean(cleanupFailure) });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function detach(job) { job.signal?.removeEventListener('abort', job.onAbort); }
|
|
55
|
+
|
|
56
|
+
function rejectQueued(job, error) {
|
|
57
|
+
detach(job);
|
|
58
|
+
job.reject(error);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function stop(job) {
|
|
62
|
+
job.aborted = true;
|
|
63
|
+
if (job.runtime && !job.disposal) {
|
|
64
|
+
job.disposal = job.runtime.dispose();
|
|
65
|
+
// The operation awaits this same promise below and reports any cleanup error.
|
|
66
|
+
job.disposal.catch(() => {});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function stopAll() {
|
|
71
|
+
for (const job of queue.splice(0)) rejectQueued(job, closed());
|
|
72
|
+
for (const job of active) stop(job);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function perform(job) {
|
|
76
|
+
if (job.aborted) throw aborted();
|
|
77
|
+
const runtime = createCoreRuntime({ dataRoot: root, mode: job.mode });
|
|
78
|
+
job.runtime = runtime;
|
|
79
|
+
let result, operationError, failed = false;
|
|
80
|
+
try {
|
|
81
|
+
runtime.start();
|
|
82
|
+
result = await job.action(runtime);
|
|
83
|
+
} catch (error) { failed = true; operationError = error; }
|
|
84
|
+
try { await (job.disposal || runtime.dispose()); }
|
|
85
|
+
catch (error) {
|
|
86
|
+
cleanupFailure ||= error;
|
|
87
|
+
state = 'closing';
|
|
88
|
+
stopAll();
|
|
89
|
+
if (failed) {
|
|
90
|
+
throw Object.assign(new AggregateError([operationError, error], 'Iris 操作失败且运行时未能安全释放'), { code: error.code });
|
|
91
|
+
}
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
if (failed) throw operationError;
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function start(job) {
|
|
99
|
+
active.add(job);
|
|
100
|
+
job.done = Promise.resolve().then(async () => {
|
|
101
|
+
let result, error, failed = false;
|
|
102
|
+
try { result = await perform(job); }
|
|
103
|
+
catch (caught) { failed = true; error = caught; }
|
|
104
|
+
detach(job);
|
|
105
|
+
active.delete(job);
|
|
106
|
+
if (writer === job) writer = null;
|
|
107
|
+
if (failed) job.reject(error); else job.resolve(result);
|
|
108
|
+
dispatch();
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function dispatch() {
|
|
113
|
+
if (state !== 'open' || writer || !queue.length) return;
|
|
114
|
+
writer = queue.shift();
|
|
115
|
+
start(writer);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function schedule(mode, action, { signal } = {}) {
|
|
119
|
+
if (signal !== undefined && !(signal instanceof AbortSignal)) throw new TypeError('signal 必须是 AbortSignal');
|
|
120
|
+
if (state !== 'open') return Promise.reject(closed());
|
|
121
|
+
if (signal?.aborted) return Promise.reject(aborted());
|
|
122
|
+
let job;
|
|
123
|
+
const result = new Promise((resolve, reject) => { job = { mode, action, signal, resolve, reject, aborted: false }; });
|
|
124
|
+
job.onAbort = () => {
|
|
125
|
+
const index = queue.indexOf(job);
|
|
126
|
+
if (index >= 0) { queue.splice(index, 1); rejectQueued(job, aborted()); }
|
|
127
|
+
else stop(job);
|
|
128
|
+
};
|
|
129
|
+
signal?.addEventListener('abort', job.onAbort, { once: true });
|
|
130
|
+
if (mode === 'writer') { queue.push(job); dispatch(); }
|
|
131
|
+
else start(job);
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Trusted in-process Host composition; never an external command/HTTP dispatch surface. */
|
|
136
|
+
function run(mode, action, options) {
|
|
137
|
+
if (!['reader', 'writer'].includes(mode)) throw new TypeError('Host 操作需要明确 reader/writer 模式');
|
|
138
|
+
if (typeof action !== 'function') throw new TypeError('Host 操作必须是函数');
|
|
139
|
+
return schedule(mode, action, options);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function execute(name, input, options) {
|
|
143
|
+
if (!Object.hasOwn(COMMAND_MODES, name)) {
|
|
144
|
+
throw new ApplicationRuntimeError('IRIS_APP_COMMAND_UNSUPPORTED', '应用运行时尚未装配该 Command');
|
|
145
|
+
}
|
|
146
|
+
const ownedInput = structuredClone(input);
|
|
147
|
+
return schedule(COMMAND_MODES[name], runtime => {
|
|
148
|
+
const ports = resolveTaskAdapter ? { resolveTaskAdapter: async task => {
|
|
149
|
+
const adapter = await resolveTaskAdapter(task, { signal: runtime.signal });
|
|
150
|
+
if (runtime.signal.aborted) throw aborted();
|
|
151
|
+
return adapter;
|
|
152
|
+
} } : {};
|
|
153
|
+
if (name.startsWith('creation.')) return createLocalCreationService(runtime, { resolveImageCandidates,
|
|
154
|
+
onPrepared: options?.onPrepared }).execute(name, ownedInput);
|
|
155
|
+
if (name.startsWith('flow.')) return createImageFlowService(runtime, { ...ports, resolveImageCandidates }).execute(name, ownedInput);
|
|
156
|
+
return createCommandService(runtime, ports).execute(name, ownedInput);
|
|
157
|
+
}, options);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function generateImage(input, options) {
|
|
161
|
+
const normalized = normalizeGenerationInput('image', input);
|
|
162
|
+
if (!resolveImageCandidates) {
|
|
163
|
+
throw new ApplicationRuntimeError('IRIS_APP_IMAGE_RESOLVER_MISSING', '图片生成需要显式提供候选解析函数');
|
|
164
|
+
}
|
|
165
|
+
return schedule('writer', async runtime => {
|
|
166
|
+
const candidates = await resolveImageCandidates({ modelRef: normalized.modelRef, signal: runtime.signal });
|
|
167
|
+
if (runtime.signal.aborted) throw aborted();
|
|
168
|
+
return createProviderTaskRunner(runtime).submit({ capability: 'image', candidates,
|
|
169
|
+
providerInput: normalized.providerInput, sourceArtifactId: normalized.sourceArtifactId });
|
|
170
|
+
}, options);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function dispose() {
|
|
174
|
+
if (disposePromise) return disposePromise;
|
|
175
|
+
state = 'closing';
|
|
176
|
+
stopAll();
|
|
177
|
+
disposePromise = Promise.all([...active].map(job => job.done)).then(() => {
|
|
178
|
+
if (cleanupFailure) throw cleanupFailure;
|
|
179
|
+
state = 'closed';
|
|
180
|
+
return snapshot();
|
|
181
|
+
});
|
|
182
|
+
return disposePromise;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return Object.freeze({ execute, generateImage, run, dispose, snapshot });
|
|
186
|
+
}
|