dsh-markdown-typora 0.3.5 → 0.3.11

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.
@@ -1,18 +1,19 @@
1
- // dsh-bridge.js — MarkPilot CM6 renderer 在 DSH 右侧栏 iframe 里的宿主桥(协议 v1)。
1
+ // dsh-bridge.js — MarkPilot CM6 renderer 在 DSH 右侧栏 iframe 里的宿主桥(文档协议 v2,兼容 v1 外壳消息)。
2
2
  //
3
3
  // 契约:
4
4
  // 上行 iframe → 父窗口:{ __dshmd:true, dir:'up', type, content }
5
5
  // 下行 父窗口 → iframe:{ __dshmd:true, dir:'down', type, content }
6
6
  // 上行类型:bridge-ready | init | change | text | insert-to-chat | host-invoke | openLink | manual-save
7
+ // | live-exchange | live-leave | live-flush-result | live-error | live-status
7
8
  // 下行类型:open | setValue | requestText | theme | toast | host-result
9
+ // | live-bind | live-unbind | live-flush | live-refresh | live-exchange-result
8
10
  //
9
11
  // 为什么桥必须早于 cm6/app.js 加载:引擎在脚本尾部同步调用 window.handler.on("open"…)/
10
12
  // .on("setValue"…)/.on("requestText"…) 注册下行处理,并在同一尾巴里 emit("init")。桥晚到
11
13
  // 会让 open 落在没有监听者的窗口里(实测坑),所以 index.html 把本文件放在 app.js 之前。
12
14
  //
13
- // 单一权威:本桥**不缓存文档/选区/历史**,只做传输与皮肤级主题/提示;open/setValue/
14
- // requestText 原样转交引擎既有 handler。change/text 由引擎自己的 post() 直接发给父窗口,
15
- // 本桥不重复转发(否则父窗口收到两份脏标)。
15
+ // 单一权威:CM6 持有正文、选区与历史。本桥以原子应用回执确认会话版本,
16
+ // 转发连续增量和带请求身份的快照;仅保留最近一次应用请求用于幂等,不另计编辑版本。
16
17
  //
17
18
  // 皮肤级事件(theme/toast)不走 window.handler 的事件表(那是引擎的单监听表,后注册会
18
19
  // 覆盖先注册),改用 window 上的 CustomEvent:dshmd:theme / dshmd:toast。
@@ -279,11 +280,217 @@
279
280
  /** 最近一次 open 的链路自证(不缓存文档,只记"探测成没成、耗时、当时的 base")。 */
280
281
  var openTrace = null;
281
282
 
