@lancc/cmark 1.1.0 → 1.2.0

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.
@@ -42,8 +42,7 @@ module.exports = function installGitPanel(win, WS) {
42
42
  --zgp-hunk:#ddf4ff; --zgp-hunk-fg:#0969da;
43
43
  --zgp-shadow:0 8px 32px rgba(31,35,40,.14),0 2px 8px rgba(31,35,40,.08);
44
44
  }
45
- @media (prefers-color-scheme:dark){
46
- :root{
45
+ :root[data-theme='dark']{
47
46
  --zgp-bg:#161b22; --zgp-bg2:#1c2128; --zgp-bg3:#22272e;
48
47
  --zgp-fg:#e6edf3; --zgp-fg2:#9aa4b2; --zgp-fg3:#6e7681;
49
48
  --zgp-bd:#30363d; --zgp-bd2:#3d444d;
@@ -53,7 +52,6 @@ module.exports = function installGitPanel(win, WS) {
53
52
  --zgp-del:#2d1214; --zgp-del-fg:#f85149;
54
53
  --zgp-hunk:#0c2d4a; --zgp-hunk-fg:#58a6ff;
55
54
  --zgp-shadow:0 8px 32px rgba(0,0,0,.6),0 2px 8px rgba(0,0,0,.4);
56
- }
57
55
  }
58
56
  #zgp-btn{display:inline-flex;align-items:center;gap:5px;font-size:12px;font-weight:500;padding:3px 10px;
59
57
  border:1px solid var(--zgp-bd2);border-radius:6px;background:var(--zgp-bg);color:var(--zgp-fg2);
@@ -128,7 +126,7 @@ module.exports = function installGitPanel(win, WS) {
128
126
  .zgp-st.D{background:var(--zgp-del);color:var(--zgp-del-fg);}
129
127
  .zgp-st.Q{background:var(--zgp-bg3);color:var(--zgp-fg3);}
130
128
  .zgp-st.U{background:var(--zgp-del);color:var(--zgp-del-fg);}
131
- @media (prefers-color-scheme:dark){.zgp-st.M{background:#3a2d00;color:#d29922;}}
129
+ :root[data-theme='dark'] .zgp-st.M{background:#3a2d00;color:#d29922;}
132
130
  .zgp-fp{flex:1;min-width:0;font-size:12.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
133
131
  font-family:Consolas,Menlo,monospace;}
134
132
  .zgp-fp span{unicode-bidi:normal;}
@@ -219,7 +217,8 @@ module.exports = function installGitPanel(win, WS) {
219
217
  .zgp-jumpbar button{padding:3px 10px;font-size:11.5px;border-radius:6px;cursor:pointer;
220
218
  border:1px solid var(--zgp-bd2);background:var(--zgp-bg);color:var(--zgp-fg2);}
221
219
  .zgp-jumpbar button:hover{background:var(--zgp-acc-bg);border-color:var(--zgp-acc);color:var(--zgp-acc);}
222
- .zgp-dl.zgp-flash{outline:2px solid var(--zgp-acc);outline-offset:-2px;}
220
+ .zgp-dl.zgp-flash{outline:none !important;border:none !important;background:rgba(9,105,218,.16) !important;}
221
+ :root[data-theme='dark'] .zgp-dl.zgp-flash{background:rgba(88,166,255,.30) !important;}
223
222
 
224
223
  .zgp-cf-ov{position:absolute;inset:0;background:rgba(0,0,0,.38);display:flex;align-items:center;
225
224
  justify-content:center;z-index:20;padding:20px;}
@@ -458,6 +457,38 @@ module.exports = function installGitPanel(win, WS) {
458
457
  })
459
458
  }
460
459
 
460
+ /** 找到元素最近的可滚动祖先(弹窗是 vbody 自己滚,内嵌是面板 body 滚) */
461
+ function scrollHostOf (el) {
462
+ var n = el
463
+ while (n && n !== doc.body) {
464
+ var cs = win.getComputedStyle(n)
465
+ if (/(auto|scroll)/.test(cs.overflowY) && n.scrollHeight > n.clientHeight + 4) return n
466
+ n = n.parentElement
467
+ }
468
+ return el
469
+ }
470
+
471
+ /**
472
+ * diff 打开后默认落到第一处改动:把第一行 add/del 滚到视口约 1/3 高度并闪一下,
473
+ * 免得长文件每次都要自己往下翻。
474
+ */
475
+ function focusFirstChange (body) {
476
+ if (!body) return
477
+ var first = body.querySelector('.zgp-dl.add, .zgp-dl.del')
478
+ if (!first) return
479
+ var host = scrollHostOf(body)
480
+ try {
481
+ var hr = host.getBoundingClientRect()
482
+ var er = first.getBoundingClientRect()
483
+ var delta = er.top - hr.top
484
+ host.scrollTop = Math.max(0, host.scrollTop + delta - Math.round(host.clientHeight / 3))
485
+ } catch (e) {
486
+ try { first.scrollIntoView({ block: 'center' }) } catch (e2) {}
487
+ }
488
+ first.classList.add('zgp-flash')
489
+ win.setTimeout(function () { first.classList.remove('zgp-flash') }, 1200)
490
+ }
491
+
461
492
  // diff 渲染:带行号
462
493
  function renderDiffLines (lines) {
463
494
  let oldNo = 0, newNo = 0
@@ -512,7 +543,12 @@ module.exports = function installGitPanel(win, WS) {
512
543
  body.innerHTML = '<span class="zgp-dl">' + esc(content || '(空)') + '</span>'
513
544
  }
514
545
  view.classList.add('on')
515
- body.scrollTop = 0
546
+ if (isDiff) {
547
+ // 必须先让弹窗可见再算位置,隐藏时 rect 全是 0
548
+ focusFirstChange(body)
549
+ } else {
550
+ body.scrollTop = 0
551
+ }
516
552
  }
517
553
 
518
554
  // ============ 当前笔记 ============
@@ -574,7 +610,7 @@ module.exports = function installGitPanel(win, WS) {
574
610
  if (!ok) return
575
611
  const r = await G.abortInProgress()
576
612
  toast(r.ok ? ('已中止 ' + st.inProgress) : ('中止失败:' + r.message), r.ok ? 'ok' : 'err')
577
- refresh()
613
+ refreshAfterAction()
578
614
  }
579
615
  wrap.appendChild(b)
580
616
  }
@@ -585,7 +621,7 @@ module.exports = function installGitPanel(win, WS) {
585
621
  b2.onclick = async () => {
586
622
  const r = await G.clearStaleLock()
587
623
  toast(r.ok ? '已清理陈旧 index.lock' : ('清理失败:' + r.message), r.ok ? 'ok' : 'err')
588
- refresh()
624
+ refreshAfterAction()
589
625
  }
590
626
  wrap.appendChild(b2)
591
627
  }
@@ -720,7 +756,7 @@ module.exports = function installGitPanel(win, WS) {
720
756
  if (r.ok) {
721
757
  toast('已推送:' + (r.data.steps.join(' · ') || '无改动'), 'ok')
722
758
  logCache = null
723
- refresh()
759
+ refreshAfterAction()
724
760
  } else toast('推送失败:' + hintOf(r), 'err')
725
761
  }
726
762
  }
@@ -732,7 +768,7 @@ module.exports = function installGitPanel(win, WS) {
732
768
  toast('已还原到最近一次提交', 'ok')
733
769
  logCache = null
734
770
  try { if (win.__znotesReloadNote) win.__znotesReloadNote(rel) } catch (e) {}
735
- refresh()
771
+ refreshAfterAction()
736
772
  } else if (rr.kind !== 'cancelled') {
737
773
  toast('还原失败:' + hintOf(rr), 'err')
738
774
  }
@@ -758,6 +794,8 @@ module.exports = function installGitPanel(win, WS) {
758
794
  const lines = stripDiffHeader(diff)
759
795
  body.innerHTML = renderDiffLines(lines)
760
796
  mountJumpBar(body)
797
+ // 首次渲染才跳;后续轮询刷新(内容没变)不打断用户当前阅读位置
798
+ if (!existedBody) focusFirstChange(body)
761
799
  }
762
800
 
763
801
  function renderChanges(st) {
@@ -806,7 +844,7 @@ module.exports = function installGitPanel(win, WS) {
806
844
  // 编辑器内部可能残留旧内容并触发自动保存,把还原结果又写回磁盘;
807
845
  // 整页刷新是唯一可靠的做法(还原属低频操作)
808
846
  try { if (win.__znotesReloadNote) win.__znotesReloadNote(f.path) } catch (err) {}
809
- refresh()
847
+ refreshAfterAction()
810
848
  } else if (r.kind !== 'cancelled') {
811
849
  toast('还原失败:' + hintOf(r), 'err')
812
850
  }
@@ -938,7 +976,7 @@ module.exports = function installGitPanel(win, WS) {
938
976
  + '<button class="zgp-ic" data-cmpprev="' + i + '" title="与提交前比对:这一次提交本身对这篇笔记做了什么改动">' + ICON.cmpPrev + '</button>'
939
977
  + '<button class="zgp-ic" data-view="' + i + '" title="查看该版本:显示这次提交时这篇笔记的完整内容">' + ICON.eye + '</button>'
940
978
  + '<span style="flex:1"></span>'
941
- + '<button class="zgp-ic danger" data-back="' + i + '" title="回溯到此版本:把这篇笔记恢复成这个版本的内容(改动前自动备份)">' + ICON.undo + '</button>'
979
+ + '<button class="zgp-ic danger" data-back="' + i + '" title="回溯到此版本:把这篇笔记恢复成这个版本的内容">' + ICON.undo + '</button>'
942
980
  + '</div>'
943
981
  : ''
944
982
  return '<div class="zgp-cm" data-c="' + i + '">'
@@ -987,7 +1025,7 @@ module.exports = function installGitPanel(win, WS) {
987
1025
  toast('已回溯到 ' + c.short, 'ok')
988
1026
  logCache = null
989
1027
  try { if (win.__znotesReloadNote) win.__znotesReloadNote(onlyFile) } catch (e) {}
990
- refresh()
1028
+ refreshAfterAction()
991
1029
  } else if (r.kind !== 'cancelled') {
992
1030
  toast('回溯失败:' + hintOf(r), 'err')
993
1031
  }
@@ -1019,7 +1057,7 @@ module.exports = function installGitPanel(win, WS) {
1019
1057
  + '<span class="zgp-fs ' + (f.status || '').charAt(0) + '">' + (f.status || '').charAt(0) + '</span>'
1020
1058
  + '<span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + esc(f.path) + '">' + esc(f.path) + '</span>'
1021
1059
  + '<button class="zgp-ic" title="查看该次改动">' + ICON.diff + '</button>'
1022
- + '<button class="zgp-ic danger" data-restore="1" title="回溯到该版本(先备份)">' + ICON.undo + '</button>'
1060
+ + '<button class="zgp-ic danger" data-restore="1" title="回溯到该版本">' + ICON.undo + '</button>'
1023
1061
  + '</div>'
1024
1062
  }).join('')
1025
1063
  }
@@ -1042,7 +1080,7 @@ module.exports = function installGitPanel(win, WS) {
1042
1080
  toast('已回溯到 ' + hash.slice(0, 7), 'ok')
1043
1081
  logCache = null
1044
1082
  try { if (win.__znotesReloadNote) win.__znotesReloadNote(p) } catch (err) {}
1045
- refresh()
1083
+ refreshAfterAction()
1046
1084
  } else if (r3.kind !== 'cancelled') {
1047
1085
  toast('回溯失败:' + hintOf(r3), 'err')
1048
1086
  }
@@ -1101,6 +1139,23 @@ module.exports = function installGitPanel(win, WS) {
1101
1139
  }
1102
1140
  }
1103
1141
 
1142
+ /**
1143
+ * 操作(提交/推送/拉取/还原/中止)完成后的刷新。
1144
+ *
1145
+ * 不能只调 refresh():① 它带签名去重,读到和上次一样的中间状态会直接返回;
1146
+ * ② git 命令返回时工作区/索引可能还没完全落稳,第一次读到的往往还是旧状态。
1147
+ * 所以先强制刷一次,再补几次重试,同时把历史页和顶部分支条一起拉新。
1148
+ */
1149
+ function refreshAfterAction() {
1150
+ try { refresh(true) } catch (e) {}
1151
+ ;[120, 400, 1000, 2200].forEach(function (ms) {
1152
+ setTimeout(function () { try { refresh(true) } catch (e) {} }, ms)
1153
+ })
1154
+ try { logCache = null } catch (e) {}
1155
+ try { if (activeTab === 'history') renderHistory(true) } catch (e) {}
1156
+ try { if (win.__zbbRefresh) win.__zbbRefresh() } catch (e) {}
1157
+ }
1158
+
1104
1159
  // ============ 事件 ============
1105
1160
  let activeTab = 'changes'
1106
1161
  let panelMode = 'global' // global:全部改动/历史;file:只看当前笔记
@@ -1212,7 +1267,7 @@ module.exports = function installGitPanel(win, WS) {
1212
1267
  } else {
1213
1268
  toast('同步失败:' + hintOf(r), 'err')
1214
1269
  }
1215
- refresh()
1270
+ refreshAfterAction()
1216
1271
  }
1217
1272
 
1218
1273
  const pushBtn = $('zgp-push')
@@ -1221,7 +1276,7 @@ module.exports = function installGitPanel(win, WS) {
1221
1276
  const r = await G.push()
1222
1277
  busyUI(false)
1223
1278
  toast(r.ok ? '已推送' : ('推送失败:' + hintOf(r)), r.ok ? 'ok' : 'err')
1224
- refresh()
1279
+ refreshAfterAction()
1225
1280
  }
1226
1281
 
1227
1282
  const pullBtn = $('zgp-pull')
@@ -1231,7 +1286,7 @@ module.exports = function installGitPanel(win, WS) {
1231
1286
  busyUI(false)
1232
1287
  toast(r.ok ? '已拉取' : ('拉取失败:' + hintOf(r)), r.ok ? 'ok' : 'err')
1233
1288
  logCache = null
1234
- refresh()
1289
+ refreshAfterAction()
1235
1290
  }
1236
1291
  }
