@ninkch/dsh-infinite-canvas 0.17.6 → 0.17.8
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/client.js +302 -95
- package/docs/release.md +2 -0
- package/index.js +420 -23
- package/package.json +1 -1
package/client.js
CHANGED
|
@@ -91,6 +91,13 @@ window.__ModuleLoader__.load({
|
|
|
91
91
|
for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
|
|
92
92
|
return new Blob([arr], { type: mime || 'application/octet-stream' });
|
|
93
93
|
}
|
|
94
|
+
function arrayBufferToBase64(buf) {
|
|
95
|
+
const bytes = new Uint8Array(buf);
|
|
96
|
+
let bin = '';
|
|
97
|
+
const CH = 0x8000;
|
|
98
|
+
for (let i = 0; i < bytes.length; i += CH) bin += String.fromCharCode.apply(null, bytes.subarray(i, i + CH));
|
|
99
|
+
return btoa(bin);
|
|
100
|
+
}
|
|
94
101
|
function blobToDataUrl(blob) {
|
|
95
102
|
return new Promise((res, rej) => {
|
|
96
103
|
const r = new FileReader();
|
|
@@ -349,6 +356,15 @@ window.__ModuleLoader__.load({
|
|
|
349
356
|
'dataRootOpen': '打开目录',
|
|
350
357
|
'dataRootMigrate': '迁移历史资产',
|
|
351
358
|
'dataRootMigrated': '已迁移 {n} 个资产到数据根目录',
|
|
359
|
+
'collapse': '折叠',
|
|
360
|
+
'foldOpen': '展开',
|
|
361
|
+
'craftReadyCount': '{r}/{n} 就绪',
|
|
362
|
+
'craftDirLabel': '当前项目后期目录',
|
|
363
|
+
'craftDataRootNeed': '请先在上方设置画布数据根目录:Craft 后期文件将存放于 数据根目录\\projects\\<项目>\\craft',
|
|
364
|
+
'craftRootMismatch': 'ArtCraft 自身数据目录与画布数据根目录不一致:请在 ArtCraft 设置里把数据目录改成同一根目录,否则后期文件会落到两处',
|
|
365
|
+
'dataRootImportBtn': '从数据目录导入',
|
|
366
|
+
'dataRootImported': '导入完成:合并 {n} 个项目、补入 {a} 条资产元数据',
|
|
367
|
+
'dataRootImportHint': '换电脑迁移:把整个数据目录拷到新机器 → 设置为数据根目录 → 点"从数据目录导入"即可恢复项目/节点/设定集/资产',
|
|
352
368
|
'dataRootMigratedDone': '已迁移',
|
|
353
369
|
'dataRootMigrating': '迁移中',
|
|
354
370
|
'dataRootUnsupported': '当前插件后台版本较旧(不支持数据根目录):重启 Harness 后可用',
|
|
@@ -677,6 +693,15 @@ window.__ModuleLoader__.load({
|
|
|
677
693
|
'dataRootOpen': 'Open folder',
|
|
678
694
|
'dataRootMigrate': 'Migrate existing assets',
|
|
679
695
|
'dataRootMigrated': 'Migrated {n} assets to the data root',
|
|
696
|
+
'collapse': 'Collapse',
|
|
697
|
+
'foldOpen': 'Expand',
|
|
698
|
+
'craftReadyCount': '{r}/{n} ready',
|
|
699
|
+
'craftDirLabel': 'Project craft dir',
|
|
700
|
+
'craftDataRootNeed': 'Set the canvas data root above first: craft files go to <root>\\projects\\<project>\\craft',
|
|
701
|
+
'craftRootMismatch': "ArtCraft's own data root differs from the canvas data root — point ArtCraft at the same root or craft files will land in two places",
|
|
702
|
+
'dataRootImportBtn': 'Import from data root',
|
|
703
|
+
'dataRootImported': 'Imported: {n} projects merged, {a} asset records added',
|
|
704
|
+
'dataRootImportHint': 'Migration: copy the whole data folder to the new machine, set it as data root, then click "Import from data root" to restore projects/nodes/bible/assets',
|
|
680
705
|
'dataRootMigratedDone': 'Migrated',
|
|
681
706
|
'dataRootMigrating': 'Migrating',
|
|
682
707
|
'dataRootUnsupported': 'The plugin backend is older and lacks data-root support — restart Harness to enable',
|
|
@@ -1079,8 +1104,8 @@ window.__ModuleLoader__.load({
|
|
|
1079
1104
|
try { localStorage.setItem(key, JSON.stringify(value)); } catch { /* 忽略配额错误 */ }
|
|
1080
1105
|
}
|
|
1081
1106
|
const DEFAULT_SETTINGS = {
|
|
1082
|
-
//
|
|
1083
|
-
textLlm: { provider: '', model: ''
|
|
1107
|
+
// 文本模型全部用 Harness 的当前模型与默认参数:画布只选择模型,不单独配置生成参数
|
|
1108
|
+
textLlm: { provider: '', model: '' },
|
|
1084
1109
|
imageApis: [], // [{id, name, baseUrl, apiKey, model, mode, size?, responseFormat?}]
|
|
1085
1110
|
videoApis: [], // [{id, name, url, apiKey, model, protocol?, watermark?, generateAudio?}]
|
|
1086
1111
|
ttsApis: [], // [{id, name, baseUrl, apiKey, model, voice}]
|
|
@@ -1094,7 +1119,7 @@ window.__ModuleLoader__.load({
|
|
|
1094
1119
|
function loadSettings() {
|
|
1095
1120
|
const s = lsGet(LS_SETTINGS, {}) || {};
|
|
1096
1121
|
const out = {
|
|
1097
|
-
textLlm: {
|
|
1122
|
+
textLlm: { provider: (s.textLlm && s.textLlm.provider) || '', model: (s.textLlm && s.textLlm.model) || '' },
|
|
1098
1123
|
imageApis: Array.isArray(s.imageApis) ? s.imageApis : [],
|
|
1099
1124
|
videoApis: Array.isArray(s.videoApis) ? s.videoApis : [],
|
|
1100
1125
|
ttsApis: Array.isArray(s.ttsApis) ? s.ttsApis : [],
|
|
@@ -1292,12 +1317,16 @@ window.__ModuleLoader__.load({
|
|
|
1292
1317
|
entLines.length ? (zh ? '设定集:' : 'Series bible: ') + entLines.join(';') : '',
|
|
1293
1318
|
].filter(Boolean).join('\n');
|
|
1294
1319
|
}
|
|
1295
|
-
/**
|
|
1320
|
+
/** 资产命名时间戳(MM-DD HH:MM):同节点多次生成靠它区分。 */
|
|
1321
|
+
function nowStamp() {
|
|
1322
|
+
return new Date().toISOString().slice(5, 16).replace('T', ' ');
|
|
1323
|
+
}
|
|
1324
|
+
/** 资产命名:**节点名 + 时间**(用户规则);无节点名退回实体名/类型前缀 + 时间。 */
|
|
1296
1325
|
function assetNameFor(node, entities, fallbackKind) {
|
|
1297
1326
|
const label = String((node && node.data && node.data.label) || '').trim();
|
|
1298
|
-
if (label) return label;
|
|
1299
|
-
if (entities && entities.length) return entityLabelOf(entities[0].kind) + '-' + entities[0].name;
|
|
1300
|
-
return (fallbackKind === 'video' ? '视频' : '图片') + '-' +
|
|
1327
|
+
if (label) return label + ' ' + nowStamp();
|
|
1328
|
+
if (entities && entities.length) return entityLabelOf(entities[0].kind) + '-' + entities[0].name + ' ' + nowStamp();
|
|
1329
|
+
return (fallbackKind === 'video' ? '视频' : '图片') + '-' + nowStamp();
|
|
1301
1330
|
}
|
|
1302
1331
|
// <<<MENTION_PURE_END>>>
|
|
1303
1332
|
|
|
@@ -1509,21 +1538,72 @@ window.__ModuleLoader__.load({
|
|
|
1509
1538
|
}
|
|
1510
1539
|
await idbPut(rec);
|
|
1511
1540
|
assetNameCache.set(rec.id, rec.name);
|
|
1541
|
+
mirrorAssetIndex(rec); // 资产元数据镜像进数据目录 assets.json(迁移便利,失败静默)
|
|
1512
1542
|
return rec;
|
|
1513
1543
|
}
|
|
1544
|
+
/** 资产元数据索引记录(去 blob;文本资产保留 text 以便导入端免读文件即可预览)。 */
|
|
1545
|
+
function assetIndexRecord(rec) {
|
|
1546
|
+
const out = {
|
|
1547
|
+
id: rec.id, projectId: rec.projectId, kind: rec.kind, mime: rec.mime,
|
|
1548
|
+
name: rec.name, createdAt: rec.createdAt, prompt: rec.prompt || '',
|
|
1549
|
+
source: rec.source || '', category: rec.category || 'general',
|
|
1550
|
+
};
|
|
1551
|
+
if (rec.rel) { out.rel = rec.rel; out.size = rec.size || 0; }
|
|
1552
|
+
if (rec.entity) out.entity = rec.entity;
|
|
1553
|
+
if (Array.isArray(rec.entities) && rec.entities.length) out.entities = rec.entities;
|
|
1554
|
+
if (rec.nodeLabel) out.nodeLabel = rec.nodeLabel;
|
|
1555
|
+
if (rec.kind === 'text' && typeof rec.text === 'string') out.text = rec.text;
|
|
1556
|
+
return out;
|
|
1557
|
+
}
|
|
1558
|
+
/** 把资产元数据镜像进数据目录的 assets.json(失败静默:索引是迁移便利,不是主存储)。 */
|
|
1559
|
+
function mirrorAssetIndex(rec) {
|
|
1560
|
+
if (!S.dataRoot || !rec || !rec.id) return;
|
|
1561
|
+
const proj = S.projects.find((p) => p.id === rec.projectId);
|
|
1562
|
+
hostRpc('assetIndexPut', {
|
|
1563
|
+
projectId: rec.projectId, projectName: (proj && proj.name) || rec.projectId, asset: assetIndexRecord(rec),
|
|
1564
|
+
}).catch(() => { /* 索引失败不影响资产本身 */ });
|
|
1565
|
+
}
|
|
1566
|
+
/** 启动/设置数据目录后:一次性把 IndexedDB 里的全部资产元数据同步进 assets.json(补齐历史资产索引)。
|
|
1567
|
+
* 全量**替换**语义(含空项目占位):IndexedDB 是资产库唯一事实源,借此自愈删除后残留的幽灵条目。 */
|
|
1568
|
+
let assetIndexSynced = false;
|
|
1569
|
+
async function syncAssetIndexToHost() {
|
|
1570
|
+
if (assetIndexSynced || !S.dataRoot) return;
|
|
1571
|
+
assetIndexSynced = true;
|
|
1572
|
+
try {
|
|
1573
|
+
const all = await idbAll();
|
|
1574
|
+
const items = [];
|
|
1575
|
+
const seen = new Set();
|
|
1576
|
+
const projName = new Map(S.projects.map((p) => [p.id, p.name]));
|
|
1577
|
+
for (const rec of all) {
|
|
1578
|
+
if (!rec || !rec.id || !rec.projectId) continue;
|
|
1579
|
+
seen.add(rec.projectId);
|
|
1580
|
+
items.push({ projectId: rec.projectId, projectName: projName.get(rec.projectId) || rec.projectId, asset: assetIndexRecord(rec) });
|
|
1581
|
+
}
|
|
1582
|
+
// 本机已知但没有任何资产的项目也要发占位:assets.json 清空对齐(否则删除后会残留幽灵条目)
|
|
1583
|
+
for (const p of S.projects) {
|
|
1584
|
+
if (!seen.has(p.id)) items.push({ projectId: p.id, projectName: p.name, asset: null });
|
|
1585
|
+
}
|
|
1586
|
+
if (items.length) await hostRpc('assetIndexBulkPut', { items });
|
|
1587
|
+
} catch { /* 索引同步失败不影响 */ }
|
|
1588
|
+
}
|
|
1514
1589
|
/** 一次性把 IndexedDB 里的历史资产迁移到数据根目录(保留元数据,移除 blob)。 */
|
|
1515
1590
|
async function migrateAssetsToDisk(onProgress) {
|
|
1516
1591
|
if (!S.dataRoot) throw new Error('请先设置数据根目录');
|
|
1517
1592
|
const all = await idbAll();
|
|
1518
|
-
|
|
1593
|
+
// 二进制资产(有 blob)与文本资产(text,此前从不落盘)都要迁移
|
|
1594
|
+
const targets = all.filter((r) => (r.blob && !r.rel) || (r.kind === 'text' && !r.rel && typeof r.text === 'string' && r.text.length));
|
|
1519
1595
|
let done = 0;
|
|
1520
1596
|
for (const rec of targets) {
|
|
1521
|
-
const
|
|
1522
|
-
const
|
|
1597
|
+
const isText = rec.kind === 'text' && !rec.blob;
|
|
1598
|
+
const rel = `projects/${rec.projectId}/${rec.kind}/${assetFileBase(rec)}.${isText ? 'md' : extFromMime(rec.mime)}`;
|
|
1599
|
+
const blob = isText ? new Blob([rec.text || ''], { type: 'text/markdown' }) : rec.blob;
|
|
1600
|
+
const r = await fetch('/' + NS + '/api/asset?rel=' + encodeURIComponent(rel), { method: 'POST', body: blob });
|
|
1523
1601
|
if (!r.ok) throw new Error('迁移失败: ' + rel + ' — ' + (await r.text()));
|
|
1524
|
-
const next = { ...rec, rel, size:
|
|
1525
|
-
|
|
1602
|
+
const next = { ...rec, rel, size: blob.size, stored: 'disk' };
|
|
1603
|
+
if (isText) next.text = rec.text; // 文本保留在元数据里:预览/下载不必回读文件
|
|
1604
|
+
else delete next.blob;
|
|
1526
1605
|
await idbPut(next);
|
|
1606
|
+
mirrorAssetIndex(next); // 迁移后刷新数据目录 assets.json(补 rel/size)
|
|
1527
1607
|
assetUrlCache.delete(rec.id); // 旧 objectURL 失效,改走文件 URL
|
|
1528
1608
|
done++;
|
|
1529
1609
|
if (onProgress) onProgress(done, targets.length);
|
|
@@ -1648,11 +1728,30 @@ window.__ModuleLoader__.load({
|
|
|
1648
1728
|
return new Blob(chunks, { type: mime.split(';')[0] });
|
|
1649
1729
|
}
|
|
1650
1730
|
|
|
1731
|
+
/** 外部 API 请求统一经 Host 代理(图片/视频/音频网关都走这里):浏览器直连第三方接口会被
|
|
1732
|
+
* CORS 拦截(带 Authorization 的 POST 触发预检,网关不返回 CORS 头 → TypeError: Failed to fetch)。
|
|
1733
|
+
* 返回 { ok, status, contentType, text, blob }。 */
|
|
1734
|
+
async function fetchViaProxy(url, { method = 'POST', headers = {}, body = null, bodyRaw = null, bodyContentType = '', timeoutMs = 120000, label = '请求' } = {}) {
|
|
1735
|
+
const payload = { url, method, headers, timeoutMs };
|
|
1736
|
+
if (bodyRaw != null) { payload.bodyRaw = bodyRaw; payload.bodyContentType = bodyContentType; }
|
|
1737
|
+
else if (body != null) payload.body = body;
|
|
1738
|
+
// 客户端→Host 的预算比上游超时多 15s:让上游超时(504/带原因)先到,而不是客户端先断
|
|
1739
|
+
const res = await fetchT('/' + NS + '/api/fetch-proxy', {
|
|
1740
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload),
|
|
1741
|
+
}, Number(timeoutMs) + 15000, label + '(代理)');
|
|
1742
|
+
const ct = res.headers.get('content-type') || '';
|
|
1743
|
+
const buf = await res.arrayBuffer();
|
|
1744
|
+
const blob = new Blob([buf], { type: ct || 'application/octet-stream' });
|
|
1745
|
+
let text = '';
|
|
1746
|
+
if (!ct || /json|text|plain/.test(ct)) { try { text = new TextDecoder().decode(buf); } catch { text = ''; } }
|
|
1747
|
+
return { ok: res.ok, status: res.status, contentType: ct, text, blob };
|
|
1748
|
+
}
|
|
1651
1749
|
async function apiImage(prompt, refBlobs, s, opts) {
|
|
1652
1750
|
if (!s.baseUrl) throw new Error('未配置图片 API');
|
|
1653
1751
|
const base = s.baseUrl.replace(/\/+$/, '');
|
|
1654
1752
|
const refs = refBlobs || [];
|
|
1655
1753
|
const auth = s.apiKey ? { Authorization: 'Bearer ' + s.apiKey } : {};
|
|
1754
|
+
const upstreamTimeout = Number(s.timeoutMs) > 0 ? Number(s.timeoutMs) : 120000; // 商汤等同步接口 20s+,默认 120s
|
|
1656
1755
|
// 参考图编辑模式:multipart 上传到 /images/edits(OpenAI gpt-image-1 兼容)
|
|
1657
1756
|
if (refs.length && s.mode === 'edits') {
|
|
1658
1757
|
const fd = new FormData();
|
|
@@ -1661,15 +1760,20 @@ window.__ModuleLoader__.load({
|
|
|
1661
1760
|
fd.append('n', '1');
|
|
1662
1761
|
if (s.size) fd.append('size', s.size);
|
|
1663
1762
|
refs.forEach((b, i) => fd.append('image[]', b, 'ref-' + i + '.png'));
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
const
|
|
1763
|
+
// FormData 经 Response 序列化为 multipart 字节(含 boundary)→ base64 经代理原样转发
|
|
1764
|
+
const serialized = new Response(fd);
|
|
1765
|
+
const bodyRaw = arrayBufferToBase64(await serialized.arrayBuffer());
|
|
1766
|
+
const bodyContentType = serialized.headers.get('content-type') || 'multipart/form-data';
|
|
1767
|
+
const res = await fetchViaProxy(base + '/images/edits', { headers: auth, bodyRaw, bodyContentType, timeoutMs: upstreamTimeout, label: '图片编辑 API' });
|
|
1768
|
+
if (!res.ok) throw new Error('图片编辑 API HTTP ' + res.status + ':' + res.text.slice(0, 300));
|
|
1769
|
+
let j = null;
|
|
1770
|
+
try { j = JSON.parse(res.text); } catch { j = null; }
|
|
1667
1771
|
const d = j && j.data && j.data[0];
|
|
1668
1772
|
if (d && d.b64_json) return base64ToBlob(d.b64_json, 'image/png');
|
|
1669
1773
|
if (d && d.url) {
|
|
1670
|
-
const r2 = await
|
|
1774
|
+
const r2 = await fetchViaProxy(d.url, { method: 'GET', timeoutMs: 120000, label: '下载图片' });
|
|
1671
1775
|
if (!r2.ok) throw new Error('下载生成图片失败 HTTP ' + r2.status);
|
|
1672
|
-
return
|
|
1776
|
+
return r2.blob;
|
|
1673
1777
|
}
|
|
1674
1778
|
throw new Error('图片编辑 API 响应中没有 data[0].b64_json / url');
|
|
1675
1779
|
}
|
|
@@ -1683,22 +1787,19 @@ window.__ModuleLoader__.load({
|
|
|
1683
1787
|
const sizeOverride = String(s.size || '').trim();
|
|
1684
1788
|
if (sizeOverride) body.size = sizeOverride;
|
|
1685
1789
|
else { const dims = imageDimsFor(opts); body.size = `${dims.w}x${dims.h}`; }
|
|
1686
|
-
|
|
1687
|
-
// 否则会把大体积 base64 数组发给不识别的网关 → HTTP 400,用户看到的却是"没反应"
|
|
1688
|
-
if (refs.length && (s.mode === 'edits')) body.images = await Promise.all(refs.map((b) => blobToDataUrl(b)));
|
|
1689
|
-
const res = await fetchT(base + '/images/generations', {
|
|
1690
|
-
method: 'POST',
|
|
1790
|
+
const res = await fetchViaProxy(base + '/images/generations', {
|
|
1691
1791
|
headers: { 'Content-Type': 'application/json', ...auth },
|
|
1692
|
-
body:
|
|
1693
|
-
}
|
|
1694
|
-
if (!res.ok) throw new Error('图片 API HTTP ' + res.status + ':' +
|
|
1695
|
-
|
|
1792
|
+
body, timeoutMs: upstreamTimeout, label: '图片 API',
|
|
1793
|
+
});
|
|
1794
|
+
if (!res.ok) throw new Error('图片 API HTTP ' + res.status + ':' + res.text.slice(0, 300));
|
|
1795
|
+
let j = null;
|
|
1796
|
+
try { j = JSON.parse(res.text); } catch { j = null; }
|
|
1696
1797
|
const d = j && j.data && j.data[0];
|
|
1697
1798
|
if (d && d.b64_json) return base64ToBlob(d.b64_json, 'image/png');
|
|
1698
1799
|
if (d && d.url) {
|
|
1699
|
-
const r2 = await
|
|
1800
|
+
const r2 = await fetchViaProxy(d.url, { method: 'GET', timeoutMs: 120000, label: '下载图片' });
|
|
1700
1801
|
if (!r2.ok) throw new Error('下载生成图片失败 HTTP ' + r2.status);
|
|
1701
|
-
return
|
|
1802
|
+
return r2.blob;
|
|
1702
1803
|
}
|
|
1703
1804
|
throw new Error('图片 API 响应中没有 data[0].b64_json / url');
|
|
1704
1805
|
}
|
|
@@ -1734,9 +1835,10 @@ window.__ModuleLoader__.load({
|
|
|
1734
1835
|
?? (j.output && (j.output.task_id ?? j.output.id));
|
|
1735
1836
|
}
|
|
1736
1837
|
async function downloadVideo(url, headers) {
|
|
1737
|
-
|
|
1838
|
+
// 经 Host 代理:签名下载直连同样会被 CORS 拦
|
|
1839
|
+
const r = await fetchViaProxy(url, { method: 'GET', headers: headers || {}, timeoutMs: 300000, label: '下载视频' });
|
|
1738
1840
|
if (!r.ok) throw new Error('下载视频失败 HTTP ' + r.status);
|
|
1739
|
-
return
|
|
1841
|
+
return r.blob;
|
|
1740
1842
|
}
|
|
1741
1843
|
/** 火山方舟 Seedance 官方协议:创建任务 → 轮询 → 下载(支持首帧 + 最多 4 张参考图)。 */
|
|
1742
1844
|
async function seedanceVideo(prompt, frameBlob, refs, s, opts) {
|
|
@@ -1762,17 +1864,17 @@ window.__ModuleLoader__.load({
|
|
|
1762
1864
|
generate_audio: s.generateAudio !== false,
|
|
1763
1865
|
};
|
|
1764
1866
|
body.ratio = frameBlob ? 'adaptive' : (o.ratio || '16:9');
|
|
1765
|
-
const res = await
|
|
1766
|
-
if (!res.ok) throw new Error('Seedance HTTP ' + res.status + ':' +
|
|
1767
|
-
const j =
|
|
1867
|
+
const res = await fetchViaProxy(base, { headers, body, timeoutMs: Number(s.timeoutMs) > 0 ? Number(s.timeoutMs) : 120000, label: 'Seedance 视频 API' });
|
|
1868
|
+
if (!res.ok) throw new Error('Seedance HTTP ' + res.status + ':' + res.text.slice(0, 300));
|
|
1869
|
+
const j = JSON.parse(res.text || '{}');
|
|
1768
1870
|
const taskId = j && j.id;
|
|
1769
1871
|
if (!taskId) throw new Error('Seedance 响应中没有任务 id');
|
|
1770
1872
|
const deadline = Date.now() + 15 * 60 * 1000;
|
|
1771
1873
|
while (Date.now() < deadline) {
|
|
1772
1874
|
await sleep(10000);
|
|
1773
|
-
const r2 = await
|
|
1875
|
+
const r2 = await fetchViaProxy(base + '/' + taskId, { method: 'GET', headers, timeoutMs: 60000, label: '查询 Seedance 任务' });
|
|
1774
1876
|
if (!r2.ok) throw new Error('查询 Seedance 任务失败 HTTP ' + r2.status);
|
|
1775
|
-
const st =
|
|
1877
|
+
const st = JSON.parse(r2.text || '{}');
|
|
1776
1878
|
if (st.status === 'succeeded') {
|
|
1777
1879
|
const u = st.content && st.content.video_url;
|
|
1778
1880
|
if (!u) throw new Error('Seedance 任务成功但未返回视频地址');
|
|
@@ -1805,10 +1907,10 @@ window.__ModuleLoader__.load({
|
|
|
1805
1907
|
if (frameBlob) {
|
|
1806
1908
|
try { body.image = await blobToDataUrl(frameBlob); } catch { /* 忽略首帧编码失败 */ }
|
|
1807
1909
|
}
|
|
1808
|
-
const res = await
|
|
1809
|
-
if (!res.ok) throw new Error('视频 API HTTP ' + res.status + ':' +
|
|
1910
|
+
const res = await fetchViaProxy(base, { headers, body, timeoutMs: Number(s.timeoutMs) > 0 ? Number(s.timeoutMs) : 120000, label: '视频 API' });
|
|
1911
|
+
if (!res.ok) throw new Error('视频 API HTTP ' + res.status + ':' + res.text.slice(0, 300));
|
|
1810
1912
|
let j = null;
|
|
1811
|
-
try { j =
|
|
1913
|
+
try { j = JSON.parse(res.text); } catch { /* 非 JSON 响应 */ }
|
|
1812
1914
|
const direct = extractVideoUrl(j);
|
|
1813
1915
|
if (direct) return await downloadVideo(direct, {});
|
|
1814
1916
|
const taskId = extractTaskId(j);
|
|
@@ -1816,10 +1918,10 @@ window.__ModuleLoader__.load({
|
|
|
1816
1918
|
const deadline = Date.now() + 15 * 60 * 1000;
|
|
1817
1919
|
while (Date.now() < deadline) {
|
|
1818
1920
|
await sleep(3000);
|
|
1819
|
-
const r2 = await
|
|
1921
|
+
const r2 = await fetchViaProxy(base + '/' + taskId, { method: 'GET', headers, timeoutMs: 60000, label: '查询视频任务' });
|
|
1820
1922
|
if (!r2.ok) throw new Error('查询视频任务失败 HTTP ' + r2.status);
|
|
1821
1923
|
let st = null;
|
|
1822
|
-
try { st =
|
|
1924
|
+
try { st = JSON.parse(r2.text); } catch { /* 忽略 */ }
|
|
1823
1925
|
const u = extractVideoUrl(st);
|
|
1824
1926
|
if (u) return await downloadVideo(u, {});
|
|
1825
1927
|
// 失败判定必须看状态字段,不能拿整串 JSON 做子串匹配:
|
|
@@ -2212,7 +2314,9 @@ window.__ModuleLoader__.load({
|
|
|
2212
2314
|
const pid = S.activeId;
|
|
2213
2315
|
const map = craftMapLoad(pid);
|
|
2214
2316
|
if (!opts.force && map[rec.id]) return map[rec.id];
|
|
2215
|
-
|
|
2317
|
+
// Craft 后期数据统一放在**画布数据根目录**的项目目录下:projects/<项目>/<slug 前缀去掉>/craft/…
|
|
2318
|
+
// (ArtCraft 数据根目录 = 画布数据根目录时,即落在 <root>/projects/<项目>/craft/media/)
|
|
2319
|
+
const rel = 'projects/' + craftProjectSlug() + '/craft/media/' + assetFileBase(rec) + extForMime(rec.mime, rec.name);
|
|
2216
2320
|
const blob = rec.blob || await assetBlob(rec.id);
|
|
2217
2321
|
if (!blob) throw new Error('资产内容不可用: ' + rec.id);
|
|
2218
2322
|
await craft.write(rel, blob);
|
|
@@ -3012,8 +3116,25 @@ window.__ModuleLoader__.load({
|
|
|
3012
3116
|
source: (meta && meta.source) || 'ai',
|
|
3013
3117
|
category: (meta && meta.category) || 'script',
|
|
3014
3118
|
};
|
|
3119
|
+
// 文本资产同样落盘到数据根目录(<root>/projects/<项目>/text/<名>.md),
|
|
3120
|
+
// IndexedDB 保留元数据与 text 字段(文本很小,预览/下载不必回读文件)。
|
|
3121
|
+
// 落盘失败降级为仅 IndexedDB(绝不阻塞生成主流程),控制台留痕。
|
|
3122
|
+
if (S.dataRoot) {
|
|
3123
|
+
try {
|
|
3124
|
+
const rel = `projects/${projectId}/text/${assetFileBase(rec)}.md`;
|
|
3125
|
+
const blob = new Blob([rec.text], { type: 'text/markdown' });
|
|
3126
|
+
const r = await fetch('/' + NS + '/api/asset?rel=' + encodeURIComponent(rel), { method: 'POST', body: blob });
|
|
3127
|
+
if (!r.ok) throw new Error(await r.text());
|
|
3128
|
+
rec.rel = rel;
|
|
3129
|
+
rec.size = blob.size;
|
|
3130
|
+
rec.stored = 'disk';
|
|
3131
|
+
} catch (err) {
|
|
3132
|
+
console.warn('[infinite-canvas] 文本资产落盘失败(降级为浏览器存储):', String((err && err.message) || err));
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3015
3135
|
await idbPut(rec);
|
|
3016
3136
|
assetNameCache.set(rec.id, rec.name);
|
|
3137
|
+
mirrorAssetIndex(rec); // 文本资产元数据同样镜像进 assets.json
|
|
3017
3138
|
return rec;
|
|
3018
3139
|
}
|
|
3019
3140
|
|
|
@@ -3541,7 +3662,7 @@ window.__ModuleLoader__.load({
|
|
|
3541
3662
|
if (!String(text || '').trim()) throw new Error(T('aiEmptyResult'));
|
|
3542
3663
|
update((s) => { const nd = s.graph.nodes[n.id]; if (nd) { nd.data.output = text; nd.data.prevOutput = null; nd.data.aiStatus = 'idle'; nd.data.status = 'done'; nd.data.error = null; } s.assetsTick++; });
|
|
3543
3664
|
await saveTextAssetRecord(S.activeId, {
|
|
3544
|
-
name:
|
|
3665
|
+
name: (String(n.data.label || '').trim() || T('scriptAssetPrefix')) + ' ' + nowStamp(),
|
|
3545
3666
|
text, category: 'script', prompt: n.data.idea,
|
|
3546
3667
|
});
|
|
3547
3668
|
update((s) => { s.assetsTick++; });
|
|
@@ -3561,6 +3682,11 @@ window.__ModuleLoader__.load({
|
|
|
3561
3682
|
}
|
|
3562
3683
|
}
|
|
3563
3684
|
|
|
3685
|
+
/** 分镜节点的资产文本(自动入库与手动"存为资产"共用,避免两处格式漂移)。 */
|
|
3686
|
+
function storyboardAssetText(shots) {
|
|
3687
|
+
return (shots || []).map((x) => `【${x.title}】${x.duration}s\n画面:${x.visual}\n动作:${x.motion}\n角色:${(x.characters || []).join('、') || '-'}|场景:${x.scene || '-'}|道具:${(x.props || []).join('、') || '-'}`).join('\n\n');
|
|
3688
|
+
}
|
|
3689
|
+
|
|
3564
3690
|
/** 分镜节点:剧本 → 镜头列表(含一致性检查)。 */
|
|
3565
3691
|
async function genStoryboardNode(n) {
|
|
3566
3692
|
const st = storyboardScriptText(n);
|
|
@@ -3579,8 +3705,8 @@ window.__ModuleLoader__.load({
|
|
|
3579
3705
|
// 分镜里出现的新角色/场景/道具 → 自动补进设定集(描述留空待补充),保证设定集始终是唯一事实源
|
|
3580
3706
|
try { await autoRegisterBibleEntries(shots); } catch { /* 补录失败不影响分镜 */ }
|
|
3581
3707
|
await saveTextAssetRecord(S.activeId, {
|
|
3582
|
-
name:
|
|
3583
|
-
text: shots
|
|
3708
|
+
name: (String(n.data.label || '').trim() || T('storyboardAssetPrefix')) + ' ' + nowStamp(),
|
|
3709
|
+
text: storyboardAssetText(shots),
|
|
3584
3710
|
category: 'storyboard', prompt: st.slice(0, 200),
|
|
3585
3711
|
});
|
|
3586
3712
|
update((s) => { s.assetsTick++; });
|
|
@@ -3635,7 +3761,7 @@ window.__ModuleLoader__.load({
|
|
|
3635
3761
|
update((s) => { const nd = s.graph.nodes[n.id]; if (nd) nd.data.progress = Math.round(frac * 100) + '% · ' + name; });
|
|
3636
3762
|
});
|
|
3637
3763
|
const rec = await saveAssetRecord(S.activeId, 'video', blob, {
|
|
3638
|
-
name:
|
|
3764
|
+
name: (String(n.data.label || '').trim() || T('finalCutDone')) + ' ' + nowStamp(),
|
|
3639
3765
|
prompt: metas.length + ' segments', source: 'finalcut', category: 'finalcut',
|
|
3640
3766
|
});
|
|
3641
3767
|
metas.forEach((m) => URL.revokeObjectURL(m.url));
|
|
@@ -4976,7 +5102,7 @@ window.__ModuleLoader__.load({
|
|
|
4976
5102
|
type: 'button', className: 'dshic-mini is-ghost', title: T('saveTextAsset'),
|
|
4977
5103
|
onPointerDown: (e) => e.stopPropagation(),
|
|
4978
5104
|
onClick: async () => {
|
|
4979
|
-
await saveTextAssetRecord(S.activeId, { name:
|
|
5105
|
+
await saveTextAssetRecord(S.activeId, { name: (String(n.data.label || '').trim() || T('scriptAssetPrefix')) + ' ' + nowStamp(), text: n.data.output, category: 'script', prompt: n.data.idea });
|
|
4980
5106
|
update((s) => { s.assetsTick++; });
|
|
4981
5107
|
toast(T('savedOk'));
|
|
4982
5108
|
},
|
|
@@ -5058,8 +5184,26 @@ window.__ModuleLoader__.load({
|
|
|
5058
5184
|
)
|
|
5059
5185
|
: null,
|
|
5060
5186
|
shots.length
|
|
5061
|
-
? h('
|
|
5062
|
-
|
|
5187
|
+
? h('div', { key: 'acts', className: 'dshic-node-row', style: { alignSelf: 'flex-start' } },
|
|
5188
|
+
// 手动"存为资产":AI 生成已自动入库(category=storyboard),这里用于留存当前分镜的一版快照
|
|
5189
|
+
h('button', {
|
|
5190
|
+
key: 'save', type: 'button', className: 'dshic-mini is-ghost', title: T('saveTextAsset'),
|
|
5191
|
+
onPointerDown: (e) => e.stopPropagation(),
|
|
5192
|
+
onClick: async () => {
|
|
5193
|
+
try {
|
|
5194
|
+
await saveTextAssetRecord(S.activeId, {
|
|
5195
|
+
name: (String(n.data.label || '').trim() || T('storyboardAssetPrefix')) + ' ' + nowStamp(),
|
|
5196
|
+
text: storyboardAssetText(shots), category: 'storyboard',
|
|
5197
|
+
prompt: String(n.data.script || '').slice(0, 200),
|
|
5198
|
+
});
|
|
5199
|
+
update((s) => { s.assetsTick++; });
|
|
5200
|
+
toast(T('savedOk'));
|
|
5201
|
+
} catch (err) { toast(String((err && err.message) || err)); }
|
|
5202
|
+
},
|
|
5203
|
+
}, Ic('download', 11), T('saveTextAsset')),
|
|
5204
|
+
h('button', { key: 'expand', type: 'button', className: 'dshic-mini', onClick: () => expandStoryboard(n), onPointerDown: (e) => e.stopPropagation() },
|
|
5205
|
+
Ic('expandAll', 11), T('expand') + ` (${shots.length})`),
|
|
5206
|
+
)
|
|
5063
5207
|
: null,
|
|
5064
5208
|
];
|
|
5065
5209
|
}
|
|
@@ -5156,14 +5300,15 @@ window.__ModuleLoader__.load({
|
|
|
5156
5300
|
if (!text) { toast(T('subPlaceholder')); return; }
|
|
5157
5301
|
update((s) => { const nd = s.graph.nodes[n.id]; if (nd) { nd.data.tts = nd.data.tts || {}; nd.data.tts[x.e.id] = { status: 'running' }; } });
|
|
5158
5302
|
try {
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
headers: {
|
|
5162
|
-
body:
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5303
|
+
// TTS 也走 Host 代理:浏览器直连 TTS 网关同样会被 CORS 拦
|
|
5304
|
+
const res = await fetchViaProxy((cfg.baseUrl || '').replace(/\/+$/, '') + '/audio/speech', {
|
|
5305
|
+
headers: { ...(cfg.apiKey ? { Authorization: 'Bearer ' + cfg.apiKey } : {}) },
|
|
5306
|
+
body: { ...(cfg.model ? { model: cfg.model } : {}), input: text, voice: cfg.voice || 'alloy', response_format: 'mp3' },
|
|
5307
|
+
timeoutMs: Number(cfg.timeoutMs) > 0 ? Number(cfg.timeoutMs) : 120000,
|
|
5308
|
+
label: 'TTS API',
|
|
5309
|
+
});
|
|
5310
|
+
if (!res.ok) throw new Error('TTS HTTP ' + res.status + ':' + res.text.slice(0, 200));
|
|
5311
|
+
const blob = /^audio\//.test(res.contentType) ? res.blob : new Blob([res.blob], { type: 'audio/mpeg' });
|
|
5167
5312
|
const rec = await saveAssetRecord(S.activeId, 'audio', blob, { name: 'TTS-' + text.slice(0, 12), prompt: text, source: 'api', category: 'audio' });
|
|
5168
5313
|
update((s) => { const nd = s.graph.nodes[n.id]; if (nd) { nd.data.tts = nd.data.tts || {}; nd.data.tts[x.e.id] = { status: 'done', assetId: rec.id }; } s.assetsTick++; });
|
|
5169
5314
|
} catch (e) {
|
|
@@ -5200,12 +5345,12 @@ window.__ModuleLoader__.load({
|
|
|
5200
5345
|
width: dims.w, height: dims.h,
|
|
5201
5346
|
});
|
|
5202
5347
|
try {
|
|
5203
|
-
await craft.writeText(slug + '/craft/timeline.xml', xml);
|
|
5204
|
-
await craft.writeText(slug + '/craft/subs.srt', buildSrt(items));
|
|
5348
|
+
await craft.writeText('projects/' + slug + '/craft/timeline.xml', xml);
|
|
5349
|
+
await craft.writeText('projects/' + slug + '/craft/subs.srt', buildSrt(items));
|
|
5205
5350
|
} catch (e) { return; /* craftFetch 已提示 */ }
|
|
5206
5351
|
setFcDialog({
|
|
5207
|
-
args: JSON.stringify(['--project', slug + '/craft/drama.fcproj', '--import', slug + '/craft/timeline.xml', '--export', slug + '/out/final.mp4'], null, 1),
|
|
5208
|
-
outRel: slug + '/out/final.mp4',
|
|
5352
|
+
args: JSON.stringify(['--project', 'projects/' + slug + '/craft/drama.fcproj', '--import', 'projects/' + slug + '/craft/timeline.xml', '--export', 'projects/' + slug + '/craft/out/final.mp4'], null, 1),
|
|
5353
|
+
outRel: 'projects/' + slug + '/craft/out/final.mp4',
|
|
5209
5354
|
});
|
|
5210
5355
|
};
|
|
5211
5356
|
const submitFc = (args, outRel) => {
|
|
@@ -5540,7 +5685,9 @@ window.__ModuleLoader__.load({
|
|
|
5540
5685
|
const confirmDel = async () => {
|
|
5541
5686
|
const pid = delTarget && delTarget.id;
|
|
5542
5687
|
if (!pid) return;
|
|
5543
|
-
flushSave
|
|
5688
|
+
// 只刷"非待删项目"的待存文档:flushSave 推的是 S.activeId 的全量文档,
|
|
5689
|
+
// 若推的正是待删项目,会与 projectsDelete 竞速把项目原地复活(资产/设定集已清的残废形态)
|
|
5690
|
+
if (pid !== S.activeId) flushSave();
|
|
5544
5691
|
update((s) => {
|
|
5545
5692
|
s.projects = s.projects.filter((p) => p.id !== pid);
|
|
5546
5693
|
try { localStorage.removeItem(LS_GRAPH + pid); } catch { /* 忽略 */ }
|
|
@@ -5755,7 +5902,11 @@ window.__ModuleLoader__.load({
|
|
|
5755
5902
|
if (files.length) setTick((x) => x + 1);
|
|
5756
5903
|
};
|
|
5757
5904
|
const del = async (a) => {
|
|
5758
|
-
|
|
5905
|
+
// 数据目录同步:删文件(逻辑路径由 Host 映射到项目名文件夹)+ 清 assets.json 索引
|
|
5906
|
+
const proj = S.projects.find((p) => p.id === (a.projectId || S.activeId));
|
|
5907
|
+
try {
|
|
5908
|
+
await hostRpc('assetDelete', { projectId: a.projectId || S.activeId, projectName: (proj && proj.name) || '', id: a.id, rel: a.rel || '' });
|
|
5909
|
+
} catch { /* 磁盘/索引清理失败不阻塞 */ }
|
|
5759
5910
|
await idbDelete(a.id);
|
|
5760
5911
|
update((s) => {
|
|
5761
5912
|
for (const nd of Object.values(s.graph.nodes)) {
|
|
@@ -5994,13 +6145,12 @@ window.__ModuleLoader__.load({
|
|
|
5994
6145
|
// 默认项 = 内置专家 persona + 内置创作方法论 canvas-craft(自动叠加)
|
|
5995
6146
|
const options = [{ value: '', label: T('builtinExpert') }].concat((data.skills || []).map((x) => ({ value: x.name, label: x.name })));
|
|
5996
6147
|
return h('div', { className: 'dshic-field' },
|
|
5997
|
-
h('span', { className: 'dshic-field-label' }, label),
|
|
6148
|
+
h('span', { className: 'dshic-field-label' }, label, InfoTip(T('skillHint'))),
|
|
5998
6149
|
h('select', {
|
|
5999
6150
|
className: 'dshic-select', value: value || '',
|
|
6000
6151
|
onChange: (e) => onChange(e.target.value),
|
|
6001
6152
|
onPointerDown: (e) => e.stopPropagation(),
|
|
6002
|
-
}, options.map((o) => h('option', { key: o.value, value: o.value }, o.label)))
|
|
6003
|
-
h('div', { className: 'dshic-hint' }, T('skillHint')));
|
|
6153
|
+
}, options.map((o) => h('option', { key: o.value, value: o.value }, o.label))));
|
|
6004
6154
|
}
|
|
6005
6155
|
|
|
6006
6156
|
function LlmModelSelect() {
|
|
@@ -6051,7 +6201,6 @@ window.__ModuleLoader__.load({
|
|
|
6051
6201
|
},
|
|
6052
6202
|
onPointerDown: (e) => e.stopPropagation(),
|
|
6053
6203
|
}),
|
|
6054
|
-
h('div', { className: 'dshic-hint' }, T('llmHint')),
|
|
6055
6204
|
);
|
|
6056
6205
|
}
|
|
6057
6206
|
|
|
@@ -6065,6 +6214,13 @@ window.__ModuleLoader__.load({
|
|
|
6065
6214
|
onPointerDown: (e) => e.stopPropagation(),
|
|
6066
6215
|
}));
|
|
6067
6216
|
}
|
|
6217
|
+
/** ⓘ 悬停说明:界面简洁化——长解释一律收进悬停提示(原生 title),不再平铺文字。 */
|
|
6218
|
+
function InfoTip(text) {
|
|
6219
|
+
return h('span', {
|
|
6220
|
+
title: String(text || ''), 'aria-label': String(text || ''),
|
|
6221
|
+
style: { flex: 'none', width: 14, height: 14, lineHeight: '13px', textAlign: 'center', fontSize: 10, borderRadius: '50%', border: '1px solid var(--dsw-alias-border-secondary, #777)', color: 'var(--dsw-alias-label-secondary, #999)', cursor: 'help', userSelect: 'none', opacity: 0.85 },
|
|
6222
|
+
}, 'i');
|
|
6223
|
+
}
|
|
6068
6224
|
function ApiListEditor({ kind }) {
|
|
6069
6225
|
const [s, setS] = useState(loadSettings);
|
|
6070
6226
|
const [openId, setOpenId] = useState(null);
|
|
@@ -6214,10 +6370,19 @@ window.__ModuleLoader__.load({
|
|
|
6214
6370
|
/** T2:设置面板 Craft 后期卡片(连接状态 / 工具灯 / 数据目录)。 */
|
|
6215
6371
|
function CraftCard() {
|
|
6216
6372
|
const [, force] = useState(0);
|
|
6373
|
+
const [open, setOpen] = useState(false); // 默认折叠:界面简洁,摘要常显
|
|
6217
6374
|
useEffect(() => { probeCraft().then(() => force((x) => x + 1)); }, []);
|
|
6218
6375
|
const cs = S.craftStatus;
|
|
6219
6376
|
const data = cs && cs.data;
|
|
6220
6377
|
const slug = craftProjectSlug();
|
|
6378
|
+
// Craft 后期目录 = **画布数据根目录** + 项目目录 + \craft(不再有 ArtCraft 独立数据目录的 second 口径)
|
|
6379
|
+
const craftDir = S.dataRoot ? (S.dataRoot.replace(/[\\/]+$/, '') + '\\projects\\' + slug + '\\craft') : '';
|
|
6380
|
+
// ArtCraft 自身的数据目录若与画布数据根目录不一致 → 后期文件会落到两处,必须提醒
|
|
6381
|
+
const artRoot = data && data.dataRoot ? data.dataRoot.path : '';
|
|
6382
|
+
const rootMismatch = !!(S.dataRoot && artRoot && artRoot.replace(/[\\/]+$/, '') !== S.dataRoot.replace(/[\\/]+$/, ''));
|
|
6383
|
+
// Craft 家族全量动态渲染:ArtCraft /status 返回几款 bins 就渲染几款(当前 12 款),不再写死
|
|
6384
|
+
const binKeys = (data && data.bins ? Object.keys(data.bins) : []).sort();
|
|
6385
|
+
const readyCount = binKeys.filter((k) => data.bins[k] && data.bins[k].found).length;
|
|
6221
6386
|
const binRow = (key) => {
|
|
6222
6387
|
const info = data && data.bins ? data.bins[key] : null;
|
|
6223
6388
|
const ok = !!(info && info.found);
|
|
@@ -6229,18 +6394,22 @@ window.__ModuleLoader__.load({
|
|
|
6229
6394
|
);
|
|
6230
6395
|
};
|
|
6231
6396
|
return h('div', { className: 'dshic-card' },
|
|
6232
|
-
h('div', {
|
|
6397
|
+
h('div', {
|
|
6398
|
+
className: 'dshic-row', style: { cursor: 'pointer' },
|
|
6399
|
+
onPointerDown: (e) => e.stopPropagation(), onClick: () => setOpen((x) => !x),
|
|
6400
|
+
title: open ? T('collapse') : T('foldOpen'),
|
|
6401
|
+
},
|
|
6233
6402
|
Ic('film', 12),
|
|
6234
6403
|
h('strong', { style: { fontSize: 12, flex: 1, minWidth: 0 } }, T('craftCard')),
|
|
6235
|
-
h('
|
|
6404
|
+
data ? h('span', { className: 'dshic-hint', style: { flex: 'none' } }, T('craftReadyCount').replace('{r}', String(readyCount)).replace('{n}', String(binKeys.length))) : null,
|
|
6405
|
+
h('button', { type: 'button', className: 'dshic-mini is-ghost', onPointerDown: (e) => e.stopPropagation(), onClick: (e) => { e.stopPropagation(); probeCraft().then(() => force((x) => x + 1)); } }, '↻'),
|
|
6406
|
+
h('span', { style: { flex: 'none', fontSize: 10, transition: 'transform .15s', transform: open ? 'rotate(90deg)' : 'rotate(0deg)' } }, '▶'),
|
|
6236
6407
|
),
|
|
6237
|
-
|
|
6238
|
-
? h('div', { className: 'dshic-hint' }, T('
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
data ? binRow('effectcraft') : null,
|
|
6243
|
-
data ? h('div', { className: 'dshic-hint' }, T('craftProjectDir') + ':' + slug + '/') : null,
|
|
6408
|
+
open ? [
|
|
6409
|
+
craftDir ? h('div', { className: 'dshic-hint' }, T('craftDirLabel') + ':' + craftDir) : h('div', { className: 'dshic-err' }, T('craftDataRootNeed')),
|
|
6410
|
+
rootMismatch ? h('div', { className: 'dshic-err' }, T('craftRootMismatch')) : null,
|
|
6411
|
+
...(data ? binKeys.map((k) => binRow(k)) : []),
|
|
6412
|
+
] : null,
|
|
6244
6413
|
);
|
|
6245
6414
|
}
|
|
6246
6415
|
|
|
@@ -6269,7 +6438,7 @@ window.__ModuleLoader__.load({
|
|
|
6269
6438
|
setRunning(true); setErr(null); setProgress('…');
|
|
6270
6439
|
try {
|
|
6271
6440
|
const mediaRel = await ensureAssetOnDisk(rec);
|
|
6272
|
-
const outRel = craftProjectSlug() + '/out/graded/' + rec.id + '-graded' + extForMime(rec.mime, rec.name);
|
|
6441
|
+
const outRel = 'projects/' + craftProjectSlug() + '/craft/out/graded/' + rec.id + '-graded' + extForMime(rec.mime, rec.name);
|
|
6273
6442
|
const job = await craft.jobStart({
|
|
6274
6443
|
bin: 'photocraft',
|
|
6275
6444
|
args: ['run', mediaRel, '--cmd', cmd, '--params', JSON.stringify(paramsObj), '--out', outRel],
|
|
@@ -6386,6 +6555,8 @@ window.__ModuleLoader__.load({
|
|
|
6386
6555
|
await hostRpc('dataRootSet', { root: val.trim() });
|
|
6387
6556
|
await refreshDataRoot();
|
|
6388
6557
|
toast(T('dataRootSaved') + (S.dataRoot ? ':' + S.dataRoot : ''));
|
|
6558
|
+
assetIndexSynced = false; // 换了目录:索引要往新目录重建
|
|
6559
|
+
syncAssetIndexToHost();
|
|
6389
6560
|
} catch (e) { toast('设置失败:' + ((e && e.message) || e)); }
|
|
6390
6561
|
setBusy(false);
|
|
6391
6562
|
};
|
|
@@ -6394,6 +6565,8 @@ window.__ModuleLoader__.load({
|
|
|
6394
6565
|
await refreshDataRoot();
|
|
6395
6566
|
setVal(S.dataRoot || root);
|
|
6396
6567
|
toast(T('dataRootSaved') + ':' + S.dataRoot);
|
|
6568
|
+
assetIndexSynced = false; // 换了目录:索引要往新目录重建
|
|
6569
|
+
syncAssetIndexToHost();
|
|
6397
6570
|
};
|
|
6398
6571
|
const browse = async () => {
|
|
6399
6572
|
if (busy || picking) return;
|
|
@@ -6410,6 +6583,31 @@ window.__ModuleLoader__.load({
|
|
|
6410
6583
|
setPicking(false); setBusy(false);
|
|
6411
6584
|
};
|
|
6412
6585
|
const openDir = async () => { try { await hostRpc('dataRootReveal'); } catch (e) { toast((e && e.message) || e); } };
|
|
6586
|
+
const importFromRoot = async () => {
|
|
6587
|
+
if (busy || picking) return;
|
|
6588
|
+
setBusy(true);
|
|
6589
|
+
try {
|
|
6590
|
+
const r = await hostRpc('dataRootImport');
|
|
6591
|
+
// 资产元数据 → IndexedDB(本机已存在的同 id 资产跳过,绝不覆盖)
|
|
6592
|
+
let assets = 0;
|
|
6593
|
+
const byProject = r.assets || {};
|
|
6594
|
+
for (const pid of Object.keys(byProject)) {
|
|
6595
|
+
for (const rec of byProject[pid] || []) {
|
|
6596
|
+
if (!rec || !rec.id) continue;
|
|
6597
|
+
const cur = await idbGet(rec.id).catch(() => null);
|
|
6598
|
+
if (cur && (cur.rel || cur.blob)) continue;
|
|
6599
|
+
await idbPut({ ...rec, projectId: pid, stored: rec.rel ? 'disk' : 'idb' });
|
|
6600
|
+
assets++;
|
|
6601
|
+
}
|
|
6602
|
+
}
|
|
6603
|
+
if (assets) update((s) => { s.assetsTick++; });
|
|
6604
|
+
toast(T('dataRootImported').replace('{n}', String((r.imported || []).length)).replace('{a}', String(assets)));
|
|
6605
|
+
// 项目文档 Host 侧已合并:立即拉一轮同步,把新项目带进画布
|
|
6606
|
+
try { await hostSyncTick(); } catch { /* 下一轮 3s 轮询会带 */ }
|
|
6607
|
+
await refreshDataRoot();
|
|
6608
|
+
} catch (e) { toast('导入失败:' + ((e && e.message) || e)); }
|
|
6609
|
+
setBusy(false);
|
|
6610
|
+
};
|
|
6413
6611
|
const migrate = async () => {
|
|
6414
6612
|
if (busy || picking || mig) return;
|
|
6415
6613
|
setBusy(true); setMig({ done: 0, total: 0 });
|
|
@@ -6421,7 +6619,11 @@ window.__ModuleLoader__.load({
|
|
|
6421
6619
|
setBusy(false);
|
|
6422
6620
|
};
|
|
6423
6621
|
return h('div', { key: 'dataroot', className: 'dshic-card' },
|
|
6424
|
-
h('div', { className: 'dshic-row' },
|
|
6622
|
+
h('div', { className: 'dshic-row' },
|
|
6623
|
+
Ic('image', 12),
|
|
6624
|
+
h('strong', { style: { fontSize: 12, flex: 1, minWidth: 0 } }, T('dataRootLabel')),
|
|
6625
|
+
InfoTip(T('dataRootHint')),
|
|
6626
|
+
),
|
|
6425
6627
|
S.dataRootSupported === false
|
|
6426
6628
|
? h('div', { className: 'dshic-hint' }, T('dataRootUnsupported'))
|
|
6427
6629
|
: [
|
|
@@ -6434,12 +6636,16 @@ window.__ModuleLoader__.load({
|
|
|
6434
6636
|
h('button', { type: 'button', className: 'dshic-mini', disabled: busy || picking, onPointerDown: (e) => e.stopPropagation(), onClick: save }, T('dataRootSave')),
|
|
6435
6637
|
),
|
|
6436
6638
|
picking ? h('div', { className: 'dshic-hint', key: 'picking' }, T('dataRootPicking')) : null,
|
|
6437
|
-
h('div', { className: 'dshic-hint', key: 'hint' }, T('dataRootHint')),
|
|
6438
6639
|
h('div', { className: 'dshic-row', key: 'row2' },
|
|
6439
6640
|
S.dataRoot ? h('button', { type: 'button', className: 'dshic-mini', onPointerDown: (e) => e.stopPropagation(), onClick: openDir }, T('dataRootOpen')) : null,
|
|
6440
6641
|
S.dataRoot ? h('button', { type: 'button', className: 'dshic-mini', disabled: busy || picking || !!mig, onPointerDown: (e) => e.stopPropagation(), onClick: migrate }, mig === 'done' ? T('dataRootMigratedDone') : T('dataRootMigrate')) : null,
|
|
6441
6642
|
(mig && mig !== 'done') ? h('span', { style: { fontSize: 11, opacity: 0.85 } }, T('dataRootMigrating') + ' ' + mig.done + '/' + mig.total) : null,
|
|
6442
6643
|
),
|
|
6644
|
+
h('div', { className: 'dshic-row', key: 'row3' },
|
|
6645
|
+
S.dataRoot ? h('button', { type: 'button', className: 'dshic-mini is-ghost', disabled: busy || picking, onPointerDown: (e) => e.stopPropagation(), onClick: importFromRoot }, Ic('folder', 11), T('dataRootImportBtn')) : null,
|
|
6646
|
+
S.dataRoot ? InfoTip(T('dataRootImportHint')) : null,
|
|
6647
|
+
h('div', { className: 'dshic-flex' }),
|
|
6648
|
+
),
|
|
6443
6649
|
],
|
|
6444
6650
|
);
|
|
6445
6651
|
}
|
|
@@ -6455,8 +6661,6 @@ window.__ModuleLoader__.load({
|
|
|
6455
6661
|
};
|
|
6456
6662
|
const field = (label, value, onChange, placeholder, password) => h(Field, { label, value, onChange, placeholder, password });
|
|
6457
6663
|
return [
|
|
6458
|
-
h(CraftCard, { key: 'craft' }),
|
|
6459
|
-
h(DataRootCard, { key: 'dataroot' }),
|
|
6460
6664
|
h('div', { key: 'lang', className: 'dshic-card' },
|
|
6461
6665
|
h('div', { className: 'dshic-row' }, Ic('text', 12), h('strong', { style: { fontSize: 12 } }, T('langLabel'))),
|
|
6462
6666
|
h('select', {
|
|
@@ -6472,24 +6676,27 @@ window.__ModuleLoader__.load({
|
|
|
6472
6676
|
h('option', { value: 'zh' }, T('langZh')),
|
|
6473
6677
|
h('option', { value: 'en' }, T('langEn'))),
|
|
6474
6678
|
),
|
|
6475
|
-
h(
|
|
6476
|
-
|
|
6477
|
-
|
|
6679
|
+
h(DataRootCard, { key: 'dataroot' }),
|
|
6680
|
+
h(CraftCard, { key: 'craft' }),
|
|
6681
|
+
// 专家与技能卡:独立于模型选择
|
|
6682
|
+
h('div', { key: 'skills', className: 'dshic-card' },
|
|
6478
6683
|
h('div', { className: 'dshic-row' },
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
|
|
6482
|
-
}),
|
|
6483
|
-
h(Field, {
|
|
6484
|
-
label: T('llmTemperature'), value: s.textLlm.temperature || '', placeholder: T('llmParamAuto'),
|
|
6485
|
-
onChange: (v) => apply((d) => { d.textLlm.temperature = v; }),
|
|
6486
|
-
}),
|
|
6684
|
+
Ic('spark', 12),
|
|
6685
|
+
h('strong', { style: { fontSize: 12, flex: 1, minWidth: 0 } }, T('skillEnhance')),
|
|
6686
|
+
InfoTip(T('skillHint')),
|
|
6487
6687
|
),
|
|
6488
|
-
h('div', { className: 'dshic-hint' }, T('llmParamHint')),
|
|
6489
|
-
h('div', { className: 'dshic-hint' }, T('skillEnhance')),
|
|
6490
6688
|
h(SkillSelect, { label: T('skillScript'), value: s.skillScript, onChange: (v) => apply((d) => { d.skillScript = v; }) }),
|
|
6491
6689
|
h(SkillSelect, { label: T('skillStoryboard'), value: s.skillStoryboard, onChange: (v) => apply((d) => { d.skillStoryboard = v; }) }),
|
|
6492
6690
|
),
|
|
6691
|
+
// 文本模型卡:只选模型,参数全用 Harness 默认;紧挨图片/视频/TTS 模型卡片
|
|
6692
|
+
h('div', { key: 'llm', className: 'dshic-card' },
|
|
6693
|
+
h('div', { className: 'dshic-row' },
|
|
6694
|
+
Ic('spark', 12),
|
|
6695
|
+
h('strong', { style: { fontSize: 12, flex: 1, minWidth: 0 } }, T('llmLabel')),
|
|
6696
|
+
InfoTip(T('llmHint')),
|
|
6697
|
+
),
|
|
6698
|
+
h(LlmModelSelect),
|
|
6699
|
+
),
|
|
6493
6700
|
h(ApiListEditor, { key: 'imglist', kind: 'image' }),
|
|
6494
6701
|
h(ApiListEditor, { key: 'vidlist', kind: 'video' }),
|
|
6495
6702
|
h(ApiListEditor, { key: 'ttslist', kind: 'tts' }),
|
|
@@ -7043,8 +7250,8 @@ window.__ModuleLoader__.load({
|
|
|
7043
7250
|
probeCraft();
|
|
7044
7251
|
// 检查更新(Host RPC;Host 旧版未带此方法时静默)
|
|
7045
7252
|
checkUpdate(true);
|
|
7046
|
-
// 数据根目录(Host 旧版静默降级为 IndexedDB
|
|
7047
|
-
refreshDataRoot();
|
|
7253
|
+
// 数据根目录(Host 旧版静默降级为 IndexedDB 模式);就绪后把 IDB 资产元数据补齐进数据目录索引
|
|
7254
|
+
refreshDataRoot().then(() => syncAssetIndexToHost()).catch(() => { /* 静默 */ });
|
|
7048
7255
|
// 对话 ↔ 画布同步(启动一次 + 3s 轮询;旧 Host 自动停轮询)
|
|
7049
7256
|
hostSyncTick().then(() => pushAllProjectsToHost());
|
|
7050
7257
|
const hostSyncTimer = setInterval(() => hostSyncTick(), 3000);
|