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.
- package/README.md +15 -14
- package/core.json +5 -5
- package/host/document-store.js +181 -0
- package/host/live-documents.js +524 -0
- package/host/native/file-worker.exe +0 -0
- package/host/native/file-worker.exe.build.json +10 -0
- package/host/native-worker-lease.js +127 -0
- package/host/native-worker-path.ps1 +63 -0
- package/host/package.json +1 -0
- package/host/safe-file.js +137 -0
- package/host/text-codec.js +67 -0
- package/host.json +50 -0
- package/lib/client.js +929 -216
- package/lib/index.js +1506 -1120
- package/package.json +5 -3
- package/renderer-cm6/cm6/app.js +193 -151
- package/renderer-cm6/dsh-bridge.js +216 -5
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
// dsh-bridge.js — MarkPilot CM6 renderer 在 DSH 右侧栏 iframe
|
|
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
|
-
//
|
|
14
|
-
//
|
|
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
|
// 首帧:若上一次宿主主题已知,直接上屏(避免深色宿主闪白/浅色宿主闪黑)。
|