@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/actions.js
CHANGED
|
@@ -7,7 +7,7 @@ import { visionInputLimits } from './vision-image-input.js';
|
|
|
7
7
|
* ① `POST /iris/api/actions/:name` 路由(GUI 直连,submit-only 不等待)
|
|
8
8
|
* ② 工具 execute 体(Agent 调用,可等待)
|
|
9
9
|
*
|
|
10
|
-
* 每个 action
|
|
10
|
+
* 每个 action 接收 `(host, args, { signal, core })`,显式使用所属实例,
|
|
11
11
|
* 结果只有可序列化 JSON(无 live 对象)。
|
|
12
12
|
*/
|
|
13
13
|
import fs from 'node:fs';
|
|
@@ -23,7 +23,6 @@ import { registerMedia, mediaLinksOf, webBase } from './media.js';
|
|
|
23
23
|
import { RED_TEST_IMAGE } from './vision.js';
|
|
24
24
|
import { runVisionRequest, readVisionImageFile, probeVisionModel, visionImageFromDataUrl } from './vision-model-routing.js';
|
|
25
25
|
import { HOST_CONTRACT_VERSION, HOST_PORTS, defineHostAdapter, hasHostPort, requireHostPort } from './host-contract.js';
|
|
26
|
-
import { coreSnapshotForDsh, importImageForDsh, cropForDsh, diffForDsh, framesForDsh, summaryFramesForDsh, visionImageForDsh, htmlForDsh, inspectProviderTaskForDsh, submitMusicForDsh, submitProviderTaskForDsh, watchProviderTaskForDsh } from './dsh-core-adapter.js';
|
|
27
26
|
import { runLocateRequest, runSummaryRequest } from './composite-vision-routing.js';
|
|
28
27
|
import { normalizeSummarySource, summaryFrameRecords, summaryTimelineLabel } from './summary-input.js';
|
|
29
28
|
import { normalizeVisionImageSource } from './vision-input.js';
|
|
@@ -31,13 +30,23 @@ import { formatOcrResult } from './ocr.js';
|
|
|
31
30
|
import { runOcrRequest } from './ocr-model-routing.js';
|
|
32
31
|
import { ffmpegAvailable, extractFrames, probeVideo } from './media-probe.js';
|
|
33
32
|
import { ensurePrivateDir, writePrivateFile } from './private-storage.js';
|
|
34
|
-
import { selectMediaProtocol, selectImageProtocol, providerForImageModel, supportsImageEdit } from './provider-protocol.js';
|
|
33
|
+
import { selectMediaProtocol, selectImageProtocol, providerForImageModel, supportsImageEdit, supportsModelCapability, dashscopeTtsProtocolIssue, dashscopeTtsInputIssue } from './provider-protocol.js';
|
|
35
34
|
import { musicCandidatesFromCatalog, providerTaskBinding } from './provider-catalog.js';
|
|
36
35
|
import { invokeProviderOperation, providerPollTaskView, prepareProviderInput } from './provider-adapter.js';
|
|
37
36
|
import { createConfiguredProviderAdapter as createProviderAdapter } from './provider-adapters.js';
|
|
38
37
|
import { deriveUserState } from './task-semantics.js';
|
|
39
38
|
import { normalizeGenerationInput } from './generation-input.js';
|
|
40
39
|
import { videoTaskCandidates } from './video-input.js';
|
|
40
|
+
import { normalizeDshProvider, withDshProviderConfig } from './dsh-provider-config.js';
|
|
41
|
+
import { modelDirectorySummary, modelUnavailableReason } from './model-directory.js';
|
|
42
|
+
import { ModelPortError } from './model-port-contract.js';
|
|
43
|
+
|
|
44
|
+
const PROVIDER_ACTIONS = new Set(['image', 'image_edit', 'video', 'music', 'tts', 'transcribe', 'look', 'relook',
|
|
45
|
+
'locate', 'ocr', 'media_summarize', 'task_reobserve', 'task_redeliver', 'task_manual_retry',
|
|
46
|
+
'providers_discover', 'providers_sync_directory', 'providers_test_vision', 'providers_test_model']);
|
|
47
|
+
const AUTO_DIRECTORY_ACTIONS = new Set(['image', 'image_edit', 'video', 'music', 'tts', 'transcribe', 'look', 'relook',
|
|
48
|
+
'locate', 'ocr', 'media_summarize']);
|
|
49
|
+
const VISION_ACTIONS = new Set(['look', 'relook', 'locate', 'ocr', 'media_summarize']);
|
|
41
50
|
|
|
42
51
|
const MEDIA_TYPES = {
|
|
43
52
|
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
@@ -46,6 +55,7 @@ const MEDIA_TYPES = {
|
|
|
46
55
|
|
|
47
56
|
function createConfiguredProviderAdapter(provider, options = {}) {
|
|
48
57
|
return createProviderAdapter(provider, { ...options,
|
|
58
|
+
onModelAvailability: event => store.recordAvailability(event),
|
|
49
59
|
onRateLimit: options.allowRateLimited ? undefined : store.recordRateLimit });
|
|
50
60
|
}
|
|
51
61
|
|
|
@@ -92,11 +102,14 @@ function throwIfAborted(signal) {
|
|
|
92
102
|
* 不因后续轮询或结果下载失败重提,避免同一请求重复计费。
|
|
93
103
|
*/
|
|
94
104
|
function selectProviders(capability, label, requestedModelRef) {
|
|
105
|
+
const policy = models.functionPolicy(store.assignments()[capability], capability);
|
|
106
|
+
if (policy.mode === 'disabled') throw new Error('iris: 没有可用' + label + '模型:此功能已停用,请在功能池中选择调用方式');
|
|
95
107
|
let providers = store.pickAllFor(capability);
|
|
96
108
|
const requested = String(requestedModelRef || '').trim();
|
|
97
109
|
if (requested) {
|
|
98
|
-
const pool = models.orderedModels(models.
|
|
110
|
+
const pool = models.orderedModels(models.functionModelPool(store.providers(), capability, store.assignments()[capability]), capability, store.assignments()[capability], { includeRateLimited: true, includeUnavailable: true });
|
|
99
111
|
const selected = models.resolvePoolModel(pool, requested, capability);
|
|
112
|
+
if (selected?.unavailableCapabilities?.includes(capability)) throw new Error('iris: ' + modelUnavailableReason());
|
|
100
113
|
if (selected?.rateLimited) throw Object.assign(new Error(store.modelHealth(selected.providerId, selected.id, capability).retryAt
|
|
101
114
|
? 'iris: 指定模型正在限流冷却,请稍后使用或手动实测'
|
|
102
115
|
: 'iris: 指定模型额度或预算耗尽,请手动实测成功后再使用'), {
|
|
@@ -106,7 +119,7 @@ function selectProviders(capability, label, requestedModelRef) {
|
|
|
106
119
|
providers = selected ? providers.filter((p) => p.id === selected.providerId && p[field] === selected.id) : [];
|
|
107
120
|
if (!providers.length) throw new Error('iris: 指定模型未分配给该功能、不可用或不具备' + label + '能力');
|
|
108
121
|
}
|
|
109
|
-
if (!providers.length) throw new Error('iris: 没有可用' + label + '
|
|
122
|
+
if (!providers.length) throw new Error('iris: 没有可用' + label + '模型:请检查连接、已确认的模型用途、协议和冷却状态;手选模式还需选择模型');
|
|
110
123
|
return providers;
|
|
111
124
|
}
|
|
112
125
|
|
|
@@ -161,13 +174,13 @@ async function observeProviderCapability(route, capability, operation, { recordS
|
|
|
161
174
|
const result = await operation();
|
|
162
175
|
if (recordSuccess && result && ['accepted', 'completed', 'pending', 'succeeded'].includes(result.kind)) {
|
|
163
176
|
recordProviderHealthSafe(route.provider.id, route.model, capability, {
|
|
164
|
-
ok: true,
|
|
177
|
+
ok: true, stage: 'poll',
|
|
165
178
|
category: 'success',
|
|
166
179
|
note: result.kind === 'accepted' ? '任务已受理' : '供应商请求成功'
|
|
167
180
|
});
|
|
168
181
|
} else if (result && result.error) {
|
|
169
182
|
recordProviderHealthSafe(route.provider.id, route.model, capability, {
|
|
170
|
-
ok: false,
|
|
183
|
+
ok: false, stage: 'poll',
|
|
171
184
|
category: result.error.category,
|
|
172
185
|
httpStatus: result.error.httpStatus,
|
|
173
186
|
note: result.error.safeMessage
|
|
@@ -176,7 +189,7 @@ async function observeProviderCapability(route, capability, operation, { recordS
|
|
|
176
189
|
return result;
|
|
177
190
|
} catch (error) {
|
|
178
191
|
recordProviderHealthSafe(route.provider.id, route.model, capability, {
|
|
179
|
-
ok: false,
|
|
192
|
+
ok: false, stage: 'poll',
|
|
180
193
|
category: error && error.category,
|
|
181
194
|
httpStatus: error && (error.httpStatus ?? error.status),
|
|
182
195
|
note: String(error && (error.safeMessage || error.message) || error)
|
|
@@ -200,7 +213,8 @@ function recordVisionHealth(errors) {
|
|
|
200
213
|
|
|
201
214
|
async function askSingleVisionWithHealth(host, request) {
|
|
202
215
|
try {
|
|
203
|
-
const result = await runVisionRequest(host, { providers: store.pickAllFor(cap.CAPABILITIES.VISION), onRateLimit: store.recordRateLimit,
|
|
216
|
+
const result = await runVisionRequest(host, { providers: store.pickAllFor(cap.CAPABILITIES.VISION), onRateLimit: store.recordRateLimit,
|
|
217
|
+
onModelAvailability: store.recordAvailability, ...request });
|
|
204
218
|
recordVisionHealth(result.errors);
|
|
205
219
|
if (result.via === 'selfstack') recordProviderHealthSafe(result.backendId, result.model, cap.CAPABILITIES.VISION, {
|
|
206
220
|
ok: true, category: 'success', note: '视觉请求成功'
|
|
@@ -229,8 +243,8 @@ function recordCoreTaskAttemptHealth(task, capability) {
|
|
|
229
243
|
}
|
|
230
244
|
|
|
231
245
|
/** DSH 图片入口:同步和异步候选共用一份 Core Task/Attempt/Artifact 事实。 */
|
|
232
|
-
async function submitCoreImage(routes, providerInput, signal, sourceArtifactId) {
|
|
233
|
-
const submitted = await submitProviderTaskForDsh({
|
|
246
|
+
async function submitCoreImage(core, routes, providerInput, signal, sourceArtifactId) {
|
|
247
|
+
const submitted = await core.submitProviderTaskForDsh({
|
|
234
248
|
capability: 'image',
|
|
235
249
|
candidates: routes.map((route) => ({
|
|
236
250
|
adapter: route.adapter,
|
|
@@ -254,7 +268,7 @@ async function submitCoreImage(routes, providerInput, signal, sourceArtifactId)
|
|
|
254
268
|
if (!selected) throw taskActionError(task.id, 'Core Task 的供应商身份无法映射回当前路由', task);
|
|
255
269
|
if (task.acceptance === 'accepted' && task.remoteTaskId
|
|
256
270
|
&& ['none', 'unknown'].includes(task.outcome) && task.phase !== 'terminal') {
|
|
257
|
-
await watchProviderTaskForDsh({ taskId: task.id, signal });
|
|
271
|
+
await core.watchProviderTaskForDsh({ taskId: task.id, signal });
|
|
258
272
|
} else if (task.outcome !== 'succeeded' || task.deliveryState !== 'ready' || !task.artifactIds.length) {
|
|
259
273
|
throw taskActionError(task.id, '图片路由没有得到可观察任务或可交付产物;事实已保留在 Core,未回退重提', task);
|
|
260
274
|
}
|
|
@@ -266,7 +280,7 @@ async function submitCoreImage(routes, providerInput, signal, sourceArtifactId)
|
|
|
266
280
|
* 不共用图片状态机——视频走自己的交付 Profile(video/mp4 / generated-video)。
|
|
267
281
|
* s2v 在写前 Attempt 之后按账号上传首帧和音频,交付复用视频 Profile。
|
|
268
282
|
*/
|
|
269
|
-
async function submitCoreVideo(routes, frame, prompt, args, signal) {
|
|
283
|
+
async function submitCoreVideo(core, routes, frame, prompt, args, signal) {
|
|
270
284
|
const normalized = normalizeGenerationInput('video', {
|
|
271
285
|
...(prompt ? { prompt } : {}),
|
|
272
286
|
...(frame ? { first_frame_path: frame.absPath } : {}),
|
|
@@ -274,7 +288,7 @@ async function submitCoreVideo(routes, frame, prompt, args, signal) {
|
|
|
274
288
|
size: optionalActionText(args.size),
|
|
275
289
|
duration: args.duration
|
|
276
290
|
}, { allowVideoPaths: true });
|
|
277
|
-
const submitted = await submitProviderTaskForDsh({
|
|
291
|
+
const submitted = await core.submitProviderTaskForDsh({
|
|
278
292
|
capability: 'video',
|
|
279
293
|
candidates: videoTaskCandidates(routes.map((route) => ({
|
|
280
294
|
adapter: route.adapter,
|
|
@@ -297,7 +311,7 @@ async function submitCoreVideo(routes, frame, prompt, args, signal) {
|
|
|
297
311
|
if (!selected) throw taskActionError(task.id, 'Core Task 的供应商身份无法映射回当前路由', task);
|
|
298
312
|
if (task.acceptance === 'accepted' && task.remoteTaskId
|
|
299
313
|
&& ['none', 'unknown'].includes(task.outcome) && task.phase !== 'terminal') {
|
|
300
|
-
await watchProviderTaskForDsh({ taskId: task.id, signal });
|
|
314
|
+
await core.watchProviderTaskForDsh({ taskId: task.id, signal });
|
|
301
315
|
} else if (task.outcome !== 'succeeded' || task.deliveryState !== 'ready' || !task.artifactIds.length) {
|
|
302
316
|
throw taskActionError(task.id, '视频路由没有得到可观察任务或可交付产物;事实已保留在 Core,未回退重提', task);
|
|
303
317
|
}
|
|
@@ -459,17 +473,34 @@ export function listActions() {
|
|
|
459
473
|
}
|
|
460
474
|
|
|
461
475
|
/** 执行一个动作 */
|
|
462
|
-
export async function runAction(host, name, args, { signal } = {}) {
|
|
476
|
+
export async function runAction(host, name, args, { signal, core } = {}) {
|
|
463
477
|
const fn = actions[name];
|
|
464
|
-
if (!fn) throw new Error(
|
|
465
|
-
|
|
478
|
+
if (!fn) throw new Error('iris: 未知动作 "' + name + '"');
|
|
479
|
+
const adapter = actionHost(host);
|
|
480
|
+
const owned = structuredClone(args || {});
|
|
481
|
+
return core.runHostOperation(operationSignal => {
|
|
482
|
+
const execute = async () => {
|
|
483
|
+
if (AUTO_DIRECTORY_ACTIONS.has(name) && !owned.model) await store.refreshAutomaticDirectories({
|
|
484
|
+
capability: ({ image: 'image-gen', image_edit: 'image-gen', video: 'video-gen', music: 'music-gen', tts: 'tts', transcribe: 'transcribe' })[name] || 'vision', signal: operationSignal
|
|
485
|
+
});
|
|
486
|
+
operationSignal.throwIfAborted();
|
|
487
|
+
return fn(adapter, owned, { signal: operationSignal, core });
|
|
488
|
+
};
|
|
489
|
+
return PROVIDER_ACTIONS.has(name) ? withDshProviderConfig(adapter, execute, { signal: operationSignal }) : execute();
|
|
490
|
+
}, { signal }).catch(error => {
|
|
491
|
+
// Ownership can stop the request before it enters the Model Port's error boundary.
|
|
492
|
+
if (VISION_ACTIONS.has(name) && signal?.aborted && error === signal.reason) {
|
|
493
|
+
throw new ModelPortError('IRIS_MODEL_ABORTED');
|
|
494
|
+
}
|
|
495
|
+
throw error;
|
|
496
|
+
});
|
|
466
497
|
}
|
|
467
498
|
|
|
468
499
|
/* ============ 动作实现 ============ */
|
|
469
500
|
|
|
470
|
-
register('image_import', async (_host, args, { signal }) => {
|
|
501
|
+
register('image_import', async (_host, args, { signal, core }) => {
|
|
471
502
|
if (args.image_path !== undefined && args.image_bytes !== undefined) throw new Error('图片导入只能选择一个来源');
|
|
472
|
-
const { artifact } = await importImageForDsh({ imagePath: args.image_path, imageBytes: args.image_bytes, signal });
|
|
503
|
+
const { artifact } = await core.importImageForDsh({ imagePath: args.image_path, imageBytes: args.image_bytes, signal });
|
|
473
504
|
const imageUrl = '/iris/api/core/artifact/' + artifact.id + '/media';
|
|
474
505
|
const text = `已导入图片(${artifact.metadata.width}×${artifact.metadata.height}),原文件保留。\n作品ID:${artifact.id}`;
|
|
475
506
|
return { ok: true, storage: 'core', artifact, artifactId: artifact.id, artifactIds: [artifact.id], imageUrl, text,
|
|
@@ -477,7 +508,7 @@ register('image_import', async (_host, args, { signal }) => {
|
|
|
477
508
|
});
|
|
478
509
|
|
|
479
510
|
/* ---------- 🖼️ 画图 ---------- */
|
|
480
|
-
async function imageAction(host, args, { signal }, editing = false) {
|
|
511
|
+
async function imageAction(host, args, { signal, core }, editing = false) {
|
|
481
512
|
if (editing && !args.source_artifact_id) throw new Error('iris: 改图必须提供 source_artifact_id');
|
|
482
513
|
const { providerInput, sourceArtifactId } = normalizeGenerationInput('image', {
|
|
483
514
|
prompt: args.prompt, size: optionalActionText(args.size), n: args.n,
|
|
@@ -486,7 +517,7 @@ async function imageAction(host, args, { signal }, editing = false) {
|
|
|
486
517
|
|
|
487
518
|
const imageProviders = selectProviders(cap.CAPABILITIES.IMAGE, '图像', args.model);
|
|
488
519
|
const routes = imageProviders.map((provider) => imageRoute(provider, args));
|
|
489
|
-
const submitted = await submitCoreImage(routes, providerInput, signal, sourceArtifactId);
|
|
520
|
+
const submitted = await submitCoreImage(core, routes, providerInput, signal, sourceArtifactId);
|
|
490
521
|
const firstArtifactId = submitted.task.artifactIds[0];
|
|
491
522
|
const ready = submitted.task.outcome === 'succeeded'
|
|
492
523
|
&& submitted.task.deliveryState === 'ready' && Boolean(firstArtifactId);
|
|
@@ -505,7 +536,7 @@ register('image', imageAction);
|
|
|
505
536
|
register('image_edit', (host, args, context) => imageAction(host, args, context, true));
|
|
506
537
|
|
|
507
538
|
/* ---------- 🎬 视频生成 ---------- */
|
|
508
|
-
register('video', async (host, args, { signal }) => {
|
|
539
|
+
register('video', async (host, args, { signal, core }) => {
|
|
509
540
|
const prompt = String(args.prompt || '').trim();
|
|
510
541
|
const frame = await resolveFirstFrameFile(host, args, signal);
|
|
511
542
|
const providers = selectProviders(cap.CAPABILITIES.VIDEO, '视频', args.model);
|
|
@@ -525,7 +556,7 @@ register('video', async (host, args, { signal }) => {
|
|
|
525
556
|
if (!coreRoutes.length) {
|
|
526
557
|
throw new Error(s2v ? 'iris: 未配置可用 s2v 数字人模型' : 'iris: 当前视频候选只有 s2v 数字人模型;请使用 audio_path 的数字人入口');
|
|
527
558
|
}
|
|
528
|
-
const submitted = await submitCoreVideo(coreRoutes, frame, prompt, args, signal);
|
|
559
|
+
const submitted = await submitCoreVideo(core, coreRoutes, frame, prompt, args, signal);
|
|
529
560
|
const firstArtifactId = submitted.task.artifactIds[0];
|
|
530
561
|
const ready = submitted.task.outcome === 'succeeded'
|
|
531
562
|
&& submitted.task.deliveryState === 'ready' && Boolean(firstArtifactId);
|
|
@@ -545,12 +576,12 @@ register('video', async (host, args, { signal }) => {
|
|
|
545
576
|
* 直接交付 generated-audio Artifact(无 remote 长轮询)。整条候选链共用一个
|
|
546
577
|
* voice(Core providerInput 静态;legacy 曾按候选各自取 provider.ttsVoice)。
|
|
547
578
|
*/
|
|
548
|
-
async function submitCoreTts(routes, normalizedInput, signal) {
|
|
579
|
+
async function submitCoreTts(core, routes, normalizedInput, signal) {
|
|
549
580
|
const voice = normalizedInput.voice || String(routes[0].provider.ttsVoice || '').trim();
|
|
550
581
|
const { providerInput } = normalizeGenerationInput('tts', {
|
|
551
582
|
text: normalizedInput.text, ...(voice ? { voice } : {})
|
|
552
583
|
});
|
|
553
|
-
const submitted = await submitProviderTaskForDsh({
|
|
584
|
+
const submitted = await core.submitProviderTaskForDsh({
|
|
554
585
|
capability: 'tts',
|
|
555
586
|
candidates: routes.map((route) => ({
|
|
556
587
|
adapter: route.adapter,
|
|
@@ -578,15 +609,15 @@ async function submitCoreTts(routes, normalizedInput, signal) {
|
|
|
578
609
|
}
|
|
579
610
|
|
|
580
611
|
/* ---------- 🔊 语音合成 ---------- */
|
|
581
|
-
register('music', async (host, args, { signal }) => {
|
|
612
|
+
register('music', async (host, args, { signal, core }) => {
|
|
582
613
|
const normalized = normalizeGenerationInput('music', {
|
|
583
614
|
prompt: args.prompt,
|
|
584
615
|
...(optionalActionText(args.timeout_ms) !== undefined ? { timeout_ms: args.timeout_ms } : {}),
|
|
585
616
|
...(optionalActionText(args.retry_502) !== undefined ? { retry_502: args.retry_502 } : {}),
|
|
586
617
|
...(optionalActionText(args.retry_delay_ms) !== undefined ? { retry_delay_ms: args.retry_delay_ms } : {})
|
|
587
618
|
});
|
|
588
|
-
const routes = musicCandidatesFromCatalog({ providers: store.
|
|
589
|
-
const submitted = await submitMusicForDsh({
|
|
619
|
+
const routes = musicCandidatesFromCatalog({ providers: store.runtimeProviders(), assignments: store.assignments() }, args.model);
|
|
620
|
+
const submitted = await core.submitMusicForDsh({
|
|
590
621
|
candidates: routes.map(route => ({ adapter: createConfiguredProviderAdapter(route.provider), model: route.modelRef,
|
|
591
622
|
selectionReason: route.selectionReason, providerBinding: providerTaskBinding(route.provider) })),
|
|
592
623
|
providerInput: normalized.providerInput, musicOptions: normalized.musicOptions, signal
|
|
@@ -605,13 +636,13 @@ register('music', async (host, args, { signal }) => {
|
|
|
605
636
|
text: `[iris] 音乐已生成(${task.modelRef},重试${submitted.retries}次,Core task: ${task.id})` };
|
|
606
637
|
});
|
|
607
638
|
|
|
608
|
-
register('tts', async (host, args, { signal }) => {
|
|
639
|
+
register('tts', async (host, args, { signal, core }) => {
|
|
609
640
|
const { providerInput } = normalizeGenerationInput('tts', {
|
|
610
641
|
text: args.text, voice: optionalActionText(args.voice)
|
|
611
642
|
});
|
|
612
643
|
const providers = selectProviders(cap.CAPABILITIES.TTS, '语音', args.model);
|
|
613
644
|
const routes = providers.map((provider) => simpleModelRoute(provider, args, cap.CAPABILITIES.TTS));
|
|
614
|
-
const submitted = await submitCoreTts(routes, providerInput, signal);
|
|
645
|
+
const submitted = await submitCoreTts(core, routes, providerInput, signal);
|
|
615
646
|
const firstArtifactId = submitted.task.artifactIds[0];
|
|
616
647
|
return {
|
|
617
648
|
ok: true, storage: 'core', taskId: submitted.task.id, model: submitted.model,
|
|
@@ -628,7 +659,7 @@ register('tts', async (host, args, { signal }) => {
|
|
|
628
659
|
* 与人工控制面复用同一 Task/Attempt 轴;正文物化为 text/plain transcript Artifact。
|
|
629
660
|
* 已知边界:临时音频 URL 属于首选候选的账号,failover 到不同账号时如实记录失败。
|
|
630
661
|
*/
|
|
631
|
-
async function submitCoreTranscribe(routes, normalized, signal) {
|
|
662
|
+
async function submitCoreTranscribe(core, routes, normalized, signal) {
|
|
632
663
|
let providerInput = normalized.providerInput;
|
|
633
664
|
if (normalized.audioPath) {
|
|
634
665
|
const prepared = await prepareProviderInput(routes[0].adapter, {
|
|
@@ -636,7 +667,7 @@ async function submitCoreTranscribe(routes, normalized, signal) {
|
|
|
636
667
|
});
|
|
637
668
|
providerInput = normalizeGenerationInput('transcribe', { audio_url: prepared.url }).providerInput;
|
|
638
669
|
}
|
|
639
|
-
const submitted = await submitProviderTaskForDsh({
|
|
670
|
+
const submitted = await core.submitProviderTaskForDsh({
|
|
640
671
|
capability: 'transcribe',
|
|
641
672
|
candidates: routes.map((route) => ({
|
|
642
673
|
adapter: route.adapter,
|
|
@@ -659,7 +690,7 @@ async function submitCoreTranscribe(routes, normalized, signal) {
|
|
|
659
690
|
if (!selected) throw taskActionError(task.id, 'Core Task 的供应商身份无法映射回当前路由', task);
|
|
660
691
|
if (task.acceptance === 'accepted' && task.remoteTaskId
|
|
661
692
|
&& ['none', 'unknown'].includes(task.outcome) && task.phase !== 'terminal') {
|
|
662
|
-
await watchProviderTaskForDsh({ taskId: task.id, signal });
|
|
693
|
+
await core.watchProviderTaskForDsh({ taskId: task.id, signal });
|
|
663
694
|
} else if (task.outcome !== 'succeeded' || task.deliveryState !== 'ready' || !task.artifactIds.length) {
|
|
664
695
|
throw taskActionError(task.id, '转写路由没有得到可观察任务或可交付产物;事实已保留在 Core,未回退重提', task);
|
|
665
696
|
}
|
|
@@ -667,7 +698,7 @@ async function submitCoreTranscribe(routes, normalized, signal) {
|
|
|
667
698
|
}
|
|
668
699
|
|
|
669
700
|
/* ---------- 👁 看图 ---------- */
|
|
670
|
-
register('look', async (host, args, { signal }) => {
|
|
701
|
+
register('look', async (host, args, { signal, core }) => {
|
|
671
702
|
const source = normalizeVisionImageSource(args);
|
|
672
703
|
const p = String(args.image_path || '').trim();
|
|
673
704
|
const mt = MEDIA_TYPES[path.extname(p).toLowerCase()];
|
|
@@ -679,18 +710,18 @@ register('look', async (host, args, { signal }) => {
|
|
|
679
710
|
const { answer, via, model } = await askSingleVisionWithHealth(host, { model: args.model,
|
|
680
711
|
question: String(args.question || '').trim() || '详细描述这张图片。',
|
|
681
712
|
prepareImage: operationSignal => source === 'artifact_id'
|
|
682
|
-
? visionImageForDsh({ artifactId: args.artifact_id, signal: operationSignal }) : readVisionImageFile(p, mt, operationSignal), signal });
|
|
713
|
+
? core.visionImageForDsh({ artifactId: args.artifact_id, signal: operationSignal }) : readVisionImageFile(p, mt, operationSignal), signal });
|
|
683
714
|
const viaLabel = via === 'selfstack' ? 'iris 自持栈' : 'DSH 全局视觉模型';
|
|
684
715
|
return { ok: true, ...(args.artifact_id ? { artifactId: args.artifact_id } : {}),
|
|
685
716
|
text: `[iris] 看图回答(${model} · ${viaLabel}):\n${answer}` };
|
|
686
717
|
});
|
|
687
718
|
|
|
688
719
|
/* ---------- ✂️ 裁剪 ---------- */
|
|
689
|
-
register('crop', async (host, args, { signal }) => {
|
|
720
|
+
register('crop', async (host, args, { signal, core }) => {
|
|
690
721
|
const p = String(args.image_path || '').trim();
|
|
691
722
|
if (!p || !path.isAbsolute(p)) throw new Error('iris: image_path 必须是绝对路径');
|
|
692
723
|
if (!fs.existsSync(p)) throw new Error('iris: 图片不存在');
|
|
693
|
-
const result = await cropForDsh({
|
|
724
|
+
const result = await core.cropForDsh({
|
|
694
725
|
imagePath: p, left: args.left, top: args.top, width: args.width, height: args.height, signal
|
|
695
726
|
});
|
|
696
727
|
const b64 = result.bytes.toString('base64');
|
|
@@ -698,9 +729,9 @@ register('crop', async (host, args, { signal }) => {
|
|
|
698
729
|
});
|
|
699
730
|
|
|
700
731
|
/* ---------- 📷 像素 diff ---------- */
|
|
701
|
-
register('diff', async (host, args, { signal }) => {
|
|
732
|
+
register('diff', async (host, args, { signal, core }) => {
|
|
702
733
|
if (!args.image_a_path || !args.image_b_path) throw new Error('iris: 需要 image_a_path 和 image_b_path');
|
|
703
|
-
const result = await diffForDsh({
|
|
734
|
+
const result = await core.diffForDsh({
|
|
704
735
|
imageA: { imagePath: args.image_a_path }, imageB: { imagePath: args.image_b_path },
|
|
705
736
|
grid: args.grid, topRegions: args.top_regions, signal
|
|
706
737
|
});
|
|
@@ -716,32 +747,33 @@ register('diff', async (host, args, { signal }) => {
|
|
|
716
747
|
});
|
|
717
748
|
|
|
718
749
|
/* ---------- 📍 定位 ---------- */
|
|
719
|
-
register('locate', async (host, args, { signal }) => {
|
|
750
|
+
register('locate', async (host, args, { signal, core }) => {
|
|
720
751
|
const source = normalizeVisionImageSource(args);
|
|
721
752
|
const p = String(args.image_path || '').trim();
|
|
722
753
|
if (source === 'image_path' && (!p || !path.isAbsolute(p))) throw new Error('iris: image_path 必须是绝对路径');
|
|
723
754
|
const mt = MEDIA_TYPES[path.extname(p).toLowerCase()] || 'image/png';
|
|
724
|
-
const r = await runLocateRequest(host, { providers: store.pickAllFor(cap.CAPABILITIES.VISION), onRateLimit: store.recordRateLimit,
|
|
755
|
+
const r = await runLocateRequest(host, { providers: store.pickAllFor(cap.CAPABILITIES.VISION), onRateLimit: store.recordRateLimit,
|
|
756
|
+
onModelAvailability: store.recordAvailability, model: args.model,
|
|
725
757
|
target: args.target, prepareImage: operationSignal => source === 'artifact_id'
|
|
726
|
-
? visionImageForDsh({ artifactId: args.artifact_id, signal: operationSignal }) : readVisionImageFile(p, mt, operationSignal), signal });
|
|
758
|
+
? core.visionImageForDsh({ artifactId: args.artifact_id, signal: operationSignal }) : readVisionImageFile(p, mt, operationSignal), signal });
|
|
727
759
|
const { width, height } = r;
|
|
728
760
|
const provenance = args.artifact_id ? { artifactId: args.artifact_id } : {};
|
|
729
761
|
if (!r.found) return { ok: true, ...provenance, text: `[iris] 在图片中未找到「${args.target}」` };
|
|
730
762
|
return { ok: true, ...provenance, text: `[iris] 定位「${args.target}」:bbox (${r.x1},${r.y1},${r.x2},${r.y2}) / ${width}x${height}(${r.via} · ${r.model})` };
|
|
731
763
|
});
|
|
732
764
|
/* ---------- 🖼️ HTML 截图 ---------- */
|
|
733
|
-
register('html', async (host, args, { signal }) => {
|
|
765
|
+
register('html', async (host, args, { signal, core }) => {
|
|
734
766
|
const html = String(args.html || '').trim();
|
|
735
767
|
if (!html) throw new Error('iris: html 不能为空');
|
|
736
768
|
const browser = requireHostPort(host, 'browser', 'HTML 截图');
|
|
737
|
-
const rendered = await htmlForDsh({ browser, html, width: args.width, height: args.height,
|
|
769
|
+
const rendered = await core.htmlForDsh({ browser, html, width: args.width, height: args.height,
|
|
738
770
|
fullPage: args.fullPage !== false, signal });
|
|
739
771
|
const png = Buffer.from(rendered.bytes);
|
|
740
772
|
return { ok: true, storage: 'core', artifactIds: [rendered.artifact.id], text: `[iris] HTML 截图完成(fullPage: ${args.fullPage !== false})`, imageDataUrl: `data:image/png;base64,${png.toString('base64')}` };
|
|
741
773
|
});
|
|
742
774
|
|
|
743
775
|
/* ---------- 📄 长截图 OCR ---------- */
|
|
744
|
-
register('ocr', async (host, args, { signal }) => {
|
|
776
|
+
register('ocr', async (host, args, { signal, core }) => {
|
|
745
777
|
const source = normalizeVisionImageSource(args);
|
|
746
778
|
const p = String(args.image_path || '').trim();
|
|
747
779
|
const mediaType = MEDIA_TYPES[path.extname(p).toLowerCase()];
|
|
@@ -750,9 +782,9 @@ register('ocr', async (host, args, { signal }) => {
|
|
|
750
782
|
if (!mediaType || mediaType === 'image/gif') throw new Error('iris: 不支持的图片格式(png/jpg/jpeg/webp)');
|
|
751
783
|
}
|
|
752
784
|
const result = await runOcrRequest(host, { providers: store.pickAllFor(cap.CAPABILITIES.VISION),
|
|
753
|
-
onRateLimit: store.recordRateLimit,
|
|
785
|
+
onRateLimit: store.recordRateLimit, onModelAvailability: store.recordAvailability,
|
|
754
786
|
prepareImage: operationSignal => source === 'artifact_id'
|
|
755
|
-
? visionImageForDsh({ artifactId: args.artifact_id, signal: operationSignal }) : readVisionImageFile(p, mediaType, operationSignal),
|
|
787
|
+
? core.visionImageForDsh({ artifactId: args.artifact_id, signal: operationSignal }) : readVisionImageFile(p, mediaType, operationSignal),
|
|
756
788
|
chunkHeight: args.chunk_height, overlap: args.overlap, signal });
|
|
757
789
|
return { ok: result.status !== 'failed', status: result.status,
|
|
758
790
|
totalChunks: result.totalChunks, successfulChunks: result.successfulChunks,
|
|
@@ -761,7 +793,7 @@ register('ocr', async (host, args, { signal }) => {
|
|
|
761
793
|
});
|
|
762
794
|
|
|
763
795
|
/* ---------- 📋 任务查询 ---------- */
|
|
764
|
-
register('status', async (host, args, { signal }) => {
|
|
796
|
+
register('status', async (host, args, { signal, core }) => {
|
|
765
797
|
const requestedId = String(args.task_id || '').trim();
|
|
766
798
|
if (requestedId) {
|
|
767
799
|
const legacy = tasks.get(requestedId);
|
|
@@ -776,14 +808,14 @@ register('status', async (host, args, { signal }) => {
|
|
|
776
808
|
}
|
|
777
809
|
if (/^task_[a-f0-9]{24}$/.test(requestedId)) {
|
|
778
810
|
try {
|
|
779
|
-
const
|
|
780
|
-
const lines = [`[${
|
|
781
|
-
if (
|
|
782
|
-
if (
|
|
783
|
-
if (
|
|
784
|
-
lines.push('artifacts:\n' +
|
|
811
|
+
const coreTask = await core.inspectProviderTaskForDsh(requestedId, { signal });
|
|
812
|
+
const lines = [`[${coreTask.capability}] ${coreTask.id} · Core ${coreTask.phase} · ${coreTask.outcome} / ${coreTask.deliveryState} · ${coreTask.modelRef || '未选择模型'}`];
|
|
813
|
+
if (coreTask.lastError) lines.push('error: ' + coreTask.lastError.safeMessage);
|
|
814
|
+
if (coreTask.remoteTaskId) lines.push('remoteTask: ' + coreTask.remoteTaskId);
|
|
815
|
+
if (coreTask.artifactIds.length) {
|
|
816
|
+
lines.push('artifacts:\n' + coreTask.artifactIds.map((id) => id + ' · /iris/api/core/artifact/' + encodeURIComponent(id) + '/media').join('\n'));
|
|
785
817
|
}
|
|
786
|
-
return { ok: true, storage: 'core', taskId:
|
|
818
|
+
return { ok: true, storage: 'core', taskId: coreTask.id, artifactIds: coreTask.artifactIds, text: lines.join('\n') };
|
|
787
819
|
} catch (_) { /* 对外统一为不存在,不暴露 Core 数据根 */ }
|
|
788
820
|
}
|
|
789
821
|
throw new Error('iris: 没有这个任务:' + args.task_id);
|
|
@@ -795,7 +827,7 @@ register('status', async (host, args, { signal }) => {
|
|
|
795
827
|
}));
|
|
796
828
|
let coreRows = [];
|
|
797
829
|
try {
|
|
798
|
-
const snapshot = await coreSnapshotForDsh({ limit: 5, signal });
|
|
830
|
+
const snapshot = await core.coreSnapshotForDsh({ limit: 5, signal });
|
|
799
831
|
coreRows = (snapshot.tasks.recent || []).map((task) => ({
|
|
800
832
|
at: task.updatedAt,
|
|
801
833
|
text: `[${task.capability}] ${task.id} · Core ${task.phase} · ${task.outcome} / ${task.deliveryState} · ${task.modelRef || '未选择模型'}`
|
|
@@ -822,7 +854,7 @@ register('task_reobserve', async (host, args) => {
|
|
|
822
854
|
return { ok: true, taskId: id, text: '[iris] 已重新观察远端任务;不会重新提交生成请求。' };
|
|
823
855
|
});
|
|
824
856
|
|
|
825
|
-
register('task_redeliver', async (host, args, { signal }) => {
|
|
857
|
+
register('task_redeliver', async (host, args, { signal, core }) => {
|
|
826
858
|
const id = String(args.task_id || '').trim();
|
|
827
859
|
const task = tasks.get(id);
|
|
828
860
|
if (!task || !tasks.isV2Task(task)) throw new Error('iris: 找不到可重新交付的 Task v2');
|
|
@@ -864,7 +896,7 @@ register('task_restore_attention', async (_host, args) => {
|
|
|
864
896
|
return { ok: true, taskId: id, text: '[iris] 已恢复任务提醒。' };
|
|
865
897
|
});
|
|
866
898
|
|
|
867
|
-
register('task_manual_retry', async (host, args, { signal }) => {
|
|
899
|
+
register('task_manual_retry', async (host, args, { signal, core }) => {
|
|
868
900
|
const id = String(args.task_id || '').trim();
|
|
869
901
|
if (args.confirm_duplicate_charge !== true) throw new Error('iris: 必须明确确认本次操作可能产生重复任务或重复费用');
|
|
870
902
|
const task = tasks.get(id);
|
|
@@ -888,7 +920,7 @@ register('task_manual_retry', async (host, args, { signal }) => {
|
|
|
888
920
|
if (task.modelRef) retryArgs.model = task.modelRef;
|
|
889
921
|
retryArgs._retryOf = id;
|
|
890
922
|
try {
|
|
891
|
-
const result = await runAction(host, action, retryArgs, { signal });
|
|
923
|
+
const result = await runAction(host, action, retryArgs, { signal, core });
|
|
892
924
|
if (result.taskId) tasks.linkManualRetry(id, result.taskId);
|
|
893
925
|
return { ...result, retryOf: id, text: (result.text || '[iris] 已创建新任务')
|
|
894
926
|
+ '\n[iris] 原任务提醒已归档,并保留新旧任务关联;本次新任务可能与远端未知任务重复。' };
|
|
@@ -1001,7 +1033,7 @@ register('tasks_purge_orphans', async () => {
|
|
|
1001
1033
|
});
|
|
1002
1034
|
|
|
1003
1035
|
/* ---------- 👁 重看(GUI:只查 iris 自己的任务产物,无会话上下文) ---------- */
|
|
1004
|
-
register('relook', async (host, args, { signal }) => {
|
|
1036
|
+
register('relook', async (host, args, { signal, core }) => {
|
|
1005
1037
|
const id = String(args.attachment_id || '').trim();
|
|
1006
1038
|
if (!id) throw new Error('iris: attachment_id 不能为空');
|
|
1007
1039
|
const question = String(args.question || '').trim();
|
|
@@ -1032,22 +1064,32 @@ register('relook', async (host, args, { signal }) => {
|
|
|
1032
1064
|
/** 列出全部供应商及其模型池(管理 GUI 用;apiKey 只给 hint) */
|
|
1033
1065
|
register('providers_list', async (host, args) => {
|
|
1034
1066
|
const list = store.allProviders();
|
|
1067
|
+
const connections = list.some(provider => provider.dshProvider) && hasHostPort(host, 'providerConnections')
|
|
1068
|
+
? await host.ports.providerConnections.list() : [];
|
|
1035
1069
|
return {
|
|
1036
1070
|
ok: true,
|
|
1071
|
+
setupSuggested: Object.keys(store.assignments()).length === 0 && list.every(provider => models.providerModels(provider).length === 0),
|
|
1037
1072
|
providers: list.map((p) => ({
|
|
1038
1073
|
id: p.id,
|
|
1039
1074
|
name: p.name || p.id,
|
|
1040
1075
|
type: p.type || 'openai',
|
|
1041
|
-
baseUrl: p.
|
|
1076
|
+
baseUrl: p.dshProvider ? (connections.find(item => item.provider === p.dshProvider.provider
|
|
1077
|
+
&& item.settingsNs === p.dshProvider.settingsNs)?.baseUrl || '') : p.baseUrl,
|
|
1078
|
+
...(p.dshProvider ? { dshProvider: p.dshProvider,
|
|
1079
|
+
connection: connections.find(item => item.provider === p.dshProvider.provider && item.settingsNs === p.dshProvider.settingsNs)
|
|
1080
|
+
|| { available: false, reason: 'DSH 原连接不可用,请检查宿主供应商设置' } } : {}),
|
|
1042
1081
|
mediaBaseUrl: p.mediaBaseUrl || '',
|
|
1043
1082
|
enabled: p.enabled !== false,
|
|
1044
1083
|
auth: p.auth === 'none' ? 'none' : 'bearer',
|
|
1045
1084
|
...selectMediaProtocol(p),
|
|
1046
1085
|
visionInput: p.visionInput || null,
|
|
1047
|
-
|
|
1048
|
-
|
|
1086
|
+
directory: modelDirectorySummary(p),
|
|
1087
|
+
apiKeyHint: p.dshProvider ? '由 DSH 管理' : String(p.apiKey || '').length <= 8 ? '****' : String(p.apiKey).slice(0, 3) + '****' + String(p.apiKey).slice(-4),
|
|
1088
|
+
models: models.providerModelsWithDirectory(p).map((m) => ({
|
|
1049
1089
|
id: m.id,
|
|
1050
1090
|
capabilities: m.capabilities,
|
|
1091
|
+
...(m.directoryOnly ? { directoryOnly: true } : {}),
|
|
1092
|
+
...(m.catalogStatus ? { catalogStatus: m.catalogStatus } : {}),
|
|
1051
1093
|
...(m.imageProtocol !== undefined ? { imageProtocol: m.imageProtocol } : {}),
|
|
1052
1094
|
...(m.musicProtocol !== undefined ? { musicProtocol: m.musicProtocol } : {}),
|
|
1053
1095
|
...(m.visionInput !== undefined ? { visionInput: m.visionInput } : {}),
|
|
@@ -1064,6 +1106,12 @@ register('providers_list', async (host, args) => {
|
|
|
1064
1106
|
};
|
|
1065
1107
|
});
|
|
1066
1108
|
|
|
1109
|
+
register('providers_host_connections', async host => {
|
|
1110
|
+
if (!hasHostPort(host, 'providerConnections')) return { ok: true, connections: [],
|
|
1111
|
+
note: '当前 DSH 未提供连接复用接口,请手动添加 Iris 供应商' };
|
|
1112
|
+
return { ok: true, connections: await host.ports.providerConnections.list() };
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1067
1115
|
/** 新增/更新供应商 */
|
|
1068
1116
|
register('providers_upsert', async (host, args) => {
|
|
1069
1117
|
const id = args.id || undefined;
|
|
@@ -1073,6 +1121,16 @@ register('providers_upsert', async (host, args) => {
|
|
|
1073
1121
|
for (const key of allowed) {
|
|
1074
1122
|
if (Object.prototype.hasOwnProperty.call(args, key)) patch[key] = args[key];
|
|
1075
1123
|
}
|
|
1124
|
+
if (Object.hasOwn(args, 'dshProvider')) {
|
|
1125
|
+
patch.dshProvider = normalizeDshProvider(args.dshProvider);
|
|
1126
|
+
const source = requireHostPort(host, 'providerConnections', '复用 DSH 供应商连接');
|
|
1127
|
+
const connection = (await source.list()).find(item => item.provider === patch.dshProvider.provider
|
|
1128
|
+
&& item.settingsNs === patch.dshProvider.settingsNs);
|
|
1129
|
+
if (!connection?.available) throw new Error(connection?.reason || 'DSH 连接不可用');
|
|
1130
|
+
if (!['dashscope', 'openai-images', 'openai-chat-images', 'openai-responses-images'].includes(patch.mediaProtocol)) {
|
|
1131
|
+
throw new Error('复用 DSH 连接时请明确选择媒体协议');
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1076
1134
|
if (!id && !Object.prototype.hasOwnProperty.call(patch, 'enabled')) patch.enabled = true;
|
|
1077
1135
|
const saved = store.upsert(patch);
|
|
1078
1136
|
return { ok: true, providerId: saved.id };
|
|
@@ -1095,33 +1153,39 @@ register('providers_set_models', async (host, args) => {
|
|
|
1095
1153
|
* 模型发现:读取供应商目录 → 明确能力元数据 → 合并模型池。
|
|
1096
1154
|
* 免费只读;拉不到/不支持则原样保留现有池(优雅降级),报人话错误。
|
|
1097
1155
|
*/
|
|
1098
|
-
register('providers_discover', async (host, args, { signal }) => {
|
|
1156
|
+
register('providers_discover', async (host, args, { signal, core }) => {
|
|
1099
1157
|
const p = store.providerById(String(args.id || ''));
|
|
1100
1158
|
if (!p) throw new Error('iris: 供应商不存在或未启用');
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
const discovered = await invokeProviderOperation(providerAdapter, 'discover', { signal });
|
|
1105
|
-
discoveredModels = discovered.models;
|
|
1106
|
-
} catch (err) {
|
|
1107
|
-
throw new Error('iris: 模型发现失败(保留现有池):' + String(err.message || err));
|
|
1108
|
-
}
|
|
1109
|
-
const entries = discoveredModels.map((model) => ({ id: model.id, capabilities: models.capabilitiesOfDiscoveredModel(model) }));
|
|
1159
|
+
const [updated] = await store.refreshProviderDirectories({ providerId: p.id, force: true, signal });
|
|
1160
|
+
if (updated.error) throw new Error('iris: 模型发现失败(保留现有池):' + updated.error);
|
|
1161
|
+
const entries = updated.directory.models;
|
|
1110
1162
|
const mediaCount = entries.filter((model) => model.capabilities.length).length;
|
|
1111
1163
|
store.mergeDiscoveredModels(p.id, entries);
|
|
1112
|
-
return { ok: true, total:
|
|
1164
|
+
return { ok: true, total: entries.length, media: mediaCount,
|
|
1113
1165
|
text: `[iris] ${p.name}:发现 ${entries.length} 个模型,${mediaCount} 个有能力声明;未标注模型请在模型池中设置能力` };
|
|
1114
1166
|
});
|
|
1115
1167
|
|
|
1168
|
+
/** 只更新目录快照;新增项在管理视图待确认,不改模型用途与功能名单。 */
|
|
1169
|
+
register('providers_sync_directory', async (host, args, { signal, core }) => {
|
|
1170
|
+
const id = String(args.id || '').trim();
|
|
1171
|
+
if (!store.providerById(id)) throw new Error('iris: 供应商不存在或连接未就绪');
|
|
1172
|
+
const [result] = await store.refreshProviderDirectories({ providerId: id, force: args.force === true, signal });
|
|
1173
|
+
if (result.error) return { ok: true, synced: false, error: result.error,
|
|
1174
|
+
text: '目录同步失败,已保留上次完整目录:' + result.error };
|
|
1175
|
+
return { ok: true, synced: true, refreshed: result.refreshed,
|
|
1176
|
+
text: result.refreshed ? '模型目录已同步;新模型仍需确认用途' : '目录仍在有效期内',
|
|
1177
|
+
directory: modelDirectorySummary(store.providerById(id)) };
|
|
1178
|
+
});
|
|
1179
|
+
|
|
1116
1180
|
/** 能力实测(红色测试图):对某供应商的 vision 模型跑 testVisionCapability */
|
|
1117
|
-
register('providers_test_vision', async (host, args, { signal }) => {
|
|
1181
|
+
register('providers_test_vision', async (host, args, { signal, core }) => {
|
|
1118
1182
|
const p = store.providerById(String(args.id || ''));
|
|
1119
1183
|
if (!p) throw new Error('iris: 供应商不存在');
|
|
1120
1184
|
const vision = models.providerModels(p).find(m => m.capabilities.includes(cap.CAPABILITIES.VISION));
|
|
1121
1185
|
if (!vision) return { ok: true, tested: false, text: '该供应商未配置 vision 模型,跳过' };
|
|
1122
1186
|
const model = vision.id;
|
|
1123
1187
|
if (args.confirm_paid !== true) throw new Error('iris: 视觉实测会发起真实供应商请求,请先明确确认');
|
|
1124
|
-
const r = await probeVisionModel(p, model, visionImageFromDataUrl(RED_TEST_IMAGE), { timeoutMs: 15000, signal });
|
|
1188
|
+
const r = await probeVisionModel(p, model, visionImageFromDataUrl(RED_TEST_IMAGE), { timeoutMs: 15000, signal, onModelAvailability: store.recordAvailability });
|
|
1125
1189
|
store.setModelVerified(p.id, model, cap.CAPABILITIES.VISION, {
|
|
1126
1190
|
ok: r.ok,
|
|
1127
1191
|
note: r.ok ? '认出红色测试图' : (r.error || '未识别出红色'),
|
|
@@ -1146,7 +1210,7 @@ async function probeModel(host, p, modelId, capability, signal) {
|
|
|
1146
1210
|
return { skipped: true, ok: false, note: 'Music 不发起小样本探测;请通过音乐功能提交实际创作要求' };
|
|
1147
1211
|
}
|
|
1148
1212
|
if (capability === cap.CAPABILITIES.VISION) {
|
|
1149
|
-
const r = await probeVisionModel(p, modelId, visionImageFromDataUrl(RED_TEST_IMAGE), { timeoutMs: 15000, signal });
|
|
1213
|
+
const r = await probeVisionModel(p, modelId, visionImageFromDataUrl(RED_TEST_IMAGE), { timeoutMs: 15000, signal, onModelAvailability: store.recordAvailability });
|
|
1150
1214
|
return {
|
|
1151
1215
|
ok: r.ok,
|
|
1152
1216
|
note: r.ok ? '认出红色测试图' : (r.error || '未识别出红色'),
|
|
@@ -1158,6 +1222,8 @@ async function probeModel(host, p, modelId, capability, signal) {
|
|
|
1158
1222
|
return { skipped: true, ok: false, note: '需真实音频;可用 CLI models test --capability transcribe --input 提供素材进行实测恢复' };
|
|
1159
1223
|
}
|
|
1160
1224
|
if (capability === cap.CAPABILITIES.TTS) {
|
|
1225
|
+
const issue = selectMediaProtocol(p).mediaProtocol === 'dashscope' && dashscopeTtsInputIssue(modelId);
|
|
1226
|
+
if (issue) return { skipped: true, ok: false, note: issue };
|
|
1161
1227
|
const providerAdapter = createConfiguredProviderAdapter(p, { allowRateLimited: true });
|
|
1162
1228
|
const submitted = await invokeProviderOperation(providerAdapter, 'submit', {
|
|
1163
1229
|
capability: 'tts', model: modelId, input: { text: '你好' }, signal
|
|
@@ -1231,6 +1297,14 @@ register('providers_add_model', async (host, args) => {
|
|
|
1231
1297
|
return { ok: true, text: `[iris] 已添加模型 ${modelId}(能力:${(caps || []).join(',') || '未标注'})` };
|
|
1232
1298
|
});
|
|
1233
1299
|
|
|
1300
|
+
register('providers_confirm_model', async (_host, args) => {
|
|
1301
|
+
const entry = store.confirmModelUsage(args.id, args.model_id, args.capabilities, {
|
|
1302
|
+
enableAutomatic: args.enableAutomatic, musicProtocol: args.musicProtocol
|
|
1303
|
+
});
|
|
1304
|
+
return { ok: true, model_id: entry.id, text: args.enableAutomatic
|
|
1305
|
+
? '已确认模型用途,并启用所选功能的自动池' : '已确认模型用途;已有调用方式保持' };
|
|
1306
|
+
});
|
|
1307
|
+
|
|
1234
1308
|
/** 从池移除模型 */
|
|
1235
1309
|
register('providers_remove_model', async (host, args) => {
|
|
1236
1310
|
const ok = store.removeProviderModel(String(args.id || ''), String(args.model_id || ''));
|
|
@@ -1266,7 +1340,7 @@ register('providers_set_model_vision_input', async (host, args) => {
|
|
|
1266
1340
|
});
|
|
1267
1341
|
|
|
1268
1342
|
/** 逐模型实测某能力 → 存 verified("test it, don't guess it";用户手动触发,发现不自动跑) */
|
|
1269
|
-
register('providers_test_model', async (host, args, { signal }) => {
|
|
1343
|
+
register('providers_test_model', async (host, args, { signal, core }) => {
|
|
1270
1344
|
const p = store.providerById(String(args.id || ''));
|
|
1271
1345
|
if (!p) throw new Error('iris: 供应商不存在或未启用');
|
|
1272
1346
|
const modelId = String(args.model_id || '').trim();
|
|
@@ -1303,22 +1377,62 @@ register('providers_test_model', async (host, args, { signal }) => {
|
|
|
1303
1377
|
register('assignments_get', async (host, args) => {
|
|
1304
1378
|
const providers = store.allProviders();
|
|
1305
1379
|
const configured = new Set(store.providers().map(provider => provider.id));
|
|
1380
|
+
const connections = providers.some(provider => provider.dshProvider) && hasHostPort(host, 'providerConnections')
|
|
1381
|
+
? await host.ports.providerConnections.list() : [];
|
|
1382
|
+
for (const provider of providers) {
|
|
1383
|
+
if (provider.enabled !== false && provider.dshProvider && connections.some(item => item.available
|
|
1384
|
+
&& item.provider === provider.dshProvider.provider && item.settingsNs === provider.dshProvider.settingsNs)) {
|
|
1385
|
+
configured.add(provider.id);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
const previewProviders = providers.map(provider => provider.dshProvider ? { ...provider,
|
|
1389
|
+
baseUrl: connections.find(item => item.provider === provider.dshProvider.provider
|
|
1390
|
+
&& item.settingsNs === provider.dshProvider.settingsNs)?.baseUrl } : provider);
|
|
1306
1391
|
const pool = models.modelPool(providers, { includeHostVision: true });
|
|
1307
1392
|
const byCap = {};
|
|
1308
1393
|
for (const m of pool) {
|
|
1309
1394
|
for (const c of m.capabilities) {
|
|
1310
1395
|
if (!byCap[c]) byCap[c] = [];
|
|
1311
|
-
const provider =
|
|
1396
|
+
const provider = previewProviders.find(provider => provider.id === m.providerId);
|
|
1397
|
+
const hostResource = m.ref === models.HOST_VISION_REF;
|
|
1398
|
+
const protocolSupported = hostResource || supportsModelCapability(provider, m.id, c);
|
|
1399
|
+
const modelUnavailable = m.unavailableCapabilities?.includes(c) === true;
|
|
1400
|
+
const connected = hostResource ? hasHostPort(host, 'textModel') : configured.has(m.providerId);
|
|
1312
1401
|
byCap[c].push({ ref: m.ref, id: m.id, providerId: m.providerId,
|
|
1313
1402
|
label: m.ref === models.HOST_VISION_REF ? 'DSH 当前默认模型' : m.id + ' · ' + m.providerId,
|
|
1314
|
-
unavailable:
|
|
1403
|
+
unavailable: !connected || !protocolSupported || m.rateLimited === true || modelUnavailable,
|
|
1404
|
+
modelUnavailable,
|
|
1405
|
+
pendingConfirmation: m.source === 'discovered', protocolSupported,
|
|
1406
|
+
reason: !connected ? '连接未就绪,请检查供应商或宿主服务' : !protocolSupported
|
|
1407
|
+
? (c === cap.CAPABILITIES.TTS && selectMediaProtocol(provider).mediaProtocol === 'dashscope'
|
|
1408
|
+
&& dashscopeTtsProtocolIssue(m.id)) || '当前接口协议不支持此功能'
|
|
1409
|
+
: modelUnavailable ? modelUnavailableReason() : m.rateLimited ? '限流冷却或额度不可用,请稍后或检查账号' : m.source === 'discovered' ? '已发现,待确认模型用途' : '',
|
|
1315
1410
|
...(c === cap.CAPABILITIES.IMAGE && provider ? { imageRouting: imageRoutingForModel(provider, m.id) } : {}) });
|
|
1316
1411
|
}
|
|
1317
1412
|
}
|
|
1318
1413
|
// order:归一化的有序分配列表(GUI 直接渲染 failover 顺序,无需处理单字符串旧格式)
|
|
1319
1414
|
const order = {};
|
|
1320
1415
|
for (const c of Object.values(cap.CAPABILITIES)) order[c] = store.assignmentOrder(c);
|
|
1321
|
-
|
|
1416
|
+
const policies = {}, readiness = {};
|
|
1417
|
+
for (const capability of Object.values(cap.CAPABILITIES)) {
|
|
1418
|
+
const assignment = store.assignments()[capability];
|
|
1419
|
+
const policy = models.functionPolicy(assignment, capability);
|
|
1420
|
+
policies[capability] = policy;
|
|
1421
|
+
if (policy.mode === 'auto') order[capability] = models.orderedModels(
|
|
1422
|
+
models.functionModelPool(previewProviders, capability, assignment, { includeHostVision: true }),
|
|
1423
|
+
capability, assignment, { includeRateLimited: true, includeUnavailable: true }).map(model => model.ref);
|
|
1424
|
+
const options = byCap[capability] || [];
|
|
1425
|
+
const candidates = options.filter(model => order[capability].includes(model.ref) && !model.unavailable);
|
|
1426
|
+
let note = '';
|
|
1427
|
+
if (policy.mode === 'disabled') note = '此功能已停用,可选择自动池或手选模型';
|
|
1428
|
+
else if (policy.mode === 'manual' && !order[capability].length) note = '尚未选择模型,可添加所选项或改用自动池';
|
|
1429
|
+
else if (!options.length) note = '请先添加模型,并确认它可用于此功能';
|
|
1430
|
+
else if (!candidates.length) note = options.some(model => model.pendingConfirmation)
|
|
1431
|
+
? '发现的模型尚待确认用途,请在供应商中确认功能'
|
|
1432
|
+
: options.find(model => model.reason)?.reason || '当前没有匹配的候选,请检查功能范围和模型';
|
|
1433
|
+
readiness[capability] = { mode: policy.mode, candidateCount: candidates.length, note };
|
|
1434
|
+
}
|
|
1435
|
+
return { ok: true, assignments: store.assignments(), order, poolByCapability: byCap, policies, readiness };
|
|
1322
1436
|
});
|
|
1323
1437
|
|
|
1324
1438
|
/**
|
|
@@ -1329,6 +1443,10 @@ register('assignments_get', async (host, args) => {
|
|
|
1329
1443
|
register('assignments_set', async (host, args) => {
|
|
1330
1444
|
const capability = String(args.capability || '').trim();
|
|
1331
1445
|
if (!capability) throw new Error('iris: capability 不能为空');
|
|
1446
|
+
if (Object.hasOwn(args, 'mode')) {
|
|
1447
|
+
const policy = store.setFunctionPolicy(capability, args.mode, { hostVision: args.hostVision });
|
|
1448
|
+
return { ok: true, capability, mode: policy.mode, model_refs: store.assignmentOrder(capability) };
|
|
1449
|
+
}
|
|
1332
1450
|
const listArg = Array.isArray(args.model_refs) ? args.model_refs : args.model_ids;
|
|
1333
1451
|
if (Array.isArray(listArg)) {
|
|
1334
1452
|
const refs = listArg.map((x) => String(x || '').trim()).filter(Boolean);
|
|
@@ -1378,7 +1496,7 @@ register('attachments_list', async (host, args) => {
|
|
|
1378
1496
|
* 把一个附件(会话 image 或 iris 产物)导出为宿主可读的文件路径,供工具消费。
|
|
1379
1497
|
* iris 产物直接返回其 outputs 路径;会话附件读字节存 uploads/(按 attachmentId 哈希缓存,不重复落盘)。
|
|
1380
1498
|
*/
|
|
1381
|
-
register('attachment_export', async (host, args, { signal }) => {
|
|
1499
|
+
register('attachment_export', async (host, args, { signal, core }) => {
|
|
1382
1500
|
const id = String(args.attachment_id || '').trim();
|
|
1383
1501
|
if (!id) throw new Error('iris: attachment_id 不能为空');
|
|
1384
1502
|
// 1) iris 自有产物:文件已在 outputs/
|
|
@@ -1409,7 +1527,7 @@ register('attachment_export', async (host, args, { signal }) => {
|
|
|
1409
1527
|
|
|
1410
1528
|
/* ============ 音频转写(阶段 7.2) ============ */
|
|
1411
1529
|
|
|
1412
|
-
register('transcribe', async (host, args, { signal }) => {
|
|
1530
|
+
register('transcribe', async (host, args, { signal, core }) => {
|
|
1413
1531
|
const normalized = normalizeGenerationInput('transcribe', {
|
|
1414
1532
|
audio_url: args.audio_url, audio_path: args.audio_path
|
|
1415
1533
|
}, { allowAudioPath: true });
|
|
@@ -1418,7 +1536,7 @@ register('transcribe', async (host, args, { signal }) => {
|
|
|
1418
1536
|
const routes = providers.map((provider) =>
|
|
1419
1537
|
simpleModelRoute(provider, args, cap.CAPABILITIES.TRANSCRIBE)
|
|
1420
1538
|
);
|
|
1421
|
-
const submitted = await submitCoreTranscribe(routes, normalized, signal);
|
|
1539
|
+
const submitted = await submitCoreTranscribe(core, routes, normalized, signal);
|
|
1422
1540
|
const firstArtifactId = submitted.task.artifactIds[0];
|
|
1423
1541
|
const ready = submitted.task.outcome === 'succeeded'
|
|
1424
1542
|
&& submitted.task.deliveryState === 'ready' && Boolean(firstArtifactId);
|
|
@@ -1435,8 +1553,8 @@ register('transcribe', async (host, args, { signal }) => {
|
|
|
1435
1553
|
|
|
1436
1554
|
/* ============ 视频抽帧(阶段 7.1) ============ */
|
|
1437
1555
|
|
|
1438
|
-
register('video_frames', async (host, args, { signal }) => {
|
|
1439
|
-
const { media: meta, frames } = await framesForDsh({
|
|
1556
|
+
register('video_frames', async (host, args, { signal, core }) => {
|
|
1557
|
+
const { media: meta, frames } = await core.framesForDsh({
|
|
1440
1558
|
videoPath: args.video_path, maxFrames: args.max_frames, targetWidth: args.target_width,
|
|
1441
1559
|
format: optionalActionText(args.format), quality: args.quality, signal
|
|
1442
1560
|
});
|
|
@@ -1456,7 +1574,7 @@ register('video_frames', async (host, args, { signal }) => {
|
|
|
1456
1574
|
|
|
1457
1575
|
/* ============ 多模态视频摘要(阶段 7.3,GUI 同步快速版) ============ */
|
|
1458
1576
|
|
|
1459
|
-
register('media_summarize', async (host, args, { signal }) => {
|
|
1577
|
+
register('media_summarize', async (host, args, { signal, core }) => {
|
|
1460
1578
|
const source = normalizeSummarySource(args);
|
|
1461
1579
|
const p = String(args.video_path || '').trim();
|
|
1462
1580
|
if (source === 'video') {
|
|
@@ -1465,11 +1583,11 @@ register('media_summarize', async (host, args, { signal }) => {
|
|
|
1465
1583
|
if (!ffmpegAvailable()) throw new Error('iris: ffmpeg/ffprobe 不可用,无法分析视频(可选安装 ffmpeg 后启用)');
|
|
1466
1584
|
}
|
|
1467
1585
|
const { answer, via, model, sheet, meta, frames } = await runSummaryRequest(host, {
|
|
1468
|
-
onRateLimit: store.recordRateLimit,
|
|
1586
|
+
onRateLimit: store.recordRateLimit, onModelAvailability: store.recordAvailability,
|
|
1469
1587
|
providers: store.pickAllFor(cap.CAPABILITIES.VISION), model: args.model, question: args.question, signal,
|
|
1470
1588
|
async prepareMedia(operationSignal) {
|
|
1471
1589
|
if (source === 'core-artifacts') {
|
|
1472
|
-
return { ...await summaryFramesForDsh({ frameArtifactIds: args.frame_artifact_ids, signal: operationSignal }), transcript: args.transcribe_text };
|
|
1590
|
+
return { ...await core.summaryFramesForDsh({ frameArtifactIds: args.frame_artifact_ids, signal: operationSignal }), transcript: args.transcribe_text };
|
|
1473
1591
|
}
|
|
1474
1592
|
const meta = probeVideo(p);
|
|
1475
1593
|
const frames = await extractFrames({ inputPath: p, maxFrames: Math.min(Number(args.max_frames) || 8, 12),
|