283
+ var boundDocToken = null, lastApply = null, applyFlight = null;
284
+ var lastRequest = null, maxApplySerial = 0;
285
+ function sessionNow() { return window.mpEngine && window.mpEngine.documentSession(); }
286
+ function sameSession(a,b) { return !!a && !!b && a.epoch===b.epoch && a.sourceRevision===b.sourceRevision; }
287
+ function applicationResult(request,applied,reason) {
288
+ return {protocol:2,docToken:request.docToken,applyId:request.applyId,applied:applied,reason:reason||null,session:sessionNow()||null,composing:!!window.mpEngine?.isComposing?.()};
289
+ }
290
+ async function applyDocument(type,request) {
291
+ if (!request.docToken || !request.applyId || typeof request.content!=='string' || !Number.isSafeInteger(request.serial)) return;
292
+ var previous=lastRequest?.applyId===request.applyId?lastRequest:null;
293
+ if(previous){
294
+ if(previous.docToken===request.docToken && previous.content===request.content && previous.kind===request.kind && JSON.stringify(previous.expected)===JSON.stringify(request.expected)) send('apply-result',previous.result);
295
+ else send('apply-result',applicationResult(request,false,'id-reused'));
296
+ return;
297
+ }
298
+ if(applyFlight){
299
+ if(applyFlight.applyId===request.applyId) return;
300
+ send('apply-result',applicationResult(request,false,'busy'));return;
301
+ }
302
+ if(boundDocToken && boundDocToken!==request.docToken){send('apply-result',applicationResult(request,false,'owner'));return;}
303
+ if(request.serial<=maxApplySerial){send('apply-result',applicationResult(request,false,'old-serial'));return;}
304
+ maxApplySerial=request.serial;
305
+ applyFlight=request;
306
+ var result;
307
+ try{
308
+ if(type==='open'){
309
+ if(boundDocToken || window.mpEngine || request.expected!==null) throw Error('already-open');
310
+ var ready=await resolveImageRoute();
311
+ if(applyFlight!==request)throw Error('superseded');
312
+ applyDocBase(ready?(request.rootPath||''):'');
313
+ var list=handlers.open||[];
314
+ if(!list.length)throw Error('not-ready');
315
+ for(var i=0;i<list.length;i++)list[i]({content:request.content,rootPath:request.rootPath});
316
+ if(!window.mpEngine || !sessionNow())throw Error('boot-failed');
317
+ boundDocToken=request.docToken;
318
+ queueMicrotask(maybeApplyPendingTheme);
319
+ }else{
320
+ if(!boundDocToken || !sameSession(request.expected,sessionNow()))throw Error('stale');
321
+ var ticket=window.mpEngine.commands.capture({flush:false});
322
+ if(!ticket)throw Error('composing');
323
+ if(!window.mpEngine.setValue(request.content,ticket))throw Error('rejected');
324
+ }
325
+ result=applicationResult(request,true,null);
326
+ lastApply={applyId:request.applyId,session:result.session,kind:request.kind};
327
+ handshakeSatisfied=true;stopWatchdog();
328
+ }catch(error){result=applicationResult(request,false,String(error?.message||error));}
329
+ finally{applyFlight=null;}
330
+ lastRequest={applyId:request.applyId,docToken:request.docToken,content:request.content,kind:request.kind,expected:request.expected,result:result};
331
+ // Keep only the last successful/current request payload reference. Failed
332
+ // requests cannot later be retried under the same id after user input.
333
+ send('apply-result',result);
334
+ }
335
+ function sendSnapshot(request){
336
+ if(request.docToken!==boundDocToken || !request.id || !window.mpEngine)return;
337
+ send('text',{protocol:2,docToken:boundDocToken,id:request.id,session:sessionNow(),lastApply:lastApply,
338
+ composing:!!window.mpEngine.isComposing?.(),text:window.mpEngine.getValue()});
339
+ }
340
+ function publishUpdate(update,beforeSession,composing){
341
+ if(!boundDocToken || applyFlight || !update.docChanged)return false;
342
+ var changes=[];
343
+ update.changes.iterChanges(function(from,to,_a,_b,inserted){changes.push({from:from,to:to,insert:inserted.toString()});});
344
+ send('document-delta',{protocol:2,docToken:boundDocToken,before:beforeSession,after:sessionNow(),
345
+ beforeLength:update.startState.doc.length,afterLength:update.state.doc.length,changes:changes,composing:!!composing,lastApply:lastApply});
346
+ return true;
347
+ }
348
+ function publishComposition(composing){
349
+ if(boundDocToken)send('composition',{protocol:2,docToken:boundDocToken,session:sessionNow(),composing:!!composing});
350
+ }
351
+
352
+ /* ── live 会话中继(协议 v2;引擎 facade mpEngine.liveDocument 的传输桥)────
353
+ * 桥不做 OT、不建第二套协同状态机:排序/变基/撤销隔离全部由引擎 facade
354
+ * (desktop 房间实现,Transaction.remote + addToHistory:false 非历史项)承担。
355
+ * 桥的职责只有三件:
356
+ * ① join 的会话票由父窗口 open(live:true) 回执供给(live-bind 下行),
357
+ * transport.join() 直接兑现,不再发第二次 open;
358
+ * ② exchange/leave 经 postMessage 上行、由父窗口 POST /api/live 应答,
359
+ * 应答归一为 facade 期望的形状({epoch,version,...} 或 {ok:false,error:String,code});
360
+ * ③ 引擎 facade 缺席(旧引擎构建)时上报 MP_LIVE_FACADE_MISSING,
361
+ * 父窗口回落单窗口安全模式 —— 绝不静默假装已同步。 */
362
+ var liveBound = null; // { docToken, nextId, pending:Map<id,resolve> }
363
+ function liveTicketOf(content) {
364
+ // 房间票:epoch 是 LiveDocuments rooms.epoch 的 UUID 字符串;version 是整数。
365
+ // 引擎 documentSession 的数字 epoch 是另一条契约(sameSession/expected),互不混用。
366
+ var t = content && content.live;
367
+ return t && typeof t === 'object' && typeof t.client === 'string' && t.client
368
+ && typeof t.epoch === 'string' && t.epoch
369
+ && Number.isSafeInteger(t.version) ? t : null;
370
+ }
371
+ function liveNormalize(response) {
372
+ if (!response || typeof response !== 'object')
373
+ return { ok: false, error: '共享编辑服务没有响应', code: 'MP_LIVE_TRANSPORT' };
374
+ if (response.ok === false) {
375
+ var error = response.error, code = response.code;
376
+ if (error && typeof error === 'object') { code = code || error.code; error = error.message; }
377
+ return { ok: false, error: String(error || '共享编辑服务错误'), code: String(code || 'MP_LIVE_TRANSPORT') };
378
+ }
379
+ if (response.ok === true) {
380
+ // API.md §2 ExchangeResult:成功形状必须带 ok:true;宿主 {ok,value} 信封在此归一。
381
+ var inner = response.value;
382
+ if (!inner || typeof inner !== 'object') return { ok: false, error: '空的共享编辑回执', code: 'MP_LIVE_TRANSPORT' };
383
+ return inner.ok === true || inner.ok === false ? inner : Object.assign({ ok: true }, inner);
384
+ }
385
+ return response;
386
+ }
387
+ function liveSettlePending(slot, code) {
388
+ slot.pending.forEach(function (done) { done({ ok: false, error: 'live 会话已解绑', code: code }); });
389
+ slot.pending.clear();
390
+ }
391
+ function liveExchangeRelay(client, request) {
392
+ var slot = liveBound;
393
+ if (!slot) return Promise.resolve({ ok: false, error: 'live 会话已解绑', code: 'MP_LIVE_GONE' });
394
+ var id = 'dshlive-' + (++slot.nextId);
395
+ return new Promise(function (resolve) {
396
+ slot.pending.set(id, resolve);
397
+ send('live-exchange', { protocol: 2, docToken: slot.docToken, id: id, client: client, request: request });
398
+ });
399
+ }
400
+ function handleLiveControl(type, content) {
401
+ var token = content && content.docToken;
402
+ if (type === 'live-bind') {
403
+ if (!boundDocToken || token !== boundDocToken) {
404
+ send('live-error', { protocol: 2, docToken: token, code: 'MP_LIVE_OWNER', error: 'live 绑定不属于当前文档' });
405
+ return;
406
+ }
407
+ var facade = window.mpEngine && window.mpEngine.liveDocument;
408
+ if (!facade || typeof facade.bind !== 'function') {
409
+ send('live-error', { protocol: 2, docToken: token, code: 'MP_LIVE_FACADE_MISSING', error: '引擎未提供 liveDocument facade' });
410
+ return;
411
+ }
412
+ if (liveBound) {
413
+ if (liveBound.docToken === token) return; // 幂等:同一文档重复 bind 只认第一次
414
+ var stale = liveBound; liveBound = null; liveSettlePending(stale, 'MP_LIVE_GONE');
415
+ }
416
+ var ticket = liveTicketOf(content);
417
+ var text = content && typeof content.text === 'string' ? content.text : '';
418
+ // 空文件(text === '')是合法正文:只拒票不完整/正文不是字符串的情形。
419
+ if (!ticket || !content || typeof content.text !== 'string') {
420
+ send('live-error', { protocol: 2, docToken: token, code: 'MP_LIVE_JOIN', error: 'live 会话票不完整' });
421
+ return;
422
+ }
423
+ liveBound = { docToken: token, nextId: 0, pending: new Map() };
424
+ var transport = {
425
+ // API.md §2 JoinResult:成功形状必须带 ok:true(facade 对 join 结果按信封判定)。
426
+ join: function () {
427
+ return Promise.resolve({
428
+ ok: true,
429
+ client: ticket.client, epoch: ticket.epoch, version: ticket.version, text: text,
430
+ diskHash: ticket.diskHash,
431
+ savedVersion: Number.isSafeInteger(ticket.savedVersion) ? ticket.savedVersion : ticket.version,
432
+ });
433
+ },
434
+ exchange: liveExchangeRelay,
435
+ leave: function (client) {
436
+ send('live-leave', { protocol: 2, docToken: liveBound ? liveBound.docToken : token, client: client });
437
+ return Promise.resolve();
438
+ },
439
+ onError: function (message, code) {
440
+ send('live-error', { protocol: 2, docToken: token, code: String(code || 'MP_LIVE_CONFLICT'), error: String(message != null ? message : '共享编辑冲突') });
441
+ },
442
+ onStatus: function (status) { send('live-status', { protocol: 2, docToken: token, status: status }); },
443
+ };
444
+ try { Promise.resolve(facade.bind(text, transport)).catch(function () { /* 错误已由 onError 上报 */ }); }
445
+ catch (error) {
446
+ liveBound = null;
447
+ send('live-error', { protocol: 2, docToken: token, code: 'MP_LIVE_JOIN', error: String(error && error.message || error) });
448
+ }
449
+ return;
450
+ }
451
+ if (type === 'live-unbind') {
452
+ if (liveBound && (!token || liveBound.docToken === token)) {
453
+ var slot = liveBound, unbind = window.mpEngine && window.mpEngine.liveDocument;
454
+ liveBound = null;
455
+ liveSettlePending(slot, 'MP_LIVE_GONE');
456
+ try { if (unbind && typeof unbind.unbind === 'function') unbind.unbind(); } catch (error) { /* 解绑失败不阻塞 */ }
457
+ }
458
+ return;
459
+ }
460
+ if (!liveBound || token !== liveBound.docToken) return;
461
+ if (type === 'live-exchange-result') {
462
+ var done = liveBound.pending.get(content.id);
463
+ if (done) { liveBound.pending.delete(content.id); done(liveNormalize(content.response)); }
464
+ return;
465
+ }
466
+ if (type === 'live-flush') {
467
+ var flusher = window.mpEngine && window.mpEngine.liveDocument;
468
+ if (!flusher || typeof flusher.flush !== 'function') {
469
+ send('live-flush-result', { protocol: 2, docToken: token, id: content.id, ok: false, code: 'MP_LIVE_FACADE_MISSING', error: '引擎未提供 liveDocument facade' });
470
+ return;
471
+ }
472
+ Promise.resolve().then(function () { return flusher.flush(); }).then(function (result) {
473
+ send('live-flush-result', { protocol: 2, docToken: token, id: content.id, ok: true, result: result });
474
+ }, function (error) {
475
+ send('live-flush-result', { protocol: 2, docToken: token, id: content.id, ok: false, code: String(error && error.code || 'MP_LIVE_FLUSH'), error: String(error && error.message || error) });
476
+ });
477
+ return;
478
+ }
479
+ if (type === 'live-refresh') {
480
+ var refresher = window.mpEngine && window.mpEngine.liveDocument;
481
+ try { if (refresher && typeof refresher.refresh === 'function') void Promise.resolve(refresher.refresh()).catch(function () {}); } catch (error) { /* 刷新失败由下一轮 exchange 兜底 */ }
482
+ return;
483
+ }
484
+ }
485
+
282
486
  window.addEventListener('message', function (event) {
283
487
  var data = event.data;
284
- if (!data || data.__dshmd !== true || data.dir !== 'down') return;
488
+ if (event.source !== window.parent || !data || data.__dshmd !== true || data.dir !== 'down') return;
285
489
  var type = data.type;
286
490
  var content = data.content;
491
+ if (content && content.protocol === 2 && typeof type === 'string' && type.indexOf('live-') === 0) { handleLiveControl(type, content); return; }
492
+ if (content && content.protocol === 2 && (type === "open" || type === "setValue")) { void applyDocument(type,content); return; }
493
+ if (content && content.protocol === 2 && type === "requestText") { sendSnapshot(content); return; }
287
494
 
288
495
  if (type === 'host-result') {
289
496
  if (content && hostPending.has(content.id)) {
@@ -384,6 +591,9 @@
384
591
  // 诊断/取证位(只读快照;dsh-selection 与 Electron 验收都从这里读)。
385
592
  window.__dshBridge = {
386
593
  send: send,
594
+ publishUpdate: publishUpdate,
595
+ composition: publishComposition,
596
+ protocolState: function(){return {docToken:boundDocToken,lastApply:lastApply,applying:!!applyFlight};},
387
597
  applyHostTheme: applyHostTheme,
388
598
  toast: showToast,
389
599
  theme: function () { return { dark: themeDark, pending: pendingThemeApply }; },
@@ -393,6 +603,7 @@
393
603
  imageRoute: function () { return IMG_ROUTE; },
394
604
  routeReady: function () { return imageRouteReady; },
395
605
  openTrace: function () { return openTrace; },
606
+ liveState: function () { return { bound: !!liveBound, docToken: liveBound && liveBound.docToken || null, pending: liveBound ? liveBound.pending.size : 0 }; },
396
607
  };
397
608
 
398
609
  // 首帧:若上一次宿主主题已知,直接上屏(避免深色宿主闪白/浅色宿主闪黑)。