@mokuyoaxis/dsh-iris 0.3.0-alpha.1 → 0.3.0-beta.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 +63 -1
- package/README.en.md +20 -13
- package/README.md +20 -13
- package/bin/dsh-iris.js +143 -51
- package/docs/AGENT_CORE_QUICKSTART.md +2 -2
- package/docs/AGENT_QUICKSTART.md +1 -1
- package/docs/ARCHITECTURE.md +5 -5
- package/docs/ARTIFACT_MANIFEST.md +4 -0
- package/docs/AUDIO_FLOWS.md +20 -4
- package/docs/BETA_SCOPE.md +42 -0
- package/docs/CONTACT_SHEET.md +1 -1
- package/docs/CORE_CONNECTIONS.md +3 -1
- package/docs/CORE_QUICKSTART.md +7 -5
- package/docs/CORE_RUNTIME_CONTRACT.md +3 -3
- package/docs/CORE_SERVICE.md +9 -5
- package/docs/CORE_SKILLS.md +2 -2
- package/docs/DSH_COMPATIBILITY.md +11 -1
- package/docs/DSH_CORE_MIGRATION.md +6 -0
- package/docs/DSH_RUNTIME.md +2 -2
- package/docs/HEADLESS_CLI.md +29 -4
- package/docs/HOST_ADAPTER_CONTRACT.md +5 -3
- package/docs/IMAGE_FLOW.md +2 -2
- package/docs/IMAGE_GENERATION.md +1 -1
- package/docs/LOCAL_API.md +2 -2
- package/docs/LOCAL_API_CONTRACT.md +17 -15
- package/docs/LOCAL_CREATION.md +2 -2
- package/docs/LOCAL_OPERATIONS.md +2 -2
- package/docs/LOCAL_SELECTION.md +1 -1
- package/docs/LOCAL_WEB.md +11 -5
- package/docs/MCP.md +30 -9
- package/docs/MEDIA_SCHEDULING.md +72 -0
- package/docs/NETWORK_RESOURCES.md +106 -0
- package/docs/NETWORK_ROUTING.md +103 -0
- package/docs/NETWORK_WORKBENCH.md +65 -0
- package/docs/PANEL_EXTENSIONS.md +1 -1
- package/docs/PLUGINS.md +9 -5
- package/docs/PLUGIN_DEVELOPMENT.md +39 -0
- package/docs/PLUGIN_STARTUP.md +15 -2
- package/docs/PROVIDER_ADAPTER_CONTRACT.md +8 -2
- package/docs/PROVIDER_CREDENTIALS.md +67 -0
- package/docs/PROVIDER_EXTENSIONS.md +8 -4
- package/docs/PROVIDER_HEALTH.md +4 -2
- package/docs/PROVIDER_NETWORK.md +45 -0
- package/docs/PROVIDER_PLUGIN_CONTRACT.md +24 -0
- package/docs/PROVIDER_SETTINGS.md +13 -7
- package/docs/PROVIDER_SUBMISSION_CONTRACT.md +3 -1
- package/docs/ROADMAP.md +6 -6
- package/docs/SECURITY.md +1 -1
- package/docs/SYNC_DELIVERY_RECOVERY.md +5 -1
- package/docs/VIDEO_FLOWS.md +89 -0
- package/docs/VISION_MODEL.md +2 -0
- package/docs/WEB_PROTOTYPE.md +3 -3
- package/docs/WEB_RECOVERY.md +2 -2
- package/docs/WORKBENCH_ARTIFACTS.md +6 -0
- package/docs/WORKBENCH_LOGIN.md +2 -2
- package/docs/releases/0.3.0-beta.1.md +34 -0
- package/docs/screenshots.md +2 -2
- package/lib/actions.js +5 -4
- package/lib/adapters.js +58 -41
- package/lib/api.js +23 -13
- package/lib/application-runtime.js +44 -15
- package/lib/application-writer.js +193 -0
- package/lib/client.d.ts +3 -3
- package/lib/client.js +62 -25
- package/lib/comfyui-adapter.js +3 -2
- package/lib/comfyui-client.js +10 -1
- package/lib/command-service.js +18 -5
- package/lib/config.js +13 -9
- package/lib/configured-image-runtime.js +28 -9
- package/lib/connection-skill.js +9 -0
- package/lib/core-artifact-store.js +68 -26
- package/lib/core-bridge-tools.js +5 -3
- package/lib/core-capability-catalog.js +21 -11
- package/lib/core-delivery-cache.js +8 -3
- package/lib/core-service-client.js +6 -2
- package/lib/core-tasks.js +86 -11
- package/lib/creation-request-store.js +8 -5
- package/lib/doctor.js +2 -2
- package/lib/dsh-core-adapter.js +61 -9
- package/lib/dsh-provider-config.js +1 -1
- package/lib/dsh-service-bridge.js +1 -0
- package/lib/flow-store.js +64 -17
- package/lib/image-flow.js +39 -17
- package/lib/index.js +10 -5
- package/lib/local-api-projection.js +6 -2
- package/lib/local-api.js +137 -49
- package/lib/local-creation.js +12 -5
- package/lib/local-operations.js +2 -2
- package/lib/local-request-root.js +2 -2
- package/lib/local-web-assets.js +2 -0
- package/lib/mcp-http.js +82 -9
- package/lib/mcp-server.js +23 -1
- package/lib/media-scheduler.js +194 -0
- package/lib/model-directory-service.js +25 -8
- package/lib/music-transport.js +4 -2
- package/lib/network-config-service.js +123 -0
- package/lib/network-config.js +127 -0
- package/lib/network-doctor.js +94 -0
- package/lib/network-management.js +51 -0
- package/lib/network-policy.js +19 -0
- package/lib/network-route-evidence.js +38 -0
- package/lib/network-routing.js +283 -0
- package/lib/operation-runner.js +83 -38
- package/lib/operation-store.js +6 -6
- package/lib/plugins.d.ts +25 -6
- package/lib/plugins.js +2 -1
- package/lib/provider-adapter.js +2 -2
- package/lib/provider-adapters.js +37 -5
- package/lib/provider-catalog.js +33 -16
- package/lib/provider-config-format.js +51 -0
- package/lib/provider-config-service.js +52 -22
- package/lib/provider-context.d.ts +90 -0
- package/lib/provider-context.js +42 -0
- package/lib/provider-credentials.js +97 -0
- package/lib/provider-diagnostics.js +5 -5
- package/lib/provider-health.js +10 -5
- package/lib/provider-monitor.js +5 -3
- package/lib/provider-network.js +52 -0
- package/lib/provider-protocol.js +3 -2
- package/lib/provider-registry.js +15 -5
- package/lib/provider-response.js +10 -0
- package/lib/provider-settings.js +14 -12
- package/lib/provider-task-runner.js +113 -37
- package/lib/provider-transport.js +16 -0
- package/lib/render.js +11 -1
- package/lib/scheduling-policy.js +66 -0
- package/lib/tasks.js +3 -2
- package/lib/video-flow-input.js +16 -0
- package/lib/video-input.js +12 -5
- package/lib/vision-model-routing.js +2 -0
- package/lib/web/app.js +107 -46
- package/lib/web/connections.js +151 -12
- package/lib/web/creation.js +29 -25
- package/lib/web/flow-view.js +4 -3
- package/lib/web/gallery.js +17 -16
- package/lib/web/index.html +58 -5
- package/lib/web/media-client.js +18 -4
- package/lib/web/network.js +232 -0
- package/lib/web/plugin-panel.js +4 -2
- package/lib/web/plugin-status.js +140 -0
- package/lib/web/providers.js +34 -13
- package/lib/web/recovery.js +2 -2
- package/lib/web/styles.css +54 -1
- package/lib/web/transport.js +6 -1
- package/lib/workbench-artifacts.js +3 -2
- package/package.json +5 -2
- package/skills/iris-core/references/comfyui.md +1 -1
- package/templates/plugin-starter/LICENSE +21 -0
- package/templates/plugin-starter/README.md +74 -0
- package/templates/plugin-starter/assets/peach.png +0 -0
- package/templates/plugin-starter/assets/violet.png +0 -0
- package/templates/plugin-starter/index.mjs +27 -0
- package/templates/plugin-starter/package.json +18 -0
- package/templates/plugin-starter/scripts/core-cli.mjs +20 -0
- package/templates/plugin-starter/scripts/environment.mjs +65 -0
- package/templates/plugin-starter/scripts/mcp-probe.mjs +22 -0
- package/templates/plugin-starter/scripts/setup.mjs +42 -0
- package/templates/plugin-starter/scripts/smoke.mjs +76 -0
- package/templates/plugin-starter/web/board.css +27 -0
- package/templates/plugin-starter/web/board.mjs +160 -0
- package/templates/plugin-starter/web/png.mjs +23 -0
- package/templates/provider-starter/LICENSE +21 -0
- package/templates/provider-starter/README.md +50 -0
- package/templates/provider-starter/index.d.mts +5 -0
- package/templates/provider-starter/index.mjs +165 -0
- package/templates/provider-starter/package.json +13 -0
- package/templates/provider-starter/scripts/core-cli.mjs +22 -0
- package/templates/provider-starter/scripts/environment.mjs +122 -0
- package/templates/provider-starter/scripts/fixture.mjs +54 -0
- package/templates/provider-starter/scripts/mcp-probe.mjs +21 -0
- package/templates/provider-starter/scripts/protocol-smoke.mjs +48 -0
- package/templates/provider-starter/scripts/setup.mjs +44 -0
- package/templates/provider-starter/scripts/smoke.mjs +145 -0
- package/user_guide.md +3 -3
package/lib/web/connections.js
CHANGED
|
@@ -6,12 +6,61 @@ const element = (tag, text, className) => { const node = document.createElement(
|
|
|
6
6
|
const checked = selector => [...document.querySelectorAll(selector + ':checked')].map(input => input.value);
|
|
7
7
|
const date = value => new Date(value).toLocaleString('zh-CN');
|
|
8
8
|
|
|
9
|
+
function selectedToolNames(record, catalog) {
|
|
10
|
+
const controls = catalog.controlTools || catalog.tools.filter(tool => !catalog.groups.some(group => group.tools.includes(tool.name))).map(tool => tool.name);
|
|
11
|
+
return new Set([...controls, ...catalog.groups.filter(group => record.capabilities.includes(group.id)).flatMap(group => group.tools),
|
|
12
|
+
...(record.pluginOperations?.length ? catalog.pluginControlTools || [] : [])]);
|
|
13
|
+
}
|
|
14
|
+
function effectiveTools(record, catalog) {
|
|
15
|
+
const selected = selectedToolNames(record, catalog);
|
|
16
|
+
const definitions = new Map([...catalog.tools, ...(catalog.pluginTools || [])].map(tool => [tool.name, tool]));
|
|
17
|
+
return record.tools.filter(name => selected.has(name) && catalog.availableTools.includes(name)
|
|
18
|
+
&& definitions.has(name) && record.scopes.includes(definitions.get(name).scope) && catalog.availableScopes.includes(definitions.get(name).scope));
|
|
19
|
+
}
|
|
20
|
+
const samePin = (a, b) => ['contractVersion', 'pluginId', 'pluginVersion', 'kind', 'contributionId', 'contributionVersion'].every(key => a[key] === b[key]);
|
|
21
|
+
|
|
22
|
+
/** Owner GET facts only. This is a configuration check, never a transport probe or authority. */
|
|
23
|
+
export function inspectMcpSetup(record, catalog, { transport = 'stdio', location = 'same' } = {}) {
|
|
24
|
+
if (catalog.contractVersion !== 0) throw new TypeError('不支持的连接管理版本');
|
|
25
|
+
const issues = [], issue = (code, message) => issues.push({ code, message });
|
|
26
|
+
const revoked = Boolean(record.revokedAt || record.state === 'revoked');
|
|
27
|
+
if (location !== 'same') issue('location', '当前导出使用 Core 主机的回环地址与本机路径,不能直接交给另一台机器上的客户端。');
|
|
28
|
+
if (transport === 'http' && !catalog.httpEnabled) issue('transport', '此服务没有开启 HTTP MCP;可选本机 stdio,或在下次明确启动服务时开启 HTTP。');
|
|
29
|
+
if (revoked) issue('revoked', '此连接已撤销,旧配置不能继续使用。需要接入时请新建连接。');
|
|
30
|
+
if (record.scopes.some(scope => !catalog.availableScopes.includes(scope))) issue('scope', '保存的操作权限超出此服务当前开放的范围,请核对服务开关与连接设置。');
|
|
31
|
+
if (record.capabilities.some(id => !catalog.groups.some(group => group.id === id))) issue('capability', '此工作区没有开放全部已选媒体分组,原授权未被自动替换。');
|
|
32
|
+
for (const pin of record.pluginOperations || []) {
|
|
33
|
+
const entry = catalog.contributions.operations.find(item => item.id === pin.contributionId);
|
|
34
|
+
if (!entry) issue('plugin', pin.contributionId + ':业务未加载,历史仍保留,请核对准确插件版本。');
|
|
35
|
+
else if (!samePin(entry.dependency, pin)) issue('plugin', pin.contributionId + ':当前业务与保存的准确依赖版本不一致。');
|
|
36
|
+
else if (entry.access.mcp !== 'requires-selection') issue('plugin', pin.contributionId + ':此服务尚未启用该业务;历史读取可能仍可用。');
|
|
37
|
+
}
|
|
38
|
+
const tools = revoked ? [] : effectiveTools(record, catalog);
|
|
39
|
+
if (!revoked && !tools.length) issue('tools', '当前选择没有可用工具,请明确选择功能、工具与对应权限;不会自动补齐。');
|
|
40
|
+
return { ok: issues.length === 0, issues, tools, mediaTested: false, transportTested: false };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A raw bridge/self-check handshake is not an MCP client's declaration. No online inference. */
|
|
44
|
+
export function mcpClientEvidence(record, catalog) {
|
|
45
|
+
if (record.revokedAt || record.state === 'revoked') return { state: 'revoked', label: '连接已撤销', instances: [] };
|
|
46
|
+
const instances = (record.instances || []).filter(instance => instance.declaredClient?.source === 'client-declared').map(instance => ({
|
|
47
|
+
state: instance.state, at: instance.declaredClient.at, clientInfo: instance.declaredClient.clientInfo,
|
|
48
|
+
transport: instance.declaredClient.transport, tools: effectiveTools(instance, catalog) }));
|
|
49
|
+
if (instances.some(instance => instance.state === 'connected')) return { state: 'observed', label: '收到客户端握手与声明', instances };
|
|
50
|
+
if (instances.length) return { state: 'disconnected', label: '客户端握手已断开', instances };
|
|
51
|
+
if (record.lastContact) return { state: 'historical', label: '有历史客户端声明', instances, contact: record.lastContact };
|
|
52
|
+
if (record.instances?.some(instance => instance.state === 'connected')) return { state: 'bridge', label: '仅收到桥接握手', instances };
|
|
53
|
+
return { state: 'waiting', label: '等待客户端握手与声明', instances };
|
|
54
|
+
}
|
|
55
|
+
|
|
9
56
|
export function createConnectionPanel({ describe }) {
|
|
10
57
|
let client, epoch = 0, catalog, records = [], selected = null, key, busy = false, visible = false, viewTicket = 0;
|
|
11
|
-
let scopes = {}
|
|
58
|
+
let scopes = {};
|
|
12
59
|
let capabilities = [], tools = [], pluginOperations = [], allowedScopes = ['read'], templates = [], skillBundle = null;
|
|
13
60
|
let view = 'settings', exportDelivery = null;
|
|
14
61
|
let formDirty = false;
|
|
62
|
+
const exports = new Map(), inspections = new Map();
|
|
63
|
+
let checkController = null;
|
|
15
64
|
const closeButton = element('button', '返回列表', 'quiet'); closeButton.type = 'button'; closeButton.id = 'connection-cancel';
|
|
16
65
|
const closeArea = element('div', '', 'provider-footer'); closeArea.append(closeButton); $('#connection-editor').append(closeArea);
|
|
17
66
|
const urls = new Set();
|
|
@@ -19,11 +68,13 @@ export function createConnectionPanel({ describe }) {
|
|
|
19
68
|
const valid = (at, connection) => at === epoch && client === connection;
|
|
20
69
|
function editor(open) { $('#connection-editor').hidden = !open; $('#connections-overview').hidden = open; }
|
|
21
70
|
function setView(next) {
|
|
71
|
+
if (next !== view) cancelCheck();
|
|
22
72
|
if (next !== view) clearExport(); view = next;
|
|
23
73
|
for (const button of document.querySelectorAll('[data-connection-view]')) {
|
|
24
74
|
const active = button.dataset.connectionView === view; button.setAttribute('aria-pressed', String(active));
|
|
25
75
|
$('#connection-view-' + button.dataset.connectionView).hidden = !active;
|
|
26
76
|
}
|
|
77
|
+
renderOnboarding(); controls();
|
|
27
78
|
}
|
|
28
79
|
function feedback(text = '', error = false) {
|
|
29
80
|
const node = $('#connections-feedback'); node.hidden = !text; node.textContent = text; node.classList.toggle('error', error);
|
|
@@ -35,6 +86,7 @@ export function createConnectionPanel({ describe }) {
|
|
|
35
86
|
$('#connection-export-note').textContent = '';
|
|
36
87
|
skillBundle = null; $('#connection-skill-preview').value = ''; $('#connection-skill-result').hidden = true;
|
|
37
88
|
for (const url of urls) URL.revokeObjectURL(url); urls.clear();
|
|
89
|
+
renderOnboarding();
|
|
38
90
|
}
|
|
39
91
|
function controls() {
|
|
40
92
|
const record = current(), editable = !record || record.managed && !record.revokedAt;
|
|
@@ -46,7 +98,11 @@ export function createConnectionPanel({ describe }) {
|
|
|
46
98
|
$('#connection-back').disabled = busy;
|
|
47
99
|
closeButton.disabled = busy;
|
|
48
100
|
for (const node of document.querySelectorAll('#connections-list .connection-item')) node.disabled = busy;
|
|
49
|
-
for (const button of document.querySelectorAll('[data-connection-view]')) button.disabled = busy || button.dataset.connectionView === 'delivery' && (!record?.managed || Boolean(record.revokedAt)) || button.dataset.connectionView
|
|
101
|
+
for (const button of document.querySelectorAll('[data-connection-view]')) button.disabled = busy || button.dataset.connectionView === 'delivery' && (!record?.managed || Boolean(record.revokedAt)) || ['details', 'verification'].includes(button.dataset.connectionView) && !record;
|
|
102
|
+
$('#connection-export-button').disabled ||= formDirty || $('#connection-location').value !== 'same';
|
|
103
|
+
$('#connection-check').disabled = busy || !catalog || !record || formDirty;
|
|
104
|
+
$('#connection-location').disabled = busy;
|
|
105
|
+
$('#connection-verify-next').disabled = busy || !record;
|
|
50
106
|
}
|
|
51
107
|
function renderList() {
|
|
52
108
|
$('#connections-count').textContent = String(records.filter(record => record.state !== 'revoked').length);
|
|
@@ -70,8 +126,7 @@ export function createConnectionPanel({ describe }) {
|
|
|
70
126
|
if (note) text.append(element('small', note)); label.append(input, text); return label;
|
|
71
127
|
}
|
|
72
128
|
function enabledTools() {
|
|
73
|
-
return new Set([...
|
|
74
|
-
...(pluginOperations.length ? catalog.pluginControlTools || [] : [])].filter(name => !catalog.availableTools || catalog.availableTools.includes(name)));
|
|
129
|
+
return new Set([...selectedToolNames({ capabilities, pluginOperations }, catalog)].filter(name => !catalog.availableTools || catalog.availableTools.includes(name)));
|
|
75
130
|
}
|
|
76
131
|
function renderTools() {
|
|
77
132
|
const enabled = enabledTools();
|
|
@@ -160,6 +215,7 @@ export function createConnectionPanel({ describe }) {
|
|
|
160
215
|
transportNote();
|
|
161
216
|
renderOptions(); renderList();
|
|
162
217
|
formDirty = false; $('#connection-leave-confirm').hidden = true; closeButton.textContent = '返回列表';
|
|
218
|
+
renderOnboarding(); controls();
|
|
163
219
|
}
|
|
164
220
|
async function reload(connection) {
|
|
165
221
|
const result = await connection.get(BASE + '/connections');
|
|
@@ -187,7 +243,6 @@ export function createConnectionPanel({ describe }) {
|
|
|
187
243
|
catalog = { ...value, tools: [...value.tools, ...(value.pluginTools || [])] };
|
|
188
244
|
// A running preview may still serve the earlier v0 DTO. Derive safe choices from its declarations.
|
|
189
245
|
scopes = Object.fromEntries((catalog.scopeOptions || catalog.availableScopes.map(id => ({ id, title: id }))).map(item => [item.id, item.title]));
|
|
190
|
-
control = catalog.controlTools || catalog.tools.filter(tool => !catalog.groups.some(group => group.tools.includes(tool.name))).map(tool => tool.name);
|
|
191
246
|
renderContributions();
|
|
192
247
|
}
|
|
193
248
|
function transportNote() {
|
|
@@ -220,6 +275,79 @@ export function createConnectionPanel({ describe }) {
|
|
|
220
275
|
const formats = value.formats || ['mcpServers', 'entry'];
|
|
221
276
|
for (const option of $('#connection-format').options) option.disabled = !formats.includes(option.value);
|
|
222
277
|
}
|
|
278
|
+
const choiceKey = () => [$('#connection-location').value, $('#connection-transport').value, $('#connection-format').value].join(':');
|
|
279
|
+
function renderOnboarding() {
|
|
280
|
+
const record = current();
|
|
281
|
+
const scopeTitles = allowedScopes.map(id => scopes[id] || id);
|
|
282
|
+
const names = (catalog?.groups || []).filter(group => capabilities.includes(group.id)).map(group => group.label || group.title);
|
|
283
|
+
for (const pin of pluginOperations) names.push(catalog?.contributions?.operations?.find(item => samePin(item.dependency, pin))?.title || pin.contributionId);
|
|
284
|
+
$('#connection-grant-summary').textContent = (names.join('、') || '仅控制工具') + ' · ' + scopeTitles.join('、');
|
|
285
|
+
$('#connection-unsaved-note').hidden = !formDirty;
|
|
286
|
+
$('#connection-location-note').textContent = $('#connection-location').value === 'same'
|
|
287
|
+
? '这里指运行 Agent 的机器,不是打开此网页的手机或电脑。两种传输都需要 Core 保持运行。'
|
|
288
|
+
: '当前配置含 Core 主机的回环地址或本机路径,不能直接交给另一台机器上的客户端。请先确认部署方案;本页不会开启公网访问。';
|
|
289
|
+
const exported = record && exports.get(record.connectionId), inspected = record && inspections.get(record.connectionId);
|
|
290
|
+
const exportCurrent = exported && exported.revision === record.revision && exported.choice === choiceKey() && !formDirty;
|
|
291
|
+
const inspectionCurrent = inspected && inspected.revision === record.revision && inspected.choice === choiceKey() && !formDirty;
|
|
292
|
+
$('#connection-export-state').textContent = exportCurrent ? '当前配置已生成' : '本页未记录当前配置导出';
|
|
293
|
+
$('#connection-export-evidence').textContent = exportCurrent ? date(exported.at) + ' · ' + exported.transport + ' · ' + exported.format + ';仍需导入客户端。'
|
|
294
|
+
: '只记录本次登录中生成的配置。若你已在客户端配置过,可直接核对接入。';
|
|
295
|
+
$('#connection-check-state').textContent = inspectionCurrent ? inspected.ok ? 'Iris 端配置检查通过' : '有待核对项' : '尚未检查当前配置';
|
|
296
|
+
$('#connection-check-state').dataset.state = inspectionCurrent ? inspected.ok ? 'passed' : 'limited' : 'pending';
|
|
297
|
+
$('#connection-check-evidence').textContent = inspectionCurrent ? date(inspected.at) + ' · 只读取此服务与保存授权,未检查客户端传输或媒体。'
|
|
298
|
+
: '检查不会建立 MCP 连接,不会代替客户端握手或调用模型。';
|
|
299
|
+
$('#connection-check-issues').replaceChildren(...(inspectionCurrent ? inspected.issues : []).map(item => { const row = element('li', item.message); row.dataset.check = item.code; return row; }));
|
|
300
|
+
$('#connection-check-tools').replaceChildren(...(inspectionCurrent ? inspected.tools : []).map(name => element('li', name)));
|
|
301
|
+
$('#connection-check-tools-note').textContent = inspectionCurrent ? inspected.tools.length + ' 个工具按当前授权允许使用;需在客户端确认是否已发现。' : '完成只读检查后显示 Core 当前允许的工具。';
|
|
302
|
+
const evidence = record && catalog ? mcpClientEvidence(record, catalog) : null;
|
|
303
|
+
$('#connection-client-state').textContent = evidence?.label || '等待创建连接';
|
|
304
|
+
$('#connection-client-state').dataset.state = evidence?.state || 'waiting';
|
|
305
|
+
$('#connection-client-evidence').replaceChildren();
|
|
306
|
+
for (const instance of evidence?.instances || []) {
|
|
307
|
+
const item = element('li', instance.clientInfo.name + ' · ' + instance.clientInfo.version + ' · ' + instance.transport + ' · '
|
|
308
|
+
+ (instance.state === 'connected' ? '握手记录' : '已断开') + ' · ' + date(instance.at));
|
|
309
|
+
const details = element('details', '', 'tool-details'), summary = element('summary', ''), disclosure = element('span', '展开', 'disclosure');
|
|
310
|
+
summary.append(element('span', '本次协商允许 ' + instance.tools.length + ' 个工具'), disclosure);
|
|
311
|
+
const names = element('ul', '', 'connection-check-tools'); names.append(...instance.tools.map(name => element('li', name)));
|
|
312
|
+
details.append(summary, names); details.addEventListener('toggle', () => { disclosure.textContent = details.open ? '收起' : '展开'; }); item.append(details);
|
|
313
|
+
$('#connection-client-evidence').append(item);
|
|
314
|
+
}
|
|
315
|
+
if (evidence?.contact) $('#connection-client-evidence').append(element('li', evidence.contact.clientInfo.name + ' · ' + date(evidence.contact.at) + ' · 仅为历史声明'));
|
|
316
|
+
}
|
|
317
|
+
function cancelCheck() {
|
|
318
|
+
if (!checkController) return;
|
|
319
|
+
checkController.abort(); checkController = null; busy = false;
|
|
320
|
+
feedback('只读检查已取消,未建立客户端连接。');
|
|
321
|
+
}
|
|
322
|
+
async function checkSetup() {
|
|
323
|
+
const original = current();
|
|
324
|
+
if (busy || !client || !original || formDirty || !visible) return;
|
|
325
|
+
const at = epoch, connection = client, id = selected, ticket = viewTicket, choice = choiceKey();
|
|
326
|
+
const controller = new AbortController(); checkController = controller; inspections.delete(id); renderOnboarding(); busy = true; controls(); feedback('正在只读核对 Iris 配置和客户端记录…');
|
|
327
|
+
const currentRequest = () => valid(at, connection) && checkController === controller && visible && selected === id && viewTicket === ticket;
|
|
328
|
+
try {
|
|
329
|
+
const options = { signal: controller.signal };
|
|
330
|
+
const [available, saved, session] = await Promise.all([connection.get(BASE + '/catalog', options), connection.get(BASE + '/connections', options), connection.get('/api/v0/session', options)]);
|
|
331
|
+
if (!currentRequest()) return;
|
|
332
|
+
if ([available, saved, session].some(value => value.contractVersion !== 0)) throw new TypeError('不支持的连接检查版本');
|
|
333
|
+
records = saved.connections; setCatalog(available);
|
|
334
|
+
const record = current();
|
|
335
|
+
if (!record || record.revision !== original.revision) {
|
|
336
|
+
inspections.delete(id); exports.delete(id); select(record?.connectionId || null);
|
|
337
|
+
feedback('连接授权已变化,请核对当前设置后重新检查。', true); return;
|
|
338
|
+
}
|
|
339
|
+
const result = inspectMcpSetup(record, catalog, { transport: $('#connection-transport').value, location: $('#connection-location').value });
|
|
340
|
+
inspections.set(id, { ...result, at: new Date().toISOString(), revision: record.revision, choice });
|
|
341
|
+
// No draft can be active here. Refresh the badge/facts from the same saved record as the evidence.
|
|
342
|
+
select(id);
|
|
343
|
+
feedback(result.ok ? mcpClientEvidence(record, catalog).state === 'observed'
|
|
344
|
+
? '已读取当前配置与客户端声明;握手不代表持续在线或媒体已实测。'
|
|
345
|
+
: 'Iris 端配置检查通过。请在客户端导入配置,再核对实际握手;媒体尚未实测。'
|
|
346
|
+
: '发现需核对的配置项,读取或历史功能可能仍可用。', !result.ok);
|
|
347
|
+
} catch (error) {
|
|
348
|
+
if (currentRequest()) { inspections.delete(id); renderOnboarding(); feedback('只读检查未完成:' + describe(error), true); }
|
|
349
|
+
} finally { if (checkController === controller) { checkController = null; busy = false; controls(); } }
|
|
350
|
+
}
|
|
223
351
|
function download(value, filename, mediaType = 'application/json') {
|
|
224
352
|
const url = URL.createObjectURL(new Blob([value], { type: mediaType })); urls.add(url);
|
|
225
353
|
const link = document.createElement('a'); link.href = url; link.download = filename; link.click();
|
|
@@ -248,6 +376,7 @@ export function createConnectionPanel({ describe }) {
|
|
|
248
376
|
});
|
|
249
377
|
});
|
|
250
378
|
$('#connection-export-button').addEventListener('click', () => {
|
|
379
|
+
if (formDirty || $('#connection-location').value !== 'same') return;
|
|
251
380
|
clearExport();
|
|
252
381
|
const id = selected, ticket = viewTicket;
|
|
253
382
|
perform(async ctx => {
|
|
@@ -256,18 +385,22 @@ export function createConnectionPanel({ describe }) {
|
|
|
256
385
|
? choice.presetId ? { presetId: choice.presetId } : { templateId: choice.templateId }
|
|
257
386
|
: { transport: $('#connection-transport').value, format: $('#connection-format').value });
|
|
258
387
|
if (!ctx.valid()) return;
|
|
259
|
-
if (selected !== id || !visible || viewTicket !== ticket)
|
|
388
|
+
if (selected !== id || !visible || viewTicket !== ticket) return;
|
|
260
389
|
$('#connection-configuration').value = result.content ?? JSON.stringify(result.configuration, null, 2) + '\n';
|
|
261
390
|
exportDelivery = { filename: result.filename || 'iris-mcp.json', mediaType: result.mediaType || 'application/json' };
|
|
262
391
|
$('#connection-download').textContent = exportDelivery.filename.endsWith('.toml') ? '下载 TOML' : '下载 JSON';
|
|
263
392
|
$('#connection-export-note').textContent = result.containsCredentials ? '此 HTTP 配置含连接密钥,请仅交给获授权的本机客户端。' : 'stdio 配置引用本机私有文件,需要使用本机路径。';
|
|
264
393
|
$('#connection-export-result').hidden = false; feedback('配置已生成。客户端是否安装或加载,需要在客户端确认。');
|
|
394
|
+
const record = current();
|
|
395
|
+
exports.set(id, { at: new Date().toISOString(), revision: record.revision, choice: choiceKey(), transport: $('#connection-transport').value, format: $('#connection-format').value });
|
|
396
|
+
renderOnboarding();
|
|
265
397
|
});
|
|
266
398
|
});
|
|
267
399
|
$('#connection-copy').addEventListener('click', async () => {
|
|
268
|
-
const at = epoch;
|
|
269
|
-
|
|
270
|
-
|
|
400
|
+
const at = epoch, id = selected, ticket = viewTicket;
|
|
401
|
+
const currentCopy = () => at === epoch && id === selected && ticket === viewTicket && visible;
|
|
402
|
+
try { await navigator.clipboard.writeText($('#connection-configuration').value); if (currentCopy()) feedback('配置已复制。'); }
|
|
403
|
+
catch (error) { if (currentCopy()) feedback('未能复制,请手动选择配置文本。' + error.message, true); }
|
|
271
404
|
});
|
|
272
405
|
$('#connection-download').addEventListener('click', () => {
|
|
273
406
|
const value = $('#connection-configuration').value; if (!value) return;
|
|
@@ -296,7 +429,7 @@ export function createConnectionPanel({ describe }) {
|
|
|
296
429
|
perform(async ctx => {
|
|
297
430
|
const result = await ctx.connection.postManagement(BASE + '/connections/' + id + '/skill', {});
|
|
298
431
|
if (!ctx.valid()) return;
|
|
299
|
-
if (selected !== id || !visible || viewTicket !== ticket)
|
|
432
|
+
if (selected !== id || !visible || viewTicket !== ticket) return;
|
|
300
433
|
skillBundle = result.bundle; $('#connection-skill-preview').value = result.preview; $('#connection-skill-result').hidden = false;
|
|
301
434
|
feedback('说明已生成。下载并安装后,还需在客户端确认已发现此 Skill。');
|
|
302
435
|
});
|
|
@@ -332,7 +465,7 @@ export function createConnectionPanel({ describe }) {
|
|
|
332
465
|
if (formDirty && !discard) { $('#connection-leave-confirm').hidden = false; $('#connection-leave-confirm').scrollIntoView({ block: 'center' }); $('#connection-leave-keep').focus(); return; }
|
|
333
466
|
clearExport(); formDirty = false; $('#connection-leave-confirm').hidden = true; editor(false); feedback(); $('#connection-new').focus();
|
|
334
467
|
}
|
|
335
|
-
$('#connection-form').addEventListener('input', () => { formDirty = true; closeButton.textContent = '取消编辑'; });
|
|
468
|
+
$('#connection-form').addEventListener('input', () => { formDirty = true; closeButton.textContent = '取消编辑'; renderOnboarding(); controls(); });
|
|
336
469
|
$('#connection-back').addEventListener('click', () => closeEditor());
|
|
337
470
|
closeButton.addEventListener('click', () => closeEditor());
|
|
338
471
|
$('#connection-leave-keep').addEventListener('click', () => { $('#connection-leave-confirm').hidden = true; });
|
|
@@ -346,6 +479,10 @@ export function createConnectionPanel({ describe }) {
|
|
|
346
479
|
for (const id of ['connection-transport', 'connection-format', 'connection-name']) $('#' + id).addEventListener('input', clearExport);
|
|
347
480
|
$('#connection-transport').addEventListener('change', transportNote);
|
|
348
481
|
$('#connection-clear-export').addEventListener('click', clearExport);
|
|
482
|
+
$('#connection-location').addEventListener('change', () => { clearExport(); controls(); });
|
|
483
|
+
$('#connection-check').addEventListener('click', checkSetup);
|
|
484
|
+
$('#connection-verify-next').addEventListener('click', () => setView('verification'));
|
|
485
|
+
$('#connection-return-settings').addEventListener('click', () => setView('settings'));
|
|
349
486
|
async function connect(connection) {
|
|
350
487
|
const at = ++epoch; client = connection; busy = true; records = []; selected = null; editor(false); clearExport(); feedback('正在读取连接…'); controls();
|
|
351
488
|
try {
|
|
@@ -361,8 +498,10 @@ export function createConnectionPanel({ describe }) {
|
|
|
361
498
|
}
|
|
362
499
|
return Object.freeze({
|
|
363
500
|
connect,
|
|
364
|
-
show(value) { if (visible !== value) viewTicket++; visible = value; if (!value) clearExport(); },
|
|
501
|
+
show(value) { if (visible !== value) viewTicket++; visible = value; if (!value) { cancelCheck(); clearExport(); feedback(); controls(); } },
|
|
365
502
|
disconnect() {
|
|
503
|
+
cancelCheck(); exports.clear(); inspections.clear();
|
|
504
|
+
capabilities = []; tools = []; pluginOperations = []; allowedScopes = ['read']; scopes = {}; $('#connection-location').value = 'same';
|
|
366
505
|
epoch++; client = null; busy = false; visible = false; catalog = null; records = []; templates = []; selected = null; editor(false); clearExport(); feedback();
|
|
367
506
|
formDirty = false; $('#connection-leave-confirm').hidden = true; closeButton.textContent = '返回列表';
|
|
368
507
|
$('#connection-template').replaceChildren(); $('#connection-template-note').replaceChildren(); $('#connection-template-note').hidden = true; $('#connection-template-file').value = '';
|
package/lib/web/creation.js
CHANGED
|
@@ -11,12 +11,13 @@ export function createCreationPanel({ openRun, refreshHistory, showDraft, descri
|
|
|
11
11
|
let preview, retryAction = null, recoveryAfterLoad = null;
|
|
12
12
|
let comfyForm = null, comfyKey = '', inspection = null;
|
|
13
13
|
const cap = name => Boolean(session?.operations.includes(name));
|
|
14
|
-
const mode = () => view ? ({ 'image-edit/v0': 'edit', 'image-generate/v0': 'generate', 'speech/v0': 'tts', 'transcribe/v0': 'transcribe' })[view.recipe] : draft.mode || 'edit';
|
|
14
|
+
const mode = () => view ? ({ 'image-edit/v0': 'edit', 'image-generate/v0': 'generate', 'speech/v0': 'tts', 'transcribe/v0': 'transcribe', 'video-generate/v0': 'video' })[view.recipe] : draft.mode || 'edit';
|
|
15
15
|
const generating = () => mode() === 'generate';
|
|
16
|
+
const video = () => mode() === 'video';
|
|
16
17
|
const audio = () => ['tts', 'transcribe'].includes(mode());
|
|
17
|
-
const createCapabilities = { generate: 'generations.create', edit: 'runs.create', tts: 'speeches.create', transcribe: 'transcriptions.create' };
|
|
18
|
+
const createCapabilities = { generate: 'generations.create', edit: 'runs.create', tts: 'speeches.create', transcribe: 'transcriptions.create', video: 'videos.create' };
|
|
18
19
|
const createCapability = () => createCapabilities[mode()];
|
|
19
|
-
const candidates = () => models.filter(model => audio() ? model.capability === mode() : !model.capability && (generating() || model.supportsImageEdit));
|
|
20
|
+
const candidates = () => models.filter(model => video() ? model.capability === 'video-gen' : audio() ? model.capability === mode() : !model.capability && (generating() || model.supportsImageEdit));
|
|
20
21
|
const valid = at => at === epoch && Boolean(client);
|
|
21
22
|
const validJob = task => valid(task.epoch) && task.intent === intent && visible && job === task;
|
|
22
23
|
function feedback(text = '', error = false) {
|
|
@@ -70,8 +71,8 @@ export function createCreationPanel({ openRun, refreshHistory, showDraft, descri
|
|
|
70
71
|
}
|
|
71
72
|
function controls() {
|
|
72
73
|
const generate = generating(), currentMode = mode(), isAudio = audio(), transcribe = currentMode === 'transcribe';
|
|
73
|
-
$('#studio-title').textContent = currentMode === 'tts' ? '让文字,有声音。' : transcribe ? '让声音,成文字。' : '让想法,有画面。';
|
|
74
|
-
$('#studio-note').textContent = currentMode === 'tts' ? '写下要说的话,留下你的声音作品。' : transcribe ? '听见的内容,留成可以继续使用的文字。' : '保留喜欢的部分,试一点新的可能。';
|
|
74
|
+
$('#studio-title').textContent = video() ? '让想法,动起来。' : currentMode === 'tts' ? '让文字,有声音。' : transcribe ? '让声音,成文字。' : '让想法,有画面。';
|
|
75
|
+
$('#studio-note').textContent = video() ? '从一段描述开始,接回原任务的视频作品。' : currentMode === 'tts' ? '写下要说的话,留下你的声音作品。' : transcribe ? '听见的内容,留成可以继续使用的文字。' : '保留喜欢的部分,试一点新的可能。';
|
|
75
76
|
const needsSource = ['edit', 'transcribe'].includes(currentMode);
|
|
76
77
|
const pending = media?.snapshot(), unresolved = pending?.actions.some(action => CREATIONS.includes(action.kind)
|
|
77
78
|
|| view && action.runId === view.id);
|
|
@@ -80,14 +81,16 @@ export function createCreationPanel({ openRun, refreshHistory, showDraft, descri
|
|
|
80
81
|
button.setAttribute('aria-pressed', String(button.dataset.creationMode === currentMode));
|
|
81
82
|
button.disabled = !cap(createCapabilities[button.dataset.creationMode]);
|
|
82
83
|
if (['tts', 'transcribe'].includes(button.dataset.creationMode)) button.hidden = !session?.audio;
|
|
84
|
+
if (button.dataset.creationMode === 'video') button.hidden = !session?.video;
|
|
83
85
|
}
|
|
84
86
|
$('#source-input').hidden = currentMode !== 'edit'; $('#audio-source-input').hidden = !transcribe;
|
|
85
87
|
$('#prompt-label').hidden = transcribe; $('#edit-prompt').hidden = transcribe; $('#edit-prompt').required = !transcribe;
|
|
88
|
+
$('#video-duration-field').hidden = !video();
|
|
86
89
|
$('#speech-options').hidden = currentMode !== 'tts'; $('#advanced-settings').hidden = isAudio;
|
|
87
|
-
$('#model-label').firstChild.textContent = currentMode === 'tts' ? '语音模型 ' : transcribe ? '转写模型 ' : '图片模型 ';
|
|
88
|
-
$('#prompt-label').textContent = currentMode === 'tts' ? '要朗读的文字' : generate ? '想画些什么?' : '想改些什么?';
|
|
89
|
-
$('#edit-prompt').maxLength = isAudio ? 20000 : 12000;
|
|
90
|
-
$('#edit-prompt').placeholder = currentMode === 'tts' ? '写下希望朗读的内容…' : generate ? '描述画面、人物、色彩与风格…' : '描述想保留的细节,以及想尝试的变化…';
|
|
90
|
+
$('#model-label').firstChild.textContent = video() ? '视频模型 ' : currentMode === 'tts' ? '语音模型 ' : transcribe ? '转写模型 ' : '图片模型 ';
|
|
91
|
+
$('#prompt-label').textContent = video() ? '想让什么动起来?' : currentMode === 'tts' ? '要朗读的文字' : generate ? '想画些什么?' : '想改些什么?';
|
|
92
|
+
$('#edit-prompt').maxLength = isAudio || video() ? 20000 : 12000;
|
|
93
|
+
$('#edit-prompt').placeholder = video() ? '描述主体、动作、镜头与氛围…' : currentMode === 'tts' ? '写下希望朗读的内容…' : generate ? '描述画面、人物、色彩与风格…' : '描述想保留的细节,以及想尝试的变化…';
|
|
91
94
|
const canCreate = cap(createCapability()) && cap('runs.continue') && (!needsSource || Boolean(draft.file || draft.artifactId)
|
|
92
95
|
&& (draft.artifactId || cap(transcribe ? 'audio.create' : 'images.create')))
|
|
93
96
|
&& Boolean($('#model').value) && (transcribe || Boolean($('#edit-prompt').value.trim()));
|
|
@@ -99,7 +102,7 @@ export function createCreationPanel({ openRun, refreshHistory, showDraft, descri
|
|
|
99
102
|
|| Boolean(activeOperation && ACTIVE.has(activeOperation.state)) || Boolean(inspection) || comfyForm && !comfyForm.valid()
|
|
100
103
|
|| !(retryAction ? canRetry : view ? canContinue : canCreate);
|
|
101
104
|
$('#start-edit').textContent = busy ? '正在处理…' : retryAction ? '按原 Key 接回' : view && view.status !== 'not_started'
|
|
102
|
-
? labels[view.status] || '查看已有作品' : currentMode === 'tts' ? '合成语音' : transcribe ? '开始转写' : generate ? '开始生成' : '开始改图';
|
|
105
|
+
? labels[view.status] || '查看已有作品' : currentMode === 'tts' ? '合成语音' : transcribe ? '开始转写' : generate || video() ? '开始生成' : '开始改图';
|
|
103
106
|
$('#locked-note').hidden = !view;
|
|
104
107
|
$('#reuse-selected').disabled = busy || loading || !cap('runs.create') || !view?.selection;
|
|
105
108
|
for (const kind of ['source', 'output']) {
|
|
@@ -110,9 +113,9 @@ export function createCreationPanel({ openRun, refreshHistory, showDraft, descri
|
|
|
110
113
|
$('#creation-help').textContent = !client ? '读取权限中…' : loading ? '当前流程尚未确认,读取完成前暂停操作。可重新读取或新建流程。'
|
|
111
114
|
: retryAction ? '使用原身份与原输入接回。明确执行仍可能产生供应商费用。'
|
|
112
115
|
: pending?.blocked ? '恢复记录需要先明确处理,暂不接收新动作。'
|
|
113
|
-
: !cap('runs.continue') ? isAudio ? '当前服务只读,可浏览和下载已保存的音频或文字。'
|
|
116
|
+
: !cap('runs.continue') ? video() ? '当前服务只读,可播放和下载已保存的视频。' : isAudio ? '当前服务只读,可浏览和下载已保存的音频或文字。'
|
|
114
117
|
: cap('runs.select') ? '当前可选择已有图片。开始创作需要显式开启执行服务。' : '当前服务只读,可浏览图片流程和下载已选作品。'
|
|
115
|
-
: !view && !candidates().length ? isAudio ? '尚无此音频功能的可用候选。请在模型服务中配置并分配用途。' : '尚无可用图片候选。可在供应商页配置模型。'
|
|
118
|
+
: !view && !candidates().length ? video() ? '尚无文生视频候选。请在模型服务中配置并分配视频用途。' : isAudio ? '尚无此音频功能的可用候选。请在模型服务中配置并分配用途。' : '尚无可用图片候选。可在供应商页配置模型。'
|
|
116
119
|
: view?.status === 'needs_attention' ? '先核对原任务;受理未知时请避免重复生成。'
|
|
117
120
|
: '明确执行可能产生供应商费用。刷新不会自动生成。';
|
|
118
121
|
$('#creation-settings').dataset.fixed = String(Boolean(view));
|
|
@@ -122,7 +125,7 @@ export function createCreationPanel({ openRun, refreshHistory, showDraft, descri
|
|
|
122
125
|
$('#edit-size-note').textContent = workflow && !workflow.sizeNode ? '此工作流未绑定尺寸节点,沿用模板尺寸。' : '填写模型支持的尺寸;留空使用默认值。';
|
|
123
126
|
}
|
|
124
127
|
function fillModels(selected = '') {
|
|
125
|
-
const choices = candidates(), label = mode() === 'tts' ? '语音' : mode() === 'transcribe' ? '转写' : '图片';
|
|
128
|
+
const choices = candidates(), label = video() ? '视频' : mode() === 'tts' ? '语音' : mode() === 'transcribe' ? '转写' : '图片';
|
|
126
129
|
$('#model').replaceChildren(new Option(choices.length ? '选择一个' + label + '模型' : '没有可用' + label + '候选', ''));
|
|
127
130
|
for (const model of choices) $('#model').append(new Option(model.displayName + ' · ' + model.modelRef, model.modelRef));
|
|
128
131
|
if (selected && !choices.some(model => model.modelRef === selected)) $('#model').append(new Option(selected + ' · 原流程模型', selected));
|
|
@@ -136,8 +139,8 @@ export function createCreationPanel({ openRun, refreshHistory, showDraft, descri
|
|
|
136
139
|
if (preview && mode === 'transcribe') { $('#audio-input-preview').src = preview; $('#audio-input-preview').hidden = false; }
|
|
137
140
|
$('#audio-file').value = ''; $('#speech-voice').value = ''; $('#audio-file-name').textContent = label || file?.name || (artifactId ? '已保存的原音频' : 'WAV / MP3 / OGG / FLAC,最大20 MiB。');
|
|
138
141
|
$('#file-name').textContent = label || file?.name || (artifactId ? '已选作品' : '选择一张图片');
|
|
139
|
-
$('#image-file').value = ''; $('#edit-prompt').value = ''; $('#edit-size').value = ''; fillModels(modelRef); renderComfy(true);
|
|
140
|
-
$('#input-summary').textContent = mode === 'tts' ? '让文字成为声音' : mode === 'transcribe' ? '把声音留成文字' : mode === 'generate' ? '从一句想法开始' : file || artifactId ? '填写你的修改要求' : '从一张图片开始';
|
|
142
|
+
$('#image-file').value = ''; $('#edit-prompt').value = ''; $('#edit-size').value = ''; $('#video-duration').value = ''; fillModels(modelRef); renderComfy(true);
|
|
143
|
+
$('#input-summary').textContent = mode === 'video' ? '从一段动态描述开始' : mode === 'tts' ? '让文字成为声音' : mode === 'transcribe' ? '把声音留成文字' : mode === 'generate' ? '从一句想法开始' : file || artifactId ? '填写你的修改要求' : '从一张图片开始';
|
|
141
144
|
$('#creation-settings').open = true; $('#advanced-settings').open = false; feedback();
|
|
142
145
|
showDraft(preview, mode); controls();
|
|
143
146
|
}
|
|
@@ -168,7 +171,7 @@ export function createCreationPanel({ openRun, refreshHistory, showDraft, descri
|
|
|
168
171
|
if (!valid(at) || !visible || view?.id !== operation.runId) return;
|
|
169
172
|
await openRun(operation.runId, { fromAction: true });
|
|
170
173
|
if (!valid(at)) return;
|
|
171
|
-
if (view?.id === operation.runId && audio() && operation.state === 'succeeded') feedback();
|
|
174
|
+
if (view?.id === operation.runId && (audio() || video()) && operation.state === 'succeeded') feedback();
|
|
172
175
|
await refreshHistory(); controls();
|
|
173
176
|
}
|
|
174
177
|
function watch(operation) {
|
|
@@ -197,9 +200,10 @@ export function createCreationPanel({ openRun, refreshHistory, showDraft, descri
|
|
|
197
200
|
const generate = generating(), currentMode = mode(), isAudio = audio(), transcribe = currentMode === 'transcribe';
|
|
198
201
|
const input = isAudio ? { modelRef: $('#model').value, ...(transcribe ? { sourceArtifactId: draft.artifactId }
|
|
199
202
|
: { text: $('#edit-prompt').value, ...($('#speech-voice').value.trim() ? { voice: $('#speech-voice').value.trim() } : {}) }) }
|
|
200
|
-
: { ...(!generate ? { sourceArtifactId: draft.artifactId } : {}), modelRef: $('#model').value, prompt: $('#edit-prompt').value,
|
|
203
|
+
: { ...(!generate && !video() ? { sourceArtifactId: draft.artifactId } : {}), modelRef: $('#model').value, prompt: $('#edit-prompt').value,
|
|
201
204
|
...($('#edit-size').value.trim() ? { size: $('#edit-size').value.trim() } : {}),
|
|
202
|
-
...(parameters && Object.keys(parameters).length ? { workflowParameters: parameters } : {})
|
|
205
|
+
...(parameters && Object.keys(parameters).length ? { workflowParameters: parameters } : {}),
|
|
206
|
+
...(video() && $('#video-duration').value !== '' ? { duration: Number($('#video-duration').value) } : {}) };
|
|
203
207
|
const file = draft.file, original = retryAction;
|
|
204
208
|
try {
|
|
205
209
|
if (original) {
|
|
@@ -216,7 +220,7 @@ export function createCreationPanel({ openRun, refreshHistory, showDraft, descri
|
|
|
216
220
|
feedback('正在保存素材…'); const imported = await media[transcribe ? 'uploadAudio' : 'uploadImage'](file);
|
|
217
221
|
if (!validJob(task)) return; draft.artifactId = input.sourceArtifactId = imported.artifact.artifactId;
|
|
218
222
|
}
|
|
219
|
-
feedback('正在建立流程…'); const created = await media[({ generate: 'createGeneration', edit: 'createRun', tts: 'createSpeech', transcribe: 'createTranscription' })[currentMode]](input);
|
|
223
|
+
feedback('正在建立流程…'); const created = await media[({ generate: 'createGeneration', edit: 'createRun', tts: 'createSpeech', transcribe: 'createTranscription', video: 'createVideo' })[currentMode]](input);
|
|
220
224
|
if (!validJob(task)) return; task.runId = created.flow.id;
|
|
221
225
|
await openRun(created.flow.id, { fromAction: true });
|
|
222
226
|
if (!validJob(task)) return;
|
|
@@ -287,7 +291,7 @@ export function createCreationPanel({ openRun, refreshHistory, showDraft, descri
|
|
|
287
291
|
});
|
|
288
292
|
$('#image-file').addEventListener('change', event => { void chooseFile(event.target.files[0]); event.target.value = ''; });
|
|
289
293
|
$('#model').addEventListener('change', () => { renderComfy(); controls(); });
|
|
290
|
-
for (const id of ['model', 'edit-prompt', 'edit-size', 'speech-voice']) $('#' + id).addEventListener('input', () => { if (id === 'model') renderComfy(); else invalidateInspection(); controls(); });
|
|
294
|
+
for (const id of ['model', 'edit-prompt', 'edit-size', 'speech-voice', 'video-duration']) $('#' + id).addEventListener('input', () => { if (id === 'model') renderComfy(); else invalidateInspection(); controls(); });
|
|
291
295
|
$('#audio-file').addEventListener('change', event => { const file = event.target.files[0]; event.target.value = ''; chooseAudio(file); });
|
|
292
296
|
$('#comfy-creation-check').addEventListener('click', () => { void checkComfy(); });
|
|
293
297
|
$('#image-drop').addEventListener('dragover', event => { event.preventDefault(); if (!view && !busy) $('#image-drop').classList.add('dragging'); });
|
|
@@ -305,7 +309,7 @@ export function createCreationPanel({ openRun, refreshHistory, showDraft, descri
|
|
|
305
309
|
beginDraft,
|
|
306
310
|
async prepareVerification({ capability, modelRef }) {
|
|
307
311
|
if (!client || busy || !cap('runs.continue')) return;
|
|
308
|
-
const at = epoch, next = capability === 'tts' ? 'tts' : capability === 'transcribe' ? 'transcribe' : cap('generations.create') ? 'generate' : 'edit';
|
|
312
|
+
const at = epoch, next = capability === 'video-gen' ? 'video' : capability === 'tts' ? 'tts' : capability === 'transcribe' ? 'transcribe' : cap('generations.create') ? 'generate' : 'edit';
|
|
309
313
|
if (!cap(createCapabilities[next])) { feedback('当前工作区没有此验证流程,请使用已接入的 CLI / DSH 功能。', true); return; }
|
|
310
314
|
let file;
|
|
311
315
|
if (next === 'edit') {
|
|
@@ -318,7 +322,7 @@ export function createCreationPanel({ openRun, refreshHistory, showDraft, descri
|
|
|
318
322
|
}
|
|
319
323
|
if (!valid(at)) return;
|
|
320
324
|
beginDraft({ mode: next, modelRef, file });
|
|
321
|
-
$('#edit-prompt').value = next === 'tts' ? '你好' : next === 'edit' ? '将红色圆形改为蓝色,保留白色背景。' : next === 'generate' ? 'A simple red circle on a plain white background.' : '';
|
|
325
|
+
$('#edit-prompt').value = next === 'tts' ? '你好' : next === 'edit' ? '将红色圆形改为蓝色,保留白色背景。' : next === 'generate' ? 'A simple red circle on a plain white background.' : next === 'video' ? 'A purple iris flower swaying gently in the breeze.' : '';
|
|
322
326
|
feedback(next === 'transcribe' ? '验证草稿已准备。请选择实际音频,确认后点击开始转写,可能产生费用。'
|
|
323
327
|
: '验证草稿已准备。请核对模型及参数,再明确点击生成;可能产生供应商费用。'); controls();
|
|
324
328
|
},
|
|
@@ -327,7 +331,7 @@ export function createCreationPanel({ openRun, refreshHistory, showDraft, descri
|
|
|
327
331
|
const changed = view?.id !== value.id;
|
|
328
332
|
loading = false; view = value; retryAction = null;
|
|
329
333
|
if (changed) { activeOperation = null; stopWatch(); }
|
|
330
|
-
if (!sameRun) { fillModels(value.input.modelRef); $('#edit-prompt').value = value.input.text ?? value.input.prompt ?? ''; $('#edit-size').value = value.input.size || ''; $('#speech-voice').value = value.input.voice || '';
|
|
334
|
+
if (!sameRun) { fillModels(value.input.modelRef); $('#edit-prompt').value = value.input.text ?? value.input.prompt ?? ''; $('#edit-size').value = value.input.size || ''; $('#speech-voice').value = value.input.voice || ''; $('#video-duration').value = value.input.duration ?? '';
|
|
331
335
|
$('#input-summary').textContent = value.input.modelRef; $('#file-name').textContent = '流程原图';
|
|
332
336
|
if (matchMedia('(max-width:640px)').matches) $('#creation-settings').open = false; }
|
|
333
337
|
renderComfy(); controls();
|
|
@@ -341,7 +345,7 @@ export function createCreationPanel({ openRun, refreshHistory, showDraft, descri
|
|
|
341
345
|
if (busy) return;
|
|
342
346
|
if (['continue', 'stop'].includes(action.kind)) { retryAction = action; feedback('将使用原身份接回此操作,请确认后点击。'); controls(); return; }
|
|
343
347
|
beginDraft({ artifactId: ['flow-create', 'transcription-create'].includes(action.kind) ? action.artifactId : undefined,
|
|
344
|
-
mode: ({ 'generation-create': 'generate', 'audio-import': 'transcribe', 'transcription-create': 'transcribe', 'speech-create': 'tts' })[action.kind] || 'edit' });
|
|
348
|
+
mode: ({ 'generation-create': 'generate', 'video-create': 'video', 'audio-import': 'transcribe', 'transcription-create': 'transcribe', 'speech-create': 'tts' })[action.kind] || 'edit' });
|
|
345
349
|
retryAction = action; feedback(IMPORTS.includes(action.kind) ? '请重新提供原素材字节,再按原 Key 接回。' : '请重新提供原模型、要求与参数,再按原 Key 接回。'); controls();
|
|
346
350
|
},
|
|
347
351
|
async refresh() { if (activeOperation && ACTIVE.has(activeOperation.state)) watch(activeOperation); controls(); },
|
|
@@ -349,7 +353,7 @@ export function createCreationPanel({ openRun, refreshHistory, showDraft, descri
|
|
|
349
353
|
epoch++; intent++; stopWatch(); client = media = session = null; view = null; draft = {}; models = []; busy = stopping = loading = false; job = activeOperation = retryAction = recoveryAfterLoad = null;
|
|
350
354
|
releasePreview(); $('#input-preview').removeAttribute('src'); $('#input-preview').hidden = true; $('#image-file').value = '';
|
|
351
355
|
$('#audio-file').value = ''; $('#audio-file-name').textContent = 'WAV / MP3 / OGG / FLAC,最大20 MiB。'; $('#speech-voice').value = '';
|
|
352
|
-
$('#edit-prompt').value = ''; $('#edit-size').value = ''; fillModels(); renderComfy(true); feedback(); controls();
|
|
356
|
+
$('#edit-prompt').value = ''; $('#edit-size').value = ''; $('#video-duration').value = ''; fillModels(); renderComfy(true); feedback(); controls();
|
|
353
357
|
}
|
|
354
358
|
});
|
|
355
359
|
}
|
package/lib/web/flow-view.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Consume the HTTP DTO, not private Core records or the prototype's in-memory transport.
|
|
2
2
|
export function presentReadOnlyFlow(view) {
|
|
3
|
-
if (view.contractVersion !== 0 || !['image-edit/v0', 'image-generate/v0', 'speech/v0', 'transcribe/v0'].includes(view.recipe)) throw new TypeError('不支持的流程版本');
|
|
3
|
+
if (view.contractVersion !== 0 || !['image-edit/v0', 'image-generate/v0', 'speech/v0', 'transcribe/v0', 'video-generate/v0'].includes(view.recipe)) throw new TypeError('不支持的流程版本');
|
|
4
4
|
switch (view.status) {
|
|
5
5
|
case 'not_started': return { title: '已保存,尚未开始', message: '输入已保存。当前页面只读,不会提交生成。', tone: 'neutral' };
|
|
6
6
|
case 'running': return { title: '原任务进行中', message: '这是本地保存的进展。页面刷新不会查询模型服务;可通过 CLI 查看原任务。', tone: 'working' };
|
|
@@ -10,7 +10,8 @@ export function presentReadOnlyFlow(view) {
|
|
|
10
10
|
case 'delivery_failed': return { title: '原结果待保存', message: '原任务已成功,本地保存尚未完成。请通过 CLI 恢复原结果。', tone: 'warning' };
|
|
11
11
|
case 'needs_comparison': return { title: '结果已保存', message: '本地比较尚未完成。可查看现有图片,通过 CLI 继续流程。', tone: 'neutral' };
|
|
12
12
|
case 'needs_result': return { title: '结果已保存', message: '原任务已交付,流程结果仍待接回。请通过 CLI 继续原流程。', tone: 'neutral' };
|
|
13
|
-
case 'ready':
|
|
13
|
+
case 'ready': if (view.recipe === 'video-generate/v0') return { title: '视频已就绪', message: '原视频已保存,可以播放或下载。', tone: 'success' };
|
|
14
|
+
return { title: view.recipe === 'speech/v0' ? '语音已就绪' : '转写已完成',
|
|
14
15
|
message: view.recipe === 'speech/v0' ? '语音已保存,可以试听或下载音频。' : '转写文字已保存,可以查看或下载文本。', tone: 'success' };
|
|
15
16
|
case 'awaiting_selection': return { title: '图片已就绪', message: '图片已保存。请通过 CLI 明确选择作品,再回来下载。', tone: 'success' };
|
|
16
17
|
case 'selected': return { title: view.source && view.selection?.artifactId === view.source.artifactId ? '已选择原图' : '已选择结果',
|
|
@@ -23,7 +24,7 @@ export function presentFlow(view, operations) {
|
|
|
23
24
|
const base = presentReadOnlyFlow(view), execute = operations.includes('runs.continue');
|
|
24
25
|
if (execute && view.status === 'not_started') return { ...base, message: '输入已固定。明确开始后才会调用模型,可能产生费用。' };
|
|
25
26
|
if (execute && view.status === 'running') return { ...base, message: '原任务仍在进行。点击“查看原任务”读取一次进展;刷新只读本地记录。' };
|
|
26
|
-
if (execute && view.status === 'delivery_failed') return { ...base, message: ['speech/v0', 'transcribe/v0'].includes(view.recipe)
|
|
27
|
+
if (execute && view.status === 'delivery_failed') return { ...base, message: ['speech/v0', 'transcribe/v0', 'video-generate/v0'].includes(view.recipe)
|
|
27
28
|
? '原任务已成功。点击“恢复原结果”接回交付,不重复提交。' : '生成已经成功。点击“恢复原结果”接回交付,不新建生成。' };
|
|
28
29
|
if (execute && view.status === 'needs_comparison') return { ...base, message: '图片已保存。点击“完成比较”执行本地比较,不产生生成请求。' };
|
|
29
30
|
if (execute && view.status === 'needs_result') return { ...base, message: '原任务已交付。点击“接回结果”保存流程引用,不产生生成请求。' };
|
package/lib/web/gallery.js
CHANGED
|
@@ -3,7 +3,7 @@ const element = (tag, text, className) => { const node = document.createElement(
|
|
|
3
3
|
|
|
4
4
|
/** Read-only gallery. Preview bytes are fetched only while a card is visible. */
|
|
5
5
|
export function createGallery({ openRun, describe }) {
|
|
6
|
-
let client, visible = false, filter = 'all', kind = 'image', audioEnabled = false, cursor = null, busy = false, ticket = 0, controller, observer, activeAudio;
|
|
6
|
+
let client, visible = false, filter = 'all', kind = 'image', audioEnabled = false, videoEnabled = false, cursor = null, busy = false, ticket = 0, controller, observer, activeAudio;
|
|
7
7
|
const previews = new Map();
|
|
8
8
|
function clearAudio() {
|
|
9
9
|
if (!activeAudio) return;
|
|
@@ -38,7 +38,7 @@ export function createGallery({ openRun, describe }) {
|
|
|
38
38
|
for (const card of previews.keys()) observer.observe(card);
|
|
39
39
|
}
|
|
40
40
|
function card(run) {
|
|
41
|
-
if (['speech/v0', 'transcribe/v0'].includes(run.recipe)) return audioCard(run);
|
|
41
|
+
if (['speech/v0', 'transcribe/v0', 'video-generate/v0'].includes(run.recipe)) return audioCard(run);
|
|
42
42
|
const button = element('button', '', 'gallery-card'); button.type = 'button'; button.dataset.galleryRun = run.id;
|
|
43
43
|
button.setAttribute('aria-label', '打开图片流程 · ' + (run.selection ? '已选择' : '未选择'));
|
|
44
44
|
const cover = element('div', '', 'gallery-cover'), image = document.createElement('img'), marker = element('span', '读取预览…');
|
|
@@ -53,27 +53,27 @@ export function createGallery({ openRun, describe }) {
|
|
|
53
53
|
return button;
|
|
54
54
|
}
|
|
55
55
|
function audioCard(run) {
|
|
56
|
-
const speech = run.recipe === 'speech/v0', article = element('article', '', 'gallery-card gallery-audio'); article.dataset.galleryRun = run.id;
|
|
56
|
+
const speech = run.recipe === 'speech/v0', video = run.recipe === 'video-generate/v0', article = element('article', '', 'gallery-card gallery-audio'); article.dataset.galleryRun = run.id;
|
|
57
57
|
const cover = element('div', '', 'gallery-audio-cover');
|
|
58
|
-
cover.append(element('span', speech ? '♪' : '≋', 'gallery-audio-icon'), element('span', speech ? 'VOICE' : 'TRANSCRIPT', 'eyebrow'));
|
|
58
|
+
cover.append(element('span', video ? '▷' : speech ? '♪' : '≋', 'gallery-audio-icon'), element('span', video ? 'MOTION' : speech ? 'VOICE' : 'TRANSCRIPT', 'eyebrow'));
|
|
59
59
|
const info = element('div', '', 'gallery-info');
|
|
60
|
-
info.append(element('strong', (run.output ? '已保存 · ' : '待完成 · ') + (speech ? '语音合成' : '音频转写')),
|
|
60
|
+
info.append(element('strong', (run.output ? '已保存 · ' : '待完成 · ') + (video ? '视频生成' : speech ? '语音合成' : '音频转写')),
|
|
61
61
|
element('p', new Date(run.updatedAt).toLocaleString('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) + ' · ' + run.modelRef));
|
|
62
|
-
const actions = element('div', '', 'export-actions'), open = element('button', speech ? '查看作品' : '查看文字', 'quiet'); open.type = 'button';
|
|
62
|
+
const actions = element('div', '', 'export-actions'), open = element('button', video || speech ? '查看作品' : '查看文字', 'quiet'); open.type = 'button';
|
|
63
63
|
open.addEventListener('click', () => openRun(run.id)); actions.append(open);
|
|
64
|
-
const artifact = speech ? run.output : run.source;
|
|
64
|
+
const artifact = speech || video ? run.output : run.source;
|
|
65
65
|
if (artifact) {
|
|
66
|
-
const play = element('button', speech ? '载入试听' : '载入原音频', 'quiet'); play.type = 'button'; play.dataset.galleryPlay = run.id; actions.append(play);
|
|
67
|
-
const player = document.createElement('audio'); player.controls = true; player.preload = 'metadata'; player.hidden = true;
|
|
66
|
+
const play = element('button', video ? '载入视频' : speech ? '载入试听' : '载入原音频', 'quiet'); play.type = 'button'; play.dataset.galleryPlay = run.id; actions.append(play);
|
|
67
|
+
const player = document.createElement(video ? 'video' : 'audio'); player.playsInline = true; player.controls = true; player.preload = 'metadata'; player.hidden = true;
|
|
68
68
|
const note = element('p', '', 'field-note');
|
|
69
69
|
play.addEventListener('click', async () => {
|
|
70
70
|
clearAudio();
|
|
71
71
|
const at = ticket, http = client, job = { controller: new AbortController(), player, button: play, url: null };
|
|
72
|
-
activeAudio = job; play.disabled = true; note.textContent = '读取音频…';
|
|
72
|
+
activeAudio = job; play.disabled = true; note.textContent = video ? '读取原视频…' : '读取音频…';
|
|
73
73
|
try {
|
|
74
|
-
const blob = await http.get('/api/v0/artifacts/' + artifact.artifactId + '/audio', { binary: true, signal: job.controller.signal });
|
|
74
|
+
const blob = await http.get('/api/v0/artifacts/' + artifact.artifactId + (video ? '/video' : '/audio'), { binary: true, signal: job.controller.signal });
|
|
75
75
|
if (!visible || at !== ticket || job !== activeAudio) return;
|
|
76
|
-
job.url = URL.createObjectURL(blob); player.src = job.url; player.hidden = false; play.hidden = true; note.textContent = '点击播放器试听。';
|
|
76
|
+
job.url = URL.createObjectURL(blob); player.src = job.url; player.hidden = false; play.hidden = true; note.textContent = video ? '点击播放;仅载入原视频,不调用模型。' : '点击播放器试听。';
|
|
77
77
|
} catch (error) { if (job === activeAudio && !job.controller.signal.aborted) note.textContent = describe(error); }
|
|
78
78
|
finally { if (job === activeAudio) play.disabled = false; }
|
|
79
79
|
});
|
|
@@ -97,7 +97,7 @@ export function createGallery({ openRun, describe }) {
|
|
|
97
97
|
$('#gallery-feedback').textContent = '正在读取作品…'; $('#gallery-empty').hidden = true;
|
|
98
98
|
try {
|
|
99
99
|
const value = await http.get('/api/v0/runs?' + new URLSearchParams({ limit: '20', selection: filter,
|
|
100
|
-
...(audioEnabled ? { kind } : {}), ...(more && cursor ? { cursor } : {}) }), { signal });
|
|
100
|
+
...(audioEnabled || videoEnabled ? { kind } : {}), ...(more && cursor ? { cursor } : {}) }), { signal });
|
|
101
101
|
if (at !== ticket || client !== http || !visible) return;
|
|
102
102
|
if (value.contractVersion !== 0 || !value.counts || !Array.isArray(value.runs)) throw new TypeError('不支持的作品列表');
|
|
103
103
|
cursor = value.nextCursor; counts(value.counts);
|
|
@@ -114,7 +114,7 @@ export function createGallery({ openRun, describe }) {
|
|
|
114
114
|
void load();
|
|
115
115
|
});
|
|
116
116
|
for (const button of document.querySelectorAll('[data-gallery-kind]')) button.addEventListener('click', () => {
|
|
117
|
-
if (!audioEnabled || button.dataset.galleryKind === kind) return;
|
|
117
|
+
if (!(audioEnabled || videoEnabled) || button.hidden || button.dataset.galleryKind === kind) return;
|
|
118
118
|
kind = button.dataset.galleryKind; filter = 'all'; cursor = null;
|
|
119
119
|
for (const item of document.querySelectorAll('[data-gallery-kind]')) item.setAttribute('aria-pressed', String(item === button));
|
|
120
120
|
$('#gallery-filters').hidden = kind !== 'image'; void load();
|
|
@@ -122,9 +122,10 @@ export function createGallery({ openRun, describe }) {
|
|
|
122
122
|
$('#gallery-refresh').addEventListener('click', () => { void load(); });
|
|
123
123
|
$('#gallery-more').addEventListener('click', () => { void load(true); });
|
|
124
124
|
return Object.freeze({
|
|
125
|
-
connect(http, session) { client = http; audioEnabled = session?.audio === true; $('#gallery-types').hidden = !audioEnabled;
|
|
125
|
+
connect(http, session) { client = http; audioEnabled = session?.audio === true; videoEnabled = session?.video === true; $('#gallery-types').hidden = !audioEnabled && !videoEnabled;
|
|
126
|
+
for (const item of document.querySelectorAll('[data-gallery-kind]')) item.hidden = item.dataset.galleryKind === 'video' ? !videoEnabled : item.dataset.galleryKind !== 'image' && !audioEnabled; },
|
|
126
127
|
show(value) { if (visible === value) return; visible = value; if (value) void load(); else { ticket++; busy = false; clear(); $('#gallery-grid').replaceChildren(); } },
|
|
127
|
-
disconnect() { visible = false; client = null; ticket++; busy = false; cursor = null; filter = 'all'; kind = 'image'; audioEnabled = false; clear();
|
|
128
|
+
disconnect() { visible = false; client = null; ticket++; busy = false; cursor = null; filter = 'all'; kind = 'image'; audioEnabled = videoEnabled = false; clear();
|
|
128
129
|
for (const item of document.querySelectorAll('[data-gallery-kind]')) item.setAttribute('aria-pressed', String(item.dataset.galleryKind === kind));
|
|
129
130
|
$('#gallery-types').hidden = true; $('#gallery-filters').hidden = false; $('#gallery-grid').replaceChildren(); counts({ all: 0, selected: 0, unselected: 0 }); $('#gallery-feedback').textContent = ''; },
|
|
130
131
|
refresh() { return load(); }
|