1237
1292
  bindStatusButtons()
@@ -20,6 +20,7 @@ const fs = require('fs')
20
20
  const path = require('path')
21
21
  const os = require('os')
22
22
  const crypto = require('crypto')
23
+ const wsPrefs = require('./ws-prefs.js')
23
24
 
24
25
  const DEFAULTS = {
25
26
  autoSync: false,
@@ -30,7 +31,8 @@ const DEFAULTS = {
30
31
  pullBeforePush: true,
31
32
  pushEnabled: true,
32
33
  commitScope: 'notes',
33
- commitMessage: 'notes: 同步 {{datetime}}',
34
+ // 提交信息固定,不开放给用户配置(少一个配置项,也避免有人把模板改坏导致历史不可读)
35
+ commitMessage: 'cmark sync:{{datetime}}',
34
36
  lastRunAt: 0,
35
37
  failCount: 0
36
38
  }
@@ -241,10 +243,42 @@ module.exports = function installGitService(win, argvPath) {
241
243
  // git 会弹出「选择凭据助手」的 GUI 窗口,在插件里就是卡死。
242
244
  const BASE_ARGS = ['-c', 'core.quotepath=false', '-c', 'credential.helper=', '-c', 'credential.helper=manager']
243
245
 
246
+ // ---- 远端凭据(一键建仓用)----
247
+ // token 存在 ~/.cmark-config.json 的 remoteToken 里,网络命令用内联 credential helper
248
+ // 从环境变量读取:不写进 .git/config,也不出现在 git 的命令行参数里。
249
+ const NET_CFG_FILE = path.join(os.homedir(), '.cmark-config.json')
250
+ let netCfgCache = { t: -1, data: null }
251
+ function readNetCfg () {
252
+ try {
253
+ const st = fs.statSync(NET_CFG_FILE)
254
+ if (netCfgCache.data && netCfgCache.t === st.mtimeMs) return netCfgCache.data
255
+ const d = JSON.parse(fs.readFileSync(NET_CFG_FILE, 'utf8'))
256
+ netCfgCache = { t: st.mtimeMs, data: d }
257
+ return d
258
+ } catch (e) { return null }
259
+ }
260
+ const NET_CMDS = { push: 1, pull: 1, fetch: 1, 'ls-remote': 1, clone: 1 }
261
+ function netToken (args) {
262
+ if (!args.some(function (a) { return NET_CMDS[a] })) return null
263
+ // 先取本工作区的凭据,再回退到旧版全局配置(迁移兼容)
264
+ try {
265
+ const p = wsPrefs.read(WS)
266
+ if (p.remoteToken && p.remoteToken.token) return p.remoteToken.token
267
+ } catch (e) {}
268
+ const c = readNetCfg()
269
+ const t = c && c.remoteToken
270
+ return t && t.token ? t.token : null
271
+ }
272
+ const TOKEN_HELPER = '!f() { echo username=x-access-token; echo password=$CMARK_GIT_TOKEN; }; f'
273
+
244
274
  function exec(args, opt) {
245
275
  const o = opt || {}
276
+ const token = o.token || netToken(args)
277
+ const pre = token
278
+ ? ['-c', 'core.quotepath=false', '-c', 'credential.helper=', '-c', 'credential.helper=' + TOKEN_HELPER]
279
+ : BASE_ARGS
246
280
  return new Promise((resolve) => {
247
- execFile('git', BASE_ARGS.concat(args), {
281
+ execFile('git', pre.concat(args), {
248
282
  cwd: repoRoot || WS,
249
283
  encoding: o.buffer ? 'buffer' : 'utf8',
250
284
  maxBuffer: 32 * 1024 * 1024,
@@ -254,7 +288,8 @@ module.exports = function installGitService(win, argvPath) {
254
288
  GCM_INTERACTIVE: 'never',
255
289
  GIT_TERMINAL_PROMPT: '0',
256
290
  GIT_LITERAL_PATHSPECS: '1',
257
- LC_ALL: 'C'
291
+ LC_ALL: 'C',
292
+ CMARK_GIT_TOKEN: token || ''
258
293
  })
259
294
  }, (err, stdout, stderr) => {
260
295
  if (err) {
@@ -688,7 +723,8 @@ module.exports = function installGitService(win, argvPath) {
688
723
  const names = staged.data.split('\0').filter(Boolean)
689
724
  if (!names.length) return { ok: false, kind: 'nothing', message: ERR_TEXT.nothing }
690
725
 
691
- const msg = formatMessage(message || cfg.commitMessage, { count: names.length })
726
+ // 固定使用内置模板:cmark sync:YYYY-MM-DD HH:mm:ss
727
+ const msg = formatMessage(DEFAULTS.commitMessage, { count: names.length })
692
728
  const r = await runGit(['commit', '-m', msg], { label: 'commit', timeout: 90000 })
693
729
  if (!r.ok) return r
694
730
  return { ok: true, data: { message: msg, files: names.length } }
@@ -1280,6 +1316,29 @@ module.exports = function installGitService(win, argvPath) {
1280
1316
  }
1281
1317
  }
1282
1318
 
1319
+ // ---- 手填远程地址的支持 ----
1320
+ // 远程地址可能是需要授权的私有仓库(GitHub/Gitee/自建 GitLab 等)。
1321
+ // 这里给两个能力:先探测(看能不能免授权访问),再用显式 token 探测/绑定。
1322
+ async function probeRemote (url, token) {
1323
+ const u = String(url || '').trim()
1324
+ if (!u) return { ok: false, kind: 'badrev', message: '请填写远程仓库地址' }
1325
+ if (!/^(https?:\/\/|git@|ssh:\/\/|file:\/\/|[A-Za-z]:[\\/]|\\\\|\/)/.test(u)) {
1326
+ return { ok: false, kind: 'badrev', message: '远程地址格式不正确:' + u }
1327
+ }
1328
+ const r = await runGit(['ls-remote', '--heads', u], { label: 'probe', timeout: 60000, token: token || undefined })
1329
+ if (r.ok) return { ok: true, data: { hasRefs: !!String(r.data || '').trim() } }
1330
+ return { ok: false, kind: r.kind, message: r.message }
1331
+ }
1332
+
1333
+ /** 保存手填地址用的访问令牌(按工作区存,记录地址用于展示) */
1334
+ function setRemoteToken (token, url) {
1335
+ const t = String(token || '').trim()
1336
+ if (!t) return { ok: false, message: '令牌为空' }
1337
+ const res = wsPrefs.patch(WS, { remoteToken: { provider: 'custom', token: t, url: String(url || '').trim() } })
1338
+ if (res && res.ok === false) return res
1339
+ return { ok: true }
1340
+ }
1341
+
1283
1342
  async function setRemote(url) {
1284
1343
  const pre = precheck()
1285
1344
  if (!pre.ok) return pre
@@ -1357,6 +1416,7 @@ module.exports = function installGitService(win, argvPath) {
1357
1416
  stage, unstage, discard, restoreTo,
1358
1417
  commitAll, push, pull, sync,
1359
1418
  remoteInfo: getRemoteInfo, setRemote,
1419
+ probeRemote, setRemoteToken,
1360
1420
  identity: getIdentity, setIdentity,
1361
1421
  abortInProgress,
1362
1422
  clearStaleLock,
@@ -0,0 +1,130 @@
1
+ /**
2
+ * 宿主适配:uTools / ZTools / 浏览器
3
+ *
4
+ * 两个宿主的 API 名字一致,但注入的全局对象不同:
5
+ * uTools 注入 window.utools
6
+ * ZTools 注入 window.ztools
7
+ *
8
+ * 两个坑必须一起处理:
9
+ * 1) preload 可能在宿主注入全局对象「之前」执行(uTools 尤其如此),
10
+ * 这时候直接读 window.utools 是 undefined,若把 undefined 当结论,
11
+ * 后面所有 window.ztools.xxx 都会炸,preload 直接中断 —— 表现就是
12
+ * 设置面板、Git 面板全都打不开。
13
+ * 2) 两个宿主的 API 并非完全同集,缺某个方法时不能让调用方 TypeError。
14
+ *
15
+ * 所以这里不写死别名,而是装一个惰性代理:读取时才去 window.utools /
16
+ * window.ztools 找真实对象,找不到就返回 no-op 并记一笔,等宿主注入后
17
+ * 同一个代理立刻恢复正常。
18
+ */
19
+
20
+ const RAW_U = (typeof window !== 'undefined' && window.utools) || null
21
+ const RAW_Z = (typeof window !== 'undefined' && window.ztools) || null
22
+ const PROXY_TAG = '__cmark_host_proxy__'
23
+
24
+ /** 真实的宿主对象(排除我们自己的代理) */
25
+ function target() {
26
+ try {
27
+ const u = window.utools
28
+ const z = window.ztools
29
+ if (u && !u[PROXY_TAG]) return u
30
+ if (z && !z[PROXY_TAG]) return z
31
+ return null
32
+ } catch (e) {
33
+ return null
34
+ }
35
+ }
36
+
37
+ function makeProxy() {
38
+ const missing = new Set()
39
+ const p = new Proxy(
40
+ {},
41
+ {
42
+ get(_, prop) {
43
+ if (prop === PROXY_TAG) return true
44
+ const t = target() || RAW_U || RAW_Z
45
+ if (!t) {
46
+ // 宿主还没注入:函数类先给 no-op,避免 preload 在早期就崩掉
47
+ return function () { return undefined }
48
+ }
49
+ const v = t[prop]
50
+ if (typeof v === 'function') return v.bind(t)
51
+ if (v === undefined) {
52
+ missing.add(String(prop))
53
+ return function () { return undefined }
54
+ }
55
+ return v
56
+ },
57
+ has(_, prop) {
58
+ const t = target() || RAW_U || RAW_Z
59
+ return !!t && prop in t
60
+ },
61
+ }
62
+ )
63
+ try {
64
+ Object.defineProperty(p, '__missing', { value: missing, enumerable: false })
65
+ } catch (e) {}
66
+ return p
67
+ }
68
+
69
+ let proxy = null
70
+
71
+ /**
72
+ * 装 shim:缺哪个补哪个(只在缺失时补,不覆盖宿主原对象)。
73
+ * 返回值就是当前识别到的宿主名。
74
+ */
75
+ function applyHostShim() {
76
+ try {
77
+ if (typeof window === 'undefined') return 'node'
78
+ if (!window.utools && !window.ztools) {
79
+ proxy = makeProxy()
80
+ window.ztools = proxy
81
+ window.utools = proxy
82
+ } else {
83
+ if (!window.utools) window.utools = window.ztools
84
+ if (!window.ztools) window.ztools = window.utools
85
+ }
86
+ // 每次读取时重新判断,宿主晚注入也能正确反映
87
+ try {
88
+ Object.defineProperty(window, '__CMARK_HOST', {
89
+ configurable: true,
90
+ get() { return detect() },
91
+ })
92
+ } catch (e) {
93
+ window.__CMARK_HOST = detect()
94
+ }
95
+ return detect()
96
+ } catch (e) {
97
+ return 'browser'
98
+ }
99
+ }
100
+
101
+ function detect() {
102
+ // 关键:shim 之后 window.utools 与 window.ztools 是同一个对象,
103
+ // 不能拿「是谁」判断,必须用 shim 之前的快照 RAW_U / RAW_Z。
104
+ if (RAW_U && !RAW_Z) return 'utools'
105
+ if (RAW_Z && !RAW_U) return 'ztools'
106
+ // 宿主晚注入的情形:看实时注入的对象里哪个是真实的(非我们的代理)
107
+ const u = window.utools
108
+ const z = window.ztools
109
+ const realU = u && !u[PROXY_TAG]
110
+ const realZ = z && !z[PROXY_TAG]
111
+ if (realU && !realZ) return 'utools'
112
+ if (realZ && !realU) return 'ztools'
113
+ return 'browser'
114
+ }
115
+
116
+ function hostApi() {
117
+ return target() || RAW_U || RAW_Z || null
118
+ }
119
+
120
+ function isInHost() {
121
+ return !!hostApi()
122
+ }
123
+
124
+ /** 哪些方法宿主没提供过(用于排查,不抛错) */
125
+ function missingMethods() {
126
+ if (proxy && proxy.__missing) return Array.from(proxy.__missing)
127
+ return []
128
+ }
129
+
130
+ module.exports = { detect, applyHostShim, hostApi, isInHost, missingMethods, target }