@ninkch/dsh-infinite-canvas 0.18.3 → 0.18.4

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.
Files changed (2) hide show
  1. package/client.js +68 -5
  2. package/package.json +1 -1
package/client.js CHANGED
@@ -2243,8 +2243,38 @@ window.__ModuleLoader__.load({
2243
2243
  // 参考图走三步上传(upload_urls → PUT 签名直传 → assets 登记)换 asset_id;
2244
2244
  // 登记结果缓存在画布资产元数据(compAssetIdCache):同一张 CAS 资产不重复上传。
2245
2245
  const compAssetIdCache = new Map(); // 画布资产 id → 优云 asset_id
2246
+ /** 从文件头字节解析像素尺寸(PNG IHDR / JPEG SOF)——与平台服务端解析口径一致:
2247
+ * createImageBitmap 会按 EXIF 方向旋转并交换宽高,平台读原始文件,两者不一致时登记会被拒。 */
2248
+ function rawImageDims(u8) {
2249
+ try {
2250
+ // PNG:签名 8 字节 + 长度 4 + 'IHDR' → 偏移 16 起是宽高(大端 32 位)
2251
+ if (u8[0] === 0x89 && u8[1] === 0x50) {
2252
+ return {
2253
+ w: ((u8[16] << 24) | (u8[17] << 16) | (u8[18] << 8) | u8[19]) >>> 0,
2254
+ h: ((u8[20] << 24) | (u8[21] << 16) | (u8[22] << 8) | u8[23]) >>> 0,
2255
+ };
2256
+ }
2257
+ // JPEG:顺序扫描段,命中 SOF0..SOF15(排除 DHT/DAC/RSTn)后取段内高/宽
2258
+ if (u8[0] === 0xff && u8[1] === 0xd8) {
2259
+ let i = 2;
2260
+ while (i + 9 < u8.length) {
2261
+ if (u8[i] !== 0xff) { i++; continue; }
2262
+ const marker = u8[i + 1];
2263
+ if (marker === 0xd8 || marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7)) { i += 2; continue; }
2264
+ const len = (u8[i + 2] << 8) | u8[i + 3];
2265
+ if (len < 2) break;
2266
+ if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
2267
+ return { w: (u8[i + 7] << 8) | u8[i + 8], h: (u8[i + 5] << 8) | u8[i + 6] };
2268
+ }
2269
+ i += 2 + len;
2270
+ }
2271
+ }
2272
+ } catch { /* 解析失败返回 null */ }
2273
+ return null;
2274
+ }
2246
2275
  /** 参考图上传前置:魔数嗅探真实编码(平台仅收 JPG/PNG/WebP,且校验声明 MIME 与实际字节一致——
2247
- * 改后缀的 AVIF/HEIC 会在登记时报 "Params [Width/Height] not available")+ 测真实像素尺寸。 */
2276
+ * 改后缀的 AVIF/HEIC 会在登记时报 "Params [Width/Height] not available")+ 测像素尺寸
2277
+ * (同时给出解码尺寸与文件原始尺寸,登记不一致时用原始尺寸重试)。 */
2248
2278
  async function imageBlobMeta(input) {
2249
2279
  const blob = input.blob || input;
2250
2280
  const head = new Uint8Array(await blob.slice(0, 16).arrayBuffer());
@@ -2257,6 +2287,8 @@ window.__ModuleLoader__.load({
2257
2287
  }
2258
2288
  const mime = isPng ? 'image/png' : isJpg ? 'image/jpeg' : 'image/webp';
2259
2289
  const ext = isPng ? 'png' : isJpg ? 'jpg' : 'webp';
2290
+ let raw = null;
2291
+ try { raw = rawImageDims(new Uint8Array(await blob.slice(0, 262144).arrayBuffer())); } catch { /* 忽略 */ }
2260
2292
  let width = 0;
2261
2293
  let height = 0;
2262
2294
  try {
@@ -2264,9 +2296,10 @@ window.__ModuleLoader__.load({
2264
2296
  width = bmp.width;
2265
2297
  height = bmp.height;
2266
2298
  try { bmp.close && bmp.close(); } catch { /* 忽略 */ }
2267
- } catch { /* 测量失败交由下方显式报错 */ }
2299
+ } catch { /* 解码失败时退回文件头尺寸 */ }
2300
+ if (!width || !height) { if (raw) { width = raw.w; height = raw.h; } }
2268
2301
  if (!width || !height) throw new Error('参考图尺寸测量失败');
2269
- return { blob, mime, ext, width, height };
2302
+ return { blob, mime, ext, width, height, rawWidth: raw ? raw.w : width, rawHeight: raw ? raw.h : height };
2270
2303
  }
2271
2304
  async function compshareImageTask(prompt, refs, s, opts) {
2272
2305
  const o = opts || {};
@@ -2275,6 +2308,19 @@ window.__ModuleLoader__.load({
2275
2308
  if (!base) throw new Error('未配置图片 API 地址(优云智算:https://cp.compshare.cn)');
2276
2309
  const headers = { 'Content-Type': 'application/json', Accept: 'application/json', Authorization: 'Bearer ' + String(s.apiKey || '').trim() };
2277
2310
  const extra = parseExtraParams(s.extra); // 透传:optimize_prompt / use_context_ir 等任务级字段
2311
+ // 报错附带实际发出的参数:登记/提交被拒时一眼看到缺什么、值是多少(不再靠猜)
2312
+ const bodySummary = (b) => {
2313
+ if (!b || typeof b !== 'object') return '';
2314
+ const parts = [];
2315
+ for (const [k, v] of Object.entries(b)) {
2316
+ if (v === undefined) { parts.push(k + '=undefined'); continue; }
2317
+ if (v === null) { parts.push(k + '=null'); continue; }
2318
+ if (typeof v === 'object') { parts.push(k + '=[' + (Array.isArray(v) ? v.length + ' 项' : '对象') + ']'); continue; }
2319
+ const t = String(v);
2320
+ parts.push(k + '=' + (t.length > 40 ? t.slice(0, 40) + '…' : t));
2321
+ }
2322
+ return parts.join(' ');
2323
+ };
2278
2324
  const api = async (method, path, body, idem) => {
2279
2325
  const h = { ...headers };
2280
2326
  if (idem) h['Idempotency-Key'] = idem;
@@ -2283,7 +2329,8 @@ window.__ModuleLoader__.load({
2283
2329
  let j = null; try { j = JSON.parse(r.text); } catch { throw new Error(`优云图片 API ${method} ${path} 响应非 JSON:` + r.text.slice(0, 200)); }
2284
2330
  // 网关业务错误可能 HTTP 200:RetCode≠0 或 {type:'error'} 都算失败
2285
2331
  if ((j.RetCode != null && Number(j.RetCode) !== 0) || j.type === 'error') {
2286
- throw new Error(`优云图片 API ${method} ${path} 失败:` + ((j.error && j.error.message) || j.Message || JSON.stringify(j).slice(0, 200)));
2332
+ throw new Error(`优云图片 API ${method} ${path} 失败:` + ((j.error && j.error.message) || j.Message || JSON.stringify(j).slice(0, 200))
2333
+ + (body ? '|已发送参数:' + bodySummary(body) : ''));
2287
2334
  }
2288
2335
  return j;
2289
2336
  };
@@ -2304,7 +2351,23 @@ window.__ModuleLoader__.load({
2304
2351
  label: '参考图上传',
2305
2352
  });
2306
2353
  if (!put.ok) throw new Error('参考图上传失败 HTTP ' + put.status + ':' + put.text.slice(0, 200));
2307
- 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 });
2354
+ // 登记:宽高必须与平台解析出的文件尺寸一致。浏览器解码尺寸与文件原始尺寸在
2355
+ // EXIF 旋转图上不同 → 先用解码尺寸,被拒(Params [Width/Height] not available)时用原始尺寸重试一次
2356
+ const regPayload = (w, h) => ({ object_key: up.object_key, file_name: fileName, content_type: meta.mime, file_size: meta.blob.size, width: w, height: h });
2357
+ let reg = null;
2358
+ try {
2359
+ reg = await api('POST', '/image/v1/assets', regPayload(meta.width, meta.height));
2360
+ } catch (e) {
2361
+ const msg = String((e && e.message) || e);
2362
+ const alt = meta.rawWidth && meta.rawHeight && (meta.rawWidth !== meta.width || meta.rawHeight !== meta.height);
2363
+ if (!/width\/?height/i.test(msg) || !alt) throw e;
2364
+ try {
2365
+ reg = await api('POST', '/image/v1/assets', regPayload(meta.rawWidth, meta.rawHeight));
2366
+ console.info('[infinite-canvas] 参考图登记改用文件原始尺寸成功:' + meta.rawWidth + '×' + meta.rawHeight);
2367
+ } catch (e2) {
2368
+ throw new Error(msg + ';改用文件原始尺寸 ' + meta.rawWidth + '×' + meta.rawHeight + ' 重试仍失败:' + ((e2 && e2.message) || e2));
2369
+ }
2370
+ }
2308
2371
  const aid = reg && reg.reference && reg.reference.asset_id;
2309
2372
  if (!aid) throw new Error('参考图登记失败:响应中没有 reference.asset_id');
2310
2373
  if (ref.assetId) compAssetIdCache.set(ref.assetId, aid);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ninkch/dsh-infinite-canvas",
3
- "version": "0.18.3",
3
+ "version": "0.18.4",
4
4
  "description": "Node-based short-drama studio for DeepSeek Harness: script → storyboard → assets → shots → voiced, subtitled final cut on an infinite canvas.",
5
5
  "keywords": [
6
6
  "dsh",