@ninkch/dsh-infinite-canvas 0.17.18 → 0.18.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/assets/canvas-craft/SKILL.md +20 -19
- package/assets/canvas-studio/SKILL.md +5 -6
- package/client.js +387 -135
- package/docs/migration-0.18.md +70 -0
- package/index.js +655 -29
- package/package.json +1 -1
package/client.js
CHANGED
|
@@ -249,6 +249,7 @@ window.__ModuleLoader__.load({
|
|
|
249
249
|
'imgMode': '参考图模式',
|
|
250
250
|
'modeGenerations': '生图 /images/generations(支持多参考图)',
|
|
251
251
|
'modeEdits': '参考图编辑 /images/edits(多参考图上传)',
|
|
252
|
+
'modeCompshare': '优云智算任务式 /image/v1/tasks(Qwen-Image,参考图登记 + 提示词优化)',
|
|
252
253
|
'catMismatch': '提示:资产分类为「{a}」,端口期望「{b}」,已保留连接',
|
|
253
254
|
'nStoryboard': '分镜表',
|
|
254
255
|
'nScript': '剧本',
|
|
@@ -656,6 +657,7 @@ window.__ModuleLoader__.load({
|
|
|
656
657
|
'imgMode': 'Reference mode',
|
|
657
658
|
'modeGenerations': 'image /images/generations (with references)',
|
|
658
659
|
'modeEdits': 'image edit /images/edits (multi-reference upload)',
|
|
660
|
+
'modeCompshare': 'Compshare task /image/v1/tasks (Qwen-Image, reference upload)',
|
|
659
661
|
'catMismatch': 'Note: asset category is "{a}", port expects "{b}". Connected anyway.',
|
|
660
662
|
'nStoryboard': 'Storyboard',
|
|
661
663
|
'nScript': 'Script',
|
|
@@ -1598,18 +1600,21 @@ window.__ModuleLoader__.load({
|
|
|
1598
1600
|
const rest = nm.slice(pref.length);
|
|
1599
1601
|
return rest === '' || !/[\p{L}\p{N}]/u.test(rest[0]);
|
|
1600
1602
|
}
|
|
1601
|
-
/** 生成用提示词:@标记去掉 + [图N] 绑定 + 命中实体的设定描述。
|
|
1602
|
-
|
|
1603
|
+
/** 生成用提示词:@标记去掉 + [图N] 绑定 + 命中实体的设定描述。
|
|
1604
|
+
* light=true(图片生成专用):只保留风格行与 [图N] 名称绑定——参考图本身就是外观事实源,
|
|
1605
|
+
* 内联设定全文会稀释参考图约束,且文字渲染型模型(Qwen-Image)会把追加文本直接画进画面(用户实测:
|
|
1606
|
+
* 设定集全文被渲染成画面右下角的乱码文字块、剧情词凭空生成道具)。视频路径不用 light(不渲染文字,上下文有益)。 */
|
|
1607
|
+
function buildEffectivePrompt(prompt, refs, entities, bible, light) {
|
|
1603
1608
|
const zh = effLang() !== 'en';
|
|
1604
1609
|
const bind = refs.map((r, i) => {
|
|
1605
1610
|
if (r.entity) {
|
|
1606
1611
|
const ent = matchBibleEntity(r.entity, bible);
|
|
1607
|
-
const desc =
|
|
1608
|
-
return `[图${i + 1}] ${entityLabelOf(ent
|
|
1612
|
+
const desc = light ? '' : ((ent && ent.desc) || '');
|
|
1613
|
+
return `[图${i + 1}] ${entityLabelOf((ent || {}).kind || 'prop')} ${r.entity}${desc ? '(' + desc + ')' : ''}`;
|
|
1609
1614
|
}
|
|
1610
1615
|
return `[图${i + 1}] ${catLabel(r.category)}:${r.name}`;
|
|
1611
1616
|
}).join(';');
|
|
1612
|
-
const entLines = entities.map((e) => `${entityLabelOf(e.kind)} ${e.name}:${e.desc}`);
|
|
1617
|
+
const entLines = light ? [] : entities.map((e) => `${entityLabelOf(e.kind)} ${e.name}:${e.desc}`);
|
|
1613
1618
|
return [
|
|
1614
1619
|
(bible.style || '').trim() ? (zh ? '风格:' : 'Style: ') + bible.style.trim() : '',
|
|
1615
1620
|
bind ? (zh ? '参考图 → ' : 'Refs -> ') + bind : '',
|
|
@@ -1769,7 +1774,8 @@ window.__ModuleLoader__.load({
|
|
|
1769
1774
|
tx.onerror = () => reject(tx.error);
|
|
1770
1775
|
});
|
|
1771
1776
|
}
|
|
1772
|
-
|
|
1777
|
+
const assetMem = new Map(); // id -> rec:服务端元数据缓存(0.18 起服务端是资产库唯一事实源)
|
|
1778
|
+
async function idbGetRaw(id) {
|
|
1773
1779
|
const db = await openDb();
|
|
1774
1780
|
return new Promise((resolve, reject) => {
|
|
1775
1781
|
const rq = db.transaction(DB_STORE, 'readonly').objectStore(DB_STORE).get(id);
|
|
@@ -1777,6 +1783,19 @@ window.__ModuleLoader__.load({
|
|
|
1777
1783
|
rq.onerror = () => reject(rq.error);
|
|
1778
1784
|
});
|
|
1779
1785
|
}
|
|
1786
|
+
/** 资产记录读取(0.18):内存缓存 → 服务端 → 旧 IndexedDB 兜底(迁移期遗留 blob)。 */
|
|
1787
|
+
async function idbGet(id) {
|
|
1788
|
+
if (!id) return null;
|
|
1789
|
+
if (assetMem.has(id)) return assetMem.get(id) || null;
|
|
1790
|
+
const local = await idbGetRaw(id).catch(() => null);
|
|
1791
|
+
if (local) return local;
|
|
1792
|
+
try {
|
|
1793
|
+
const r = await hostRpc('assetsGet', { ids: [id] });
|
|
1794
|
+
const rec = (r && r.items && r.items[0]) || null;
|
|
1795
|
+
if (rec) { assetMem.set(rec.id, rec); assetNameCache.set(rec.id, rec.name); }
|
|
1796
|
+
return rec;
|
|
1797
|
+
} catch { return null; }
|
|
1798
|
+
}
|
|
1780
1799
|
async function idbDelete(id) {
|
|
1781
1800
|
assetNameCache.delete(id);
|
|
1782
1801
|
// 释放该资产的 objectURL:否则 Blob(视频可达几十 MB)会被 URL 钉在内存里直到刷新
|
|
@@ -1793,7 +1812,7 @@ window.__ModuleLoader__.load({
|
|
|
1793
1812
|
tx.onerror = () => reject(tx.error);
|
|
1794
1813
|
});
|
|
1795
1814
|
}
|
|
1796
|
-
async function
|
|
1815
|
+
async function idbByProjectRaw(pid) {
|
|
1797
1816
|
const db = await openDb();
|
|
1798
1817
|
return new Promise((resolve, reject) => {
|
|
1799
1818
|
const idx = db.transaction(DB_STORE, 'readonly').objectStore(DB_STORE).index('projectId');
|
|
@@ -1802,6 +1821,29 @@ window.__ModuleLoader__.load({
|
|
|
1802
1821
|
rq.onerror = () => reject(rq.error);
|
|
1803
1822
|
});
|
|
1804
1823
|
}
|
|
1824
|
+
/** 项目资产清单(0.18):服务端分页拉全 + 旧 IndexedDB 遗留合并(服务端不可用时本地兜底)。 */
|
|
1825
|
+
async function idbByProject(pid) {
|
|
1826
|
+
const out = [];
|
|
1827
|
+
try {
|
|
1828
|
+
let cursor = null;
|
|
1829
|
+
for (;;) {
|
|
1830
|
+
const r = await hostRpc('assetsList', { projectId: pid, limit: 200, cursor }).catch(() => null);
|
|
1831
|
+
if (!r || !Array.isArray(r.items)) break;
|
|
1832
|
+
for (const rec of r.items) {
|
|
1833
|
+
out.push(rec);
|
|
1834
|
+
if (rec) { assetMem.set(rec.id, rec); assetNameCache.set(rec.id, rec.name); }
|
|
1835
|
+
}
|
|
1836
|
+
if (!r.nextCursor) break;
|
|
1837
|
+
cursor = r.nextCursor;
|
|
1838
|
+
}
|
|
1839
|
+
} catch { /* 服务端不可用 → 本地兜底 */ }
|
|
1840
|
+
try {
|
|
1841
|
+
const locals = await idbByProjectRaw(pid);
|
|
1842
|
+
const have = new Set(out.map((x) => x.id));
|
|
1843
|
+
for (const l of locals) if (l && !have.has(l.id)) out.push(l);
|
|
1844
|
+
} catch { /* 忽略 */ }
|
|
1845
|
+
return out.sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
|
|
1846
|
+
}
|
|
1805
1847
|
async function idbAll() {
|
|
1806
1848
|
const db = await openDb();
|
|
1807
1849
|
return new Promise((resolve, reject) => {
|
|
@@ -1817,14 +1859,20 @@ window.__ModuleLoader__.load({
|
|
|
1817
1859
|
const sub = String(mime || '').split(';')[0].split('/')[1] || 'bin';
|
|
1818
1860
|
return /^[a-z0-9]{1,8}$/i.test(sub) ? sub : 'bin';
|
|
1819
1861
|
}
|
|
1862
|
+
/** 资产内容 URL:CAS 对象(sha)→ /api/object(immutable,远程可拉);旧落盘(rel)→ /api/file;本地 blob → objectURL。 */
|
|
1863
|
+
function contentUrlFor(rec) {
|
|
1864
|
+
if (!rec) return null;
|
|
1865
|
+
if (rec.sha) return '/' + NS + '/api/object?sha=' + encodeURIComponent(rec.sha) + (rec.mime ? '&mime=' + encodeURIComponent(rec.mime) : '');
|
|
1866
|
+
if (rec.rel) return fileUrlFor(rec.rel);
|
|
1867
|
+
return null;
|
|
1868
|
+
}
|
|
1820
1869
|
async function assetUrl(id) {
|
|
1821
1870
|
if (!id) return null;
|
|
1822
1871
|
if (assetUrlCache.has(id)) return assetUrlCache.get(id);
|
|
1823
1872
|
const rec = await idbGet(id);
|
|
1824
1873
|
if (!rec) return null;
|
|
1825
|
-
let url =
|
|
1826
|
-
if (
|
|
1827
|
-
else if (rec.blob) {
|
|
1874
|
+
let url = contentUrlFor(rec);
|
|
1875
|
+
if (!url && rec.blob) {
|
|
1828
1876
|
// IndexedDB 资产:objectURL 有内存成本 → 缓存总量设上限,超出时释放最早的一批
|
|
1829
1877
|
if (assetUrlCache.size >= 60) {
|
|
1830
1878
|
const first = assetUrlCache.keys().next();
|
|
@@ -1839,13 +1887,14 @@ window.__ModuleLoader__.load({
|
|
|
1839
1887
|
if (url) assetUrlCache.set(id, url);
|
|
1840
1888
|
return url;
|
|
1841
1889
|
}
|
|
1842
|
-
/** 取资产 Blob
|
|
1890
|
+
/** 取资产 Blob:CAS/磁盘资产经 Host 文件服务拉取(远程同样可用),旧资产直接读 IndexedDB。 */
|
|
1843
1891
|
async function assetBlob(id) {
|
|
1844
1892
|
const rec = await idbGet(id);
|
|
1845
1893
|
if (!rec) return null;
|
|
1846
1894
|
if (rec.blob) return rec.blob;
|
|
1847
|
-
|
|
1848
|
-
|
|
1895
|
+
const url = contentUrlFor(rec);
|
|
1896
|
+
if (url) {
|
|
1897
|
+
const r = await fetch(url);
|
|
1849
1898
|
if (!r.ok) throw new Error('资产读取失败 HTTP ' + r.status);
|
|
1850
1899
|
return await r.blob();
|
|
1851
1900
|
}
|
|
@@ -1869,7 +1918,22 @@ window.__ModuleLoader__.load({
|
|
|
1869
1918
|
if (meta && meta.nodeLabel) rec.nodeLabel = meta.nodeLabel;
|
|
1870
1919
|
if (meta && meta.effectivePrompt) rec.effectivePrompt = meta.effectivePrompt;
|
|
1871
1920
|
if (S.dataRoot) {
|
|
1872
|
-
|
|
1921
|
+
if (S.store === 'sqlite') {
|
|
1922
|
+
// 0.18 主路径:上传 CAS 对象(内容寻址去重)→ 服务端建资产行(任意浏览器/远程立即可见)
|
|
1923
|
+
const r = await fetch('/' + NS + '/api/object', { method: 'POST', body: blob });
|
|
1924
|
+
if (!r.ok) throw new Error('资产上传失败: ' + (await r.text()));
|
|
1925
|
+
const j = await r.json();
|
|
1926
|
+
rec.sha = j.sha;
|
|
1927
|
+
rec.size = j.size || blob.size;
|
|
1928
|
+
rec.stored = 'cas';
|
|
1929
|
+
const proj = S.projects.find((p) => p.id === projectId);
|
|
1930
|
+
const saved = await hostRpc('assetCreate', { asset: assetIndexRecord(rec), projectName: (proj && proj.name) || '' });
|
|
1931
|
+
const out = (saved && saved.asset) || rec;
|
|
1932
|
+
assetMem.set(out.id, out);
|
|
1933
|
+
assetNameCache.set(out.id, out.name);
|
|
1934
|
+
return out;
|
|
1935
|
+
}
|
|
1936
|
+
// JSON 降级(Host 无 node:sqlite):旧落盘路径 + 索引镜像
|
|
1873
1937
|
const rel = `projects/${projectId}/${kind}/${assetFileBase(rec)}.${extFromMime(mime)}`;
|
|
1874
1938
|
const r = await fetch('/' + NS + '/api/asset?rel=' + encodeURIComponent(rel), { method: 'POST', body: blob });
|
|
1875
1939
|
if (!r.ok) throw new Error('资产落盘失败: ' + (await r.text()));
|
|
@@ -1882,7 +1946,7 @@ window.__ModuleLoader__.load({
|
|
|
1882
1946
|
}
|
|
1883
1947
|
await idbPut(rec);
|
|
1884
1948
|
assetNameCache.set(rec.id, rec.name);
|
|
1885
|
-
mirrorAssetIndex(rec); //
|
|
1949
|
+
if (S.dataRoot) mirrorAssetIndex(rec); // JSON 降级模式:元数据镜像进 assets.json
|
|
1886
1950
|
return rec;
|
|
1887
1951
|
}
|
|
1888
1952
|
/** 资产元数据索引记录(去 blob;文本资产保留 text 以便导入端免读文件即可预览)。 */
|
|
@@ -1893,6 +1957,7 @@ window.__ModuleLoader__.load({
|
|
|
1893
1957
|
source: rec.source || '', category: rec.category || 'general',
|
|
1894
1958
|
};
|
|
1895
1959
|
if (rec.rel) { out.rel = rec.rel; out.size = rec.size || 0; }
|
|
1960
|
+
if (rec.sha) { out.sha = rec.sha; out.size = rec.size || 0; }
|
|
1896
1961
|
if (rec.entity) out.entity = rec.entity;
|
|
1897
1962
|
if (Array.isArray(rec.entities) && rec.entities.length) out.entities = rec.entities;
|
|
1898
1963
|
if (rec.nodeLabel) out.nodeLabel = rec.nodeLabel;
|
|
@@ -1907,84 +1972,33 @@ window.__ModuleLoader__.load({
|
|
|
1907
1972
|
projectId: rec.projectId, projectName: (proj && proj.name) || rec.projectId, asset: assetIndexRecord(rec),
|
|
1908
1973
|
}).catch(() => { /* 索引失败不影响资产本身 */ });
|
|
1909
1974
|
}
|
|
1910
|
-
/**
|
|
1911
|
-
*
|
|
1912
|
-
let assetIndexSynced = false;
|
|
1913
|
-
async function syncAssetIndexToHost() {
|
|
1914
|
-
if (assetIndexSynced || !S.dataRoot) return;
|
|
1915
|
-
assetIndexSynced = true;
|
|
1916
|
-
try {
|
|
1917
|
-
const all = await idbAll();
|
|
1918
|
-
const items = [];
|
|
1919
|
-
const seen = new Set();
|
|
1920
|
-
const projName = new Map(S.projects.map((p) => [p.id, p.name]));
|
|
1921
|
-
for (const rec of all) {
|
|
1922
|
-
if (!rec || !rec.id || !rec.projectId) continue;
|
|
1923
|
-
seen.add(rec.projectId);
|
|
1924
|
-
items.push({ projectId: rec.projectId, projectName: projName.get(rec.projectId) || rec.projectId, asset: assetIndexRecord(rec) });
|
|
1925
|
-
}
|
|
1926
|
-
// 本机已知但没有任何资产的项目也要发占位:assets.json 清空对齐(否则删除后会残留幽灵条目)
|
|
1927
|
-
for (const p of S.projects) {
|
|
1928
|
-
if (!seen.has(p.id)) items.push({ projectId: p.id, projectName: p.name, asset: null });
|
|
1929
|
-
}
|
|
1930
|
-
if (items.length) await hostRpc('assetIndexBulkPut', { items });
|
|
1931
|
-
} catch { /* 索引同步失败不影响 */ }
|
|
1932
|
-
}
|
|
1933
|
-
/** 一次性把 IndexedDB 里的历史资产迁移到数据根目录(保留元数据,移除 blob)。 */
|
|
1934
|
-
async function migrateAssetsToDisk(onProgress) {
|
|
1935
|
-
if (!S.dataRoot) throw new Error('请先设置数据根目录');
|
|
1936
|
-
const all = await idbAll();
|
|
1937
|
-
// 二进制资产(有 blob)与文本资产(text,此前从不落盘)都要迁移
|
|
1938
|
-
const targets = all.filter((r) => (r.blob && !r.rel) || (r.kind === 'text' && !r.rel && typeof r.text === 'string' && r.text.length));
|
|
1939
|
-
let done = 0;
|
|
1940
|
-
for (const rec of targets) {
|
|
1941
|
-
const isText = rec.kind === 'text' && !rec.blob;
|
|
1942
|
-
const rel = `projects/${rec.projectId}/${rec.kind}/${assetFileBase(rec)}.${isText ? 'md' : extFromMime(rec.mime)}`;
|
|
1943
|
-
const blob = isText ? new Blob([rec.text || ''], { type: 'text/markdown' }) : rec.blob;
|
|
1944
|
-
const r = await fetch('/' + NS + '/api/asset?rel=' + encodeURIComponent(rel), { method: 'POST', body: blob });
|
|
1945
|
-
if (!r.ok) throw new Error('迁移失败: ' + rel + ' — ' + (await r.text()));
|
|
1946
|
-
const next = { ...rec, rel, size: blob.size, stored: 'disk' };
|
|
1947
|
-
if (isText) next.text = rec.text; // 文本保留在元数据里:预览/下载不必回读文件
|
|
1948
|
-
else delete next.blob;
|
|
1949
|
-
await idbPut(next);
|
|
1950
|
-
mirrorAssetIndex(next); // 迁移后刷新数据目录 assets.json(补 rel/size)
|
|
1951
|
-
assetUrlCache.delete(rec.id); // 旧 objectURL 失效,改走文件 URL
|
|
1952
|
-
done++;
|
|
1953
|
-
if (onProgress) onProgress(done, targets.length);
|
|
1954
|
-
}
|
|
1955
|
-
return { migrated: done, total: targets.length };
|
|
1956
|
-
}
|
|
1957
|
-
/** 启动自检:把分类为「通用」但命名符合规范前缀(角色-/场景-/道具-/剧本-/分镜-)的存量资产纠正分类。 */
|
|
1958
|
-
async function fixGeneralCategories() {
|
|
1959
|
-
try {
|
|
1960
|
-
const all = await idbAll();
|
|
1961
|
-
let fixed = 0;
|
|
1962
|
-
for (const rec of all) {
|
|
1963
|
-
if (!rec || !rec.id || (rec.kind !== 'image' && rec.kind !== 'video')) continue;
|
|
1964
|
-
if ((rec.category || 'general') !== 'general') continue;
|
|
1965
|
-
const c = categoryFromName(rec.name);
|
|
1966
|
-
if (!c) continue;
|
|
1967
|
-
const next = { ...rec, category: c };
|
|
1968
|
-
await idbPut(next);
|
|
1969
|
-
mirrorAssetIndex(next);
|
|
1970
|
-
fixed++;
|
|
1971
|
-
}
|
|
1972
|
-
if (fixed) {
|
|
1973
|
-
update((s) => { s.assetsTick++; });
|
|
1974
|
-
toast(T('assetCategoriesFixed').replace('{n}', String(fixed)));
|
|
1975
|
-
}
|
|
1976
|
-
} catch { /* 静默:失败不影响主流程,下次启动再试 */ }
|
|
1977
|
-
}
|
|
1978
|
-
/** 数据根目录就绪后自动把浏览器 IndexedDB 里的存量资产搬到磁盘(替代原"迁移历史资产"按钮,全自动无需手动触发)。 */
|
|
1975
|
+
/** 数据根目录就绪后:把浏览器 IndexedDB 里的存量 blob 资产搬进服务端(CAS + 资产行),搬完即从本地退役。
|
|
1976
|
+
* 0.17 旧版本的"落盘到项目文件夹 + assets.json 索引"由 Host 侧 v1→v2 迁移负责,这里只处理从未落盘的遗留。 */
|
|
1979
1977
|
async function autoMigrateAssetsToRoot() {
|
|
1980
1978
|
if (!S.dataRoot) return;
|
|
1981
1979
|
try {
|
|
1982
1980
|
const all = await idbAll();
|
|
1983
|
-
const pending = all.
|
|
1984
|
-
if (!pending) return;
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1981
|
+
const pending = all.filter((r) => r && r.blob && !r.rel && !r.sha);
|
|
1982
|
+
if (!pending.length) return;
|
|
1983
|
+
let done = 0;
|
|
1984
|
+
for (const rec of pending) {
|
|
1985
|
+
try {
|
|
1986
|
+
const r = await fetch('/' + NS + '/api/object', { method: 'POST', body: rec.blob });
|
|
1987
|
+
if (!r.ok) continue;
|
|
1988
|
+
const j = await r.json();
|
|
1989
|
+
const meta = { ...rec, sha: j.sha, size: rec.size || rec.blob.size };
|
|
1990
|
+
delete meta.blob;
|
|
1991
|
+
meta.stored = 'cas';
|
|
1992
|
+
const proj = S.projects.find((p) => p.id === rec.projectId);
|
|
1993
|
+
const saved = await hostRpc('assetCreate', { asset: assetIndexRecord(meta), projectName: (proj && proj.name) || '' });
|
|
1994
|
+
const out = (saved && saved.asset) || meta;
|
|
1995
|
+
assetMem.set(out.id, out);
|
|
1996
|
+
try { await idbDelete(rec.id); } catch { /* 本地清理失败不阻塞迁移 */ }
|
|
1997
|
+
done++;
|
|
1998
|
+
} catch { /* 单个失败继续下一个 */ }
|
|
1999
|
+
}
|
|
2000
|
+
if (done) {
|
|
2001
|
+
toast(T('dataRootMigrated').replace('{n}', String(done)));
|
|
1988
2002
|
update((s) => { s.assetsTick++; });
|
|
1989
2003
|
}
|
|
1990
2004
|
} catch { /* 静默:迁移失败不影响主流程,下次设置数据根目录时再试 */ }
|
|
@@ -2142,6 +2156,8 @@ window.__ModuleLoader__.load({
|
|
|
2142
2156
|
const refs = refBlobs || [];
|
|
2143
2157
|
const auth = s.apiKey ? { Authorization: 'Bearer ' + s.apiKey } : {};
|
|
2144
2158
|
const extra = parseExtraParams(s.extra); // 厂商专属参数(留空不发送)
|
|
2159
|
+
// 优云智算(compshare)任务式:Qwen-Image 异步任务(提交 → 轮询 → output_url 下载)
|
|
2160
|
+
if (s.mode === 'compshare') return await compshareImageTask(prompt, refs, s, opts);
|
|
2145
2161
|
const upstreamTimeout = Number(s.timeoutMs) > 0 ? Number(s.timeoutMs) : 120000; // 商汤等同步接口 20s+,默认 120s
|
|
2146
2162
|
// 参考图编辑模式:multipart 上传到 /images/edits(OpenAI gpt-image-1 兼容)
|
|
2147
2163
|
if (refs.length && s.mode === 'edits') {
|
|
@@ -2207,6 +2223,106 @@ window.__ModuleLoader__.load({
|
|
|
2207
2223
|
throw new Error('图片 API 响应中没有 data[0].b64_json / url');
|
|
2208
2224
|
}
|
|
2209
2225
|
|
|
2226
|
+
// —— 优云智算(compshare)Qwen-Image 任务式图片 API ——
|
|
2227
|
+
// 提交 POST /image/v1/tasks(Idempotency-Key 必填,UUID)→ 3s 轮询 GET /image/v1/tasks/{id}
|
|
2228
|
+
// → succeeded 取 task.output_url 下载;failed/cancelled/blocked 抛出明确原因。
|
|
2229
|
+
// 参考图走三步上传(upload_urls → PUT 签名直传 → assets 登记)换 asset_id;
|
|
2230
|
+
// 登记结果缓存在画布资产元数据(compAssetIdCache):同一张 CAS 资产不重复上传。
|
|
2231
|
+
const compAssetIdCache = new Map(); // 画布资产 id → 优云 asset_id
|
|
2232
|
+
/** 参考图上传前置:统一 MIME/扩展名并测真实像素尺寸(平台校验上传宽高必须与图片一致)。 */
|
|
2233
|
+
async function imageBlobMeta(input) {
|
|
2234
|
+
const blob = input.blob || input;
|
|
2235
|
+
const type = (blob.type || 'image/png').toLowerCase();
|
|
2236
|
+
const mime = /jpeg|jpg/.test(type) ? 'image/jpeg' : /webp/.test(type) ? 'image/webp' : 'image/png';
|
|
2237
|
+
const ext = mime === 'image/jpeg' ? 'jpg' : mime === 'image/webp' ? 'webp' : 'png';
|
|
2238
|
+
let width = 0;
|
|
2239
|
+
let height = 0;
|
|
2240
|
+
try {
|
|
2241
|
+
const bmp = await createImageBitmap(blob);
|
|
2242
|
+
width = bmp.width;
|
|
2243
|
+
height = bmp.height;
|
|
2244
|
+
try { bmp.close && bmp.close(); } catch { /* 忽略 */ }
|
|
2245
|
+
} catch { /* 测量失败交由下方显式报错 */ }
|
|
2246
|
+
if (!width || !height) throw new Error('参考图尺寸测量失败(仅支持 PNG/JPG/WebP)');
|
|
2247
|
+
return { blob, mime, ext, width, height };
|
|
2248
|
+
}
|
|
2249
|
+
async function compshareImageTask(prompt, refs, s, opts) {
|
|
2250
|
+
const o = opts || {};
|
|
2251
|
+
let base = String(s.baseUrl || '').trim().replace(/\/+$/, '');
|
|
2252
|
+
base = base.replace(/\/image\/v1$/i, ''); // 粘贴了完整路径前缀也兼容
|
|
2253
|
+
if (!base) throw new Error('未配置图片 API 地址(优云智算:https://cp.compshare.cn)');
|
|
2254
|
+
const headers = { 'Content-Type': 'application/json', Accept: 'application/json', Authorization: 'Bearer ' + (s.apiKey || '') };
|
|
2255
|
+
const extra = parseExtraParams(s.extra); // 透传:optimize_prompt / use_context_ir 等任务级字段
|
|
2256
|
+
const api = async (method, path, body, idem) => {
|
|
2257
|
+
const h = { ...headers };
|
|
2258
|
+
if (idem) h['Idempotency-Key'] = idem;
|
|
2259
|
+
const r = await fetchViaProxy(base + path, { method, headers: h, ...(body !== undefined ? { body } : {}), timeoutMs: 120000, label: '优云图片 ' + method });
|
|
2260
|
+
if (!r.ok) throw new Error('优云图片 API HTTP ' + r.status + ':' + r.text.slice(0, 300));
|
|
2261
|
+
let j = null; try { j = JSON.parse(r.text); } catch { throw new Error('优云图片 API 响应非 JSON:' + r.text.slice(0, 200)); }
|
|
2262
|
+
// 网关业务错误可能 HTTP 200:RetCode≠0 或 {type:'error'} 都算失败
|
|
2263
|
+
if ((j.RetCode != null && Number(j.RetCode) !== 0) || j.type === 'error') {
|
|
2264
|
+
throw new Error('优云图片 API 失败:' + ((j.error && j.error.message) || j.Message || JSON.stringify(j).slice(0, 200)));
|
|
2265
|
+
}
|
|
2266
|
+
return j;
|
|
2267
|
+
};
|
|
2268
|
+
// 参考图:三步上传换 asset_id(≤10 张,顺序生效;已登记过的资产直接复用)
|
|
2269
|
+
const referenceImages = [];
|
|
2270
|
+
for (const ref of (refs || []).slice(0, 10)) {
|
|
2271
|
+
const cached = ref.assetId ? compAssetIdCache.get(ref.assetId) : null;
|
|
2272
|
+
if (cached) { referenceImages.push({ asset_id: cached }); continue; }
|
|
2273
|
+
const meta = await imageBlobMeta(ref);
|
|
2274
|
+
const fileName = 'ref-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 6) + '.' + meta.ext;
|
|
2275
|
+
const up = await api('POST', '/image/v1/upload_urls', { file_name: fileName, content_type: meta.mime, file_size: meta.blob.size });
|
|
2276
|
+
const put = await fetchViaProxy(up.upload_url, {
|
|
2277
|
+
method: 'PUT',
|
|
2278
|
+
headers: { 'Content-Type': meta.mime, Authorization: up.authorization, bucket: up.bucket, ufile_indicated_region: up.region },
|
|
2279
|
+
bodyRaw: arrayBufferToBase64(await meta.blob.arrayBuffer()),
|
|
2280
|
+
bodyContentType: meta.mime,
|
|
2281
|
+
timeoutMs: 180000,
|
|
2282
|
+
label: '参考图上传',
|
|
2283
|
+
});
|
|
2284
|
+
if (!put.ok) throw new Error('参考图上传失败 HTTP ' + put.status + ':' + put.text.slice(0, 200));
|
|
2285
|
+
const reg = await api('POST', '/image/v1/assets', { object_key: up.object_key, file_name: fileName, content_type: meta.mime, file_size: meta.blob.size, width: meta.width, height: meta.height });
|
|
2286
|
+
const aid = reg && reg.reference && reg.reference.asset_id;
|
|
2287
|
+
if (!aid) throw new Error('参考图登记失败:响应中没有 reference.asset_id');
|
|
2288
|
+
if (ref.assetId) compAssetIdCache.set(ref.assetId, aid);
|
|
2289
|
+
referenceImages.push({ asset_id: aid });
|
|
2290
|
+
}
|
|
2291
|
+
// 任务体:用户可变项透传(比例/清晰度原样发送,厂商不支持会明确报错);文本任务不带模型字段(端点即 Qwen-Image)
|
|
2292
|
+
const body = {
|
|
2293
|
+
prompt,
|
|
2294
|
+
aspect_ratio: String(o.ratio || '1:1'),
|
|
2295
|
+
resolution: String(o.size || '1K'),
|
|
2296
|
+
...(referenceImages.length ? { reference_images: referenceImages } : {}),
|
|
2297
|
+
...(extra || {}),
|
|
2298
|
+
};
|
|
2299
|
+
const idem = (globalThis.crypto && crypto.randomUUID) ? crypto.randomUUID() : ('idem-' + Date.now().toString(36) + Math.random().toString(36).slice(2));
|
|
2300
|
+
const j1 = await api('POST', '/image/v1/tasks', body, idem);
|
|
2301
|
+
const tid = j1 && j1.task_id;
|
|
2302
|
+
if (!tid) throw new Error('优云图片任务创建失败:响应中没有 task_id');
|
|
2303
|
+
if (o.onTask) { try { o.onTask(tid, base + '/image/v1/tasks/' + encodeURIComponent(tid)); } catch { /* 忽略 */ } }
|
|
2304
|
+
const FINAL = { failed: 1, cancelled: 1, blocked: 1 };
|
|
2305
|
+
const deadline = Date.now() + 20 * 60 * 1000;
|
|
2306
|
+
for (;;) {
|
|
2307
|
+
await sleep(3000); // 官方推荐 3s 轮询
|
|
2308
|
+
if (genAborted()) {
|
|
2309
|
+
try { await api('POST', '/image/v1/tasks/' + encodeURIComponent(tid) + '/cancel', {}); } catch { /* 取消失败不影响中断(积分自动释放) */ }
|
|
2310
|
+
throw new Error(T('stoppedNote'));
|
|
2311
|
+
}
|
|
2312
|
+
const q = await api('GET', '/image/v1/tasks/' + encodeURIComponent(tid)).catch((e) => { console.warn('[infinite-canvas] 优云图片轮询失败(继续):', String((e && e.message) || e)); return null; });
|
|
2313
|
+
if (!q || !q.task) continue;
|
|
2314
|
+
const st = String(q.task.status || '');
|
|
2315
|
+
if (st === 'succeeded') {
|
|
2316
|
+
if (!q.task.output_url) throw new Error('优云图片任务成功但没有 output_url');
|
|
2317
|
+
const dl = await fetchViaProxy(q.task.output_url, { method: 'GET', timeoutMs: 180000, label: '下载生成图片' });
|
|
2318
|
+
if (!dl.ok) throw new Error('下载生成图片失败 HTTP ' + dl.status);
|
|
2319
|
+
return dl.blob;
|
|
2320
|
+
}
|
|
2321
|
+
if (FINAL[st]) throw new Error('优云图片任务' + (st === 'blocked' ? '被审核拦截' : st) + ':' + (q.task.error_message || q.task.error_code || st));
|
|
2322
|
+
if (Date.now() > deadline) throw new Error('优云图片任务超过 20 分钟未完成(任务 id ' + tid + ',可凭 id 继续回查)');
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2210
2326
|
/** 带超时的 fetch:网关挂住时不会让节点永久处于 running(停止/刷新仍能恢复)。 */
|
|
2211
2327
|
async function fetchT(url, init, timeoutMs, label) {
|
|
2212
2328
|
const ms = Number(timeoutMs) > 0 ? Number(timeoutMs) : 120000;
|
|
@@ -2431,7 +2547,8 @@ window.__ModuleLoader__.load({
|
|
|
2431
2547
|
seedance: { label: '火山方舟 · Seedance(豆包视频)', base: 'https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks', keyPh: 'ARK API Key', note: 'Base URL 填方舟提交端点或 /api/v3 均可;模型 id 如 doubao-seedance-2-0-260128' },
|
|
2432
2548
|
kling: { label: '快手可灵 Kling', base: 'https://api.klingai.com', keyPh: 'AK:SK(冒号分隔)', note: 'Key 填 AK:SK,代码自动签 JWT;国际站把 Base URL 改为 api-singapore.klingai.com;时长按官方 5/10s 取整' },
|
|
2433
2549
|
wan: { label: '阿里通义万相 Wan(DashScope)', base: 'https://dashscope.aliyuncs.com', note: '异步任务制;国际站把 Base URL 改为 dashscope-intl.aliyuncs.com;图生视频走 img_url' },
|
|
2434
|
-
minimax: { label: 'MiniMax
|
|
2550
|
+
minimax: { label: 'MiniMax 海螺视频(V1 协议)', base: 'https://api.minimaxi.com', note: '异步任务制;时长按官方 6/10s 取整;首帧图支持 base64' },
|
|
2551
|
+
minimaxv2: { label: 'MiniMax H3 · V2 协议(优云智算 / 官方)', base: 'https://cp.compshare.cn', keyPh: 'sk-ml-…', note: 'V2 协议自动适配:优云智算走 /minimax/v2,官方(api.minimax.io/.cn)走 /v2;content[] 多模态(首帧/尾帧/参考图≤9/视频≤3/音频≤3),原生音频,分辨率 480P/768P/1080P/2K/4K(大小写敏感),时长 4-30s;use_context_ir / mute_audio 等走扩展参数' },
|
|
2435
2552
|
vidu: { label: '生数科技 Vidu', base: 'https://api.vidu.com', note: '图片支持 base64;时长按官方 4/8s 取整' },
|
|
2436
2553
|
runway: { label: 'Runway Gen-4', base: 'https://api.dev.runwayml.com', note: '代码自动带 X-Runway-Version 头;时长 5/10s;首帧图支持 dataURL' },
|
|
2437
2554
|
veo: { label: 'Google Veo(Gemini API)', base: 'https://generativelanguage.googleapis.com', keyPh: 'API Key', note: '模型 id 如 veo-3.0-fast-generate-preview;图生视频走 inlineData base64' },
|
|
@@ -2513,6 +2630,7 @@ window.__ModuleLoader__.load({
|
|
|
2513
2630
|
const model = String(s.model || '').toLowerCase();
|
|
2514
2631
|
// 模型名优先:第三方网关(relayrouter 等)的 URL 看不出真实厂商,只有模型名能说明
|
|
2515
2632
|
if (/wanx|万相|(^|[^a-z])wan(\d|[-_]\d|x)/.test(model)) return 'wan';
|
|
2633
|
+
if (/minimax-?h3|hailuo-?03/.test(model)) return 'minimaxv2'; // H3 走 V2 协议(必须先于 V1 minimax 判定)
|
|
2516
2634
|
if (/(^|[^a-z])kling/.test(model)) return 'kling';
|
|
2517
2635
|
if (/minimax|hailuo/.test(model)) return 'minimax';
|
|
2518
2636
|
if (/vidu/.test(model)) return 'vidu';
|
|
@@ -2696,6 +2814,62 @@ window.__ModuleLoader__.load({
|
|
|
2696
2814
|
});
|
|
2697
2815
|
return await downloadVideo(url, {});
|
|
2698
2816
|
},
|
|
2817
|
+
// —— MiniMax H3 · V2 协议(优云智算 /minimax/v2,官方 api.minimax.io|api.minimaxi.com /v2):
|
|
2818
|
+
// content[] 多模态自动路由(文生/首尾帧/参考素材),原生音频,分辨率大小写敏感 ——
|
|
2819
|
+
async minimaxv2(prompt, frameBlob, s, o) {
|
|
2820
|
+
const base = vendorBase(s.url, VIDEO_VENDORS.minimaxv2.base);
|
|
2821
|
+
const ex = (() => { try { return parseExtraParams(s.extra) || {}; } catch (e) { throw new Error('扩展参数配置有误:' + ((e && e.message) || e)); } })();
|
|
2822
|
+
// 路径风格:优云智算网关挂 /minimax/v2/...,MiniMax 官方裸 /v2/...;扩展参数 pathPrefix 可显式覆盖
|
|
2823
|
+
const prefix = String(ex.pathPrefix || (/compshare/i.test(base) ? '/minimax' : ''));
|
|
2824
|
+
const headers = { 'Content-Type': 'application/json', Accept: 'application/json', Authorization: 'Bearer ' + (s.apiKey || '') };
|
|
2825
|
+
// 分辨率大小写敏感(480P/768P/1080P/2K/4K):画布的 720P 语义等同原生 768P,显式映射不依赖厂商静默降级
|
|
2826
|
+
const RESO = { '480P': '480P', '720P': '768P', '768P': '768P', '1080P': '1080P', '2K': '2K', '4K': '4K' };
|
|
2827
|
+
const resolution = String(ex.resolution || RESO[String(o.resolution || '').toUpperCase()] || '768P');
|
|
2828
|
+
// content[] 组装:H3 规则——首尾帧与参考素材不可混用;参考音频不能独立使用(必须伴随图/视频)
|
|
2829
|
+
const refs = o.hardFrame ? [] : (o.refs || []).slice(0, 9);
|
|
2830
|
+
const auds = (o.audioRefs || []).slice(0, 3);
|
|
2831
|
+
const refMode = refs.length > 0 || auds.length > 0; // 有参考素材(含音频)→ 全部走参考模式
|
|
2832
|
+
const content = [{ type: 'text', text: prompt }];
|
|
2833
|
+
if (frameBlob && !refMode) {
|
|
2834
|
+
content.push({ type: 'image_url', image_url: { url: await blobToDataUrlScaled(frameBlob) }, role: 'first_frame' });
|
|
2835
|
+
} else {
|
|
2836
|
+
if (frameBlob) content[0].text = videoRefNote(o.refNames) + '\n' + prompt; // 首帧降级为首张参考图,衔接标注前置
|
|
2837
|
+
if (frameBlob) content.push({ type: 'image_url', image_url: { url: await blobToDataUrlScaled(frameBlob) }, role: 'reference_image' });
|
|
2838
|
+
for (const b of refs.slice(0, refMode && frameBlob ? 8 : 9)) content.push({ type: 'image_url', image_url: { url: await blobToDataUrlScaled(b) }, role: 'reference_image' });
|
|
2839
|
+
for (const b of auds) content.push({ type: 'audio_url', audio_url: { url: await blobToDataUrl(b) }, role: 'reference_audio' }); // 音色对齐(原生音画联合)
|
|
2840
|
+
}
|
|
2841
|
+
const body = {
|
|
2842
|
+
model: s.model || 'MiniMax-H3',
|
|
2843
|
+
content,
|
|
2844
|
+
resolution,
|
|
2845
|
+
duration: Math.round(Number(o.duration)) || 5, // 官方 4-30s 整数(优云智算扩展上限 30)
|
|
2846
|
+
ratio: (frameBlob && !refMode) ? 'adaptive' : (o.ratio || '16:9'), // 纯首帧模式比例跟随素材;其余用节点比例
|
|
2847
|
+
aigc_watermark: false, // 稳定默认:不加 AIGC 水印
|
|
2848
|
+
};
|
|
2849
|
+
// 扩展参数透传:use_context_ir / skill_id / mute_audio / pathPrefix / resolution 覆盖等
|
|
2850
|
+
for (const k of ['use_context_ir', 'skill_id', 'mute_audio', 'pathPrefix', 'resolution', 'callback_url', 'callback_token']) { if (ex[k] !== undefined) body[k] = ex[k]; }
|
|
2851
|
+
const r1 = await fetchViaProxy(base + prefix + '/v2/video_generation', { headers, body, label: 'H3 提交' });
|
|
2852
|
+
if (!r1.ok) throw new Error('H3 提交 HTTP ' + r1.status + ':' + r1.text.slice(0, 300));
|
|
2853
|
+
let j1 = null; try { j1 = JSON.parse(r1.text); } catch { throw new Error('H3 响应非 JSON:' + r1.text.slice(0, 200)); }
|
|
2854
|
+
const tid = j1 && (j1.task_id || (j1.data && j1.data.task_id));
|
|
2855
|
+
if (!tid) throw new Error('H3 响应中没有 task_id:' + r1.text.slice(0, 200));
|
|
2856
|
+
if (o.onTask) { try { o.onTask(tid, base + prefix + '/v2/query/video_generation/' + encodeURIComponent(tid)); } catch { /* 持久化失败不影响生成 */ } }
|
|
2857
|
+
const url = await vendorPoll('H3', 8000, async () => {
|
|
2858
|
+
const r = await fetchViaProxy(base + prefix + '/v2/query/video_generation/' + encodeURIComponent(tid), { method: 'GET', headers, timeoutMs: 60000, label: 'H3 查询' });
|
|
2859
|
+
if (!r || !r.ok) return { state: 'wait' };
|
|
2860
|
+
let j = null; try { j = JSON.parse(r.text); } catch { return { state: 'wait' }; }
|
|
2861
|
+
const t = (j && j.task) || j;
|
|
2862
|
+
const st = String((t && t.status) || '').toLowerCase();
|
|
2863
|
+
if (st === 'succeeded') {
|
|
2864
|
+
const u = t && t.content && t.content.url;
|
|
2865
|
+
return u ? { state: 'done', url: u } : { state: 'fail', error: '成功但响应中没有 content.url' };
|
|
2866
|
+
}
|
|
2867
|
+
if (st === 'failed') return { state: 'fail', error: (t && t.error && (t.error.message || t.error.code)) || '失败' };
|
|
2868
|
+
if (st === 'cancelled' || st === 'expired') return { state: 'fail', error: '任务' + st };
|
|
2869
|
+
return { state: 'wait' };
|
|
2870
|
+
});
|
|
2871
|
+
return await downloadVideo(url, {});
|
|
2872
|
+
},
|
|
2699
2873
|
// —— 生数 Vidu:img2video/text2video,图片 base64 直传 ——
|
|
2700
2874
|
async vidu(prompt, frameBlob, s, o) {
|
|
2701
2875
|
const base = vendorBase(s.url, VIDEO_VENDORS.vidu.base);
|
|
@@ -3664,9 +3838,9 @@ window.__ModuleLoader__.load({
|
|
|
3664
3838
|
async function refreshDataRoot() {
|
|
3665
3839
|
try {
|
|
3666
3840
|
const r = await hostRpc('dataRootGet');
|
|
3667
|
-
update((s) => { s.dataRoot = r.dataRoot || null; s.dataRootExists = !!r.exists; s.dataRootSupported = true; });
|
|
3841
|
+
update((s) => { s.dataRoot = r.dataRoot || null; s.dataRootExists = !!r.exists; s.dataRootSupported = true; s.store = r.store || (r.sql ? 'sqlite' : (r.dataRoot ? 'json' : null)); });
|
|
3668
3842
|
} catch (e) {
|
|
3669
|
-
update((s) => { s.dataRoot = null; s.dataRootSupported = !/未知操作/.test(String((e && e.message) || e)); });
|
|
3843
|
+
update((s) => { s.dataRoot = null; s.store = null; s.dataRootSupported = !/未知操作/.test(String((e && e.message) || e)); });
|
|
3670
3844
|
}
|
|
3671
3845
|
}
|
|
3672
3846
|
// ---------- 对话 ↔ 画布同步:轮询 Host 项目索引 + 跳转请求 ----------
|
|
@@ -4393,10 +4567,20 @@ window.__ModuleLoader__.load({
|
|
|
4393
4567
|
source: (meta && meta.source) || 'ai',
|
|
4394
4568
|
category: (meta && meta.category) || 'script',
|
|
4395
4569
|
};
|
|
4396
|
-
//
|
|
4397
|
-
//
|
|
4398
|
-
|
|
4399
|
-
|
|
4570
|
+
// 文本资产:0.18 起正文随元数据进 canvas.db(预览/下载不必回读文件);
|
|
4571
|
+
// JSON 降级落盘 .md 文件;无数据目录降级为仅 IndexedDB。绝不阻塞生成主流程。
|
|
4572
|
+
if (S.dataRoot && S.store === 'sqlite') {
|
|
4573
|
+
try {
|
|
4574
|
+
const proj = S.projects.find((p) => p.id === projectId);
|
|
4575
|
+
const saved = await hostRpc('assetCreate', { asset: assetIndexRecord(rec), projectName: (proj && proj.name) || '' });
|
|
4576
|
+
const out = (saved && saved.asset) || rec;
|
|
4577
|
+
assetMem.set(out.id, out);
|
|
4578
|
+
assetNameCache.set(out.id, out.name);
|
|
4579
|
+
return out;
|
|
4580
|
+
} catch (err) {
|
|
4581
|
+
console.warn('[infinite-canvas] 文本资产入库失败(降级为浏览器存储):', String((err && err.message) || err));
|
|
4582
|
+
}
|
|
4583
|
+
} else if (S.dataRoot) {
|
|
4400
4584
|
try {
|
|
4401
4585
|
const rel = `projects/${projectId}/text/${assetFileBase(rec)}.md`;
|
|
4402
4586
|
const blob = new Blob([rec.text], { type: 'text/markdown' });
|
|
@@ -4411,7 +4595,7 @@ window.__ModuleLoader__.load({
|
|
|
4411
4595
|
}
|
|
4412
4596
|
await idbPut(rec);
|
|
4413
4597
|
assetNameCache.set(rec.id, rec.name);
|
|
4414
|
-
mirrorAssetIndex(rec); //
|
|
4598
|
+
if (S.dataRoot) mirrorAssetIndex(rec); // JSON 降级模式:镜像进 assets.json
|
|
4415
4599
|
return rec;
|
|
4416
4600
|
}
|
|
4417
4601
|
|
|
@@ -4433,6 +4617,7 @@ window.__ModuleLoader__.load({
|
|
|
4433
4617
|
tick: 0,
|
|
4434
4618
|
craftStatus: null, // {at, data} | {at, error}
|
|
4435
4619
|
dataRoot: null, // 数据根目录(Host 配置);null = 未设置(资产走 IndexedDB)
|
|
4620
|
+
store: null, // 服务端存储模式:'sqlite'(0.18 主路径)| 'json'(降级)| null(未就绪/未设置)
|
|
4436
4621
|
dataRootExists: false,
|
|
4437
4622
|
dataRootSupported: true,
|
|
4438
4623
|
updateInfo: null, // Host updateCheck 结果
|
|
@@ -5172,8 +5357,8 @@ window.__ModuleLoader__.load({
|
|
|
5172
5357
|
if (type === 'image-gen') {
|
|
5173
5358
|
// 参考图 = 连线(用户顺序)+ @引用命中的设定集资产(按出现顺序)
|
|
5174
5359
|
const { refs, entities } = await collectRefs(n, prompt, bible);
|
|
5175
|
-
const metas = refs.map((r) => ({ blob: r.blob, name: r.name, category: r.category }));
|
|
5176
|
-
const p = buildEffectivePrompt(prompt, refs, entities, bible);
|
|
5360
|
+
const metas = refs.map((r) => ({ blob: r.blob, name: r.name, category: r.category, assetId: r.assetId }));
|
|
5361
|
+
const p = buildEffectivePrompt(prompt, refs, entities, bible, true); // light:图片不内联设定全文(防文字渲染污染 + 参考图约束稀释)
|
|
5177
5362
|
// 模型解析:节点选择优先,未指定路由到星标默认模型;都没有 = 明确报错(不再静默走演示模式)
|
|
5178
5363
|
const mcfg = resolveImageModelCfg(settings, nodeImageModelRef(n));
|
|
5179
5364
|
if (!mcfg) throw new Error(T('errNoImageModel'));
|
|
@@ -6065,6 +6250,18 @@ window.__ModuleLoader__.load({
|
|
|
6065
6250
|
const models = kind === 'image' ? flattenImageModels(settings) : flattenVideoModels(settings);
|
|
6066
6251
|
const star = kind === 'image' ? settings.imageStar : settings.videoStar;
|
|
6067
6252
|
const cur = kind === 'image' ? nodeImageModelRef(n) : nodeVideoModelRef(n);
|
|
6253
|
+
// 当前选中模型的厂商:H3(V2 协议)分辨率档位扩展为 480P/768P/1080P/2K/4K(大小写敏感)
|
|
6254
|
+
const videoModelName = (() => {
|
|
6255
|
+
if (kind !== 'video') return '';
|
|
6256
|
+
const [pid, mid] = String(cur || '').split('|');
|
|
6257
|
+
const p = (settings.videoProviders || []).find((x) => x.id === pid);
|
|
6258
|
+
const m = p && (p.models || []).find((x) => x.id === mid);
|
|
6259
|
+
return (m && m.model) || '';
|
|
6260
|
+
})();
|
|
6261
|
+
const isH3 = kind === 'video' && /minimax-?h3|hailuo-?03/i.test(videoModelName);
|
|
6262
|
+
const resos = (kind === 'video' && isH3)
|
|
6263
|
+
? (['480P', '768P', '1080P', '2K', '4K'].includes(String(n.data.resolution)) ? ['480P', '768P', '1080P', '2K', '4K'] : ['480P', '768P', '1080P', '2K', '4K'].concat([String(n.data.resolution || '')]))
|
|
6264
|
+
: (VIDEO_RESOLUTIONS.includes(String(n.data.resolution)) ? VIDEO_RESOLUTIONS : VIDEO_RESOLUTIONS.concat([String(n.data.resolution || '')]));
|
|
6068
6265
|
const setModel = (v) => update((s) => {
|
|
6069
6266
|
const nd = s.graph.nodes[n.id];
|
|
6070
6267
|
if (!nd) return;
|
|
@@ -6108,7 +6305,7 @@ window.__ModuleLoader__.load({
|
|
|
6108
6305
|
}),
|
|
6109
6306
|
h(MiniSelect, {
|
|
6110
6307
|
key: 'res', value: n.data.resolution || '720P', title: T('resolutionLabel'), width: 68,
|
|
6111
|
-
options:
|
|
6308
|
+
options: resos.map((x) => ({ value: x, label: x })),
|
|
6112
6309
|
onChange: (v) => update((s) => { const nd = s.graph.nodes[n.id]; if (nd) nd.data.resolution = v; }),
|
|
6113
6310
|
}),
|
|
6114
6311
|
],
|
|
@@ -7607,15 +7804,17 @@ window.__ModuleLoader__.load({
|
|
|
7607
7804
|
lsSet(LS_PROJECTS, S.projects);
|
|
7608
7805
|
lsSet(LS_ACTIVE, S.activeId);
|
|
7609
7806
|
try { hostRpc('projectsDelete', { id: pid }).catch(() => { /* 静默 */ }); } catch { /* 忽略 */ }
|
|
7610
|
-
|
|
7611
|
-
|
|
7612
|
-
|
|
7613
|
-
|
|
7614
|
-
|
|
7615
|
-
await idbDelete(r.id);
|
|
7616
|
-
}
|
|
7617
|
-
}
|
|
7618
|
-
}
|
|
7807
|
+
if (!S.store) {
|
|
7808
|
+
// IndexedDB 模式(未设数据目录):本地逐个清理
|
|
7809
|
+
try {
|
|
7810
|
+
const recs = await idbByProjectRaw(pid);
|
|
7811
|
+
for (const r of recs) {
|
|
7812
|
+
try { await idbDelete(r.id); } catch { /* 逐个容错 */ }
|
|
7813
|
+
}
|
|
7814
|
+
} catch { /* 资产清理失败不影响项目删除 */ }
|
|
7815
|
+
} else {
|
|
7816
|
+
assetMem.clear(); // 服务端随项目删除资产行;清本地元数据缓存
|
|
7817
|
+
}
|
|
7619
7818
|
setDelTarget(null);
|
|
7620
7819
|
};
|
|
7621
7820
|
return [
|
|
@@ -7728,23 +7927,53 @@ window.__ModuleLoader__.load({
|
|
|
7728
7927
|
if (S.activeId && S.activeId !== prevActiveRef.current) { prevActiveRef.current = S.activeId; setScope(S.activeId); }
|
|
7729
7928
|
}, [S.activeId]);
|
|
7730
7929
|
const [search, setSearch] = useState('');
|
|
7930
|
+
const [qDeb, setQDeb] = useState('');
|
|
7931
|
+
const [total, setTotal] = useState(0);
|
|
7932
|
+
const [cursor, setCursor] = useState(null);
|
|
7731
7933
|
const [craftFor, setCraftFor] = useState(null);
|
|
7732
7934
|
const [delTarget, setDelTarget] = useState(null); // 待确认删除的资产(确认框见 DeleteAssetModal)
|
|
7733
7935
|
const [assetName, setAssetName] = useState(''); // 删除确认框的输入内容
|
|
7734
7936
|
const fileRef = useRef(null);
|
|
7937
|
+
// 搜索防抖:0.18 起检索在服务端(SQLite trigram 中文全文索引)
|
|
7938
|
+
useEffect(() => {
|
|
7939
|
+
const t = setTimeout(() => setQDeb(search), 300);
|
|
7940
|
+
return () => clearTimeout(t);
|
|
7941
|
+
}, [search]);
|
|
7735
7942
|
useEffect(() => {
|
|
7736
7943
|
probeCraft();
|
|
7737
7944
|
let ok = true;
|
|
7738
7945
|
(async () => {
|
|
7739
7946
|
try {
|
|
7740
|
-
|
|
7741
|
-
|
|
7742
|
-
|
|
7743
|
-
|
|
7947
|
+
if (!S.store) {
|
|
7948
|
+
// IndexedDB 模式(未设数据目录):本地全量
|
|
7949
|
+
let list;
|
|
7950
|
+
if (scope === 'all') list = await idbAll();
|
|
7951
|
+
else list = (await idbAll()).filter((a) => a.projectId === scope || a.projectId === GENERAL_PID);
|
|
7952
|
+
if (ok) { setAssets(list); setTotal(list.length); setCursor(null); }
|
|
7953
|
+
return;
|
|
7954
|
+
}
|
|
7955
|
+
// 服务端分页:首屏 50 条,滚动到底「加载更多」
|
|
7956
|
+
const r = await hostRpc('assetsList', {
|
|
7957
|
+
projectId: scope === 'all' ? '' : scope,
|
|
7958
|
+
category: filter === 'all' ? '' : filter,
|
|
7959
|
+
q: qDeb.trim(), limit: 50, cursor: null,
|
|
7960
|
+
}).catch(() => null);
|
|
7961
|
+
if (ok && r) { setAssets(r.items || []); setTotal(r.total || 0); setCursor(r.nextCursor || null); }
|
|
7744
7962
|
} catch { if (ok) setAssets([]); }
|
|
7745
7963
|
})();
|
|
7746
7964
|
return () => { ok = false; };
|
|
7747
|
-
}, [S.activeId, tick, S.assetsTick, scope]);
|
|
7965
|
+
}, [S.activeId, tick, S.assetsTick, scope, filter, qDeb, S.store]);
|
|
7966
|
+
const loadMore = async () => {
|
|
7967
|
+
if (!cursor) return;
|
|
7968
|
+
const r = await hostRpc('assetsList', {
|
|
7969
|
+
projectId: scope === 'all' ? '' : scope,
|
|
7970
|
+
category: filter === 'all' ? '' : filter,
|
|
7971
|
+
q: qDeb.trim(), limit: 50, cursor,
|
|
7972
|
+
}).catch(() => null);
|
|
7973
|
+
if (!r) return;
|
|
7974
|
+
setAssets((prev) => prev.concat(r.items || []));
|
|
7975
|
+
setCursor(r.nextCursor || null);
|
|
7976
|
+
};
|
|
7748
7977
|
const projectName = (pid) => {
|
|
7749
7978
|
if (pid === GENERAL_PID) return T('generalProject');
|
|
7750
7979
|
const p = S.projects.find((x) => x.id === pid);
|
|
@@ -7753,10 +7982,17 @@ window.__ModuleLoader__.load({
|
|
|
7753
7982
|
/** 资产改所属项目(通用/其它项目):通用资产多项目共用。 */
|
|
7754
7983
|
const moveAsset = async (a, targetPid) => {
|
|
7755
7984
|
if (!targetPid || targetPid === (a.projectId || '')) return;
|
|
7756
|
-
|
|
7757
|
-
|
|
7758
|
-
|
|
7759
|
-
|
|
7985
|
+
if (S.store) {
|
|
7986
|
+
try {
|
|
7987
|
+
const r = await hostRpc('assetUpdate', { id: a.id, patch: { projectId: targetPid } });
|
|
7988
|
+
if (r && r.asset) assetMem.set(r.asset.id, r.asset);
|
|
7989
|
+
} catch { /* 服务端失败不动本地 */ }
|
|
7990
|
+
} else {
|
|
7991
|
+
const rec = await idbGet(a.id);
|
|
7992
|
+
if (!rec) return;
|
|
7993
|
+
rec.projectId = targetPid;
|
|
7994
|
+
await idbPut(rec);
|
|
7995
|
+
}
|
|
7760
7996
|
update((s) => { s.assetsTick++; });
|
|
7761
7997
|
setTick((x) => x + 1);
|
|
7762
7998
|
toast(T('moved'));
|
|
@@ -7779,7 +8015,8 @@ window.__ModuleLoader__.load({
|
|
|
7779
8015
|
};
|
|
7780
8016
|
const changeCategory = (a, category) => {
|
|
7781
8017
|
a.category = category;
|
|
7782
|
-
|
|
8018
|
+
if (S.store) hostRpc('assetUpdate', { id: a.id, patch: { category } }).catch(() => {});
|
|
8019
|
+
else idbPut(a).catch(() => {});
|
|
7783
8020
|
update((s) => { s.assetsTick++; });
|
|
7784
8021
|
};
|
|
7785
8022
|
const onImport = async (e) => {
|
|
@@ -7797,12 +8034,13 @@ window.__ModuleLoader__.load({
|
|
|
7797
8034
|
if (files.length) setTick((x) => x + 1);
|
|
7798
8035
|
};
|
|
7799
8036
|
const del = async (a) => {
|
|
7800
|
-
//
|
|
8037
|
+
// 服务端删除(SQL:行 + FTS + CAS 引用计数;JSON:文件 + assets.json),随后清本地缓存
|
|
7801
8038
|
const proj = S.projects.find((p) => p.id === (a.projectId || S.activeId));
|
|
7802
8039
|
try {
|
|
7803
8040
|
await hostRpc('assetDelete', { projectId: a.projectId || S.activeId, projectName: (proj && proj.name) || '', id: a.id, rel: a.rel || '' });
|
|
7804
8041
|
} catch { /* 磁盘/索引清理失败不阻塞 */ }
|
|
7805
|
-
await idbDelete(a.id);
|
|
8042
|
+
try { await idbDelete(a.id); } catch { /* 本地无记录则忽略 */ }
|
|
8043
|
+
assetMem.delete(a.id);
|
|
7806
8044
|
update((s) => {
|
|
7807
8045
|
for (const nd of Object.values(s.graph.nodes)) {
|
|
7808
8046
|
if (nd.data && nd.data.assetId === a.id) { nd.data.assetId = null; nd.data.status = 'idle'; }
|
|
@@ -7811,12 +8049,19 @@ window.__ModuleLoader__.load({
|
|
|
7811
8049
|
});
|
|
7812
8050
|
setTick((x) => x + 1);
|
|
7813
8051
|
};
|
|
7814
|
-
//
|
|
8052
|
+
// 资产重命名:服务端写回(SQL/JSON 各自落库)+ 刷新名称缓存(@引用与连线名称都吃这份缓存)
|
|
7815
8053
|
const rename = async (a, name) => {
|
|
7816
|
-
|
|
7817
|
-
|
|
7818
|
-
|
|
7819
|
-
|
|
8054
|
+
if (S.store) {
|
|
8055
|
+
try {
|
|
8056
|
+
const r = await hostRpc('assetUpdate', { id: a.id, patch: { name } });
|
|
8057
|
+
if (r && r.asset) assetMem.set(r.asset.id, r.asset);
|
|
8058
|
+
} catch { /* 服务端失败时仍更新本地缓存保持 UI 一致 */ }
|
|
8059
|
+
} else {
|
|
8060
|
+
const rec = await idbGet(a.id);
|
|
8061
|
+
if (!rec) return;
|
|
8062
|
+
rec.name = name;
|
|
8063
|
+
await idbPut(rec);
|
|
8064
|
+
}
|
|
7820
8065
|
assetNameCache.set(a.id, name);
|
|
7821
8066
|
update((s) => { s.assetsTick++; });
|
|
7822
8067
|
setTick((x) => x + 1);
|
|
@@ -7876,6 +8121,10 @@ window.__ModuleLoader__.load({
|
|
|
7876
8121
|
projectName: projectName(a.projectId),
|
|
7877
8122
|
})),
|
|
7878
8123
|
),
|
|
8124
|
+
cursor
|
|
8125
|
+
? h('button', { key: 'more', type: 'button', className: 'dshic-mini', style: { alignSelf: 'flex-start' }, onClick: loadMore, onPointerDown: (e) => e.stopPropagation() },
|
|
8126
|
+
'加载更多 / Load more(' + assets.length + ' / ' + total + ')')
|
|
8127
|
+
: null,
|
|
7879
8128
|
delTarget ? h(DeleteAssetModal, {
|
|
7880
8129
|
key: 'delasset', target: delTarget, name: assetName, setName: setAssetName,
|
|
7881
8130
|
onCancel: () => { setDelTarget(null); setAssetName(''); },
|
|
@@ -8220,7 +8469,8 @@ window.__ModuleLoader__.load({
|
|
|
8220
8469
|
h('span', { className: 'dshic-field-label' }, T('imgMode')),
|
|
8221
8470
|
h('select', { className: 'dshic-select', value: p.mode || 'generations', onChange: (e) => patchProvider(p.id, 'mode', e.target.value), onPointerDown: (e) => e.stopPropagation() },
|
|
8222
8471
|
h('option', { value: 'generations' }, T('modeGenerations')),
|
|
8223
|
-
h('option', { value: 'edits' }, T('modeEdits'))
|
|
8472
|
+
h('option', { value: 'edits' }, T('modeEdits')),
|
|
8473
|
+
h('option', { value: 'compshare' }, T('modeCompshare')))),
|
|
8224
8474
|
] : [
|
|
8225
8475
|
(detectVideoVendor(p) === 'openai' || detectVideoVendor(p) === 'seedance') ? h(Field, { key: 'of', label: T('vidOutFormat'), value: p.outputFormat || '', onChange: (v) => patchProvider(p.id, 'outputFormat', v), placeholder: 'mp4(留空不发送)' }) : null,
|
|
8226
8476
|
(detectVideoVendor(p) === 'openai' || detectVideoVendor(p) === 'seedance') ? h('label', { key: 'ga', className: 'dshic-row', style: { gap: 6, fontSize: 11 } },
|
|
@@ -8593,9 +8843,10 @@ window.__ModuleLoader__.load({
|
|
|
8593
8843
|
await hostRpc('dataRootSet', { root: val.trim() });
|
|
8594
8844
|
await refreshDataRoot();
|
|
8595
8845
|
toast(T('dataRootSaved') + (S.dataRoot ? ':' + S.dataRoot : ''));
|
|
8596
|
-
if (same) { setBusy(false); return; } //
|
|
8597
|
-
|
|
8598
|
-
|
|
8846
|
+
if (same) { setBusy(false); return; } // 同目录重复保存:不重复迁移
|
|
8847
|
+
autoMigrateAssetsToRoot(); // 换了目录:把本地遗留 blob 搬进新目录(v1 旧布局由 Host 侧自动迁移)
|
|
8848
|
+
assetMem.clear();
|
|
8849
|
+
update((s) => { s.assetsTick++; });
|
|
8599
8850
|
} catch (e) { toast('设置失败:' + ((e && e.message) || e)); }
|
|
8600
8851
|
setBusy(false);
|
|
8601
8852
|
};
|
|
@@ -8605,9 +8856,10 @@ window.__ModuleLoader__.load({
|
|
|
8605
8856
|
await refreshDataRoot();
|
|
8606
8857
|
setVal(S.dataRoot || root);
|
|
8607
8858
|
toast(T('dataRootSaved') + ':' + S.dataRoot);
|
|
8608
|
-
if (same) return; //
|
|
8609
|
-
|
|
8610
|
-
|
|
8859
|
+
if (same) return; // 同目录:不重复迁移
|
|
8860
|
+
autoMigrateAssetsToRoot(); // 换了目录:把本地遗留 blob 搬进新目录
|
|
8861
|
+
assetMem.clear();
|
|
8862
|
+
update((s) => { s.assetsTick++; });
|
|
8611
8863
|
};
|
|
8612
8864
|
const browse = async () => {
|
|
8613
8865
|
if (busy || picking) return;
|
|
@@ -9361,8 +9613,8 @@ window.__ModuleLoader__.load({
|
|
|
9361
9613
|
probeCraft();
|
|
9362
9614
|
// 检查更新(Host RPC;Host 旧版未带此方法时静默)
|
|
9363
9615
|
checkUpdate(true);
|
|
9364
|
-
// 数据根目录(Host 旧版静默降级为 IndexedDB
|
|
9365
|
-
refreshDataRoot().then(() => autoMigrateAssetsToRoot()).
|
|
9616
|
+
// 数据根目录(Host 旧版静默降级为 IndexedDB 模式);就绪后把本地遗留 blob 搬进服务端(v1 旧布局由 Host 侧自动迁移)
|
|
9617
|
+
refreshDataRoot().then(() => autoMigrateAssetsToRoot()).catch(() => { /* 静默 */ });
|
|
9366
9618
|
// 对话 ↔ 画布同步(启动一次 + 3s 轮询;旧 Host 自动停轮询)
|
|
9367
9619
|
hostSyncTick().then(() => pushAllProjectsToHost());
|
|
9368
9620
|
const hostSyncTimer = setInterval(() => hostSyncTick(), 3000);
|