@mokuyoaxis/dsh-iris 0.2.6 → 0.3.0-alpha.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/CHANGELOG.md +103 -1
- package/README.en.md +104 -304
- package/README.md +101 -302
- package/bin/dsh-iris.js +446 -64
- package/docs/AGENT_CORE_QUICKSTART.md +64 -0
- package/docs/AGENT_QUICKSTART.md +7 -5
- package/docs/ARCHITECTURE.md +20 -4
- package/docs/AUDIO_FLOWS.md +71 -0
- package/docs/CLI_MANAGEMENT.md +1 -1
- package/docs/CONTACT_SHEET.md +62 -0
- package/docs/CORE_CONNECTIONS.md +99 -0
- package/docs/CORE_QUICKSTART.md +108 -0
- package/docs/CORE_RUNTIME_CONTRACT.md +41 -5
- package/docs/CORE_SERVICE.md +97 -0
- package/docs/CORE_SKILLS.md +94 -0
- package/docs/DSH_COMPATIBILITY.md +14 -4
- package/docs/DSH_CORE_MIGRATION.md +4 -2
- package/docs/DSH_PROVIDER_CONNECTIONS.md +1 -1
- package/docs/DSH_RC2_ADAPTATION.md +1 -1
- package/docs/DSH_RUNTIME.md +40 -0
- package/docs/FUNCTION_MODEL_POOLS.md +1 -1
- package/docs/HEADLESS_CLI.md +12 -4
- package/docs/HOST_DOCTOR.md +13 -1
- package/docs/IMAGE_FLOW.md +75 -0
- package/docs/IMAGE_GENERATION.md +127 -0
- package/docs/LOCAL_API.md +62 -0
- package/docs/LOCAL_API_CONTRACT.md +140 -0
- package/docs/LOCAL_CREATION.md +56 -0
- package/docs/LOCAL_OPERATIONS.md +46 -0
- package/docs/LOCAL_SELECTION.md +57 -0
- package/docs/LOCAL_WEB.md +201 -0
- package/docs/MCP.md +191 -0
- package/docs/MUSIC.md +2 -0
- package/docs/PANEL_EXTENSIONS.md +64 -0
- package/docs/PLUGINS.md +189 -0
- package/docs/PLUGIN_STARTUP.md +100 -0
- package/docs/PROVIDER_ADAPTER_CONTRACT.md +1 -1
- package/docs/PROVIDER_EXTENSIONS.md +78 -0
- package/docs/PROVIDER_HEALTH.md +4 -2
- package/docs/PROVIDER_SETTINGS.md +85 -0
- package/docs/PROVIDER_SUBMISSION_CONTRACT.md +6 -0
- package/docs/ROADMAP.md +28 -6
- package/docs/SECURITY.md +9 -1
- package/docs/SYNC_DELIVERY_RECOVERY.md +2 -0
- package/docs/VISION_CLI.md +2 -2
- package/docs/WEB_PROTOTYPE.md +62 -0
- package/docs/WEB_RECOVERY.md +46 -0
- package/docs/WORKBENCH_LOGIN.md +63 -0
- package/docs/assets/screenshots/core-connections-preview.webp +0 -0
- package/docs/assets/screenshots/core-model-pools-preview.webp +0 -0
- package/docs/assets/screenshots/core-overview-preview.webp +0 -0
- package/docs/assets/screenshots/core-spectrum-preview.webp +0 -0
- package/docs/assets/screenshots/core-studio-dark-preview.webp +0 -0
- package/docs/assets/screenshots/core-studio-mobile-preview.webp +0 -0
- package/docs/assets/screenshots/core-studio-preview.webp +0 -0
- package/docs/releases/0.2.7.md +62 -0
- package/docs/releases/0.3.0-alpha.1.md +51 -0
- package/docs/screenshots.md +41 -1
- package/lib/actions.js +132 -70
- package/lib/adapters.js +20 -11
- package/lib/api.js +38 -38
- package/lib/application-runtime.js +213 -0
- package/lib/audio-flow-input.js +37 -0
- package/lib/client.js +434 -55
- package/lib/comfyui-adapter.js +94 -0
- package/lib/comfyui-client.js +51 -0
- package/lib/comfyui-inspection.js +73 -0
- package/lib/comfyui-workflow.js +94 -0
- package/lib/config.js +36 -1
- package/lib/configured-image-runtime.js +50 -0
- package/lib/connection-skill.js +89 -0
- package/lib/connection-template.js +23 -0
- package/lib/core-artifact-store.js +21 -2
- package/lib/core-bridge-selection.js +31 -0
- package/lib/core-bridge-tools.js +83 -0
- package/lib/core-capability-catalog.js +182 -0
- package/lib/core-connection-diagnostics.js +49 -0
- package/lib/core-connection-store.js +277 -0
- package/lib/core-connections.js +225 -0
- package/lib/core-maintenance.js +31 -2
- package/lib/core-runtime.js +20 -0
- package/lib/core-service-client.js +170 -0
- package/lib/core-tasks.js +84 -13
- package/lib/core-writer-lease.js +16 -0
- package/lib/creation-request-store.js +133 -0
- package/lib/doctor.js +12 -1
- package/lib/dsh-core-adapter.js +666 -572
- package/lib/dsh-host-adapter.js +8 -2
- package/lib/dsh-service-bridge.js +66 -0
- package/lib/execution-ownership.js +43 -0
- package/lib/flow-store.js +290 -0
- package/lib/generation-input.js +10 -1
- package/lib/headless-vision.js +7 -7
- package/lib/image-edit-input.js +1 -1
- package/lib/image-flow.js +232 -0
- package/lib/index.js +94 -68
- package/lib/local-api-projection.js +32 -0
- package/lib/local-api-token-directory.js +31 -0
- package/lib/local-api.js +1083 -0
- package/lib/local-creation.js +150 -0
- package/lib/local-operations.js +36 -0
- package/lib/local-request-root.js +86 -0
- package/lib/local-web-assets.js +37 -0
- package/lib/mcp-bridge.js +58 -0
- package/lib/mcp-check.js +26 -0
- package/lib/mcp-config.js +42 -0
- package/lib/mcp-delivery.js +46 -0
- package/lib/mcp-http.js +101 -0
- package/lib/mcp-server.js +47 -0
- package/lib/model-directory-service.js +16 -6
- package/lib/model-directory.js +3 -3
- package/lib/models.js +2 -2
- package/lib/music-transport.js +2 -1
- package/lib/operation-runner.js +236 -0
- package/lib/operation-store.js +216 -0
- package/lib/panel-extension.js +36 -0
- package/lib/plugin-contract.js +194 -0
- package/lib/plugin-execution.js +51 -0
- package/lib/plugin-launch.js +86 -0
- package/lib/plugin-operation-store.js +101 -0
- package/lib/plugin-operations.js +28 -0
- package/lib/plugin-registry.js +109 -0
- package/lib/plugin-selection.js +15 -0
- package/lib/plugin-source-image.js +5 -0
- package/lib/plugin-web-assets.js +54 -0
- package/lib/plugins.d.ts +288 -0
- package/lib/plugins.js +58 -0
- package/lib/private-storage.js +12 -0
- package/lib/provider-adapter.js +5 -1
- package/lib/provider-adapters.js +56 -18
- package/lib/provider-catalog.js +40 -29
- package/lib/provider-config-service.js +105 -23
- package/lib/provider-diagnostics.js +56 -0
- package/lib/provider-extensions.js +29 -0
- package/lib/provider-health.js +8 -1
- package/lib/provider-monitor.js +79 -0
- package/lib/provider-protocol.js +70 -20
- package/lib/provider-registry.js +118 -0
- package/lib/provider-response.js +28 -0
- package/lib/provider-settings.js +134 -0
- package/lib/provider-task-runner.js +40 -7
- package/lib/render-address.js +35 -0
- package/lib/render.js +52 -13
- package/lib/skill-installation.js +211 -0
- package/lib/vision-model-routing.js +3 -0
- package/lib/web/app.js +642 -0
- package/lib/web/appearance.js +111 -0
- package/lib/web/comfyui.js +146 -0
- package/lib/web/connections.js +377 -0
- package/lib/web/creation.js +355 -0
- package/lib/web/flow-view.js +33 -0
- package/lib/web/gallery.js +132 -0
- package/lib/web/index.html +291 -0
- package/lib/web/media-client.js +204 -0
- package/lib/web/plugin-panel.js +221 -0
- package/lib/web/plugin-surface.css +17 -0
- package/lib/web/providers.js +492 -0
- package/lib/web/recovery.js +105 -0
- package/lib/web/startup.js +18 -0
- package/lib/web/styles.css +518 -0
- package/lib/web/transport.js +96 -0
- package/lib/web/workbench-login.js +165 -0
- package/lib/workbench-artifacts.js +1 -2
- package/lib/workbench-auth-cli.js +47 -0
- package/lib/workbench-auth.js +200 -0
- package/lib/workbench-management.js +8 -9
- package/package.json +22 -6
- package/plugins/contact-sheet/index.d.mts +4 -0
- package/plugins/contact-sheet/index.mjs +33 -0
- package/plugins/contact-sheet/web/board.css +14 -0
- package/plugins/contact-sheet/web/board.mjs +93 -0
- package/skills/iris-core/SKILL.md +44 -0
- package/skills/iris-core/agents/openai.yaml +4 -0
- package/skills/iris-core/references/cli.md +95 -0
- package/skills/iris-core/references/comfyui.md +69 -0
- package/skills/iris-core/references/tasks.md +60 -0
- package/user_guide.md +19 -9
package/lib/adapters.js
CHANGED
|
@@ -15,6 +15,7 @@ import { dashscopeApiBase, imageSizeViolation, isDashScopeBaseUrl, normalizeImag
|
|
|
15
15
|
import { chmodPrivateFile, ensurePrivateDir, privateSibling } from './private-storage.js';
|
|
16
16
|
import { isFreeQuotaExhausted, retryAfterDelay } from './provider-health.js';
|
|
17
17
|
import { ProviderContractError } from './provider-contract.js';
|
|
18
|
+
import { boundedProviderBody, readProviderText, PROVIDER_RESPONSE_LIMITS } from './provider-response.js';
|
|
18
19
|
export { generateMusic } from './music-transport.js';
|
|
19
20
|
|
|
20
21
|
/**
|
|
@@ -42,8 +43,8 @@ function authHeaders(key) {
|
|
|
42
43
|
};
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
async function readJson(res) {
|
|
46
|
-
const text = await res
|
|
46
|
+
async function readJson(res, maxBytes = PROVIDER_RESPONSE_LIMITS.json) {
|
|
47
|
+
const text = await readProviderText(res, { maxBytes });
|
|
47
48
|
try {
|
|
48
49
|
return text ? JSON.parse(text) : null;
|
|
49
50
|
} catch (_) {
|
|
@@ -149,8 +150,9 @@ async function fetchImageJson(url, options) {
|
|
|
149
150
|
let response;
|
|
150
151
|
try {
|
|
151
152
|
response = await fetch(url, options);
|
|
152
|
-
return { res: response, body: await readJson(response) };
|
|
153
|
+
return { res: response, body: await readJson(response, response.ok ? PROVIDER_RESPONSE_LIMITS.mediaJson : PROVIDER_RESPONSE_LIMITS.json) };
|
|
153
154
|
} catch (err) {
|
|
155
|
+
if (err instanceof ProviderContractError) throw err;
|
|
154
156
|
throw transportFetchError(err, response);
|
|
155
157
|
}
|
|
156
158
|
}
|
|
@@ -287,7 +289,11 @@ export async function submitVideo({ key, baseUrl, model, prompt, imgDataUrl, siz
|
|
|
287
289
|
* 返回 oss:// 形式 URL;调用模型时请求头须带 X-DashScope-OssResourceResolve: enable
|
|
288
290
|
* (submitVideo 的 s2v 分支已自动处理)。
|
|
289
291
|
*/
|
|
290
|
-
export async function uploadTempFile({ key, baseUrl, model, filePath, timeoutMs = T_UPLOAD, signal }) {
|
|
292
|
+
export async function uploadTempFile({ key, baseUrl, model, filePath, bytes, filename, timeoutMs = T_UPLOAD, signal }) {
|
|
293
|
+
if ((filePath === undefined) === (bytes === undefined)) throw new TypeError('上传须且只能提供本地路径或明确字节');
|
|
294
|
+
const name = filePath !== undefined ? path.basename(filePath) : filename;
|
|
295
|
+
if (typeof name !== 'string' || !name || name.length > 128 || /[\/\\\0]/.test(name)
|
|
296
|
+
|| bytes !== undefined && (!(bytes instanceof Uint8Array) || !bytes.length)) throw new TypeError('上传字节或文件名无效');
|
|
291
297
|
const apiBase = dashscopeApiBase(baseUrl);
|
|
292
298
|
const u = new URL(`${apiBase}/uploads`);
|
|
293
299
|
u.searchParams.set('action', 'getPolicy');
|
|
@@ -296,7 +302,6 @@ export async function uploadTempFile({ key, baseUrl, model, filePath, timeoutMs
|
|
|
296
302
|
const pbody = await readJson(pres);
|
|
297
303
|
if (!pres.ok || !pbody || !pbody.data) throw providerError(pres.status, pbody, '获取上传凭证失败', pres.headers);
|
|
298
304
|
const pol = pbody.data;
|
|
299
|
-
const name = path.basename(filePath);
|
|
300
305
|
const keyPath = `${pol.upload_dir}/${Date.now()}-${name}`;
|
|
301
306
|
const fd = new FormData();
|
|
302
307
|
fd.append('OSSAccessKeyId', pol.oss_access_key_id);
|
|
@@ -306,7 +311,7 @@ export async function uploadTempFile({ key, baseUrl, model, filePath, timeoutMs
|
|
|
306
311
|
fd.append('x-oss-forbid-overwrite', pol.x_oss_forbid_overwrite);
|
|
307
312
|
fd.append('key', keyPath);
|
|
308
313
|
fd.append('success_action_status', '200');
|
|
309
|
-
fd.append('file', new Blob([fs.readFileSync(filePath)]), name);
|
|
314
|
+
fd.append('file', new Blob([bytes ?? fs.readFileSync(filePath)]), name);
|
|
310
315
|
const up = await fetch(pol.upload_host, { method: 'POST', body: fd, signal: timeoutSignal(timeoutMs, signal) });
|
|
311
316
|
if (up.status !== 200) throw new Error(`临时上传失败:HTTP ${up.status}`);
|
|
312
317
|
return 'oss://' + keyPath;
|
|
@@ -366,7 +371,7 @@ export async function synthesizeTts({ key, baseUrl, model = 'qwen-tts-latest', t
|
|
|
366
371
|
signal: timeoutSignal(timeoutMs, signal),
|
|
367
372
|
body: JSON.stringify({ model, input: { text, voice } })
|
|
368
373
|
});
|
|
369
|
-
const body = await readJson(res);
|
|
374
|
+
const body = await readJson(res, res.ok ? PROVIDER_RESPONSE_LIMITS.mediaJson : PROVIDER_RESPONSE_LIMITS.json);
|
|
370
375
|
if (!res.ok) {
|
|
371
376
|
const error = providerError(res.status, body, '合成失败', res.headers);
|
|
372
377
|
if (res.status === 404 && (/NotFound\.Model|model_not_found/i.test(error.providerCode)
|
|
@@ -614,7 +619,7 @@ export async function visionStream({ key, baseUrl, model = 'qwen-vl-plus', promp
|
|
|
614
619
|
throw networkError(err); // 网络层失败归类为 network
|
|
615
620
|
}
|
|
616
621
|
if (!res.ok) {
|
|
617
|
-
const text = await res.
|
|
622
|
+
const text = await readProviderText(res, { maxBytes: PROVIDER_RESPONSE_LIMITS.json, stage: 'response', acceptance: 'unknown' });
|
|
618
623
|
let body = null;
|
|
619
624
|
try { body = JSON.parse(text); } catch (_) { /* keep null */ }
|
|
620
625
|
throw providerError(res.status, body, '视觉理解失败', res.headers);
|
|
@@ -655,15 +660,19 @@ export function extFromUrl(url, fallback = 'bin') {
|
|
|
655
660
|
return fallback;
|
|
656
661
|
}
|
|
657
662
|
|
|
658
|
-
export async function downloadTo(url, filePath, { timeoutMs = T_DOWNLOAD, signal } = {}) {
|
|
663
|
+
export async function downloadTo(url, filePath, { timeoutMs = T_DOWNLOAD, signal, maxBytes = PROVIDER_RESPONSE_LIMITS.video } = {}) {
|
|
659
664
|
const res = await fetch(url, { signal: timeoutSignal(timeoutMs, signal) });
|
|
660
|
-
if (!res.ok)
|
|
665
|
+
if (!res.ok) {
|
|
666
|
+
await res.body?.cancel();
|
|
667
|
+
throw Object.assign(new Error(`下载结果失败:HTTP ${res.status}`), { stage: 'download', category: 'provider', httpStatus: res.status, acceptance: 'accepted' });
|
|
668
|
+
}
|
|
661
669
|
if (!res.body) throw new Error('下载结果失败:响应没有内容');
|
|
662
670
|
ensurePrivateDir(path.dirname(filePath));
|
|
663
671
|
const tmp = privateSibling(filePath, 'download');
|
|
664
672
|
try {
|
|
665
673
|
const output = fs.createWriteStream(tmp, { flags: 'wx', mode: 0o600 });
|
|
666
|
-
|
|
674
|
+
const closed = new Promise(resolve => output.once('close', resolve));
|
|
675
|
+
await pipeline(Readable.from(boundedProviderBody(res, { maxBytes, stage: 'download', acceptance: 'accepted' })), output).finally(() => closed);
|
|
667
676
|
const bytes = fs.statSync(tmp).size;
|
|
668
677
|
fs.renameSync(tmp, filePath);
|
|
669
678
|
chmodPrivateFile(filePath);
|
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 {
|
|
@@ -332,16 +331,17 @@ function serveSse(res) {
|
|
|
332
331
|
}
|
|
333
332
|
|
|
334
333
|
/** webServer 前缀路由 handler:状态、任务、动作与提示词优化器 API。 */
|
|
335
|
-
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' });
|
|
336
336
|
try {
|
|
337
337
|
const method = String(req.method || 'GET').toUpperCase();
|
|
338
338
|
const urlPath = String(req.url || '').split('?')[0];
|
|
339
339
|
|
|
340
340
|
// POST /iris/api/actions/:name —— GUI 直连触发工具执行
|
|
341
341
|
if (method === 'POST') {
|
|
342
|
-
if (urlPath === '/iris/api/works/download') return handleWorkbenchPost(req, res, downloadWorkbenchSelection, true);
|
|
343
|
-
if (urlPath === '/iris/api/works/delete') return handleWorkbenchPost(req, res, deleteWorkbenchArtifacts);
|
|
344
|
-
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);
|
|
345
345
|
if (urlPath === '/iris/api/host-client') {
|
|
346
346
|
if (!host || typeof host !== 'object') return sendJson(res, 503, { error: 'host unavailable' });
|
|
347
347
|
return handleJsonPost(req, res, (body) => recordHostClient(body));
|
|
@@ -352,16 +352,16 @@ export function serveApi(req, res, host) {
|
|
|
352
352
|
}
|
|
353
353
|
// D4 人工 retry as new task:产生新的真实计费,请求体必须显式 confirmBilling:true
|
|
354
354
|
const coreTaskRetry = urlPath.match(/^\/iris\/api\/core\/task\/([^/]+)\/retry\/?$/);
|
|
355
|
-
if (coreTaskRetry) return handleCoreTaskRetry(coreTaskRetry[1], req, res);
|
|
355
|
+
if (coreTaskRetry) return handleCoreTaskRetry(coreTaskRetry[1], req, res, core);
|
|
356
356
|
// D1/D2/D3 人工动作:reobserve 显式单步 poll 一次;redeliver 只对 succeeded+failed
|
|
357
357
|
// 重新取回产物;cancel 只有 Provider 明确确认才写已取消。三者绝不 submit。
|
|
358
358
|
const coreTaskAction = urlPath.match(/^\/iris\/api\/core\/task\/([^/]+)\/(reobserve|redeliver|cancel|acknowledge|restore|hide|unhide)\/?$/);
|
|
359
|
-
if (coreTaskAction) return handleCoreTaskManual(coreTaskAction[2], coreTaskAction[1], req, res);
|
|
359
|
+
if (coreTaskAction) return handleCoreTaskManual(coreTaskAction[2], coreTaskAction[1], req, res, core);
|
|
360
360
|
const promptMatch = typeof host === 'object' && urlPath.match(/^\/iris\/api\/prompt-optimizer\/(optimize|import|reset|enabled)\/?$/);
|
|
361
|
-
if (promptMatch) return handlePromptOptimizer(host, promptMatch[1], req, res);
|
|
361
|
+
if (promptMatch) return handlePromptOptimizer(host, promptMatch[1], req, res, core);
|
|
362
362
|
const actionMatch = typeof host === 'object' && urlPath.match(/^\/iris\/api\/actions\/([A-Za-z0-9_-]+)\/?$/);
|
|
363
363
|
if (!actionMatch) { return sendJson(res, 405, { error: 'method not allowed' }); }
|
|
364
|
-
return handleAction(host, actionMatch[1], req, res);
|
|
364
|
+
return handleAction(host, actionMatch[1], req, res, core);
|
|
365
365
|
}
|
|
366
366
|
// GET/HEAD 原逻辑
|
|
367
367
|
if (method !== 'GET' && method !== 'HEAD') {
|
|
@@ -382,16 +382,16 @@ export function serveApi(req, res, host) {
|
|
|
382
382
|
if (urlPath === '/iris/api/core/snapshot') {
|
|
383
383
|
if (method === 'HEAD') return sendJson(res, 200, {});
|
|
384
384
|
const query = new URL(req.url || '', 'http://iris.local').searchParams;
|
|
385
|
-
return serveCoreSnapshot(res, { limit: Number(query.get('limit') || 12) });
|
|
385
|
+
return serveCoreSnapshot(res, { limit: Number(query.get('limit') || 12) }, core);
|
|
386
386
|
}
|
|
387
387
|
if (urlPath === '/iris/api/works') {
|
|
388
|
-
return serveWorkbenchArtifacts(req, method, res);
|
|
388
|
+
return serveWorkbenchArtifacts(req, method, res, core);
|
|
389
389
|
}
|
|
390
|
-
if (urlPath === '/iris/api/works/transactions') return serveWorkbenchRead(res, method, () => workbenchTransactions());
|
|
390
|
+
if (urlPath === '/iris/api/works/transactions') return serveWorkbenchRead(res, method, () => workbenchTransactions({ core }));
|
|
391
391
|
const workDetailMatch = urlPath.match(/^\/iris\/api\/works\/(core|legacy)\/([^/]+)$/);
|
|
392
|
-
if (workDetailMatch) return serveWorkbenchRead(res, method, () => workbenchDetail(workDetailMatch[1], workDetailMatch[2]));
|
|
392
|
+
if (workDetailMatch) return serveWorkbenchRead(res, method, () => workbenchDetail(workDetailMatch[1], workDetailMatch[2], { core }));
|
|
393
393
|
const coreMediaMatch = urlPath.match(/^\/iris\/api\/core\/artifact\/(artifact_[a-f0-9]{24})\/media$/);
|
|
394
|
-
if (coreMediaMatch) return serveCoreArtifactMedia(coreMediaMatch[1], method, res);
|
|
394
|
+
if (coreMediaMatch) return serveCoreArtifactMedia(coreMediaMatch[1], method, res, core);
|
|
395
395
|
if (urlPath === '/iris/api/artifacts') {
|
|
396
396
|
const query = new URL(req.url || '', 'http://iris.local').searchParams;
|
|
397
397
|
const page = artifacts.page({ offset: Number(query.get('offset') || 0), limit: Number(query.get('limit') || 24) });
|
|
@@ -417,11 +417,11 @@ export function serveApi(req, res, host) {
|
|
|
417
417
|
}
|
|
418
418
|
}
|
|
419
419
|
|
|
420
|
-
async function serveWorkbenchArtifacts(req, method, res) {
|
|
420
|
+
async function serveWorkbenchArtifacts(req, method, res, core) {
|
|
421
421
|
const params = new URL(req.url || '', 'http://iris.local').searchParams;
|
|
422
422
|
const input = Object.fromEntries(params);
|
|
423
423
|
for (const key of ['offset', 'limit']) if (input[key] !== undefined) input[key] = Number(input[key]);
|
|
424
|
-
try { return sendJson(res, 200, method === 'HEAD' ? {} : await workbenchArtifacts(input)); }
|
|
424
|
+
try { return sendJson(res, 200, method === 'HEAD' ? {} : await workbenchArtifacts(input, { core })); }
|
|
425
425
|
catch (error) { return sendJson(res, error.statusCode || 500, { error: '作品读取失败', code: error.code || 'IRIS_GALLERY_FAILED' }); }
|
|
426
426
|
}
|
|
427
427
|
|
|
@@ -430,9 +430,9 @@ async function serveWorkbenchRead(res, method, execute) {
|
|
|
430
430
|
catch (error) { const safe = workbenchError(error); return sendJson(res, safe.statusCode || 400, { error: safe.message, code: safe.code }); }
|
|
431
431
|
}
|
|
432
432
|
|
|
433
|
-
function handleWorkbenchPost(req, res, execute, download = false) {
|
|
433
|
+
function handleWorkbenchPost(req, res, execute, download = false, core) {
|
|
434
434
|
return handleJsonPost(req, res, async (body, signal) => {
|
|
435
|
-
try { return await execute(body, { signal }); }
|
|
435
|
+
try { return await core.runHostOperation(operationSignal => execute(body, { signal: operationSignal, core }), { signal }); }
|
|
436
436
|
catch (error) { throw workbenchError(error); }
|
|
437
437
|
}, download ? (response, _status, bytes) => {
|
|
438
438
|
if (response.writableEnded || response.destroyed) return;
|
|
@@ -443,9 +443,9 @@ function handleWorkbenchPost(req, res, execute, download = false) {
|
|
|
443
443
|
} : sendJson);
|
|
444
444
|
}
|
|
445
445
|
|
|
446
|
-
async function serveCoreArtifactMedia(artifactId, method, res) {
|
|
446
|
+
async function serveCoreArtifactMedia(artifactId, method, res, core) {
|
|
447
447
|
try {
|
|
448
|
-
const result = await readCoreArtifactMediaForDsh(artifactId);
|
|
448
|
+
const result = await core.readCoreArtifactMediaForDsh(artifactId);
|
|
449
449
|
if (res.writableEnded || res.destroyed) return;
|
|
450
450
|
res.writeHead(200, {
|
|
451
451
|
'Content-Type': result.artifact.mediaType,
|
|
@@ -462,9 +462,9 @@ async function serveCoreArtifactMedia(artifactId, method, res) {
|
|
|
462
462
|
}
|
|
463
463
|
}
|
|
464
464
|
|
|
465
|
-
async function serveCoreSnapshot(res, options = {}) {
|
|
465
|
+
async function serveCoreSnapshot(res, options = {}, core) {
|
|
466
466
|
try {
|
|
467
|
-
return sendJson(res, 200, await coreSnapshotForDsh({ limit: options.limit || 12 }));
|
|
467
|
+
return sendJson(res, 200, await core.coreSnapshotForDsh({ limit: options.limit || 12 }));
|
|
468
468
|
} catch (error) {
|
|
469
469
|
console.error('[iris] Core 只读快照失败:', error && error.code || 'unknown');
|
|
470
470
|
return sendJson(res, 409, {
|
|
@@ -527,8 +527,8 @@ function handleJsonPost(req, res, execute, respond = sendJson) {
|
|
|
527
527
|
req.on('error', () => finish(400, { error: 'bad request' }));
|
|
528
528
|
}
|
|
529
529
|
|
|
530
|
-
function handleAction(host, name, req, res) {
|
|
531
|
-
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 }));
|
|
532
532
|
}
|
|
533
533
|
|
|
534
534
|
/**
|
|
@@ -539,14 +539,14 @@ function handleAction(host, name, req, res) {
|
|
|
539
539
|
* 经 AbortSignal 传入在途操作,与 GUI actions 同一约定。
|
|
540
540
|
*/
|
|
541
541
|
const CORE_TASK_MANUAL_ACTIONS = Object.freeze({
|
|
542
|
-
reobserve: { command: 'task.reobserve', run: reobserveProviderTaskForDsh },
|
|
543
|
-
redeliver: { command: 'task.redeliver', run: redeliverProviderTaskForDsh },
|
|
544
|
-
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) },
|
|
545
545
|
// 注意力处置是 Host 偏好(不写 Core 事实):acknowledge/restore/hide/unhide。
|
|
546
|
-
acknowledge: { command: 'attention.acknowledge', run: (input) => disposeCoreTaskAttention({ ...input, action: 'acknowledge' }) },
|
|
547
|
-
restore: { command: 'attention.restore', run: (input) => disposeCoreTaskAttention({ ...input, action: 'restore' }) },
|
|
548
|
-
hide: { command: 'attention.hide', run: (input) => disposeCoreTaskAttention({ ...input, action: 'hide' }) },
|
|
549
|
-
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' }) }
|
|
550
550
|
});
|
|
551
551
|
|
|
552
552
|
/**
|
|
@@ -555,7 +555,7 @@ const CORE_TASK_MANUAL_ACTIONS = Object.freeze({
|
|
|
555
555
|
* Prompt,只能由调用方重新提供;缺显式计费确认在读取旧 Task 之前拒绝。
|
|
556
556
|
* 成功返回 200 + 新任务的五类用户投影行与 retriedFrom 关系。
|
|
557
557
|
*/
|
|
558
|
-
function handleCoreTaskRetry(taskId, req, res) {
|
|
558
|
+
function handleCoreTaskRetry(taskId, req, res, core) {
|
|
559
559
|
if (!/^task_[a-f0-9]{24}$/.test(taskId)) {
|
|
560
560
|
return sendJson(res, 400, { error: { code: 'IRIS_DSH_TASK_INVALID', message: 'Core Task ID 无效' } });
|
|
561
561
|
}
|
|
@@ -568,7 +568,7 @@ function handleCoreTaskRetry(taskId, req, res) {
|
|
|
568
568
|
throw error;
|
|
569
569
|
}
|
|
570
570
|
const prompt = String(body.prompt || '');
|
|
571
|
-
const result = await retryProviderTaskForDsh({
|
|
571
|
+
const result = await core.retryProviderTaskForDsh({
|
|
572
572
|
taskId,
|
|
573
573
|
prompt,
|
|
574
574
|
...(body.source_artifact_id !== undefined ? { sourceArtifactId: body.source_artifact_id } : {}),
|
|
@@ -597,7 +597,7 @@ function handleCoreTaskRetry(taskId, req, res) {
|
|
|
597
597
|
});
|
|
598
598
|
}
|
|
599
599
|
|
|
600
|
-
function handleCoreTaskManual(action, taskId, req, res) {
|
|
600
|
+
function handleCoreTaskManual(action, taskId, req, res, core) {
|
|
601
601
|
if (!/^task_[a-f0-9]{24}$/.test(taskId)) {
|
|
602
602
|
return sendJson(res, 400, { error: { code: 'IRIS_DSH_TASK_INVALID', message: 'Core Task ID 无效' } });
|
|
603
603
|
}
|
|
@@ -614,7 +614,7 @@ function handleCoreTaskManual(action, taskId, req, res) {
|
|
|
614
614
|
if (typeof req.on === 'function') req.on('aborted', disconnect);
|
|
615
615
|
if (typeof res.on === 'function') res.on('close', disconnect);
|
|
616
616
|
if (typeof req.resume === 'function') req.resume(); // 无 body 入参;避免小请求体滞留
|
|
617
|
-
entry.run({ taskId, signal: controller.signal })
|
|
617
|
+
entry.run(core, { taskId, signal: controller.signal })
|
|
618
618
|
.then((result) => finish(200, {
|
|
619
619
|
ok: true,
|
|
620
620
|
contractVersion: 0,
|
|
@@ -631,8 +631,8 @@ function handleCoreTaskManual(action, taskId, req, res) {
|
|
|
631
631
|
});
|
|
632
632
|
}
|
|
633
633
|
|
|
634
|
-
function handlePromptOptimizer(host, operation, req, res) {
|
|
635
|
-
return handleJsonPost(req, res,
|
|
634
|
+
function handlePromptOptimizer(host, operation, req, res, core) {
|
|
635
|
+
return handleJsonPost(req, res, (body, requestSignal) => core.runHostOperation(async signal => {
|
|
636
636
|
if (operation === 'optimize') return optimizePrompt(host, body, { signal });
|
|
637
637
|
if (operation === 'import') {
|
|
638
638
|
const imported = body && body.config !== undefined ? body.config : body;
|
|
@@ -645,7 +645,7 @@ function handlePromptOptimizer(host, operation, req, res) {
|
|
|
645
645
|
}
|
|
646
646
|
if (operation === 'enabled') return { ok: true, ...setPromptOptimizerEnabled(body && body.enabled), capabilities: PROMPT_CONFIG_CAPABILITIES };
|
|
647
647
|
throw new Error('unknown prompt optimizer operation');
|
|
648
|
-
});
|
|
648
|
+
}, { signal: requestSignal }));
|
|
649
649
|
}
|
|
650
650
|
|
|
651
651
|
/* ---------- 文件选择器 L2:本地文件上传(阶段 10) ---------- */
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { normalizeCoreOptions } from './core-contract.js';
|
|
4
|
+
import { createCoreRuntime, inspectCoreWriterLease } 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
|
+
import { createLocalOperationService } from './local-operations.js';
|
|
11
|
+
import { reservePluginOperation } from './plugin-operations.js';
|
|
12
|
+
import { invokeReservedPlugin } from './plugin-execution.js';
|
|
13
|
+
|
|
14
|
+
// Internal first-slice execution surface, not a public API or an HTTP allowlist.
|
|
15
|
+
const COMMAND_MODES = Object.freeze({
|
|
16
|
+
'artifact.import': 'writer', 'artifact.export': 'writer', 'media.diff': 'writer',
|
|
17
|
+
'task.observe': 'writer', 'task.reobserve': 'writer', 'task.redeliver': 'writer', 'task.cancel': 'writer',
|
|
18
|
+
'artifact.inspect': 'reader', 'artifact.list': 'reader', 'task.inspect': 'reader', 'task.list': 'reader',
|
|
19
|
+
'flow.init': 'writer', 'flow.create': 'writer', 'flow.continue': 'writer', 'flow.select': 'writer', 'flow.export': 'writer',
|
|
20
|
+
'flow.inspect': 'reader', 'flow.list': 'reader',
|
|
21
|
+
'creation.init': 'writer', 'creation.image': 'writer', 'creation.flow': 'writer', 'creation.generate': 'writer',
|
|
22
|
+
'creation.audio': 'writer', 'creation.speech': 'writer', 'creation.transcribe': 'writer',
|
|
23
|
+
'creation.root': 'reader', 'creation.lookup': 'reader',
|
|
24
|
+
'execution.init': 'writer', 'operation.accept': 'writer', 'operation.transition': 'writer',
|
|
25
|
+
'operation.inspect': 'reader', 'operation.lookup': 'reader', 'operation.list': 'reader'
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export class ApplicationRuntimeError extends Error {
|
|
29
|
+
constructor(code, message) { super(message); this.name = 'ApplicationRuntimeError'; this.code = code; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function aborted() {
|
|
33
|
+
return Object.assign(new ApplicationRuntimeError('IRIS_APP_ABORTED', 'Iris 操作已取消'), { name: 'AbortError' });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function closed() {
|
|
37
|
+
return new ApplicationRuntimeError('IRIS_APP_CLOSED', 'Iris 应用运行时正在关闭或已关闭');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Instance-owned FIFO and operation-scoped Core leases. No I/O until an explicit operation. */
|
|
41
|
+
export function createApplicationRuntime({ dataRoot, resolveImageCandidates, resolveAudioCandidates, resolveTaskAdapter } = {}) {
|
|
42
|
+
const root = normalizeCoreOptions({ dataRoot }).dataRoot;
|
|
43
|
+
if (resolveImageCandidates !== undefined && typeof resolveImageCandidates !== 'function') {
|
|
44
|
+
throw new TypeError('resolveImageCandidates 必须是显式候选解析函数');
|
|
45
|
+
}
|
|
46
|
+
if (resolveTaskAdapter !== undefined && typeof resolveTaskAdapter !== 'function') {
|
|
47
|
+
throw new TypeError('resolveTaskAdapter 必须是显式 Task Provider 解析函数');
|
|
48
|
+
}
|
|
49
|
+
if (resolveAudioCandidates !== undefined && typeof resolveAudioCandidates !== 'function') throw new TypeError('音频候选解析须为函数');
|
|
50
|
+
let state = 'open';
|
|
51
|
+
const queue = [];
|
|
52
|
+
const active = new Set();
|
|
53
|
+
let writer = null;
|
|
54
|
+
let disposePromise;
|
|
55
|
+
let cleanupFailure;
|
|
56
|
+
|
|
57
|
+
function snapshot() {
|
|
58
|
+
return Object.freeze({ state, queuedWrites: queue.length, activeWrites: Number(Boolean(writer)),
|
|
59
|
+
activeReaders: active.size - Number(Boolean(writer)), cleanupFailed: Boolean(cleanupFailure) });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function detach(job) { job.signal?.removeEventListener('abort', job.onAbort); }
|
|
63
|
+
|
|
64
|
+
function rejectQueued(job, error) {
|
|
65
|
+
detach(job);
|
|
66
|
+
job.reject(error);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function stop(job) {
|
|
70
|
+
job.aborted = true;
|
|
71
|
+
if (job.runtime && !job.disposal) {
|
|
72
|
+
job.disposal = job.runtime.dispose();
|
|
73
|
+
// The operation awaits this same promise below and reports any cleanup error.
|
|
74
|
+
job.disposal.catch(() => {});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function stopAll() {
|
|
79
|
+
for (const job of queue.splice(0)) rejectQueued(job, closed());
|
|
80
|
+
for (const job of active) stop(job);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function perform(job) {
|
|
84
|
+
if (job.aborted) throw aborted();
|
|
85
|
+
const runtime = createCoreRuntime({ dataRoot: root, mode: job.mode });
|
|
86
|
+
job.runtime = runtime;
|
|
87
|
+
let result, operationError, failed = false;
|
|
88
|
+
try {
|
|
89
|
+
runtime.start();
|
|
90
|
+
result = await job.action(runtime);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
failed = true; operationError = error;
|
|
93
|
+
if (job.mode === 'reader' && error?.code === 'IRIS_ARTIFACT_INDEX_INVALID' && error.missingFile === true) {
|
|
94
|
+
const lease = inspectCoreWriterLease(root);
|
|
95
|
+
if (lease.status === 'present' && lease.ownerStatus === 'alive') {
|
|
96
|
+
operationError = new ApplicationRuntimeError('IRIS_CORE_DATA_ROOT_BUSY', '作品读取遇到活动写入,请在写入结束后显式重试');
|
|
97
|
+
Object.defineProperty(operationError, 'cause', { value: error });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
try { await (job.disposal || runtime.dispose()); }
|
|
102
|
+
catch (error) {
|
|
103
|
+
cleanupFailure ||= error;
|
|
104
|
+
state = 'closing';
|
|
105
|
+
stopAll();
|
|
106
|
+
if (failed) {
|
|
107
|
+
throw Object.assign(new AggregateError([operationError, error], 'Iris 操作失败且运行时未能安全释放'), { code: error.code });
|
|
108
|
+
}
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
if (failed) throw operationError;
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function start(job) {
|
|
116
|
+
active.add(job);
|
|
117
|
+
job.done = Promise.resolve().then(async () => {
|
|
118
|
+
let result, error, failed = false;
|
|
119
|
+
try { result = await perform(job); }
|
|
120
|
+
catch (caught) { failed = true; error = caught; }
|
|
121
|
+
detach(job);
|
|
122
|
+
active.delete(job);
|
|
123
|
+
if (writer === job) writer = null;
|
|
124
|
+
if (failed) job.reject(error); else job.resolve(result);
|
|
125
|
+
dispatch();
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function dispatch() {
|
|
130
|
+
if (state !== 'open' || writer || !queue.length) return;
|
|
131
|
+
writer = queue.shift();
|
|
132
|
+
start(writer);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function schedule(mode, action, { signal } = {}) {
|
|
136
|
+
if (signal !== undefined && !(signal instanceof AbortSignal)) throw new TypeError('signal 必须是 AbortSignal');
|
|
137
|
+
if (state !== 'open') return Promise.reject(closed());
|
|
138
|
+
if (signal?.aborted) return Promise.reject(aborted());
|
|
139
|
+
let job;
|
|
140
|
+
const result = new Promise((resolve, reject) => { job = { mode, action, signal, resolve, reject, aborted: false }; });
|
|
141
|
+
job.onAbort = () => {
|
|
142
|
+
const index = queue.indexOf(job);
|
|
143
|
+
if (index >= 0) { queue.splice(index, 1); rejectQueued(job, aborted()); }
|
|
144
|
+
else stop(job);
|
|
145
|
+
};
|
|
146
|
+
signal?.addEventListener('abort', job.onAbort, { once: true });
|
|
147
|
+
if (mode === 'writer') { queue.push(job); dispatch(); }
|
|
148
|
+
else start(job);
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Trusted in-process Host composition; never an external command/HTTP dispatch surface. */
|
|
153
|
+
function run(mode, action, options) {
|
|
154
|
+
if (!['reader', 'writer'].includes(mode)) throw new TypeError('Host 操作需要明确 reader/writer 模式');
|
|
155
|
+
if (typeof action !== 'function') throw new TypeError('Host 操作必须是函数');
|
|
156
|
+
return schedule(mode, action, options);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function execute(name, input, options) {
|
|
160
|
+
if (!Object.hasOwn(COMMAND_MODES, name)) {
|
|
161
|
+
throw new ApplicationRuntimeError('IRIS_APP_COMMAND_UNSUPPORTED', '应用运行时尚未装配该 Command');
|
|
162
|
+
}
|
|
163
|
+
const ownedInput = structuredClone(input);
|
|
164
|
+
return schedule(COMMAND_MODES[name], runtime => {
|
|
165
|
+
const ports = resolveTaskAdapter ? { resolveTaskAdapter: async task => {
|
|
166
|
+
const adapter = await resolveTaskAdapter(task, { signal: runtime.signal });
|
|
167
|
+
if (runtime.signal.aborted) throw aborted();
|
|
168
|
+
return adapter;
|
|
169
|
+
} } : {};
|
|
170
|
+
if (name === 'execution.init' || name.startsWith('operation.')) return createLocalOperationService(runtime).execute(name, ownedInput);
|
|
171
|
+
if (name.startsWith('creation.')) return createLocalCreationService(runtime, { resolveImageCandidates, resolveAudioCandidates,
|
|
172
|
+
onPrepared: options?.onPrepared }).execute(name, ownedInput);
|
|
173
|
+
if (name.startsWith('flow.')) return createImageFlowService(runtime, { ...ports, resolveImageCandidates, resolveAudioCandidates }).execute(name, ownedInput);
|
|
174
|
+
return createCommandService(runtime, ports).execute(name, ownedInput);
|
|
175
|
+
}, options);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function generateImage(input, options) {
|
|
179
|
+
const normalized = normalizeGenerationInput('image', input);
|
|
180
|
+
if (!resolveImageCandidates) {
|
|
181
|
+
throw new ApplicationRuntimeError('IRIS_APP_IMAGE_RESOLVER_MISSING', '图片生成需要显式提供候选解析函数');
|
|
182
|
+
}
|
|
183
|
+
return schedule('writer', async runtime => {
|
|
184
|
+
const candidates = await resolveImageCandidates({ modelRef: normalized.modelRef, signal: runtime.signal });
|
|
185
|
+
if (runtime.signal.aborted) throw aborted();
|
|
186
|
+
return createProviderTaskRunner(runtime).submit({ capability: 'image', candidates,
|
|
187
|
+
providerInput: normalized.providerInput, sourceArtifactId: normalized.sourceArtifactId });
|
|
188
|
+
}, options);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Explicit trusted assembly only. No HTTP/MCP route or persistent Operation action is added. */
|
|
192
|
+
async function executePluginOperation(registry, id, input, { allowedOperations = [], dependency, signal } = {}) {
|
|
193
|
+
const reserved = reservePluginOperation(registry, id, input, allowedOperations, dependency);
|
|
194
|
+
try {
|
|
195
|
+
return await schedule(reserved.mode, runtime => runtime.run(reserved.mode === 'reader' ? 'inspect' : 'execute',
|
|
196
|
+
() => invokeReservedPlugin(runtime, reserved)), { signal });
|
|
197
|
+
} finally { reserved.release(); }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function dispose() {
|
|
201
|
+
if (disposePromise) return disposePromise;
|
|
202
|
+
state = 'closing';
|
|
203
|
+
stopAll();
|
|
204
|
+
disposePromise = Promise.all([...active].map(job => job.done)).then(() => {
|
|
205
|
+
if (cleanupFailure) throw cleanupFailure;
|
|
206
|
+
state = 'closed';
|
|
207
|
+
return snapshot();
|
|
208
|
+
});
|
|
209
|
+
return disposePromise;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return Object.freeze({ execute, generateImage, executePluginOperation, run, dispose, snapshot });
|
|
213
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { normalizeGenerationInput } from './generation-input.js';
|
|
2
|
+
import { parseModelRef } from './models.js';
|
|
3
|
+
|
|
4
|
+
export const AUDIO_RECIPES = Object.freeze({
|
|
5
|
+
'speech/v0': Object.freeze({ capability: 'tts', step: 'speak' }),
|
|
6
|
+
'transcribe/v0': Object.freeze({ capability: 'transcribe', step: 'transcribe' })
|
|
7
|
+
});
|
|
8
|
+
export const MAX_AUDIO_INPUT_BYTES = 20 * 1024 * 1024;
|
|
9
|
+
export const AUDIO_TYPES = Object.freeze({ 'audio/wav': 'wav', 'audio/mpeg': 'mp3', 'audio/ogg': 'ogg', 'audio/flac': 'flac' });
|
|
10
|
+
export const isAudioRecipe = recipe => Object.hasOwn(AUDIO_RECIPES, recipe);
|
|
11
|
+
const invalid = message => { throw Object.assign(new Error(message), { code: 'IRIS_AUDIO_INPUT_INVALID' }); };
|
|
12
|
+
|
|
13
|
+
export function normalizeAudioFlowInput(recipe, input) {
|
|
14
|
+
if (!isAudioRecipe(recipe) || !input || typeof input !== 'object' || Array.isArray(input)) invalid('不支持的音频流程输入');
|
|
15
|
+
const speech = recipe === 'speech/v0', required = speech ? ['model_ref', 'text'] : ['model_ref', 'source_artifact_id'];
|
|
16
|
+
if (Object.keys(input).some(key => ![...required, ...(speech ? ['voice'] : [])].includes(key))
|
|
17
|
+
|| required.some(key => typeof input[key] !== 'string') || !parseModelRef(input.model_ref)
|
|
18
|
+
|| input.voice !== undefined && typeof input.voice !== 'string') invalid('请提供明确模型和文字或当前工作区的音频作品');
|
|
19
|
+
if (speech) {
|
|
20
|
+
const normalized = normalizeGenerationInput('tts', input);
|
|
21
|
+
return { model_ref: normalized.modelRef, ...normalized.providerInput };
|
|
22
|
+
}
|
|
23
|
+
if (!/^artifact_[a-f0-9]{24}$/.test(input.source_artifact_id)) invalid('转写来源须为当前工作区的音频作品');
|
|
24
|
+
return { model_ref: input.model_ref, source_artifact_id: input.source_artifact_id };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Bounded container sniffing, not a decoder or a claim of provider compatibility. */
|
|
28
|
+
export function readImportAudio(bytes, mediaType) {
|
|
29
|
+
if (!(bytes instanceof Uint8Array) || !bytes.length || bytes.length > MAX_AUDIO_INPUT_BYTES) invalid('音频须为最多20 MiB的WAV、MP3、OGG或FLAC');
|
|
30
|
+
const value = Buffer.from(bytes);
|
|
31
|
+
const actual = value.length >= 12 && value.toString('ascii', 0, 4) === 'RIFF' && value.toString('ascii', 8, 12) === 'WAVE' ? 'audio/wav'
|
|
32
|
+
: value.length >= 10 && (value.toString('ascii', 0, 3) === 'ID3' || value[0] === 0xff && (value[1] & 0xe0) === 0xe0) ? 'audio/mpeg'
|
|
33
|
+
: value.length >= 27 && value.toString('ascii', 0, 4) === 'OggS' ? 'audio/ogg'
|
|
34
|
+
: value.length >= 8 && value.toString('ascii', 0, 4) === 'fLaC' ? 'audio/flac' : null;
|
|
35
|
+
if (!actual || actual !== mediaType) invalid('音频容器与Content-Type不一致,或不是支持的格式');
|
|
36
|
+
return { bytes: value, mediaType: actual, metadata: {} };
|
|
37
|
+
}
|